Skip to content

Commit

Permalink
joystick-config-view: Add support for mapping with modifier keys
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaellehmkuhl committed Dec 7, 2023
1 parent 0bacabf commit 6fd628f
Showing 1 changed file with 90 additions and 149 deletions.
239 changes: 90 additions & 149 deletions src/views/ConfigurationJoystickView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,31 @@
</p>
</div>
<div
v-if="
controllerStore.availableProtocolButtonFunctions.every(
(b) => b.input.protocol === JoystickProtocol.CockpitAction
)
"
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"
>
<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 class="flex items-center px-5 py-3 m-5 font-bold border rounded-md">
<Button
v-for="key in availableModifierKeys"
:key="key.id"
class="m-2"
:class="{ 'bg-slate-700': currentModifierKey.id === key.id }"
@click="currentModifierKey = key"
>
{{ key.name }}
</Button>
</div>
</div>
<div
v-for="[key, joystick] in controllerStore.joysticks"
:key="key"
class="w-[95%] p-4 shadow-md rounded-2xl flex-centered flex-column position-relative"
>
<p class="text-xl font-semibold text-grey-darken-3">{{ joystick.model }} controller</p>
<div class="flex items-center justify-center w-full">
<div v-if="showJoystickLayout" class="flex flex-col items-center justify-center w-full">
<JoystickPS
class="w-[70%]"
:model="joystick.model"
Expand Down Expand Up @@ -105,6 +112,7 @@
<v-card-title class="flex justify-center w-full">Update mapping</v-card-title>
<v-card-text class="flex justify-between align-center">
<JoystickPS
v-if="showJoystickLayout"
class="w-[50%] p-6"
:model="currentJoystick.model"
:left-axis-horiz="currentJoystick.state.axes[0]"
Expand Down Expand Up @@ -132,17 +140,15 @@
:buttons-actions-correspondency="currentButtonActions"
/>
<div>
<div v-for="(input, i) in currentInputs" :key="i" class="flex flex-col items-center justify-between">
<div v-if="input.type === InputType.Axis" class="flex items-center justify-between ma-2">
<v-icon class="mr-3"
>{{
[JoystickAxis.A0, JoystickAxis.A2].includes(Number(input.value))
? 'mdi-pan-horizontal'
: 'mdi-pan-vertical'
<div class="flex flex-col items-center justify-between">
<div v-for="input in currentAxisInputs" :key="input.id" class="flex items-center justify-between ma-2">
<v-icon class="mr-3">
{{
[JoystickAxis.A0, JoystickAxis.A2].includes(input.id) ? 'mdi-pan-horizontal' : 'mdi-pan-vertical'
}}
</v-icon>
<v-text-field
v-model.number="controllerStore.protocolMapping.axesMins[input.value]"
v-model.number="controllerStore.protocolMapping.axesCorrespondencies[input.id].min"
style="width: 10ch; margin: 5px"
label="Min"
type="number"
Expand All @@ -151,18 +157,17 @@
hide-details
/>
<v-select
:model-value="controllerStore.protocolMapping.axesCorrespondencies[input.value]"
:items="controllerStore.availableProtocolAxesFunctions"
item-title="prettyName"
item-value="input"
v-model="controllerStore.protocolMapping.axesCorrespondencies[input.id].action"
:items="controllerStore.availableAxesActions"
item-title="name"
hide-details
density="compact"
variant="solo"
class="w-40 m-3"
@update:model-value="(newValue: ProtocolInput) => updateMapping(input.value, newValue, input.type)"
return-object
/>
<v-text-field
v-model.number="controllerStore.protocolMapping.axesMaxs[input.value]"
v-model.number="controllerStore.protocolMapping.axesCorrespondencies[input.id].max"
style="width: 10ch; margin: 5px"
label="Max"
type="number"
Expand All @@ -171,7 +176,11 @@
hide-details
/>
</div>
<div v-if="input.type === InputType.Button" class="flex flex-col justify-between p-6 align-center">
<div
v-for="input in currentButtonInputs"
:key="input.id"
class="flex flex-col justify-between p-6 align-center"
>
<div class="flex flex-col items-center justify-between">
<span>Calibrate</span>
<p>
Expand All @@ -196,27 +205,23 @@
<div class="flex flex-col items-center justify-between">
<span>Assign</span>
<div class="flex flex-col flex-wrap">
<div
v-for="[protocol, buttons] in Object.entries(availableProtocolButtonFunctions)"
:key="protocol"
class="flex flex-col m-2"
>
<div v-for="protocol in JoystickProtocol" :key="protocol" class="flex flex-col m-2">
<span class="mb-2 text-xl font-bold">{{ protocol }}</span>
<div class="overflow-y-auto max-h-40 protocol-button-container">
<Button
v-for="buttonFunction in buttons"
:key="buttonFunction.prettyName"
v-for="action in controllerStore.availableButtonActions.filter(
(a) => a.protocol === protocol
)"
:key="action.name"
class="m-1 hover:bg-slate-700"
:class="{
'bg-slate-700':
controllerStore.protocolMapping.buttonsCorrespondencies[input.value].protocol ==
buttonFunction.input.protocol &&
controllerStore.protocolMapping.buttonsCorrespondencies[input.value].value ==
buttonFunction.input.value,
currentButtonActions[input.id].action.protocol == action.protocol &&
currentButtonActions[input.id].action.id == action.id,
}"
@click="updateMapping(input.value, buttonFunction.input, input.type)"
@click="updateButtonAction(input, action)"
>
{{ buttonFunction.prettyName }}
{{ action.name }}
</Button>
</div>
</div>
Expand All @@ -232,32 +237,33 @@
</template>

<script setup lang="ts">
import Swal from 'sweetalert2'
import { computed, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { onMounted } from 'vue'
import { onUnmounted } from 'vue'
import { computed } from 'vue'
import { nextTick } from 'vue'
import Button from '@/components/Button.vue'
import JoystickPS from '@/components/joysticks/JoystickPS.vue'
import { MavParamType } from '@/libs/connection/m2r/messages/mavlink2rest-enum'
import { type InputWithPrettyName, mavlinkAvailableButtons, OtherProtocol } from '@/libs/joystick/protocols'
import type { ArduPilotParameterSetData } from '@/libs/vehicle/ardupilot/types'
import { modifierKeyActions } from '@/libs/joystick/protocols/other'
import { useControllerStore } from '@/stores/controller'
import { useMainVehicleStore } from '@/stores/mainVehicle'
import {
type CockpitButton,
type Joystick,
type JoystickInput,
type ProtocolInput,
type ProtocolAction,
CockpitModifierKeyOption,
InputType,
JoystickAxis,
JoystickAxisInput,
JoystickButton,
JoystickButtonInput,
JoystickProtocol,
} from '@/types/joystick'
import BaseConfigurationView from './BaseConfigurationView.vue'
const controllerStore = useControllerStore()
const vehicleStore = useMainVehicleStore()
onMounted(() => {
controllerStore.enableForwarding = false
Expand All @@ -268,34 +274,26 @@ onUnmounted(() => {
})
const currentJoystick = ref<Joystick>()
const currentInputs = ref()
const currentButtonInputs = ref<JoystickButtonInput[]>([])
const currentAxisInputs = ref<JoystickAxisInput[]>([])
const remappingInput = ref(false)
const justRemappedInput = ref<boolean>()
const inputClickedDialog = ref(false)
const currentModifierKey = ref(modifierKeyActions.regular)
const availableModifierKeys = Object.values(modifierKeyActions)
const showJoystickLayout = ref(true)
watch(inputClickedDialog, () => (justRemappedInput.value = undefined))
const availableProtocolButtonFunctions = computed(() => {
// eslint-disable-next-line jsdoc/require-jsdoc
const organizedButtons: { [key in JoystickProtocol]: InputWithPrettyName[] } = {
[JoystickProtocol.MAVLinkManualControl]: [],
[JoystickProtocol.CockpitAction]: [],
[JoystickProtocol.Other]: [],
}
controllerStore.availableProtocolButtonFunctions.forEach((btn) => organizedButtons[btn.input.protocol].push(btn))
vehicleStore.buttonParameterTable.forEach((btn) => {
if (organizedButtons[JoystickProtocol.MAVLinkManualControl].map((b) => b.prettyName).includes(btn.title)) return
organizedButtons[JoystickProtocol.MAVLinkManualControl].push({
input: { protocol: JoystickProtocol.MAVLinkManualControl, value: btn.title },
prettyName: btn.title,
})
})
return organizedButtons
})
const setCurrentInputs = (joystick: Joystick, inputs: JoystickInput[]): void => {
currentJoystick.value = joystick
currentInputs.value = inputs
currentButtonInputs.value = inputs
.filter((i) => i.type === InputType.Button)
.map((i) => new JoystickButtonInput(i.id as JoystickButton))
currentAxisInputs.value = inputs
.filter((i) => i.type === InputType.Axis)
.map((i) => new JoystickAxisInput(i.id as JoystickAxis))
inputClickedDialog.value = true
}
Expand Down Expand Up @@ -329,101 +327,44 @@ const remapInput = async (joystick: Joystick, input: JoystickInput): Promise<voi
// If a button was pressed, update the mapping of that joystick model in the controller store and return
if (![undefined, -1].includes(pressedButtonIndex)) {
justRemappedInput.value = true
controllerStore.cockpitStdMappings[joystick.model].buttons[input.value] = pressedButtonIndex as CockpitButton
controllerStore.cockpitStdMappings[joystick.model].buttons[input.id] = pressedButtonIndex as CockpitButton
return
}
// If remapping was unsuccessful, indicate it, so we can warn the user
justRemappedInput.value = false
}
/**
* Updates which physical button or axis in the joystick maps to it's correspondent virtual input.
* @param {number} index - The index of the input mapping to update.
* @param {ProtocolInput} newValue - The new value for the input mapping.
* @param {InputType} inputType - The type of input (either Axis or Button).
*/
const updateMapping = (index: number, newValue: ProtocolInput, inputType: InputType): void => {
// Ensure the input type is either a Axis or a Button
if (![InputType.Axis, InputType.Button].includes(inputType)) {
console.error('Input type should be Axis or Button.')
return
}
// Get the current input mapping based on the input type
const oldInputMapping =
inputType === InputType.Axis
? controllerStore.protocolMapping.axesCorrespondencies
: controllerStore.protocolMapping.buttonsCorrespondencies
if (inputType === InputType.Axis) {
// If the input type is an Axis, create a new input mapping, unassigning indexes use to held
// the selected value, so we don't have two axis sending data to the same channel
const undefinedInput = { protocol: JoystickProtocol.Other, value: OtherProtocol.NO_FUNCTION }
const newInputMapping = oldInputMapping.map((oldValue) => {
return oldValue.protocol === newValue.protocol && oldValue.value === newValue.value ? undefinedInput : oldValue
})
// Update the axes correspondences and current protocol mapping
newInputMapping[index] = newValue
controllerStore.protocolMapping.axesCorrespondencies = newInputMapping
} else {
// If the input type is a Button, simply update the value at the specified index
const newInputMapping = oldInputMapping
let newInput = newValue
// When we use an unmapped MAVLink function, we use the same mapping but we have te new function to that button
if (newValue.protocol === JoystickProtocol.MAVLinkManualControl && typeof newValue.value !== 'number') {
const buttonParameterValue = vehicleStore.buttonParameterTable.find((btn) => btn.title === newValue.value)?.value
if (buttonParameterValue === undefined) {
Swal.fire({ text: `Could not find MAVLink parameter ${newValue.value}.`, icon: 'error', timer: 5000 })
return
}
let mavlinkButton: undefined | number = undefined
const usedMavButtons = oldInputMapping
.filter((i) => i.protocol === JoystickProtocol.MAVLinkManualControl)
.map((i) => i.value)
const availableMavButtons = mavlinkAvailableButtons.filter((b) => !usedMavButtons.includes(b))
const oldButtonInput = oldInputMapping[index]
const currentButtonActions = computed(
() => controllerStore.protocolMapping.buttonsCorrespondencies[currentModifierKey.value.id as CockpitModifierKeyOption]
)
if (oldButtonInput.protocol !== JoystickProtocol.MAVLinkManualControl && !availableMavButtons.isEmpty()) {
mavlinkButton = availableMavButtons[0]
} else if (oldButtonInput.protocol === JoystickProtocol.MAVLinkManualControl) {
// Check if there's more than one Cockpit button assigned to this same MAVLink button
const doubleMapped = usedMavButtons.filter((b) => b === oldButtonInput.value).length > 1
if (doubleMapped && !availableMavButtons.isEmpty()) {
// In case there's a double mapping but there are MAVLink buttons still not used, pick one.
mavlinkButton = availableMavButtons[0]
} else if (!doubleMapped) {
// If there's onlyy one Cockpit button mapped to this MAVLink button, use it.
mavlinkButton = oldButtonInput.value as number
}
}
if (mavlinkButton === undefined) {
// If the variable is still undefined, it means we could not find an available MAVLink button to be used, thus we cannot proceed mapping.
const errorMessage = `None of the 16 MAVLink Manual Control buttons are available.
Please assign "No function" to one already used.`
console.error(errorMessage)
Swal.fire({ text: errorMessage, icon: 'error', timer: 5000 })
return
}
newInput = { protocol: JoystickProtocol.MAVLinkManualControl, value: mavlinkButton }
const configurationSettings: ArduPilotParameterSetData = {
id: `BTN${mavlinkButton}_FUNCTION`,
value: buttonParameterValue,
type: { type: MavParamType.MAV_PARAM_TYPE_INT8 },
}
vehicleStore.configure(configurationSettings)
}
const updateButtonAction = (input: JoystickInput, action: ProtocolAction): void => {
controllerStore.protocolMapping.buttonsCorrespondencies[currentModifierKey.value.id as CockpitModifierKeyOption][
input.id
].action = action
showJoystickLayout.value = false
nextTick(() => (showJoystickLayout.value = true))
}
newInputMapping[index] = newInput
controllerStore.protocolMapping.buttonsCorrespondencies = newInputMapping
// Automatically change between modifier key tabs/layouts when they are pressed
watch(controllerStore.joysticks, () => {
if (currentJoystick.value === undefined) {
if (controllerStore.joysticks.size <= 0) return
currentJoystick.value = controllerStore.joysticks.entries().next().value[1]
}
const protocolButtonContainers = document.getElementsByClassName('protocol-button-container')
for (let container of protocolButtonContainers as unknown as HTMLElement[]) {
container.scrollTo({ top: 0, left: 0, behavior: 'smooth' })
const modifierKeysIds = Object.values(modifierKeyActions).map((v) => v.id)
const regularLayout = controllerStore.protocolMapping.buttonsCorrespondencies[CockpitModifierKeyOption.regular]
const activeModKeys = Object.entries(regularLayout)
.filter((v) => currentJoystick.value?.state.buttons[Number(v[0]) as JoystickButton])
.map((v) => v[1].action)
.filter((v) => v.protocol === JoystickProtocol.CockpitModifierKey)
.filter((v) => modifierKeysIds.includes(v.id))
.filter((v) => v !== modifierKeyActions.regular)
if (activeModKeys.isEmpty() && currentModifierKey.value !== modifierKeyActions.regular) {
currentModifierKey.value = modifierKeyActions.regular
return
}
}
currentModifierKey.value = activeModKeys[0]
})
</script>

0 comments on commit 6fd628f

Please sign in to comment.