feat: add locale selector; move audio; detect connectivity

This commit is contained in:
Youwen Wu 2024-02-29 13:48:54 -08:00
parent ada2b49700
commit 84dabc67f2
72 changed files with 183 additions and 123 deletions

View file

@ -49,8 +49,8 @@ interface TelemetryData {
'jerk-x': number 'jerk-x': number
'jerk-y': number 'jerk-y': number
'voltage': number 'voltage': number
'acc-profile': Mode | '-999' 'acc-profile': Mode
'gear': Gear | '-999' 'gear': Gear
'ebrake': boolean 'ebrake': boolean
'reorient': boolean 'reorient': boolean
'gpws': boolean 'gpws': boolean

View file

@ -18,6 +18,7 @@ export const setStationaryTelemetry = () => {
'ebrake': false, 'ebrake': false,
'reorient': false, 'reorient': false,
'gpws': false, 'gpws': false,
'connected': true,
}) })
} }

View file

@ -2,6 +2,7 @@
import { Notifications } from '../../Notifications/notifications' import { Notifications } from '../../Notifications/notifications'
import { settingsStore } from '../../stores/settingsStore' import { settingsStore } from '../../stores/settingsStore'
import AppContainer from '../AppContainer.svelte' import AppContainer from '../AppContainer.svelte'
import SettingsSelector from './SettingsSelector.svelte'
import SettingsInput from './SettingsInput.svelte' import SettingsInput from './SettingsInput.svelte'
import SettingsToggle from './SettingsToggle.svelte' import SettingsToggle from './SettingsToggle.svelte'
@ -20,6 +21,7 @@
class="flex gap-6 bg-blue-200 bg-opacity-25 backdrop-blur-xl media-background rounded-3xl flex-wrap px-10 py-20" class="flex gap-6 bg-blue-200 bg-opacity-25 backdrop-blur-xl media-background rounded-3xl flex-wrap px-10 py-20"
> >
<h1 class="text-5xl font-medium text-slate-100 basis-full">Settings</h1> <h1 class="text-5xl font-medium text-slate-100 basis-full">Settings</h1>
<p class="text-slate-300">Hover over setting names to see helpful tooltips</p>
<h2 class="text-2xl font-medium text-slate-200 mt-4 basis-full">General</h2> <h2 class="text-2xl font-medium text-slate-200 mt-4 basis-full">General</h2>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<SettingsToggle <SettingsToggle
@ -39,6 +41,12 @@
> >
RNG Weight RNG Weight
</SettingsInput> </SettingsInput>
<SettingsSelector
setting="voiceLang"
options={['en-US', 'en-RU']}
tooltip="Selects the language/locale used for Jankboard voice prompts. Does not affect application language (ie. Jankboard itself will always be in English)."
>Voice Prompt Language</SettingsSelector
>
<button <button
class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min" class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min"
on:click={resetSettings}>Reset</button on:click={resetSettings}>Reset</button

View file

@ -0,0 +1,46 @@
<!--
@component
A selector component that updates settings with the selected value. Designed
to be used with settings which have a fixed amount of set values. Only works with
settings with number or string values. Prefer the Toggle input type for boolean
settings.
@param setting - The setting to be toggled
@param options - The options to be shown in the selector. Must be possible (valid)
values for the setting.
@param tooltip - Helpful tooltip for the setting
-->
<script lang="ts">
import { settingsStore } from '../../stores/settingsStore'
import type { SettingsStoreData } from '../../stores/settingsStore'
import { tooltip as tooltipAction } from '@svelte-plugins/tooltips'
export let setting: keyof SettingsStoreData
export let options: string[] | number[]
export let tooltip: string = ''
if (typeof setting !== 'string') {
throw new Error('Selector setting must be a string')
}
let selected: string | number = $settingsStore[setting] as string | number
// Setting is guaranteed to be string by guard clause above
// @ts-expect-error
$: selected && settingsStore.update(setting, selected)
</script>
<div class="flex gap-2 my-1">
<select bind:value={selected} class="w-min bg-slate-400 text-md">
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
<label
class="text-xl font-medium text-slate-100"
for={setting}
use:tooltipAction={{ content: tooltip, action: 'hover', arrow: false }}
><slot /></label
>
</div>

View file

@ -4,6 +4,7 @@
@param accx - Acceleration in x @param accx - Acceleration in x
@param accy - Acceleration in y @param accy - Acceleration in y
@param orientation - Heading in degrees @param orientation - Heading in degrees
@param placeholder - Whether or not to show the placeholder skeleton
Displays the heading direction and acceleration as human readable text Displays the heading direction and acceleration as human readable text
--> -->
@ -15,9 +16,9 @@
export let accx: number export let accx: number
export let accy: number export let accy: number
export let orientation: number export let orientation: number
export let placeholder: boolean
$: accResolved = Math.hypot(accx, accy) $: accResolved = Math.hypot(accx, accy)
$: placeholder = accx === -999 && accy === -999
</script> </script>
<div class="flex flex-col gap-2 text-center"> <div class="flex flex-col gap-2 text-center">

View file

@ -29,11 +29,18 @@
selectedGear={$telemetryReadonlyStore.gear} selectedGear={$telemetryReadonlyStore.gear}
selectedMode={$telemetryReadonlyStore['acc-profile']} selectedMode={$telemetryReadonlyStore['acc-profile']}
voltage={$telemetryReadonlyStore.voltage} voltage={$telemetryReadonlyStore.voltage}
placeholder={!$telemetryReadonlyStore.connected}
/> />
<div class="h-0.5 mt-1 w-full bg-slate-300 border-0"></div> <div class="h-0.5 mt-1 w-full bg-slate-300 border-0"></div>
<div class="mt-8 flex justify-between"> <div class="mt-8 flex justify-between">
<Speedometer speed={speedResolved} /> <Speedometer
<SpeedLimit speedLimit={-999} /> speed={speedResolved}
placeholder={!$telemetryReadonlyStore.connected}
/>
<SpeedLimit
speedLimit={5}
placeholder={!$telemetryReadonlyStore.connected}
/>
</div> </div>
</div> </div>
@ -47,6 +54,7 @@
accx={$telemetryReadonlyStore['accx']} accx={$telemetryReadonlyStore['accx']}
accy={$telemetryReadonlyStore['accy']} accy={$telemetryReadonlyStore['accy']}
orientation={$telemetryReadonlyStore['orientation']} orientation={$telemetryReadonlyStore['orientation']}
placeholder={!$telemetryReadonlyStore.connected}
/> />
</div> </div>
<MediaDisplay /> <MediaDisplay />

View file

@ -2,13 +2,13 @@
@component @component
@param speedLimit - Speed limit in Miles Per Hour (MPH) @param speedLimit - Speed limit in Miles Per Hour (MPH)
@param placeholder - Whether or not to show the placeholder skeleton
Displays the speed limit Displays the speed limit
--> -->
<script lang="ts"> <script lang="ts">
export let speedLimit: number = 5.0 export let speedLimit: number
export let placeholder: boolean
$: placeholder = speedLimit === -999
</script> </script>
<div <div

View file

@ -2,6 +2,7 @@
@component @component
@param speed - Speed in meters per second @param speed - Speed in meters per second
@param placeholder - Whether or not to show the placeholder skeleton
Displays the speed in miles per hour Displays the speed in miles per hour
--> -->
@ -10,10 +11,9 @@
import { mps2mph } from '../utils/unitConversions' import { mps2mph } from '../utils/unitConversions'
export let speed: number = 0.0 export let speed: number = 0.0
export let placeholder: boolean
$: formatted = mps2mph(speed).toFixed(1) $: formatted = mps2mph(speed).toFixed(1)
$: placeholder = speed === Math.hypot(-999, -999)
</script> </script>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
@ -24,10 +24,5 @@
> >
{placeholder ? '-----' : formatted} {placeholder ? '-----' : formatted}
</div> </div>
<div <div class="text-2xl font-medium transition" class:placeholder>MPH</div>
class="text-2xl font-medium transition"
class:placeholder={speed === Math.hypot(-999, -999)}
>
MPH
</div>
</div> </div>

View file

@ -1,9 +1,8 @@
<script lang="ts"> <script lang="ts">
export let voltage: number export let voltage: number
export let placeholder: boolean
$: formatted = voltage.toFixed(1) $: formatted = voltage.toFixed(1)
$: placeholder = voltage === -999
</script> </script>
<span class="flex gap-1"> <span class="flex gap-1">

View file

@ -1,18 +1,19 @@
<script lang="ts"> <script lang="ts">
export let selectedGear: Gear | '-999' export let selectedGear: Gear
export let placeholder: boolean
</script> </script>
<div class="flex justify-center w-full transition"> <div class="flex justify-center w-full transition">
<div <div
class="flex flex-row gap-2 text-neutral-400 text-xl font-bold" class="flex flex-row gap-2 text-neutral-400 text-xl font-bold"
class:placeholder={selectedGear === '-999'} class:placeholder
> >
<div class:highlighted={selectedGear === 'park'}>P</div> <div class:highlighted={selectedGear === 'park' && !placeholder}>P</div>
<div class:highlighted={selectedGear === 'reverse'}>R</div> <div class:highlighted={selectedGear === 'reverse' && !placeholder}>R</div>
<div class:highlighted={selectedGear === 'neutral'}>N</div> <div class:highlighted={selectedGear === 'neutral' && !placeholder}>N</div>
<div class:highlighted={selectedGear === 'low'}>L</div> <div class:highlighted={selectedGear === 'low' && !placeholder}>L</div>
<div class:highlighted={selectedGear === 'auto'}>A</div> <div class:highlighted={selectedGear === 'auto' && !placeholder}>A</div>
<div class:highlighted={selectedGear === 'drive'}>D</div> <div class:highlighted={selectedGear === 'drive' && !placeholder}>D</div>
</div> </div>
</div> </div>

View file

@ -2,17 +2,23 @@
@component @component
@param selectedMode - Selected mode @param selectedMode - Selected mode
@param placeholder - Whether or not to show the placeholder skeleton
Displays the drive mode Displays the drive mode
--> -->
<script lang="ts"> <script lang="ts">
import { fade } from 'svelte/transition' import { fade } from 'svelte/transition'
export let selectedMode: Mode | '-999' export let selectedMode: Mode
export let placeholder: boolean
let modeText = '' let modeText = ''
$: switch (selectedMode) { $: {
if (placeholder) {
modeText = 'DISCONNECTED'
} else {
switch (selectedMode) {
case 'chill': case 'chill':
modeText = 'CHILL' modeText = 'CHILL'
break break
@ -22,9 +28,8 @@
case 'ludicrous': case 'ludicrous':
modeText = 'LUDICROUS' modeText = 'LUDICROUS'
break break
case '-999': }
modeText = 'DISCONNECTED' }
break
} }
</script> </script>

View file

@ -4,6 +4,7 @@
@param selectedGear - Selected gear @param selectedGear - Selected gear
@param selectedMode - Selected mode @param selectedMode - Selected mode
@param voltage - Battery voltage @param voltage - Battery voltage
@param placeholder - Whether or not to show placeholder skeleton UIs
Displays the top bar of the dashboard Displays the top bar of the dashboard
--> -->
@ -13,19 +14,20 @@
import GearSelector from './GearSelector.svelte' import GearSelector from './GearSelector.svelte'
import ModeSelector from './ModeSelector.svelte' import ModeSelector from './ModeSelector.svelte'
export let selectedGear: Gear | '-999' export let selectedGear: Gear
export let selectedMode: Mode | '-999' export let selectedMode: Mode
export let voltage: number export let voltage: number
export let placeholder: boolean
</script> </script>
<div class="flex flex-row w-full justify-between"> <div class="flex flex-row w-full justify-between">
<div> <div>
<GearSelector {selectedGear} /> <GearSelector {selectedGear} {placeholder} />
</div> </div>
<div> <div>
<ModeSelector {selectedMode} /> <ModeSelector {selectedMode} {placeholder} />
</div> </div>
<div> <div>
<BatteryDisplay {voltage} /> <BatteryDisplay {voltage} {placeholder} />
</div> </div>
</div> </div>

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { T, useTask } from "@threlte/core"; import { T, useTask } from '@threlte/core'
import { ContactShadows, Float, Grid, OrbitControls } from "@threlte/extras"; import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
import Controls from "./Controls.svelte"; import Controls from './Controls.svelte'
import { import {
Vector3, Vector3,
@ -9,15 +9,15 @@
type Group, type Group,
type Object3D, type Object3D,
type Object3DEventMap, type Object3DEventMap,
} from "three"; } from 'three'
import { import {
telemetryReadonlyStore, telemetryReadonlyStore,
telemetryStore, telemetryStore,
} from "../../stores/telemetryStore"; } from '../../stores/telemetryStore'
import { get } from "svelte/store"; import { get } from 'svelte/store'
import { Vector2 } from "three"; import { Vector2 } from 'three'
import { SmoothMotionController } from "./smoothMotionController"; import { SmoothMotionController } from './smoothMotionController'
import { onMount } from "svelte"; import { onMount } from 'svelte'
/* This is the root scene where the robot visualization is built. /* This is the root scene where the robot visualization is built.
It renders an infinite grid (it's not actually infinite, but we shouldn't run out It renders an infinite grid (it's not actually infinite, but we shouldn't run out
@ -32,91 +32,91 @@
is the most esoteric and jank code ever written. is the most esoteric and jank code ever written.
*/ */
let shouldOrbit = true; let shouldOrbit = true
// CONSTANTS // CONSTANTS
const maxAngularVelocity = 2; // Max angular velocity, in radians per second const maxAngularVelocity = 2 // Max angular velocity, in radians per second
const stoppingThreshold = 0.005; // Threshold in radians for when to consider the rotation close enough to stop const stoppingThreshold = 0.005 // Threshold in radians for when to consider the rotation close enough to stop
// Proportional control factor // Proportional control factor
const kP = 2; // Adjust this value based on responsiveness and stability needs const kP = 2 // Adjust this value based on responsiveness and stability needs
// Sync robot orientation with target rotation // Sync robot orientation with target rotation
let targetRot = 0; let targetRot = 0
// Updates rotation to match target with PID controller (intended to be invoked in useTask) // Updates rotation to match target with PID controller (intended to be invoked in useTask)
let rot = 0; // (initial) rotation in radians let rot = 0 // (initial) rotation in radians
let angularVelocity = 0; let angularVelocity = 0
const updateRotation = (delta: number) => { const updateRotation = (delta: number) => {
let angleDifference = targetRot - rot; let angleDifference = targetRot - rot
// Normalize angle difference to the range [-π, π] // Normalize angle difference to the range [-π, π]
angleDifference = ((angleDifference + Math.PI) % (2 * Math.PI)) - Math.PI; angleDifference = ((angleDifference + Math.PI) % (2 * Math.PI)) - Math.PI
// Calculate the desired angular velocity based on the angle difference // Calculate the desired angular velocity based on the angle difference
let desiredVelocity = let desiredVelocity =
Math.sign(angleDifference) * Math.sign(angleDifference) *
Math.min(maxAngularVelocity, Math.abs(kP * angleDifference)); Math.min(maxAngularVelocity, Math.abs(kP * angleDifference))
// If the object is very close to the target, adjust the desired velocity to zero to prevent overshooting // If the object is very close to the target, adjust the desired velocity to zero to prevent overshooting
if (Math.abs(angleDifference) < stoppingThreshold) { if (Math.abs(angleDifference) < stoppingThreshold) {
desiredVelocity = 0; desiredVelocity = 0
} }
// Adjust angular velocity towards desired velocity // Adjust angular velocity towards desired velocity
angularVelocity = desiredVelocity; angularVelocity = desiredVelocity
// Update rotation // Update rotation
rot += angularVelocity * delta; rot += angularVelocity * delta
// Normalize rot to the range [0, 2π] // Normalize rot to the range [0, 2π]
if (rot < 0) rot += 2 * Math.PI; if (rot < 0) rot += 2 * Math.PI
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI; else if (rot > 2 * Math.PI) rot -= 2 * Math.PI
// Snap to the target rotation to prevent tiny oscillations if close enough // Snap to the target rotation to prevent tiny oscillations if close enough
if (Math.abs(angleDifference) < stoppingThreshold) { if (Math.abs(angleDifference) < stoppingThreshold) {
rot = targetRot; rot = targetRot
angularVelocity = 0; angularVelocity = 0
}
} }
};
let robotPos: Vector3 = new Vector3(0, 0, 0); let robotPos: Vector3 = new Vector3(0, 0, 0)
const robotPosition = new Vector2(0, 0); // Initial position const robotPosition = new Vector2(0, 0) // Initial position
const initialVelocity = { x: 0, y: 0 }; // Initial velocity const initialVelocity = { x: 0, y: 0 } // Initial velocity
// The smooth motion controller utilizes a cubic hermite spline to interpolate between // The smooth motion controller utilizes a cubic hermite spline to interpolate between
// the current simulation velocity and the robot's actual velocity // the current simulation velocity and the robot's actual velocity
const controller = new SmoothMotionController(robotPosition, initialVelocity); const controller = new SmoothMotionController(robotPosition, initialVelocity)
onMount(() => { onMount(() => {
telemetryReadonlyStore.subscribe((value) => { telemetryReadonlyStore.subscribe(value => {
targetRot = (value["orientation"] * Math.PI) / 180; // convert deg to rad targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
controller.setTargetVelocity({ controller.setTargetVelocity({
x: value["chassis-x-speed"], x: value['chassis-x-speed'],
y: value["chassis-y-speed"], y: value['chassis-y-speed'],
}); })
shouldOrbit = value.gear === "park" || value.gear === "-999"; // shouldOrbit = value.gear === "park" || value.gear === "-999";
if (shouldOrbit) { if (shouldOrbit) {
robotPos = new Vector3(0, 0, 0); robotPos = new Vector3(0, 0, 0)
controller.reset(); controller.reset()
} }
}); })
}); })
useTask((delta) => { useTask(delta => {
if (!shouldOrbit) { if (!shouldOrbit) {
updateRotation(delta); updateRotation(delta)
controller.update(delta); controller.update(delta)
robotPos.x = controller.getPosition().x; robotPos.x = controller.getPosition().x
robotPos.z = controller.getPosition().y; robotPos.z = controller.getPosition().y
} }
}); })
let capsule: Group<Object3DEventMap>; let capsule: Group<Object3DEventMap>
let capRef: Group<Object3DEventMap>; let capRef: Group<Object3DEventMap>
$: if (capsule) { $: if (capsule) {
capRef = capsule; capRef = capsule
} }
</script> </script>
@ -145,7 +145,6 @@
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} /> <ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
<!-- <Hornet <!-- <Hornet
position.y={2} position.y={2}
position.z={robotPos.z} position.z={robotPos.z}

View file

@ -1,15 +1,15 @@
/* stores global app wide settings */ /* stores global app wide settings */
import { writable } from "svelte/store"; import { writable } from 'svelte/store'
type SupportedLanguage = "en" | "rus"; type SupportedLanguage = 'en-US' | 'en-RU'
export interface SettingsStoreData { export interface SettingsStoreData {
disableAnnoyances: boolean; disableAnnoyances: boolean
goWoke: boolean; goWoke: boolean
fastStartup: boolean; fastStartup: boolean
randomWeight: number; randomWeight: number
voiceLang: SupportedLanguage; voiceLang: SupportedLanguage
} }
export const defaults: SettingsStoreData = { export const defaults: SettingsStoreData = {
@ -17,26 +17,26 @@ export const defaults: SettingsStoreData = {
goWoke: false, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences goWoke: false, // go woke (for showing parents or other officials where DEI has taken over), disables "offensive" sequences
fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.) fastStartup: false, // skip the loading splash screen (for development purposes. Setting this from within the app has no effect.)
randomWeight: 1, // the weight of random events (multiplied by the original probability) randomWeight: 1, // the weight of random events (multiplied by the original probability)
voiceLang: "en", voiceLang: 'en-US',
}; }
const createSequenceStore = () => { const createSequenceStore = () => {
const { subscribe, set, update } = writable<SettingsStoreData>(defaults); const { subscribe, set, update } = writable<SettingsStoreData>(defaults)
return { return {
subscribe, subscribe,
update: ( update: (
data: keyof SettingsStoreData, data: keyof SettingsStoreData,
newValue: SettingsStoreData[typeof data] newValue: SettingsStoreData[typeof data]
) => { ) => {
update((store) => { update(store => {
// @ts-expect-error // @ts-expect-error
store[data] = newValue; store[data] = newValue
return store; return store
}); })
}, },
reset: () => set(defaults), reset: () => set(defaults),
set: (data: SettingsStoreData) => set(data), set: (data: SettingsStoreData) => set(data),
}; }
}; }
export const settingsStore = createSequenceStore(); export const settingsStore = createSequenceStore()

View file

@ -1,5 +1,5 @@
import { get } from "svelte/store"; import { get } from 'svelte/store'
import { settingsStore } from "../stores/settingsStore"; import { settingsStore } from '../stores/settingsStore'
/** /**
* Retrieves the voice audio path for the given audio file. * Retrieves the voice audio path for the given audio file.
@ -8,18 +8,13 @@ import { settingsStore } from "../stores/settingsStore";
* @param lang - the language of the audio * @param lang - the language of the audio
* @return the path of the audio file * @return the path of the audio file
*/ */
type SupportedLanguage = "en" | "rus"; type SupportedLanguage = 'en-US' | 'en-RU'
let currentLang = "en";
settingsStore.subscribe((data) => {
currentLang = data.voiceLang;
});
export default function getVoicePath(audio: string, lang?: SupportedLanguage) { export default function getVoicePath(audio: string, lang?: SupportedLanguage) {
console.log(get(settingsStore).voiceLang); console.log(get(settingsStore).voiceLang)
if (!lang) { if (!lang) {
return `/static/voices/${get(settingsStore).voiceLang}/${audio}.wav`; return `/static/voices/${get(settingsStore).voiceLang}/${audio}.wav`
} }
return `/static/voices/${lang}/${audio}.wav`; return `/static/voices/${lang}/${audio}.wav`
} }