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

Sync joystick mapping and functions mapping with vehicle #630

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
106 changes: 98 additions & 8 deletions src/stores/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import Swal from 'sweetalert2'
import { ref } from 'vue'

import { availableGamepadToCockpitMaps, cockpitStandardToProtocols } from '@/assets/joystick-profiles'
import { getKeyDataFromCockpitVehicleStorage, setKeyDataOnCockpitVehicleStorage } from '@/libs/blueos'
import { type JoystickEvent, EventType, joystickManager, JoystickModel } from '@/libs/joystick/manager'
import { allAvailableAxes, allAvailableButtons } from '@/libs/joystick/protocols'
import { modifierKeyActions, otherAvailableActions } from '@/libs/joystick/protocols/other'
import {
type GamepadToCockpitStdMapping,
type JoystickProtocolActionsMapping,
type JoystickState,
type ProtocolAction,
Expand All @@ -24,11 +26,14 @@ export type controllerUpdateCallback = (
activeButtonActions: ProtocolAction[]
) => void

const protocolMappingKey = 'cockpit-protocol-mapping-v4'
const cockpitStdMappingsKey = 'cockpit-standard-mappings'

export const useControllerStore = defineStore('controller', () => {
const joysticks = ref<Map<number, Joystick>>(new Map())
const updateCallbacks = ref<controllerUpdateCallback[]>([])
const protocolMapping = useStorage('cockpit-protocol-mapping-v4', cockpitStandardToProtocols)
const cockpitStdMappings = useStorage('cockpit-standard-mappings', availableGamepadToCockpitMaps)
const protocolMapping = useStorage(protocolMappingKey, cockpitStandardToProtocols)
const cockpitStdMappings = useStorage(cockpitStdMappingsKey, availableGamepadToCockpitMaps)
const availableAxesActions = allAvailableAxes
const availableButtonActions = allAvailableButtons
const enableForwarding = ref(true)
Expand Down Expand Up @@ -128,7 +133,7 @@ export const useControllerStore = defineStore('controller', () => {
})
protocolMapping.value.buttonsCorrespondencies[v.modKey][v.button].action = otherAvailableActions.no_function
})
}, 1000)
}, 500)

// If there's a mapping in our database that is not on the user storage, add it to the user
// This will happen whenever a new joystick profile is added to Cockpit's database
Expand All @@ -137,19 +142,19 @@ export const useControllerStore = defineStore('controller', () => {
cockpitStdMappings.value[k as JoystickModel] = v
})

const downloadJoystickProfile = (joystick: Joystick): void => {
const exportJoystickMapping = (joystick: Joystick): void => {
const blob = new Blob([JSON.stringify(joystick.gamepadToCockpitMap)], { type: 'text/plain;charset=utf-8' })
saveAs(blob, `cockpit-std-profile-joystick-${joystick.model}.json`)
}

const loadJoystickProfile = async (joystick: Joystick, e: Event): Promise<void> => {
const importJoystickMapping = async (joystick: Joystick, e: Event): Promise<void> => {
const reader = new FileReader()
reader.onload = (event: Event) => {
// @ts-ignore: We know the event type and need refactor of the event typing
const contents = event.target.result
const maybeProfile = JSON.parse(contents)
if (!maybeProfile['name'] || !maybeProfile['axes'] || !maybeProfile['buttons']) {
Swal.fire({ icon: 'error', text: 'Invalid profile file.', timer: 3000 })
Swal.fire({ icon: 'error', text: 'Invalid joystick mapping file.', timer: 3000 })
return
}
cockpitStdMappings.value[joystick.model] = maybeProfile
Expand All @@ -158,6 +163,85 @@ export const useControllerStore = defineStore('controller', () => {
reader.readAsText(e.target.files[0])
}

const exportJoysticksMappingsToVehicle = async (
vehicleAddress: string,
joystickMappings: { [key in JoystickModel]: GamepadToCockpitStdMapping }
): Promise<void> => {
await setKeyDataOnCockpitVehicleStorage(vehicleAddress, cockpitStdMappingsKey, joystickMappings)
Swal.fire({ icon: 'success', text: 'Joystick mapping exported to vehicle.', timer: 3000 })
}

const importJoysticksMappingsFromVehicle = async (vehicleAddress: string): Promise<void> => {
const newMapping = await getKeyDataFromCockpitVehicleStorage(vehicleAddress, cockpitStdMappingsKey)
if (!newMapping) {
Swal.fire({ icon: 'error', text: 'No joystick mappings to import from vehicle.', timer: 3000 })
return
}
try {
Object.values(newMapping).forEach((mapping) => {
if (!mapping['name'] || !mapping['axes'] || !mapping['buttons']) {
throw Error('Invalid joystick mapping inside vehicle.')
}
})
} catch (error) {
Swal.fire({ icon: 'error', text: `Could not import joystick mapping from vehicle. ${error}`, timer: 3000 })
return
}

// @ts-ignore: We check for the necessary fields in the if before
cockpitStdMappings.value = newMapping
Swal.fire({ icon: 'success', text: 'Joystick mapping imported from vehicle.', timer: 3000 })
}

const exportFunctionsMapping = (protocolActionsMapping: JoystickProtocolActionsMapping): void => {
const blob = new Blob([JSON.stringify(protocolActionsMapping)], { type: 'text/plain;charset=utf-8' })
saveAs(blob, `cockpit-std-profile-joystick-${protocolActionsMapping.name}.json`)
}

const importFunctionsMapping = async (e: Event): Promise<void> => {
const reader = new FileReader()
reader.onload = (event: Event) => {
// @ts-ignore: We know the event type and need refactor of the event typing
const contents = event.target.result
const maybeFunctionsMapping = JSON.parse(contents)
if (
!maybeFunctionsMapping['name'] ||
!maybeFunctionsMapping['axesCorrespondencies'] ||
!maybeFunctionsMapping['buttonsCorrespondencies']
) {
Swal.fire({ icon: 'error', text: 'Invalid functions mapping file.', timer: 3000 })
return
}
protocolMapping.value = maybeFunctionsMapping
}
// @ts-ignore: We know the event type and need refactor of the event typing
reader.readAsText(e.target.files[0])
}

const exportFunctionsMappingToVehicle = async (
vehicleAddress: string,
functionsMapping: JoystickProtocolActionsMapping
): Promise<void> => {
await setKeyDataOnCockpitVehicleStorage(vehicleAddress, protocolMappingKey, 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
}
// @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 })
}

return {
registerControllerUpdateCallback,
enableForwarding,
Expand All @@ -166,7 +250,13 @@ export const useControllerStore = defineStore('controller', () => {
cockpitStdMappings,
availableAxesActions,
availableButtonActions,
downloadJoystickProfile,
loadJoystickProfile,
exportJoystickMapping,
importJoystickMapping,
exportJoysticksMappingsToVehicle,
importJoysticksMappingsFromVehicle,
exportFunctionsMapping,
importFunctionsMapping,
exportFunctionsMappingToVehicle,
importFunctionsMappingFromVehicle,
}
})
15 changes: 5 additions & 10 deletions src/stores/widgetManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import { type Profile, type View, type Widget, isProfile, isView, WidgetType } f

import { useMainVehicleStore } from './mainVehicle'

const savedProfilesKey = 'cockpit-saved-profiles-v8'

export const useWidgetManagerStore = defineStore('widget-manager', () => {
const vehicleStore = useMainVehicleStore()
const editingMode = ref(false)
const showGrid = ref(true)
const gridInterval = ref(0.01)
const currentMiniWidgetsProfile = useStorage('cockpit-mini-widgets-profile-v4', miniWidgetsProfile)
const savedProfiles = useStorage<Profile[]>('cockpit-saved-profiles-v8', [])
const savedProfiles = useStorage<Profile[]>(savedProfilesKey, [])
const currentViewIndex = useStorage('cockpit-current-view-index', 0)
const currentProfileIndex = useStorage('cockpit-current-profile-index', 0)

Expand Down Expand Up @@ -164,10 +166,7 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
}

const importProfilesFromVehicle = async (): Promise<void> => {
const newProfiles = await getKeyDataFromCockpitVehicleStorage(
vehicleStore.globalAddress,
'cockpit-saved-profiles-v7'
)
const newProfiles = await getKeyDataFromCockpitVehicleStorage(vehicleStore.globalAddress, savedProfilesKey)
if (!Array.isArray(newProfiles) || !newProfiles.every((profile) => isProfile(profile))) {
Swal.fire({ icon: 'error', text: 'Could not import profiles from vehicle. Invalid data.', timer: 3000 })
return
Expand All @@ -177,11 +176,7 @@ export const useWidgetManagerStore = defineStore('widget-manager', () => {
}

const exportProfilesToVehicle = async (): Promise<void> => {
await setKeyDataOnCockpitVehicleStorage(
vehicleStore.globalAddress,
'cockpit-saved-profiles-v7',
savedProfiles.value
)
await setKeyDataOnCockpitVehicleStorage(vehicleStore.globalAddress, savedProfilesKey, savedProfiles.value)
Swal.fire({ icon: 'success', text: 'Cockpit profiles exported to vehicle.', timer: 3000 })
}

Expand Down
105 changes: 83 additions & 22 deletions src/views/ConfigurationJoystickView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,22 +86,73 @@
@click="(e) => setCurrentInputs(joystick, e)"
/>
</div>
<div class="flex">
<button
class="w-auto p-3 m-2 font-medium rounded-md shadow-md text-uppercase"
@click="controllerStore.downloadJoystickProfile(joystick)"
>
Download profile
</button>
<label class="w-auto p-3 m-2 font-medium rounded-md shadow-md cursor-pointer text-uppercase">
<input
type="file"
accept="application/json"
hidden
@change="(e) => controllerStore.loadJoystickProfile(joystick, e)"
/>
Load profile
</label>
<div class="flex items-center justify-evenly">
<div class="flex flex-col items-center max-w-[30%] mb-4">
<span class="mb-2 text-xl font-medium text-slate-500">Joystick mapping</span>
<div class="flex flex-wrap items-center justify-evenly">
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.exportJoystickMapping(joystick)"
>
Export to computer
</button>
<label class="p-2 m-1 font-medium border rounded-md cursor-pointer text-uppercase">
<input
type="file"
accept="application/json"
hidden
@change="(e) => controllerStore.importJoystickMapping(joystick, e)"
/>
Import from computer
</label>
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="
controllerStore.exportJoysticksMappingsToVehicle(globalAddress, controllerStore.cockpitStdMappings)
"
>
Export to vehicle
</button>
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.importJoysticksMappingsFromVehicle(globalAddress)"
>
Import from vehicle
</button>
</div>
</div>
<div class="flex flex-col items-center max-w-[30%] mb-4">
<span class="mb-2 text-xl font-medium text-slate-500">Functions mapping</span>
<div class="flex flex-wrap items-center justify-evenly">
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.exportFunctionsMapping(controllerStore.protocolMapping)"
>
Export to computer
</button>
<label class="p-2 m-1 font-medium border rounded-md cursor-pointer text-uppercase">
<input
type="file"
accept="application/json"
hidden
@change="(e) => controllerStore.importFunctionsMapping(e)"
/>
Import from computer
</label>
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.exportFunctionsMappingToVehicle(globalAddress, controllerStore.protocolMapping)"
>
Export to vehicle
</button>
<button
class="p-2 m-1 font-medium border rounded-md text-uppercase"
@click="controllerStore.importFunctionsMappingFromVehicle(globalAddress)"
>
Import from vehicle
</button>
</div>
</div>
</div>
</div>
</template>
Expand Down Expand Up @@ -139,7 +190,7 @@
<span class="mx-auto text-xl font-bold">{{ protocol }}</span>
<div class="flex flex-col items-center px-2 py-1 overflow-y-auto">
<Button
v-for="action in controllerStore.availableButtonActions.filter((a) => a.protocol === protocol)"
v-for="action in buttonActionsToShow.filter((a) => a.protocol === protocol)"
:key="action.name"
class="w-full my-1 text-sm hover:bg-slate-700"
:class="{
Expand All @@ -161,8 +212,10 @@
{{ buttonFunctionAssignmentFeedback }}
</p>
</Transition>
<div class="w-[90%] h-[2px] my-5 bg-slate-900/20" />
<p class="flex items-center justify-center w-full text-xl font-bold text-slate-600">Axis mapping</p>
<template v-if="currentAxisInputs.length > 0">
<div class="w-[90%] h-[2px] my-5 bg-slate-900/20" />
<p class="flex items-center justify-center w-full text-xl font-bold text-slate-600">Axis mapping</p>
</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' }}
Expand Down Expand Up @@ -203,12 +256,13 @@
</template>

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

import Button from '@/components/Button.vue'
import JoystickPS from '@/components/joysticks/JoystickPS.vue'
import { modifierKeyActions } from '@/libs/joystick/protocols/other'
import { useControllerStore } from '@/stores/controller'
import { useMainVehicleStore } from '@/stores/mainVehicle'
import {
type CockpitButton,
type Joystick,
Expand All @@ -226,6 +280,7 @@ import {
import BaseConfigurationView from './BaseConfigurationView.vue'

const controllerStore = useControllerStore()
const { globalAddress } = useMainVehicleStore()

onMounted(() => {
controllerStore.enableForwarding = false
Expand Down Expand Up @@ -313,8 +368,10 @@ const updateButtonAction = (input: JoystickInput, action: ProtocolAction): void
controllerStore.protocolMapping.buttonsCorrespondencies[currentModifierKey.value.id as CockpitModifierKeyOption][
input.id
].action = action
showJoystickLayout.value = false
setTimeout(() => (showJoystickLayout.value = true), 3000)
setTimeout(() => {
showJoystickLayout.value = false
nextTick(() => (showJoystickLayout.value = true))
}, 1000)
buttonFunctionAssignmentFeedback.value = `Button ${input.id} remapped to function '${action.name}'.`
showButtonFunctionAssignmentFeedback.value = true
setTimeout(() => (showButtonFunctionAssignmentFeedback.value = false), 5000)
Expand Down Expand Up @@ -351,4 +408,8 @@ const buttonRemappingText = computed(() => {
? 'Input remapped.'
: 'No input detected.'
})

const buttonActionsToShow = computed(() =>
controllerStore.availableButtonActions.filter((a) => JSON.stringify(a) !== JSON.stringify(modifierKeyActions.regular))
)
</script>
Loading