Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Joystick: Support extended MANUAL_CONTROL message (buttons2 + s&t axes) #559

Merged
merged 9 commits into from
Jan 31, 2024
Binary file modified bun.lockb
Binary file not shown.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@
"gamepad.js": "^1.0.4",
"gsap": "^3.11.3",
"haversine-distance": "^1.2.1",
"ky": "^1.2.0",
"leaflet": "1.9.3",
"localforage": "^1.10.0",
"pinia": "^2.0.13",
"roboto-fontface": "*",
"semver": "^7.5.4",
"sweetalert2": "^11.7.1",
"tailwind-scrollbar-hide": "^1.1.7",
"uuid": "^8.3.2",
Expand All @@ -60,6 +62,7 @@
"@types/jsdom": "^16.2.14",
"@types/leaflet": "^1.9.0",
"@types/node": "^17.0.29",
"@types/semver": "^7.5.6",
"@types/uuid": "^8.3.4",
"@types/webfontloader": "^1.0.0",
"@vitejs/plugin-vue": "3.1.0",
Expand Down
50 changes: 35 additions & 15 deletions src/libs/blueos.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import ky from 'ky'

/* eslint-disable @typescript-eslint/no-explicit-any */
export const getBagOfHoldingFromVehicle = async (
vehicleAddress: string,
bagName: string
): Promise<Record<string, any>> => {
try {
const response = await fetch(`http://${vehicleAddress}/bag/v1.0/get/${bagName}`)
if (!(await response.ok)) {
throw new Error(await response.text())
}
return await response.json()
return await ky.get(`http://${vehicleAddress}/bag/v1.0/get/${bagName}`, { timeout: 5000 }).json()
} catch (error) {
throw new Error(`Could not get bag of holdings for ${bagName}. ${error}`)
}
Expand Down Expand Up @@ -36,11 +34,7 @@ export const setBagOfHoldingOnVehicle = async (
bagData: Record<string, any> | any
): Promise<void> => {
try {
await fetch(`http://${vehicleAddress}/bag/v1.0/set/${bagName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bagData),
})
await ky.post(`http://${vehicleAddress}/bag/v1.0/set/${bagName}`, { json: bagData, timeout: 5000 })
} catch (error) {
throw new Error(`Could not set bag of holdings for ${bagName}. ${error}`)
}
Expand Down Expand Up @@ -81,15 +75,41 @@ type IpInfo = { ipv4Address: string; interfaceType: string }

export const getIpsInformationFromVehicle = async (vehicleAddress: string): Promise<IpInfo[]> => {
try {
const response = await fetch(`http://${vehicleAddress}/beacon/v1.0/services`)
if (!(await response.ok)) {
throw new Error(await response.text())
}
const rawIpsInfo: RawIpInfo[] = await response.json()
const url = `http://${vehicleAddress}/beacon/v1.0/services`
const rawIpsInfo: RawIpInfo[] = await ky.get(url, { timeout: 5000 }).json()
return rawIpsInfo
.filter((ipInfo) => ipInfo['service_type'] === '_http')
.map((ipInfo) => ({ ipv4Address: ipInfo.ip, interfaceType: ipInfo.interface_type }))
} catch (error) {
throw new Error(`Could not get information about IPs on BlueOS. ${error}`)
}
}

/* eslint-disable jsdoc/require-jsdoc */
type RawM2rServiceInfo = { name: string; version: string; sha: string; build_date: string; authors: string }
type RawM2rInfo = { version: number; service: RawM2rServiceInfo }
/* eslint-enable jsdoc/require-jsdoc */

export const getMavlink2RestVersion = async (vehicleAddress: string): Promise<string> => {
try {
const url = `http://${vehicleAddress}/mavlink2rest/info`
const m2rRawInfo: RawM2rInfo = await ky.get(url, { timeout: 5000 }).json()
return m2rRawInfo.service.version
} catch (error) {
throw new Error(`Could not get Mavlink2Rest version. ${error}`)
}
}

/* eslint-disable jsdoc/require-jsdoc */
type RawArdupilotFirmwareInfo = { version: string; type: string }
/* eslint-enable jsdoc/require-jsdoc */

export const getArdupilotVersion = async (vehicleAddress: string): Promise<string> => {
try {
const url = `http://${vehicleAddress}/ardupilot-manager/v1.0/firmware_info`
const ardupilotFirmwareRawInfo: RawArdupilotFirmwareInfo = await ky.get(url, { timeout: 5000 }).json()
return ardupilotFirmwareRawInfo.version
} catch (error) {
throw new Error(`Could not get Ardupilot firmware version. ${error}`)
}
}
59 changes: 57 additions & 2 deletions src/libs/joystick/protocols/mavlink-manual-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export enum MAVLinkAxisFunction {
Y = 'axis_y',
Z = 'axis_z',
R = 'axis_r',
S = 'axis_s',
T = 'axis_t',
}

/**
Expand Down Expand Up @@ -143,6 +145,38 @@ export enum MAVLinkManualControlButton {
S14 = 'BTN14_SFUNCTION',
R15 = 'BTN15_FUNCTION',
S15 = 'BTN15_SFUNCTION',
R16 = 'BTN16_FUNCTION',
S16 = 'BTN16_SFUNCTION',
R17 = 'BTN17_FUNCTION',
S17 = 'BTN17_SFUNCTION',
R18 = 'BTN18_FUNCTION',
S18 = 'BTN18_SFUNCTION',
R19 = 'BTN19_FUNCTION',
S19 = 'BTN19_SFUNCTION',
R20 = 'BTN20_FUNCTION',
S20 = 'BTN20_SFUNCTION',
R21 = 'BTN21_FUNCTION',
S21 = 'BTN21_SFUNCTION',
R22 = 'BTN22_FUNCTION',
S22 = 'BTN22_SFUNCTION',
R23 = 'BTN23_FUNCTION',
S23 = 'BTN23_SFUNCTION',
R24 = 'BTN24_FUNCTION',
S24 = 'BTN24_SFUNCTION',
R25 = 'BTN25_FUNCTION',
S25 = 'BTN25_SFUNCTION',
R26 = 'BTN26_FUNCTION',
S26 = 'BTN26_SFUNCTION',
R27 = 'BTN27_FUNCTION',
S27 = 'BTN27_SFUNCTION',
R28 = 'BTN28_FUNCTION',
S28 = 'BTN28_SFUNCTION',
R29 = 'BTN29_FUNCTION',
S29 = 'BTN29_SFUNCTION',
R30 = 'BTN30_FUNCTION',
S30 = 'BTN30_SFUNCTION',
R31 = 'BTN31_FUNCTION',
S31 = 'BTN31_SFUNCTION',
}

const manualControlButtonFromParameterName = (name: string): MAVLinkManualControlButton | undefined => {
Expand Down Expand Up @@ -182,6 +216,8 @@ export const mavlinkManualControlAxes: { [key in MAVLinkAxisFunction]: MAVLinkMa
[MAVLinkAxisFunction.Y]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.Y, 'Axis Y'),
[MAVLinkAxisFunction.Z]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.Z, 'Axis Z'),
[MAVLinkAxisFunction.R]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.R, 'Axis R'),
[MAVLinkAxisFunction.S]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.S, 'Axis S'),
[MAVLinkAxisFunction.T]: new MAVLinkManualControlAxisAction(MAVLinkAxisFunction.T, 'Axis T'),
}

// Available button actions
Expand Down Expand Up @@ -281,7 +317,10 @@ export class MavlinkManualControlState {
y = 0
z = 0
r = 0
s = 0
t = 0
buttons = 0
buttons2 = 0
target = 1
}

Expand Down Expand Up @@ -381,18 +420,34 @@ export class MavlinkManualControlManager {
buttons_int += buttonState * 2 ** i
}

// Calculate buttons2 value
let buttons2_int = 0
for (let i = 16; i < 2 * MavlinkManualControlState.BUTTONS_PER_BITFIELD; i++) {
let buttonState = 0
vehicleButtonsToActivate.forEach((btn) => {
if (btn !== undefined && Number(btn.replace('R', '').replace('S', '')) === i) {
buttonState = 1
}
})
buttons2_int += buttonState * 2 ** (i - 16)
}
// 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)
const sCorrespondency = Object.entries(this.currentActionsMapping.axesCorrespondencies).find((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl && entry[1].action.id === mavlinkManualControlAxes.axis_s.id)
const tCorrespondency = Object.entries(this.currentActionsMapping.axesCorrespondencies).find((entry) => entry[1].action.protocol === JoystickProtocol.MAVLinkManualControl && entry[1].action.id === mavlinkManualControlAxes.axis_t.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.s = sCorrespondency === undefined ? 0 : round(scale(this.joystickState.axes[sCorrespondency[0] as unknown as JoystickAxis] ?? 0, -1, 1, sCorrespondency[1].min, sCorrespondency[1].max), 0)
this.manualControlState.t = tCorrespondency === undefined ? 0 : round(scale(this.joystickState.axes[tCorrespondency[0] as unknown as JoystickAxis] ?? 0, -1, 1, tCorrespondency[1].min, tCorrespondency[1].max), 0)
this.manualControlState.buttons = buttons_int
this.manualControlState.buttons2 = buttons2_int
}

updateVehicleButtonsParameters = (): void => {
Expand Down Expand Up @@ -510,7 +565,7 @@ export class MavlinkManualControlManager {
} else {
const mavlinkActionValue = this.vehicleButtonParameterTable.find((e) => e.title === actionId)
if (mavlinkActionValue === undefined) return
this.vehicle?.setParameter({ id: unnecessaryVehicleRegularButtons[indexRegularButtonToUse].button, value: mavlinkActionValue.value })
this.vehicle?.setParameter({ id: unnecessaryVehicleRegularButtons[unnecessaryVehicleRegularButtons.length - 1].button, value: mavlinkActionValue.value })
}
indexRegularButtonToUse++
})
Expand All @@ -523,7 +578,7 @@ export class MavlinkManualControlManager {
} else {
const mavlinkActionValue = this.vehicleButtonParameterTable.find((e) => e.title === actionId)
if (mavlinkActionValue === undefined) return
this.vehicle?.setParameter({ id: unnecessaryVehicleShiftButtons[indexShiftButtonToUse].button, value: mavlinkActionValue.value })
this.vehicle?.setParameter({ id: unnecessaryVehicleShiftButtons[unnecessaryVehicleShiftButtons.length - 1].button, value: mavlinkActionValue.value })
}
indexShiftButtonToUse++
})
Expand Down
3 changes: 3 additions & 0 deletions src/libs/vehicle/ardupilot/ardupilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,10 @@ export abstract class ArduPilotVehicle<Modes> extends Vehicle.AbstractVehicle<Mo
y: state.y,
z: state.z,
r: state.r,
s: state.s,
t: state.t,
buttons: state.buttons,
buttons2: state.buttons2,
target: 1,
}
this.write(manualControlMessage)
Expand Down
26 changes: 25 additions & 1 deletion src/views/ConfigurationJoystickView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
button controls, as whel as set axis limits.
</p>
</div>
<div
v-if="!m2rSupportsExtendedManualControl || !ardupilotSupportsExtendedManualControl"
class="px-5 py-3 m-1 text-center border-yellow-300 bg-yellow-200/10 border-2 rounded-md max-w-[80%] text-slate-600"
>
<p class="font-semibold">System update is recommended</p>
<br />
<p class="font-medium">
It seems like you're running versions of Mavlink2Rest (BlueOS) and/or ArduPilot that do not support the
extended MAVLink MANUAL_CONTROL message. We strongly suggest upgrading both so you can have support for
additional buttons and axes on the joystick. This is especially important if you sometimes use other control
station software, like QGroundControl, as Cockpit can preferentially use the extended buttons to reduce
configuration clashes. We recommend using BlueOS &ge; 1.2.0, and &ge; version 4.1.2 for ArduPilot-based
autopilot firmware.
</p>
<p />
</div>
<div
v-if="controllerStore.availableButtonActions.every((b) => b.protocol === JoystickProtocol.CockpitAction)"
class="flex flex-col items-center px-5 py-3 m-5 font-bold border rounded-md text-blue-grey-darken-1 bg-blue-lighten-5 w-fit"
Expand Down Expand Up @@ -273,11 +289,13 @@
</template>

<script setup lang="ts">
import semver from 'semver'
import Swal from 'sweetalert2'
import { type Ref, computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'

import Button from '@/components/Button.vue'
import JoystickPS from '@/components/joysticks/JoystickPS.vue'
import { getArdupilotVersion, getMavlink2RestVersion } from '@/libs/blueos'
import { modifierKeyActions } from '@/libs/joystick/protocols/other'
import { useControllerStore } from '@/stores/controller'
import { useMainVehicleStore } from '@/stores/mainVehicle'
Expand All @@ -300,8 +318,14 @@ import BaseConfigurationView from './BaseConfigurationView.vue'
const controllerStore = useControllerStore()
const { globalAddress } = useMainVehicleStore()

onMounted(() => {
const m2rSupportsExtendedManualControl = ref<boolean>()
const ardupilotSupportsExtendedManualControl = ref<boolean>()
onMounted(async () => {
controllerStore.enableForwarding = false
const m2rVersion = await getMavlink2RestVersion(globalAddress)
m2rSupportsExtendedManualControl.value = semver.gte(m2rVersion, '0.11.19')
const ardupilotVersion = await getArdupilotVersion(globalAddress)
ardupilotSupportsExtendedManualControl.value = semver.gte(ardupilotVersion, '4.1.2')
})

onUnmounted(() => {
Expand Down
Loading