diff --git a/src/libs/actions/data-lake.ts b/src/libs/actions/data-lake.ts index 404b335b2..ed0c0cbe1 100644 --- a/src/libs/actions/data-lake.ts +++ b/src/libs/actions/data-lake.ts @@ -67,7 +67,6 @@ export const setDataLakeVariableData = ( notifyDataLakeVariableListeners(id) } - export const deleteDataLakeVariable = (id: string): void => { delete dataLakeVariableInfo[id] delete dataLakeVariableData[id] diff --git a/src/libs/vehicle/ardupilot/ardupilot.ts b/src/libs/vehicle/ardupilot/ardupilot.ts index 2476bc2e4..3f7cfbfad 100644 --- a/src/libs/vehicle/ardupilot/ardupilot.ts +++ b/src/libs/vehicle/ardupilot/ardupilot.ts @@ -46,6 +46,7 @@ import type { MetadataFile } from '@/types/ardupilot-metadata' import { type MissionLoadingCallback, type Waypoint, defaultLoadingCallback } from '@/types/mission' import * as Vehicle from '../vehicle' +import { flattenData } from './data-flattener' import { defaultMessageFrequency } from './defaults' // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -295,9 +296,20 @@ export abstract class ArduPilotVehicle extends Vehicle.AbstractVehicle setDataLakeVariableData(path, value)) + const messageType = mavlink_message.message.type + + // Special handling for NAMED_VALUE_FLOAT messages + if (messageType === 'NAMED_VALUE_FLOAT') { + const name = (mavlink_message.message.name as string[]).join('').replace(/\0/g, '') + setDataLakeVariableData(`${messageType}/${name}`, mavlink_message.message.value) + return + } + + // For all other messages, use the flattener + const flattened = flattenData(mavlink_message.message) + flattened.forEach(({ path, value }) => { + setDataLakeVariableData(path, value) + }) // Update our internal messages this._messages.set(mavlink_message.message.type, { ...mavlink_message.message, epoch: new Date().getTime() }) diff --git a/src/libs/vehicle/ardupilot/data-flattener.ts b/src/libs/vehicle/ardupilot/data-flattener.ts new file mode 100644 index 000000000..755ace42d --- /dev/null +++ b/src/libs/vehicle/ardupilot/data-flattener.ts @@ -0,0 +1,104 @@ +/** + * Result of flattening a data structure + */ +export type FlattenedPair = { + /** + * Full path to the data, including message type and field name + * e.g. "ATTITUDE/roll" or "NAMED_VALUE_FLOAT/GpsHDOP" + */ + path: string + /** + * The actual value of the field after flattening + * - Primitive values are kept as-is + * - String arrays are joined + * - Number arrays create multiple entries + */ + value: string | number | boolean + /** + * The type of the flattened value + * Used to create the appropriate DataLakeVariable + */ + type: 'string' | 'number' | 'boolean' +} + +/** + * Type guard to check if a value is an array of numbers + * @param {unknown[]} data The data to check + * @returns {data is number[]} True if the array contains numbers + */ +function isNumberArray(data: unknown[]): data is number[] { + return typeof data[0] === 'number' +} + +/** + * Type guard to check if a value is an array of strings + * @param {unknown[]} data The data to check + * @returns {data is string[]} True if the array contains strings + */ +function isStringArray(data: unknown[]): data is string[] { + return typeof data[0] === 'string' +} + +/** + * Flattens complex data structures into simple types that can be stored in the data lake + * @param {Record} data The data to flatten + * @returns {FlattenedPair[]} Array of flattened path/value pairs + */ +export function flattenData(data: Record): FlattenedPair[] { + if (!('type' in data)) return [] + const messageName = data.type as string + + // Special handling for NAMED_VALUE_FLOAT messages + if (messageName === 'NAMED_VALUE_FLOAT') { + const name = (data.name as string[]).join('').replace(/\0/g, '') + return [ + { + path: `${messageName}/${name}`, + type: 'number', + value: data.value as number, + }, + ...Object.entries(data) + .filter(([key]) => !['name', 'value', 'type'].includes(key)) + .map(([key, value]) => ({ + path: `${messageName}/${key}`, + type: typeof value as 'string' | 'number' | 'boolean', + value: value as string | number | boolean, + })), + ] + } + + // For all other messages + return Object.entries(data) + .filter(([key]) => key !== 'type') + .flatMap(([key, value]) => { + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return [ + { + path: `${messageName}/${key}`, + type: typeof value as 'string' | 'number' | 'boolean', + value, + }, + ] + } + if (Array.isArray(value)) { + if (value.length === 0) return [] + if (isNumberArray(value)) { + return value.map((item, index) => ({ + path: `${messageName}/${key}/${index}`, + type: 'number', + value: item, + })) + } + if (isStringArray(value)) { + return [ + { + path: `${messageName}/${key}`, + type: 'string', + value: value.join(''), + }, + ] + } + } + return [] + }) +}