diff --git a/src/App.vue b/src/App.vue
index 04b9aaea8..bb3959cc0 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -148,7 +148,11 @@ import { useRoute } from 'vue-router'
import ConfigurationMenu from '@/components/ConfigurationMenu.vue'
import { coolMissionNames } from '@/libs/funny-name/words'
-import { CockpitAction, registerActionCallback, unregisterActionCallback } from '@/libs/joystick/protocols'
+import {
+ availableCockpitActions,
+ registerActionCallback,
+ unregisterActionCallback,
+} from '@/libs/joystick/protocols/cockpit-actions'
import { useMissionStore } from '@/stores/mission'
import Dialog from './components/Dialog.vue'
@@ -175,7 +179,10 @@ const routerSection = ref()
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen()
const debouncedToggleFullScreen = useDebounceFn(() => toggleFullscreen(), 10)
-const fullScreenCallbackId = registerActionCallback(CockpitAction.TOGGLE_FULL_SCREEN, debouncedToggleFullScreen)
+const fullScreenCallbackId = registerActionCallback(
+ availableCockpitActions.toggle_full_screen,
+ debouncedToggleFullScreen
+)
onBeforeUnmount(() => unregisterActionCallback(fullScreenCallbackId))
const fullScreenToggleIcon = computed(() => (isFullscreen.value ? 'mdi-fullscreen-exit' : 'mdi-overscan'))
@@ -209,7 +216,10 @@ watch([() => widgetStore.currentView, () => widgetStore.currentView.showBottomBa
showBottomBarNow.value = widgetStore.currentView.showBottomBarOnBoot
})
const debouncedToggleBottomBar = useDebounceFn(() => (showBottomBarNow.value = !showBottomBarNow.value), 25)
-const bottomBarToggleCallbackId = registerActionCallback(CockpitAction.TOGGLE_BOTTOM_BAR, debouncedToggleBottomBar)
+const bottomBarToggleCallbackId = registerActionCallback(
+ availableCockpitActions.toggle_bottom_bar,
+ debouncedToggleBottomBar
+)
onBeforeUnmount(() => unregisterActionCallback(bottomBarToggleCallbackId))
// Start datalogging
diff --git a/src/assets/joystick-profiles.ts b/src/assets/joystick-profiles.ts
index 1c289de24..facb7b107 100644
--- a/src/assets/joystick-profiles.ts
+++ b/src/assets/joystick-profiles.ts
@@ -1,38 +1,69 @@
import { JoystickModel } from '@/libs/joystick/manager'
-import { CockpitAction, MAVLinkAxis } from '@/libs/joystick/protocols'
-import { type GamepadToCockpitStdMapping, type ProtocolControllerMapping, JoystickProtocol } from '@/types/joystick'
+import { availableCockpitActions } from '@/libs/joystick/protocols/cockpit-actions'
+import {
+ availableMavlinkManualControlButtonFunctions,
+ mavlinkManualControlAxes,
+} from '@/libs/joystick/protocols/mavlink-manual-control'
+import { modifierKeyActions, otherAvailableActions } from '@/libs/joystick/protocols/other'
+import {
+ type GamepadToCockpitStdMapping,
+ type JoystickProtocolActionsMapping,
+ CockpitModifierKeyOption,
+ JoystickAxis,
+ JoystickButton,
+} from '@/types/joystick'
// TODO: Adjust mapping for PS5 controller
-export const cockpitStandardToProtocols: ProtocolControllerMapping = {
- name: 'Cockpit Standard Gamepad to Protocols',
- axesCorrespondencies: [
- { protocol: JoystickProtocol.MAVLinkManualControl, value: MAVLinkAxis.Y },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: MAVLinkAxis.X },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: MAVLinkAxis.R },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: MAVLinkAxis.Z },
- ],
- axesMins: [-1000, 1000, -1000, 1000],
- axesMaxs: [1000, -1000, 1000, 0],
- buttonsCorrespondencies: [
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 0 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 1 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 2 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 3 },
- { protocol: JoystickProtocol.CockpitAction, value: CockpitAction.GO_TO_PREVIOUS_VIEW },
- { protocol: JoystickProtocol.CockpitAction, value: CockpitAction.GO_TO_NEXT_VIEW },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 9 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 10 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 4 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 6 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 7 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 8 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 11 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 12 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 13 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 14 },
- { protocol: JoystickProtocol.MAVLinkManualControl, value: 5 },
- { protocol: JoystickProtocol.CockpitAction, value: CockpitAction.TOGGLE_FULL_SCREEN },
- ],
+export const cockpitStandardToProtocols: JoystickProtocolActionsMapping = {
+ name: 'Standard for ArduSub',
+ axesCorrespondencies: {
+ [JoystickAxis.A0]: { action: mavlinkManualControlAxes.axis_y, min: -1000, max: +1000 },
+ [JoystickAxis.A1]: { action: mavlinkManualControlAxes.axis_x, min: +1000, max: -1000 },
+ [JoystickAxis.A2]: { action: mavlinkManualControlAxes.axis_r, min: -1000, max: +1000 },
+ [JoystickAxis.A3]: { action: mavlinkManualControlAxes.axis_z, min: +1000, max: 0 },
+ },
+ buttonsCorrespondencies: {
+ [CockpitModifierKeyOption.regular]: {
+ [JoystickButton.B0]: { action: availableMavlinkManualControlButtonFunctions['Arm'] },
+ [JoystickButton.B1]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B2]: { action: availableMavlinkManualControlButtonFunctions['Mount tilt up'] },
+ [JoystickButton.B3]: { action: availableMavlinkManualControlButtonFunctions['Mount tilt down'] },
+ [JoystickButton.B4]: { action: availableCockpitActions.go_to_previous_view },
+ [JoystickButton.B5]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B6]: { action: availableMavlinkManualControlButtonFunctions['Gain inc'] },
+ [JoystickButton.B7]: { action: availableMavlinkManualControlButtonFunctions['Gain dec'] },
+ [JoystickButton.B8]: { action: availableMavlinkManualControlButtonFunctions['Lights1 brighter'] },
+ [JoystickButton.B9]: { action: availableMavlinkManualControlButtonFunctions['Lights1 dimmer'] },
+ [JoystickButton.B10]: { action: availableCockpitActions.toggle_full_screen },
+ [JoystickButton.B11]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B12]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B13]: { action: modifierKeyActions.shift },
+ [JoystickButton.B14]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B15]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B16]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B17]: { action: otherAvailableActions.no_function },
+ },
+ [CockpitModifierKeyOption.shift]: {
+ [JoystickButton.B0]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B1]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B2]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B3]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B4]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B5]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B6]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B7]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B8]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B9]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B10]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B11]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B12]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B13]: { action: modifierKeyActions.shift },
+ [JoystickButton.B14]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B15]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B16]: { action: otherAvailableActions.no_function },
+ [JoystickButton.B17]: { action: otherAvailableActions.no_function },
+ },
+ },
}
/**
diff --git a/src/libs/joystick/protocols.ts b/src/libs/joystick/protocols.ts
index 896f2e2ba..e24ae286f 100644
--- a/src/libs/joystick/protocols.ts
+++ b/src/libs/joystick/protocols.ts
@@ -1,189 +1,20 @@
-import { v4 as uuid4 } from 'uuid'
+import { type ProtocolAction } from '@/types/joystick'
-import { round, scale } from '@/libs/utils'
-import { sequentialArray } from '@/libs/utils'
+import { availableCockpitActions } from './protocols/cockpit-actions'
import {
- type JoystickState,
- type ProtocolControllerMapping,
- type ProtocolInput,
- JoystickProtocol,
- ProtocolControllerState,
-} from '@/types/joystick'
-
-/**
- * Correspondency between protocol input and it's pretty name (usually the actual function it triggers)
- */
-export interface InputWithPrettyName {
- /**
- * Input which triggers the function
- */
- input: ProtocolInput
- /**
- * Name of the parameter option
- */
- prettyName: string
-}
-
-/**
- * Current state of the controller in the MavLink protocol
- */
-export class MavlinkControllerState extends ProtocolControllerState {
- x: number
- y: number
- z: number
- r: number
- buttons: number
- target: number
- public static readonly BUTTONS_PER_BITFIELD = 16
-
- /**
- *
- * @param { JoystickState } joystickState - Cockpit standard mapped values for the joystick
- * @param { ProtocolControllerMapping } mapping - Gamepad API to Protocols joystick mapping, where assignments and limits are got from.
- * @param { number } target - Specify targeted vehicle ID.
- */
- constructor(joystickState: JoystickState, mapping: ProtocolControllerMapping, target = 1) {
- super()
-
- const isMavlinkInput = (input: ProtocolInput): boolean => input.protocol === JoystickProtocol.MAVLinkManualControl
-
- let buttons_int = 0
- for (let i = 0; i < MavlinkControllerState.BUTTONS_PER_BITFIELD; i++) {
- let buttonState = 0
- mapping.buttonsCorrespondencies.forEach((b, idx) => {
- if (isMavlinkInput(b) && b.value === i && joystickState.buttons[idx]) {
- buttonState = 1
- }
- })
- buttons_int += buttonState * 2 ** i
- }
-
- const xIndex = mapping.axesCorrespondencies.findIndex((v) => isMavlinkInput(v) && v.value === MAVLinkAxis.X)
- const yIndex = mapping.axesCorrespondencies.findIndex((v) => isMavlinkInput(v) && v.value === MAVLinkAxis.Y)
- const zIndex = mapping.axesCorrespondencies.findIndex((v) => isMavlinkInput(v) && v.value === MAVLinkAxis.Z)
- const rIndex = mapping.axesCorrespondencies.findIndex((v) => isMavlinkInput(v) && v.value === MAVLinkAxis.R)
-
- const absLimits = mavlinkAxesLimits
-
- const xLimits = [mapping.axesMins[xIndex] ?? absLimits[0], mapping.axesMaxs[xIndex] ?? absLimits[1]]
- const yLimits = [mapping.axesMins[yIndex] ?? absLimits[0], mapping.axesMaxs[yIndex] ?? absLimits[1]]
- const zLimits = [mapping.axesMins[zIndex] ?? absLimits[0], mapping.axesMaxs[zIndex] ?? absLimits[1]]
- const rLimits = [mapping.axesMins[rIndex] ?? absLimits[0], mapping.axesMaxs[rIndex] ?? absLimits[1]]
-
- this.x = xIndex === undefined ? 0 : round(scale(joystickState.axes[xIndex] ?? 0, -1, 1, xLimits[0], xLimits[1]), 0)
- this.y = yIndex === undefined ? 0 : round(scale(joystickState.axes[yIndex] ?? 0, -1, 1, yLimits[0], yLimits[1]), 0)
- this.z = zIndex === undefined ? 0 : round(scale(joystickState.axes[zIndex] ?? 0, -1, 1, zLimits[0], zLimits[1]), 0)
- this.r = rIndex === undefined ? 0 : round(scale(joystickState.axes[rIndex] ?? 0, -1, 1, rLimits[0], rLimits[1]), 0)
-
- this.buttons = buttons_int
- this.target = round(target, 0)
- }
-}
-
-/**
- * Possible other protocol functions
- */
-export enum OtherProtocol {
- NO_FUNCTION = 'No function',
-}
-
-/**
- * Possible Cockpit Actions
- */
-export enum CockpitAction {
- GO_TO_NEXT_VIEW = 'Go to next view',
- GO_TO_PREVIOUS_VIEW = 'Go to previous view',
- TOGGLE_FULL_SCREEN = 'Toggle full-screen',
- MAVLINK_ARM = 'Mavlink Command - Arm',
- MAVLINK_DISARM = 'Mavlink Command - Disarm',
- TOGGLE_BOTTOM_BAR = 'Toggle bottom bar',
-}
-
-export type CockpitActionCallback = () => void
-
-/**
- * Callback entry
- */
-interface CallbackEntry {
- /**
- * Unique ID for that callback register
- */
- action: CockpitAction
- /**
- * Callback to be called
- */
- callback: CockpitActionCallback
-}
-
-// @ts-ignore: Typescript does not get that we are initializing the object dinamically
-const actionsCallbacks: { [id in string]: CallbackEntry } = {}
-
-export const registerActionCallback = (action: CockpitAction, callback: CockpitActionCallback): string => {
- const id = uuid4()
- actionsCallbacks[id] = { action, callback }
- return id
-}
-export const unregisterActionCallback = (id: string): void => {
- delete actionsCallbacks[id]
-}
-
-export const sendCockpitActions = (joystickState: JoystickState, mapping: ProtocolControllerMapping): void => {
- const actionsToCallback: CockpitAction[] = []
- joystickState.buttons.forEach((state, idx) => {
- const mappedButton = mapping.buttonsCorrespondencies[idx]
- if (state && mappedButton.protocol === JoystickProtocol.CockpitAction) {
- actionsToCallback.push(mappedButton.value as CockpitAction)
- }
- })
- Object.values(actionsCallbacks).forEach((entry) => {
- if (actionsToCallback.includes(entry.action)) {
- entry.callback()
- }
- })
-}
-
-/**
- * Possible axes in the MAVLink protocol
- */
-export enum MAVLinkAxis {
- X = 'x',
- Y = 'y',
- Z = 'z',
- R = 'r',
-}
-const mavlinkAvailableAxes = Object.values(MAVLinkAxis)
-export const mavlinkAvailableButtons = sequentialArray(16)
-
-const mavlinkAxesLimits = [-1000, 1000]
-export const protocolAxesLimits = (protocol: JoystickProtocol): number[] => {
- switch (protocol) {
- case JoystickProtocol.MAVLinkManualControl:
- return mavlinkAxesLimits
- default:
- // Mavlink is the current main protocol and will be used by default
- return mavlinkAxesLimits
- }
-}
-
-export const allAvailableAxes: InputWithPrettyName[] = []
-mavlinkAvailableAxes.forEach((axis) =>
- allAvailableAxes.push({ input: { protocol: JoystickProtocol.MAVLinkManualControl, value: axis }, prettyName: axis })
-)
-
-Object.values(OtherProtocol).forEach((fn) =>
- allAvailableAxes.push({ input: { protocol: JoystickProtocol.Other, value: fn }, prettyName: fn })
-)
-
-export const allAvailableButtons: InputWithPrettyName[] = []
-mavlinkAvailableButtons.forEach((btn) =>
- allAvailableButtons.push({
- input: { protocol: JoystickProtocol.MAVLinkManualControl, value: btn },
- prettyName: btn.toString(),
- })
-)
-Object.values(CockpitAction).forEach((action) =>
- allAvailableButtons.push({ input: { protocol: JoystickProtocol.CockpitAction, value: action }, prettyName: action })
-)
-Object.values(OtherProtocol).forEach((fn) =>
- allAvailableButtons.push({ input: { protocol: JoystickProtocol.Other, value: fn }, prettyName: fn })
-)
+ availableMavlinkManualControlButtonFunctions,
+ mavlinkManualControlAxes,
+} from './protocols/mavlink-manual-control'
+import { modifierKeyActions, otherAvailableActions } from './protocols/other'
+
+export const allAvailableAxes: ProtocolAction[] = [
+ ...Object.values(mavlinkManualControlAxes),
+ ...Object.values(otherAvailableActions),
+]
+
+export const allAvailableButtons: ProtocolAction[] = [
+ ...Object.values(availableCockpitActions),
+ ...Object.values(availableMavlinkManualControlButtonFunctions),
+ ...Object.values(otherAvailableActions),
+ ...Object.values(modifierKeyActions),
+]
diff --git a/src/libs/joystick/protocols/cockpit-actions.ts b/src/libs/joystick/protocols/cockpit-actions.ts
new file mode 100644
index 000000000..31230e268
--- /dev/null
+++ b/src/libs/joystick/protocols/cockpit-actions.ts
@@ -0,0 +1,98 @@
+/* eslint-disable vue/max-len */
+/* eslint-disable prettier/prettier */
+/* eslint-disable max-len */
+import { v4 as uuid4 } from 'uuid'
+
+import { type JoystickProtocolActionsMapping,type JoystickState, type ProtocolAction, JoystickProtocol } from '@/types/joystick'
+
+/**
+ * Possible functions in the MAVLink `MANUAL_CONTROL` message protocol
+ */
+export enum CockpitActionsFunction {
+ go_to_next_view = 'go_to_next_view',
+ go_to_previous_view = 'go_to_previous_view',
+ toggle_full_screen = 'toggle_full_screen',
+ mavlink_arm = 'mavlink_arm',
+ mavlink_disarm = 'mavlink_disarm',
+ toggle_bottom_bar = 'toggle_bottom_bar',
+}
+
+/**
+ * An action to be performed by Cockpit itself
+ */
+export class CockpitAction implements ProtocolAction {
+ id: CockpitActionsFunction
+ name: string
+ readonly protocol = JoystickProtocol.CockpitAction
+
+ // eslint-disable-next-line jsdoc/require-jsdoc
+ constructor(id: CockpitActionsFunction, name: string) {
+ this.id = id
+ this.name = name
+ }
+}
+
+// Available actions
+export const availableCockpitActions: { [key in CockpitActionsFunction]: CockpitAction } = {
+ [CockpitActionsFunction.go_to_next_view]: new CockpitAction(CockpitActionsFunction.go_to_next_view, 'Go to next view'),
+ [CockpitActionsFunction.go_to_previous_view]: new CockpitAction(CockpitActionsFunction.go_to_previous_view, 'Go to previous view'),
+ [CockpitActionsFunction.toggle_full_screen]: new CockpitAction(CockpitActionsFunction.toggle_full_screen, 'Toggle full screen'),
+ [CockpitActionsFunction.mavlink_arm]: new CockpitAction(CockpitActionsFunction.mavlink_arm, 'Mavlink arm'),
+ [CockpitActionsFunction.mavlink_disarm]: new CockpitAction(CockpitActionsFunction.mavlink_disarm, 'Mavlink disarm'),
+ [CockpitActionsFunction.toggle_bottom_bar]: new CockpitAction(CockpitActionsFunction.toggle_bottom_bar, 'Toggle bottom bar'),
+}
+
+export type CockpitActionCallback = () => void
+
+/**
+ * Callback entry
+ */
+interface CallbackEntry {
+ /**
+ * Unique ID for that callback register
+ */
+ action: CockpitAction
+ /**
+ * Callback to be called
+ */
+ callback: CockpitActionCallback
+}
+
+// @ts-ignore: Typescript does not get that we are initializing the object dinamically
+const actionsCallbacks: { [id in string]: CallbackEntry } = {}
+
+export const registerActionCallback = (action: CockpitAction, callback: CockpitActionCallback): string => {
+ const id = uuid4()
+ actionsCallbacks[id] = { action, callback }
+ return id
+}
+export const unregisterActionCallback = (id: string): void => {
+ delete actionsCallbacks[id]
+}
+
+/**
+ * Responsible for routing cockpit actions
+ */
+export class CockpitActionsManager {
+ joystickState: JoystickState
+ currentActionsMapping: JoystickProtocolActionsMapping
+ activeButtonsActions: ProtocolAction[]
+
+ updateControllerData = (state: JoystickState, protocolActionsMapping: JoystickProtocolActionsMapping, activeButtonsActions: ProtocolAction[]): void => {
+ this.joystickState = state
+ this.currentActionsMapping = protocolActionsMapping
+ this.activeButtonsActions = activeButtonsActions
+ }
+
+ sendCockpitActions = (): void => {
+ if (!this.joystickState || !this.currentActionsMapping || !this.activeButtonsActions) return
+
+ const actionsToCallback = this.activeButtonsActions.filter((a) => a.protocol === JoystickProtocol.CockpitAction)
+ Object.values(actionsCallbacks).forEach((entry) => {
+ if (actionsToCallback.map((a) => a.id).includes(entry.action.id)) {
+ console.log(entry.action.name)
+ entry.callback()
+ }
+ })
+ }
+}
diff --git a/src/libs/joystick/protocols/mavlink-manual-control.ts b/src/libs/joystick/protocols/mavlink-manual-control.ts
new file mode 100644
index 000000000..297cf992e
--- /dev/null
+++ b/src/libs/joystick/protocols/mavlink-manual-control.ts
@@ -0,0 +1,557 @@
+/* eslint-disable prettier/prettier */
+/* eslint-disable vue/max-len */
+/* eslint-disable max-len */
+/* eslint-disable jsdoc/require-jsdoc */
+import Swal from 'sweetalert2'
+import { capitalize } from 'vue'
+
+import { modifierKeyActions, otherAvailableActions } from '@/libs/joystick/protocols/other'
+import { round, scale } from '@/libs/utils'
+import type { ArduPilot } from '@/libs/vehicle/ardupilot/ardupilot'
+import { type JoystickProtocolActionsMapping, type JoystickState, type ProtocolAction, JoystickAxis, JoystickButton, JoystickProtocol } from '@/types/joystick'
+
+/**
+ * Possible axes in the MAVLink `MANUAL_CONTROL` message protocol
+ */
+export enum MAVLinkAxisFunction {
+ X = 'axis_x',
+ Y = 'axis_y',
+ Z = 'axis_z',
+ R = 'axis_r',
+}
+
+/**
+ * Possible functions in the MAVLink `MANUAL_CONTROL` message protocol
+ */
+export enum MAVLinkButtonFunction {
+ disabled = 'Disabled', // 0
+ shift = 'Shift', // 1
+ arm_toggle = 'Arm toggle', // 2
+ arm = 'Arm', // 3
+ disarm = 'Disarm', // 4
+ mode_manual = 'Mode manual', // 5
+ mode_stabilize = 'Mode stabilize', // 6
+ mode_depth_hold = 'Mode depth hold', // 7
+ mode_poshold = 'Mode poshold', // 8
+ mode_auto = 'Mode auto', // 9
+ mode_circle = 'Mode circle', // 10
+ mode_guided = 'Mode guided', // 11
+ mode_acro = 'Mode acro', // 12
+ mount_center = 'Mount center', // 21
+ mount_tilt_up = 'Mount tilt up', // 22
+ mount_tilt_down = 'Mount tilt down', // 23
+ camera_trigger = 'Camera trigger', // 24
+ camera_source_toggle = 'Camera source toggle', // 25
+ mount_pan_right = 'Mount pan right', // 26
+ mount_pan_left = 'Mount pan left', // 27
+ lights1_cycle = 'Lights1 cycle', // 31
+ lights1_brighter = 'Lights1 brighter', // 32
+ lights1_dimmer = 'Lights1 dimmer', // 33
+ lights2_cycle = 'Lights2 cycle', // 34
+ lights2_brighter = 'Lights2 brighter', // 35
+ lights2_dimmer = 'Lights2 dimmer', // 36
+ gain_toggle = 'Gain toggle', // 41
+ gain_inc = 'Gain inc', // 42
+ gain_dec = 'Gain dec', // 43
+ trim_roll_inc = 'Trim roll inc', // 44
+ trim_roll_dec = 'Trim roll dec', // 45
+ trim_pitch_inc = 'Trim pitch inc', // 46
+ trim_pitch_dec = 'Trim pitch dec', // 47
+ input_hold_set = 'Input hold set', // 48
+ roll_pitch_toggle = 'Roll pitch toggle', // 49
+ relay1_on = 'Relay 1 on', // 51
+ relay1_off = 'Relay 1 off', // 52
+ relay1_toggle = 'Relay 1 toggle', // 53
+ relay2_on = 'Relay 2 on', // 54
+ relay2_off = 'Relay 2 off', // 55
+ relay2_toggle = 'Relay 2 toggle', // 56
+ relay3_on = 'Relay 3 on', // 57
+ relay3_off = 'Relay 3 off', // 58
+ relay3_toggle = 'Relay 3 toggle', // 59
+ servo1_inc = 'Servo 1 inc', // 61
+ servo1_dec = 'Servo 1 dec', // 62
+ servo1_min = 'Servo 1 min', // 63
+ servo1_max = 'Servo 1 max', // 64
+ servo1_center = 'Servo 1 center', // 65
+ servo2_inc = 'Servo 2 inc', // 66
+ servo2_dec = 'Servo 2 dec', // 67
+ servo2_min = 'Servo 2 min', // 68
+ servo2_max = 'Servo 2 max', // 69
+ servo2_center = 'Servo 2 center', // 70
+ servo3_inc = 'Servo 3 inc', // 71
+ servo3_dec = 'Servo 3 dec', // 72
+ servo3_min = 'Servo 3 min', // 73
+ servo3_max = 'Servo 3 max', // 74
+ servo3_center = 'Servo 3 center', // 75
+ servo1_min_momentary = 'Servo 1 min momentary', // 76
+ servo1_max_momentary = 'Servo 1 max momentary', // 77
+ servo1_min_toggle = 'Servo 1 min toggle', // 78
+ servo1_max_toggle = 'Servo 1 max toggle', // 79
+ servo2_min_momentary = 'Servo 2 min momentary', // 80
+ servo2_max_momentary = 'Servo 2 max momentary', // 81
+ servo2_min_toggle = 'Servo 2 min toggle', // 82
+ servo2_max_toggle = 'Servo 2 max toggle', // 83
+ servo3_min_momentary = 'Servo 3 min momentary', // 84
+ servo3_max_momentary = 'Servo 3 max momentary', // 85
+ servo3_min_toggle = 'Servo 3 min toggle', // 86
+ servo3_max_toggle = 'Servo 3 max toggle', // 87
+ custom1 = 'Custom 1', // 91
+ custom2 = 'Custom 2', // 92
+ custom3 = 'Custom 3', // 93
+ custom4 = 'Custom 4', // 94
+ custom5 = 'Custom 5', // 95
+ custom6 = 'Custom 6', // 96
+ relay4_on = 'Relay 4 on', // 101
+ relay4_off = 'Relay 4 off', // 102
+ relay4_toggle = 'Relay 4 toggle', // 103
+ relay1_momentary = 'Relay 1 momentary', // 104
+ relay2_momentary = 'Relay 2 momentary', // 105
+ relay3_momentary = 'Relay 3 momentary', // 106
+ relay4_momentary = 'Relay 4 momentary', // 107
+}
+
+export enum MAVLinkManualControlButton {
+ R0 = 'BTN0_FUNCTION',
+ S0 = 'BTN0_SFUNCTION',
+ R1 = 'BTN1_FUNCTION',
+ S1 = 'BTN1_SFUNCTION',
+ R2 = 'BTN2_FUNCTION',
+ S2 = 'BTN2_SFUNCTION',
+ R3 = 'BTN3_FUNCTION',
+ S3 = 'BTN3_SFUNCTION',
+ R4 = 'BTN4_FUNCTION',
+ S4 = 'BTN4_SFUNCTION',
+ R5 = 'BTN5_FUNCTION',
+ S5 = 'BTN5_SFUNCTION',
+ R6 = 'BTN6_FUNCTION',
+ S6 = 'BTN6_SFUNCTION',
+ R7 = 'BTN7_FUNCTION',
+ S7 = 'BTN7_SFUNCTION',
+ R8 = 'BTN8_FUNCTION',
+ S8 = 'BTN8_SFUNCTION',
+ R9 = 'BTN9_FUNCTION',
+ S9 = 'BTN9_SFUNCTION',
+ R10 = 'BTN10_FUNCTION',
+ S10 = 'BTN10_SFUNCTION',
+ R11 = 'BTN11_FUNCTION',
+ S11 = 'BTN11_SFUNCTION',
+ R12 = 'BTN12_FUNCTION',
+ S12 = 'BTN12_SFUNCTION',
+ R13 = 'BTN13_FUNCTION',
+ S13 = 'BTN13_SFUNCTION',
+ R14 = 'BTN14_FUNCTION',
+ S14 = 'BTN14_SFUNCTION',
+ R15 = 'BTN15_FUNCTION',
+ S15 = 'BTN15_SFUNCTION',
+}
+
+const manualControlButtonFromParameterName = (name: string): MAVLinkManualControlButton | undefined => {
+ const button = Object.entries(MAVLinkManualControlButton).find((entry) => entry[1] === name)?.[0]
+ return button === undefined ? button : button as MAVLinkManualControlButton
+}
+
+/**
+ * An axis action meant to be used with MAVLink's `MANUAL_CONTROL` message
+ */
+export class MAVLinkManualControlAxisAction implements ProtocolAction {
+ readonly protocol = JoystickProtocol.MAVLinkManualControl
+ /**
+ * Create an axis input
+ * @param {MAVLinkAxisFunction} id Axis identification
+ * @param {string} name Axis human-readable name
+ */
+ constructor(public id: MAVLinkAxisFunction, public name: string) {}
+}
+
+/**
+ * A button action meant to be used with MAVLink's `MANUAL_CONTROL` message
+ */
+export class MAVLinkManualControlButtonAction implements ProtocolAction {
+ readonly protocol = JoystickProtocol.MAVLinkManualControl
+ /**
+ * Create a button input
+ * @param {MAVLinkButtonFunction} id Button identification
+ * @param {string} name Button human-readable name
+ */
+ constructor(public id: MAVLinkButtonFunction, public name: string) {}
+}
+
+// Available axis actions
+export const mavlinkManualControlAxes: { [key in MAVLinkAxisFunction]: MAVLinkManualControlAxisAction } = {
+ [MAVLinkAxisFunction.X]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.X, 'Axis X'),
+ [MAVLinkAxisFunction.Y]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.Y, 'Axis Y'),
+ [MAVLinkAxisFunction.Z]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.Z, 'Axis Z'),
+ [MAVLinkAxisFunction.R]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.R, 'Axis R'),
+}
+
+// Available button actions
+const mavlinkManualControlButtonFunctions: { [key in MAVLinkButtonFunction]: MAVLinkManualControlButtonAction } = {
+ [MAVLinkButtonFunction.disabled]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.disabled, 'Disabled'),
+ [MAVLinkButtonFunction.shift]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.shift, 'Shift'),
+ [MAVLinkButtonFunction.arm_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.arm_toggle, 'Arm toggle'),
+ [MAVLinkButtonFunction.arm]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.arm, 'Arm'),
+ [MAVLinkButtonFunction.disarm]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.disarm, 'Disarm'),
+ [MAVLinkButtonFunction.mode_manual]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_manual, 'Mode manual'),
+ [MAVLinkButtonFunction.mode_stabilize]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_stabilize, 'Mode stabilize'),
+ [MAVLinkButtonFunction.mode_depth_hold]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_depth_hold, 'Mode depth hold'),
+ [MAVLinkButtonFunction.mode_poshold]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_poshold, 'Mode poshold'),
+ [MAVLinkButtonFunction.mode_auto]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_auto, 'Mode auto'),
+ [MAVLinkButtonFunction.mode_circle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_circle, 'Mode circle'),
+ [MAVLinkButtonFunction.mode_guided]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_guided, 'Mode guided'),
+ [MAVLinkButtonFunction.mode_acro]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mode_acro, 'Mode acro'),
+ [MAVLinkButtonFunction.mount_center]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mount_center, 'Mount center'),
+ [MAVLinkButtonFunction.mount_tilt_up]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mount_tilt_up, 'Mount tilt up'),
+ [MAVLinkButtonFunction.mount_tilt_down]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mount_tilt_down, 'Mount tilt down'),
+ [MAVLinkButtonFunction.camera_trigger]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.camera_trigger, 'Camera trigger'),
+ [MAVLinkButtonFunction.camera_source_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.camera_source_toggle, 'Camera source toggle'),
+ [MAVLinkButtonFunction.mount_pan_right]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mount_pan_right, 'Mount pan right'),
+ [MAVLinkButtonFunction.mount_pan_left]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.mount_pan_left, 'Mount pan left'),
+ [MAVLinkButtonFunction.lights1_cycle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.lights1_cycle, 'Lights1 cycle'),
+ [MAVLinkButtonFunction.lights1_brighter]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.lights1_brighter, 'Lights1 brighter'),
+ [MAVLinkButtonFunction.lights1_dimmer]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.lights1_dimmer, 'Lights1 dimmer'),
+ [MAVLinkButtonFunction.lights2_cycle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.lights2_cycle, 'Lights2 cycle'),
+ [MAVLinkButtonFunction.lights2_brighter]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.lights2_brighter, 'Lights2 brighter'),
+ [MAVLinkButtonFunction.lights2_dimmer]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.lights2_dimmer, 'Lights2 dimmer'),
+ [MAVLinkButtonFunction.gain_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.gain_toggle, 'Gain toggle'),
+ [MAVLinkButtonFunction.gain_inc]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.gain_inc, 'Gain inc'),
+ [MAVLinkButtonFunction.gain_dec]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.gain_dec, 'Gain dec'),
+ [MAVLinkButtonFunction.trim_roll_inc]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.trim_roll_inc, 'Trim roll inc'),
+ [MAVLinkButtonFunction.trim_roll_dec]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.trim_roll_dec, 'Trim roll dec'),
+ [MAVLinkButtonFunction.trim_pitch_inc]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.trim_pitch_inc, 'Trim pitch inc'),
+ [MAVLinkButtonFunction.trim_pitch_dec]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.trim_pitch_dec, 'Trim pitch dec'),
+ [MAVLinkButtonFunction.input_hold_set]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.input_hold_set, 'Input hold set'),
+ [MAVLinkButtonFunction.roll_pitch_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.roll_pitch_toggle, 'Roll pitch toggle'),
+ [MAVLinkButtonFunction.relay1_on]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay1_on, 'Relay 1 on'),
+ [MAVLinkButtonFunction.relay1_off]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay1_off, 'Relay 1 off'),
+ [MAVLinkButtonFunction.relay1_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay1_toggle, 'Relay 1 toggle'),
+ [MAVLinkButtonFunction.relay2_on]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay2_on, 'Relay 2 on'),
+ [MAVLinkButtonFunction.relay2_off]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay2_off, 'Relay 2 off'),
+ [MAVLinkButtonFunction.relay2_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay2_toggle, 'Relay 2 toggle'),
+ [MAVLinkButtonFunction.relay3_on]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay3_on, 'Relay 3 on'),
+ [MAVLinkButtonFunction.relay3_off]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay3_off, 'Relay 3 off'),
+ [MAVLinkButtonFunction.relay3_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay3_toggle, 'Relay 3 toggle'),
+ [MAVLinkButtonFunction.servo1_inc]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_inc, 'Servo 1 inc'),
+ [MAVLinkButtonFunction.servo1_dec]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_dec, 'Servo 1 dec'),
+ [MAVLinkButtonFunction.servo1_min]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_min, 'Servo 1 min'),
+ [MAVLinkButtonFunction.servo1_max]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_max, 'Servo 1 max'),
+ [MAVLinkButtonFunction.servo1_center]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_center, 'Servo 1 center'),
+ [MAVLinkButtonFunction.servo2_inc]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_inc, 'Servo 2 inc'),
+ [MAVLinkButtonFunction.servo2_dec]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_dec, 'Servo 2 dec'),
+ [MAVLinkButtonFunction.servo2_min]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_min, 'Servo 2 min'),
+ [MAVLinkButtonFunction.servo2_max]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_max, 'Servo 2 max'),
+ [MAVLinkButtonFunction.servo2_center]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_center, 'Servo 2 center'),
+ [MAVLinkButtonFunction.servo3_inc]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_inc, 'Servo 3 inc'),
+ [MAVLinkButtonFunction.servo3_dec]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_dec, 'Servo 3 dec'),
+ [MAVLinkButtonFunction.servo3_min]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_min, 'Servo 3 min'),
+ [MAVLinkButtonFunction.servo3_max]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_max, 'Servo 3 max'),
+ [MAVLinkButtonFunction.servo3_center]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_center, 'Servo 3 center'),
+ [MAVLinkButtonFunction.servo1_min_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_min_momentary, 'Servo 1 min momentary'),
+ [MAVLinkButtonFunction.servo1_max_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_max_momentary, 'Servo 1 max momentary'),
+ [MAVLinkButtonFunction.servo1_min_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_min_toggle, 'Servo 1 min toggle'),
+ [MAVLinkButtonFunction.servo1_max_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo1_max_toggle, 'Servo 1 max toggle'),
+ [MAVLinkButtonFunction.servo2_min_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_min_momentary, 'Servo 2 min momentary'),
+ [MAVLinkButtonFunction.servo2_max_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_max_momentary, 'Servo 2 max momentary'),
+ [MAVLinkButtonFunction.servo2_min_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_min_toggle, 'Servo 2 min toggle'),
+ [MAVLinkButtonFunction.servo2_max_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo2_max_toggle, 'Servo 2 max toggle'),
+ [MAVLinkButtonFunction.servo3_min_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_min_momentary, 'Servo 3 min momentary'),
+ [MAVLinkButtonFunction.servo3_max_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_max_momentary, 'Servo 3 max momentary'),
+ [MAVLinkButtonFunction.servo3_min_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_min_toggle, 'Servo 3 min toggle'),
+ [MAVLinkButtonFunction.servo3_max_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.servo3_max_toggle, 'Servo 3 max toggle'),
+ [MAVLinkButtonFunction.custom1]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.custom1, 'Custom 1'),
+ [MAVLinkButtonFunction.custom2]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.custom2, 'Custom 2'),
+ [MAVLinkButtonFunction.custom3]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.custom3, 'Custom 3'),
+ [MAVLinkButtonFunction.custom4]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.custom4, 'Custom 4'),
+ [MAVLinkButtonFunction.custom5]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.custom5, 'Custom 5'),
+ [MAVLinkButtonFunction.custom6]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.custom6, 'Custom 6'),
+ [MAVLinkButtonFunction.relay4_on]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay4_on, 'Relay 4 on'),
+ [MAVLinkButtonFunction.relay4_off]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay4_off, 'Relay 4 off'),
+ [MAVLinkButtonFunction.relay4_toggle]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay4_toggle, 'Relay 4 toggle'),
+ [MAVLinkButtonFunction.relay1_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay1_momentary, 'Relay 1 momentary'),
+ [MAVLinkButtonFunction.relay2_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay2_momentary, 'Relay 2 momentary'),
+ [MAVLinkButtonFunction.relay3_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay3_momentary, 'Relay 3 momentary'),
+ [MAVLinkButtonFunction.relay4_momentary]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.relay4_momentary, 'Relay 4 momentary'),
+}
+
+// Exclude shift key so it's not mapped by user, as it's automatically handled by Cockpit backend.
+export const { [MAVLinkButtonFunction.shift]: _, ...availableMavlinkManualControlButtonFunctions } = mavlinkManualControlButtonFunctions
+
+export class MavlinkManualControlState {
+ public static readonly BUTTONS_PER_BITFIELD = 16
+ x = 0
+ y = 0
+ z = 0
+ r = 0
+ buttons = 0
+ target = 1
+}
+
+export class MavlinkManualControlManager {
+ joystickState: JoystickState
+ currentActionsMapping: JoystickProtocolActionsMapping
+ activeButtonsActions: ProtocolAction[]
+ manualControlState: MavlinkManualControlState | undefined = undefined
+ lastValidActionsMapping: JoystickProtocolActionsMapping
+ parametersTable: { title: string; value: number }[] = []
+ vehicleButtonParameterTable: { title: string; value: number }[] = []
+ currentVehicleParameters: { [key in string]: number } = {}
+ vehicleTotalParametersCount: number | undefined = undefined
+ public vehicle: ArduPilot | undefined
+
+ constructor() {
+ setInterval(() => {
+ this.remapActionsToVehicleButtonParameters()
+ }, 1000)
+ }
+
+ setVehicle(vehicle: ArduPilot): void {
+ // Set vehicle instance
+ this.vehicle = vehicle
+
+ // Update interval parameters when they are available
+ this.vehicle.onParameter.add(([newParameter, parametersCount]) => {
+ const newVehicleParameters = { ...this.currentVehicleParameters, ...{ [newParameter.name]: newParameter.value } }
+ this.currentVehicleParameters = newVehicleParameters
+ this.vehicleTotalParametersCount = parametersCount
+ })
+
+ this.vehicle.requestParametersList()
+ this.updateVehicleButtonsParameters()
+ }
+
+ sendManualControl(): void {
+ if (!this.vehicle || !this.manualControlState) return
+ this.vehicle.sendManualControl(this.manualControlState)
+ }
+
+ updateControllerData = (state: JoystickState, protocolActionsMapping: JoystickProtocolActionsMapping, activeButtonsActions: ProtocolAction[]): void => {
+ this.joystickState = state
+ this.currentActionsMapping = protocolActionsMapping
+ this.activeButtonsActions = activeButtonsActions
+
+ // Update our mapping with the changes made by the user.
+ // Act over the vehicle (by changing it's buttons parameters) if needed.
+ this.upadteManualControlState()
+ }
+
+ upadteManualControlState(): void {
+ // Return if insuficient joystick data
+ if (!this.joystickState || !this.currentActionsMapping || !this.activeButtonsActions) return
+
+ // Return if insuficient vehicle data (prevent dangerous manual control messages)
+ if (!this.currentVehicleParameters || !this.vehicleButtonParameterTable) return
+
+ // Instantiate manual control state if not already done
+ if (!this.manualControlState) {
+ this.manualControlState = new MavlinkManualControlState()
+ }
+
+ const buttonParametersNamedObject: { [key in number]: string } = {}
+ this.vehicleButtonParameterTable.forEach((entry) => (buttonParametersNamedObject[entry.value] = entry.title))
+ const currentRegularButtonParameters = Object.entries(this.currentVehicleParameters)
+ .filter(([k]) => k.includes('BTN') && !k.includes('S'))
+ .map((btn) => ({ button: btn[0], actionId: buttonParametersNamedObject[btn[1]] }))
+ const currentShiftButtonParameters = Object.entries(this.currentVehicleParameters)
+ .filter(([k]) => k.includes('BTN') && k.includes('S'))
+ .map((btn) => ({ button: btn[0], actionId: buttonParametersNamedObject[btn[1]] }))
+
+ const activeMavlinkManualControlActions = this.activeButtonsActions.filter((a) => a.protocol === JoystickProtocol.MAVLinkManualControl).map((action) => action.id)
+ const regularVehicleButtonsToActivate = currentRegularButtonParameters
+ .filter((entry) => activeMavlinkManualControlActions.includes(entry.actionId as MAVLinkButtonFunction))
+ .map((entry) => manualControlButtonFromParameterName(entry.button))
+ const shiftVehicleButtonsToActivate = currentShiftButtonParameters
+ .filter((entry) => activeMavlinkManualControlActions.includes(entry.actionId as MAVLinkButtonFunction))
+ .map((entry) => manualControlButtonFromParameterName(entry.button))
+
+ const useShift = this.activeButtonsActions.map((a) => a.id).includes(modifierKeyActions.shift.id)
+ const shiftButton = currentRegularButtonParameters.find((v) => v.actionId === MAVLinkButtonFunction.shift)
+
+ if (useShift && shiftButton === undefined) return
+
+ const vehicleButtonsToActivate = useShift ? shiftVehicleButtonsToActivate.concat([manualControlButtonFromParameterName(shiftButton!.button)]) : regularVehicleButtonsToActivate
+
+ // Calculate buttons value
+ let buttons_int = 0
+ for (let i = 0; i < MavlinkManualControlState.BUTTONS_PER_BITFIELD; i++) {
+ let buttonState = 0
+ vehicleButtonsToActivate.forEach((btn) => {
+ if (btn !== undefined && Number(btn.replace('R', '').replace('S', '')) === i) {
+ buttonState = 1
+ }
+ })
+ buttons_int += buttonState * 2 ** i
+ }
+
+ // Calculate axes values
+ const xCorrespondency = Object.entries(this.currentActionsMapping.axesCorrespondencies).find((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl && entry[1].action.id === mavlinkManualControlAxes.axis_x.id)
+ const yCorrespondency = Object.entries(this.currentActionsMapping.axesCorrespondencies).find((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl && entry[1].action.id === mavlinkManualControlAxes.axis_y.id)
+ const zCorrespondency = Object.entries(this.currentActionsMapping.axesCorrespondencies).find((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl && entry[1].action.id === mavlinkManualControlAxes.axis_z.id)
+ const rCorrespondency = Object.entries(this.currentActionsMapping.axesCorrespondencies).find((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl && entry[1].action.id === mavlinkManualControlAxes.axis_r.id)
+
+ // Populate MAVLink Manual Control state of axes and buttons
+ this.manualControlState.x = xCorrespondency === undefined ? 0 : round(scale(this.joystickState.axes[xCorrespondency[0] as unknown as JoystickAxis] ?? 0, -1, 1, xCorrespondency[1].min, xCorrespondency[1].max), 0)
+ this.manualControlState.y = yCorrespondency === undefined ? 0 : round(scale(this.joystickState.axes[yCorrespondency[0] as unknown as JoystickAxis] ?? 0, -1, 1, yCorrespondency[1].min, yCorrespondency[1].max), 0)
+ this.manualControlState.z = zCorrespondency === undefined ? 0 : round(scale(this.joystickState.axes[zCorrespondency[0] as unknown as JoystickAxis] ?? 0, -1, 1, zCorrespondency[1].min, zCorrespondency[1].max), 0)
+ this.manualControlState.r = rCorrespondency === undefined ? 0 : round(scale(this.joystickState.axes[rCorrespondency[0] as unknown as JoystickAxis] ?? 0, -1, 1, rCorrespondency[1].min, rCorrespondency[1].max), 0)
+ this.manualControlState.buttons = buttons_int
+ }
+
+ updateVehicleButtonsParameters = (): void => {
+ if (!this.vehicle) return
+
+ const updatedParameterTable = {}
+ for (const category of Object.values(this.vehicle.metadata())) {
+ for (const [name, parameter] of Object.entries(category)) {
+ if (!isNaN(Number(parameter))) {
+ continue
+ }
+ const newParameterTable = { ...this.parametersTable, ...{ [name]: parameter } }
+ Object.assign(updatedParameterTable, newParameterTable)
+ }
+ }
+ Object.assign(this.parametersTable, updatedParameterTable)
+
+
+ this.vehicleButtonParameterTable.splice(0)
+ // @ts-ignore: This type is huge. Needs refactoring typing here.
+ if (this.parametersTable['BTN0_FUNCTION'] && this.parametersTable['BTN0_FUNCTION']['Values']) {
+ // @ts-ignore: This type is huge. Needs refactoring typing here.
+ Object.entries(this.parametersTable['BTN0_FUNCTION']['Values']).forEach((param) => {
+ const rawText = param[1] as string
+ const formatedText = capitalize(rawText).replace(new RegExp('_', 'g'), ' ')
+ this.vehicleButtonParameterTable.push({ title: formatedText as string, value: Number(param[0]) })
+ })
+ }
+ }
+
+ remapActionsToVehicleButtonParameters = (): void => {
+ // TODO: Refactor this routine to reuse all methods for regular and shift
+ if (!this.vehicle || !this.currentActionsMapping || !this.currentVehicleParameters || !this.vehicleButtonParameterTable || !this.vehicleTotalParametersCount) return
+ // Do not proceed with remapping unless we already have all parameters downloaded
+ // This prevents us from thinking some function is not mapped in the vehicle when in fact we just didn't download it yet
+ const allParametersDownloaded = Object.entries(this.currentVehicleParameters).length >= this.vehicleTotalParametersCount
+ if (!allParametersDownloaded) {
+ return
+ }
+
+ const buttonParametersNamedObject: { [key in number]: string } = {}
+ this.vehicleButtonParameterTable.forEach((entry) => (buttonParametersNamedObject[entry.value] = entry.title))
+ const currentRegularButtonParameters = Object.entries(this.currentVehicleParameters)
+ .filter(([k]) => k.includes('BTN') && !k.includes('S'))
+ .map((btn) => ({ button: btn[0], actionId: buttonParametersNamedObject[btn[1]] }))
+ const currentShiftButtonParameters = Object.entries(this.currentVehicleParameters)
+ .filter(([k]) => k.includes('BTN') && k.includes('S'))
+ .map((btn) => ({ button: btn[0], actionId: buttonParametersNamedObject[btn[1]] }))
+
+ // Re-use shift button if already mapped. Otherwise use R0 and S0.
+ const regularShiftFunction = currentRegularButtonParameters.find((v) => v.actionId === MAVLinkButtonFunction.shift)
+ const shiftActionValue = this.vehicleButtonParameterTable.find((e) => e.title === MAVLinkButtonFunction.shift)
+ if (regularShiftFunction === undefined) {
+ // Map shift to R0
+ this.vehicle.setParameter({ id: MAVLinkManualControlButton.R0, value: shiftActionValue!.value })
+
+ // Map shift to S0
+ this.vehicle.setParameter({ id: MAVLinkManualControlButton.S0, value: shiftActionValue!.value })
+ } else {
+ const sFunction = regularShiftFunction.button.replace('FUNCTION', 'SFUNCTION')
+ this.vehicle.setParameter({ id: sFunction, value: shiftActionValue!.value })
+ }
+
+ const currentMappedActionsInRegularButtons = currentRegularButtonParameters.map((v) => v.actionId)
+ const currentMappedActionsInShiftButtons = currentShiftButtonParameters.map((v) => v.actionId)
+
+ const wantedRegularMavlinkActions = Object.entries(this.currentActionsMapping.buttonsCorrespondencies.regular)
+ .filter((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl)
+ .map((corr) => corr[1].action.id)
+ const wantedUnmappedRegularMavlinkActions = wantedRegularMavlinkActions
+ .filter((actionId) => !currentMappedActionsInRegularButtons.includes(actionId))
+ const wantedShiftMavlinkActions = Object.entries(this.currentActionsMapping.buttonsCorrespondencies.shift)
+ .filter((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl)
+ .map((corr) => corr[1].action.id)
+ const wantedUnmappedShiftMavlinkActions = wantedShiftMavlinkActions
+ .filter((actionId) => !currentMappedActionsInShiftButtons.includes(actionId))
+
+ const disabledVehicleRegularButtons = currentRegularButtonParameters.filter((v) => v.actionId === 'Disabled')
+ const disabledVehicleShiftButtons = currentShiftButtonParameters.filter((v) => v.actionId === 'Disabled')
+
+ const remainingUnmappedRegularMavlinkActions: string[] = []
+ let indexRegularButtonToUse = 0
+ wantedUnmappedRegularMavlinkActions.forEach((actionId) => {
+ if (indexRegularButtonToUse >= disabledVehicleRegularButtons.length) {
+ remainingUnmappedRegularMavlinkActions.push(actionId)
+ } else {
+ const mavlinkActionValue = this.vehicleButtonParameterTable.find((e) => e.title === actionId)
+ if (mavlinkActionValue === undefined) return
+ this.vehicle?.setParameter({ id: disabledVehicleRegularButtons[indexRegularButtonToUse].button, value: mavlinkActionValue.value })
+ }
+ indexRegularButtonToUse++
+ })
+
+ const remainingUnmappedShiftMavlinkActions: string[] = []
+ let indexShiftButtonToUse = 0
+ wantedUnmappedShiftMavlinkActions.forEach((actionId) => {
+ if (indexShiftButtonToUse >= disabledVehicleShiftButtons.length) {
+ remainingUnmappedShiftMavlinkActions.push(actionId)
+ } else {
+ const mavlinkActionValue = this.vehicleButtonParameterTable.find((e) => e.title === actionId)
+ if (mavlinkActionValue === undefined) return
+ this.vehicle?.setParameter({ id: disabledVehicleShiftButtons[indexShiftButtonToUse].button, value: mavlinkActionValue.value })
+ }
+ indexShiftButtonToUse++
+ })
+
+ const unnecessaryVehicleRegularButtons = currentRegularButtonParameters.filter((v) => v.actionId !== MAVLinkButtonFunction.shift && !wantedRegularMavlinkActions.includes(v.actionId))
+ const unnecessaryVehicleShiftButtons = currentShiftButtonParameters.filter((v) => v.actionId !== MAVLinkButtonFunction.shift && !wantedShiftMavlinkActions.includes(v.actionId))
+
+ const finallyRemainedUnmappedRegularMavlinkActions: string[] = []
+ indexRegularButtonToUse = 0
+ remainingUnmappedRegularMavlinkActions.forEach((actionId) => {
+ if (indexRegularButtonToUse >= unnecessaryVehicleRegularButtons.length) {
+ finallyRemainedUnmappedRegularMavlinkActions.push(actionId)
+ } else {
+ const mavlinkActionValue = this.vehicleButtonParameterTable.find((e) => e.title === actionId)
+ if (mavlinkActionValue === undefined) return
+ this.vehicle?.setParameter({ id: unnecessaryVehicleRegularButtons[indexRegularButtonToUse].button, value: mavlinkActionValue.value })
+ }
+ indexRegularButtonToUse++
+ })
+
+ const finallyRemainedUnmappedShiftMavlinkActions: string[] = []
+ indexShiftButtonToUse = 0
+ remainingUnmappedShiftMavlinkActions.forEach((actionId) => {
+ if (indexShiftButtonToUse >= unnecessaryVehicleShiftButtons.length) {
+ finallyRemainedUnmappedShiftMavlinkActions.push(actionId)
+ } else {
+ const mavlinkActionValue = this.vehicleButtonParameterTable.find((e) => e.title === actionId)
+ if (mavlinkActionValue === undefined) return
+ this.vehicle?.setParameter({ id: unnecessaryVehicleShiftButtons[indexShiftButtonToUse].button, value: mavlinkActionValue.value })
+ }
+ indexShiftButtonToUse++
+ })
+
+ // There are no spots left to map the remaining ones, so we throw a warning and un-map them from the joystick.
+ finallyRemainedUnmappedRegularMavlinkActions.forEach((actionId) => {
+ const buttonAction = Object.entries(this.currentActionsMapping.buttonsCorrespondencies.regular).find((v) => v[1].action.id === actionId)
+ if (buttonAction === undefined) return
+ Swal.fire({
+ text: `There are no spots left in the vehicle for the MAVLink Manual Control function ${actionId}.
+ Consider mapping this function to a shift button.`,
+ icon: 'error',
+ timer: 6000,
+ })
+ this.currentActionsMapping.buttonsCorrespondencies.regular[Number(buttonAction[0]) as JoystickButton].action = otherAvailableActions.no_function
+ })
+
+ finallyRemainedUnmappedShiftMavlinkActions.forEach((actionId) => {
+ const buttonAction = Object.entries(this.currentActionsMapping.buttonsCorrespondencies.shift).find((v) => v[1].action.id === actionId)
+ if (buttonAction === undefined) return
+ Swal.fire({
+ text: `There are no spots left in the vehicle for the MAVLink Manual Control function ${actionId}.
+ Consider mapping this function to a shift button.`,
+ icon: 'error',
+ timer: 6000,
+ })
+ this.currentActionsMapping.buttonsCorrespondencies.shift[Number(buttonAction[0]) as JoystickButton].action = otherAvailableActions.no_function
+ })
+ }
+}
+
diff --git a/src/libs/joystick/protocols/other.ts b/src/libs/joystick/protocols/other.ts
new file mode 100644
index 000000000..679e51b7f
--- /dev/null
+++ b/src/libs/joystick/protocols/other.ts
@@ -0,0 +1,29 @@
+import { type ProtocolAction, CockpitModifierKeyOption, JoystickProtocol } from '@/types/joystick'
+
+/**
+ * Possible other protocol functions
+ */
+export enum OtherProtocol {
+ no_function = 'no_function',
+}
+
+export const otherAvailableActions: { [key in OtherProtocol]: ProtocolAction } = {
+ [OtherProtocol.no_function]: {
+ protocol: JoystickProtocol.Other,
+ id: OtherProtocol.no_function,
+ name: 'No function',
+ },
+}
+
+export const modifierKeyActions: { [key in CockpitModifierKeyOption]: ProtocolAction } = {
+ [CockpitModifierKeyOption.regular]: {
+ protocol: JoystickProtocol.CockpitModifierKey,
+ id: CockpitModifierKeyOption.regular,
+ name: 'Regular',
+ },
+ [CockpitModifierKeyOption.shift]: {
+ protocol: JoystickProtocol.CockpitModifierKey,
+ id: CockpitModifierKeyOption.shift,
+ name: 'Shift',
+ },
+}
diff --git a/src/libs/vehicle/ardupilot/ardupilot.ts b/src/libs/vehicle/ardupilot/ardupilot.ts
index 7b75820dc..2915defab 100644
--- a/src/libs/vehicle/ardupilot/ardupilot.ts
+++ b/src/libs/vehicle/ardupilot/ardupilot.ts
@@ -20,7 +20,7 @@ import {
} from '@/libs/connection/m2r/messages/mavlink2rest-enum'
import { MavFrame } from '@/libs/connection/m2r/messages/mavlink2rest-enum'
import { type Message } from '@/libs/connection/m2r/messages/mavlink2rest-message'
-import { MavlinkControllerState } from '@/libs/joystick/protocols'
+import { type MavlinkManualControlState } from '@/libs/joystick/protocols/mavlink-manual-control'
import { SignalTyped } from '@/libs/signal'
import { round } from '@/libs/utils'
import {
@@ -43,7 +43,6 @@ import {
Velocity,
} from '@/libs/vehicle/types'
import type { MetadataFile } from '@/types/ardupilot-metadata'
-import { ProtocolControllerState } from '@/types/joystick'
import { type MissionLoadingCallback, type Waypoint, defaultLoadingCallback } from '@/types/mission'
import * as Vehicle from '../vehicle'
@@ -509,10 +508,10 @@ export abstract class ArduPilotVehicle
Could not stablish communication with the vehicle.
Button functions will appear as numbers. If connection is restablished, function names will appear.
{{ joystick.model }} controller
-@@ -196,27 +205,23 @@