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-y': number
'voltage': number
'acc-profile': Mode | '-999'
'gear': Gear | '-999'
'acc-profile': Mode
'gear': Gear
'ebrake': boolean
'reorient': boolean
'gpws': boolean

View file

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

View file

@ -2,6 +2,7 @@
import { Notifications } from '../../Notifications/notifications'
import { settingsStore } from '../../stores/settingsStore'
import AppContainer from '../AppContainer.svelte'
import SettingsSelector from './SettingsSelector.svelte'
import SettingsInput from './SettingsInput.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"
>
<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>
<div class="flex flex-col gap-2">
<SettingsToggle
@ -39,6 +41,12 @@
>
RNG Weight
</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
class="mt-10 px-4 py-2 bg-amber-600 hover:brightness-75 text-medium rounded-lg w-min"
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 accy - Acceleration in y
@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
-->
@ -15,9 +16,9 @@
export let accx: number
export let accy: number
export let orientation: number
export let placeholder: boolean
$: accResolved = Math.hypot(accx, accy)
$: placeholder = accx === -999 && accy === -999
</script>
<div class="flex flex-col gap-2 text-center">

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { T, useTask } from "@threlte/core";
import { ContactShadows, Float, Grid, OrbitControls } from "@threlte/extras";
import Controls from "./Controls.svelte";
import { T, useTask } from '@threlte/core'
import { ContactShadows, Float, Grid, OrbitControls } from '@threlte/extras'
import Controls from './Controls.svelte'
import {
Vector3,
@ -9,15 +9,15 @@
type Group,
type Object3D,
type Object3DEventMap,
} from "three";
} from 'three'
import {
telemetryReadonlyStore,
telemetryStore,
} from "../../stores/telemetryStore";
import { get } from "svelte/store";
import { Vector2 } from "three";
import { SmoothMotionController } from "./smoothMotionController";
import { onMount } from "svelte";
} from '../../stores/telemetryStore'
import { get } from 'svelte/store'
import { Vector2 } from 'three'
import { SmoothMotionController } from './smoothMotionController'
import { onMount } from 'svelte'
/* 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
@ -32,91 +32,91 @@
is the most esoteric and jank code ever written.
*/
let shouldOrbit = true;
let shouldOrbit = true
// CONSTANTS
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 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
// 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
let targetRot = 0;
let targetRot = 0
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
let rot = 0; // (initial) rotation in radians
let angularVelocity = 0;
let rot = 0 // (initial) rotation in radians
let angularVelocity = 0
const updateRotation = (delta: number) => {
let angleDifference = targetRot - rot;
let angleDifference = targetRot - rot
// 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
let desiredVelocity =
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 (Math.abs(angleDifference) < stoppingThreshold) {
desiredVelocity = 0;
desiredVelocity = 0
}
// Adjust angular velocity towards desired velocity
angularVelocity = desiredVelocity;
angularVelocity = desiredVelocity
// Update rotation
rot += angularVelocity * delta;
rot += angularVelocity * delta
// Normalize rot to the range [0, 2π]
if (rot < 0) rot += 2 * Math.PI;
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI;
if (rot < 0) 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
if (Math.abs(angleDifference) < stoppingThreshold) {
rot = targetRot;
angularVelocity = 0;
rot = targetRot
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 initialVelocity = { x: 0, y: 0 }; // Initial velocity
const robotPosition = new Vector2(0, 0) // Initial position
const initialVelocity = { x: 0, y: 0 } // Initial velocity
// The smooth motion controller utilizes a cubic hermite spline to interpolate between
// the current simulation velocity and the robot's actual velocity
const controller = new SmoothMotionController(robotPosition, initialVelocity);
const controller = new SmoothMotionController(robotPosition, initialVelocity)
onMount(() => {
telemetryReadonlyStore.subscribe((value) => {
targetRot = (value["orientation"] * Math.PI) / 180; // convert deg to rad
telemetryReadonlyStore.subscribe(value => {
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
controller.setTargetVelocity({
x: value["chassis-x-speed"],
y: value["chassis-y-speed"],
});
shouldOrbit = value.gear === "park" || value.gear === "-999";
x: value['chassis-x-speed'],
y: value['chassis-y-speed'],
})
// shouldOrbit = value.gear === "park" || value.gear === "-999";
if (shouldOrbit) {
robotPos = new Vector3(0, 0, 0);
controller.reset();
robotPos = new Vector3(0, 0, 0)
controller.reset()
}
});
});
})
})
useTask((delta) => {
useTask(delta => {
if (!shouldOrbit) {
updateRotation(delta);
updateRotation(delta)
controller.update(delta);
robotPos.x = controller.getPosition().x;
robotPos.z = controller.getPosition().y;
controller.update(delta)
robotPos.x = controller.getPosition().x
robotPos.z = controller.getPosition().y
}
});
})
let capsule: Group<Object3DEventMap>;
let capRef: Group<Object3DEventMap>;
let capsule: Group<Object3DEventMap>
let capRef: Group<Object3DEventMap>
$: if (capsule) {
capRef = capsule;
capRef = capsule
}
</script>
@ -145,7 +145,6 @@
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
<!-- <Hornet
position.y={2}
position.z={robotPos.z}

View file

@ -1,15 +1,15 @@
/* 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 {
disableAnnoyances: boolean;
goWoke: boolean;
fastStartup: boolean;
randomWeight: number;
voiceLang: SupportedLanguage;
disableAnnoyances: boolean
goWoke: boolean
fastStartup: boolean
randomWeight: number
voiceLang: SupportedLanguage
}
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
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)
voiceLang: "en",
};
voiceLang: 'en-US',
}
const createSequenceStore = () => {
const { subscribe, set, update } = writable<SettingsStoreData>(defaults);
const { subscribe, set, update } = writable<SettingsStoreData>(defaults)
return {
subscribe,
update: (
data: keyof SettingsStoreData,
newValue: SettingsStoreData[typeof data]
) => {
update((store) => {
update(store => {
// @ts-expect-error
store[data] = newValue;
return store;
});
store[data] = newValue
return store
})
},
reset: () => set(defaults),
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 { settingsStore } from "../stores/settingsStore";
import { get } from 'svelte/store'
import { settingsStore } from '../stores/settingsStore'
/**
* 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
* @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) {
console.log(get(settingsStore).voiceLang);
console.log(get(settingsStore).voiceLang)
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`
}