From 5147655434a6e79bb807c3e3273362329e0bdd2f Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Tue, 5 Nov 2024 14:13:47 -0300 Subject: [PATCH] cockpit-actions: Add MAVLink message actions Any message is accepted as long as it's a valid MAVLink message. For COMMAND_LONG and COMMAND_INT messages, the message fields are pre-defined in the UI. For all other messages, the user should specify the JSON message config manually. --- .../MavlinkMessageActionConfig.vue | 352 ++++++++++++++++++ ...ink-message-actions-message-definitions.ts | 164 ++++++++ src/libs/actions/mavlink-message-actions.ts | 163 ++++++++ src/views/ConfigurationActionsView.vue | 2 + 4 files changed, 681 insertions(+) create mode 100644 src/components/configuration/MavlinkMessageActionConfig.vue create mode 100644 src/libs/actions/mavlink-message-actions-message-definitions.ts create mode 100644 src/libs/actions/mavlink-message-actions.ts diff --git a/src/components/configuration/MavlinkMessageActionConfig.vue b/src/components/configuration/MavlinkMessageActionConfig.vue new file mode 100644 index 000000000..589861af6 --- /dev/null +++ b/src/components/configuration/MavlinkMessageActionConfig.vue @@ -0,0 +1,352 @@ + + + + + diff --git a/src/libs/actions/mavlink-message-actions-message-definitions.ts b/src/libs/actions/mavlink-message-actions-message-definitions.ts new file mode 100644 index 000000000..95055f7d2 --- /dev/null +++ b/src/libs/actions/mavlink-message-actions-message-definitions.ts @@ -0,0 +1,164 @@ +import { MAVLinkType } from '../connection/m2r/messages/mavlink2rest-enum' +import { MessageFieldType } from './mavlink-message-actions' + +/* eslint-disable jsdoc/require-jsdoc */ +interface MessageField { + value: number | string | boolean + type: MessageFieldType + description: string + units?: string + required: boolean +} +/* eslint-enable jsdoc/require-jsdoc */ + +// Partial message field definitions for each MAVLink message type +export const messageFieldDefinitions: Partial>> = { + [MAVLinkType.COMMAND_LONG]: { + target_system: { + value: 1, + type: MessageFieldType.NUMBER, + description: 'System ID of target system', + required: true, + }, + target_component: { + value: 1, + type: MessageFieldType.NUMBER, + description: 'Component ID of target component', + required: true, + }, + command: { + value: 'MAV_CMD_COMPONENT_ARM_DISARM', + type: MessageFieldType.TYPE_STRUCT_ENUM, + description: 'Command ID', + required: true, + }, + confirmation: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Confirmation value (0: first transmission, 1-255: confirmations)', + required: true, + }, + param1: { + value: 1, + type: MessageFieldType.NUMBER, + description: 'Parameter 1', + required: false, + }, + param2: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 2', + required: false, + }, + param3: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 3', + required: false, + }, + param4: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 4', + required: false, + }, + param5: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 5', + required: false, + }, + param6: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 6', + required: false, + }, + param7: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 7', + required: false, + }, + }, + [MAVLinkType.COMMAND_INT]: { + target_system: { + value: 1, + type: MessageFieldType.NUMBER, + description: 'System ID of target system', + required: true, + }, + target_component: { + value: 1, + type: MessageFieldType.NUMBER, + description: 'Component ID of target component', + required: true, + }, + command: { + value: 'MAV_CMD_DO_REPOSITION', + type: MessageFieldType.TYPE_STRUCT_ENUM, + description: 'Command ID', + required: true, + }, + frame: { + value: 'MAV_FRAME_GLOBAL', + type: MessageFieldType.TYPE_STRUCT_ENUM, + description: 'Coordinate frame', + required: true, + }, + current: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Current sequence number', + required: true, + }, + autocontinue: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Autocontinue bit', + required: true, + }, + param1: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 1', + required: false, + }, + param2: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 2', + required: false, + }, + param3: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 3', + required: false, + }, + param4: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Parameter 4', + required: false, + }, + x: { + value: -275600000, + type: MessageFieldType.NUMBER, + description: 'Latitude', + required: false, + }, + y: { + value: -484500000, + type: MessageFieldType.NUMBER, + description: 'Longitude', + required: false, + }, + z: { + value: 0, + type: MessageFieldType.NUMBER, + description: 'Altitude', + required: false, + }, + }, +} as const diff --git a/src/libs/actions/mavlink-message-actions.ts b/src/libs/actions/mavlink-message-actions.ts new file mode 100644 index 000000000..6cdcf8579 --- /dev/null +++ b/src/libs/actions/mavlink-message-actions.ts @@ -0,0 +1,163 @@ +import { sendMavlinkMessage } from '../communication/mavlink' +import type { Message } from '../connection/m2r/messages/mavlink2rest' +import { MAVLinkType } from '../connection/m2r/messages/mavlink2rest-enum' +import { + availableCockpitActions, + CockpitAction, + CockpitActionsFunction, + deleteAction, + registerActionCallback, + registerNewAction, +} from '../joystick/protocols/cockpit-actions' +import { getCockpitActionVariableData } from './data-lake' + +const mavlinkMessageActionIdPrefix = 'mavlink-message-action' + +/** + * Enum with the possible message field types + */ +export enum MessageFieldType { + NUMBER = 'number', + STRING = 'string', + BOOLEAN = 'boolean', + TYPE_STRUCT_ENUM = 'type_struct_enum', +} + +export type MavlinkMessageConfigField = { + /** + * The type of the field + * Determines how the value is processed + */ + type: MessageFieldType + /** + * The value of the field + */ + value: any +} + +export type MavlinkMessageConfig = Record | string + +export type MavlinkMessageActionConfig = { + /** + * The name of the action + */ + name: string + /** + * The type of MAVLink message to send + */ + messageType: MAVLinkType + /** + * The key-value pairs of the message fields + */ + messageConfig: MavlinkMessageConfig +} + +let registeredMavlinkMessageActionConfigs: Record = {} + +export const registerMavlinkMessageActionConfig = (action: MavlinkMessageActionConfig): void => { + const id = `${mavlinkMessageActionIdPrefix} (${action.name})` + registeredMavlinkMessageActionConfigs[id] = action + saveMavlinkMessageActionConfigs() + updateCockpitActions() +} + +export const getMavlinkMessageActionConfig = (id: string): MavlinkMessageActionConfig | undefined => { + return registeredMavlinkMessageActionConfigs[id] +} + +export const getAllMavlinkMessageActionConfigs = (): Record => { + return registeredMavlinkMessageActionConfigs +} + +export const deleteMavlinkMessageActionConfig = (id: string): void => { + delete registeredMavlinkMessageActionConfigs[id] + saveMavlinkMessageActionConfigs() + updateCockpitActions() +} + +export const updateMavlinkMessageActionConfig = (id: string, updatedAction: MavlinkMessageActionConfig): void => { + registeredMavlinkMessageActionConfigs[id] = updatedAction + saveMavlinkMessageActionConfigs() + updateCockpitActions() +} + +export const updateCockpitActions = (): void => { + // Remove existing MAVLink message actions + Object.entries(availableCockpitActions).forEach(([id]) => { + if (id.includes(mavlinkMessageActionIdPrefix)) { + deleteAction(id as CockpitActionsFunction) + } + }) + + // Register new actions + const mavlinkActions = getAllMavlinkMessageActionConfigs() + for (const [id, action] of Object.entries(mavlinkActions)) { + try { + const cockpitAction = new CockpitAction(id as CockpitActionsFunction, action.name) + registerNewAction(cockpitAction) + registerActionCallback(cockpitAction, getMavlinkMessageActionCallback(id)) + } catch (error) { + console.error(`Error registering action ${id}: ${error}`) + } + } +} + +export const loadMavlinkMessageActionConfigs = (): void => { + const savedActions = localStorage.getItem('cockpit-mavlink-message-actions') + if (savedActions) { + registeredMavlinkMessageActionConfigs = JSON.parse(savedActions) + } +} + +export const saveMavlinkMessageActionConfigs = (): void => { + localStorage.setItem('cockpit-mavlink-message-actions', JSON.stringify(registeredMavlinkMessageActionConfigs)) +} + +export type MavlinkMessageActionCallback = () => void + +export const getMavlinkMessageActionCallback = (id: string): MavlinkMessageActionCallback => { + const action = getMavlinkMessageActionConfig(id) + if (!action) { + throw new Error(`Action with id ${id} not found.`) + } + + return () => { + const message: Message = { + type: action.messageType, + ...processMessageConfig(action.messageConfig), + } + sendMavlinkMessage(message) + } +} + +const processMessageConfig = (config: MavlinkMessageConfig): Record => { + let processedConfig: Record = {} + + if (typeof config === 'string') { + const configWithDynamicValues = config.replace(/{{\s*([^{}\s]+)\s*}}/g, (match, p1) => { + const variableValue = getCockpitActionVariableData(p1) + return variableValue ? variableValue.toString() : match + }) + processedConfig = JSON.parse(configWithDynamicValues) + } else { + for (const [k, v] of Object.entries(config)) { + if (typeof v.value === 'string' && v.value.startsWith('{{') && v.value.endsWith('}}')) { + const variableName = v.value.slice(2, -2).trim() + const variableValue = getCockpitActionVariableData(variableName) + processedConfig[k] = variableValue + } else if (v.type === MessageFieldType.TYPE_STRUCT_ENUM) { + processedConfig[k] = { type: v.value } + } else if (v.type === MessageFieldType.NUMBER) { + processedConfig[k] = Number(v.value) + } else { + processedConfig[k] = v.value + } + } + } + + return processedConfig +} + +// Initialize actions on module load +loadMavlinkMessageActionConfigs() +updateCockpitActions() diff --git a/src/views/ConfigurationActionsView.vue b/src/views/ConfigurationActionsView.vue index 171fea4f7..4100b5e1f 100644 --- a/src/views/ConfigurationActionsView.vue +++ b/src/views/ConfigurationActionsView.vue @@ -105,6 +105,7 @@ + @@ -314,6 +315,7 @@