UNPKG

js-dataflash-parser

Version:

A JavaScript parser for ArduPilot dataflash logs

919 lines (853 loc) 32.2 kB
/* eslint-disable no-unused-vars */ /* eslint-disable no-var */ /* eslint-disable no-prototype-builtins */ const MAV_TYPE_GENERIC = 0 // Generic micro air vehicle. const MAV_TYPE_FIXED_WING = 1 // Fixed wing aircraft. const MAV_TYPE_QUADROTOR = 2 // Quadrotor const MAV_TYPE_COAXIAL = 3 // Coaxial helicopter const MAV_TYPE_HELICOPTER = 4 // Normal helicopter with tail rotor. const MAV_TYPE_ANTENNA_TRACKER = 5 // Ground installation const MAV_TYPE_GCS = 6 // Operator control unit / ground control station const MAV_TYPE_AIRSHIP = 7 // Airship, controlled const MAV_TYPE_FREE_BALLOON = 8 // Free balloon, uncontrolled const MAV_TYPE_ROCKET = 9 // Rocket const MAV_TYPE_GROUND_ROVER = 10 // Ground rover const MAV_TYPE_SURFACE_BOAT = 11 // Surface vessel, boat, ship const MAV_TYPE_SUBMARINE = 12 // Submarine const MAV_TYPE_HEXAROTOR = 13 // Hexarotor const MAV_TYPE_OCTOROTOR = 14 // Octorotor const MAV_TYPE_TRICOPTER = 15 // Tricopter const MAV_TYPE_FLAPPING_WING = 16 // Flapping wing const MAV_TYPE_KITE = 17 // Kite const MAV_TYPE_ONBOARD_CONTROLLER = 18 // Onboard companion controller const MAV_TYPE_VTOL_DUOROTOR = 19 // Two-rotor VTOL using control surfaces in vertical operation in addition. Tailsitter const MAV_TYPE_VTOL_QUADROTOR = 20 // Quad-rotor VTOL using a V-shaped quad config in vertical operation. // Tailsitter. const MAV_TYPE_VTOL_TILTROTOR = 21 // Tiltrotor VTOL const MAV_TYPE_VTOL_RESERVED2 = 22 // VTOL reserved 2 const MAV_TYPE_VTOL_RESERVED3 = 23 // VTOL reserved 3 const MAV_TYPE_VTOL_RESERVED4 = 24 // VTOL reserved 4 const MAV_TYPE_VTOL_RESERVED5 = 25 // VTOL reserved 5 const MAV_TYPE_GIMBAL = 26 // Onboard gimbal const MAV_TYPE_ADSB = 27 // Onboard ADSB peripheral const MAV_TYPE_PARAFOIL = 28 // Steerable, nonrigid airfoil const MAV_TYPE_DODECAROTOR = 29 // Dodecarotor const MAV_TYPE_CAMERA = 30 // Camera const MAV_TYPE_CHARGING_STATION = 31 // Charging station const MAV_TYPE_FLARM = 32 // Onboard FLARM collision avoidance system const MAV_TYPE_ENUM_END = 33 // const modeMappingApm = { 0: 'MANUAL', 1: 'CIRCLE', 2: 'STABILIZE', 3: 'TRAINING', 4: 'ACRO', 5: 'FBWA', 6: 'FBWB', 7: 'CRUISE', 8: 'AUTOTUNE', 10: 'AUTO', 11: 'RTL', 12: 'LOITER', 13: 'TAKEOFF', 14: 'AVOID_ADSB', 15: 'GUIDED', 16: 'INITIALISING', 17: 'QSTABILIZE', 18: 'QHOVER', 19: 'QLOITER', 20: 'QLAND', 21: 'QRTL', 22: 'QAUTOTUNE', 23: 'QACRO', 24: 'THERMAL' } const modeMappingAcm = { 0: 'STABILIZE', 1: 'ACRO', 2: 'ALT_HOLD', 3: 'AUTO', 4: 'GUIDED', 5: 'LOITER', 6: 'RTL', 7: 'CIRCLE', 9: 'LAND', 11: 'DRIFT', 13: 'SPORT', 14: 'FLIP', 15: 'AUTOTUNE', 16: 'POSHOLD', 17: 'BRAKE', 18: 'THROW', 19: 'AVOID_ADSB', 20: 'GUIDED_NOGPS', 21: 'SMART_RTL', 22: 'FLOWHOLD', 23: 'FOLLOW', 24: 'ZIGZAG', 25: 'SYSTEMID', 26: 'AUTOROTATE' } const modeMappingRover = { 0: 'MANUAL', 1: 'ACRO', 3: 'STEERING', 4: 'HOLD', 5: 'LOITER', 6: 'FOLLOW', 7: 'SIMPLE', 10: 'AUTO', 11: 'RTL', 12: 'SMART_RTL', 15: 'GUIDED', 16: 'INITIALISING' } const modeMappingTracker = { 0: 'MANUAL', 1: 'STOP', 2: 'SCAN', 3: 'SERVO_TEST', 10: 'AUTO', 16: 'INITIALISING' } const modeMappingSub = { 0: 'STABILIZE', 1: 'ACRO', 2: 'ALT_HOLD', 3: 'AUTO', 4: 'GUIDED', 7: 'CIRCLE', 9: 'SURFACE', 16: 'POSHOLD', 19: 'MANUAL', 20: 'MOTOR_DETECT' } const multipliers = { '-': 0, // no multiplier e.g. a string '?': 1, // multipliers which haven't been worked out yet.... // <leave a gap here, just in case....> 2: 1e2, 1: 1e1, 0: 1e0, A: 1e-1, B: 1e-2, C: 1e-3, D: 1e-4, E: 1e-5, F: 1e-6, G: 1e-7, // <leave a gap here, just in case....> '!': 3.6, // (ampere*second => milliampere*hour) and (km/h => m/s) '/': 3600 // (ampere*second => ampere*hour) } const multipliersTable = { 0.000001: 'n', 1000: 'M', 0.001: 'm' } const HEAD1 = 163 const HEAD2 = 149 const units = { '-': '', // no units e.g. Pi, or a string '?': 'UNKNOWN', // Units which haven't been worked out yet.... A: 'A', // Ampere d: '°', // of the angular variety, -180 to 180 b: 'B', // bytes k: '°/s', // degrees per second. Degrees are NOT SI, but is some situations more user-friendly than radians D: '°', // degrees of latitude e: '°/s/s', // degrees per second per second. Degrees are NOT SI, but is some situations more user-friendly E: 'rad/s', // radians per second G: 'Gauss', // Gauss is not an SI unit, but 1 tesla = 10000 gauss so a simple replacement is not possible here h: '°', // 0.? to 359.? i: 'A.s', // Ampere second J: 'W.s', // Joule (Watt second) // { 'l', "l" }, // litres L: 'rad/s/s', // radians per second per second m: 'm', // metres n: 'm/s', // metres per second // { 'N', "N" }, // Newton o: 'm/s/s', // metres per second per second O: '°C', // degrees Celsius. Not SI, but Kelvin is too cumbersome for most users '%': '%', // percent S: 'satellites', // number of satellites s: 's', // seconds q: 'rpm', // rounds per minute. Not SI, but sometimes more intuitive than Hertz r: 'rad', // radians U: '°', // degrees of longitude u: 'ppm', // pulses per minute v: 'V', // Volt P: 'Pa', // Pascal w: 'Ohm', // Ohm Y: 'us', // pulse width modulation in microseconds z: 'Hz', // Hertz '#': 'instance' // instance number for message } function getModeMap (mavType) { let map if ([MAV_TYPE_QUADROTOR, MAV_TYPE_HELICOPTER, MAV_TYPE_HEXAROTOR, MAV_TYPE_OCTOROTOR, MAV_TYPE_COAXIAL, MAV_TYPE_TRICOPTER].includes(mavType)) { map = modeMappingAcm } if (mavType === MAV_TYPE_FIXED_WING) { map = modeMappingApm } if (mavType === MAV_TYPE_GROUND_ROVER) { map = modeMappingRover } if (mavType === MAV_TYPE_ANTENNA_TRACKER) { map = modeMappingTracker } if (mavType === MAV_TYPE_SUBMARINE) { map = modeMappingSub } if (map == null) { return null } return map } function assignColumn (obj) { const ArrayOfString = obj.split(',') return ArrayOfString } // Converts from degrees to radians. Math.radians = function (degrees) { return degrees * Math.PI / 180 } // Converts from radians to degrees. Math.degrees = function (radians) { return radians * 180 / Math.PI } class DataflashParser { constructor () { this.time = null this.timebase = null this.buffer = null this.data = null this.FMT = [] this.FMT[128] = { Type: '128', length: '89', Name: 'FMT', Format: 'BBnNZ', Columns: 'Type,Length,Name,Format,Columns' } this.offset = 0 this.msgType = [] this.offsetArray = [] this.totalSize = null this.messages = {} this.lastPercentage = 0 this.sent = false this.maxPercentageInterval = 0.05 this.messageTypes = {} this.alreadyParsed = [] // Check if we're in a web worker environment this.isWebWorker = typeof self !== 'undefined' && typeof self.postMessage === 'function' } FORMAT_TO_STRUCT (obj) { try { let temp const dict = { name: obj.Name, fieldnames: obj.Columns.split(',') } const column = assignColumn(obj.Columns) let low let n for (let i = 0; i < obj.Format.length; i++) { temp = obj.Format.charAt(i) try { switch (temp) { case 'a': // int16_t[32] dict[column[i]] = [] for (let j = 0; j < 32; j++) { dict[column[i]][j] = this.data.getInt16(this.offset, true) this.offset += 2 } break case 'b': dict[column[i]] = this.data.getInt8(this.offset) this.offset += 1 break case 'B': dict[column[i]] = this.data.getUint8(this.offset) this.offset += 1 break case 'h': dict[column[i]] = this.data.getInt16(this.offset, true) this.offset += 2 break case 'H': dict[column[i]] = this.data.getUint16(this.offset, true) this.offset += 2 break case 'i': dict[column[i]] = this.data.getInt32(this.offset, true) this.offset += 4 break case 'I': dict[column[i]] = this.data.getUint32(this.offset, true) this.offset += 4 break case 'f': dict[column[i]] = this.data.getFloat32(this.offset, true) this.offset += 4 break case 'd': dict[column[i]] = this.data.getFloat64(this.offset, true) this.offset += 8 break case 'Q': low = this.data.getUint32(this.offset, true) this.offset += 4 n = this.data.getUint32(this.offset, true) * 4294967296.0 + low if (low < 0) n += 4294967296 dict[column[i]] = n this.offset += 4 break case 'q': low = this.data.getInt32(this.offset, true) this.offset += 4 n = this.data.getInt32(this.offset, true) * 4294967296.0 + low if (low < 0) n += 4294967296 dict[column[i]] = n this.offset += 4 break case 'n': // TODO: fix these regex and unsilent linter // eslint-disable-next-line dict[column[i]] = String.fromCharCode.apply(null, new Uint8Array(this.buffer, this.offset, 4)).replace(/\x00+$/g, '') this.offset += 4 break case 'N': // eslint-disable-next-line dict[column[i]] = String.fromCharCode.apply(null, new Uint8Array(this.buffer, this.offset, 16)).replace(/\x00+$/g, '') this.offset += 16 break case 'Z': // eslint-disable-next-line dict[column[i]] = String.fromCharCode.apply(null, new Uint8Array(this.buffer, this.offset, 64)).replace(/\x00+$/g, '') this.offset += 64 break case 'c': // this.this.data.setInt16(offset,true); dict[column[i]] = this.data.getInt16(this.offset, true) / 100 this.offset += 2 break case 'C': // this.data.setUint16(offset,true); dict[column[i]] = this.data.getUint16(this.offset, true) / 100 this.offset += 2 break case 'E': // this.data.setUint32(offset,true); dict[column[i]] = this.data.getUint32(this.offset, true) / 100 this.offset += 4 break case 'e': // this.data.setInt32(offset,true); dict[column[i]] = this.data.getInt32(this.offset, true) / 100 this.offset += 4 break case 'L': // this.data.setInt32(offset,true); dict[column[i]] = this.data.getInt32(this.offset, true) this.offset += 4 break case 'M': // this.data.setInt32(offset,true); dict[column[i]] = this.data.getUint8(this.offset) this.offset += 1 break } } catch (e) { console.warn(`Error parsing field ${i} of message type ${obj.Name}:`, e.message); // Set a default value based on the format type dict[column[i]] = temp.match(/[bB]/) ? 0 : (temp.match(/[fF]/) ? 0.0 : ''); } } return dict } catch (e) { console.error('Error in FORMAT_TO_STRUCT:', e.message); return null; } } gpstimetoTime (week, msec) { const epoch = 86400 * (10 * 365 + (1980 - 1969) / 4 + 1 + 6 - 2) return epoch + 86400 * 7 * week + msec * 0.001 - 15 } setTimeBase (base) { this.timebase = base } findTimeBase (gps) { const temp = this.gpstimetoTime(parseInt(gps.GWk), parseInt(gps.GMS)) this.setTimeBase(parseInt(temp - gps.TimeUS * 0.000001)) } getMsgType (element) { for (let i = 0; i < this.FMT.length; i++) { if (this.FMT[i] != null) { // eslint-disable-next-line if (this.FMT[i].Name == element) { return i } } } } onMessage (message) { if (this.totalSize == null) { // for percentage calculation this.totalSize = this.buffer.byteLength } if (message.name in this.messages) { this.messages[message.name].push(this.fixData(message)) } else { this.messages[message.name] = [this.fixData(message)] } const percentage = 100 * this.offset / this.totalSize if ((percentage - this.lastPercentage) > this.maxPercentageInterval) { self.postMessage({ percentage: percentage }) this.lastPercentage = percentage } } messageHasInstances (name) { const type = this.FMT.find(msg => msg !== undefined && msg.Name === name) return type !== undefined && type.units !== undefined && type.units.includes('instance') } getInstancesFieldName (name) { const type = this.FMT.find(msg => msg !== undefined && msg.Name === name) if (type.units === undefined) { return null } return type.Columns.split(',')[type.units.indexOf('instance')] } // Next three functions are used for transfering data on postmessage, instead of cloning isTypedArray (arr) { return ArrayBuffer.isView(arr) && !(arr instanceof DataView) } getType (arr) { return this.isTypedArray(arr) && arr.constructor.name } postData (data) { data.dataType = {} const transferables = [] for (const field of Object.keys(data.messageList)) { const arrayType = this.getType(data.messageList[field]) if (arrayType) { transferables.push(data.messageList[field].buffer) } // Apparently it is magically decoded on the other end, no need for metadata // data['dataType'][field] = arrayType } self.postMessage(data, transferables) } parseAtOffset (name) { if (!this.FMT) return; const type = this.getMsgType(name) if (type === undefined) return; const parsed = [] for (let i = 0; i < this.msgType.length; i++) { if (type === this.msgType[i]) { this.offset = this.offsetArray[i] try { const msg = this.FORMAT_TO_STRUCT(this.FMT[type]) if (msg) { parsed.push(msg) } } catch (e) { console.error('Error parsing message:', e) } } } if (parsed.length > 0) { this.messages[name] = parsed } } checkNumberOfInstances (name) { // Similar to parseOffset, but finishes earlier and updates messageTypes const type = this.getMsgType(name) const availableInstances = [] const instanceField = this.getInstancesFieldName(name) if (instanceField === null) { return [1] } let repeats = 0 for (let i = 0; i < this.msgType.length; i++) { if (type === this.msgType[i]) { this.offset = this.offsetArray[i] try { const temp = this.FORMAT_TO_STRUCT(this.FMT[this.msgType[i]]) if (temp.name != null) { const msg = temp if (!msg.hasOwnProperty(instanceField)) { break } // we do an early return after we get 20 repeated instance numbers. should we? const instance = msg[instanceField] if (availableInstances.includes(instance)) { repeats += 1 if (repeats > 20) { return availableInstances } } else { availableInstances.push(instance) } } } catch (e) { console.log(e) } } } return availableInstances } timestamp (TimeUs) { const temp = this.timebase + TimeUs * 0.000001 if (temp > 0) { TimeUs = temp } let date = new Date(TimeUs * 1000) const hours = date.getHours() const minutes = '0' + date.getMinutes() const seconds = '0' + date.getSeconds() const formattedTime = hours + ':' + minutes.substr(-2) + ':' + seconds.substr(-2) date = date.toString() const time = date.split(' ') if (time[0] !== 'Invalid') { this.time = time[0] + ' ' + time[1] + ' ' + time[2] + ' ' + time[3] } return formattedTime } DfReader () { let lastOffset = 0 while (this.offset < (this.buffer.byteLength - 3)) { if (this.data.getUint8(this.offset) !== HEAD1 || this.data.getUint8(this.offset + 1) !== HEAD2) { this.offset += 1 continue } this.offset += 2 const attribute = this.data.getUint8(this.offset) if (this.FMT[attribute] != null) { this.offset += 1 this.offsetArray.push(this.offset) this.msgType.push(attribute) try { var value = this.FORMAT_TO_STRUCT(this.FMT[attribute]) if (this.FMT[attribute].Name === 'GPS') { this.findTimeBase(value) } } catch (e) { // console.log('reached log end?') // console.log(e) this.offset += 1 } if (attribute === 128) { this.FMT[value.Type] = { Type: value.Type, length: value.Length, Name: value.Name, Format: value.Format, Columns: value.Columns } } // this.onMessage(value) } else { this.offset += 1 } if (this.offset - lastOffset > 50000) { const perc = 100 * this.offset / this.buffer.byteLength if (this.isWebWorker && typeof self !== 'undefined') { self.postMessage({ action: 'progress', payload: { percentage: perc } }) } lastOffset = this.offset } } if (this.isWebWorker && typeof self !== 'undefined') { self.postMessage({ action: 'progress', payload: { percentage: 100 } }) self.postMessage({ action: 'messages', payload: { messages: this.messages } }) } this.sent = true } getModeString (cmode) { if (typeof cmode !== 'number') { return 'UNKNOWN'; } let mavtype = MAV_TYPE_QUADROTOR; // Default to quadcopter if (this.messages.MSG) { for (const msg of this.messages.MSG) { if (msg && msg.Message) { const msgLower = msg.Message.toLowerCase(); if (msgLower.includes('arduplane')) { mavtype = MAV_TYPE_FIXED_WING; break; } else if (msgLower.includes('arducopter')) { mavtype = MAV_TYPE_QUADROTOR; break; } else if (msgLower.includes('ardusub')) { mavtype = MAV_TYPE_SUBMARINE; break; } else if (msgLower.includes('rover')) { mavtype = MAV_TYPE_GROUND_ROVER; break; } else if (msgLower.includes('tracker')) { mavtype = MAV_TYPE_ANTENNA_TRACKER; break; } } } } const modeMap = getModeMap(mavtype); return modeMap ? (modeMap[cmode] || 'UNKNOWN') : 'UNKNOWN'; } fixData (message) { // Add TimeUS to time_boot_ms conversion for all messages if (message.TimeUS !== undefined) { message.time_boot_ms = message.TimeUS / 1000; } if (message.name === 'MODE') { message.asText = this.getModeString(message.Mode); } else if (message.name === 'GPS') { // Ensure GPS fields are properly named if (message.Lat !== undefined) message.Lat = message.Lat / 1e7; // Convert to degrees if (message.Lng !== undefined) message.Lng = message.Lng / 1e7; // Convert to degrees if (message.Alt !== undefined) message.Alt = message.Alt / 100; // Convert to meters } return message; } fixDataOnce (name) { if (!['GPS', 'ATT', 'AHR2', 'MODE'].includes(name)) { if (this.messageTypes.hasOwnProperty(name)) { const fields = this.messages[name][0].fieldnames if (this.messageTypes[name].hasOwnProperty('multipliers')) { for (const message in this.messages[name]) { for (let i = 1; i < fields.length; i++) { const fieldname = fields[i] if (!isNaN(this.messageTypes[name].multipliers[i])) { this.messages[name][message][fieldname] *= this.messageTypes[name].multipliers[i] } } } } } } } concatTypedArrays (a, b) { // a, b TypedArray of same type const c = new (a.constructor)(a.length + b.length) c.set(a, 0) c.set(b, a.length) return c } createUint8ArrayFromString (str) { const array = new Uint8Array(str.length) for (let i = 0, strLen = str.length; i < strLen; i++) { array[i] = str.charCodeAt(i) } return array } processFiles () { this.files = {}; if (!this.messages.FILE || !Array.isArray(this.messages.FILE)) { return; } for (const msg of this.messages.FILE) { if (!msg || !msg.FileName || !msg.Data) { continue; } if (!this.files.hasOwnProperty(msg.FileName)) { this.files[msg.FileName] = this.createUint8ArrayFromString(msg.Data); } else { const newArray = new Uint8Array(this.files[msg.FileName].length + msg.Data.length); newArray.set(this.files[msg.FileName]); newArray.set(this.createUint8ArrayFromString(msg.Data), this.files[msg.FileName].length); this.files[msg.FileName] = newArray; } } } simplifyData (name) { if (name === 'MODE') { this.messageTypes[name].expressions.push('asText') } if (name === 'FILE') { return } if (!['FMTU'].includes(name)) { if (this.messageTypes.hasOwnProperty(name)) { const fields = this.messageTypes[name].expressions if (!fields.includes('time_boot_ms')) { fields.push('time_boot_ms') } const mergedData = {} for (const field of fields) { mergedData[field] = [] } for (const message of this.messages[name]) { for (let i = 0; i < fields.length; i++) { const fieldname = fields[i] mergedData[fieldname].push(message[fieldname]) } } delete this.messages[name] this.messages[name] = mergedData for (const field of this.messageTypes[name].expressions) { if (this.messages[name][field] && !isNaN(this.messages[name][field][0])) { const newData = new Float64Array(this.messages[name][field]) delete this.messages[name][field] this.messages[name][field] = newData } } } } } populateUnits () { if (!this.messages.FMTU || !Array.isArray(this.messages.FMTU)) { return; } for (const msg of this.messages.FMTU) { if (!msg || !msg.FmtType || !this.FMT[msg.FmtType]) { continue; } this.FMT[msg.FmtType].units = []; if (Array.isArray(msg.UnitIds)) { for (const unit of msg.UnitIds) { this.FMT[msg.FmtType].units.push(units[unit] || '?'); } } this.FMT[msg.FmtType].multipliers = []; if (Array.isArray(msg.MultIds)) { for (const multiplier of msg.MultIds) { this.FMT[msg.FmtType].multipliers.push(multiplier || 1); } } } } extractStartTime () { const msgs = this.messages.GPS if (msgs === undefined) { return } for (const i in msgs.time_boot_ms) { if (msgs.GWk[i] > 1000) { // lousy validation const weeks = msgs.GWk[i] const ms = msgs.GMS[i] let d = new Date((315964800.0 + ((60 * 60 * 24 * 7) * weeks) + ms / 1000.0) * 1000.0) // adjusting for leap seconds d = new Date(d.getTime() - this.leapSecondsGPS(d.getUTCFullYear(), d.getUTCMonth() + 1) * 1000) return d } } } leapSecondsGPS (year, month) { return this.leapSecondsTAI(year, month) - 19 } leapSecondsTAI (year, month) { const yyyymm = year * 100 + month if (yyyymm >= 201701) return 37 if (yyyymm >= 201507) return 36 if (yyyymm >= 201207) return 35 if (yyyymm >= 200901) return 34 if (yyyymm >= 200601) return 33 if (yyyymm >= 199901) return 32 if (yyyymm >= 199707) return 31 if (yyyymm >= 199601) return 30 return 0 } processData (data, msgs) { if (!data || data.byteLength === 0) { return { messageTypes: [], messages: [] } } try { this.buffer = data this.data = new DataView(this.buffer) this.DfReader() const messageTypes = [] // First parse FMT messages to get message definitions this.parseAtOffset('FMT') this.parseAtOffset('FMTU') this.populateUnits() // Collect all message types from FMT messages for (const msg of this.FMT) { if (msg && msg.Name) { messageTypes.push(msg.Name) } } // Process MSG messages first to determine vehicle type this.parseAtOffset('MSG') // Default messages if msgs is null or undefined const messagesToProcess = msgs || messageTypes; // Process each message type for (const msg of messagesToProcess) { if (msg !== 'MSG') { // Skip MSG as we've already processed it this.parseAtOffset(msg) } if (msg === 'FILE') { this.processFiles() } } // Convert messages object to array format and fix data const messagesArray = [] for (const [type, msgs] of Object.entries(this.messages)) { if (Array.isArray(msgs)) { msgs.forEach(msg => { if (msg) { const fixedMsg = this.fixData({...msg, name: type}); if (fixedMsg.time_boot_ms === undefined && fixedMsg.TimeUS !== undefined) { fixedMsg.time_boot_ms = fixedMsg.TimeUS / 1000; } messagesArray.push(fixedMsg); } }) } } // Sort messages by timestamp messagesArray.sort((a, b) => { const timeA = a.time_boot_ms || 0; const timeB = b.time_boot_ms || 0; return timeA - timeB; }); return { messageTypes: messageTypes, messages: messagesArray } } catch (e) { console.error('Error processing data:', e); return { messageTypes: [], messages: [], error: e.message } } } loadType (type) { if (!this.messages[type]) { return []; } return Array.isArray(this.messages[type]) ? this.messages[type] : []; } } // Setup web worker event listener only if in a web worker environment if (typeof self !== 'undefined' && typeof self.postMessage === 'function') { self.addEventListener('message', function (event) { let parser if (event.data === null) { console.log('got bad file message!') } else if (event.data.action === 'parse') { parser = new DataflashParser() const data = event.data.file parser.processData(data) } else if (event.data.action === 'loadType') { parser.loadType(event.data.type.split('[')[0]) } else if (event.data.action === 'trimFile') { parser.trimFile(event.data.time) } }) } // Export for Node.js environment if (typeof module !== 'undefined' && module.exports) { module.exports = { DataflashParser }; }