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

Multi joystick functions profile #667

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 100 additions & 47 deletions src/assets/joystick-profiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,57 +14,110 @@ import {
} from '@/types/joystick'

// TODO: Adjust mapping for PS5 controller
export const cockpitStandardToProtocols: JoystickProtocolActionsMapping = {
name: 'Default ROV functions mapping',
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 },
export const cockpitStandardToProtocols: JoystickProtocolActionsMapping[] = [
{
name: 'ROV functions mapping',
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: modifierKeyActions.shift },
[JoystickButton.B1]: { action: availableMavlinkManualControlButtonFunctions['Mode manual'] },
[JoystickButton.B2]: { action: availableMavlinkManualControlButtonFunctions['Mode depth hold'] },
[JoystickButton.B3]: { action: availableMavlinkManualControlButtonFunctions['Mode stabilize'] },
[JoystickButton.B4]: { action: availableCockpitActions.go_to_previous_view },
[JoystickButton.B5]: { action: availableCockpitActions.go_to_next_view },
[JoystickButton.B6]: { action: availableMavlinkManualControlButtonFunctions['Mount tilt down'] },
[JoystickButton.B7]: { action: availableMavlinkManualControlButtonFunctions['Mount tilt up'] },
[JoystickButton.B8]: { action: availableMavlinkManualControlButtonFunctions['Disarm'] },
[JoystickButton.B9]: { action: availableMavlinkManualControlButtonFunctions['Arm'] },
[JoystickButton.B10]: { action: availableMavlinkManualControlButtonFunctions['Mount center'] },
[JoystickButton.B11]: { action: availableMavlinkManualControlButtonFunctions['Input hold set'] },
[JoystickButton.B12]: { action: availableMavlinkManualControlButtonFunctions['Gain inc'] },
[JoystickButton.B13]: { action: availableMavlinkManualControlButtonFunctions['Gain dec'] },
[JoystickButton.B14]: { action: availableMavlinkManualControlButtonFunctions['Lights1 dimmer'] },
[JoystickButton.B15]: { action: availableMavlinkManualControlButtonFunctions['Lights1 brighter'] },
[JoystickButton.B16]: { action: availableCockpitActions.toggle_bottom_bar },
[JoystickButton.B17]: { action: availableMavlinkManualControlButtonFunctions['Roll pitch toggle'] },
},
[CockpitModifierKeyOption.shift]: {
[JoystickButton.B0]: { action: otherAvailableActions.no_function },
[JoystickButton.B1]: { action: otherAvailableActions.no_function },
[JoystickButton.B2]: { action: availableMavlinkManualControlButtonFunctions['Mode poshold'] },
[JoystickButton.B3]: { action: availableMavlinkManualControlButtonFunctions['Mode acro'] },
[JoystickButton.B4]: { action: otherAvailableActions.no_function },
[JoystickButton.B5]: { action: otherAvailableActions.no_function },
[JoystickButton.B6]: { action: availableMavlinkManualControlButtonFunctions['Servo 1 min'] },
[JoystickButton.B7]: { action: availableMavlinkManualControlButtonFunctions['Servo 1 max'] },
[JoystickButton.B8]: { action: otherAvailableActions.no_function },
[JoystickButton.B9]: { action: otherAvailableActions.no_function },
[JoystickButton.B10]: { action: availableMavlinkManualControlButtonFunctions['Relay 1 toggle'] },
[JoystickButton.B11]: { action: otherAvailableActions.no_function },
[JoystickButton.B12]: { action: availableMavlinkManualControlButtonFunctions['Trim pitch inc'] },
[JoystickButton.B13]: { action: availableMavlinkManualControlButtonFunctions['Trim pitch dec'] },
[JoystickButton.B14]: { action: availableMavlinkManualControlButtonFunctions['Trim roll dec'] },
[JoystickButton.B15]: { action: availableMavlinkManualControlButtonFunctions['Trim roll inc'] },
[JoystickButton.B16]: { action: otherAvailableActions.no_function },
[JoystickButton.B17]: { action: otherAvailableActions.no_function },
},
},
},
buttonsCorrespondencies: {
[CockpitModifierKeyOption.regular]: {
[JoystickButton.B0]: { action: modifierKeyActions.shift },
[JoystickButton.B1]: { action: availableMavlinkManualControlButtonFunctions['Mode manual'] },
[JoystickButton.B2]: { action: availableMavlinkManualControlButtonFunctions['Mode depth hold'] },
[JoystickButton.B3]: { action: availableMavlinkManualControlButtonFunctions['Mode stabilize'] },
[JoystickButton.B4]: { action: availableCockpitActions.go_to_previous_view },
[JoystickButton.B5]: { action: availableCockpitActions.go_to_next_view },
[JoystickButton.B6]: { action: availableMavlinkManualControlButtonFunctions['Mount tilt down'] },
[JoystickButton.B7]: { action: availableMavlinkManualControlButtonFunctions['Mount tilt up'] },
[JoystickButton.B8]: { action: availableMavlinkManualControlButtonFunctions['Disarm'] },
[JoystickButton.B9]: { action: availableMavlinkManualControlButtonFunctions['Arm'] },
[JoystickButton.B10]: { action: availableMavlinkManualControlButtonFunctions['Mount center'] },
[JoystickButton.B11]: { action: availableMavlinkManualControlButtonFunctions['Input hold set'] },
[JoystickButton.B12]: { action: availableMavlinkManualControlButtonFunctions['Gain inc'] },
[JoystickButton.B13]: { action: availableMavlinkManualControlButtonFunctions['Gain dec'] },
[JoystickButton.B14]: { action: availableMavlinkManualControlButtonFunctions['Lights1 dimmer'] },
[JoystickButton.B15]: { action: availableMavlinkManualControlButtonFunctions['Lights1 brighter'] },
[JoystickButton.B16]: { action: availableCockpitActions.toggle_bottom_bar },
[JoystickButton.B17]: { action: availableMavlinkManualControlButtonFunctions['Roll pitch toggle'] },
{
name: 'Boat functions mapping',
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: -1000 },
},
[CockpitModifierKeyOption.shift]: {
[JoystickButton.B0]: { action: otherAvailableActions.no_function },
[JoystickButton.B1]: { action: otherAvailableActions.no_function },
[JoystickButton.B2]: { action: availableMavlinkManualControlButtonFunctions['Mode poshold'] },
[JoystickButton.B3]: { action: availableMavlinkManualControlButtonFunctions['Mode acro'] },
[JoystickButton.B4]: { action: otherAvailableActions.no_function },
[JoystickButton.B5]: { action: otherAvailableActions.no_function },
[JoystickButton.B6]: { action: availableMavlinkManualControlButtonFunctions['Servo 1 min'] },
[JoystickButton.B7]: { action: availableMavlinkManualControlButtonFunctions['Servo 1 max'] },
[JoystickButton.B8]: { action: otherAvailableActions.no_function },
[JoystickButton.B9]: { action: otherAvailableActions.no_function },
[JoystickButton.B10]: { action: availableMavlinkManualControlButtonFunctions['Relay 1 toggle'] },
[JoystickButton.B11]: { action: otherAvailableActions.no_function },
[JoystickButton.B12]: { action: availableMavlinkManualControlButtonFunctions['Trim pitch inc'] },
[JoystickButton.B13]: { action: availableMavlinkManualControlButtonFunctions['Trim pitch dec'] },
[JoystickButton.B14]: { action: availableMavlinkManualControlButtonFunctions['Trim roll dec'] },
[JoystickButton.B15]: { action: availableMavlinkManualControlButtonFunctions['Trim roll inc'] },
[JoystickButton.B16]: { action: otherAvailableActions.no_function },
[JoystickButton.B17]: { action: otherAvailableActions.no_function },
buttonsCorrespondencies: {
[CockpitModifierKeyOption.regular]: {
[JoystickButton.B0]: { action: modifierKeyActions.shift },
[JoystickButton.B1]: { action: otherAvailableActions.no_function },
[JoystickButton.B2]: { action: otherAvailableActions.no_function },
[JoystickButton.B3]: { action: otherAvailableActions.no_function },
[JoystickButton.B4]: { action: availableCockpitActions.go_to_previous_view },
[JoystickButton.B5]: { action: availableCockpitActions.go_to_next_view },
[JoystickButton.B6]: { action: otherAvailableActions.no_function },
[JoystickButton.B7]: { action: otherAvailableActions.no_function },
[JoystickButton.B8]: { action: availableCockpitActions.mavlink_disarm },
[JoystickButton.B9]: { action: availableCockpitActions.mavlink_arm },
[JoystickButton.B10]: { action: otherAvailableActions.no_function },
[JoystickButton.B11]: { action: otherAvailableActions.no_function },
[JoystickButton.B12]: { action: otherAvailableActions.no_function },
[JoystickButton.B13]: { action: otherAvailableActions.no_function },
[JoystickButton.B14]: { action: otherAvailableActions.no_function },
[JoystickButton.B15]: { action: otherAvailableActions.no_function },
[JoystickButton.B16]: { action: availableCockpitActions.toggle_bottom_bar },
[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: otherAvailableActions.no_function },
[JoystickButton.B14]: { action: otherAvailableActions.no_function },
[JoystickButton.B15]: { action: otherAvailableActions.no_function },
[JoystickButton.B16]: { action: otherAvailableActions.no_function },
[JoystickButton.B17]: { action: otherAvailableActions.no_function },
},
},
},
}
]

/**
* Follows the standard controller in the Gamepad API: https://www.w3.org/TR/gamepad/#dfn-standard-gamepad
Expand Down
57 changes: 41 additions & 16 deletions src/stores/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useStorage } from '@vueuse/core'
import { saveAs } from 'file-saver'
import { defineStore } from 'pinia'
import Swal from 'sweetalert2'
import { ref } from 'vue'
import { computed, ref } from 'vue'

import { availableGamepadToCockpitMaps, cockpitStandardToProtocols } from '@/assets/joystick-profiles'
import { getKeyDataFromCockpitVehicleStorage, setKeyDataOnCockpitVehicleStorage } from '@/libs/blueos'
Expand All @@ -26,18 +26,42 @@ export type controllerUpdateCallback = (
activeButtonActions: ProtocolAction[]
) => void

const protocolMappingKey = 'cockpit-protocol-mapping-v5'
const protocolMappingsKey = 'cockpit-protocol-mappings-v1'
const protocolMappingIndexKey = 'cockpit-protocol-mapping-index-v1'
const cockpitStdMappingsKey = 'cockpit-standard-mappings-v2'

export const useControllerStore = defineStore('controller', () => {
const joysticks = ref<Map<number, Joystick>>(new Map())
const updateCallbacks = ref<controllerUpdateCallback[]>([])
const protocolMapping = useStorage(protocolMappingKey, cockpitStandardToProtocols)
const protocolMappings = useStorage(protocolMappingsKey, cockpitStandardToProtocols)
const protocolMappingIndex = useStorage(protocolMappingIndexKey, 0)
const cockpitStdMappings = useStorage(cockpitStdMappingsKey, availableGamepadToCockpitMaps)
const availableAxesActions = allAvailableAxes
const availableButtonActions = allAvailableButtons
const enableForwarding = ref(true)

const protocolMapping = computed<JoystickProtocolActionsMapping>({
get() {
return protocolMappings.value[protocolMappingIndex.value]
},
set(newValue) {
protocolMappings.value[protocolMappingIndex.value] = newValue
},
})

/**
* Change current protocol mapping for given one
* @param { JoystickProtocolActionsMapping } mapping - The functions mapping to be loaded
*/
const loadProtocolMapping = (mapping: JoystickProtocolActionsMapping): void => {
const mappingIndex = protocolMappings.value.findIndex((p) => p.name === mapping.name)
if (mappingIndex === -1) {
Swal.fire({ icon: 'error', text: 'Could not find mapping.', timer: 3000 })
return
}
protocolMappingIndex.value = mappingIndex
}

const registerControllerUpdateCallback = (callback: controllerUpdateCallback): void => {
updateCallbacks.value.push(callback)
}
Expand Down Expand Up @@ -220,36 +244,37 @@ export const useControllerStore = defineStore('controller', () => {

const exportFunctionsMappingToVehicle = async (
vehicleAddress: string,
functionsMapping: JoystickProtocolActionsMapping
functionsMapping: JoystickProtocolActionsMapping[]
): Promise<void> => {
await setKeyDataOnCockpitVehicleStorage(vehicleAddress, protocolMappingKey, functionsMapping)
await setKeyDataOnCockpitVehicleStorage(vehicleAddress, protocolMappingsKey, functionsMapping)
Swal.fire({ icon: 'success', text: 'Joystick functions mapping exported to vehicle.', timer: 3000 })
}

const importFunctionsMappingFromVehicle = async (vehicleAddress: string): Promise<void> => {
const newMapping = await getKeyDataFromCockpitVehicleStorage(vehicleAddress, protocolMappingKey)
if (
!newMapping ||
!newMapping['name'] ||
!newMapping['axesCorrespondencies'] ||
!newMapping['buttonsCorrespondencies']
) {
Swal.fire({ icon: 'error', text: 'Could not import functions mapping from vehicle. Invalid data.', timer: 3000 })
return
const newMappings = await getKeyDataFromCockpitVehicleStorage(vehicleAddress, protocolMappingsKey)
if (!newMappings) {
throw new Error('Could not import functions mapping from vehicle. No data available.')
}

newMappings.forEach((mapping: JoystickProtocolActionsMapping) => {
if (!mapping || !mapping['name'] || !mapping['axesCorrespondencies'] || !mapping['buttonsCorrespondencies']) {
throw new Error('Could not import joystick funtions from vehicle. Invalid data.')
}
})
// @ts-ignore: We check for the necessary fields in the if before
protocolMapping.value = newMapping
Swal.fire({ icon: 'success', text: 'Joystick functions mapping imported from vehicle.', timer: 3000 })
protocolMappings.value = newMappings
}

return {
registerControllerUpdateCallback,
enableForwarding,
joysticks,
protocolMapping,
protocolMappings,
cockpitStdMappings,
availableAxesActions,
availableButtonActions,
loadProtocolMapping,
exportJoystickMapping,
importJoystickMapping,
exportJoysticksMappingsToVehicle,
Expand Down
33 changes: 30 additions & 3 deletions src/views/ConfigurationJoystickView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@
<p>Could not stablish communication with the vehicle.</p>
<p>Button functions will appear as numbers. If connection is restablished, function names will appear.</p>
</div>
<div v-if="availableModifierKeys" class="flex items-center px-5 py-3 m-5 font-bold border rounded-md">
<Button
v-for="functionMapping in controllerStore.protocolMappings"
:key="functionMapping.name"
class="m-2"
:class="{ 'bg-slate-700': controllerStore.protocolMapping.name === functionMapping.name }"
@click="controllerStore.loadProtocolMapping(functionMapping)"
>
{{ functionMapping.name }}
</Button>
Comment on lines +42 to +50
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the buttons would be more intuitive if they said something like "Restore default ROV controls" - general users don't need to know that we're calling them "functions mappings" internally, and the active "Restore" makes it clearer that the current mapping is going to be replaced by a different one, as opposed to the similarly styled modifier buttons directly below which just display different parts of the current controls mapping.

Tangentially related, having buttons for this that look the same as the buttons for the modified profiles (regular/shift) is unintuitive and somewhat unpleasant. That's probably out of scope for this PR though, since the preferred improvement is to change the modified profiles buttons into tabs (like in #617), and the new restore functions can be left as buttons.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with almost everything. The restore I'm not sure about as this is actually not a "restore" button, but a switch between mappings. I agree thought that it is currently very non-intuitive. I want to change this whole interface once we get the needed functionalities on (I think we are only missing the 32 buttons support after this PR).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not actually a "restore" button, but a switch between mappings

That doesn't make sense to me - if it's changing the mappings to a default one (not determined by the user) then I would call that restoring (from their current customised one). If it's just two separate vehicle-type-specific profiles that the user has independent control over then we shouldn't have "default" in the button names.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are determined by the user when he changes it. They have default in the name because they start as default, but I agree with you on this. Will remove the "default" from the name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

</div>
<div class="flex items-center px-5 py-3 m-5 font-bold border rounded-md">
<Button
v-for="key in availableModifierKeys"
Expand Down Expand Up @@ -141,13 +152,15 @@
</label>
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.exportFunctionsMappingToVehicle(globalAddress, controllerStore.protocolMapping)"
@click="
controllerStore.exportFunctionsMappingToVehicle(globalAddress, controllerStore.protocolMappings)
"
>
Export to vehicle
</button>
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.importFunctionsMappingFromVehicle(globalAddress)"
@click="importFunctionsMappingFromVehicle"
>
Import from vehicle
</button>
Expand Down Expand Up @@ -218,7 +231,11 @@
</template>
<div v-for="input in currentAxisInputs" :key="input.id" class="flex items-center justify-between p-2">
<v-icon class="mr-3">
{{ [JoystickAxis.A0, JoystickAxis.A2].includes(input.id) ? 'mdi-pan-horizontal' : 'mdi-pan-vertical' }}
{{
[JoystickAxis.A0, JoystickAxis.A2].includes(Number(input.id))
? 'mdi-pan-horizontal'
: 'mdi-pan-vertical'
}}
</v-icon>
<v-text-field
v-model.number="controllerStore.protocolMapping.axesCorrespondencies[input.id].min"
Expand Down Expand Up @@ -256,6 +273,7 @@
</template>

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

import Button from '@/components/Button.vue'
Expand Down Expand Up @@ -318,6 +336,15 @@ const setCurrentInputs = (joystick: Joystick, inputs: JoystickInput[]): void =>
inputClickedDialog.value = true
}

const importFunctionsMappingFromVehicle = async (): Promise<void> => {
try {
await controllerStore.importFunctionsMappingFromVehicle(globalAddress)
Swal.fire({ icon: 'success', text: 'Joystick functions mappings imported from the vehicle.' })
} catch (error) {
Swal.fire({ icon: 'error', text: `${error}` })
}
}

/**
* Remaps the input of a given joystick. The function waits for a button press on the joystick and then
* updates the mapping to associate the joystick input with the pressed button. If no button is pressed
Expand Down
Loading