diff --git a/package-lock.json b/package-lock.json index de48887..d621bf2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "colormap": "^2.3.2", "crypto-browserify": "^3.12.0", "d3": "^7.6.1", + "dji-log-parser-js": "^0.5.4", "epic-spinners": "^1.1.0", "eslint-config-standard": "^16.0.3", "eslint-loader": "^4.0.2", @@ -11977,6 +11978,15 @@ "node": ">=8" } }, + "node_modules/dji-log-parser-js": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/dji-log-parser-js/-/dji-log-parser-js-0.5.4.tgz", + "integrity": "sha512-+v3Q7RQ0wsJpdO9ZtfzSVIvC9XBxGs8GL+wzGHKCejC6ydCQVlpNI6CuSvJgiDvgOgq+ES+CCSkoDKluDe2E2w==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", @@ -40595,6 +40605,11 @@ } } }, + "dji-log-parser-js": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/dji-log-parser-js/-/dji-log-parser-js-0.5.4.tgz", + "integrity": "sha512-+v3Q7RQ0wsJpdO9ZtfzSVIvC9XBxGs8GL+wzGHKCejC6ydCQVlpNI6CuSvJgiDvgOgq+ES+CCSkoDKluDe2E2w==" + }, "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", diff --git a/package.json b/package.json index 424dee8..6f21c4e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "colormap": "^2.3.2", "crypto-browserify": "^3.12.0", "d3": "^7.6.1", + "dji-log-parser-js": "^0.5.4", "epic-spinners": "^1.1.0", "eslint-config-standard": "^16.0.3", "eslint-loader": "^4.0.2", diff --git a/src/components/CesiumViewer.vue b/src/components/CesiumViewer.vue index 85add0b..b514beb 100644 --- a/src/components/CesiumViewer.vue +++ b/src/components/CesiumViewer.vue @@ -68,6 +68,7 @@ import tzlookup from 'tz-lookup' import { store } from './Globals.js' import { DataflashDataExtractor } from '../tools/dataflashDataExtractor' import { MavlinkDataExtractor } from '../tools/mavlinkDataExtractor' +import { djiDataExtractor } from '../tools/djiDataExtractor' import 'cesium/Build/Cesium/Widgets/widgets.css' import CesiumSettingsWidget from './widgets/CesiumSettingsWidget.vue' import ColorCoderMode from './cesiumExtra/colorCoderMode.js' @@ -1116,6 +1117,9 @@ export default { let dataExtractor = null if (this.state.logType === 'tlog') { dataExtractor = MavlinkDataExtractor + } else if (this.state.logType === 'dji') { + console.log('Using DJI extractor') + dataExtractor = djiDataExtractor } else { dataExtractor = DataflashDataExtractor } diff --git a/src/components/Home.vue b/src/components/Home.vue index b07405a..d14efe7 100644 --- a/src/components/Home.vue +++ b/src/components/Home.vue @@ -57,6 +57,7 @@ import { Color } from 'cesium' import colormap from 'colormap' import { DataflashDataExtractor } from '../tools/dataflashDataExtractor' import { MavlinkDataExtractor } from '../tools/mavlinkDataExtractor' +import { DjiDataExtractor } from '../tools/djiDataExtractor' import MagFitTool from '@/components/widgets/MagFitTool.vue' import EkfHelperTool from '@/components/widgets/EkfHelperTool.vue' import Vue from 'vue' @@ -85,6 +86,8 @@ export default { if (this.dataExtractor === null) { if (this.state.logType === 'tlog') { this.dataExtractor = MavlinkDataExtractor + } else if (this.state.logType === 'dji') { + this.dataExtractor = DjiDataExtractor } else { this.dataExtractor = DataflashDataExtractor } @@ -113,17 +116,19 @@ export default { this.state.vehicle = this.dataExtractor.extractVehicleType(this.state.messages) if (this.state.params === undefined) { this.state.params = this.dataExtractor.extractParams(this.state.messages) - this.state.defaultParams = this.dataExtractor.extractDefaultParams(this.state.messages) if (this.state.params !== undefined) { - this.$eventHub.$on('cesium-time-changed', (time) => { - this.state.params.seek(time) - }) + this.state.defaultParams = this.dataExtractor.extractDefaultParams(this.state.messages) + if (this.state.params !== undefined) { + this.$eventHub.$on('cesium-time-changed', (time) => { + this.state.params.seek(time) + }) + } } } if (this.state.vehicle === 'quadcopter') { - if (this.state.params.get('FRAME_TYPE') === 0) { + if (this.state.params?.get('FRAME_TYPE') === 0) { this.state.vehicle += '+' - } else if (this.state.params.get('FRAME_TYPE') === 1) { + } else { this.state.vehicle += 'x' } } diff --git a/src/components/Plotly.vue b/src/components/Plotly.vue index 564c79f..a9fb217 100644 --- a/src/components/Plotly.vue +++ b/src/components/Plotly.vue @@ -682,7 +682,11 @@ export default { try { x = this.state.messages.ATT.time_boot_ms } catch { - x = this.state.messages.ATTITUDE.time_boot_ms + try { + x = this.state.messages.ATTITUDE.time_boot_ms + } catch { + x = this.state.messages.osd.time_boot_ms + } } } // used to find the corresponding time indexes between messages diff --git a/src/components/SideBarFileManager.vue b/src/components/SideBarFileManager.vue index 742c236..88d8064 100644 --- a/src/components/SideBarFileManager.vue +++ b/src/components/SideBarFileManager.vue @@ -86,7 +86,12 @@ export default { this.transferMessage = 'Download Done' this.sampleLoaded = true - worker.postMessage({ action: 'parse', file: arrayBuffer, isTlog: (url.indexOf('.tlog') > 0) }) + worker.postMessage({ + action: 'parse', + file: arrayBuffer, + isTlog: (url.indexOf('.tlog') > 0), + isDji: (url.indexOf('.txt') > 0) + }) } oReq.addEventListener('progress', (e) => { if (e.lengthComputable) { @@ -142,11 +147,14 @@ export default { worker.postMessage({ action: 'parse', file: data, - isTlog: (file.name.endsWith('tlog')) + isTlog: (file.name.endsWith('tlog')), + isDji: (file.name.endsWith('txt')) }) } this.state.logType = file.name.endsWith('tlog') ? 'tlog' : 'bin' - + if (file.name.endsWith('.txt')) { + this.state.logType = 'dji' + } reader.readAsArrayBuffer(file) }, uploadFile () { @@ -215,7 +223,8 @@ export default { worker.postMessage({ action: 'parse', file: event.data.data, - isTlog: false + isTlog: false, + isDji: false }) } }) diff --git a/src/tools/parsers/JsDataflashParser b/src/tools/parsers/JsDataflashParser index 22cf03c..766c8e1 160000 --- a/src/tools/parsers/JsDataflashParser +++ b/src/tools/parsers/JsDataflashParser @@ -1 +1 @@ -Subproject commit 22cf03cfcb8a540baac9bc2049561bd140e43951 +Subproject commit 766c8e1ae39cd5f94d2da3bcd8c62c61c7d26e9e diff --git a/src/tools/parsers/djiParser.js b/src/tools/parsers/djiParser.js new file mode 100644 index 0000000..2760cde --- /dev/null +++ b/src/tools/parsers/djiParser.js @@ -0,0 +1,236 @@ +import { DJILog } from "dji-log-parser-js"; +const messageTypes = { + OSD: { + expressions: [ + "flyTime", + "latitude", + "longitude", + "height", + "heightMax", + "vpsHeight", + "altitude", + "xSpeed", + "xSpeedMax", + "ySpeed", + "ySpeedMax", + "zSpeed", + "zSpeedMax", + "pitch", + "roll", + "yaw", + "flycState", + "flycCommand", + "flightAction", + "isGpdUsed", + "nonGpsCause", + "gpsNum", + "gpsLevel", + "droneType", + "isSwaveWork", + "waveError", + "goHomeStatus", + "batteryType", + "isOnGround", + "isMotorOn", + "isMotorBlocked", + "motorStartFailedCause", + "isImuPreheated", + "imuInitFailReason", + "isAcceletorOverRange", + "isBarometerDeadInAir", + "isCompassError", + "isGoHomeHeightModified", + "canIocWork", + "isNotEnoughForce", + "isOutOfLimit", + "isPropellerCatapult", + "isVibrating", + "isVisionUsed", + "voltageWarning" + ] + }, + gimbal: { + expressions: [ + "mode", + "pitch", + "roll", + "yaw", + "isPitchAtLimit", + "isRollAtLimit", + "isYawAtLimit", + "isStuck" + ] + }, + CAMERA: { + expressions: [ + "isPhoto", + "isVideo", + "sdCardIsInserted", + "sdCardState" + ] + }, + RC: { + expressions: [ + "downlinkSignal", + "uplinkSignal", + "aileron", + "elevator", + "throttle", + "rudder" + ] + }, + BATTERY: { + expressions: [ + "chargeLevel", + "voltage", + "current", + "currentCapacity", + "fullCapacity", + "cellNum", + "isCellVoltageEstimated", + "cellVoltages", + "cellVoltageDeviation", + "maxCellVoltageDeviation", + "temperature", + "minTemperature", + "maxTemperature" + ] + }, + HOME: { + expressions: [ + "latitude", + "longitude", + "altitude", + "heightLimit", + "isHomeRecord", + "goHomeMode", + "isDynamicHomePointEnabled", + "isNearDistanceLimit", + "isNearHeightLimit", + "isCompassCalibrating", + "isMultipleModeEnabled", + "isBeginnerMode", + "isIocEnabled", + "goHomeHeight", + "maxAllowedHeight", + "currentFlightRecordIndex" + ] + }, + RECOVER: { + expressions: [ + "appPlatform", + "appVersion", + "aircraftName", + "aircraftSn", + "cameraSn", + "rcSn", + "batterySn" + ] + }, + APP: { + expressions: [ + "tip", + "warn" + ] + } +} + +for (const key of Object.keys(messageTypes)) { + messageTypes[key].complexFields = messageTypes[key].expressions.map(e => { + return { + name: e, + units: "?", + multiplier: 1 + } + }) +} +function transformData(dataArray, startTime) { + if (!dataArray || dataArray.length === 0) { + return {}; + } + + // Helper function to capitalize first letter + const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1).toUpperCase(); + + // Initialize the messages object + const messages = {}; + + // First pass: initialize the structure based on the first item + const firstItem = dataArray[0]; + Object.keys(firstItem).forEach(key => { + const capitalizedKey = capitalize(key); + messages[capitalizedKey] = { + time_boot_ms: [] + }; + + // Helper function to initialize arrays for nested objects + function initializeArraysForObject(obj, targetObj) { + Object.keys(obj).forEach(nestedKey => { + if (typeof obj[nestedKey] === "object" && !Array.isArray(obj[nestedKey])) { + targetObj[nestedKey] = {}; + initializeArraysForObject(obj[nestedKey], targetObj[nestedKey]); + } else { + targetObj[nestedKey] = []; + } + }); + } + + // Initialize arrays for the current section + if (firstItem[key] && typeof firstItem[key] === "object") { + initializeArraysForObject(firstItem[key], messages[capitalizedKey]); + } + }); + + // Second pass: populate the arrays + dataArray.forEach(item => { + const timestamp = new Date(item.custom.dateTime).getTime() - startTime; + Object.keys(item).forEach(key => { + const capitalizedKey = capitalize(key); + if (messages[capitalizedKey]) { + // Add timestamp to this section + messages[capitalizedKey].time_boot_ms.push(timestamp); + + // Helper function to populate arrays for nested objects + function populateArrays(sourceObj, targetObj) { + Object.keys(sourceObj).forEach(nestedKey => { + if (typeof sourceObj[nestedKey] === "object" && !Array.isArray(sourceObj[nestedKey])) { + populateArrays(sourceObj[nestedKey], targetObj[nestedKey]); + } else if (targetObj[nestedKey]) { // Check if the target array exists + targetObj[nestedKey].push(sourceObj[nestedKey]); + } + }); + } + + // Populate arrays for the current section + if (item[key] && typeof item[key] === "object") { + populateArrays(item[key], messages[capitalizedKey]); + } + } + }); + }); + console.log(messages) + return messages; +} + +class DjiParser { + + loadType() { + console.warn("DjiParser.loadType() is not implemented") + } + + async processData(data) { + const parser = new DJILog(new Uint8Array(data)); + const keychains = await parser.fetchKeychains( + "f05e96fa44f3f36eb9962948bac0f77", + "http://new.galvanicloop.com:5000/https://dev.dji.com/openapi/v1/flight-records/keychains" + ); + const frames = parser.frames(keychains) + const startTime = new Date(frames[0].custom.dateTime).getTime() + self.postMessage({ metadata: {startTime: new Date(frames[0].custom.dateTime).getTime()}}) + self.postMessage({ availableMessages: messageTypes }) + self.postMessage({ messages: transformData(frames, startTime) }) + self.postMessage({ messagesDoneLoading: true }) + + } +} +export default DjiParser \ No newline at end of file diff --git a/src/tools/parsers/parser.worker.js b/src/tools/parsers/parser.worker.js index 3550fc8..43365a8 100644 --- a/src/tools/parsers/parser.worker.js +++ b/src/tools/parsers/parser.worker.js @@ -2,9 +2,10 @@ // import MavlinkParser from 'mavlinkParser' const mavparser = require('./mavlinkParser') const DataflashParser = require('./JsDataflashParser/parser').default +const DjiParser = require('./djiParser').default let parser -self.addEventListener('message', function (event) { +self.addEventListener('message', async function (event) { if (event.data === null) { console.log('got bad file message!') } else if (event.data.action === 'parse') { @@ -12,6 +13,9 @@ self.addEventListener('message', function (event) { if (event.data.isTlog) { parser = new mavparser.MavlinkParser() parser.processData(data) + } else if (event.data.isDji) { + parser = new DjiParser() + await parser.processData(data) } else { parser = new DataflashParser(true) parser.processData(data, ['CMD', 'MSG', 'FILE', 'MODE', 'AHR2', 'ATT', 'GPS', 'POS',