From 9e44d916fb989c41ced9a2cace591d8dadcca63f Mon Sep 17 00:00:00 2001 From: Rafael Araujo Lehmkuhl Date: Tue, 7 Nov 2023 16:50:06 -0300 Subject: [PATCH] WIP --- src/App.vue | 11 +- src/assets/joystick-profiles.ts | 71 ++--- src/components/joysticks/JoystickPS.vue | 25 +- src/libs/joystick/protocols.ts | 186 +------------ .../joystick/protocols/cockpit-actions.ts | 84 ++++++ .../protocols/mavlink-manual-control.ts | 129 +++++++++ src/libs/vehicle/ardupilot/ardupilot.ts | 2 +- src/stores/controller.ts | 17 +- src/stores/mainVehicle.ts | 31 ++- src/stores/widgetManager.ts | 16 +- src/types/joystick.ts | 94 +++++-- src/views/ConfigurationJoystickView.vue | 262 ++++++++---------- 12 files changed, 526 insertions(+), 402 deletions(-) create mode 100644 src/libs/joystick/protocols/cockpit-actions.ts create mode 100644 src/libs/joystick/protocols/mavlink-manual-control.ts diff --git a/src/App.vue b/src/App.vue index 6a05a2f80..648d111cd 100644 --- a/src/App.vue +++ b/src/App.vue @@ -134,7 +134,11 @@ import { import { useRoute } from 'vue-router' import ConfigurationMenu from '@/components/ConfigurationMenu.vue' -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' @@ -160,7 +164,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')) diff --git a/src/assets/joystick-profiles.ts b/src/assets/joystick-profiles.ts index 8747fb1db..6c6b6bc40 100644 --- a/src/assets/joystick-profiles.ts +++ b/src/assets/joystick-profiles.ts @@ -1,38 +1,45 @@ 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 { + mavlinkManualControlAxes, + mavlinkManualControlButtonFunctions, +} from '@/libs/joystick/protocols/mavlink-manual-control' +import { + type GamepadToCockpitStdMapping, + type JoystickProtocolActionsMapping, + 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: { + [JoystickButton.B0]: { action: mavlinkManualControlButtonFunctions.arm }, + [JoystickButton.B1]: { action: mavlinkManualControlButtonFunctions.disarm }, + [JoystickButton.B2]: { action: mavlinkManualControlButtonFunctions.camera_tilt_up }, + [JoystickButton.B3]: { action: mavlinkManualControlButtonFunctions.camera_tilt_down }, + [JoystickButton.B4]: { action: availableCockpitActions.go_to_previous_view }, + [JoystickButton.B5]: { action: availableCockpitActions.go_to_next_view }, + [JoystickButton.B6]: { action: mavlinkManualControlButtonFunctions.pilot_gain_up }, + [JoystickButton.B7]: { action: mavlinkManualControlButtonFunctions.pilot_gain_down }, + [JoystickButton.B8]: { action: mavlinkManualControlButtonFunctions.light_1_up }, + [JoystickButton.B9]: { action: mavlinkManualControlButtonFunctions.light_1_down }, + [JoystickButton.B10]: { action: availableCockpitActions.toggle_full_screen }, + [JoystickButton.B11]: undefined, + [JoystickButton.B12]: undefined, + [JoystickButton.B13]: undefined, + [JoystickButton.B14]: undefined, + [JoystickButton.B15]: undefined, + [JoystickButton.B16]: undefined, + [JoystickButton.B17]: undefined, + }, } /** diff --git a/src/components/joysticks/JoystickPS.vue b/src/components/joysticks/JoystickPS.vue index 6f7b80a03..2e71c576b 100644 --- a/src/components/joysticks/JoystickPS.vue +++ b/src/components/joysticks/JoystickPS.vue @@ -6,9 +6,13 @@ import { v4 as uuid4 } from 'uuid' import { computed, onBeforeUnmount, ref, toRefs, watch } from 'vue' -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' @@ -74,8 +78,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<{ @@ -86,10 +89,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).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 } @@ -171,16 +174,14 @@ watch( () => updateButtonsState() ) -const buttonLabelCorrespondency = toRefs(props).buttonLabelCorrespondency -const protocolMapping = toRefs(props).protocolMapping -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 = props.buttonsActionsCorrespondency[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/libs/joystick/protocols.ts b/src/libs/joystick/protocols.ts index 624e82e20..0a09e0e15 100644 --- a/src/libs/joystick/protocols.ts +++ b/src/libs/joystick/protocols.ts @@ -1,84 +1,11 @@ -import { v4 as uuid4 } from 'uuid' +import { type ProtocolAction, JoystickProtocol } from '@/types/joystick' -import { round, scale } from '@/libs/utils' -import { sequentialArray } from '@/libs/utils' -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 -} +import { availableCockpitActions } from './protocols/cockpit-actions' +import { mavlinkManualControlAxes, mavlinkManualControlButtonFunctions } from './protocols/mavlink-manual-control' /** * 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 @@ -87,102 +14,13 @@ 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', -} - -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 allAvailableAxes: ProtocolAction[] = [ + ...Object.values(mavlinkManualControlAxes), + ...Object.values(OtherProtocol).map((fn) => ({ protocol: JoystickProtocol.Other, id: fn, name: 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 }) -) +export const allAvailableButtons: ProtocolAction[] = [ + ...Object.values(availableCockpitActions), + ...Object.values(mavlinkManualControlButtonFunctions), + ...Object.values(OtherProtocol).map((fn) => ({ protocol: JoystickProtocol.Other, id: fn, name: fn })), +] diff --git a/src/libs/joystick/protocols/cockpit-actions.ts b/src/libs/joystick/protocols/cockpit-actions.ts new file mode 100644 index 000000000..2920ee212 --- /dev/null +++ b/src/libs/joystick/protocols/cockpit-actions.ts @@ -0,0 +1,84 @@ +/* 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, JoystickButton,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', +} + +/** + * 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'), +} + +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: JoystickProtocolActionsMapping): void => { + const actionsToCallback: CockpitAction[] = [] + joystickState.buttons.forEach((state) => { + const mappedButton = mapping.buttonsCorrespondencies[state as JoystickButton] + if (state && mappedButton?.action.protocol === JoystickProtocol.CockpitAction) { + actionsToCallback.push(mappedButton.action as CockpitAction) + } + }) + Object.values(actionsCallbacks).forEach((entry) => { + if (actionsToCallback.includes(entry.action)) { + entry.callback() + } + }) +} \ No newline at end of file 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..c9e50ce43 --- /dev/null +++ b/src/libs/joystick/protocols/mavlink-manual-control.ts @@ -0,0 +1,129 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable vue/max-len */ +/* eslint-disable max-len */ +/* eslint-disable jsdoc/require-jsdoc */ +import { round, scale } from '@/libs/utils' +import { type JoystickProtocolActionsMapping, type JoystickState, type ProtocolAction, InputType, JoystickAxis, JoystickProtocol, ProtocolControllerState } 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 { + arm = 'arm', + disarm = 'disarm', + camera_tilt_up = 'camera_tilt_up', + camera_tilt_down = 'camera_tilt_down', + pilot_gain_up = 'pilot_gain_up', + pilot_gain_down = 'pilot_gain_down', + light_1_up = 'light_1_up', + light_1_down = 'light_1_down', +} + +/** + * 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 +export const mavlinkManualControlButtonFunctions: { [key in MAVLinkButtonFunction]: MAVLinkManualControlButtonAction } = { + [MAVLinkButtonFunction.arm]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.arm, 'Arm'), + [MAVLinkButtonFunction.disarm]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.disarm, 'Disarm'), + [MAVLinkButtonFunction.camera_tilt_up]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.camera_tilt_up, 'Tilt camera up'), + [MAVLinkButtonFunction.camera_tilt_down]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.camera_tilt_down, 'Tilt camera down'), + [MAVLinkButtonFunction.pilot_gain_up]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.pilot_gain_up, 'Pilot gain up'), + [MAVLinkButtonFunction.pilot_gain_down]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.pilot_gain_down, 'Pilot gain down'), + [MAVLinkButtonFunction.light_1_up]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.light_1_up, 'Light 1 up'), + [MAVLinkButtonFunction.light_1_down]: new MAVLinkManualControlButtonAction(MAVLinkButtonFunction.light_1_down, 'Light 1 down'), +} + +// Available actions (axes and buttons) +export const availableMAVLinkManualControlActions = { + ...mavlinkManualControlAxes, + ...mavlinkManualControlButtonFunctions, +} + +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 { JoystickProtocolActionsMapping } 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: JoystickProtocolActionsMapping, target = 1) { + super() + + const isMavlinkAction = (action: ProtocolAction): boolean => action.protocol === JoystickProtocol.MAVLinkManualControl + + const buttons_int = 0 + // for (let i = 0; i < MavlinkControllerState.BUTTONS_PER_BITFIELD; i++) { + // let buttonState = 0 + // mapping.buttonsCorrespondencies.forEach((b, idx) => { + // if (isMavlinkAction(b.action) && Number(b.input.id.split('_')[1] || -1) === i && joystickState.buttons[idx]) { + // buttonState = 1 + // } + // }) + // buttons_int += buttonState * 2 ** i + // } + + // const xCorrespondency = Object.entries(mapping.axesCorrespondencies).find(([, v]) => v.action === mavlinkManualControlAxes.axis_x) + // const yCorrespondency = Object.entries(mapping.axesCorrespondencies).find(([, v]) => v.action === mavlinkManualControlAxes.axis_y) + // const zCorrespondency = Object.entries(mapping.axesCorrespondencies).find(([, v]) => v.action === mavlinkManualControlAxes.axis_z) + // const rCorrespondency = Object.entries(mapping.axesCorrespondencies).find(([, v]) => v.action === mavlinkManualControlAxes.axis_r) + + // this.x = xCorrespondency === undefined ? 0 : round(scale(joystickState.axes[xCorrespondency[0] as JoystickAxis] ?? 0, -1, 1, xCorrespondency.min, xCorrespondency.max), 0) + // this.y = yCorrespondency === undefined ? 0 : round(scale(joystickState.axes[yCorrespondency[0] as JoystickAxis] ?? 0, -1, 1, yCorrespondency.min, yCorrespondency.max), 0) + // this.z = zCorrespondency === undefined ? 0 : round(scale(joystickState.axes[zCorrespondency[0] as JoystickAxis] ?? 0, -1, 1, zCorrespondency.min, zCorrespondency.max), 0) + // this.r = rCorrespondency === undefined ? 0 : round(scale(joystickState.axes[rCorrespondency[0] as JoystickAxis] ?? 0, -1, 1, rCorrespondency.min, rCorrespondency.max), 0) + + this.buttons = buttons_int + this.target = round(target, 0) + } +} + diff --git a/src/libs/vehicle/ardupilot/ardupilot.ts b/src/libs/vehicle/ardupilot/ardupilot.ts index b7624c5ff..0efdedbad 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 { MavlinkControllerState } from '@/libs/joystick/protocols/mavlink-manual-control' import { SignalTyped } from '@/libs/signal' import { round } from '@/libs/utils' import { diff --git a/src/stores/controller.ts b/src/stores/controller.ts index 9ac92c0bf..c2176798b 100644 --- a/src/stores/controller.ts +++ b/src/stores/controller.ts @@ -7,17 +7,20 @@ 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 { type JoystickProtocolActionsMapping, type JoystickState, Joystick } from '@/types/joystick' -export type controllerUpdateCallback = (state: JoystickState, protocolMapping: ProtocolControllerMapping) => void +export type controllerUpdateCallback = ( + state: JoystickState, + protocolActionsMapping: JoystickProtocolActionsMapping +) => 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-v4', 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 => { @@ -91,8 +94,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..3960e07c9 100644 --- a/src/stores/mainVehicle.ts +++ b/src/stores/mainVehicle.ts @@ -9,13 +9,12 @@ 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, registerActionCallback, sendCockpitActions, unregisterActionCallback, -} from '@/libs/joystick/protocols' +} from '@/libs/joystick/protocols/cockpit-actions' +import { MavlinkControllerState } 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' @@ -39,8 +38,9 @@ import * as Vehicle from '@/libs/vehicle/vehicle' import { VehicleFactory } from '@/libs/vehicle/vehicle-factory' import { type MetadataFile } from '@/types/ardupilot-metadata' import { + type JoystickProtocolActionsMapping, type JoystickState, - type ProtocolControllerMapping, + type ProtocolAction, JoystickProtocol, ProtocolControllerState, } from '@/types/joystick' @@ -329,8 +329,8 @@ 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) @@ -376,8 +376,8 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { const controllerStore = useControllerStore() const currentControllerState = ref() - const currentProtocolMapping = ref() - const updateCurrentControllerState = (newState: JoystickState, newMapping: ProtocolControllerMapping): void => { + const currentProtocolMapping = ref() + const updateCurrentControllerState = (newState: JoystickState, newMapping: JoystickProtocolActionsMapping): void => { currentControllerState.value = newState currentProtocolMapping.value = newMapping } @@ -421,7 +421,7 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { const updateMavlinkButtonsPrettyNames = (): void => { if (!currentParameters || !parametersTable) return - const newMavlinkButtonsNames: InputWithPrettyName[] = [] + const newMavlinkButtonsNames: ProtocolAction[] = [] buttonParameterTable.splice(0) // @ts-ignore: This type is huge. Needs refactoring typing here. if (parametersTable['BTN0_FUNCTION'] && parametersTable['BTN0_FUNCTION']['Values']) { @@ -437,17 +437,18 @@ export const useMainVehicleStore = defineStore('main-vehicle', () => { const functionName = buttonParameterTable.find((p) => p.value === param[1])?.title if (functionName === undefined) return newMavlinkButtonsNames.push({ - input: { protocol: JoystickProtocol.MAVLinkManualControl, value: buttonId }, - prettyName: functionName, + protocol: JoystickProtocol.MAVLinkManualControl, + id: buttonId, + name: functionName, }) }) } if (newMavlinkButtonsNames.isEmpty()) return - let newProtocolButtonsFunctions = controllerStore.availableProtocolButtonFunctions.filter((btn) => { - return btn.input.protocol !== JoystickProtocol.MAVLinkManualControl + let newProtocolButtonsFunctions = controllerStore.availableButtonActions.filter((btn) => { + return btn.protocol !== JoystickProtocol.MAVLinkManualControl }) newProtocolButtonsFunctions = newProtocolButtonsFunctions.concat(newMavlinkButtonsNames) - controllerStore.availableProtocolButtonFunctions = newProtocolButtonsFunctions + controllerStore.availableButtonActions = newProtocolButtonsFunctions } setInterval(() => updateMavlinkButtonsPrettyNames(), 1000) diff --git a/src/stores/widgetManager.ts b/src/stores/widgetManager.ts index cdf1a483a..c16013bd1 100644 --- a/src/stores/widgetManager.ts +++ b/src/stores/widgetManager.ts @@ -10,7 +10,11 @@ import { computed, onBeforeMount, onBeforeUnmount, ref, watch } from 'vue' import { widgetProfiles } from '@/assets/defaults' import { miniWidgetsProfile } from '@/assets/defaults' 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' @@ -429,7 +433,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 => { @@ -437,7 +444,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 20ce0af5d..57f4f02b5 100644 --- a/src/types/joystick.ts +++ b/src/types/joystick.ts @@ -68,43 +68,81 @@ 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 | number + /** + * 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 + } + | undefined +} + +/** + * 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 + } + | undefined +} + +/** + * 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: 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 +240,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 5bcf9e7cb..8f2004454 100644 --- a/src/views/ConfigurationJoystickView.vue +++ b/src/views/ConfigurationJoystickView.vue @@ -32,11 +32,7 @@

Could not stablish communication with the vehicle.

@@ -75,8 +71,7 @@ :b15="joystick.state.buttons[15]" :b16="joystick.state.buttons[16]" :b17="joystick.state.buttons[17]" - :protocol-mapping="controllerStore.protocolMapping" - :button-label-correspondency="controllerStore.availableProtocolButtonFunctions" + :buttons-actions-correspondency="controllerStore.protocolMapping.buttonsCorrespondencies" @click="(e) => setCurrentInputs(joystick, e)" />
@@ -130,21 +125,18 @@ :b15="currentJoystick.state.buttons[15]" :b16="currentJoystick.state.buttons[16]" :b17="currentJoystick.state.buttons[17]" - :protocol-mapping="controllerStore.protocolMapping" - :button-label-correspondency="controllerStore.availableProtocolButtonFunctions" + :buttons-actions-correspondency="controllerStore.protocolMapping.buttonsCorrespondencies" />
-
-
- {{ - [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 +193,16 @@

Assign
-
+
{{ protocol }}
@@ -235,25 +219,24 @@