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/components/joysticks/JoystickPS.vue b/src/components/joysticks/JoystickPS.vue index 66420bf7b..0545005ec 100644 --- a/src/components/joysticks/JoystickPS.vue +++ b/src/components/joysticks/JoystickPS.vue @@ -7,9 +7,13 @@ import { v4 as uuid4 } from 'uuid' import { computed, onBeforeUnmount, ref, toRefs, watch } from 'vue' import { JoystickModel } from '@/libs/joystick/manager' -import type { InputWithPrettyName } from '@/libs/joystick/protocols' import { scale } from '@/libs/utils' -import { type JoystickInput, type ProtocolControllerMapping, JoystickAxis, JoystickButton } from '@/types/joystick' +import { + type JoystickButtonActionCorrespondency, + type JoystickInput, + JoystickAxis, + JoystickButton, +} from '@/types/joystick' import { InputType } from '@/types/joystick' const textColor = '#747474' @@ -97,8 +101,7 @@ const props = defineProps<{ leftAxisVert?: number // State of the vertical left axis as a floating point number, between -1 and +1 rightAxisHoriz?: number // State of the horizontal right axis as a floating point number, between -1 and +1 rightAxisVert?: number // State of the vertical right axis as a floating point number, between -1 and +1 - protocolMapping: ProtocolControllerMapping // Mapping from the Cockpit standard to the protocol functions - buttonLabelCorrespondency: InputWithPrettyName[] // Mapping from the protocol functions to human readable names + buttonsActionsCorrespondency: JoystickButtonActionCorrespondency // Mapping from the Cockpit standard to the protocol functions }>() const emit = defineEmits<{ @@ -109,10 +112,10 @@ const emit = defineEmits<{ const findInputFromPath = (path: string): JoystickInput[] => { const inputs: JoystickInput[] = [] Object.entries(buttonPath).filter(([, v]) => v === path).forEach((button) => { - inputs.push({ type: InputType.Button, value: button[0] as unknown as JoystickButton }) + inputs.push({ type: InputType.Button, id: button[0] as unknown as JoystickButton }) }) Object.entries(axisPath.value).filter(([, v]) => v === path).forEach((axis) => { - inputs.push({ type: InputType.Axis, value: axis[0] as unknown as JoystickAxis }) + inputs.push({ type: InputType.Axis, id: axis[0] as unknown as JoystickAxis }) }) return inputs } @@ -194,17 +197,15 @@ watch( () => updateButtonsState() ) -const buttonLabelCorrespondency = toRefs(props).buttonLabelCorrespondency -const protocolMapping = toRefs(props).protocolMapping const joystickModel = toRefs(props).model -watch([protocolMapping, buttonLabelCorrespondency], () => updateLabelsState()) +const buttonsActionsCorrespondency = toRefs(props).buttonsActionsCorrespondency +watch(buttonsActionsCorrespondency, () => updateLabelsState()) const updateLabelsState = (): void => { Object.values(JoystickButton).forEach((button) => { if (isNaN(Number(button))) return - const protocolButton = props.protocolMapping.buttonsCorrespondencies[button as JoystickButton] || undefined - const param = props.buttonLabelCorrespondency.find((btn) => btn.input.protocol === protocolButton.protocol && btn.input.value === protocolButton.value) - const functionName = param === undefined ? `${protocolButton.value} (${protocolButton.protocol})` : param.prettyName + const buttonActionCorrespondency = buttonsActionsCorrespondency.value[button as JoystickButton] || undefined + const functionName = buttonActionCorrespondency === undefined ? 'unassigned' : buttonActionCorrespondency.action.name if (!svg) return // @ts-ignore: we already check if button is a number and so if button is a valid index const labelId = buttonPath[button].replace('path', 'text') diff --git a/src/composables/webRTC.ts b/src/composables/webRTC.ts index bd8744b7a..95181d428 100644 --- a/src/composables/webRTC.ts +++ b/src/composables/webRTC.ts @@ -59,7 +59,7 @@ export class WebRTCManager { * @param {RTCConfiguration} rtcConfiguration */ constructor(webRTCSignallingURI: Connection.URI, rtcConfiguration: RTCConfiguration) { - console.debug('[WebRTC] Trying to connect to signalling server.') + // console.debug('[WebRTC] Trying to connect to signalling server.') this.rtcConfiguration = rtcConfiguration this.signaller = new Signaller( webRTCSignallingURI, @@ -96,7 +96,7 @@ export class WebRTCManager { } const msg = `Selected stream changed from "${oldStream?.id}" to "${newStream?.id}".` - console.debug('[WebRTC] ' + msg) + // console.debug('[WebRTC] ' + msg) if (oldStream !== undefined) { this.stopSession(msg) } @@ -112,7 +112,7 @@ export class WebRTCManager { } const msg = `Selected IPs changed from "${oldIps}" to "${newIps}".` - console.debug('[WebRTC] ' + msg) + // console.debug('[WebRTC] ' + msg) this.selectedICEIPs = newIps @@ -139,7 +139,7 @@ export class WebRTCManager { * @param {string} newStatus */ private updateStreamStatus(newStatus: string): void { - console.debug(`[WebRTC] Stream status updated from "${this.streamStatus.value}" to "${newStatus}"`) + // console.debug(`[WebRTC] Stream status updated from "${this.streamStatus.value}" to "${newStatus}"`) const time = new Date().toTimeString().split(' ').first() this.streamStatus.value = `${newStatus} (${time})` } @@ -149,7 +149,7 @@ export class WebRTCManager { * @param {string} newStatus */ private updateSignallerStatus(newStatus: string): void { - console.debug(`[WebRTC] Signaller status updated from "${this.signallerStatus.value}" to "${newStatus}"`) + // console.debug(`[WebRTC] Signaller status updated from "${this.signallerStatus.value}" to "${newStatus}"`) const time = new Date().toTimeString().split(' ').first() this.signallerStatus.value = `${newStatus} (${time})` } @@ -228,10 +228,10 @@ export class WebRTCManager { }) console.groupCollapsed('[WebRTC] Track added') - console.debug('Event:', event) - console.debug('Settings:', event.track.getSettings?.()) - console.debug('Constraints:', event.track.getConstraints?.()) - console.debug('Capabilities:', event.track.getCapabilities?.()) + // console.debug('Event:', event) + // console.debug('Settings:', event.track.getSettings?.()) + // console.debug('Constraints:', event.track.getConstraints?.()) + // console.debug('Capabilities:', event.track.getCapabilities?.()) console.groupEnd() } @@ -241,7 +241,7 @@ export class WebRTCManager { * @param {string} consumerId */ private requestSession(stream: Stream, consumerId: string): void { - console.debug(`[WebRTC] Requesting stream:`, stream) + // console.debug(`[WebRTC] Requesting stream:`, stream) // Requests a new Session ID this.signaller.requestSessionId( @@ -285,7 +285,7 @@ export class WebRTCManager { const msg = `Starting session with producer "${stream.id}" ("${this.streamName}")` this.updateStreamStatus(msg) - console.debug('[WebRTC] ' + msg) + // console.debug('[WebRTC] ' + msg) if (this.consumerId === undefined) { const error = @@ -342,7 +342,7 @@ export class WebRTCManager { producerId, this.session.id, (sessionId, reason) => { - console.debug(`[WebRTC] Session ${sessionId} ended. Reason: ${reason}`) + // console.debug(`[WebRTC] Session ${sessionId} ended. Reason: ${reason}`) this.session = undefined this.hasEnded = true }, @@ -360,7 +360,7 @@ export class WebRTCManager { ) const msg = `Session ${this.session.id} successfully started` - console.debug('[WebRTC] ' + msg) + // console.debug('[WebRTC] ' + msg) this.updateStreamStatus(msg) } @@ -370,12 +370,12 @@ export class WebRTCManager { */ private stopSession(reason: string): void { if (this.session === undefined) { - console.debug('[WebRTC] Stopping an undefined session, probably it was already stopped?') + // console.debug('[WebRTC] Stopping an undefined session, probably it was already stopped?') return } const msg = `Stopping session ${this.session.id}. Reason: ${reason}` this.updateStreamStatus(msg) - console.debug('[WebRTC] ' + msg) + // console.debug('[WebRTC] ' + msg) this.session.end() this.session = undefined 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/arducopter.ts b/src/libs/vehicle/ardupilot/arducopter.ts index 4f40f4a74..9e97d9327 100644 --- a/src/libs/vehicle/ardupilot/arducopter.ts +++ b/src/libs/vehicle/ardupilot/arducopter.ts @@ -1,6 +1,7 @@ import type { Package } from '@/libs/connection/m2r/messages/mavlink2rest' import { MAVLinkType, MavModeFlag } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import type { Message } from '@/libs/connection/m2r/messages/mavlink2rest-message' +import * as arducopter_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Copter-4.3/apm.pdef.json' import * as Vehicle from '../vehicle' import { ArduPilotVehicle } from './ardupilot' @@ -71,6 +72,7 @@ export enum CustomMode { */ export class ArduCopter extends ArduPilotVehicle { _mode: CustomMode = CustomMode.PRE_FLIGHT + _metadata = arducopter_metadata /** * Create ArduCopter vehicle diff --git a/src/libs/vehicle/ardupilot/ardupilot.ts b/src/libs/vehicle/ardupilot/ardupilot.ts index febc9006d..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 { @@ -42,7 +42,7 @@ import { StatusText, Velocity, } from '@/libs/vehicle/types' -import { ProtocolControllerState } from '@/types/joystick' +import type { MetadataFile } from '@/types/ardupilot-metadata' import { type MissionLoadingCallback, type Waypoint, defaultLoadingCallback } from '@/types/mission' import * as Vehicle from '../vehicle' @@ -69,11 +69,13 @@ export abstract class ArduPilotVehicle extends Vehicle.AbstractVehicle extends Vehicle.AbstractVehicle extends Vehicle.AbstractVehicle extends Vehicle.AbstractVehicle extends Vehicle.AbstractVehicle extends Vehicle.AbstractVehicle { _mode: CustomMode = CustomMode.PRE_FLIGHT + _metadata = arduplane_metadata /** * Create ArduPlane vehicle diff --git a/src/libs/vehicle/ardupilot/ardurover.ts b/src/libs/vehicle/ardupilot/ardurover.ts index 4b233eb67..69d4fc787 100644 --- a/src/libs/vehicle/ardupilot/ardurover.ts +++ b/src/libs/vehicle/ardupilot/ardurover.ts @@ -1,6 +1,7 @@ import type { Package } from '@/libs/connection/m2r/messages/mavlink2rest' import { MAVLinkType, MavModeFlag } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import type { Message } from '@/libs/connection/m2r/messages/mavlink2rest-message' +import * as ardurover_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Rover-4.2/apm.pdef.json' import * as Vehicle from '../vehicle' import { ArduPilotVehicle } from './ardupilot' @@ -32,6 +33,7 @@ export enum CustomMode { */ export class ArduRover extends ArduPilotVehicle { _mode: CustomMode = CustomMode.PRE_FLIGHT + _metadata = ardurover_metadata /** * Create ArduRover vehicle diff --git a/src/libs/vehicle/ardupilot/ardusub.ts b/src/libs/vehicle/ardupilot/ardusub.ts index 06331e2ae..88e48cafd 100644 --- a/src/libs/vehicle/ardupilot/ardusub.ts +++ b/src/libs/vehicle/ardupilot/ardusub.ts @@ -1,6 +1,7 @@ import type { Package } from '@/libs/connection/m2r/messages/mavlink2rest' import { MAVLinkType, MavModeFlag } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import type { Message } from '@/libs/connection/m2r/messages/mavlink2rest-message' +import * as ardusub_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Sub-4.1/apm.pdef.json' import * as Vehicle from '../vehicle' import { ArduPilotVehicle } from './ardupilot' @@ -38,6 +39,7 @@ export enum CustomMode { */ export class ArduSub extends ArduPilotVehicle { _mode: CustomMode = CustomMode.PRE_FLIGHT + _metadata = ardusub_metadata /** * Create ArduSub vehicle diff --git a/src/libs/vehicle/vehicle.ts b/src/libs/vehicle/vehicle.ts index 451cea653..7f7f71039 100644 --- a/src/libs/vehicle/vehicle.ts +++ b/src/libs/vehicle/vehicle.ts @@ -79,7 +79,7 @@ export abstract class AbstractVehicle { onMode = new Signal() onPosition = new Signal() onPowerSupply = new Signal() - onParameter = new Signal() + onParameter = new Signal<[Parameter, number | undefined]>() onStatusGPS = new Signal() onStatusText = new Signal() onVelocity = new Signal() @@ -114,7 +114,7 @@ export abstract class AbstractVehicle { this.onMode.register_caller(() => this.mode()) this.onPosition.register_caller(() => this.position()) this.onPowerSupply.register_caller(() => this.powerSupply()) - this.onParameter.register_caller(() => this.lastParameter()) + this.onParameter.register_caller(() => [this.lastParameter(), this.totalParametersCount()]) this.onStatusText.register_caller(() => this.statusText()) this.onStatusGPS.register_caller(() => this.statusGPS()) this.onVelocity.register_caller(() => this.velocity()) @@ -180,6 +180,7 @@ export abstract class AbstractVehicle { abstract position(): Coordinates abstract velocity(): Velocity abstract powerSupply(): PowerSupply + abstract totalParametersCount(): number | undefined abstract lastParameter(): Parameter abstract statusText(): StatusText abstract statusGPS(): StatusGPS diff --git a/src/libs/webrtc/session.ts b/src/libs/webrtc/session.ts index eebf382b5..d1e3e781f 100644 --- a/src/libs/webrtc/session.ts +++ b/src/libs/webrtc/session.ts @@ -95,7 +95,7 @@ export class Session { * @returns {RTCPeerConnection} - A new instance of RTCPeerConnection */ private createRTCPeerConnection(configuration: RTCConfiguration): RTCPeerConnection { - console.debug('[WebRTC] [Session] Creating RTCPeerConnection') + // console.debug('[WebRTC] [Session] Creating RTCPeerConnection') const peerConnection = new RTCPeerConnection(configuration) peerConnection.addTransceiver('video', { @@ -123,7 +123,7 @@ export class Session { this.peerConnection .setRemoteDescription(description) .then(() => { - console.debug(`[WebRTC] [Session] Remote description set to ${JSON.stringify(description, null, 4)}`) + // console.debug(`[WebRTC] [Session] Remote description set to ${JSON.stringify(description, null, 4)}`) this.onRemoteDescriptionSet() }) .catch((reason) => @@ -138,7 +138,7 @@ export class Session { this.peerConnection .createAnswer() .then((description: RTCSessionDescriptionInit) => { - console.debug(`[WebRTC] [Session] SDP Answer created as: ${JSON.stringify(description, null, 4)}`) + // console.debug(`[WebRTC] [Session] SDP Answer created as: ${JSON.stringify(description, null, 4)}`) this.onAnswerCreated(description) }) .catch((reason) => console.error(`[WebRTC] [Session] Failed creating description answer. Reason: ${reason}`)) @@ -152,7 +152,7 @@ export class Session { this.peerConnection .setLocalDescription(description) .then(() => { - console.debug(`[WebRTC] [Session] Local description set as${JSON.stringify(description, null, 4)}`) + // console.debug(`[WebRTC] [Session] Local description set as${JSON.stringify(description, null, 4)}`) this.onLocalDescriptionSet() }) .catch(function (reason) { @@ -195,13 +195,12 @@ export class Session { !this.selectedICEIPs.isEmpty() && !this.selectedICEIPs.some((address) => candidate.candidate!.includes(address)) ) { - console.debug(`[WebRTC] [Session] ICE candidate ignored: ${JSON.stringify(candidate, null, 4)}`) + // console.debug(`[WebRTC] [Session] ICE candidate ignored: ${JSON.stringify(candidate, null, 4)}`) return } this.peerConnection .addIceCandidate(candidate) - .then(() => console.debug(`[WebRTC] [Session] ICE candidate added: ${JSON.stringify(candidate, null, 4)}`)) .catch((reason) => console.error(`[WebRTC] [Session] Failed adding ICE candidate ${candidate}. Reason: ${reason}`) ) @@ -225,7 +224,7 @@ export class Session { */ private onIceCandidateError(event: Event): void { const ev = event as RTCPeerConnectionIceErrorEvent - console.debug(`[WebRTC] [Session] ICE Candidate "${ev.url}" negotiation failed`) + // console.debug(`[WebRTC] [Session] ICE Candidate "${ev.url}" negotiation failed`) } /** @@ -242,7 +241,7 @@ export class Session { */ // eslint-disable-next-line @typescript-eslint/no-unused-vars private onNegotiationNeeded(_event: Event): void { - console.debug('[WebRTC] [Session] Peer Connection is waiting for negotiation...') + // console.debug('[WebRTC] [Session] Peer Connection is waiting for negotiation...') } /** @@ -250,7 +249,7 @@ export class Session { */ private onIceConnectionStateChange(): void { const msg = `ICEConnection state changed to "${this.peerConnection.iceConnectionState}"` - console.debug('[WebRTC] [Session] ' + msg) + // console.debug('[WebRTC] [Session] ' + msg) if (this.peerConnection.iceConnectionState === 'failed') { this.peerConnection.restartIce() @@ -262,7 +261,7 @@ export class Session { */ private onConnectionStateChange(): void { const msg = `RTCPeerConnection state changed to "${this.peerConnection.connectionState}"` - console.debug('[WebRTC] [Session] ' + msg) + // console.debug('[WebRTC] [Session] ' + msg) if (this.peerConnection.connectionState === 'failed') { this.onClose?.(this.id, 'PeerConnection failed') @@ -275,7 +274,7 @@ export class Session { */ private onSignalingStateChange(): void { const msg = `Signalling state changed to "${this.peerConnection.iceConnectionState}"` - console.debug('[WebRTC] [Session] ' + msg) + // console.debug('[WebRTC] [Session] ' + msg) } /** @@ -283,7 +282,7 @@ export class Session { */ private onIceGatheringStateChange(): void { if (this.peerConnection.iceGatheringState === 'complete') { - console.debug(`[WebRTC] [Session] ICE gathering completed for session ${this.id}`) + // console.debug(`[WebRTC] [Session] ICE gathering completed for session ${this.id}`) } } @@ -300,7 +299,7 @@ export class Session { this.ended = true - console.debug(`[WebRTC] [Session] Session ${this.id} ended.`) + // console.debug(`[WebRTC] [Session] Session ${this.id} ended.`) } /** diff --git a/src/libs/webrtc/signaller.ts b/src/libs/webrtc/signaller.ts index 3ceb40cf0..acbdeea22 100644 --- a/src/libs/webrtc/signaller.ts +++ b/src/libs/webrtc/signaller.ts @@ -39,7 +39,7 @@ export class Signaller { this.url = url const status = `Connecting to signalling server on ${url}` - console.debug('[WebRTC] [Signaller] ' + status) + // console.debug('[WebRTC] [Signaller] ' + status) this.onStatusChange?.(status) this.ws = this.connect() @@ -139,7 +139,7 @@ export class Signaller { return } - console.debug('[WebRTC] [Signaller] Message accepted from requestConsumerId:', message) + // console.debug('[WebRTC] [Signaller] Message accepted from requestConsumerId:', message) signaller.removeEventListener('message', consumerIdListener) @@ -162,7 +162,7 @@ export class Signaller { try { this.ws.send(JSON.stringify(message)) - console.debug('[WebRTC] [Signaller] Message sent:', message) + // console.debug('[WebRTC] [Signaller] Message sent:', message) onStatusChanged?.('Consumer Id requested, waiting answer...') } catch (reason) { const error = `Failed requesting peer id. Reason: ${reason}` @@ -196,7 +196,7 @@ export class Signaller { return } this.ws.send(JSON.stringify(message)) - console.debug('[WebRTC] [Signaller] Message sent:', message) + // console.debug('[WebRTC] [Signaller] Message sent:', message) onStatusChanged?.('StreamsAvailable requested') } catch (error) { const errorMsg = `Failed requesting available streams. Reason: ${error}` @@ -232,7 +232,7 @@ export class Signaller { return } - console.debug('[WebRTC] [Signaller] Message accepted from requestSessionId:', message) + // console.debug('[WebRTC] [Signaller] Message accepted from requestSessionId:', message) const sessionId = answer.content.session_id if (sessionId === undefined) { @@ -265,7 +265,7 @@ export class Signaller { try { this.ws.send(JSON.stringify(message)) - console.debug('[WebRTC] [Signaller] Message sent:', message) + // console.debug('[WebRTC] [Signaller] Message sent:', message) onStatusChanged?.('Session Id requested, waiting answer...') } catch (reason) { const error = `Failed requesting Session Id. Reason: ${reason}` @@ -302,11 +302,11 @@ export class Signaller { }, } - console.debug(`[WebRTC] [Signaller] Sending ICE answer: ${JSON.stringify(message, null, 4)}`) + // console.debug(`[WebRTC] [Signaller] Sending ICE answer: ${JSON.stringify(message, null, 4)}`) try { this.ws.send(JSON.stringify(message)) - console.debug('[WebRTC] [Signaller] Message sent:', message) + // console.debug('[WebRTC] [Signaller] Message sent:', message) onStatusChanged?.('ICE Candidate sent') } catch (error) { const errorMsg = `Failed sending ICE Candidate. Reason: ${error}` @@ -345,7 +345,7 @@ export class Signaller { try { this.ws.send(JSON.stringify(message)) - console.debug('[WebRTC] [Signaller] Message sent:', message) + // console.debug('[WebRTC] [Signaller] Message sent:', message) onStatusChanged?.('ICE Candidate sent') } catch (error) { const errorMsg = `Failed sending SDP. Reason: ${error}` @@ -388,15 +388,6 @@ export class Signaller { onSessionEnd: OnSessionEndCallback, onStatusChanged?: OnStatusChangeCallback ): void { - console.debug( - '[WebRTC] [Signaller] Registering parseEndSessionQuestion for ' + - `Consumer "${consumerId}", ` + - `Producer "${producerId}", ` + - `Session "${sessionId}", ` + - 'with callbacks:', - onSessionEnd, - onStatusChanged - ) // eslint-disable-next-line @typescript-eslint/no-this-alias const signaller = this this.addEventListener('message', function endSessionListener(ev: MessageEvent): void { @@ -411,7 +402,7 @@ export class Signaller { return } - console.debug('[WebRTC] [Signaller] Message accepted from parseEndSessionQuestion:', message) + // console.debug('[WebRTC] [Signaller] Message accepted from parseEndSessionQuestion:', message) const endSessionQuestion = question.content if ( @@ -453,16 +444,6 @@ export class Signaller { onMediaNegotiation?: OnMediaNegotiationCallback, onStatusChanged?: OnStatusChangeCallback ): void { - console.debug( - '[WebRTC] [Signaller] Registering parseNegotiation for ' + - `Consumer "${consumerId}", ` + - `Producer "${producerId}", ` + - `Session "${sessionId}", ` + - 'with callbacks:', - onIceNegotiation, - onMediaNegotiation, - onStatusChanged - ) this.addEventListener('message', (ev: MessageEvent): void => { try { const message: Message = JSON.parse(ev.data) @@ -471,7 +452,7 @@ export class Signaller { return } - console.debug('[WebRTC] [Signaller] Message accepted from parseNegotiation:', message) + // console.debug('[WebRTC] [Signaller] Message accepted from parseNegotiation:', message) const negotiation: Negotiation = message.content @@ -512,11 +493,6 @@ export class Signaller { onAvailableStreams: OnAvailableStreamsCallback, onStatusChanged?: OnStatusChangeCallback ): void { - console.debug( - `[WebRTC] [Signaller] Registering parseAvailableStreamsAnswer with callbacks:`, - onAvailableStreams, - onStatusChanged - ) // eslint-disable-next-line @typescript-eslint/no-this-alias const signaller = this this.addEventListener('message', function availableStreamListener(ev: MessageEvent): void { @@ -531,7 +507,7 @@ export class Signaller { return } - console.debug('[WebRTC] [Signaller] Message accepted from parseAvailableStreamsAnswer:', message) + // console.debug('[WebRTC] [Signaller] Message accepted from parseAvailableStreamsAnswer:', message) signaller.removeEventListener('message', availableStreamListener) @@ -566,7 +542,7 @@ export class Signaller { if (this.ws.readyState !== this.ws.OPEN) { return } - console.debug(`[WebRTC] [Signaller] Closing WebSocket. Reason: ${reason}`) + // console.debug(`[WebRTC] [Signaller] Closing WebSocket. Reason: ${reason}`) this.ws.close() } @@ -597,7 +573,7 @@ export class Signaller { */ private reconnect(): void { const status = `Reconnecting to signalling` - console.debug('[WebRTC] [Signaller] ' + status) + // console.debug('[WebRTC] [Signaller] ' + status) this.onStatusChange?.(status) this.end('reconnect') @@ -618,7 +594,7 @@ export class Signaller { */ private onOpenCallback(event: Event): void { const status = `Signaller Connected` - console.debug('[WebRTC] [Signaller] ' + status, event) + // console.debug('[WebRTC] [Signaller] ' + status, event) this.onStatusChange?.(status) this.onOpen?.(event) @@ -630,7 +606,7 @@ export class Signaller { */ private onCloseCallback(event: CloseEvent): void { const status = `Signaller connection closed` - console.debug('[WebRTC] [Signaller] ' + status, event) + // console.debug('[WebRTC] [Signaller] ' + status, event) this.onStatusChange?.(status) if (this.shouldReconnect) { @@ -649,7 +625,7 @@ export class Signaller { */ private onErrorCallback(event: Event): void { const status = `Signaller connection Error` - console.debug('[WebRTC] [Signaller] ' + status, event) + // console.debug('[WebRTC] [Signaller] ' + status, event) this.onStatusChange?.(status) } } diff --git a/src/stores/controller.ts b/src/stores/controller.ts index 9ac92c0bf..8d8699b30 100644 --- a/src/stores/controller.ts +++ b/src/stores/controller.ts @@ -7,17 +7,30 @@ import { ref } from 'vue' import { availableGamepadToCockpitMaps, cockpitStandardToProtocols } from '@/assets/joystick-profiles' import { type JoystickEvent, EventType, joystickManager, JoystickModel } from '@/libs/joystick/manager' import { allAvailableAxes, allAvailableButtons } from '@/libs/joystick/protocols' -import { type JoystickState, type ProtocolControllerMapping, Joystick } from '@/types/joystick' +import { modifierKeyActions, otherAvailableActions } from '@/libs/joystick/protocols/other' +import { + type JoystickProtocolActionsMapping, + type JoystickState, + type ProtocolAction, + CockpitModifierKeyOption, + Joystick, + JoystickButton, + JoystickProtocol, +} from '@/types/joystick' -export type controllerUpdateCallback = (state: JoystickState, protocolMapping: ProtocolControllerMapping) => void +export type controllerUpdateCallback = ( + state: JoystickState, + protocolActionsMapping: JoystickProtocolActionsMapping, + activeButtonActions: ProtocolAction[] +) => void export const useControllerStore = defineStore('controller', () => { const joysticks = ref>(new Map()) const updateCallbacks = ref([]) - const protocolMapping = useStorage('cockpit-protocol-mapping-v3', cockpitStandardToProtocols) + const protocolMapping = useStorage('cockpit-protocol-mapping-v5.4', cockpitStandardToProtocols) const cockpitStdMappings = useStorage('cockpit-standard-mappings', availableGamepadToCockpitMaps) - const availableProtocolAxesFunctions = allAvailableAxes - const availableProtocolButtonFunctions = allAvailableButtons + const availableAxesActions = allAvailableAxes + const availableButtonActions = allAvailableButtons const enableForwarding = ref(true) const registerControllerUpdateCallback = (callback: controllerUpdateCallback): void => { @@ -53,10 +66,71 @@ export const useControllerStore = defineStore('controller', () => { joystick.gamepadToCockpitMap = cockpitStdMappings.value[joystickModel] for (const callback of updateCallbacks.value) { - callback(joystick.state, protocolMapping.value) + callback(joystick.state, protocolMapping.value, activeButtonActions(joystick.state, protocolMapping.value)) } } + const activeButtonActions = ( + joystickState: JoystickState, + mapping: JoystickProtocolActionsMapping + ): ProtocolAction[] => { + let modifierKeyId = modifierKeyActions.regular.id + + Object.entries(mapping.buttonsCorrespondencies.regular).forEach((e) => { + const buttonActive = joystickState.buttons[Number(e[0])] ?? 0 > 0.5 + const isModifier = Object.values(modifierKeyActions) + .map((a) => JSON.stringify(a)) + .includes(JSON.stringify(e[1].action)) + if (buttonActive && isModifier) { + modifierKeyId = e[1].action.id + } + }) + + const modKeyAction = modifierKeyActions[modifierKeyId as CockpitModifierKeyOption] + + const activeActions = joystickState.buttons + .map((btnState, idx) => ({ id: idx, value: btnState })) + .filter((btn) => btn.value ?? 0 > 0.5) + .map( + (btn) => + mapping.buttonsCorrespondencies[modifierKeyId as CockpitModifierKeyOption][btn.id as JoystickButton].action + ) + + return activeActions.concat(modKeyAction) + } + + setInterval(() => { + // eslint-disable-next-line jsdoc/require-jsdoc + const btnsToUnmap: { modKey: CockpitModifierKeyOption; button: JoystickButton }[] = [] + Object.entries(protocolMapping.value.buttonsCorrespondencies.regular).forEach((v) => { + if (v[1].action.protocol == JoystickProtocol.CockpitModifierKey) { + btnsToUnmap.push({ modKey: v[1].action.id as CockpitModifierKeyOption, button: Number(v[0]) as JoystickButton }) + } + }) + + Object.entries(protocolMapping.value.buttonsCorrespondencies).forEach(([modKey, mapping]) => { + Object.entries(mapping).forEach(([btn, action]) => { + const modKeyAction = modifierKeyActions[modKey as CockpitModifierKeyOption] + if (JSON.stringify(action.action) !== JSON.stringify(modKeyAction)) return + Swal.fire({ text: "Cannot map modifier key to it's own layout.", icon: 'warning', timer: 5000 }) + protocolMapping.value.buttonsCorrespondencies[modKey as CockpitModifierKeyOption][ + Number(btn) as JoystickButton + ].action = otherAvailableActions.no_function + }) + }) + + btnsToUnmap.forEach((v) => { + const actionToUnmap = protocolMapping.value.buttonsCorrespondencies[v.modKey][v.button].action + if (JSON.stringify(actionToUnmap) === JSON.stringify(otherAvailableActions.no_function)) return + Swal.fire({ + text: `Unmapping '${actionToUnmap.name} from ${v.modKey} layout. Cannot use same button as the modifier.`, + icon: 'warning', + timer: 5000, + }) + protocolMapping.value.buttonsCorrespondencies[v.modKey][v.button].action = otherAvailableActions.no_function + }) + }, 1000) + // If there's a mapping in our database that is not on the user storage, add it to the user // This will happen whenever a new joystick profile is added to Cockpit's database Object.entries(availableGamepadToCockpitMaps).forEach(([k, v]) => { @@ -91,8 +165,8 @@ export const useControllerStore = defineStore('controller', () => { joysticks, protocolMapping, cockpitStdMappings, - availableProtocolAxesFunctions, - availableProtocolButtonFunctions, + availableAxesActions, + availableButtonActions, downloadJoystickProfile, loadJoystickProfile, } diff --git a/src/stores/mainVehicle.ts b/src/stores/mainVehicle.ts index 325dacce6..3da88b297 100644 --- a/src/stores/mainVehicle.ts +++ b/src/stores/mainVehicle.ts @@ -1,6 +1,6 @@ import { useStorage, useTimestamp } from '@vueuse/core' import { defineStore } from 'pinia' -import { capitalize, computed, onBeforeUnmount, reactive, ref, watch } from 'vue' +import { computed, onBeforeUnmount, reactive, ref, watch } from 'vue' import { defaultGlobalAddress } from '@/assets/defaults' import * as Connection from '@/libs/connection/connection' @@ -9,18 +9,13 @@ import type { Package } from '@/libs/connection/m2r/messages/mavlink2rest' import { MavAutopilot, MAVLinkType, MavType } from '@/libs/connection/m2r/messages/mavlink2rest-enum' import type { Message } from '@/libs/connection/m2r/messages/mavlink2rest-message' import { - type InputWithPrettyName, - CockpitAction, - MavlinkControllerState, + availableCockpitActions, + CockpitActionsManager, registerActionCallback, - sendCockpitActions, unregisterActionCallback, -} from '@/libs/joystick/protocols' +} from '@/libs/joystick/protocols/cockpit-actions' +import { MavlinkManualControlManager } from '@/libs/joystick/protocols/mavlink-manual-control' import type { ArduPilot } from '@/libs/vehicle/ardupilot/ardupilot' -import * as arducopter_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Copter-4.3/apm.pdef.json' -import * as arduplane_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Plane-4.3/apm.pdef.json' -import * as ardurover_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Rover-4.2/apm.pdef.json' -import * as ardusub_metadata from '@/libs/vehicle/ardupilot/ParameterRepository/Sub-4.1/apm.pdef.json' import type { ArduPilotParameterSetData } from '@/libs/vehicle/ardupilot/types' import * as Protocol from '@/libs/vehicle/protocol/protocol' import type { @@ -28,7 +23,6 @@ import type { Attitude, Coordinates, PageDescription, - Parameter, PowerSupply, StatusGPS, StatusText, @@ -37,13 +31,6 @@ import type { } from '@/libs/vehicle/types' import * as Vehicle from '@/libs/vehicle/vehicle' import { VehicleFactory } from '@/libs/vehicle/vehicle-factory' -import { type MetadataFile } from '@/types/ardupilot-metadata' -import { - type JoystickState, - type ProtocolControllerMapping, - JoystickProtocol, - ProtocolControllerState, -} from '@/types/joystick' import type { MissionLoadingCallback, Waypoint } from '@/types/mission' import { useControllerStore } from './controller' @@ -115,10 +102,6 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { const coordinates: Coordinates = reactive({} as Coordinates) const powerSupply: PowerSupply = reactive({} as PowerSupply) const velocity: Velocity = reactive({} as Velocity) - const parametersTable = reactive({}) - // eslint-disable-next-line jsdoc/require-jsdoc - const buttonParameterTable = reactive<{ title: string; value: number }[]>([]) - const currentParameters = reactive({}) const mainVehicle = ref(undefined) const isArmed = ref(undefined) const icon = ref(undefined) @@ -164,14 +147,6 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { } } - /** - * Send manual control message - * @param {ProtocolControllerState} controllerState Current state of the controller - */ - function sendManualControl(controllerState: ProtocolControllerState): void { - mainVehicle.value?.sendManualControl(controllerState) - } - /** * Send heartbeat from GCS */ @@ -179,13 +154,6 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { mainVehicle.value?.sendGcsHeartbeat() } - /** - * Request current parameters from vehicle - */ - function requestParametersList(): void { - mainVehicle.value?.requestParametersList() - } - /** * Upload mission items to vehicle * @param { Waypoint[] } items Mission items that will be sent @@ -281,10 +249,6 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { mainVehicle.value.onPowerSupply.add((newPowerSupply: PowerSupply) => { Object.assign(powerSupply, newPowerSupply) }) - mainVehicle.value.onParameter.add((newParameter: Parameter) => { - const newCurrentParameters = { ...currentParameters, ...{ [newParameter.name]: newParameter.value } } - Object.assign(currentParameters, newCurrentParameters) - }) mainVehicle.value.onStatusText.add((newStatusText: StatusText) => { Object.assign(statusText, newStatusText) }) @@ -329,75 +293,40 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { }, setFlightMode: setFlightMode, } - const mavlinkArmId = registerActionCallback(CockpitAction.MAVLINK_ARM, arm) - const mavlinkDisarmId = registerActionCallback(CockpitAction.MAVLINK_DISARM, disarm) + const mavlinkArmId = registerActionCallback(availableCockpitActions.mavlink_arm, arm) + const mavlinkDisarmId = registerActionCallback(availableCockpitActions.mavlink_disarm, disarm) onBeforeUnmount(() => { unregisterActionCallback(mavlinkArmId) unregisterActionCallback(mavlinkDisarmId) }) }) - watch(vehicleType, (newType, oldType) => { - if (newType !== undefined && newType !== oldType) { - // Default to submarine metadata - let metadata: MetadataFile = ardusub_metadata - // This is to avoid importing a 40 lines enum from mavlink and adding a switch case with 40 cases - if ( - vehicleType.value?.toString().toLowerCase().includes('vtol') || - vehicleType.value?.toString().toLowerCase().includes('wing') - ) { - metadata = arduplane_metadata - } else if ( - vehicleType.value?.toString().toLowerCase().includes('copter') || - vehicleType.value?.toString().toLowerCase().includes('rotor') - ) { - metadata = arducopter_metadata - } else if ( - vehicleType.value?.toString().toLowerCase().includes('rover') || - vehicleType.value?.toString().toLowerCase().includes('boat') - ) { - metadata = ardurover_metadata - } - - const updatedParameterTable = {} - for (const category of Object.values(metadata)) { - for (const [name, parameter] of Object.entries(category)) { - if (!isNaN(Number(parameter))) { - continue - } - const newParameterTable = { ...parametersTable, ...{ [name]: parameter } } - Object.assign(updatedParameterTable, newParameterTable) - } - } - Object.assign(parametersTable, updatedParameterTable) - } - requestParametersList() - }) - const controllerStore = useControllerStore() - const currentControllerState = ref() - const currentProtocolMapping = ref() - const updateCurrentControllerState = (newState: JoystickState, newMapping: ProtocolControllerMapping): void => { - currentControllerState.value = newState - currentProtocolMapping.value = newMapping - } - controllerStore.registerControllerUpdateCallback(updateCurrentControllerState) + const mavlinkManualControlManager = new MavlinkManualControlManager() + const cockpitActionsManager = new CockpitActionsManager() + controllerStore.registerControllerUpdateCallback(mavlinkManualControlManager.updateControllerData) + controllerStore.registerControllerUpdateCallback(cockpitActionsManager.updateControllerData) // Loop to send MAVLink Manual Control messages setInterval(() => { - if (!currentControllerState.value || !currentProtocolMapping.value || controllerStore.joysticks.size === 0) return - const newControllerState = new MavlinkControllerState(currentControllerState.value, currentProtocolMapping.value) + if (!mainVehicle.value) return + + // Set the manager vehicle instance if yet undefined + if (mavlinkManualControlManager.vehicle === undefined) { + mavlinkManualControlManager.setVehicle(mainVehicle.value as ArduPilot) + } + + // Send MAVLink Manual Control message if (controllerStore.enableForwarding) { - sendManualControl(newControllerState) + mavlinkManualControlManager.sendManualControl() } }, 40) setInterval(() => sendGcsHeartbeat(), 1000) // Loop to send Cockpit Action messages setInterval(() => { - if (!currentControllerState.value || !currentProtocolMapping.value || controllerStore.joysticks.size === 0) return if (controllerStore.enableForwarding) { - sendCockpitActions(currentControllerState.value, currentProtocolMapping.value) + cockpitActionsManager.sendCockpitActions() } }, 10) @@ -419,46 +348,12 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { } as RTCConfiguration) ) - const updateMavlinkButtonsPrettyNames = (): void => { - if (!currentParameters || !parametersTable) return - const newMavlinkButtonsNames: InputWithPrettyName[] = [] - buttonParameterTable.splice(0) - // @ts-ignore: This type is huge. Needs refactoring typing here. - if (parametersTable['BTN0_FUNCTION'] && parametersTable['BTN0_FUNCTION']['Values']) { - // @ts-ignore: This type is huge. Needs refactoring typing here. - Object.entries(parametersTable['BTN0_FUNCTION']['Values']).forEach((param) => { - const rawText = param[1] as string - const formatedText = capitalize(rawText).replace(new RegExp('_', 'g'), ' ') - buttonParameterTable.push({ title: formatedText as string, value: Number(param[0]) }) - }) - Object.entries(currentParameters).forEach((param) => { - if (!param[0].startsWith('BTN') || !param[0].endsWith('_FUNCTION')) return - const buttonId = Number(param[0].replace('BTN', '').replace('_FUNCTION', '')) - const functionName = buttonParameterTable.find((p) => p.value === param[1])?.title - if (functionName === undefined) return - newMavlinkButtonsNames.push({ - input: { protocol: JoystickProtocol.MAVLinkManualControl, value: buttonId }, - prettyName: functionName, - }) - }) - } - if (newMavlinkButtonsNames.isEmpty()) return - let newProtocolButtonsFunctions = controllerStore.availableProtocolButtonFunctions.filter((btn) => { - return btn.input.protocol !== JoystickProtocol.MAVLinkManualControl - }) - newProtocolButtonsFunctions = newProtocolButtonsFunctions.concat(newMavlinkButtonsNames) - controllerStore.availableProtocolButtonFunctions = newProtocolButtonsFunctions - } - - setInterval(() => updateMavlinkButtonsPrettyNames(), 1000) - return { arm, disarm, modesAvailable, setFlightMode, sendGcsHeartbeat, - requestParametersList, configure, fetchMission, uploadMission, @@ -483,9 +378,6 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { isArmed, isVehicleOnline, icon, - parametersTable, - currentParameters, - buttonParameterTable, configurationPages, rtcConfiguration, genericVariables, diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts index f38c43af6..7313b4186 100644 --- a/src/stores/widgetManager.ts +++ b/src/stores/widgetManager.ts @@ -11,7 +11,11 @@ import { widgetProfiles } from '@/assets/defaults' import { miniWidgetsProfile } from '@/assets/defaults' import { getKeyDataFromCockpitVehicleStorage, setKeyDataOnCockpitVehicleStorage } from '@/libs/blueos' import * as Words from '@/libs/funny-name/words' -import { CockpitAction, registerActionCallback, unregisterActionCallback } from '@/libs/joystick/protocols' +import { + availableCockpitActions, + registerActionCallback, + unregisterActionCallback, +} from '@/libs/joystick/protocols/cockpit-actions' import { isEqual } from '@/libs/utils' import type { Point2D, SizeRect2D } from '@/types/general' import type { MiniWidget, MiniWidgetContainer } from '@/types/miniWidgets' @@ -455,7 +459,10 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { selectView(currentProfile.value.views[newIndex]) } const debouncedSelectNextView = useDebounceFn(() => selectNextView(), 10) - const selectNextViewCallbackId = registerActionCallback(CockpitAction.GO_TO_NEXT_VIEW, debouncedSelectNextView) + const selectNextViewCallbackId = registerActionCallback( + availableCockpitActions.go_to_next_view, + debouncedSelectNextView + ) onBeforeUnmount(() => unregisterActionCallback(selectNextViewCallbackId)) const selectPreviousView = (): void => { @@ -463,7 +470,10 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => { selectView(currentProfile.value.views[newIndex]) } const debouncedSelectPreviousView = useDebounceFn(() => selectPreviousView(), 10) - const selectPrevViewCBId = registerActionCallback(CockpitAction.GO_TO_PREVIOUS_VIEW, debouncedSelectPreviousView) + const selectPrevViewCBId = registerActionCallback( + availableCockpitActions.go_to_previous_view, + debouncedSelectPreviousView + ) onBeforeUnmount(() => unregisterActionCallback(selectPrevViewCBId)) // Profile migrations diff --git a/src/types/joystick.ts b/src/types/joystick.ts index 59d9054b2..df24ad02c 100644 --- a/src/types/joystick.ts +++ b/src/types/joystick.ts @@ -5,11 +5,20 @@ import { JoystickModel } from '@/libs/joystick/manager' * Each protocol is expected to have it's own way of doing thing, including mapping, limiting, communicating, etc. */ export enum JoystickProtocol { + CockpitModifierKey = 'cockpit-modifier-key', MAVLinkManualControl = 'mavlink-manual-control', CockpitAction = 'cockpit-action', Other = 'other', } +/** + * Modifier keys + */ +export enum CockpitModifierKeyOption { + regular = 'regular', + shift = 'shift', +} + /** * Current state of joystick inputs */ @@ -24,11 +33,6 @@ export interface JoystickState { axes: (number | undefined)[] } -/** - * Current state of the controller in the protocol POV - */ -export class ProtocolControllerState {} - /** * Joystick abstraction for widget */ @@ -68,43 +72,82 @@ export class Joystick { /** * */ -export interface ProtocolInput { +export interface ProtocolAction { /** - * Protocol which this input is used to + * Protocol that holds the action */ protocol: JoystickProtocol /** - * Value for that input + * Action identification */ - value: number | string + id: string + /** + * Human-readable name for the action + */ + name: string } /** - * Interface that represents the necessary information for mapping a Gamepad API controller to a specific protocol. + * Correspondency between the hardware axis input and the protocol action that should be triggered by it */ -export interface ProtocolControllerMapping { +export type JoystickAxisActionCorrespondency = { /** - * Name to help identification of a mapping profile + * The ID of the axis that holds the correspondent action */ - name: string + [key in JoystickAxis]: { + /** + * The protocol action that should be triggered + */ + action: ProtocolAction + /** + * The + */ + min: number + /** + * Maximum axis value + */ + max: number + } +} + +/** + * Correspondency between the hardware button input and the protocol action that should be triggered by it + */ +export type JoystickButtonActionCorrespondency = { /** - * Values to which each Gamepad API axis state of -1 should be mapped to + * The ID of the button that holds the correspondent action */ - axesMins: number[] + [key in JoystickButton]: { + /** + * The protocol action that should be triggered + */ + action: ProtocolAction + } +} + +/** + * Interface that represents the necessary information for mapping a Gamepad API controller to a specific protocol. + */ +export interface JoystickProtocolActionsMapping { /** - * Values to which each Gamepad API axis state of 1 should be mapped to + * Name to help identification of a mapping profile */ - axesMaxs: number[] + name: string /** * Correspondency from Gamepad API to protocol axis. * Corresponds to which Axis in the protocol should the Nth axis be mapped to. */ - axesCorrespondencies: ProtocolInput[] + axesCorrespondencies: JoystickAxisActionCorrespondency /** * Correspondency from Gamepad API to protocol button. * Corresponds to which button in the protocol should the Nth button be mapped to. */ - buttonsCorrespondencies: ProtocolInput[] + buttonsCorrespondencies: { + /** + * Defines the buttons correspondencies for each modifier key + */ + [key in CockpitModifierKeyOption]: JoystickButtonActionCorrespondency + } } export type CockpitButton = null | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 // eslint-disable-line @@ -202,7 +245,31 @@ export interface JoystickInput { */ type: InputType.Axis | InputType.Button /** - * Input value + * Input identification + */ + id: JoystickAxis | JoystickButton +} + +/** + * Joystick button input + */ +export class JoystickButtonInput implements JoystickInput { + readonly type = InputType.Button + /** + * Create an axis input + * @param {JoystickAxis} id Axis identification + */ + constructor(public id: JoystickButton) {} +} + +/** + * Joystick axis input + */ +export class JoystickAxisInput implements JoystickInput { + readonly type = InputType.Axis + /** + * Create an axis input + * @param {JoystickAxis} id Axis identification */ - value: JoystickAxis | JoystickButton + constructor(public id: JoystickAxis) {} } diff --git a/src/views/ConfigurationJoystickView.vue b/src/views/ConfigurationJoystickView.vue index 9175e9fb1..9c0032bc1 100644 --- a/src/views/ConfigurationJoystickView.vue +++ b/src/views/ConfigurationJoystickView.vue @@ -32,16 +32,23 @@

Could not stablish communication with the vehicle.

Button functions will appear as numbers. If connection is restablished, function names will appear.

+
+ +

{{ joystick.model }} controller

-
+
@@ -106,6 +112,7 @@ Update mapping
-
-
- {{ - [JoystickAxis.A0, JoystickAxis.A2].includes(Number(input.value)) - ? 'mdi-pan-horizontal' - : 'mdi-pan-vertical' +
+
+ + {{ + [JoystickAxis.A0, JoystickAxis.A2].includes(input.id) ? 'mdi-pan-horizontal' : 'mdi-pan-vertical' }}
-
+
Calibrate

@@ -198,27 +205,23 @@

Assign
-
+
{{ protocol }}
@@ -234,32 +237,33 @@