feat: synced telemetry, basic features done
This commit is contained in:
parent
8d7ef7a898
commit
a82bc5a04a
20 changed files with 441 additions and 35 deletions
|
@ -5,8 +5,29 @@
|
|||
import 'material-symbols'
|
||||
import AppBar from './lib/Apps/AppBar.svelte'
|
||||
import { appList } from './lib/Apps/appList'
|
||||
import { initializeTelemetry } from './lib/utils/initializeTelemetry'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
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>
|
||||
|
||||
<main class="select-none">
|
||||
|
|
55
client/src/globals.d.ts
vendored
55
client/src/globals.d.ts
vendored
|
@ -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'
|
||||
|
||||
|
@ -18,3 +18,56 @@ interface AppData {
|
|||
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[]
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
export let activeApp: App
|
||||
export let appList: AppData
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
Displays the list of songs
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Song from './Song.svelte'
|
||||
import { songList } from '../../Dashboard/MediaPlayer/songList'
|
||||
|
@ -6,6 +11,8 @@
|
|||
<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"
|
||||
>
|
||||
<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]}
|
||||
<Song {song} {slug} />
|
||||
{/each}
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
@param song - Song data
|
||||
@param slug - Song slug
|
||||
|
||||
Displays a song and its metadata
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { musicStore } from '../../stores/musicStore'
|
||||
|
||||
|
|
3
client/src/lib/Dashboard/Bottom.svelte
Normal file
3
client/src/lib/Dashboard/Bottom.svelte
Normal file
|
@ -0,0 +1,3 @@
|
|||
<div class="fixed bottom-0 w-[35vw]">
|
||||
<slot />
|
||||
</div>
|
29
client/src/lib/Dashboard/Compass.svelte
Normal file
29
client/src/lib/Dashboard/Compass.svelte
Normal 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>
|
|
@ -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">
|
||||
import TopBar from './TopBar/TopBar.svelte'
|
||||
import Speedometer from './Speedometer.svelte'
|
||||
import SpeedLimit from './SpeedLimit.svelte'
|
||||
import MediaDisplay from './MediaPlayer/MediaDisplay.svelte'
|
||||
// import Player from './MediaPlayer/Player.svelte'
|
||||
</script>
|
||||
import Compass from './Compass.svelte'
|
||||
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="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="mt-8 flex justify-between">
|
||||
<Speedometer />
|
||||
<Speedometer speed={speedResolved} />
|
||||
<SpeedLimit />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 w-[35vw]">
|
||||
<Bottom>
|
||||
<div class="mb-10">
|
||||
<Compass
|
||||
accx={$telemetryReadonlyStore['accx']}
|
||||
accy={$telemetryReadonlyStore['accy']}
|
||||
orientation={$telemetryReadonlyStore['orientation']}
|
||||
/>
|
||||
</div>
|
||||
<MediaDisplay />
|
||||
</div>
|
||||
</Bottom>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
@param playing - Whether the music is playing
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { musicStore } from '../../stores/musicStore'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let playing = false
|
||||
|
||||
let startTime = Date.now()
|
||||
|
||||
$: if (playing) {
|
||||
startTime = Date.now()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="my-auto flex gap-4 mr-4">
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
The dashboard's media controller. Automatically updates with the music service.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Controls from './Controls.svelte'
|
||||
import { musicStore } from '../../stores/musicStore'
|
||||
import { songList } from './songList'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { quintInOut } from 'svelte/easing'
|
||||
import { cubicInOut } from 'svelte/easing'
|
||||
import { onMount } from 'svelte'
|
||||
import { flip } from 'svelte/animate'
|
||||
|
||||
$: currentSong = $musicStore.queue[$musicStore.currentIndex]
|
||||
$: songData = songList[currentSong]
|
||||
|
@ -31,7 +37,7 @@
|
|||
{#if songData}
|
||||
<div
|
||||
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="aspect-square">
|
||||
|
|
|
@ -11,7 +11,7 @@ export const songList: { [key: string]: SongData } = {
|
|||
src: '/static/songs/deja-vu/audio.m4a',
|
||||
coverImg: '/static/songs/deja-vu/cover.jpg',
|
||||
},
|
||||
'Xenogenesis': {
|
||||
'xenogenesis': {
|
||||
title: 'Xenogenesis',
|
||||
artist: 'TheFatRat',
|
||||
src: '/static/songs/xenogenesis/audio.m4a',
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
@param speedLimit - Speed limit in Miles Per Hour (MPH)
|
||||
|
||||
Displays the speed limit
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let speedLimit: number = 5.0
|
||||
|
||||
|
@ -10,9 +17,7 @@
|
|||
<div
|
||||
class="px-3 py-1 border-black rounded-xl border-2 flex flex-col text-center gap-1"
|
||||
>
|
||||
<div class="text-lg font-medium">
|
||||
SPEED<br />LIMIT
|
||||
</div>
|
||||
<div class="text-lg font-medium">SPEED<br />LIMIT</div>
|
||||
<div class="text-2xl font-bold">{formatted}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
@param speed - Speed in meters per second
|
||||
|
||||
Displays the speed in miles per hour
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
// in mph
|
||||
import { mps2mph } from '../utils/unitConversions'
|
||||
|
||||
export let speed: number = 0.0
|
||||
|
||||
$: formatted = speed.toFixed(1)
|
||||
$: formatted = mps2mph(speed).toFixed(1)
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<script lang="ts">
|
||||
export let selectedGear: Gear
|
||||
export let selectedGear: Gear | '-999'
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center w-full">
|
||||
<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 === 'r'}>R</div>
|
||||
<div class:highlighted={selectedGear === 'n'}>N</div>
|
||||
<div class:highlighted={selectedGear === 'l'}>L</div>
|
||||
<div class:highlighted={selectedGear === 'a'}>A</div>
|
||||
<div class:highlighted={selectedGear === 'd'}>D</div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
<!--
|
||||
@component
|
||||
|
||||
@param selectedMode - Selected mode
|
||||
|
||||
Displays the drive mode
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let selectedMode: Mode
|
||||
export let selectedMode: Mode | '-999'
|
||||
|
||||
let modeText = ''
|
||||
|
||||
|
@ -13,6 +20,9 @@
|
|||
case 'ludicrous':
|
||||
modeText = 'LUDICROUS'
|
||||
break
|
||||
case '-999':
|
||||
modeText = 'DISCONNECTED'
|
||||
break
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -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 GearSelector from './GearSelector.svelte'
|
||||
import ModeSelector from './ModeSelector.svelte'
|
||||
|
||||
export let selectedGear: Gear | '-999'
|
||||
export let selectedMode: Mode | '-999'
|
||||
export let voltage: number
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-full justify-between">
|
||||
<div>
|
||||
<GearSelector selectedGear="p" />
|
||||
<GearSelector {selectedGear} />
|
||||
</div>
|
||||
<div>
|
||||
<ModeSelector selectedMode="chill" />
|
||||
<ModeSelector {selectedMode} />
|
||||
</div>
|
||||
<div>
|
||||
<BatteryDisplay voltage={12.5} />
|
||||
<BatteryDisplay {voltage} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
36
client/src/lib/stores/telemetryStore.ts
Normal file
36
client/src/lib/stores/telemetryStore.ts
Normal 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)
|
111
client/src/lib/utils/helpers.ts
Normal file
111
client/src/lib/utils/helpers.ts
Normal 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!`)
|
||||
}
|
44
client/src/lib/utils/initializeTelemetry.ts
Normal file
44
client/src/lib/utils/initializeTelemetry.ts
Normal 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))
|
||||
})
|
||||
}
|
19
client/src/lib/utils/unitConversions.ts
Normal file
19
client/src/lib/utils/unitConversions.ts
Normal 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
|
||||
}
|
Loading…
Reference in a new issue