Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Nov 8, 2023
1 parent d43f0b1 commit b7bd580
Show file tree
Hide file tree
Showing 12 changed files with 526 additions and 402 deletions.
11 changes: 9 additions & 2 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'))
Expand Down
71 changes: 39 additions & 32 deletions src/assets/joystick-profiles.ts
Original file line number Diff line number Diff line change
@@ -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,
},
}

/**
Expand Down
25 changes: 13 additions & 12 deletions src/components/joysticks/JoystickPS.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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<{
Expand All @@ -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
}
Expand Down Expand Up @@ -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')
Expand Down
186 changes: 12 additions & 174 deletions src/libs/joystick/protocols.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 })),
]
Loading

0 comments on commit b7bd580

Please sign in to comment.