feat: synced telemetry, basic features done

This commit is contained in:
Youwen Wu 2024-02-23 14:56:35 -08:00
parent 8d7ef7a898
commit a82bc5a04a
20 changed files with 441 additions and 35 deletions

View file

@ -5,8 +5,29 @@
import 'material-symbols' import 'material-symbols'
import AppBar from './lib/Apps/AppBar.svelte' import AppBar from './lib/Apps/AppBar.svelte'
import { appList } from './lib/Apps/appList' import { appList } from './lib/Apps/appList'
import { initializeTelemetry } from './lib/utils/initializeTelemetry'
import { onMount } from 'svelte'
let activeApp: App = 'media-player' let activeApp: App = 'media-player'
let topics: TelemetryTopics = {
doubles: [
'orientation',
'chassis-x-speed',
'chassis-y-speed',
'accx',
'accy',
'accz',
'jerk-x',
'jerk-y',
'voltage',
],
strings: ['acc-profile', 'gear'],
booleans: ['ebrake', 'reorient', 'gpws'],
}
onMount(() => {
initializeTelemetry(topics, 5)
})
</script> </script>
<main class="select-none"> <main class="select-none">

View file

@ -1,4 +1,4 @@
type Gear = 'p' | 'r' | 'n' | 'l' | 'a' | 'd' type Gear = 'park' | 'reverse' | 'neutral' | 'low' | 'auto' | 'drive'
type Mode = 'chill' | 'ludicrous' | 'cruise' type Mode = 'chill' | 'ludicrous' | 'cruise'
@ -18,3 +18,56 @@ interface AppData {
icon: string icon: string
} }
} }
type Polar = {
R: number
theta: number
}
/*
* Represents a network table with various vehicle parameters.
*
* @property orientation - The orientation of the vehicle.
* @property chassis-x-speed - The speed of the vehicle along the x-axis.
* @property chassis-y-speed - The speed of the vehicle along the y-axis.
* @property accx - The acceleration of the vehicle along the x-axis.
* @property accy - The acceleration of the vehicle along the y-axis.
* @property accz - The acceleration of the vehicle along the z-axis.
* @property jerk-x - The jerk of the vehicle along the x-axis.
* @property jerk-y - The jerk of the vehicle along the y-axis.
* @property voltage - The voltage of the vehicle's battery.
* @property acc-profile - The acceleration profile of the vehicle.
* @property gear - The current gear of the vehicle.
*/
interface TelemetryData {
'orientation': number
'chassis-x-speed': number
'chassis-y-speed': number
'accx': number
'accy': number
'accz': number
'jerk-x': number
'jerk-y': number
'voltage': number
'acc-profile': Mode | '-999'
'gear': Gear | '-999'
'ebrake': boolean
'reorient': boolean
'gpws': boolean
}
type CardinalDirection =
| 'North'
| 'Northeast'
| 'East'
| 'Southeast'
| 'South'
| 'Southwest'
| 'West'
| 'Northwest'
interface TelemetryTopics {
doubles: string[]
strings: string[]
booleans: string[]
}

View file

@ -1,3 +1,11 @@
<!--
@component
@param activeApp - Currently selected app
@param appList - List of apps
Displays the app bar, automatically populated from appList. Bind to activeApp and apps will be automagically updated
-->
<script lang="ts"> <script lang="ts">
export let activeApp: App export let activeApp: App
export let appList: AppData export let appList: AppData

View file

@ -1,3 +1,8 @@
<!--
@component
Displays the list of songs
-->
<script lang="ts"> <script lang="ts">
import Song from './Song.svelte' import Song from './Song.svelte'
import { songList } from '../../Dashboard/MediaPlayer/songList' import { songList } from '../../Dashboard/MediaPlayer/songList'
@ -6,6 +11,8 @@
<div <div
class="flex gap-4 w-full py-10 px-10 bg-blue-200 bg-opacity-25 backdrop-blur-xl h-full media-background rounded-3xl flex-wrap" class="flex gap-4 w-full py-10 px-10 bg-blue-200 bg-opacity-25 backdrop-blur-xl h-full media-background rounded-3xl flex-wrap"
> >
<h2 class="text-8xl font-bold basis-full text-slate-200">Music</h2>
<div class="basis-full h-2" />
{#each Object.entries(songList) as [slug, song]} {#each Object.entries(songList) as [slug, song]}
<Song {song} {slug} /> <Song {song} {slug} />
{/each} {/each}

View file

@ -1,3 +1,11 @@
<!--
@component
@param song - Song data
@param slug - Song slug
Displays a song and its metadata
-->
<script lang="ts"> <script lang="ts">
import { musicStore } from '../../stores/musicStore' import { musicStore } from '../../stores/musicStore'

View file

@ -0,0 +1,3 @@
<div class="fixed bottom-0 w-[35vw]">
<slot />
</div>

View file

@ -0,0 +1,29 @@
<!--
@component
@param accx - Acceleration in x
@param accy - Acceleration in y
@param orientation - Heading in degrees
Displays the heading direction and acceleration as human readable text
-->
<script lang="ts">
import { getAcceleration, getDirection } from '../utils/helpers'
import { mpss2knps } from '../utils/unitConversions'
export let accx: number
export let accy: number
export let orientation: number
$: accResolved = Math.hypot(accx, accy)
</script>
<div class="flex flex-col gap-2 text-center transition-all">
<p class="text-xl font-medium">
Heading {getDirection(orientation)} ({orientation}°)
</p>
<p class="text-lg font-medium">
{getAcceleration(accResolved)} ({mpss2knps(accResolved).toFixed(2)}
kn/s)
</p>
</div>

View file

@ -1,24 +1,49 @@
<!--
@component
@param selectedGear - Selected gear
@param selectedMode - Selected mode
@param voltage - Battery voltage
Displays the driver dashboard and HUD
-->
<script lang="ts"> <script lang="ts">
import TopBar from './TopBar/TopBar.svelte' import TopBar from './TopBar/TopBar.svelte'
import Speedometer from './Speedometer.svelte' import Speedometer from './Speedometer.svelte'
import SpeedLimit from './SpeedLimit.svelte' import SpeedLimit from './SpeedLimit.svelte'
import MediaDisplay from './MediaPlayer/MediaDisplay.svelte' import MediaDisplay from './MediaPlayer/MediaDisplay.svelte'
// import Player from './MediaPlayer/Player.svelte' import Compass from './Compass.svelte'
</script> import { telemetryReadonlyStore } from '../stores/telemetryStore'
import Bottom from './Bottom.svelte'
<!-- <Player /> --> $: speedResolved = Math.hypot(
$telemetryReadonlyStore['chassis-x-speed'],
$telemetryReadonlyStore['chassis-y-speed']
)
</script>
<div class="mt-2"> <div class="mt-2">
<div class="px-5"> <div class="px-5">
<TopBar /> <TopBar
selectedGear={$telemetryReadonlyStore.gear}
selectedMode={$telemetryReadonlyStore['acc-profile']}
voltage={$telemetryReadonlyStore.voltage}
/>
<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 /> <Speedometer speed={speedResolved} />
<SpeedLimit /> <SpeedLimit />
</div> </div>
</div> </div>
<div class="fixed bottom-0 w-[35vw]"> <Bottom>
<MediaDisplay /> <div class="mb-10">
<Compass
accx={$telemetryReadonlyStore['accx']}
accy={$telemetryReadonlyStore['accy']}
orientation={$telemetryReadonlyStore['orientation']}
/>
</div> </div>
<MediaDisplay />
</Bottom>
</div> </div>

View file

@ -1,16 +1,14 @@
<!--
@component
@param playing - Whether the music is playing
-->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { musicStore } from '../../stores/musicStore'
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
export let playing = false export let playing = false
let startTime = Date.now()
$: if (playing) {
startTime = Date.now()
}
</script> </script>
<div class="my-auto flex gap-4 mr-4"> <div class="my-auto flex gap-4 mr-4">

View file

@ -1,10 +1,16 @@
<!--
@component
The dashboard's media controller. Automatically updates with the music service.
-->
<script lang="ts"> <script lang="ts">
import Controls from './Controls.svelte' import Controls from './Controls.svelte'
import { musicStore } from '../../stores/musicStore' import { musicStore } from '../../stores/musicStore'
import { songList } from './songList' import { songList } from './songList'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { quintInOut } from 'svelte/easing' import { cubicInOut } from 'svelte/easing'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { flip } from 'svelte/animate'
$: currentSong = $musicStore.queue[$musicStore.currentIndex] $: currentSong = $musicStore.queue[$musicStore.currentIndex]
$: songData = songList[currentSong] $: songData = songList[currentSong]
@ -31,7 +37,7 @@
{#if songData} {#if songData}
<div <div
class="rounded-t-lg bg-neutral-800 px-4 py-2 h-24 flex justify-between" class="rounded-t-lg bg-neutral-800 px-4 py-2 h-24 flex justify-between"
transition:fly={{ y: 100, duration: 300, easing: quintInOut }} transition:fly={{ y: 100, duration: 300, easing: cubicInOut }}
> >
<div class="flex gap-6"> <div class="flex gap-6">
<div class="aspect-square"> <div class="aspect-square">

View file

@ -11,7 +11,7 @@ export const songList: { [key: string]: SongData } = {
src: '/static/songs/deja-vu/audio.m4a', src: '/static/songs/deja-vu/audio.m4a',
coverImg: '/static/songs/deja-vu/cover.jpg', coverImg: '/static/songs/deja-vu/cover.jpg',
}, },
'Xenogenesis': { 'xenogenesis': {
title: 'Xenogenesis', title: 'Xenogenesis',
artist: 'TheFatRat', artist: 'TheFatRat',
src: '/static/songs/xenogenesis/audio.m4a', src: '/static/songs/xenogenesis/audio.m4a',

View file

@ -1,3 +1,10 @@
<!--
@component
@param speedLimit - Speed limit in Miles Per Hour (MPH)
Displays the speed limit
-->
<script lang="ts"> <script lang="ts">
export let speedLimit: number = 5.0 export let speedLimit: number = 5.0
@ -10,9 +17,7 @@
<div <div
class="px-3 py-1 border-black rounded-xl border-2 flex flex-col text-center gap-1" class="px-3 py-1 border-black rounded-xl border-2 flex flex-col text-center gap-1"
> >
<div class="text-lg font-medium"> <div class="text-lg font-medium">SPEED<br />LIMIT</div>
SPEED<br />LIMIT
</div>
<div class="text-2xl font-bold">{formatted}</div> <div class="text-2xl font-bold">{formatted}</div>
</div> </div>
</div> </div>

View file

@ -1,8 +1,17 @@
<!--
@component
@param speed - Speed in meters per second
Displays the speed in miles per hour
-->
<script lang="ts"> <script lang="ts">
// in mph import { mps2mph } from '../utils/unitConversions'
export let speed: number = 0.0 export let speed: number = 0.0
$: formatted = speed.toFixed(1) $: formatted = mps2mph(speed).toFixed(1)
</script> </script>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">

View file

@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
export let selectedGear: Gear export let selectedGear: Gear | '-999'
</script> </script>
<div class="flex justify-center w-full"> <div class="flex justify-center w-full">
<div class="flex flex-row gap-2 text-neutral-400 text-xl font-bold"> <div class="flex flex-row gap-2 text-neutral-400 text-xl font-bold">
<div class:highlighted={selectedGear === 'p'}>P</div> <div class:highlighted={selectedGear === 'park'}>P</div>
<div class:highlighted={selectedGear === 'r'}>R</div> <div class:highlighted={selectedGear === 'reverse'}>R</div>
<div class:highlighted={selectedGear === 'n'}>N</div> <div class:highlighted={selectedGear === 'neutral'}>N</div>
<div class:highlighted={selectedGear === 'l'}>L</div> <div class:highlighted={selectedGear === 'low'}>L</div>
<div class:highlighted={selectedGear === 'a'}>A</div> <div class:highlighted={selectedGear === 'auto'}>A</div>
<div class:highlighted={selectedGear === 'd'}>D</div> <div class:highlighted={selectedGear === 'drive'}>D</div>
</div> </div>
</div> </div>

View file

@ -1,5 +1,12 @@
<!--
@component
@param selectedMode - Selected mode
Displays the drive mode
-->
<script lang="ts"> <script lang="ts">
export let selectedMode: Mode export let selectedMode: Mode | '-999'
let modeText = '' let modeText = ''
@ -13,6 +20,9 @@
case 'ludicrous': case 'ludicrous':
modeText = 'LUDICROUS' modeText = 'LUDICROUS'
break break
case '-999':
modeText = 'DISCONNECTED'
break
} }
</script> </script>

View file

@ -1,17 +1,31 @@
<script> <!--
@component
@param selectedGear - Selected gear
@param selectedMode - Selected mode
@param voltage - Battery voltage
Displays the top bar of the dashboard
-->
<script lang="ts">
import BatteryDisplay from './BatteryDisplay.svelte' import BatteryDisplay from './BatteryDisplay.svelte'
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 selectedMode: Mode | '-999'
export let voltage: number
</script> </script>
<div class="flex flex-row w-full justify-between"> <div class="flex flex-row w-full justify-between">
<div> <div>
<GearSelector selectedGear="p" /> <GearSelector {selectedGear} />
</div> </div>
<div> <div>
<ModeSelector selectedMode="chill" /> <ModeSelector {selectedMode} />
</div> </div>
<div> <div>
<BatteryDisplay voltage={12.5} /> <BatteryDisplay {voltage} />
</div> </div>
</div> </div>

View file

@ -0,0 +1,36 @@
import { writable, readonly } from 'svelte/store'
let defaults: TelemetryData = {
'orientation': -999,
'chassis-x-speed': -999,
'chassis-y-speed': -999,
'accx': -999,
'accy': -999,
'accz': -999,
'jerk-x': -999,
'jerk-y': -999,
'voltage': -999,
'acc-profile': '-999',
'gear': '-999',
'ebrake': false,
'reorient': false,
'gpws': false,
}
const createTelemetryStore = () => {
const { subscribe, set, update } = writable<TelemetryData>(defaults)
return {
subscribe,
update: (data: TelemetryData) => {
update(store => {
if (data !== store) store = data
return store
})
},
reset: () => set(defaults),
}
}
export const telemetryStore = createTelemetryStore()
export const telemetryReadonlyStore = readonly(telemetryStore)

View file

@ -0,0 +1,111 @@
// various utilities to help with displaying data
/**
* Function to filter the network table, replacing any -999 values with default values.
*
* @param table - the network table to be filtered
* @return the filtered network table
*/
export const getAcceleration = (acc: number) => {
if (acc > 0.75) {
return 'Rapidly accelerating'
}
if (acc > 0.25) {
return 'Accelerating'
}
return 'Not accelerating'
}
let defaults: TelemetryData = {
'orientation': 0,
'chassis-x-speed': 0,
'chassis-y-speed': 0,
'accx': 0,
'accy': 0,
'accz': 0,
'jerk-x': 0,
'jerk-y': 0,
'voltage': 12,
'acc-profile': 'chill',
'gear': 'park',
'ebrake': false,
'reorient': false,
'gpws': false,
}
/**
* Function to filter the network table, replacing any -999 values with default values.
*
* @param table - the network table to be filtered
* @return the filtered network table
*/
export const filter = (table: TelemetryData) => {
// if any entry of the table object has value -999, replace with default value
for (let key in table) {
if (
table[key as keyof TelemetryData] === -999 ||
table[key as keyof TelemetryData] === '-999'
) {
;(table[key as keyof TelemetryData] as number | string | boolean) =
defaults[key as keyof TelemetryData]
}
}
return table
}
/**
* Calculate the resultant acceleration and its direction based on the given acceleration components and velocity components.
*
* @param acc_x - The x-component of acceleration
* @param acc_y - The y-component of acceleration
* @param v_x - The x-component of velocity
* @param v_y - The y-component of velocity
* @return An object containing the resultant acceleration (R) and its direction (theta)
*/
export const resolveAcceleration = (
acc_x: number,
acc_y: number,
v_x: number,
v_y: number
): Polar => {
let R = (acc_x * v_x + acc_y * v_y) / Math.hypot(v_x, v_y)
let theta = Math.atan2(v_y, v_x)
return { R, theta }
}
/* SHUT UP SONARLINT */
/**
* Returns the cardinal direction based on the input angle.
*
* @param angle - The input angle in degrees
* @return The cardinal direction based on the input angle
*/
export const getDirection = (angle: number): CardinalDirection => {
if (angle < 0 || angle > 360)
if (angle < 22.5 || angle > 337.5) {
return 'North'
}
if (angle > 22.5 && angle < 67.5) {
return 'Northeast'
}
if (angle > 67.5 && angle < 112.5) {
return 'East'
}
if (angle > 112.5 && angle < 157.5) {
return 'Southeast'
}
if (angle > 157.5 && angle < 202.5) {
return 'South'
}
if (angle > 202.5 && angle < 247.5) {
return 'Southwest'
}
if (angle > 247.5 && angle < 292.5) {
return 'West'
}
if (angle > 292.5 && angle < 337.5) {
return 'Northwest'
}
throw new Error(`Angle ${angle} out of range!`)
}

View file

@ -0,0 +1,44 @@
import { io } from 'socket.io-client'
import { telemetryStore } from '../stores/telemetryStore'
/**
* Connects to sockets and subscribes to specified topics to receive telemetry data.
*
* @param topics - the topics to subscribe to
* @param refreshRate - the refresh rate in Hz to be sent to the backend
* which will be called with the NetworkTable object every time an update is received from the backend.
*/
const onUpdate = (data: TelemetryData) => {
telemetryStore.update(data)
// console.log(data)
}
export const initializeTelemetry = (
topics: TelemetryTopics,
refreshRate: number
) => {
// Make sure refreshRate is valid
if (!Number.isInteger(refreshRate) || refreshRate < 1) {
throw new Error(
'refreshRate must be an integer greater than or equal to 1.'
)
}
const socket = io()
socket.on('connect', () => {
console.log('Socket-IO connected!')
socket.emit('subscribe', topics)
console.log(`Subscribing to topics: ${JSON.stringify(topics)}`)
})
socket.on('subscribed', () => {
console.log('Successfully subscribed to requested topics!')
socket.emit('request_data', { refresh_rate: refreshRate })
console.log(`Refreshing at ${refreshRate} Hz`)
})
socket.on('telemetry_data', (data: string) => {
onUpdate(JSON.parse(data))
})
}

View file

@ -0,0 +1,19 @@
/**
* Convert meters per second to miles per hour.
*
* @param mps - the speed in meters per second
* @return the speed in miles per hour, rounded to one decimal place
*/
export const mps2mph = (mps: number) => {
return mps * 2.23694
}
/**
* Converts meters per second to knots per second.
*
* @param mpss - the value in meters per second
* @return the value in knots per second with 2 decimal places
*/
export const mpss2knps = (mpss: number) => {
return mpss / 0.5144
}