commit
a454d3085e
95 changed files with 1181 additions and 537 deletions
21
README.md
21
README.md
|
@ -32,20 +32,23 @@ If you would like to contribute to Jankboard 2, there's only a few simple steps
|
||||||
- If you don't have access to a development environment that supports running standalone executables (eg. Github Codespaces), you can try running `npm run dev` instead of `npm run tauri dev`, which will open a development server at `localhost:5173` with the frontend running in the web. However, this may break at any time as critical functionality is more directly attached to the Rust backend.
|
- If you don't have access to a development environment that supports running standalone executables (eg. Github Codespaces), you can try running `npm run dev` instead of `npm run tauri dev`, which will open a development server at `localhost:5173` with the frontend running in the web. However, this may break at any time as critical functionality is more directly attached to the Rust backend.
|
||||||
- If for some reason you need to install and use the Python backend while we are migrating to Rust, run `poetry install --no-root` in the root directory of the project to install dependencies. You can start the server with `poetry run flask --app app/server.py run --host localhost --port 1280` (it must be running at port `1280` for the frontend to detect it).
|
- If for some reason you need to install and use the Python backend while we are migrating to Rust, run `poetry install --no-root` in the root directory of the project to install dependencies. You can start the server with `poetry run flask --app app/server.py run --host localhost --port 1280` (it must be running at port `1280` for the frontend to detect it).
|
||||||
|
|
||||||
## Current progress
|
## Current progress and improvements over (original) Jankboard
|
||||||
|
|
||||||
- Basic UI layout complete
|
- Layout, toasts/notifications, music player, and app system ported.
|
||||||
- Media player working with a few small issues
|
- Toast and audio cue system is much more robust
|
||||||
- App system working smoothly
|
- Transitions added almost everywhere to make things smoother
|
||||||
- Camera feed likely working
|
- Tauri app created successfully, currently still using Flask backend
|
||||||
- Frontend syncs basic telemetry data with robot through the same Socket-IO code that powered Jankboard v1
|
- Visualization vastly improved with Threlte (Three.js) powered 3D robot simulation
|
||||||
- Notification service installed, with Toast and audio capability
|
- Robot model ported successfully via massive optimization through polygon decimation
|
||||||
|
- Added settings app with options to disable certain features and developer tools for testing
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- Camera cutout overlay
|
- Camera cutout overlay
|
||||||
- Overhaul audio player system
|
- Overhaul audio player system
|
||||||
- Robot visualization (3D, in Threlte).
|
- Overhaul visualization (especially camera)
|
||||||
- Overhaul backend
|
- Overhaul backend in Rust
|
||||||
- Further integrate telemetry (like GPWS, collision warning, etc)
|
- Further integrate telemetry (like GPWS, collision warning, etc)
|
||||||
- Finish re-creating / adding various voice alerts and sequences
|
- Finish re-creating / adding various voice alerts and sequences
|
||||||
|
- Create dynamic voice prompt system to support new languages very easily
|
||||||
|
- Add dynamic voice prompt fallback to support incremental voice prompt migration
|
||||||
|
|
24
client/package-lock.json
generated
24
client/package-lock.json
generated
|
@ -9,8 +9,10 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.0.8",
|
"@fontsource/roboto": "^5.0.8",
|
||||||
|
"@tauri-apps/api": "^1.5.3",
|
||||||
"@threlte/core": "^7.1.0",
|
"@threlte/core": "^7.1.0",
|
||||||
"@threlte/extras": "^8.8.0",
|
"@threlte/extras": "^8.8.0",
|
||||||
|
"camera-controls": "^2.8.3",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
"material-symbols": "^0.15.0",
|
"material-symbols": "^0.15.0",
|
||||||
|
@ -810,6 +812,20 @@
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/api": {
|
||||||
|
"version": "1.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.3.tgz",
|
||||||
|
"integrity": "sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.6.0",
|
||||||
|
"npm": ">= 6.6.0",
|
||||||
|
"yarn": ">= 1.19.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/tauri"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/cli": {
|
"node_modules/@tauri-apps/cli": {
|
||||||
"version": "1.5.10",
|
"version": "1.5.10",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.10.tgz",
|
||||||
|
@ -1300,6 +1316,14 @@
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camera-controls": {
|
||||||
|
"version": "2.8.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-2.8.3.tgz",
|
||||||
|
"integrity": "sha512-zFjqUR6onLkG+z1A6vAWfzovxZxWVSvp6e5t3lfZgfgPZtX3n74aykNAUaoRbq8Y3tOxadHkDjbfGDOP9hFf2w==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"three": ">=0.126.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001588",
|
"version": "1.0.30001588",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001588.tgz",
|
||||||
|
|
|
@ -29,8 +29,10 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/roboto": "^5.0.8",
|
"@fontsource/roboto": "^5.0.8",
|
||||||
|
"@tauri-apps/api": "^1.5.3",
|
||||||
"@threlte/core": "^7.1.0",
|
"@threlte/core": "^7.1.0",
|
||||||
"@threlte/extras": "^8.8.0",
|
"@threlte/extras": "^8.8.0",
|
||||||
|
"camera-controls": "^2.8.3",
|
||||||
"howler": "^2.2.4",
|
"howler": "^2.2.4",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
"material-symbols": "^0.15.0",
|
"material-symbols": "^0.15.0",
|
||||||
|
|
176
client/src-tauri/Cargo.lock
generated
176
client/src-tauri/Cargo.lock
generated
|
@ -66,10 +66,14 @@ checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"network-tables",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -788,6 +792,12 @@ dependencies = [
|
||||||
"syn 2.0.50",
|
"syn 2.0.50",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.30"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.30"
|
version = "0.3.30"
|
||||||
|
@ -802,6 +812,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-macro",
|
"futures-macro",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
|
@ -1184,6 +1195,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iana-time-zone"
|
name = "iana-time-zone"
|
||||||
version = "0.1.60"
|
version = "0.1.60"
|
||||||
|
@ -1528,6 +1545,17 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mio"
|
||||||
|
version = "0.8.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
@ -1556,6 +1584,26 @@ dependencies = [
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "network-tables"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7d34242b4ee3505f5d9f6eeb8cc409cfa1f18a517825c78e6001fe304c3977b"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"parking_lot",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"rmp",
|
||||||
|
"rmp-serde",
|
||||||
|
"rmpv",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -1721,6 +1769,12 @@ dependencies = [
|
||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paste"
|
||||||
|
version = "1.0.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "percent-encoding"
|
name = "percent-encoding"
|
||||||
version = "2.3.1"
|
version = "2.3.1"
|
||||||
|
@ -2142,6 +2196,40 @@ version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp"
|
||||||
|
version = "0.8.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"num-traits",
|
||||||
|
"paste",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp-serde"
|
||||||
|
version = "1.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"rmp",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmpv"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e0e0214a4a2b444ecce41a4025792fc31f77c7bb89c46d253953ea8c65701ec"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
"rmp",
|
||||||
|
"serde",
|
||||||
|
"serde_bytes",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -2247,6 +2335,15 @@ dependencies = [
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_bytes"
|
||||||
|
version = "0.11.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_derive"
|
name = "serde_derive"
|
||||||
version = "1.0.197"
|
version = "1.0.197"
|
||||||
|
@ -2352,6 +2449,17 @@ dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.8"
|
version = "0.10.8"
|
||||||
|
@ -2372,6 +2480,15 @@ dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-registry"
|
||||||
|
version = "1.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "simd-adler32"
|
name = "simd-adler32"
|
||||||
version = "0.3.7"
|
version = "0.3.7"
|
||||||
|
@ -2399,6 +2516,16 @@ version = "1.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "soup2"
|
name = "soup2"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
@ -2895,8 +3022,38 @@ checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
"num_cpus",
|
"num_cpus",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.50",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3037,6 +3194,25 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.13.1",
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror",
|
||||||
|
"url",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
|
|
|
@ -18,6 +18,10 @@ tauri-build = { version = "1.5.1", features = [] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.6.0", features = [] }
|
tauri = { version = "1.6.0", features = [] }
|
||||||
|
tokio = { version = "1.23.0", features = ["full"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||||
|
network-tables = { version = "=0.1.3", features = ["client-v4"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri_build::build()
|
tauri_build::build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,30 @@
|
||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
fn main() {
|
use tauri::Manager;
|
||||||
tauri::Builder::default()
|
mod telemetry;
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
#[derive(Clone, serde::Serialize)]
|
||||||
|
struct Payload {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let rt = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
tauri::Builder::default()
|
||||||
|
.setup(|app| {
|
||||||
|
// create app handle and send it to our event listeners
|
||||||
|
let app_handle = app.app_handle();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
crate::telemetry::subscribe_topics(app_handle.clone()).await;
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("failed to run app")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
75
client/src-tauri/src/telemetry.rs
Normal file
75
client/src-tauri/src/telemetry.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use network_tables::v4::client_config::Config;
|
||||||
|
use network_tables::v4::{Client, SubscriptionOptions};
|
||||||
|
use serde_json::to_string;
|
||||||
|
use std::net::{Ipv4Addr, SocketAddrV4};
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use tokio::time::{sleep, Duration};
|
||||||
|
|
||||||
|
const NTABLE_IP: (u8, u8, u8, u8) = (10, 12, 80, 2);
|
||||||
|
const NTABLE_PORT: u16 = 5810;
|
||||||
|
|
||||||
|
pub async fn subscribe_topics(app_handle: AppHandle) {
|
||||||
|
loop {
|
||||||
|
// I hope this doesn't lead to a catastrophic infinite loop failure
|
||||||
|
let client = loop {
|
||||||
|
match Client::try_new_w_config(
|
||||||
|
SocketAddrV4::new(
|
||||||
|
Ipv4Addr::new(NTABLE_IP.0, NTABLE_IP.1, NTABLE_IP.2, NTABLE_IP.3),
|
||||||
|
NTABLE_PORT,
|
||||||
|
),
|
||||||
|
Config {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(client) => {
|
||||||
|
println!("Client created");
|
||||||
|
app_handle
|
||||||
|
.emit_all("telemetry_connected", "connected")
|
||||||
|
.expect("Failed to emit telemetry_status connected event");
|
||||||
|
break client; // Exit the loop if the client is successfully created
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Failed to create client: {}. Retrying in 3 seconds...", e);
|
||||||
|
app_handle
|
||||||
|
.emit_all("telemetry_status", "disconnected")
|
||||||
|
.expect("Failed to emit telemetry_status disconnected event");
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(3)).await; // Wait for 3 seconds before retrying
|
||||||
|
continue; // Continue the loop to retry
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut subscription = client
|
||||||
|
.subscribe_w_options(
|
||||||
|
&["/SmartDashboard"],
|
||||||
|
Some(SubscriptionOptions {
|
||||||
|
all: Some(true),
|
||||||
|
prefix: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("Failed to subscribe");
|
||||||
|
while let Some(message) = subscription.next().await {
|
||||||
|
let mut modified_message = message.clone();
|
||||||
|
|
||||||
|
if let Some(stripped) = modified_message.topic_name.strip_prefix("/SmartDashboard/") {
|
||||||
|
modified_message.topic_name = stripped.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let json_message = to_string(&modified_message).expect("Failed to serialize message");
|
||||||
|
app_handle
|
||||||
|
.emit_all("telemetry_data", json_message.clone())
|
||||||
|
.expect("Failed to send telemetry message");
|
||||||
|
|
||||||
|
println!("{}", json_message);
|
||||||
|
}
|
||||||
|
println!("disconnected");
|
||||||
|
app_handle
|
||||||
|
.emit_all("telemetry_status", "disconnected")
|
||||||
|
.expect("Failed to emit telemetry_disconnected event");
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,14 @@
|
||||||
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 { initializeTelemetry } from './lib/utils/initializeTelemetry'
|
||||||
import { onMount } from 'svelte'
|
import { onDestroy, onMount } from 'svelte'
|
||||||
import { Toaster } from 'svelte-french-toast'
|
import { Toaster } from 'svelte-french-toast'
|
||||||
import { initializationSequence } from './lib/Sequences/sequences'
|
import { initializationSequence } from './lib/Sequences/sequences'
|
||||||
import Loading from './lib/Loading/Loading.svelte'
|
import Loading from './lib/Loading/Loading.svelte'
|
||||||
import { settingsStore } from './lib/stores/settingsStore'
|
import { settingsStore } from './lib/stores/settingsStore'
|
||||||
import getSettings from './lib/utils/getSettings'
|
import getSettings from './lib/utils/getSettings'
|
||||||
import { Canvas } from '@threlte/core'
|
import { Canvas } from '@threlte/core'
|
||||||
|
import { emit } from '@tauri-apps/api/event'
|
||||||
|
|
||||||
let activeApp: App = 'camera'
|
let activeApp: App = 'camera'
|
||||||
let topics: TelemetryTopics = {
|
let topics: TelemetryTopics = {
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let loading = $settingsStore.fastStartup ? false : true
|
let loading = $settingsStore.fastStartup ? false : true
|
||||||
|
let unlistenAll: () => void
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
let savedSettings = getSettings()
|
let savedSettings = getSettings()
|
||||||
|
@ -40,11 +42,21 @@
|
||||||
}
|
}
|
||||||
window.ResizeObserver = ResizeObserver
|
window.ResizeObserver = ResizeObserver
|
||||||
// disabled while migrating away from python
|
// disabled while migrating away from python
|
||||||
initializeTelemetry(topics, 200)
|
initializeTelemetry(topics, 200).then((unsubFunction: () => void) => {
|
||||||
|
unlistenAll = unsubFunction
|
||||||
|
})
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
loading = false
|
loading = false
|
||||||
initializationSequence()
|
initializationSequence()
|
||||||
}, 3000)
|
}, 3000)
|
||||||
|
|
||||||
|
settingsStore.subscribe(value => {
|
||||||
|
localStorage.setItem('settings', JSON.stringify(value))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
unlistenAll && unlistenAll()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
overflow: auto; /* or 'scroll' if you always want scrollability */
|
overflow: auto; /* or 'scroll' if you always want scrollability */
|
||||||
scrollbar-width: none; /* Hide scrollbar for Firefox */
|
scrollbar-width: none; /* Hide scrollbar for Firefox */
|
||||||
-ms-overflow-style: none; /* Hide scrollbar for IE 10+ and Edge */
|
-ms-overflow-style: none; /* Hide scrollbar for IE 10+ and Edge */
|
||||||
|
|
||||||
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
body::-webkit-scrollbar {
|
body::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
5
client/src/globals.d.ts
vendored
5
client/src/globals.d.ts
vendored
|
@ -49,11 +49,12 @@ 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
|
||||||
|
'connected': boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type CardinalDirection =
|
type CardinalDirection =
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onDestroy } from 'svelte'
|
||||||
|
import {
|
||||||
|
cameraControls,
|
||||||
|
cameraState,
|
||||||
|
} from '../../Dashboard/Visualization/CameraControls/utils/cameraStore'
|
||||||
import AppContainer from '../AppContainer.svelte'
|
import AppContainer from '../AppContainer.svelte'
|
||||||
import { simulateMotion } from './simulateMotion'
|
import { simulateMotion } from './simulateMotion'
|
||||||
import {
|
import {
|
||||||
|
@ -6,6 +11,16 @@
|
||||||
increaseSpeedTo,
|
increaseSpeedTo,
|
||||||
setStationaryTelemetry,
|
setStationaryTelemetry,
|
||||||
} from './telemetrySimulators'
|
} from './telemetrySimulators'
|
||||||
|
|
||||||
|
let value: typeof $cameraState.mode = $cameraState.mode
|
||||||
|
|
||||||
|
$: {
|
||||||
|
cameraState.set('mode', value)
|
||||||
|
}
|
||||||
|
const unsubscribe = cameraState.subscribe(state => {
|
||||||
|
if (value !== state.mode) value = state.mode
|
||||||
|
})
|
||||||
|
onDestroy(unsubscribe)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppContainer
|
<AppContainer
|
||||||
|
@ -49,6 +64,11 @@
|
||||||
<button class="button" on:click={simulateMotion}>
|
<button class="button" on:click={simulateMotion}>
|
||||||
Simulate random motion
|
Simulate random motion
|
||||||
</button>
|
</button>
|
||||||
|
<select bind:value class="bg-slate-300">
|
||||||
|
<option value="orbit">Orbit</option>
|
||||||
|
<option value="follow-facing">Follow Facing</option>
|
||||||
|
<option value="follow-direction">Follow Direction</option>
|
||||||
|
</select>
|
||||||
</AppContainer>
|
</AppContainer>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import { telemetryStore } from '../../stores/telemetryStore'
|
import { telemetryStore } from '../../stores/telemetryStore'
|
||||||
|
import { increaseRotationTo, increaseSpeedTo } from './telemetrySimulators'
|
||||||
|
|
||||||
// simulate some turning for testing
|
// simulate some turning for testing
|
||||||
export const simulateMotion = () => {
|
export const simulateMotion = () => {
|
||||||
let delay = Math.random() * 4500 + 500
|
let delay = Math.random() * 4500 + 500
|
||||||
let randOffset = Math.random() * 360
|
let randOffset = Math.random() * 360
|
||||||
telemetryStore.update({
|
|
||||||
...get(telemetryStore),
|
increaseSpeedTo(
|
||||||
'orientation': randOffset,
|
Math.random() * 4 * (Math.random() < 0.5 ? -1 : 1),
|
||||||
'chassis-x-speed': Math.random() * 4 * (Math.random() < 0.5 ? -1 : 1),
|
Math.random() * 4 * (Math.random() < 0.5 ? -1 : 1)
|
||||||
'chassis-y-speed': Math.random() * 4 * (Math.random() < 0.5 ? -1 : 1),
|
)
|
||||||
})
|
|
||||||
|
increaseRotationTo(randOffset)
|
||||||
|
|
||||||
setTimeout(simulateMotion, delay)
|
setTimeout(simulateMotion, delay)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ export const setStationaryTelemetry = () => {
|
||||||
'ebrake': false,
|
'ebrake': false,
|
||||||
'reorient': false,
|
'reorient': false,
|
||||||
'gpws': false,
|
'gpws': false,
|
||||||
|
'connected': true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ export const increaseSpeedTo = async (targetX: number, targetY: number) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const delay = () => new Promise(resolve => setTimeout(resolve, 1000)) // Assuming a 100ms tick for demonstration
|
const delay = () => new Promise(resolve => setTimeout(resolve, 500)) // Assuming a 100ms tick for demonstration
|
||||||
const lerp = (start: number, end: number, alpha: number) =>
|
const lerp = (start: number, end: number, alpha: number) =>
|
||||||
start + (end - start) * alpha
|
start + (end - start) * alpha
|
||||||
|
|
||||||
|
@ -68,3 +69,47 @@ export const changeGear = (gear: Gear) => {
|
||||||
gear: gear,
|
gear: gear,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cancelPreviousCall = () => {} // Function to cancel the previous interpolation
|
||||||
|
|
||||||
|
const getAngle = () => {
|
||||||
|
return get(telemetryStore)['orientation']
|
||||||
|
}
|
||||||
|
|
||||||
|
const setAngle = (angle: number) => {
|
||||||
|
telemetryStore.update({
|
||||||
|
...get(telemetryStore),
|
||||||
|
orientation: angle,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const increaseRotationTo = async (targetAngle: number) => {
|
||||||
|
let isCancelled = false
|
||||||
|
cancelPreviousCall() // Cancel any ongoing interpolation
|
||||||
|
cancelPreviousCall = () => {
|
||||||
|
isCancelled = true
|
||||||
|
} // Setup cancellation for the current call
|
||||||
|
|
||||||
|
const lerp = (start: number, end: number, alpha: number) =>
|
||||||
|
start + (end - start) * alpha
|
||||||
|
const tick = () => new Promise(resolve => setTimeout(resolve, 1000)) // Assuming a 100ms tick for demonstration
|
||||||
|
|
||||||
|
let currentAngle = getAngle() // Assume getAngle() retrieves the current angle
|
||||||
|
|
||||||
|
const steps = 10 // Number of steps for the interpolation
|
||||||
|
for (let i = 1; i <= steps; i++) {
|
||||||
|
if (isCancelled) return // Exit if a new target angle is set
|
||||||
|
|
||||||
|
const alpha = i / steps // Calculate interpolation fraction
|
||||||
|
|
||||||
|
// Interpolate angle
|
||||||
|
const nextAngle = lerp(currentAngle, targetAngle, alpha)
|
||||||
|
|
||||||
|
setAngle(nextAngle) // Update angle
|
||||||
|
|
||||||
|
await tick() // Wait for state update synchronization
|
||||||
|
|
||||||
|
// Update current angle for the next iteration
|
||||||
|
currentAngle = nextAngle
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,17 @@
|
||||||
>
|
>
|
||||||
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
|
||||||
|
>
|
||||||
|
<SettingsToggle
|
||||||
|
setting="sentry"
|
||||||
|
tooltip="Sentry mode protects the robot and operator from foreign threats."
|
||||||
|
>Sentry Mode</SettingsToggle
|
||||||
|
>
|
||||||
<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
|
||||||
|
|
46
client/src/lib/Apps/Settings/SettingsSelector.svelte
Normal file
46
client/src/lib/Apps/Settings/SettingsSelector.svelte
Normal 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>
|
|
@ -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">
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
|
@ -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
|
||||||
|
@ -16,19 +16,17 @@
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="px-3 py-1 border-black rounded-xl border-2 flex flex-col text-center gap-1 transition-all"
|
class="px-3 py-1 border-black rounded-xl border-2 flex flex-col text-center gap-1 transition-all"
|
||||||
|
class:speed-limit-placeholder={placeholder}
|
||||||
>
|
>
|
||||||
<div class="text-lg font-medium">SPEED<br />LIMIT</div>
|
<div class="text-lg font-medium">SPEED<br />LIMIT</div>
|
||||||
<div
|
<div class="text-2xl font-bold transition">
|
||||||
class="text-2xl font-bold transition"
|
{speedLimit.toFixed(1)}
|
||||||
class:speed-limit-placeholder={placeholder}
|
|
||||||
>
|
|
||||||
{speedLimit}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.speed-limit-placeholder {
|
.speed-limit-placeholder {
|
||||||
@apply text-neutral-200 bg-neutral-200 animate-pulse rounded-lg;
|
@apply text-neutral-300 bg-neutral-300 animate-pulse rounded-lg;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -1,18 +1,53 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
export let selectedGear: Gear | '-999'
|
import {
|
||||||
|
shiftedInAutoSequence,
|
||||||
|
shiftedInDriveSequence,
|
||||||
|
shiftedInLowSequence,
|
||||||
|
shiftedInNeutralSequence,
|
||||||
|
shiftedInParkSequence,
|
||||||
|
shiftedInReverseSequence,
|
||||||
|
} from '../../Sequences/sequences'
|
||||||
|
|
||||||
|
export let selectedGear: Gear
|
||||||
|
export let placeholder: boolean
|
||||||
|
|
||||||
|
const shift = (selectedGear: Gear) => {
|
||||||
|
switch (selectedGear) {
|
||||||
|
case 'park':
|
||||||
|
shiftedInParkSequence()
|
||||||
|
break
|
||||||
|
case 'reverse':
|
||||||
|
shiftedInReverseSequence()
|
||||||
|
break
|
||||||
|
case 'neutral':
|
||||||
|
shiftedInNeutralSequence()
|
||||||
|
break
|
||||||
|
case 'low':
|
||||||
|
shiftedInLowSequence()
|
||||||
|
break
|
||||||
|
case 'auto':
|
||||||
|
shiftedInAutoSequence()
|
||||||
|
break
|
||||||
|
case 'drive':
|
||||||
|
shiftedInDriveSequence()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: shift(selectedGear)
|
||||||
</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>
|
||||||
|
|
||||||
|
|
|
@ -2,29 +2,45 @@
|
||||||
@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 {
|
||||||
|
modeChillSequence,
|
||||||
|
modeCruiseSequence,
|
||||||
|
modeLudicrousSequence,
|
||||||
|
} from '../../Sequences/sequences'
|
||||||
|
|
||||||
export let selectedMode: Mode | '-999'
|
export let selectedMode: Mode
|
||||||
|
export let placeholder: boolean
|
||||||
|
|
||||||
let modeText = ''
|
let modeText = ''
|
||||||
|
|
||||||
$: switch (selectedMode) {
|
const setModeText = (selectedMode: Mode) => {
|
||||||
case 'chill':
|
switch (selectedMode) {
|
||||||
modeText = 'CHILL'
|
case 'chill':
|
||||||
break
|
modeText = 'CHILL'
|
||||||
case 'cruise':
|
modeChillSequence()
|
||||||
modeText = 'CRUISE'
|
break
|
||||||
break
|
case 'cruise':
|
||||||
case 'ludicrous':
|
modeText = 'CRUISE'
|
||||||
modeText = 'LUDICROUS'
|
modeCruiseSequence()
|
||||||
break
|
break
|
||||||
case '-999':
|
case 'ludicrous':
|
||||||
|
modeText = 'LUDICROUS'
|
||||||
|
modeLudicrousSequence()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (placeholder) {
|
||||||
modeText = 'DISCONNECTED'
|
modeText = 'DISCONNECTED'
|
||||||
break
|
} else {
|
||||||
|
setModeText(selectedMode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
let installed = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
T,
|
||||||
|
forwardEventHandlers,
|
||||||
|
useTask,
|
||||||
|
useParent,
|
||||||
|
useThrelte,
|
||||||
|
} from '@threlte/core'
|
||||||
|
import type {
|
||||||
|
CameraControlsEvents,
|
||||||
|
CameraControlsProps,
|
||||||
|
CameraControlsSlots,
|
||||||
|
} from './CameraControls.svelte'
|
||||||
|
|
||||||
|
type $$Props = CameraControlsProps
|
||||||
|
type $$Events = CameraControlsEvents
|
||||||
|
type $$Slots = CameraControlsSlots
|
||||||
|
|
||||||
|
import CameraControls from 'camera-controls'
|
||||||
|
import {
|
||||||
|
Box3,
|
||||||
|
Matrix4,
|
||||||
|
Quaternion,
|
||||||
|
Raycaster,
|
||||||
|
Sphere,
|
||||||
|
Spherical,
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
Vector4,
|
||||||
|
type PerspectiveCamera,
|
||||||
|
} from 'three'
|
||||||
|
// @ts-expect-error
|
||||||
|
import { DEG2RAD } from 'three/src/math/MathUtils'
|
||||||
|
import { cameraState } from './utils/cameraStore'
|
||||||
|
|
||||||
|
const subsetOfTHREE = {
|
||||||
|
Vector2,
|
||||||
|
Vector3,
|
||||||
|
Vector4,
|
||||||
|
Quaternion,
|
||||||
|
Matrix4,
|
||||||
|
Spherical,
|
||||||
|
Box3,
|
||||||
|
Sphere,
|
||||||
|
Raycaster,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installed) {
|
||||||
|
CameraControls.install({ THREE: subsetOfTHREE })
|
||||||
|
installed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const parent = useParent()
|
||||||
|
|
||||||
|
if (!$parent) {
|
||||||
|
throw new Error('CameraControls must be a child of a ThreeJS camera')
|
||||||
|
}
|
||||||
|
|
||||||
|
const { renderer, invalidate } = useThrelte()
|
||||||
|
|
||||||
|
let autoRotate = true
|
||||||
|
export let autoRotateSpeed = 1
|
||||||
|
|
||||||
|
export const ref = new CameraControls(
|
||||||
|
$parent as PerspectiveCamera,
|
||||||
|
renderer?.domElement
|
||||||
|
)
|
||||||
|
|
||||||
|
const getControls = () => ref
|
||||||
|
|
||||||
|
useTask(
|
||||||
|
delta => {
|
||||||
|
if (autoRotate && $cameraState.mode === 'orbit') {
|
||||||
|
getControls().azimuthAngle += 4 * delta * DEG2RAD * autoRotateSpeed
|
||||||
|
}
|
||||||
|
const updated = getControls().update(delta)
|
||||||
|
if (updated) invalidate()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
autoInvalidate: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const forwardingComponent = forwardEventHandlers()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<T
|
||||||
|
is={ref}
|
||||||
|
on:controlstart={e => {
|
||||||
|
autoRotate = false
|
||||||
|
}}
|
||||||
|
on:controlend={() => {
|
||||||
|
autoRotate = true
|
||||||
|
}}
|
||||||
|
{...$$restProps}
|
||||||
|
bind:this={$forwardingComponent}
|
||||||
|
>
|
||||||
|
<slot {ref} />
|
||||||
|
</T>
|
16
client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts
vendored
Normal file
16
client/src/lib/Dashboard/Visualization/CameraControls/CameraControls.svelte.d.ts
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import type { Events, Props, Slots } from '@threlte/core'
|
||||||
|
import CC from 'camera-controls'
|
||||||
|
import type { SvelteComponent } from 'svelte'
|
||||||
|
|
||||||
|
export type CameraControlsProps = Props<CC> & {
|
||||||
|
autoRotate?: boolean
|
||||||
|
autoRotateSpeed?: number
|
||||||
|
}
|
||||||
|
export type CameraControlsEvents = Events<CC>
|
||||||
|
export type CameraControlsSlots = Slots<CC>
|
||||||
|
|
||||||
|
export default class CameraControls extends SvelteComponent<
|
||||||
|
CameraControlsProps,
|
||||||
|
CameraControlsEvents,
|
||||||
|
CameraControlsSlots
|
||||||
|
> {}
|
|
@ -0,0 +1,33 @@
|
||||||
|
import type CameraControls from 'camera-controls'
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
import type { Mesh } from 'three'
|
||||||
|
|
||||||
|
export const cameraControls = writable<CameraControls>()
|
||||||
|
export const mesh = writable<Mesh>()
|
||||||
|
|
||||||
|
type CameraMode =
|
||||||
|
| 'orbit'
|
||||||
|
| 'follow-facing'
|
||||||
|
| 'follow-direction'
|
||||||
|
| 'follow-position'
|
||||||
|
| 'showcase'
|
||||||
|
|
||||||
|
interface CameraState {
|
||||||
|
mode: CameraMode
|
||||||
|
}
|
||||||
|
|
||||||
|
const { set, update, subscribe } = writable<CameraState>({
|
||||||
|
mode: 'orbit',
|
||||||
|
})
|
||||||
|
|
||||||
|
const createCameraState = () => {
|
||||||
|
return {
|
||||||
|
update,
|
||||||
|
subscribe,
|
||||||
|
set: (prop: keyof CameraState, val: any) =>
|
||||||
|
update(state => ({ ...state, [prop]: val })),
|
||||||
|
reset: () => set({ mode: 'orbit' }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cameraState = createCameraState()
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useThrelteUserContext } from '@threlte/core'
|
||||||
|
import { writable, type Writable } from 'svelte/store'
|
||||||
|
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||||
|
|
||||||
|
type ControlsContext = {
|
||||||
|
orbitControls: Writable<OrbitControls | undefined>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ### `useControlsContext`
|
||||||
|
*
|
||||||
|
* This hook is used to register the `OrbitControls` instance with the
|
||||||
|
* `ControlsContext`. We're using this context to enable and disable the
|
||||||
|
* controls when the user is interacting with the TransformControls.
|
||||||
|
*/
|
||||||
|
export const useControlsContext = (): ControlsContext => {
|
||||||
|
return useThrelteUserContext<ControlsContext>('threlte-controls', {
|
||||||
|
orbitControls: writable<OrbitControls | undefined>(undefined),
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,165 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
|
||||||
import {
|
|
||||||
Camera,
|
|
||||||
Vector2,
|
|
||||||
Vector3,
|
|
||||||
Quaternion,
|
|
||||||
Object3D,
|
|
||||||
type Object3DEventMap,
|
|
||||||
Group,
|
|
||||||
} from 'three'
|
|
||||||
import { useThrelte, useParent, useTask } from '@threlte/core'
|
|
||||||
|
|
||||||
export let object: Group<Object3DEventMap>
|
|
||||||
export let rotateSpeed = 1.0
|
|
||||||
export let shouldOrbit: boolean
|
|
||||||
|
|
||||||
$: if (object) {
|
|
||||||
// console.log(object)
|
|
||||||
// object.position.y = 10
|
|
||||||
// // Calculate the direction vector towards (0, 0, 0)
|
|
||||||
// const target = new Vector3(0, 0, 0)
|
|
||||||
// const direction = target.clone().sub(object.position).normalize()
|
|
||||||
// // Extract the forward direction from the object's current rotation matrix
|
|
||||||
// const currentDirection = new Vector3(0, 1, 0)
|
|
||||||
// currentDirection.applyQuaternion(object.quaternion)
|
|
||||||
// // Calculate the axis and angle to rotate the object
|
|
||||||
// const rotationAxis = currentDirection.clone().cross(direction).normalize()
|
|
||||||
// const rotationAngle = Math.acos(currentDirection.dot(direction))
|
|
||||||
// // Rotate the object using rotateOnAxis()
|
|
||||||
// object.rotateOnAxis(rotationAxis, rotationAngle)
|
|
||||||
}
|
|
||||||
|
|
||||||
export let idealOffset = { x: -0.5, y: 2, z: -3 }
|
|
||||||
export let idealLookAt = { x: 0, y: 1, z: 5 }
|
|
||||||
|
|
||||||
const currentPosition = new Vector3()
|
|
||||||
const currentLookAt = new Vector3()
|
|
||||||
|
|
||||||
let isOrbiting = false
|
|
||||||
let pointerDown = false
|
|
||||||
|
|
||||||
const rotateStart = new Vector2()
|
|
||||||
const rotateEnd = new Vector2()
|
|
||||||
const rotateDelta = new Vector2()
|
|
||||||
|
|
||||||
const axis = new Vector3(0, 1, 0)
|
|
||||||
const rotationQuat = new Quaternion()
|
|
||||||
|
|
||||||
const { renderer, invalidate } = useThrelte()
|
|
||||||
|
|
||||||
const domElement = renderer.domElement
|
|
||||||
const camera = useParent()
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
|
|
||||||
const isCamera = (p: any): p is Camera => {
|
|
||||||
return p.isCamera
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isCamera($camera)) {
|
|
||||||
throw new Error(
|
|
||||||
'Parent missing: <PointerLockControls> need to be a child of a <Camera>'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is basically your update function
|
|
||||||
useTask(delta => {
|
|
||||||
// the object's position is bound to the prop
|
|
||||||
if (!object) return
|
|
||||||
|
|
||||||
// then we calculate our ideal's
|
|
||||||
const offset = vectorFromObject(idealOffset)
|
|
||||||
const lookAt = vectorFromObject(idealLookAt)
|
|
||||||
|
|
||||||
// camera is based on character so we rotation character first
|
|
||||||
// rotationQuat.setFromAxisAngle(axis, rotateSpeed * delta)
|
|
||||||
// object.quaternion.multiply(rotationQuat)
|
|
||||||
|
|
||||||
// and how far we should move towards them
|
|
||||||
const t = 1.0 - Math.pow(0.001, delta)
|
|
||||||
currentPosition.lerp(offset, t)
|
|
||||||
currentLookAt.lerp(lookAt, t)
|
|
||||||
|
|
||||||
// typescript HACKS! never do this! How does this work? who knows!
|
|
||||||
const robotPosition = vectorFromObject(
|
|
||||||
object as unknown as { x: number; y: number; z: number }
|
|
||||||
)
|
|
||||||
|
|
||||||
const horizontalOffsetDistance = 15 // Distance behind the leading vector
|
|
||||||
const direction = new Vector3(0, 0, 1) // Default forward direction in Three.js is negative z-axis, so behind is positive z-axis
|
|
||||||
const verticalOffset = new Vector3(0, -5, 0)
|
|
||||||
|
|
||||||
// Calculate the offset vector
|
|
||||||
const offsetVector = direction
|
|
||||||
.normalize()
|
|
||||||
.multiplyScalar(horizontalOffsetDistance)
|
|
||||||
.add(verticalOffset)
|
|
||||||
|
|
||||||
// If the leading object is rotating, apply its rotation to the offset vector
|
|
||||||
const rotatedOffsetVector = offsetVector.applyQuaternion(object.quaternion)
|
|
||||||
|
|
||||||
const leftOffset = -1.5
|
|
||||||
function shiftVectorLeft(
|
|
||||||
vector: Vector3,
|
|
||||||
amount: number,
|
|
||||||
upDirection = axis
|
|
||||||
): void {
|
|
||||||
// Calculate the left direction. Assuming 'up' is the global up direction or a custom up vector.
|
|
||||||
// This creates a vector pointing to the "left" of the original vector, in a 3D context.
|
|
||||||
let leftDirection = new Vector3()
|
|
||||||
.crossVectors(upDirection, vector)
|
|
||||||
.normalize()
|
|
||||||
|
|
||||||
// Scale the left direction by the desired amount
|
|
||||||
leftDirection.multiplyScalar(amount)
|
|
||||||
|
|
||||||
// Add the scaled left direction to the original vector, mutating it
|
|
||||||
vector.add(leftDirection)
|
|
||||||
}
|
|
||||||
|
|
||||||
shiftVectorLeft(rotatedOffsetVector, leftOffset)
|
|
||||||
|
|
||||||
// Calculate the trailing vector's position
|
|
||||||
const trailingVector = robotPosition.clone().sub(rotatedOffsetVector)
|
|
||||||
|
|
||||||
function shiftVectorLeftNonMutate(
|
|
||||||
vector: Vector3,
|
|
||||||
amount: number,
|
|
||||||
upDirection = axis
|
|
||||||
): Vector3 {
|
|
||||||
// Create a new vector to avoid mutating the original vector
|
|
||||||
let shiftedVector = vector.clone()
|
|
||||||
|
|
||||||
// Calculate the left direction. Assuming 'up' is the global up direction or a custom up vector.
|
|
||||||
// This creates a vector pointing to the "left" of the original vector, in a 3D context.
|
|
||||||
let leftDirection = new Vector3()
|
|
||||||
.crossVectors(upDirection, vector)
|
|
||||||
.normalize()
|
|
||||||
|
|
||||||
// Scale the left direction by the desired amount and add it to the original vector
|
|
||||||
leftDirection.multiplyScalar(amount)
|
|
||||||
shiftedVector.add(leftDirection)
|
|
||||||
|
|
||||||
return shiftedVector
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldOrbit) {
|
|
||||||
// then finally set the camera, a bit behind the model
|
|
||||||
$camera!.position.copy(trailingVector)
|
|
||||||
// Rotate the offset around the Y-axis
|
|
||||||
$camera!.lookAt(shiftVectorLeftNonMutate(currentLookAt, -leftOffset))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function vectorFromObject(vec: { x: number; y: number; z: number }) {
|
|
||||||
const { x, y, z } = vec
|
|
||||||
const ideal = new Vector3(x, y, z)
|
|
||||||
ideal.applyQuaternion(object.quaternion)
|
|
||||||
ideal.add(
|
|
||||||
new Vector3(object.position.x, object.position.y, object.position.z)
|
|
||||||
)
|
|
||||||
return ideal
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -1,155 +1,154 @@
|
||||||
<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 { Grid } from '@threlte/extras'
|
||||||
import Robot from './models/RobotDecimated.svelte'
|
import CameraControls from './CameraControls/CameraControls.svelte'
|
||||||
import Controls from './Controls.svelte'
|
|
||||||
import {
|
import {
|
||||||
Vector3,
|
cameraControls,
|
||||||
type Camera,
|
mesh,
|
||||||
type Group,
|
cameraState,
|
||||||
type Object3D,
|
} from './CameraControls/utils/cameraStore'
|
||||||
type Object3DEventMap,
|
import { Vector3 } 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'
|
import { onMount } from 'svelte'
|
||||||
|
import RobotDecimated from './models/RobotDecimated.svelte'
|
||||||
|
import { telemetryReadonlyStore } from '../../stores/telemetryStore'
|
||||||
|
import { DEG2RAD } from 'three/src/math/MathUtils.js'
|
||||||
|
|
||||||
/* This is the root scene where the robot visualization is built.
|
const SPEED_MULTIPLIER = 4
|
||||||
It renders an infinite grid (it's not actually infinite, but we shouldn't run out
|
const axis = new Vector3(0, 1, 0)
|
||||||
of space in realistic use), and a 3D model of the robot. The camera is locked
|
|
||||||
to the model, and the model is rotated to match the robot's orientation.
|
|
||||||
A PID controller is used to smoothly rotate the model to match the robot's
|
|
||||||
orientation and dampen out jittering. How does it work? Who knows!
|
|
||||||
75% percent of this was created while reading
|
|
||||||
https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation,
|
|
||||||
and rest was generated by AI!
|
|
||||||
The rest of this codebase is remarkably jank-free, but this visualization module
|
|
||||||
is the most esoteric and jank code ever written.
|
|
||||||
*/
|
|
||||||
|
|
||||||
let shouldOrbit = true
|
const follow = (delta: number) => {
|
||||||
|
// the object's position is bound to the prop
|
||||||
|
if (!$mesh || !$cameraControls) return
|
||||||
|
|
||||||
// CONSTANTS
|
const offsetPosition = new Vector3()
|
||||||
const maxAngularVelocity = 2 // Max angular velocity, in radians per second
|
offsetPosition.copy($mesh.position)
|
||||||
const stoppingThreshold = 0.005 // Threshold in radians for when to consider the rotation close enough to stop
|
const offsetVector = new Vector3(2.5, 0, -2)
|
||||||
|
offsetVector.applyAxisAngle(axis, $mesh.rotation.y)
|
||||||
|
offsetPosition.add(offsetVector)
|
||||||
|
|
||||||
// Proportional control factor
|
const followDirection = () => {
|
||||||
const kP = 2 // Adjust this value based on responsiveness and stability needs
|
const angle = Math.atan2(
|
||||||
|
$telemetryReadonlyStore['chassis-y-speed'],
|
||||||
|
$telemetryReadonlyStore['chassis-x-speed']
|
||||||
|
)
|
||||||
|
|
||||||
// Sync robot orientation with target rotation
|
$cameraControls.setLookAt(
|
||||||
let targetRot = 0
|
offsetPosition.x - 13 * Math.sin(angle),
|
||||||
|
offsetPosition.y + 4,
|
||||||
// Updates rotation to match target with PID controller (intended to be invoked in useTask)
|
offsetPosition.z - 13 * Math.cos(angle),
|
||||||
let rot = 0 // (initial) rotation in radians
|
offsetPosition.x,
|
||||||
let angularVelocity = 0
|
offsetPosition.y,
|
||||||
const updateRotation = (delta: number) => {
|
offsetPosition.z,
|
||||||
let angleDifference = targetRot - rot
|
true
|
||||||
|
)
|
||||||
// Normalize angle difference to the range [-π, π]
|
$cameraControls.zoomTo(1.1, true)
|
||||||
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))
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust angular velocity towards desired velocity
|
const followFacing = () => {
|
||||||
angularVelocity = desiredVelocity
|
if ($cameraState.mode === 'follow-facing') {
|
||||||
|
$cameraControls.setLookAt(
|
||||||
|
offsetPosition.x + 13 * Math.sin($mesh.rotation.y),
|
||||||
|
offsetPosition.y + 5,
|
||||||
|
offsetPosition.z + 13 * Math.cos($mesh.rotation.y),
|
||||||
|
offsetPosition.x,
|
||||||
|
offsetPosition.y + 2,
|
||||||
|
offsetPosition.z,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
$cameraControls.zoomTo(1.2, true)
|
||||||
|
}
|
||||||
|
|
||||||
// Update rotation
|
const orbit = () => {
|
||||||
rot += angularVelocity * delta
|
$cameraControls.zoomTo(0.8, true)
|
||||||
|
$cameraControls.moveTo(
|
||||||
|
offsetPosition.x + 4 * Math.sin($mesh.rotation.y),
|
||||||
|
offsetPosition.y + 3,
|
||||||
|
offsetPosition.z * Math.cos($mesh.rotation.y),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize rot to the range [0, 2π]
|
switch ($cameraState.mode) {
|
||||||
if (rot < 0) rot += 2 * Math.PI
|
case 'follow-direction':
|
||||||
else if (rot > 2 * Math.PI) rot -= 2 * Math.PI
|
followDirection()
|
||||||
|
break
|
||||||
// Snap to the target rotation to prevent tiny oscillations if close enough
|
case 'follow-facing':
|
||||||
if (Math.abs(angleDifference) < stoppingThreshold) {
|
followFacing()
|
||||||
rot = targetRot
|
break
|
||||||
angularVelocity = 0
|
case 'orbit':
|
||||||
|
orbit()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
orbit()
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let robotPos: Vector3 = new Vector3(0, 0, 0)
|
let gridFadeDistance = 30
|
||||||
|
|
||||||
const robotPosition = new Vector2(0, 0) // Initial position
|
const fadeGridIn = (delta: number) => {
|
||||||
const initialVelocity = { x: 0, y: 0 } // Initial velocity
|
if (gridFadeDistance < 100) {
|
||||||
// The smooth motion controller utilizes a cubic hermite spline to interpolate between
|
gridFadeDistance += delta * 40
|
||||||
// the current simulation velocity and the robot's actual velocity
|
}
|
||||||
const controller = new SmoothMotionController(robotPosition, initialVelocity)
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const fadeGridOut = (delta: number) => {
|
||||||
telemetryReadonlyStore.subscribe(value => {
|
if (gridFadeDistance > 30) {
|
||||||
targetRot = (value['orientation'] * Math.PI) / 180 // convert deg to rad
|
gridFadeDistance -= delta * 40
|
||||||
controller.setTargetVelocity({
|
}
|
||||||
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()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
useTask(delta => {
|
useTask(delta => {
|
||||||
if (!shouldOrbit) {
|
/* TODO: standardize a scale (meters : grid lengths) so we can have
|
||||||
updateRotation(delta)
|
accurate positioning of sensor detected objects */
|
||||||
|
// update position data for robot model
|
||||||
|
$mesh.position.x +=
|
||||||
|
$telemetryReadonlyStore['chassis-y-speed'] * delta * SPEED_MULTIPLIER
|
||||||
|
$mesh.position.z +=
|
||||||
|
$telemetryReadonlyStore['chassis-x-speed'] * delta * SPEED_MULTIPLIER
|
||||||
|
$mesh.rotation.y = $telemetryReadonlyStore.orientation * DEG2RAD
|
||||||
|
|
||||||
controller.update(delta)
|
if ($cameraState.mode === 'orbit') {
|
||||||
robotPos.x = controller.getPosition().x
|
fadeGridOut(delta)
|
||||||
robotPos.z = controller.getPosition().y
|
} else {
|
||||||
|
fadeGridIn(delta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// run the follow function
|
||||||
|
follow(delta)
|
||||||
})
|
})
|
||||||
|
|
||||||
let capsule: Group<Object3DEventMap>
|
onMount(() => {})
|
||||||
let capRef: Group<Object3DEventMap>
|
|
||||||
$: if (capsule) {
|
|
||||||
capRef = capsule
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<T.PerspectiveCamera makeDefault position={[0, 8, -20]} fov={30} on:create>
|
<T.PerspectiveCamera makeDefault position={[12, 10, 12]}>
|
||||||
<OrbitControls
|
<CameraControls
|
||||||
autoRotateSpeed={1.5}
|
on:create={({ ref }) => {
|
||||||
target.y={1.5}
|
$cameraControls = ref
|
||||||
autoRotate
|
}}
|
||||||
enableDamping
|
autoRotateSpeed={3}
|
||||||
/>
|
/>
|
||||||
<Controls {shouldOrbit} bind:object={capRef} rotateSpeed={angularVelocity} />
|
|
||||||
</T.PerspectiveCamera>
|
</T.PerspectiveCamera>
|
||||||
|
|
||||||
<T.DirectionalLight intensity={0.8} position.x={5} position.y={10} />
|
<T.DirectionalLight position={[3, 10, 7]} />
|
||||||
<T.AmbientLight intensity={0.2} />
|
<T.AmbientLight color={'#f0f0f0'} intensity={0.1} />
|
||||||
|
|
||||||
|
<RobotDecimated
|
||||||
|
scale={[10, 10, 10]}
|
||||||
|
position.y={0}
|
||||||
|
on:create={({ ref }) => {
|
||||||
|
// @ts-expect-error
|
||||||
|
$mesh = ref
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<Grid
|
<Grid
|
||||||
position.y={1}
|
sectionColor={'#ff3e00'}
|
||||||
cellColor="#ffffff"
|
sectionThickness={1}
|
||||||
sectionColor="#ffffff"
|
bind:fadeDistance={gridFadeDistance}
|
||||||
sectionThickness={0}
|
|
||||||
fadeDistance={100}
|
|
||||||
cellSize={6}
|
cellSize={6}
|
||||||
|
sectionSize={24}
|
||||||
|
cellColor={'#cccccc'}
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ContactShadows scale={10} blur={2} far={2.5} opacity={0.5} />
|
|
||||||
|
|
||||||
<Robot
|
|
||||||
position.y={1}
|
|
||||||
position.z={robotPos.z}
|
|
||||||
position.x={robotPos.x}
|
|
||||||
scale={[5, 5, 5]}
|
|
||||||
bind:ref={capsule}
|
|
||||||
rotation.y={rot}
|
|
||||||
/>
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
import { Vector2 } from 'three'
|
|
||||||
|
|
||||||
interface Velocity {
|
|
||||||
x: number
|
|
||||||
y: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SmoothMotionController {
|
|
||||||
private currentPosition: Vector2
|
|
||||||
private currentVelocity: Vector2
|
|
||||||
private targetVelocity: Velocity
|
|
||||||
private dampingFactor: number
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
initialPosition: Vector2,
|
|
||||||
initialVelocity: Velocity,
|
|
||||||
dampingFactor: number = 0.1
|
|
||||||
) {
|
|
||||||
this.currentPosition = initialPosition
|
|
||||||
this.currentVelocity = new Vector2(initialVelocity.x, initialVelocity.y)
|
|
||||||
this.targetVelocity = { ...initialVelocity }
|
|
||||||
this.dampingFactor = dampingFactor
|
|
||||||
}
|
|
||||||
|
|
||||||
setTargetVelocity(velocity: Velocity) {
|
|
||||||
this.targetVelocity = velocity
|
|
||||||
}
|
|
||||||
|
|
||||||
update(delta: number) {
|
|
||||||
// Apply cubic interpolation to smoothly transition the current velocity towards the target velocity
|
|
||||||
this.currentVelocity.x +=
|
|
||||||
(this.targetVelocity.x - this.currentVelocity.x) *
|
|
||||||
this.dampingFactor *
|
|
||||||
delta
|
|
||||||
this.currentVelocity.y +=
|
|
||||||
(this.targetVelocity.y - this.currentVelocity.y) *
|
|
||||||
this.dampingFactor *
|
|
||||||
delta
|
|
||||||
|
|
||||||
// Update position based on the current velocity and the time delta
|
|
||||||
this.currentPosition.x += this.currentVelocity.x * delta * 3
|
|
||||||
this.currentPosition.y += this.currentVelocity.y * delta * 3
|
|
||||||
}
|
|
||||||
|
|
||||||
getPosition(): Vector2 {
|
|
||||||
return this.currentPosition
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset() {
|
|
||||||
this.currentPosition = new Vector2(0, 0)
|
|
||||||
this.currentVelocity = new Vector2(0, 0)
|
|
||||||
this.targetVelocity = { x: 0, y: 0 }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,113 +1,147 @@
|
||||||
import { toast } from 'svelte-french-toast'
|
import { toast } from "svelte-french-toast";
|
||||||
import type { ToastOptions } from 'svelte-french-toast'
|
import type { ToastOptions } from "svelte-french-toast";
|
||||||
import InfoIcon from './InfoIcon.svelte'
|
import InfoIcon from "./InfoIcon.svelte";
|
||||||
import { Howl } from 'howler'
|
import { Howl } from "howler";
|
||||||
import WarnIcon from './WarnIcon.svelte'
|
import WarnIcon from "./WarnIcon.svelte";
|
||||||
|
|
||||||
interface NotificationOptions extends ToastOptions {
|
interface NotificationOptions extends ToastOptions {
|
||||||
withAudio?: boolean
|
withAudio?: boolean;
|
||||||
src?: string
|
src?: string;
|
||||||
onComplete?: () => void
|
onComplete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get colors from https://tailwindcss.com/docs/customizing-colors
|
// get colors from https://tailwindcss.com/docs/customizing-colors
|
||||||
export class Notifications {
|
export class Notifications {
|
||||||
private static readonly defaultDuration = 3000
|
private static readonly defaultDuration = 3000;
|
||||||
|
|
||||||
public static success(message: string, options?: NotificationOptions) {
|
public static success(message: string, options?: NotificationOptions) {
|
||||||
if (options?.withAudio && !options.src)
|
if (options?.withAudio && !options.src)
|
||||||
throw new Error('No audio source provided')
|
throw new Error("No audio source provided");
|
||||||
|
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
if (options?.onComplete) options.onComplete()
|
if (options?.onComplete) options.onComplete();
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast.success(message, {
|
toast.success(message, {
|
||||||
style:
|
style:
|
||||||
'padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;',
|
"padding: 25px; font-size: 1.5rem; background-color: #15803d; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;",
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
if (options?.withAudio && options?.src) {
|
if (options?.withAudio && options?.src) {
|
||||||
let sound: Howl
|
let sound: Howl;
|
||||||
sound = new Howl({
|
sound = new Howl({
|
||||||
src: [options.src],
|
src: [options.src],
|
||||||
preload: true,
|
preload: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
onload: () => {
|
onload: () => {
|
||||||
let duration = sound.duration() * 1000
|
let duration = sound.duration() * 1000;
|
||||||
sendToast(duration)
|
sendToast(duration);
|
||||||
setTimeout(onComplete, duration)
|
setTimeout(onComplete, duration);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
sendToast(this.defaultDuration)
|
sendToast(this.defaultDuration);
|
||||||
setTimeout(onComplete, this.defaultDuration)
|
setTimeout(onComplete, this.defaultDuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static error(message: string, options?: NotificationOptions) {
|
||||||
|
if (options?.withAudio && !options.src)
|
||||||
|
throw new Error("No audio source provided");
|
||||||
|
|
||||||
|
const onComplete = () => {
|
||||||
|
if (options?.onComplete) options.onComplete();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendToast = (duration: number) => {
|
||||||
|
toast.error(message, {
|
||||||
|
style:
|
||||||
|
"padding: 25px; font-size: 1.5rem; background-color: #dc2626; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;",
|
||||||
|
duration,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options?.withAudio && options?.src) {
|
||||||
|
let sound: Howl;
|
||||||
|
sound = new Howl({
|
||||||
|
src: [options.src],
|
||||||
|
preload: true,
|
||||||
|
autoplay: true,
|
||||||
|
onload: () => {
|
||||||
|
let duration = sound.duration() * 1000;
|
||||||
|
sendToast(duration);
|
||||||
|
setTimeout(onComplete, duration);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sendToast(this.defaultDuration);
|
||||||
|
setTimeout(onComplete, this.defaultDuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static info(message: string, options?: NotificationOptions) {
|
public static info(message: string, options?: NotificationOptions) {
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
if (options?.onComplete) options.onComplete()
|
if (options?.onComplete) options.onComplete();
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast(message, {
|
toast(message, {
|
||||||
style:
|
style:
|
||||||
'padding: 25px; font-size: 1.5rem; gap: 0.5rem; user-select: none; max-width-600px; max-width: 70vw;',
|
"padding: 25px; font-size: 1.5rem; gap: 0.5rem; user-select: none; max-width-600px; max-width: 70vw;",
|
||||||
icon: InfoIcon,
|
icon: InfoIcon,
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
if (options?.withAudio && options?.src) {
|
if (options?.withAudio && options?.src) {
|
||||||
let sound: Howl
|
let sound: Howl;
|
||||||
sound = new Howl({
|
sound = new Howl({
|
||||||
src: [options.src],
|
src: [options.src],
|
||||||
preload: true,
|
preload: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
onload: () => {
|
onload: () => {
|
||||||
let duration = sound.duration() * 1000
|
let duration = sound.duration() * 1000;
|
||||||
sendToast(duration)
|
sendToast(duration);
|
||||||
setTimeout(onComplete, duration)
|
setTimeout(onComplete, duration);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
sendToast(this.defaultDuration)
|
sendToast(this.defaultDuration);
|
||||||
setTimeout(onComplete, this.defaultDuration)
|
setTimeout(onComplete, this.defaultDuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static warn(message: string, options?: NotificationOptions) {
|
public static warn(message: string, options?: NotificationOptions) {
|
||||||
const onComplete = () => {
|
const onComplete = () => {
|
||||||
if (options?.onComplete) options.onComplete()
|
if (options?.onComplete) options.onComplete();
|
||||||
}
|
};
|
||||||
|
|
||||||
const sendToast = (duration: number) => {
|
const sendToast = (duration: number) => {
|
||||||
toast(message, {
|
toast(message, {
|
||||||
style:
|
style:
|
||||||
'padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;',
|
"padding: 25px; font-size: 1.5rem; background-color: #f59e0b; color: #fafafa; gap: 0.5rem; user-select: none; max-width: 70vw;",
|
||||||
icon: WarnIcon,
|
icon: WarnIcon,
|
||||||
duration,
|
duration,
|
||||||
...options,
|
...options,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
if (options?.withAudio && options?.src) {
|
if (options?.withAudio && options?.src) {
|
||||||
let sound: Howl
|
let sound: Howl;
|
||||||
sound = new Howl({
|
sound = new Howl({
|
||||||
src: [options.src],
|
src: [options.src],
|
||||||
preload: true,
|
preload: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
onload: () => {
|
onload: () => {
|
||||||
let duration = sound.duration() * 1000
|
let duration = sound.duration() * 1000;
|
||||||
sendToast(duration)
|
sendToast(duration);
|
||||||
setTimeout(onComplete, duration)
|
setTimeout(onComplete, duration);
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
sendToast(this.defaultDuration)
|
sendToast(this.defaultDuration);
|
||||||
setTimeout(onComplete, this.defaultDuration)
|
setTimeout(onComplete, this.defaultDuration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static playAudio(src: string, onComplete: () => void = () => {}) {
|
public static playAudio(src: string, onComplete: () => void = () => {}) {
|
||||||
|
@ -116,8 +150,8 @@ export class Notifications {
|
||||||
preload: true,
|
preload: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
onload: () => {
|
onload: () => {
|
||||||
setTimeout(onComplete, 1000 * sound.duration())
|
setTimeout(onComplete, 1000 * sound.duration());
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { settingsStore } from '../stores/settingsStore'
|
||||||
import { get } from 'svelte/store'
|
import { get } from 'svelte/store'
|
||||||
import getVoicePath from '../utils/getVoicePath'
|
import getVoicePath from '../utils/getVoicePath'
|
||||||
import { tick } from 'svelte'
|
import { tick } from 'svelte'
|
||||||
|
import { cameraState } from '../Dashboard/Visualization/CameraControls/utils/cameraStore'
|
||||||
|
|
||||||
// await a "tick" (a svelte update frame) at the start of every sequence so that
|
// await a "tick" (a svelte update frame) at the start of every sequence so that
|
||||||
// state is synced and no weird side effects occur
|
// state is synced and no weird side effects occur
|
||||||
|
@ -28,30 +29,34 @@ export const initializationSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.info('Jankboard initialized!', {
|
Notifications.info('Jankboard initialized!', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('jankboard-initialized', 'en'),
|
src: getVoicePath('jankboard-initialized'),
|
||||||
})
|
onComplete: () => {
|
||||||
setTimeout(() => {
|
if (get(settingsStore).goWoke) {
|
||||||
if (get(settingsStore).goWoke) return
|
sequenceStore.update('initializationComplete', true)
|
||||||
Notifications.success('LittenOS is online', {
|
periodicSequence()
|
||||||
withAudio: true,
|
return
|
||||||
src: getVoicePath('littenos-is-online', 'en'),
|
}
|
||||||
})
|
Notifications.success('LittenOS is online', {
|
||||||
setTimeout(() => {
|
|
||||||
Notifications.warn('Breaching Monte Vista codebase', {
|
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('breaching-monte-vista', 'en'),
|
src: getVoicePath('littenos-is-online'),
|
||||||
|
onComplete: () => {
|
||||||
|
Notifications.warn('Breaching Monte Vista codebase', {
|
||||||
|
withAudio: true,
|
||||||
|
src: getVoicePath('breaching-monte-vista'),
|
||||||
|
onComplete: () => {
|
||||||
|
Notifications.playAudio(
|
||||||
|
getVoicePath('hello-virtual-assistant'),
|
||||||
|
() => {
|
||||||
|
sequenceStore.update('initializationComplete', true)
|
||||||
|
periodicSequence()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
},
|
||||||
Notifications.playAudio(
|
})
|
||||||
getVoicePath('hello-virtual-assistant', 'en'),
|
|
||||||
() => {
|
|
||||||
sequenceStore.update('initializationComplete', true)
|
|
||||||
periodicSequence()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}, 3000)
|
|
||||||
}, 3000)
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let counter = 1
|
let counter = 1
|
||||||
|
@ -108,7 +113,7 @@ export const criticalFailureIminentSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.error('Critical robot failure imminent', {
|
Notifications.error('Critical robot failure imminent', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('critical-robot-failure', 'en'),
|
src: getVoicePath('critical-robot-failure'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,7 +121,7 @@ export const collisionDetectedSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.error('Collision detected', {
|
Notifications.error('Collision detected', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('collision-detected', 'en'),
|
src: getVoicePath('collision-detected'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +129,7 @@ export const collisionImminentSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.error('Collision imminent', {
|
Notifications.error('Collision imminent', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('collision-imminent', 'en'),
|
src: getVoicePath('collision-imminent'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +138,7 @@ export const cruiseControlEngagedSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.success('Cruise control engaged', {
|
Notifications.success('Cruise control engaged', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('cruise-control-engaged', 'en'),
|
src: getVoicePath('cruise-control-engaged'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,7 +147,7 @@ export const retardSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.warn('Retard', {
|
Notifications.warn('Retard', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('retard', 'en'),
|
src: getVoicePath('retard'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,7 +156,7 @@ const breaching254Sequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.warn('Breaching 254 mainframe', {
|
Notifications.warn('Breaching 254 mainframe', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('breaching-254-mainframe', 'en'),
|
src: getVoicePath('breaching-254-mainframe'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +165,7 @@ const breaching1323Sequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.warn('Breaching 1323 mainframe', {
|
Notifications.warn('Breaching 1323 mainframe', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('breaching-1323-mainframe', 'en'),
|
src: getVoicePath('breaching-1323-mainframe'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,7 +174,7 @@ const bullyingRohanSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.info('Bullying Rohan', {
|
Notifications.info('Bullying Rohan', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('bullying-rohan', 'en'),
|
src: getVoicePath('bullying-rohan'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +182,7 @@ export const userErrorDetectedSequence = async () => {
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.error('User error detected', {
|
Notifications.error('User error detected', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('user-error-detected', 'en'),
|
src: getVoicePath('user-error-detected'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,8 +193,9 @@ export const infotainmentBootupSequence = async () => {
|
||||||
get(sequenceStore).infotainmentStartedFirstTime ||
|
get(sequenceStore).infotainmentStartedFirstTime ||
|
||||||
get(settingsStore).disableAnnoyances ||
|
get(settingsStore).disableAnnoyances ||
|
||||||
infotainmentStarted
|
infotainmentStarted
|
||||||
)
|
) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
|
||||||
infotainmentStarted = true
|
infotainmentStarted = true
|
||||||
await tick()
|
await tick()
|
||||||
|
@ -197,17 +203,17 @@ export const infotainmentBootupSequence = async () => {
|
||||||
const sequence = () => {
|
const sequence = () => {
|
||||||
Notifications.info('Infotainment system buffering', {
|
Notifications.info('Infotainment system buffering', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('infotainment-system-buffering', 'en'),
|
src: getVoicePath('infotainment-system-buffering'),
|
||||||
|
onComplete: () => {
|
||||||
|
Notifications.success('Infotainment system online', {
|
||||||
|
withAudio: true,
|
||||||
|
src: getVoicePath('infotainment-system-online'),
|
||||||
|
onComplete: () => {
|
||||||
|
sequenceStore.update('infotainmentStartedFirstTime', true)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
setTimeout(() => {
|
|
||||||
Notifications.success('Infotainment system online', {
|
|
||||||
withAudio: true,
|
|
||||||
src: getVoicePath('infotainment-system-online', 'en'),
|
|
||||||
onComplete: () => {
|
|
||||||
sequenceStore.update('infotainmentStartedFirstTime', true)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!get(sequenceStore).initializationComplete) {
|
if (!get(sequenceStore).initializationComplete) {
|
||||||
|
@ -256,7 +262,7 @@ export const musicPlayerBootupSequence = async () => {
|
||||||
waitForInfotainmentBootup(() => {
|
waitForInfotainmentBootup(() => {
|
||||||
Notifications.info('Downloading copyrighted music...', {
|
Notifications.info('Downloading copyrighted music...', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('downloading-copyrighted-music', 'en'),
|
src: getVoicePath('downloading-copyrighted-music'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -274,7 +280,7 @@ export const gbaEmulatorBootupSequence = async () => {
|
||||||
waitForInfotainmentBootup(() => {
|
waitForInfotainmentBootup(() => {
|
||||||
Notifications.info('Loading pirated Nintendo ROMs', {
|
Notifications.info('Loading pirated Nintendo ROMs', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('loading-pirated-nintendo', 'en'),
|
src: getVoicePath('loading-pirated-nintendo'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -292,16 +298,135 @@ export const doomBootupSequence = async () => {
|
||||||
waitForInfotainmentBootup(() => {
|
waitForInfotainmentBootup(() => {
|
||||||
Notifications.success('Doom Engaged', {
|
Notifications.success('Doom Engaged', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('doom-engaged', 'en'),
|
src: getVoicePath('doom-engaged'),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const bypassCoprocessorRestrictionsSequence = async () => {
|
const bypassCoprocessorRestrictionsSequence = async () => {
|
||||||
if (get(settingsStore).disableAnnoyances) return
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
await tick()
|
await tick()
|
||||||
Notifications.warn('Bypassing coprocessor restrictions', {
|
Notifications.warn('Bypassing coprocessor restrictions', {
|
||||||
withAudio: true,
|
withAudio: true,
|
||||||
src: getVoicePath('bypassing-coprocessor-restrictions', 'en'),
|
src: getVoicePath('bypassing-coprocessor-restrictions'),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shiftedInParkSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
cameraState.set('mode', 'orbit')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('parked-brakes-engaged'), () => {
|
||||||
|
if (!get(settingsStore).sentry) return
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('sentry-mode-engaged'))
|
||||||
|
Notifications.warn('Sentry mode engaged. Threats will be neutralized')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInReverseSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-direction')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('reverse'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInNeutralSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'orbit')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('neutral-brakes-engaged'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInLowSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-facing')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('shifted-into-low'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInAutoSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-direction')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('shifted-into-automatic'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const shiftedInDriveSequence = async () => {
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
cameraState.set('mode', 'follow-facing')
|
||||||
|
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
Notifications.playAudio(getVoicePath('shifted-into-drive'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modeChillSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('set-acceleration-profile-chill'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modeCruiseSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('cruise-control-engaged'))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const modeLudicrousSequence = async () => {
|
||||||
|
if (
|
||||||
|
get(settingsStore).disableAnnoyances ||
|
||||||
|
!get(sequenceStore).initializationComplete
|
||||||
|
)
|
||||||
|
return
|
||||||
|
await tick()
|
||||||
|
|
||||||
|
Notifications.playAudio(getVoicePath('set-acceleration-profile-ludicrous'))
|
||||||
|
}
|
||||||
|
|
|
@ -2,11 +2,15 @@
|
||||||
|
|
||||||
import { writable } from 'svelte/store'
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
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
|
||||||
|
sentry: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaults: SettingsStoreData = {
|
export const defaults: SettingsStoreData = {
|
||||||
|
@ -14,6 +18,8 @@ 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-US',
|
||||||
|
sentry: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createSequenceStore = () => {
|
const createSequenceStore = () => {
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
import { writable, readonly } from 'svelte/store'
|
import { writable, readonly, get } from 'svelte/store'
|
||||||
|
|
||||||
let defaults: TelemetryData = {
|
let defaults: TelemetryData = {
|
||||||
'orientation': -999,
|
'orientation': 0,
|
||||||
'chassis-x-speed': -999,
|
'chassis-x-speed': 0,
|
||||||
'chassis-y-speed': -999,
|
'chassis-y-speed': 0,
|
||||||
'accx': -999,
|
'accx': 0,
|
||||||
'accy': -999,
|
'accy': 0,
|
||||||
'accz': -999,
|
'accz': 0,
|
||||||
'jerk-x': -999,
|
'jerk-x': 0,
|
||||||
'jerk-y': -999,
|
'jerk-y': 0,
|
||||||
'voltage': -999,
|
'voltage': 0,
|
||||||
'acc-profile': '-999',
|
'acc-profile': 'chill',
|
||||||
'gear': '-999',
|
'gear': 'park',
|
||||||
'ebrake': false,
|
'ebrake': false,
|
||||||
'reorient': false,
|
'reorient': false,
|
||||||
'gpws': false,
|
'gpws': false,
|
||||||
|
'connected': false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createTelemetryStore = () => {
|
const createTelemetryStore = () => {
|
||||||
|
@ -27,6 +28,16 @@ const createTelemetryStore = () => {
|
||||||
return store
|
return store
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
set: (key: keyof TelemetryData, value: any) => {
|
||||||
|
let newObj = {
|
||||||
|
...get(telemetryStore),
|
||||||
|
}
|
||||||
|
newObj = {
|
||||||
|
...newObj,
|
||||||
|
[key]: value,
|
||||||
|
}
|
||||||
|
set(newObj)
|
||||||
|
},
|
||||||
reset: () => set(defaults),
|
reset: () => set(defaults),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
import { get } from 'svelte/store'
|
||||||
|
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.
|
||||||
*
|
*
|
||||||
|
@ -5,7 +8,13 @@
|
||||||
* @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'
|
||||||
export default function getVoicePath(audio: string, lang: SupportedLanguage) {
|
|
||||||
|
export default function getVoicePath(audio: string, lang?: SupportedLanguage) {
|
||||||
|
console.log(get(settingsStore).voiceLang)
|
||||||
|
if (!lang) {
|
||||||
|
return `/static/voices/${get(settingsStore).voiceLang}/${audio}.wav`
|
||||||
|
}
|
||||||
|
|
||||||
return `/static/voices/${lang}/${audio}.wav`
|
return `/static/voices/${lang}/${audio}.wav`
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { io } from 'socket.io-client'
|
import { get } from 'svelte/store'
|
||||||
import { telemetryStore } from '../stores/telemetryStore'
|
import { telemetryStore } from '../stores/telemetryStore'
|
||||||
|
import { emit, listen } from '@tauri-apps/api/event'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects to sockets and subscribes to specified topics to receive telemetry data.
|
* Connects to sockets and subscribes to specified topics to receive telemetry data.
|
||||||
|
@ -14,7 +15,7 @@ const onUpdate = (data: TelemetryData) => {
|
||||||
// console.log(data)
|
// console.log(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const initializeTelemetry = (
|
export const initializeTelemetry = async (
|
||||||
topics: TelemetryTopics,
|
topics: TelemetryTopics,
|
||||||
refreshRate: number
|
refreshRate: number
|
||||||
) => {
|
) => {
|
||||||
|
@ -25,20 +26,24 @@ export const initializeTelemetry = (
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const socket = io('localhost:1280')
|
const unlistenStatus = await listen('telemetry_status', event => {
|
||||||
socket.on('connect', () => {
|
if (event.payload === 'connected') {
|
||||||
console.log('Socket-IO connected!')
|
telemetryStore.set('connected', false)
|
||||||
socket.emit('subscribe', topics)
|
} else if (event.payload === 'disconnected') {
|
||||||
console.log(`Subscribing to topics: ${JSON.stringify(topics)}`)
|
telemetryStore.set('connected', false)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('subscribed', () => {
|
const unlistenTelemetry = await listen('telemetry_data', event => {
|
||||||
console.log('Successfully subscribed to requested topics!')
|
const data = JSON.parse(event.payload as string)
|
||||||
socket.emit('request_data', { refresh_rate: refreshRate })
|
// console.log(JSON.parse)
|
||||||
console.log(`Refreshing at ${refreshRate} Hz`)
|
telemetryStore.set(data['topic_name'], data['data'])
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('telemetry_data', (data: string) => {
|
const unlistenAll = () => {
|
||||||
onUpdate(JSON.parse(data))
|
unlistenStatus()
|
||||||
})
|
unlistenTelemetry()
|
||||||
|
}
|
||||||
|
|
||||||
|
return unlistenAll
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue