diff --git a/src/libs/actions/data-lake.ts b/src/libs/actions/data-lake.ts index 3b60c3806..5e1409d6e 100644 --- a/src/libs/actions/data-lake.ts +++ b/src/libs/actions/data-lake.ts @@ -52,28 +52,21 @@ export const getDataLakeVariableData = (id: string): string | number | boolean | return dataLakeVariableData[id] } -export const setDataLakeVariableData = (id: string, data: object | string | number | boolean): void => { - const newData = data - if (data === null) { - return - } - if (dataLakeVariableData[id] === undefined) { - console.trace(`Cockpit action variable with id '${id}' does not exist. Creating it.`) - const type_of_variable = typeof data - if (type_of_variable === 'object') { - // TODO: support strings - } - if (type_of_variable !== 'string' && type_of_variable !== 'number') { - console.debug(`attempting to create a variable with type ${type_of_variable}. Skipping`) - return +export const setDataLakeVariableData = ( + id: string, + data: object | string | number | boolean | Array +): void => { + if (data === null) return + + // Handle already-flat primitive types first + if (typeof data === 'string' || typeof data === 'number') { + if (dataLakeVariableInfo[id] === undefined) { + createDataLakeVariable(new DataLakeVariable(id, id, typeof data === 'string' ? 'string' : 'number')) } - createDataLakeVariable(new DataLakeVariable(id, id, typeof data)) - } - if (newData === undefined || typeof newData === 'object') { + dataLakeVariableData[id] = data + notifyDataLakeVariableListeners(id) return } - dataLakeVariableData[id] = newData - notifyDataLakeVariableListeners(id) } export const deleteDataLakeVariable = (id: string): void => { diff --git a/src/libs/vehicle/ardupilot/ardupilot.ts b/src/libs/vehicle/ardupilot/ardupilot.ts index 0e1835087..d458c68ff 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 @@ -296,8 +297,19 @@ export abstract class ArduPilotVehicle extends Vehicle.AbstractVehicle setDataLakeVariableData(path, value)) + + // Special handling for NAMED_VALUE_FLOAT messages + if (messageName === 'NAMED_VALUE_FLOAT') { + const name = (mavlink_message.message.name as string[]).join('').replace(/\0/g, '') + setDataLakeVariableData(`${messageName}/${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..adf7b2257 --- /dev/null +++ b/src/libs/vehicle/ardupilot/data-flattener.ts @@ -0,0 +1,99 @@ +/** + * Result of flattening a data structure + */ +export type FlattenedPair = { + /** + * + */ + path: string + /** + * + */ + value: string | number | boolean + /** + * + */ + 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 [] + }) +}