256 Commits

Author SHA1 Message Date
Rune Harlyk 3c12ef332e ️Improve eventbus static alloc 2025-07-10 11:17:50 +02:00
Rune Harlyk 481dfaf8e5 🤹 Update adapters with better handling 2025-07-08 22:28:46 +02:00
Rune Harlyk 6769ffeb20 🎨 Moving project to use event bus 2025-07-08 21:59:59 +02:00
Rune Harlyk 0586775849 Adds more messages 2025-07-08 18:17:33 +02:00
Rune Harlyk 4766f47e7e ⚰️ Removes old topic 2025-07-08 18:16:24 +02:00
Rune Harlyk d2d7d8e323 🚩 Updates build flags 2025-07-08 18:15:39 +02:00
Rune Harlyk c5155fe641 Adds servo to topics 2025-07-08 15:22:36 +02:00
Rune Harlyk f1312fb5c6 Adds hasSubscribers function to event bus 2025-07-08 15:20:26 +02:00
Rune Harlyk a592848f34 Adds new messages 2025-07-08 15:20:01 +02:00
Rune Harlyk 06b05b2dc1 🚌 Adds eventbus with bluetooth adapter 2025-07-08 01:25:35 +02:00
Rune Harlyk 01d46f283b 👔 Update model utils to be able to load both urdf and xacro 2025-07-02 22:55:31 +02:00
Rune Harlyk 7c8c5b40a1 👔 Update visualization to better align with robot 2025-07-02 22:55:00 +02:00
Rune Harlyk 632f603fda 👔 Calculate default feet positions from kinematics 2025-07-02 22:53:58 +02:00
Rune Harlyk 4101ad033c 🐛 Expand allowed _numberEndpoints 2025-06-30 22:00:52 +02:00
Rune Harlyk 3ee096bfab 🚸 Update default feet positions 2025-06-30 22:00:26 +02:00
Rune Harlyk 753e692fe2 🔧 Adds support for Yertle legs
https://github.com/Jerome-Graves/yertle/
2025-06-27 22:50:25 +02:00
Rune Harlyk 40025a55c3 💄 Simplify calibration UX 2025-06-27 22:39:18 +02:00
Rune Harlyk 98262b2efc 🗃️ Improves UI filesystem interface 2025-05-24 19:23:46 +02:00
Rune Harlyk 01e174f337 🧃 Adds IMU orientations indicator 2025-05-17 12:37:06 +02:00
Rune Harlyk a9fea7fd56 🎍 Updates feature flags and adds BNO055 2025-05-17 11:57:00 +02:00
Rune Harlyk e09ec81f1d 🤹 Adds option for direct control of multiple servos 2025-05-15 19:59:06 +02:00
Rune Harlyk ee17f6862c 👆 Fixes on click for system status view 2025-05-05 20:56:34 +02:00
Rune Harlyk 8be7546eba 🎍 Updates reset reason mapping 2025-04-21 13:14:57 +02:00
Rune Harlyk e156b732eb 🏎️ Simplifies kinematics by removing matrix muls 2025-04-20 14:48:43 +02:00
Rune Harlyk 20c5a8ee92 🎮 Adds gamepad api control 2025-04-18 21:17:06 +02:00
Rune Harlyk dac21a499f 🪻 Hides menu overflow-x 2025-04-03 10:08:51 +02:00
Rune Harlyk 9a6c240140 🎋 Updates adafruit pwm lib to own fork until pr merged 2025-03-29 14:13:52 +01:00
Rune Harlyk 8733ecd9b7 ⏱️ Updates the frequency of main control loop from 100 hz to 200 2025-03-29 14:13:52 +01:00
Rune Harlyk fba531d3e8 🫅 Updates spot control task priority 3 -> 5 2025-03-29 14:13:52 +01:00
Rune Harlyk fc04d1b8d6 ✍️ Updates I2C freq to Fast Mode Plus 2025-03-29 14:13:52 +01:00
Rune Harlyk 4c33a75164 ✍️ Adds bulk writing of pwm values to PCA9685 2025-03-29 14:13:52 +01:00
Rune Harlyk 6015e67d05 🧼 Clean up MDNS UI 2025-03-23 20:14:01 +01:00
Rune Harlyk f59f32ce26 🧼 Removes unused imports 2025-03-23 20:14:01 +01:00
Rune Harlyk 3671610860 🖥️ Adds mDNS service 2025-03-23 20:14:01 +01:00
Rune Harlyk c346f7f553 🚇 Enables metrics in ui 2025-03-23 16:52:24 +01:00
Rune Harlyk f864616303 🖨️ Adds printing of feature flags 2025-03-23 16:44:22 +01:00
Rune Harlyk ad2d28c9ba ⚒️ Enables bigger range of motion for servo controller 2025-03-23 16:25:46 +01:00
Rune Harlyk 967923321f 📦 Use std:move for callback 2025-03-23 16:25:12 +01:00
Rune Harlyk 6b7e3281cf 🎋 Updates kinematics with modifiers 2025-03-23 16:24:26 +01:00
Rune Harlyk fdf70f7eb8 ⚒️ Updates build workflow file 2025-03-23 16:18:57 +01:00
Rune Harlyk e4cb035ad9 📦 Moves platform ini to root 2025-03-23 16:18:57 +01:00
Rune Harlyk c02938b567 💫 Update menu styling 2025-03-23 16:06:20 +01:00
TitanDynamics c24740e8ec Add Servo Motor Designations
PCA9685 Servo PWM numbers to joint:
PWM_0: Front Left Shoulder 
PWM_1: Front Left Upper-Limb
PWM_2: Front Left Leg (Lower-Limb)
PWM_3: Front Right Shoulder
PWM_4: Front Right Upper-Limb
PWM_5: Front Right Leg (Lower-Limb)
PWM_6: Rear Left Shoulder
PWM_7: Rear Left Upper-Limb
PWM_8: Rear Left Leg (Lower-Limb)
PWM_9: Rear Right Shoulder
PWM_10: Rear Right Upper-Limb
PWM_11: Rear Right Leg (Lower-limb)
2025-03-21 09:32:44 +01:00
TitanDynamics e0d3912d83 Fixed Grammatical Errors and updated documentation. 2025-03-21 09:32:44 +01:00
Rune Harlyk b113a30942 🥷 Adds i2c configurator 2025-03-20 15:49:53 +01:00
Rune Harlyk 9534529e50 🎋 Adds i2c configuration type 2025-03-20 15:49:53 +01:00
Rune Harlyk 23a41d26b1 🎋 Makes icon optional for status item 2025-03-20 15:49:53 +01:00
Rune Harlyk 569c19ad1d 🧼 Cleans up setting card 2025-03-20 15:49:53 +01:00
Rune Harlyk 17e30ebfe9 🧼 Simplifies and updates color scheme for confirm 2025-03-20 15:49:53 +01:00
Rune Harlyk 170e180c11 🌌 Adds edit icons 2025-03-20 15:49:53 +01:00
Rune Harlyk 5a24038d68 📂 Fixes file system view 2025-03-08 16:18:42 +01:00
Rune Harlyk 99660b9a23 🧼 Refactors wifi and ap to use StatusItem 2025-03-08 14:48:48 +01:00
Rune Harlyk 72f3bcfd78 🌌 Makes front page simplere 2025-03-08 13:22:41 +01:00
Rune Harlyk 297d61c188 🌌 Adds svelte adaptor back 2025-03-08 01:12:07 +01:00
Rune Harlyk 382a58fc53 💅 Refactors system status view 2025-03-08 01:11:23 +01:00
Rune Harlyk 5daa299895 📂 Updates filessystem view 2025-03-08 00:45:24 +01:00
Rune Harlyk af6609c97b 💅 Updates status bare page store 2025-03-08 00:06:24 +01:00
Rune Harlyk aca258268f 🕹️ Updates keyboard controls 2025-03-08 00:05:58 +01:00
Rune Harlyk 9ff6dd7d4f 🍇 Bumps vite version 2025-03-07 23:19:23 +01:00
Rune Harlyk 40509cdd1f 🧼 Removes battery indicator 2025-03-07 21:43:49 +01:00
Rune Harlyk 37f9238c55 🕹️ Fixes vertical range input 2025-03-07 21:42:38 +01:00
Rune Harlyk d14446d09e 💅 Updates prettier config 2025-03-07 21:20:20 +01:00
Rune Harlyk 74cb4aaee4 💫 Migrate to TailwindCSS 4 and DaisyUI 5 2025-03-07 21:17:47 +01:00
Rune Harlyk d90dbbcf21 🧼 Migrates svelte-modals to 2.0 2025-03-01 23:34:18 +01:00
Rune Harlyk 31dd7f7ba4 🧼 Updates menu list 2025-03-01 23:34:18 +01:00
Rune Harlyk 113ac1bc2c 🧼 Updates inputs after svelte migration 2025-03-01 23:34:18 +01:00
Rune Harlyk 0b01634a20 🐛 Updates svelte-modals 2025-03-01 23:34:18 +01:00
Rune Harlyk 788f4ffea3 🌌 Migrate app to svelte-5 2025-02-26 22:40:40 +01:00
Rune Harlyk d9285bbdc0 🐛 Makes menu items stay open 2025-02-26 21:47:34 +01:00
Rune Harlyk a4eca1460e 🧼 Removes unnecessary config for xiao esp32s3 2025-02-06 19:19:26 +01:00
Rune Harlyk 0c0061c9e0 🪵 Adds logging to gait performance test 2025-02-05 10:07:57 +01:00
Rune Harlyk 88fec1e5d2 💐 Updates package manager order - default npm 2025-02-05 09:38:04 +01:00
Rune Harlyk d4b485160b 🕊️ Adds import after install 2025-02-05 09:32:28 +01:00
Rune Harlyk 97030e53a1 🎈 Updates action upload artifact to v4 2025-02-04 20:47:24 +01:00
Rune Harlyk 3da6e3c043 📃 Adds board config for seeed xiao esp32 s3 2025-02-04 20:47:24 +01:00
Rune Harlyk 625b228103 🛜 Faster connection for one network 2025-01-17 09:22:43 +01:00
Rune Harlyk 6ae44241d5 🧮 Updates min pwm value 2025-01-17 09:21:56 +01:00
Rune Harlyk 1174fa4e12 🐛 Fixes undefined I2C behavior 2025-01-17 09:21:39 +01:00
Rune Harlyk 01cbd7c117 💐 Updates @playwright/test 2025-01-07 09:58:43 +01:00
Rune Harlyk 1caa0ff96e 🦠 Fixes the servo switch actions 2025-01-07 09:58:43 +01:00
Rune Harlyk d83dd0badb 📃 Adds initial calibration guiding 2025-01-06 13:29:27 +01:00
Rune Harlyk da0c1de47d 🧽 Makes servo testing and calibration easier 2024-11-26 20:50:11 +01:00
Rune Harlyk 15ec137edb 🧼 Removes WWWData.h 2024-11-23 17:13:03 +01:00
Rune Harlyk abdf763215 🧽 Refactors stateful service 2024-11-23 13:54:10 +01:00
Rune Harlyk a7c5a5f1cf 📛 Renames members 2024-11-23 13:00:31 +01:00
Rune Harlyk b37e8706a6 📛 Renames cb to callback 2024-11-23 12:57:17 +01:00
Rune Harlyk e109f3584a 🆘 Adds helper function 2024-11-23 12:56:54 +01:00
Rune Harlyk 8792c06e8a 🫏 Adds getter for state 2024-11-23 12:54:15 +01:00
Rune Harlyk 852ff91b7d 🌊 Adds control flow, kinematic and motion controller docs 2024-11-19 17:25:37 +01:00
Rune Harlyk ba9d8e1bec 📃 Adds more docs 2024-11-16 00:32:51 +01:00
Rune Harlyk b7882ee6cf 🪡 Makes control loop frequency const value 2024-11-16 00:32:17 +01:00
Rune Harlyk d8ca913188 🪡 Moves server on robot facade 2024-11-16 00:31:37 +01:00
Rune Harlyk ad86bc5fd4 🪵 Adds log for use of default settings 2024-11-15 22:40:44 +01:00
Rune Harlyk f04cdaa031 🌹 Updates kinematic L3 parameter 2024-11-15 20:14:00 +01:00
Rune Harlyk 4b909adfb7 🧼 Renames kinematics 2024-11-14 20:58:24 +01:00
Rune Harlyk f75b224a76 🧼 Renames service 2024-11-14 19:41:05 +01:00
Rune Harlyk 1a6e3626f6 ☁️ Makes barometer facade 2024-11-14 16:55:13 +01:00
Rune Harlyk b5a8fe88ca 🧼 Makes magnometer own class 2024-11-14 16:55:13 +01:00
Rune Harlyk d5b003ab94 🧼 Removes ntp 2024-11-14 16:12:04 +01:00
Rune Harlyk 24d39e540e 📦 Moves templates 2024-11-14 15:56:23 +01:00
Rune Harlyk b6fe8844f4 🧼 Moves camera to peripherals 2024-11-14 15:52:53 +01:00
Rune Harlyk 62f3ee1bcb 🧼 Renames stateful socket service 2024-11-14 15:50:51 +01:00
Rune Harlyk 07dc8b1d49 🧼 Removes display service 2024-11-14 15:46:23 +01:00
Rune Harlyk 9c600f0773 📦 Moves math utils 2024-11-14 15:45:43 +01:00
Rune Harlyk 66b1b8fa1b 🧼 Moves led and servo controller 2024-11-14 15:44:24 +01:00
Rune Harlyk eeff317abe 🧼 Makes appname be a const char* 2024-11-14 15:38:33 +01:00
Rune Harlyk b346e104d4 📦 Moves all i2c servo control to controller 2024-11-14 15:38:19 +01:00
Rune Harlyk c9548e2da1 🧼 Removes extern adc 2024-11-14 15:20:23 +01:00
Rune Harlyk 841ae91c33 🧼 Removes battery service 2024-11-14 15:20:23 +01:00
Rune Harlyk f2d86115fb 📦 Moves utilities to own folder 2024-11-14 15:12:25 +01:00
Rune Harlyk 0d596d9d3c 📷 Merges camera setting service and camera service 2024-11-14 15:05:50 +01:00
Rune Harlyk 0cce6075b9 🧼 Only write angles once 2024-11-14 11:40:40 +01:00
Rune Harlyk 0b1c27819e 🚒 Updates firmware download service interface 2024-11-14 11:27:01 +01:00
Rune Harlyk 35c9f54f52 🗨️ Updates comment casing 2024-11-14 11:26:31 +01:00
Rune Harlyk f4bf5562f4 🛢️ Makes mdns use class member 2024-11-14 11:20:40 +01:00
Rune Harlyk 57c126a7bc 🎮 Makes controller sub for imu updates 2024-11-14 11:19:35 +01:00
Rune Harlyk 35e1cc678a 🎐 Makes analytics a part of system service 2024-11-14 11:19:11 +01:00
Rune Harlyk f3d2fec0e9 🌹 Switches to an explicit sense plan act flow 2024-11-14 10:51:38 +01:00
Rune Harlyk e919b2aa41 🪮 Moves imu definition to own service 2024-11-14 10:15:49 +01:00
Rune Harlyk 09f5460db7 🧼 Renames stateful service 2024-11-14 09:43:12 +01:00
Rune Harlyk c92a931846 🚨 Adds feature check for led service 2024-11-13 22:05:31 +01:00
Rune Harlyk 8f64edc3e4 🪛 Revert gait body shift update 2024-11-13 22:05:11 +01:00
Rune Harlyk 7f03790cf7 🕊️ Renames gait folder 2024-11-12 13:34:31 +01:00
Rune Harlyk 622e15278f 🕊️ Renames gait folder 2024-11-12 13:34:31 +01:00
Rune Harlyk 118caff7ba ♨️ Udate performance test 2024-11-12 13:34:31 +01:00
Rune Harlyk dcfce13f4b 🧼 Splits motions state to own files 2024-11-12 13:34:31 +01:00
Rune Harlyk 8283e24407 🦠 Ignores pycache 2024-11-12 13:06:34 +01:00
Rune Harlyk 2f8bcaa291 🧼 Removes HttpEndpoint 2024-11-12 11:58:56 +01:00
Rune Harlyk 426b4a1332 🧼 Removes the need to pass reference to fs 2024-11-12 11:54:10 +01:00
Rune Harlyk 316b1a52cb 🎐 Makes Analytics service use system service for metrics 2024-11-12 11:50:28 +01:00
Rune Harlyk 1b7ae688a6 🧼 Removes server dependencies from service 2024-11-12 11:28:01 +01:00
Rune Harlyk 9ca42dbc69 🔦 Turn on ring led firstly at boot 2024-11-12 11:11:12 +01:00
Rune Harlyk d52a15eff7 ♨️ Adds test for gait performance 2024-11-11 20:43:29 +01:00
Rune Harlyk 9dc6742e82 ♨️ Implements more use of std for gait 2024-11-11 20:43:29 +01:00
Rune Harlyk c1e12bffe8 🗺️ Makes visualizer use s1 slider for step height 2024-11-11 20:43:29 +01:00
Rune Harlyk fc0914ded4 🏏 Updates gait to use leg phase time instead of contact phase 2024-11-11 20:43:29 +01:00
Rune Harlyk f57d798971 🎐 Updates body shift amount 2024-11-11 15:52:43 +01:00
Rune Harlyk 2de1238405 🎍 Moves static definitions of bezier steps and height 2024-11-11 15:52:43 +01:00
Rune Harlyk 0fd729be4a 🐳 Makes phase power JIT 2024-11-11 15:52:43 +01:00
Rune Harlyk ea42cc0aac 🫚 Makes combinatorial a constant expression 2024-11-11 15:52:43 +01:00
Rune Harlyk 7046957669 🥽 Adds rotation to the native implementation 2024-11-11 15:52:43 +01:00
Rune Harlyk 634b3292b4 🥽 Makes use of Atan2 to calculate lateral fraction 2024-11-11 15:52:43 +01:00
Rune Harlyk 4de6be7815 👟 Implementes bezier gait in c++ 2024-11-11 15:52:43 +01:00
Rune Harlyk da27ba37be 🧼 Refactors bezier gait 2024-11-11 15:52:43 +01:00
Rune Harlyk 87fe566d0d 🫚 Adds bezier gait 2024-11-11 15:52:43 +01:00
Rune Harlyk ea5b16de0c 🎛️ Renames timing 2024-11-08 17:29:57 +01:00
Rune Harlyk 386f1c627d 🐹 Renames member 2024-11-08 17:24:06 +01:00
Rune Harlyk e77de7dbdb 📦 Update firmware service 2024-11-08 17:23:46 +01:00
Rune Harlyk a7eec4f7f2 Refactors feature service 2024-11-08 17:03:31 +01:00
Rune Harlyk 4fff03ce54 Updates stateful service 2024-11-08 16:56:06 +01:00
Rune Harlyk 9be13d1df5 📦 Moves setting to folder 2024-11-08 16:30:50 +01:00
Rune Harlyk 698b7fbba9 🕊️ Makes task manager global 2024-11-08 15:09:03 +01:00
Rune Harlyk a3fc3eca2e Adds typing to joint name store 2024-11-08 10:36:09 +01:00
Rune Harlyk 6ce4747b4b 🕊️ Renames define 2024-11-08 10:35:51 +01:00
Rune Harlyk 89611b5e3e Refactors event socket 2024-11-07 17:12:21 +01:00
Rune Harlyk fd652bd967 Refactors filesystem service 2024-11-07 16:57:59 +01:00
Rune Harlyk 3a3de53752 Refactors ntp service 2024-11-07 16:45:19 +01:00
Rune Harlyk 10b0aa3c45 Updates Leika cover image 2024-11-04 20:54:03 +01:00
Rune Harlyk d587b42987 🪈 Updates pai path for ap and sta 2024-11-04 08:50:15 +01:00
Rune Harlyk 9f3c4ffdf2 🎛️ Adds system service 2024-11-04 08:50:15 +01:00
Rune Harlyk 3054d5eb12 🧼 Removes unused factory setting 2024-11-04 08:50:15 +01:00
Rune Harlyk 84633e5707 🔐 Removes auth from frontend 2024-11-04 08:50:14 +01:00
Rune Harlyk 1c6b9f79c5 🔐 Removes auth from backend 2024-11-04 08:50:14 +01:00
Rune Harlyk 9923b66208 🧍‍♂️ Adds stl files for robot stand 2024-11-04 08:50:14 +01:00
Rune Harlyk 48d8b4f958 💐 Updates tailwind config to import daisyui 2024-11-04 08:50:10 +01:00
Rune Harlyk 490207c9ff Updates pnpm version for frontend-tests.yml 2024-11-04 08:49:14 +01:00
Rune Harlyk 91156c42ae 🧟‍♀️ Updates import for the github firmware manager 2024-11-04 08:49:11 +01:00
Rune Harlyk 7dd5797481 🐹 Fixes crawl gait body shift order 2024-10-27 11:43:53 +01:00
Rune Harlyk 7849f77712 🧼 Formats components 2024-09-12 23:40:49 +02:00
Rune Harlyk 1990501a66 🗃️ Update servoController file save 2024-09-12 23:40:49 +02:00
Rune Harlyk aad698f486 🤖 Updates gait params 2024-09-12 23:40:49 +02:00
Rune Harlyk 6ab093786a 🏮 Adds servo config table 2024-09-12 23:40:49 +02:00
Rune Harlyk b02d633f41 📏 Updates umbrella class structure 2024-09-09 14:33:37 +02:00
Rune Harlyk d2094aa527 🍎 Adds cross env in build script 2024-09-03 21:47:23 +02:00
Rune Harlyk 4d304cb567 🪕 Adds string utilities 2024-09-03 21:47:23 +02:00
Rune Harlyk fb06437c43 🍎 Update import casing 2024-09-03 21:47:23 +02:00
Rune Harlyk 5e0b31aaf2 🍎 Updates ui 2024-09-03 21:47:23 +02:00
Rune Harlyk 756f1c0148 🛜 Simplifies ap service 2024-09-03 21:47:23 +02:00
Rune Harlyk 8ac2fad1b1 🛜 Simplify wifi services 2024-09-03 21:47:23 +02:00
Rune Harlyk e69e48533f 🛜 Simplify wifi services 2024-09-03 20:57:24 +02:00
Rune Harlyk 9dbe31d207 🍎 Adds cross-env 2024-09-03 20:00:45 +02:00
Rune Harlyk 2bffac6558 🏫 Collects all config file paths in espfs 2024-09-03 19:56:53 +02:00
Rune Harlyk db203e1503 🍎 Update partion scheme 2024-09-03 19:49:30 +02:00
Rune Harlyk 97e0512dc3 Redo workflow cache changes 2024-09-03 18:26:55 +02:00
Rune Harlyk e8605336df 🃏 Adds mobile capable meta tag 2024-08-26 20:37:59 +02:00
Rune Harlyk 818ed06a9a 🦄 Updates prettier config with spaces over tabs 2024-08-26 20:37:59 +02:00
Rune Harlyk 539600b0d3 🗽 Makes selector sm by default 2024-08-26 20:37:59 +02:00
Rune Harlyk a4d8f0f613 🚝 Adds gap between hamburger and view selector 2024-08-26 20:37:59 +02:00
Rune Harlyk d903bd5a1c 🃏 Makes layout connect to event socket correctly 2024-08-26 20:37:59 +02:00
Rune Harlyk fb8ee64ee4 🦄 Adds build command with env 2024-08-26 20:37:59 +02:00
Rune Harlyk ac17be696b 🐙 Makes connection route conditional 2024-08-26 20:37:59 +02:00
Rune Harlyk 787d202a91 🐙 Adds types for the env 2024-08-26 20:37:59 +02:00
Rune Harlyk ea7f7dc544 🚿 Adds connection route to menu 2024-08-26 20:37:59 +02:00
Rune Harlyk bbd7d75b92 🍭 Updates the event socket url to be dynamic 2024-08-26 20:37:59 +02:00
Rune Harlyk 2d6466050b 🦄 Adds route for changing robot connection 2024-08-26 20:37:59 +02:00
Rune Harlyk 73c2038497 🚿 Adds api function to ensure a host 2024-08-26 20:37:59 +02:00
Rune Harlyk 16e653afa8 🍭 Updates the persistent store to save onload 2024-08-26 20:37:59 +02:00
Rune Harlyk ce1558a6ab 🧼 Removes embedded build flag 2024-08-26 20:37:59 +02:00
Rune Harlyk 3ac81b376d 🎥 Updates stream location to be dynamic 2024-08-26 20:37:59 +02:00
Rune Harlyk cd3ad93196 🍁 Updates location store 2024-08-26 20:37:59 +02:00
Rune Harlyk a63b6b3633 🔮 Adds environment caster dependency 2024-08-26 20:37:59 +02:00
Rune Harlyk d77010ad41 🛹 Adds a persistentStore for location 2024-08-26 20:37:59 +02:00
Rune Harlyk 92184e9456 🦋 Adorns readme.md 2024-08-22 14:54:03 +02:00
Rune Harlyk 092b19ae40 🥫 Updates prettier config 2024-08-20 00:18:31 +02:00
Rune Harlyk 44fa0bd3e2 🍧 Updates imports to use barrel exports 2024-08-19 23:53:38 +02:00
Rune Harlyk 62fa5f79b6 🍧 Collects all icons 2024-08-19 23:45:59 +02:00
Rune Harlyk 42405ec93f 🧼 Simplifies the menu structure 2024-08-19 23:06:53 +02:00
Rune Harlyk d07b0b5d7f 🥀 Adds listener for feature flags 2024-08-19 22:18:07 +02:00
Rune Harlyk b9f24d9f4c 🧞‍♂️ Adds logging of the AP name 2024-08-19 22:10:31 +02:00
Rune Harlyk 2c5ac4dc5c 🛸 Moves PhychicHTTP lib from /libs 2024-08-19 22:06:45 +02:00
Rune Harlyk a19d6a2f4f 🆚 Updates psychic version to 1.2.1 2024-08-19 22:06:45 +02:00
Rune Harlyk 951bfb4cd2 📦 Moves camera to own namespace 2024-08-19 21:49:51 +02:00
Rune Harlyk 586dbc7a9a 🏕️ Renames the feature flag 2024-08-19 21:49:51 +02:00
Rune Harlyk cf55bf509e 🧼 Moves adafruit libs to platform 2024-08-19 21:49:51 +02:00
Rune Harlyk 1ecc30fb21 🛸 Makes menu reactive to features 2024-08-19 21:34:11 +02:00
Rune Harlyk 47dd527c70 🧼 Makes fullscreen button a button 2024-08-19 21:34:11 +02:00
Rune Harlyk d4c40a2a53 🧼 Updates password input component 2024-08-19 21:34:11 +02:00
Rune Harlyk a0c58841d7 📦 Moves models to /types 2024-08-19 21:34:11 +02:00
Rune Harlyk cfa729ff70 ⛹️‍♂️ Makes feature flag store only run once 2024-08-19 21:34:11 +02:00
Rune Harlyk 7ba5b5118a 🪇 Refactors file-service to handle non-browser context 2024-08-19 21:34:11 +02:00
Rune Harlyk 3da1717341 🪇 Implements major structure and service refactors 2024-08-19 21:34:11 +02:00
Rune Harlyk 9978918bf9 📦 Moves statusbar to own component 2024-08-19 21:34:11 +02:00
Rune Harlyk 904a1c5852 🏕️ Updates cache for embedded-build.yml 2024-08-19 11:38:46 +02:00
Rune Harlyk 420428ec3e 🧼 Clean up esp32Sveltekit 2024-08-18 17:06:05 +02:00
Rune Harlyk ae7b1d8c99 📦 Moves notebook to dedicated folder 2024-08-18 17:06:05 +02:00
Rune Harlyk bdc535472d 🧼 Removes NN from main 2024-08-18 17:06:05 +02:00
Rune Harlyk ef4e476b89 📦 Moves NN 2024-08-18 17:06:05 +02:00
Rune Harlyk d33ffc7d95 🤖 Fixes model input type 2024-08-18 17:06:05 +02:00
Rune Harlyk 1dd5cb631a 🦼 Adds test for convert status 2024-08-18 17:06:05 +02:00
Rune Harlyk 4e530b4bff 🦘 Adds test script to train sin and convert model to tf lite 2024-08-18 17:06:05 +02:00
Rune Harlyk c32e327320 🍇 Adds tensorflow sin example 2024-08-18 17:06:05 +02:00
Rune Harlyk c9c6125462 🤖 Adds tensorflow lite micro lib 2024-08-18 17:06:05 +02:00
Rune Harlyk 2827b7c1b5 🎬 Adds missing state in UI 2024-08-18 16:33:37 +02:00
Rune Harlyk 314a4939e2 🦾 Updates default state to deactivated 2024-08-18 16:30:37 +02:00
Rune Harlyk e805f017b9 🌀 Adds mathutils module types 2024-08-18 16:30:09 +02:00
Rune Harlyk ed2a2b5c83 🧹 Renames layout to page 2024-08-18 16:27:37 +02:00
Rune Harlyk 75fc3d9809 🌀 Adds uzip types 2024-08-18 16:26:58 +02:00
Rune Harlyk a86b2fa50e 📦 Moves all model loading to model-utilities 2024-08-18 16:26:42 +02:00
Rune Harlyk 296adfee51 ⛹️‍♂️ Simplifies layout handling 2024-08-18 13:18:25 +02:00
Rune Harlyk 00c56a2d68 🎚️ Expands layout manager with widget sizing 2024-08-18 13:18:25 +02:00
Rune Harlyk 3fd7f28d7e 📱 Adds wrap mode for phones 2024-08-18 13:18:25 +02:00
Rune Harlyk d8659f8ed5 👱‍♂️ Adds layout manager 2024-08-18 13:18:25 +02:00
Rune Harlyk 5e2f34f792 🎨 Updates themes colors 2024-08-17 21:42:47 +02:00
Rune Harlyk 63459acc7f 🎨 Updates joystick colors to be darker 2024-08-17 21:29:56 +02:00
Rune Harlyk db01879419 🎬 Adds fullscreen button 2024-08-17 21:29:27 +02:00
Rune Harlyk ce8b48b101 🚫 Adds big red stop button 2024-08-17 21:29:06 +02:00
Rune Harlyk 42607df3d6 🖼️ Updates controller demo gif 2024-08-17 21:05:01 +02:00
Rune Harlyk af6015d6a0 🎯 Hides position target by default 2024-08-17 20:48:34 +02:00
Rune Harlyk 1a3dabbc1e 🧹 Clean up components 2024-08-17 20:48:34 +02:00
Rune Harlyk 8afe3424d3 🕯️Updates scene ligthing 2024-08-17 20:48:34 +02:00
Rune Harlyk 0e89643555 🎚️ Simplifies vertical slider 2024-08-17 20:48:34 +02:00
Rune Harlyk 41c22399dc 🧣 Adds minimum and maximum viewport scale 2024-08-17 20:48:34 +02:00
Rune Harlyk 89ddd58935 🪴 Updates dark theme primary color 2024-08-17 20:48:34 +02:00
Rune Harlyk d6b3793275 〰️ Updates controller layout 2024-08-17 20:48:34 +02:00
Rune Harlyk 6988c61a50 🪞 Adds mirror effect on groundplane 2024-08-17 20:48:34 +02:00
Rune Harlyk 3f6348c49c 🎚️ Adds a vertical slider component 2024-08-17 20:48:34 +02:00
530 changed files with 70558 additions and 65423 deletions
+9 -10
View File
@@ -2,20 +2,19 @@ name: PlatformIO CI
on: on:
push: push:
branches: [ master ] branches: [master]
paths: paths:
- 'esp32/**' - "esp32/**"
- "platformio.ini"
pull_request: pull_request:
branches: [ master ] branches: [master]
paths: paths:
- 'esp32/**' - "esp32/**"
- "platformio.ini"
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults:
run:
working-directory: ./esp32
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -28,8 +27,8 @@ jobs:
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: with:
python-version: '3.x' python-version: "3.x"
- run: pip install -r ./scripts/requirements.txt - run: pip install -r esp32/scripts/requirements.txt
- name: Install PlatformIO Core - name: Install PlatformIO Core
run: pip install --upgrade platformio run: pip install --upgrade platformio
@@ -37,7 +36,7 @@ jobs:
run: pio run run: pio run
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: build-artifacts name: build-artifacts
path: esp32/build/firmware path: esp32/build/firmware
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 9
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
+5
View File
@@ -2,3 +2,8 @@
.vscode/c_cpp_properties.json .vscode/c_cpp_properties.json
.vscode/launch.json .vscode/launch.json
.vscode/ipch .vscode/ipch
__pycache__/
*.py[cod]
*$py.class
.pio
+11 -6
View File
@@ -1,8 +1,13 @@
{ {
"useTabs": true, "useTabs": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "tabWidth": 2,
"printWidth": 100, "trailingComma": "none",
"plugins": ["prettier-plugin-svelte"], "arrowParens": "avoid",
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] "experimentalTernaries": true,
"printWidth": 100,
"semi": false,
"svelteBracketNewLine": false,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }
+8 -17
View File
@@ -8,30 +8,21 @@ If you're seeing this, you've probably already done this step. Congrats!
```bash ```bash
# create a new project in the current directory # create a new project in the current directory
npm create svelte@latest npx sv create
# create a new project in my-app
npm create svelte@latest my-app
``` ```
## Developing ## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: Once you've created your project, follow these steps:
```bash 1: Delete package-lock.json
npm run dev 2: Check `git status`. If you see any changes other than package-lock.json or favicon.ico, run the command `git restore ./` (See below)
3: Run `npm install` or `pnpm install` or `yarn` to install the dependencies
4: Run `npm run build` to build the project
# or start the server and open the app in a new browser tab Running `git status` should show:
npm run dev -- --open
```
## Building [![example.png](https://i.postimg.cc/yddM3hH3/example.png)](https://postimg.cc/7CFsp2bq)
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`. You can preview the production build with `npm run preview`.
+8
View File
@@ -0,0 +1,8 @@
declare module "app-env" {
interface ENV {
VITE_USE_HOST_NAME: boolean;
}
const appEnv: ENV;
export default appEnv;
}
+65 -59
View File
@@ -1,61 +1,67 @@
{ {
"name": "spot_micro_controller", "name": "spot_micro_controller",
"version": "0.0.1", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite dev --host", "dev": "vite dev --host",
"build": "vite build", "build": "vite build",
"preview": "vite preview", "build:embedded": "cross-env VITE_USE_HOST_NAME=true vite build",
"test": "npm run test:integration && npm run test:unit", "preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "test": "pnpm run test:integration && pnpm run test:unit",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"lint": "prettier --check . && eslint .", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .", "lint": "prettier --check . && eslint .",
"test:integration": "playwright test", "format": "prettier --write .",
"test:unit": "vitest" "test:integration": "playwright test",
}, "test:unit": "vitest"
"devDependencies": { },
"@iconify-json/mdi": "^1.1.64", "devDependencies": {
"@iconify-json/tabler": "^1.1.109", "@iconify-json/mdi": "^1.1.64",
"@playwright/test": "^1.28.1", "@iconify-json/tabler": "^1.1.109",
"@sveltejs/adapter-auto": "^3.0.0", "@playwright/test": "^1.49.1",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.5.5", "@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^5.0.3",
"@types/eslint": "^8.56.0", "@types/eslint": "^8.56.0",
"@types/three": "^0.162.0", "@types/three": "^0.162.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.45.1",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"postcss": "^8.4.38", "prettier": "^3.1.1",
"prettier": "^3.1.1", "prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-svelte": "^3.1.2", "svelte": "^5.0.0",
"svelte": "^4.2.7", "svelte-check": "^4.0.0",
"svelte-check": "^3.6.0", "svelte-focus-trap": "^1.2.0",
"svelte-focus-trap": "^1.2.0", "tailwindcss": "^4.0.12",
"tailwindcss": "^3.4.3", "tslib": "^2.6.1",
"tslib": "^2.6.1", "typescript": "^5.5.0",
"typescript": "^5.1.6", "unplugin-icons": "^0.18.5",
"unplugin-icons": "^0.18.5", "vite": "^6.2.1",
"vite": "^5.0.3", "vitest": "^1.2.0"
"vitest": "^1.2.0" },
}, "type": "module",
"type": "module", "dependencies": {
"dependencies": { "@niku/vite-env-caster": "^1.0.2",
"chart.js": "^4.4.2", "@sveltejs/adapter-auto": "^4.0.0",
"compare-versions": "^6.1.0", "@tailwindcss/vite": "^4.0.12",
"daisyui": "^4.10.2", "chart.js": "^4.4.2",
"jwt-decode": "^4.0.0", "compare-versions": "^6.1.0",
"nipplejs": "^0.10.1", "cross-env": "^7.0.3",
"svelte-dnd-list": "^0.1.8", "daisyui": "^5.0.0",
"svelte-modals": "^1.3.0", "jwt-decode": "^4.0.0",
"three": "^0.162.0", "nipplejs": "^0.10.1",
"urdf-loader": "^0.12.1", "svelte-dnd-list": "^0.1.8",
"uzip": "^0.20201231.0", "svelte-modals": "^2.0.0",
"xacro-parser": "^0.3.9" "three": "^0.162.0",
} "urdf-loader": "^0.12.1",
"uzip": "^0.20201231.0",
"xacro-parser": "^0.3.9",
"@types/msgpack-lite": "^0.1.11",
"msgpack-lite": "^0.1.26"
},
"packageManager": "pnpm@9.3.0"
} }
+1353 -1115
View File
File diff suppressed because it is too large Load Diff
-5
View File
@@ -1,5 +0,0 @@
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
export default {
plugins: [tailwindcss(), autoprefixer()]
};
+27 -5
View File
@@ -1,9 +1,31 @@
@tailwind base; @import 'tailwindcss';
@tailwind components; @plugin "daisyui";
@tailwind utilities;
#nipple_0_0, #nipple_1_1 { @plugin "daisyui" {
z-index: 10!important; themes:
light --default,
dark --prefersdark;
}
@plugin "daisyui/theme" {
name: 'light';
default: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: white;
}
@plugin "daisyui/theme" {
name: 'dark';
prefersdark: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: oklch(0.3 0.012 256);
}
#nipple_0_0,
#nipple_1_1 {
z-index: 10 !important;
} }
#three-gui-panel { #three-gui-panel {
+3 -1
View File
@@ -3,7 +3,9 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo512.png" /> <link rel="icon" href="%sveltekit.assets%/logo512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
+60 -53
View File
@@ -1,73 +1,80 @@
import { user } from '$lib/stores/user';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import { Err, Ok, type Result } from './utilities'; import { Err, Ok, type Result } from './utilities';
import { location } from './stores';
export namespace api { export namespace api {
export function get<TResponse>(endpoint: string, params?: RequestInit) { export function get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params); return sendRequest<TResponse>(endpoint, 'GET', null, params);
} }
export function post<TResponse>(endpoint: string, data?: unknown) { export function post<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'POST', data); return sendRequest<TResponse>(endpoint, 'POST', data);
} }
export function put<TResponse>(endpoint: string, data?: unknown) { export function put<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'PUT', data); return sendRequest<TResponse>(endpoint, 'PUT', data);
} }
export function remove<TResponse>(endpoint: string) { export function remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE'); return sendRequest<TResponse>(endpoint, 'DELETE');
} }
} }
async function sendRequest<TResponse>( async function sendRequest<TResponse>(
endpoint: string, endpoint: string,
method: string, method: string,
data?: unknown, data?: unknown,
params?: RequestInit params?: RequestInit
): Promise<Result<TResponse, Error>> { ): Promise<Result<TResponse, Error>> {
const user_token = get(user).bearer_token; endpoint = resolveUrl(endpoint);
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined; const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
const request = { const request = {
...params, ...params,
method, method,
body, body,
headers: { headers: {
...params?.headers, ...params?.headers,
Authorization: user_token ? 'Bearer ' + user_token : 'Basic', Authorization: 'Basic',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}; };
let response; let response;
try { try {
response = await fetch(endpoint, request); response = await fetch(endpoint, request);
} catch (error) { } catch (error) {
return Err.new(new Error(), 'An error has occurred'); return Err.new(new Error(), 'An error has occurred');
} }
const isResponseOk = response.status >= 200 && response.status < 400; const isResponseOk = response.status >= 200 && response.status < 400;
if (!isResponseOk) { if (!isResponseOk) {
if (response.status === 401) { if (response.status === 401) {
return Err.new(new ApiError(response), 'User was not authorized'); return Err.new(new ApiError(response), 'User was not authorized');
} }
return Err.new(new ApiError(response), 'An error has occurred'); return Err.new(new ApiError(response), 'An error has occurred');
} }
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type'); const contentType =
if (contentType && contentType.includes('application/json')) { response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
const data = await response.json(); if (contentType && contentType.includes('application/json')) {
return Ok.new(data as TResponse); const data = await response.json();
} else { return Ok.new(data as TResponse);
// Handle empty object as response } else {
return Ok.new(null as TResponse); // Handle empty object as response
} return Ok.new(null as TResponse);
}
}
function resolveUrl(url: string): string {
if (url.startsWith('http') || !get(location)) return url;
const protocol = window.location.protocol;
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`;
} }
export class ApiError extends Error { export class ApiError extends Error {
constructor(public readonly response: Response) { constructor(public readonly response: Response) {
super(`${response.status}`); super(`${response.status}`);
} }
} }
@@ -1,27 +0,0 @@
<script lang="ts">
import Battery0 from '~icons/tabler/battery';
import Battery25 from '~icons/tabler/battery-1';
import Battery50 from '~icons/tabler/battery-2';
import Battery75 from '~icons/tabler/battery-3';
import Battery100 from '~icons/tabler/battery-4';
import BatteryCharging from '~icons/tabler/battery-charging-2';
export let current = 0;
export let voltage = 0;
</script>
<div class="tooltip tooltip-left z-10" data-tip="{voltage}V {Math.floor(current*10)/10} mA">
{#if voltage == 0}
<BatteryCharging class="{$$props.class || ''} -rotate-90 animate-pulse" />
{:else if voltage > 8.2}
<Battery100 class="{$$props.class || ''} -rotate-90" />
{:else if voltage > 8}
<Battery75 class="{$$props.class || ''} -rotate-90" />
{:else if voltage > 7.8}
<Battery50 class="{$$props.class || ''} -rotate-90" />
{:else if voltage > 7.6}
<Battery25 class="{$$props.class || ''} -rotate-90" />
{:else}
<Battery0 class="{$$props.class || ''} text-error -rotate-90 animate-pulse" />
{/if}
</div>
+38 -37
View File
@@ -1,43 +1,44 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import Down from '~icons/tabler/chevron-down'; import { Down } from './icons';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher(); function openCollapsible() {
open = !open;
if (open) {
opened();
} else {
closed();
}
}
function openCollapsible() { let { icon, title, children, open, opened, closed, class: klass } = $props();
open = !open;
if (open) {
dispatch('opened');
} else {
dispatch('closed');
}
}
export let open = false;
</script> </script>
<div class="{$$props.class || ''} relative grid w-full max-w-2xl self-center overflow-hidden"> <div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"> <div
<span class="inline-flex items-baseline"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
<slot name="icon" /> >
<slot name="title" /> <span class="inline-flex items-baseline">
</span> {@render icon?.()}
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => openCollapsible()}> {@render title?.()}
<Down </span>
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open <button class="btn btn-circle btn-ghost btn-sm" onclick={() => openCollapsible()}>
? 'rotate-180' <Down
: ''}" class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
/> open
</button> ) ?
</div> 'rotate-180'
{#if open} : ''}"
<div />
class="flex flex-col gap-2 p-4 pt-0" </button>
transition:slide|local={{ duration: 300, easing: cubicOut }} </div>
> {#if open}
<slot /> <div
</div> class="flex flex-col gap-2 p-4 pt-0"
{/if} transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div> </div>
+37 -46
View File
@@ -1,52 +1,43 @@
<script lang="ts"> <script lang="ts">
import { closeModal } from 'svelte-modals'; import { focusTrap } from 'svelte-focus-trap'
import { focusTrap } from 'svelte-focus-trap'; import { fly } from 'svelte/transition'
import { fly } from 'svelte/transition'; import { Cancel, Check } from '$lib/components/icons'
import Cancel from '~icons/tabler/x'; import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals'
import Check from '~icons/tabler/check';
// provided by <Modals /> let {
export let isOpen: boolean; isOpen,
title,
export let title: string; message,
export let message: string; onConfirm,
export let onConfirm: any; labels = {
export let labels = { cancel: { label: 'Cancel', icon: Cancel },
cancel: { label: 'Cancel', icon: Cancel }, confirm: { label: 'OK', icon: Check }
confirm: { label: 'OK', icon: Check } }
}; }: ModalProps = $props()
</script> </script>
{#if isOpen} {#if isOpen}
<div {@const SvelteComponent = labels?.confirm.icon}
role="dialog" <div
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" role="dialog"
transition:fly={{ y: 50 }} class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
on:introstart transition:fly={{ y: 50 }}
on:outroend use:exitBeforeEnter
use:focusTrap use:focusTrap>
> <div
<div class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
> <div class="divider my-2"></div>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" /> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <div class="flex justify-end gap-2">
<div class="divider my-2" /> <button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
<div class="flex justify-end gap-2"> <labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
<button class="btn btn-primary inline-flex items-center" on:click={closeModal} </button>
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span <button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
>{labels?.cancel.label}</span <SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
></button </button>
> </div>
<button </div>
class="btn btn-warning text-warning-content inline-flex items-center" </div>
on:click={onConfirm}
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span
>{labels?.confirm.label}</span
></button
>
</div>
</div>
</div>
{/if} {/if}
@@ -1,92 +1,101 @@
<script lang="ts"> <script lang="ts">
import { closeAllModals, onBeforeClose } from 'svelte-modals'; import { focusTrap } from 'svelte-focus-trap';
import { focusTrap } from 'svelte-focus-trap'; import { fly } from 'svelte/transition';
import { fly } from 'svelte/transition'; import { telemetry } from '$lib/stores/telemetry';
import { telemetry } from '$lib/stores/telemetry'; import { Cancel } from './icons';
import Cancel from '~icons/tabler/x'; import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean; interface Props {
isOpen: boolean;
}
let updating = true; let { isOpen }: Props = $props();
let progress = 0; let updating = $state(true);
$: if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress;
}
$: if ($telemetry.download_ota.status == 'error') { let progress = $state(0);
updating = false; $effect(() => {
} if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress;
}
});
let message = 'Preparing ...'; $effect(() => {
let timerId: number; if ($telemetry.download_ota.status == 'error') {
updating = false;
}
});
$: if ($telemetry.download_ota.status == 'progress') { let message = $state('Preparing ...');
message = 'Downloading ...';
} else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error;
} else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...';
progress = 0;
// Reload page after 5 sec
timerId = setTimeout(() => {
closeAllModals();
location.reload();
}, 5000);
}
onBeforeClose(() => { $effect(() => {
if (updating) { if ($telemetry.download_ota.status == 'progress') {
// prevents modal from closing message = 'Downloading ...';
return false; } else if ($telemetry.download_ota.status == 'error') {
} else { message = $telemetry.download_ota.error;
$telemetry.download_ota.status = 'idle'; } else if ($telemetry.download_ota.status == 'finished') {
$telemetry.download_ota.error = ''; message = 'Restarting ...';
$telemetry.download_ota.progress = 0; progress = 0;
return true; // Reload page after 5 sec
} setTimeout(() => {
}); modals.closeAll();
location.reload();
}, 5000);
}
});
onBeforeClose(() => {
if (updating) {
// prevents modal from closing
return false;
} else {
$telemetry.download_ota.status = 'idle';
$telemetry.download_ota.error = '';
$telemetry.download_ota.progress = 0;
return true;
}
});
</script> </script>
{#if isOpen} {#if isOpen}
<div <div
role="dialog" role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
on:introstart use:exitBeforeEnter
on:outroend use:focusTrap
use:focusTrap >
> <div
<div class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" >
> <h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2> <div class="divider my-2"></div>
<div class="divider my-2" /> <div class="overflow-y-auto">
<div class="overflow-y-auto"> <div class="bg-base-100 flex flex-col items-center justify-center p-6">
<div class="bg-base-100 flex flex-col items-center justify-center p-6"> {#if $telemetry.download_ota.status == 'progress'}
{#if $telemetry.download_ota.status == 'progress'} <progress class="progress progress-primary w-56" value={progress} max="100"
<progress class="progress progress-primary w-56" value={progress} max="100" /> ></progress>
{:else} {:else}
<progress class="progress progress-primary w-56" /> <progress class="progress progress-primary w-56"></progress>
{/if} {/if}
<p class="mt-8 text-2xl">{message}</p> <p class="mt-8 text-2xl">{message}</p>
</div> </div>
</div> </div>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2"> <div class="flex flex-wrap justify-end gap-2">
<div class="flex-grow" /> <div class="grow"></div>
<button <button
class="btn btn-warning text-warning-content inline-flex flex-none items-center" class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating} disabled={updating}
on:click={() => { onclick={() => {
closeAllModals(); modals.closeAll();
location.reload(); location.reload();
}} }}
> >
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button <Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
> >
</div> </div>
</div> </div>
</div> </div>
{/if} {/if}
+44 -35
View File
@@ -1,42 +1,51 @@
<script lang="ts"> <script lang="ts">
import { closeModal } from 'svelte-modals'; import { focusTrap } from 'svelte-focus-trap';
import { focusTrap } from 'svelte-focus-trap'; import { fly } from 'svelte/transition';
import { fly } from 'svelte/transition'; import { Check } from './icons';
import Check from '~icons/tabler/check'; import { exitBeforeEnter } from 'svelte-modals';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean;
export let title: string; interface Props {
export let message: string; isOpen: boolean;
export let onDismiss: any; title: string;
export let dismiss = { label: 'Dismiss', icon: Check }; message: string;
onDismiss: any;
dismiss?: any;
}
let {
isOpen,
title,
message,
onDismiss,
dismiss = { label: 'Dismiss', icon: Check }
}: Props = $props();
</script> </script>
{#if isOpen} {#if isOpen}
<div <div
role="dialog" role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
on:introstart use:exitBeforeEnter
on:outroend use:focusTrap
use:focusTrap >
> <div
<div class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" >
> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <div class="divider my-2"></div>
<div class="divider my-2" /> <p class="text-base-content mb-1 text-start">{message}</p>
<p class="text-base-content mb-1 text-start">{message}</p> <div class="divider my-2"></div>
<div class="divider my-2" /> <div class="flex justify-end gap-2">
<div class="flex justify-end gap-2"> <button
<button class="btn btn-warning text-warning-content inline-flex items-center"
class="btn btn-warning text-warning-content inline-flex items-center" onclick={onDismiss}
on:click={onDismiss} >
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span <dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
></button </button>
> </div>
</div> </div>
</div> </div>
</div>
{/if} {/if}
@@ -1,60 +0,0 @@
<script lang="ts">
let show = false;
$: type = show ? 'text' : 'password';
export let value = '';
export let id = '';
function handleInput(e: any) {
value = e.target.value;
}
</script>
<div class="relative">
<input {type} class="input input-bordered w-full" {value} on:input={handleInput} {id} />
<div class="absolute inset-y-0 right-0 flex items-center pr-1">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}"
on:click={() => (show = false)}
width="40"
height="40"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
role="button"
tabindex="0"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
<path
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
/>
<path d="M3 3l18 18" />
</svg>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}"
on:click={() => (show = true)}
width="40"
height="40"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
role="button"
tabindex="0"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
</svg>
</div>
</div>
@@ -0,0 +1,78 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import * as THREE from 'three'
import { imu } from '$lib/stores/imu'
import SceneBuilder from '$lib/sceneBuilder'
let canvas: HTMLCanvasElement = $state()
let sceneBuilder: SceneBuilder
let cube: THREE.Mesh
let targetRotation = new THREE.Euler()
let lastUpdateTime = 0
const LERP_SPEED = 5 // rotations per second
const initThreeJS = () => {
sceneBuilder = new SceneBuilder()
.addRenderer({ canvas: canvas, antialias: true, alpha: true })
.addPerspectiveCamera({ x: 2, y: 0, z: 2 })
.addOrbitControls(1, 10, false)
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
.fillParent()
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.8
})
cube = new THREE.Mesh(geometry, material)
sceneBuilder.scene.add(cube)
sceneBuilder.addRenderCb(() => {
if (!cube) return
const currentTime = performance.now()
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
lastUpdateTime = currentTime
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
})
sceneBuilder.startRenderLoop()
}
const updateOrientation = () => {
if (!cube) return
const y = -$imu.x[$imu.x.length - 1] || 0
const x = $imu.y[$imu.y.length - 1] || 0
const z = -$imu.z[$imu.z.length - 1] || 0
targetRotation.set(
THREE.MathUtils.degToRad(x),
THREE.MathUtils.degToRad(y),
THREE.MathUtils.degToRad(z)
)
}
onMount(() => {
initThreeJS()
})
onDestroy(() => {
sceneBuilder?.renderer?.dispose()
})
$effect(() => {
if ($imu) {
updateOrientation()
}
})
</script>
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
<canvas class="w-full h-full" bind:this={canvas}></canvas>
</div>
@@ -1,40 +0,0 @@
<script lang="ts">
import WiFi from '~icons/tabler/wifi';
import WiFi0 from '~icons/tabler/wifi-0';
import WiFi1 from '~icons/tabler/wifi-1';
import WiFi2 from '~icons/tabler/wifi-2';
import WifiOff from '~icons/tabler/wifi-off';
export let showDBm = true;
export let rssi_dbm = 0;
</script>
<div class="indicator">
<div class="tooltip tooltip-left" data-tip={rssi_dbm + " dBm"}>
{#if showDBm}
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
{rssi_dbm} dBm
</span>
{/if}
{#if rssi_dbm >= -55}
<WiFi class={$$props.class || ''} />
{:else if rssi_dbm >= -75}
<div class="{$$props.class || ''} relative">
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
<WiFi2 class="absolute inset-0 h-full w-full" />
</div>
{:else if rssi_dbm >= -85}
<div class="{$$props.class || ''} relative">
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
<WiFi1 class="absolute inset-0 h-full w-full" />
</div>
{:else if rssi_dbm === 0}
<WifiOff class={$$props.class || ''} />
{:else}
<div class="{$$props.class || ''} relative">
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
<WiFi0 class="absolute inset-0 h-full w-full" />
</div>
{/if}
</div>
</div>
+54 -50
View File
@@ -1,56 +1,60 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import Down from '~icons/tabler/chevron-down'; import { Down } from './icons'
export let open = true; interface Props {
export let collapsible = true; open?: boolean
collapsible?: boolean
icon?: import('svelte').Snippet
title?: import('svelte').Snippet
children?: import('svelte').Snippet
right?: import('svelte').Snippet
}
let { open = $bindable(true), collapsible = true, icon, title, children, right }: Props = $props()
</script> </script>
{#if collapsible} {#if collapsible}
<div <div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg" class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
> <div
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium" <span class="inline-flex items-baseline">
> {@render icon?.()}
<span class="inline-flex items-baseline"> {@render title?.()}
<slot name="icon" /> </span>
<slot name="title" /> <button
</span> class="btn btn-circle btn-ghost btn-sm"
<button onclick={() => {
class="btn btn-circle btn-ghost btn-sm" open = !open
on:click={() => { }}>
open = !open; <Down
}} class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ?
> 'rotate-180'
<Down : ''}" />
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open </button>
? 'rotate-180' </div>
: ''}" {#if open}
/> <div
</button> class="flex flex-col gap-2 p-4 pt-0"
</div> transition:slide|local={{ duration: 300, easing: cubicOut }}>
{#if open} {@render children?.()}
<div </div>
class="flex flex-col gap-2 p-4 pt-0" {/if}
transition:slide|local={{ duration: 300, easing: cubicOut }} </div>
>
<slot />
</div>
{/if}
</div>
{:else} {:else}
<div <div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg" class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
> <div
<div class="min-h-16 w-full p-4 text-xl font-medium"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
<slot name="icon" /> {@render icon?.()}
<slot name="title" /> {@render title?.()}
</span> </span>
</div> {@render right?.()}
<div class="flex flex-col gap-2 p-4 pt-0"> </div>
<slot /> <div class="flex flex-col gap-2 p-4 pt-0">
</div> {@render children?.()}
</div> </div>
</div>
{/if} {/if}
+2 -1
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import Loader from '~icons/tabler/loader-2'; import { Loader } from "./icons";
</script> </script>
<div class="flex h-full w-full flex-col items-center justify-center p-6"> <div class="flex h-full w-full flex-col items-center justify-center p-6">
+45
View File
@@ -0,0 +1,45 @@
<script lang="ts">
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
const {
icon,
title,
description = '',
variant = 'primary',
class: klass = '',
children = null
} = $props<{
icon?: any
title: string
description?: string | number
variant?: Variant
class?: string
children?: () => any
}>()
const Icon = $derived(icon)
const variants: Record<Variant, [string, string]> = {
success: ['bg-success', 'text-success-content'],
error: ['bg-error', 'text-error-content'],
primary: ['bg-primary', 'text-primary-content'],
info: ['bg-info', 'text-info-content'],
warning: ['bg-warning', 'text-warning-content']
}
const variantKey: Variant = (variant as Variant) in variants ? (variant as Variant) : 'primary'
const [bgColor, textColor] = variants[variantKey]
</script>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2 {klass}">
{#if icon}
<div class="mask mask-hexagon {bgColor} h-auto w-10 flex-none">
<Icon class="{textColor} h-auto w-full scale-75" />
</div>
{/if}
<div class="grow">
<div class="font-bold">{title}</div>
<div class="text-sm opacity-75 grow">{description}</div>
</div>
{@render children?.()}
</div>
+10 -14
View File
@@ -1,21 +1,17 @@
<script lang="ts"> <script lang="ts">
import { user } from '$lib/stores/user'; import { onDestroy } from 'svelte';
import { onDestroy } from 'svelte'; import { location } from '$lib/stores';
const ws_token = `?access_token=${$user.bearer_token}` let source = $state(`${$location}/api/camera/stream`);
let source = "/api/camera/stream"+ ws_token; onDestroy(() => (source = '#'));
onDestroy(() => {
source = '#';
});
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
<img <img
src={source} src={source}
class="absolute object-cover blur-3xl w-full h-full -z-10" class="absolute object-cover blur-3xl w-full h-full -z-10"
alt="Live stream is down" alt="Live stream is down"
/> />
<img src={source} class="object-contain w-full h-full" alt="Live stream is down" /> <img src={source} class="object-contain w-full h-full" alt="Live stream is down" />
</div> </div>
+8 -10
View File
@@ -2,35 +2,33 @@
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import error from '~icons/tabler/circle-x'; import { error, info, success, warning } from './icons';
import success from '~icons/tabler/circle-check';
import warning from '~icons/tabler/alert-triangle';
import info from '~icons/tabler/info-circle';
export let theme = {
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}; }, icon = {
export let icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
}; } } = $props();
</script> </script>
<div class="toast toast-end mr-4"> <div class="toast toast-end mr-4">
{#each $notifications as notification (notification.id)} {#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div <div
animate:flip={{ duration: 400 }} animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}" class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }} in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }} out:fly={{ x: 100, duration: 400 }}
> >
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" /> <SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span> <span>{notification.message}</span>
</div> </div>
{/each} {/each}
@@ -1,106 +0,0 @@
<script lang="ts">
import { page } from '$app/stores';
import { openModal, closeAllModals } from 'svelte-modals';
import { user } from '$lib/stores/user';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import Firmware from '~icons/tabler/refresh-alert';
import Cancel from '~icons/tabler/x';
import CloudDown from '~icons/tabler/cloud-download';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/models';
export let update = false;
let firmwareVersion: string;
let firmwareDownloadLink: string;
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
const result = await api.get<GithubRelease>(`https://api.github.com/repos/${$page.data.github}/releases/latest`, {headers})
if (result.inner.message === "404" || result.inner.message == "Not Found") {
console.warn('Error: Could not find releases in the repository');
return
}
if (result.isErr()) {
console.error('Error:', result.inner);
return
}
const results = result.inner;
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($page.data.features.firmware_built_target)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
}
}
async function postGithubDownload(url: string) {
const result = await api.post('/api/downloadUpdate', { download_url: url });
if (result.isErr()){
console.error('Error:', result.inner);
return
}
}
onMount(async () => {
if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) {
await getGithubAPI();
const interval = setInterval(
async () => {
await getGithubAPI();
},
60 * 60 * 1000
); // once per hour
}
});
function confirmGithubUpdate(url: string) {
openModal(ConfirmDialog, {
title: 'Confirm flashing new firmware to the device',
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Update', icon: CloudDown }
},
onConfirm: () => {
postGithubDownload(url);
openModal(GithubUpdateDialog, {
onConfirm: () => closeAllModals()
});
}
});
}
</script>
{#if update}
<button
class="btn btn-square btn-ghost h-9 w-9"
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>{firmwareVersion}</span
>
<Firmware class="h-7 w-7" />
</button>
{/if}
+316 -285
View File
@@ -1,308 +1,339 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte'
import { BufferGeometry, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, Object3D, SphereGeometry, Vector3, type NormalBufferAttributes, type Object3DEventMap } from 'three'; import {
import uzip from 'uzip'; BufferGeometry,
import { ModesEnum, kinematicData, mode, model, outControllerData, servoAnglesOut, servoAngles, mpu, jointNames } from '$lib/stores'; Line,
import { footColor, isEmbeddedApp, throttler, toeWorldPositions } from '$lib/utilities'; LineBasicMaterial,
import { fileService } from '$lib/services'; Mesh,
import SceneBuilder from '$lib/sceneBuilder'; MeshBasicMaterial,
import { lerp, degToRad } from 'three/src/math/MathUtils'; Object3D,
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'; SphereGeometry,
import Kinematic, { type body_state_t } from '$lib/kinematic'; Vector3,
import {EightPhaseWalkState, FourPhaseWalkState, IdleState, RestState, StandState} from '$lib/gait' type NormalBufferAttributes,
import { radToDeg } from 'three/src/math/MathUtils.js'; type Object3DEventMap
import type { URDFRobot } from 'urdf-loader'; } from 'three'
import { get } from 'svelte/store'; import {
ModesEnum,
kinematicData,
mode,
model,
outControllerData,
servoAnglesOut,
servoAngles,
mpu,
jointNames
} from '$lib/stores'
import {
extractFootColor,
populateModelCache,
throttler,
getToeWorldPositions
} from '$lib/utilities'
import SceneBuilder from '$lib/sceneBuilder'
import { lerp, degToRad } from 'three/src/math/MathUtils'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import Kinematic, { type body_state_t } from '$lib/kinematic'
import {
BezierState,
CalibrationState,
EightPhaseWalkState,
FourPhaseWalkState,
IdleState,
RestState,
StandState
} from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store'
export let sky = true interface Props {
export let orbit = false sky?: boolean
export let panel = true orbit?: boolean
export let debug = false panel?: boolean
export let ground = true debug?: boolean
ground?: boolean
}
let sceneManager = new SceneBuilder(); let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
let canvas: HTMLCanvasElement
let currentModelAngles: number[] = new Array(12).fill(0); let sceneManager = $state(new SceneBuilder())
let modelTargetAngles: number[] = new Array(12).fill(0) let canvas: HTMLCanvasElement = $state()
let gui_panel: GUI
let Throttler = new throttler()
let feet_trace = new Array(4).fill([]); let currentModelAngles: number[] = new Array(12).fill(0)
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = [] let modelTargetAngles: number[] = new Array(12).fill(0)
let target: Object3D<Object3DEventMap>; let gui_panel: GUI
let Throttler = new throttler()
let target_position = {x: 0, z: 0, yaw: 0} let feet_trace = new Array(4).fill([])
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
let target: Object3D<Object3DEventMap>
let kinematic = new Kinematic() let target_position = { x: 0, z: 0, yaw: 0 }
let planners = { let kinematic = new Kinematic()
[ModesEnum.Idle]: new IdleState(),
[ModesEnum.Rest]: new RestState(), let planners = {
[ModesEnum.Stand]: new StandState(), [ModesEnum.Deactivated]: new IdleState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(), [ModesEnum.Idle]: new IdleState(),
[ModesEnum.Walk]: new FourPhaseWalkState() [ModesEnum.Calibration]: new CalibrationState(),
[ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(),
[ModesEnum.Walk]: new BezierState()
}
let lastTick = performance.now()
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
let body_state = {
omega: 0,
phi: 0,
psi: 0,
xm: 0,
ym: 0.5,
zm: 0,
feet: kinematic.getDefaultFeetPos()
}
let settings = {
'Internal kinematic': true,
'Robot transform controls': false,
'Auto orient robot': true,
'Trace feet': debug,
'Target position': false,
'Trace points': 30,
'Fix camera on robot': true,
'Smooth motion': true,
omega: 0,
phi: 0,
psi: 0,
xm: 0,
ym: 0.7,
zm: 0,
Background: 'black'
}
onMount(async () => {
await populateModelCache()
await createScene()
servoAngles.subscribe(updateAnglesFromStore)
if (panel) createPanel()
})
onDestroy(() => {
canvas.remove()
gui_panel?.destroy()
})
const updateAnglesFromStore = (angles: number[]) => {
if (sceneManager.isDragging) return
if (settings['Internal kinematic']) return
modelTargetAngles = angles
}
const createPanel = () => {
gui_panel = new GUI({ width: 310 })
gui_panel.close()
gui_panel.domElement.id = 'three-gui-panel'
const general = gui_panel.addFolder('General')
general.add(settings, 'Internal kinematic')
general.add(settings, 'Robot transform controls')
general.add(settings, 'Auto orient robot')
const kinematic = gui_panel.addFolder('Kinematics')
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
const visibility = gui_panel.addFolder('Visualization')
visibility.add(settings, 'Trace feet')
visibility.add(settings, 'Trace points', 1, 1000, 1)
visibility.add(settings, 'Target position')
visibility.add(settings, 'Smooth motion')
visibility.addColor(settings, 'Background')
}
const updateKinematicPosition = () => {
kinematicData.set([
settings.omega,
settings.phi,
settings.psi,
settings.xm,
settings.ym,
settings.zm
])
}
const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
}
const createScene = async () => {
sceneManager
.addRenderer({ antialias: true, canvas, alpha: true })
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
.addOrbitControls(8, 30, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
.addFogExp2(0xcccccc, 0.015)
.addModel($model)
.addTransformControls(sceneManager.model)
.fillParent()
.addRenderCb(render)
.startRenderLoop()
if (ground) sceneManager.addGroundPlane()
const geometry = new SphereGeometry(0.1, 32, 16)
const material = new MeshBasicMaterial({ color: 0xffff00 })
target = new Mesh(geometry, material)
sceneManager.scene.add(target)
if (debug) {
sceneManager.addDragControl(updateAngles)
} }
let lastTick = performance.now() if (sky) sceneManager.addSky()
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1] for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry()
const material = new LineBasicMaterial({ color: extractFootColor() })
const line = new Line(geometry, material)
trace_lines.push(geometry)
sceneManager.scene.add(line)
}
}
let body_state = { const renderTraceLines = (foot_positions: Vector3[]) => {
omega: 0, if (!settings['Trace feet']) {
phi: 0, if (!feet_trace.length) return
psi: 0, trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
xm: 0, feet_trace = new Array(4).fill([])
ym: 0.5, return
zm: 0,
feet: planners[ModesEnum.Idle].default_feet_pos
} }
let settings = { trace_lines.forEach((line, i) => {
'Internal kinematic':false, feet_trace[i].push(foot_positions[i])
'Robot transform controls':false, feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
'Auto orient robot':true, line.setFromPoints(feet_trace[i])
'Trace feet':debug, })
'Trace points': 30, }
'Fix camera on robot': true,
'Smooth motion': true, const calculate_kinematics = () => {
'omega': 0, if (sceneManager.isDragging || !settings['Internal kinematic']) return
'phi': 0, const position: body_state_t = {
'psi': 0, omega: settings.omega,
'xm': 0, phi: settings.phi,
'ym': 0.7, psi: settings.psi,
'zm': 0, xm: settings.xm,
'Background': "black" ym: settings.ym,
zm: settings.zm,
feet: body_state.feet
} }
onMount(async () => { let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
await cacheModelFiles() modelTargetAngles = new_angles
await createScene(); }
if (!isEmbeddedApp && panel) createPanel();
servoAngles.subscribe(updateAnglesFromStore)
});
onDestroy(() => { const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
canvas.remove() if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
gui_panel?.destroy() robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
});
const updateAnglesFromStore = (angles: number[]) => { robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
if (sceneManager.isDragging) return robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
if (settings['Internal kinematic']) return
modelTargetAngles = angles; robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
}
const update_camera = (robot: URDFRobot) => {
if (!settings['Fix camera on robot']) return
sceneManager.orbit.target = robot.position.clone()
}
const smooth = (start: number, end: number, amount: number) => {
return settings['Smooth motion'] ? lerp(start, end, amount) : end
}
const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData)
const data = {
stop: controlData[0],
lx: controlData[1],
ly: controlData[2],
rx: controlData[3],
ry: controlData[4],
h: controlData[5],
s: controlData[6],
s1: controlData[7]
}
body_state.ym = ((data.h + 127) * 0.35) / 100
let planner = planners[get(mode)]
const delta = performance.now() - lastTick
lastTick = performance.now()
body_state = planner.step(body_state, data, delta)
settings.omega = body_state.omega
settings.phi = body_state.phi
settings.psi = body_state.psi
settings.xm = body_state.xm
settings.ym = body_state.ym
settings.zm = body_state.zm
}
const update_robot_position = (robot: URDFRobot) => {
if (!settings['Robot transform controls']) return
settings.omega = radToDeg(robot.rotation.y)
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
settings.psi = radToDeg(robot.rotation.x) + 90
settings.xm = robot.position.z * 100
settings.zm = -robot.position.x * 100
}
const updateTargetPosition = () => {
target.visible = settings['Target position']
target.position.x = smooth(target.position.x, target_position.x, 0.5)
target.position.z = smooth(target.position.z, target_position.z, 0.5)
}
const render = () => {
const robot = sceneManager.model
if (!robot) return
const toes = getToeWorldPositions(robot)
renderTraceLines(toes)
update_camera(robot)
update_gait()
calculate_kinematics()
update_robot_position(robot)
sceneManager.transformControl.showX = settings['Robot transform controls']
sceneManager.transformControl.showY = settings['Robot transform controls']
sceneManager.transformControl.showZ = settings['Robot transform controls']
for (let i = 0; i < $jointNames.length; i++) {
currentModelAngles[i] = smooth(
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
modelTargetAngles[i],
0.1
)
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
} }
const createPanel = () => { orient_robot(robot, toes)
gui_panel = new GUI({width: 310}); updateTargetPosition()
gui_panel.close(); }
gui_panel.domElement.id = 'three-gui-panel';
const general = gui_panel.addFolder('General');
general.add(settings, 'Internal kinematic')
general.add(settings, 'Robot transform controls')
general.add(settings, 'Auto orient robot')
const kinematic = gui_panel.addFolder('Kinematics');
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
const visibility = gui_panel.addFolder('Visualization');
visibility.add(settings, 'Trace feet')
visibility.add(settings, 'Trace points', 1, 1000, 1)
visibility.add(settings, 'Smooth motion')
visibility.addColor(settings, 'Background')
}
const updateKinematicPosition = () => {
kinematicData.set([
settings.omega,
settings.phi,
settings.psi,
settings.xm,
settings.ym,
settings.zm
])
}
const cacheModelFiles = async () => {
let data = await fetch('/stl.zip').then((data) => data.arrayBuffer());
var files = uzip.parse(data);
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const url = new URL(path, window.location.href);
fileService.saveFile(url.toString(), data);
}
};
const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
};
const createScene = async () => {
sceneManager
.addRenderer({ antialias: true, canvas, alpha: true })
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
.addOrbitControls(8, 30, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 0.9 })
.addAmbientLight({ color: 0xffffff, intensity: 0.6 })
.addFogExp2(0xcccccc, 0.015)
.addModel($model)
.addTransformControls(sceneManager.model)
.fillParent()
.addRenderCb(render)
.startRenderLoop();
if (ground) sceneManager
.addGroundPlane()
.addGridHelper({ size: 30, divisions: 25 })
const geometry = new SphereGeometry(0.1, 32, 16 );
const material = new MeshBasicMaterial( { color: 0xffff00 } );
target = new Mesh(geometry, material);
if (debug) {
sceneManager.scene.add(target);
sceneManager.addDragControl(updateAngles)
}
if (sky) sceneManager.addSky()
for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry();
const material = new LineBasicMaterial({ color: footColor() });
const line = new Line(geometry, material);
trace_lines.push(geometry);
sceneManager.scene.add(line);
}
};
const renderTraceLines = (foot_positions: Vector3[]) => {
if (!settings['Trace feet']) {
if (!feet_trace.length) return
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
feet_trace = new Array(4).fill([])
return
}
trace_lines.forEach((line, i) => {
feet_trace[i].push(foot_positions[i])
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
line.setFromPoints(feet_trace[i]);
})
}
const calculate_kinematics = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const position:body_state_t = {
omega: settings.omega,
phi: settings.phi,
psi: settings.psi,
xm: settings.xm,
ym: settings.ym,
zm: settings.zm,
feet: body_state.feet
}
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]));
modelTargetAngles = new_angles;
}
const orient_robot = (robot: URDFRobot, toes:Vector3[]) => {
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1);
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1);
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1);
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1);
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1);
}
const update_camera = (robot:URDFRobot) => {
if (!settings['Fix camera on robot']) return
sceneManager.orbit.target = robot.position.clone()
}
const smooth = (start:number, end:number, amount:number) => {
return settings['Smooth motion'] ? lerp(start, end, amount) : end
}
const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData)
const data = {
stop: controlData[0],
lx: controlData[1],
ly: controlData[2],
rx: controlData[3],
ry: controlData[4],
h: controlData[5],
s: controlData[6],
};
body_state.ym = ((data.h + 127) * 0.75) / 100;
let planner = planners[get(mode)]
const delta = performance.now() - lastTick
lastTick = performance.now()
body_state = planner.step(body_state, data, delta);
settings.omega = body_state.omega
settings.phi = body_state.phi
settings.psi = body_state.psi
settings.xm = body_state.xm
settings.ym = body_state.ym
settings.zm = body_state.zm
}
const update_robot_position = (robot:URDFRobot) => {
if (!settings['Robot transform controls']) return
settings.omega = radToDeg(robot.rotation.y)
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading -90
settings.psi = radToDeg(robot.rotation.x) + 90
settings.xm = robot.position.z * 100
settings.zm = -robot.position.x * 100
}
const updateTargetPosition = () => {
target.position.x = smooth(target.position.x, target_position.x, 0.5)
target.position.z = smooth(target.position.z, target_position.z, 0.5)
}
const render = () => {
const robot = sceneManager.model;
if (!robot) return;
const toes = toeWorldPositions(robot)
renderTraceLines(toes)
update_camera(robot)
update_gait()
calculate_kinematics()
update_robot_position(robot)
sceneManager.transformControl.showX = settings['Robot transform controls']
sceneManager.transformControl.showY = settings['Robot transform controls']
sceneManager.transformControl.showZ = settings['Robot transform controls']
for (let i = 0; i < $jointNames.length; i++) {
currentModelAngles[i] = smooth((robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI), modelTargetAngles[i], 0.1);
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
}
orient_robot(robot, toes)
updateTargetPosition();
};
</script> </script>
<svelte:window on:resize={sceneManager.fillParent} /> <svelte:window onresize={sceneManager.fillParent} />
<canvas bind:this={canvas}></canvas> <canvas bind:this={canvas}></canvas>
+96
View File
@@ -0,0 +1,96 @@
export { default as Connection } from '~icons/mdi/connection'
export { default as Users } from '~icons/mdi/users'
export { default as Settings } from '~icons/mdi/settings'
export { default as MdiController } from '~icons/mdi/controller'
export { default as Devices } from '~icons/mdi/devices'
export { default as Camera } from '~icons/mdi/camera-outline'
export { default as Rotate3d } from '~icons/mdi/rotate-3d'
export { default as MotorOutline } from '~icons/mdi/motor-outline'
export { default as Health } from '~icons/mdi/stethoscope'
export { default as Folder } from '~icons/mdi/folder-outline'
export { default as Update } from '~icons/mdi/reload'
export { default as Router } from '~icons/mdi/router'
export { default as AP } from '~icons/mdi/access-point'
export { default as Remote } from '~icons/mdi/network'
export { default as Copyright } from '~icons/mdi/copyright'
export { default as NTP } from '~icons/mdi/clock-check'
export { default as Metrics } from '~icons/mdi/report-bar'
export { default as MdiEyeOutline } from '~icons/mdi/eye-outline'
export { default as MdiEyeOffOutline } from '~icons/mdi/eye-off-outline'
export { default as Github } from '~icons/mdi/github'
export { default as Avatar } from '~icons/mdi/user-circle'
export { default as Logout } from '~icons/mdi/logout'
export { default as Record } from '~icons/mdi/radio-button-unchecked'
export { default as MdiFullscreen } from '~icons/mdi/fullscreen'
export { default as MdiFullscreenExit } from '~icons/mdi/fullscreen-exit'
export { default as WiFi } from '~icons/tabler/wifi'
export { default as WiFi0 } from '~icons/tabler/wifi-0'
export { default as WiFi1 } from '~icons/tabler/wifi-1'
export { default as WiFi2 } from '~icons/tabler/wifi-2'
export { default as WifiOff } from '~icons/tabler/wifi-off'
export { default as MdiWeatherSunny } from '~icons/mdi/weather-sunny'
export { default as MdiMoonAndStars } from '~icons/mdi/moon-and-stars'
export { default as Hamburger } from '~icons/mdi/hamburger-menu'
export { default as FileIcon } from '~icons/mdi/file'
export { default as FolderIcon } from '~icons/mdi/folder-outline'
export { default as FolderOpenOutline } from '~icons/mdi/folder-open-outline'
export { default as TrashIcon } from '~icons/mdi/trash'
export { default as RotateCcw } from '~icons/mdi/rotate-left'
export { default as RotateCw } from '~icons/mdi/rotate-right'
export { default as Down } from '~icons/tabler/chevron-down'
export { default as Cancel } from '~icons/tabler/x'
export { default as Check } from '~icons/tabler/check'
export { default as Login } from '~icons/tabler/login'
export { default as Loader } from '~icons/tabler/loader-2'
export { default as error } from '~icons/tabler/circle-x'
export { default as success } from '~icons/tabler/circle-check'
export { default as warning } from '~icons/tabler/alert-triangle'
export { default as info } from '~icons/tabler/info-circle'
export { default as Power } from '~icons/tabler/power'
export { default as MAC } from '~icons/tabler/dna-2'
export { default as Home } from '~icons/tabler/home'
export { default as SSID } from '~icons/tabler/router'
export { default as DNS } from '~icons/mdi/dns'
export { default as Gateway } from '~icons/tabler/torii'
export { default as Subnet } from '~icons/tabler/grid-dots'
export { default as Channel } from '~icons/tabler/antenna'
export { default as Scan } from '~icons/tabler/radar-2'
export { default as Add } from '~icons/tabler/circle-plus'
export { default as Edit } from '~icons/mdi/edit'
export { default as EditOff } from '~icons/mdi/edit-off'
export { default as Delete } from '~icons/tabler/trash'
export { default as Network } from '~icons/tabler/router'
export { default as Reload } from '~icons/tabler/reload'
export { default as Firmware } from '~icons/tabler/refresh-alert'
export { default as CloudDown } from '~icons/tabler/cloud-download'
export { default as Server } from '~icons/tabler/server'
export { default as Clock } from '~icons/tabler/clock'
export { default as UTC } from '~icons/tabler/clock-pin'
export { default as Stopwatch } from '~icons/tabler/24-hours'
export { default as CPU } from '~icons/tabler/cpu'
export { default as CPP } from '~icons/tabler/binary'
export { default as Sleep } from '~icons/tabler/zzz'
export { default as FactoryReset } from '~icons/tabler/refresh-dot'
export { default as Speed } from '~icons/tabler/activity'
export { default as Flash } from '~icons/tabler/device-sd-card'
export { default as Pyramid } from '~icons/tabler/pyramid'
export { default as Sketch } from '~icons/tabler/chart-pie'
export { default as Heap } from '~icons/tabler/box-model'
export { default as Temperature } from '~icons/tabler/temperature'
export { default as SDK } from '~icons/tabler/sdk'
export { default as Prerelease } from '~icons/tabler/test-pipe'
export { default as Error } from '~icons/tabler/circle-x'
export { default as OTA } from '~icons/tabler/file-upload'
export { default as Warning } from '~icons/tabler/alert-triangle'
export { default as AddUser } from '~icons/tabler/user-plus'
export { default as Admin } from '~icons/tabler/key'
export { default as Save } from '~icons/tabler/device-floppy'
@@ -0,0 +1,26 @@
<script lang="ts">
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons";
interface Props {
show?: boolean;
value?: string;
id?: string;
}
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props();
let type = $derived(show ? 'text' : 'password');
const handleInput = (e: any) => value = e.target.value
const togglePassword = () => show = !show
</script>
<label class="input input-bordered flex items-center gap-2">
<input {type} class="grow" {value} oninput={handleInput} {id} />
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div onclick={togglePassword} role="button" tabindex="0">
<MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" />
<MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" />
</div>
</label>
@@ -0,0 +1,34 @@
<script lang="ts">
interface Props {
min?: number
max?: number
step?: number
value?: any
oninput?: any
}
let {
min = 0,
max = 100,
step = 1,
value = $bindable((max - min) / 2),
...rest
}: Props = $props()
</script>
<input
type="range"
style="writing-mode: vertical-lr; direction: rtl"
class="cursor-pointer"
{min}
{max}
{step}
bind:value
{...rest} />
<style>
input[type='range']::-webkit-slider-runnable-track {
background: oklch(var(--p) / 1);
border-radius: var(--rounded-box, 1rem);
}
</style>
+2
View File
@@ -0,0 +1,2 @@
export { default as PasswordInput } from './InputPassword.svelte';
export { default as VerticalSlider } from './VerticalSlider.svelte';
@@ -0,0 +1,11 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script>
<div class="box-border overflow-hidden flex-1">
{@render children?.()}
</div>
@@ -0,0 +1,37 @@
<script lang="ts">
import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
import Widget from './Widget.svelte';
interface Props {
container: WidgetContainerConfig;
}
let { container }: Props = $props();
</script>
<div class="w-full h-full flex flex-col overflow-hidden">
<div
class="flex w-full h-full"
class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}
>
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
{@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets}
<WidgetContainer container={widget} />
{/if}
</Widget>
{#if index !== container.widgets.length - 1}
<div
class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}
></div>
{/if}
{/each}
</div>
</div>
@@ -0,0 +1,15 @@
<script lang="ts">
import { Github } from "../icons";
interface Props {
github: any;
}
let { github }: Props = $props();
</script>
{#if github.active}
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer">
<Github class="h-5 w-5" />
</a>
{/if}
@@ -0,0 +1,14 @@
<script>
import logo from '$lib/assets/logo512.png';
/** @type {{appName: any}} */
let { appName } = $props();
</script>
<a
href="/"
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
>
<img src={logo} alt="Logo" class="h-12 w-12" />
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
</a>
+189
View File
@@ -0,0 +1,189 @@
<script lang="ts">
import { page } from '$app/state'
import { useFeatureFlags } from '$lib/stores/featureFlags'
import GithubButton from '../menu/GithubButton.svelte'
import LogoButton from '../menu/LogoButton.svelte'
import MenuList from '../menu/MenuList.svelte'
import {
Connection,
Settings,
MdiController,
Devices,
Camera,
Rotate3d,
MotorOutline,
Health,
Folder,
Update,
WiFi,
Router,
AP,
Copyright,
Metrics,
DNS
} from '$lib/components/icons'
import appEnv from 'app-env'
const features = useFeatureFlags()
const appName = page.data.app_name
const copyright = page.data.copyright
const github = { href: 'https://github.com/' + page.data.github, active: true }
type menuItem = {
title: string
icon: ConstructorOfATypedSvelteComponent
href?: string
feature: boolean
active?: boolean
submenu?: menuItem[]
}
let menuItems = $state<menuItem[]>([])
$effect(() => {
menuItems = [
{
title: 'Connection',
icon: WiFi,
href: '/connection',
feature: !appEnv.VITE_USE_HOST_NAME
},
{
title: 'Controller',
icon: MdiController,
href: '/controller',
feature: true
},
{
title: 'Peripherals',
icon: Devices,
feature: true,
submenu: [
{
title: 'I2C',
icon: Connection,
href: '/peripherals/i2c',
feature: true
},
{
title: 'Camera',
icon: Camera,
href: '/peripherals/camera',
feature: $features.camera
},
{
title: 'Servo',
icon: MotorOutline,
href: '/peripherals/servo',
feature: true
},
{
title: 'IMU',
icon: Rotate3d,
href: '/peripherals/imu',
feature: $features.imu || $features.mag || $features.bmp
}
]
},
{
title: 'WiFi',
icon: WiFi,
feature: true,
submenu: [
{
title: 'WiFi Station',
icon: Router,
href: '/wifi/sta',
feature: true
},
{
title: 'Access Point',
icon: AP,
href: '/wifi/ap',
feature: true
},
{
title: 'mDNS',
icon: DNS,
href: '/wifi/mdns',
feature: true
}
]
},
{
title: 'System',
icon: Settings,
feature: true,
submenu: [
{
title: 'System Status',
icon: Health,
href: '/system/status',
feature: true
},
{
title: 'File System',
icon: Folder,
href: '/system/filesystem',
feature: true
},
{
title: 'System Metrics',
icon: Metrics,
href: '/system/metrics',
feature: true
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
feature: $features.ota || $features.upload_firmware || $features.download_firmware
}
]
}
] as menuItem[]
})
const { menuClicked } = $props()
function setActiveMenuItem(targetTitle: string) {
menuItems.forEach(item => {
item.active = item.title === targetTitle
item.submenu?.forEach(subItem => {
subItem.active = subItem.title === targetTitle
})
})
menuItems = menuItems
menuClicked()
}
$effect(() => {
setActiveMenuItem(page.data.title)
})
const updateMenu = (event: any) => {
setActiveMenuItem(event.details)
}
</script>
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
<LogoButton {appName} />
<MenuList
{menuItems}
select={updateMenu}
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
level="0" />
<div class="divider my-0"></div>
<div class="flex items-center justify-between">
<GithubButton {github} />
<div class="flex items-center justify-end text-sm gap-2">
<Copyright class="h-4 w-4" />{copyright}
</div>
</div>
</div>
@@ -0,0 +1,48 @@
<script lang="ts">
import MenuList from './MenuList.svelte'
type MenuItem = {
title: string
icon: ConstructorOfATypedSvelteComponent
href?: string
feature: boolean
active?: boolean
submenu?: MenuItem[]
}
let { level, menuItems, select, class: klass } = $props()
const selectMenuItem = (title: string) => {
select(title)
}
</script>
<ul class={klass + ' menu w-full'}>
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
{#if menuItem.feature}
<li>
{#if menuItem.submenu}
<details open={menuItem.submenu.some(subItem => subItem.active)}>
<summary class="font-bold">
<menuItem.icon class="h-6 w-6" />
{menuItem.title}
</summary>
<div class="pl-4">
<MenuList menuItems={menuItem.submenu} level={level + 1} {select} class={klass} />
</div>
</details>
{:else}
<a
href={menuItem.href}
class="font-bold"
class:bg-base-100={menuItem.active}
class:text-lg={level === 0}
class:text-md={level === 1}
onclick={() => selectMenuItem(menuItem.title)}>
<menuItem.icon class="h-6 w-6" />
{menuItem.title}
</a>
{/if}
</li>
{/if}
{/each}
</ul>
@@ -0,0 +1,10 @@
<script lang="ts">
import { isFullscreen, toggleFullscreen } from '$lib/stores';
import { MdiFullscreenExit, MdiFullscreen } from '../icons';
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
</script>
<button onclick={toggleFullscreen}>
<SvelteComponent class="h-7 w-7" />
</button>
@@ -0,0 +1,33 @@
<script lang="ts">
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
interface Props {
showDBm?: boolean;
rssi?: number;
}
let { showDBm = false, rssi = 0 }: Props = $props();
const getWiFiIcon = () => {
if (rssi === 0) return WifiOff;
if (rssi >= -55) return WiFi;
if (rssi >= -75) return WiFi2;
if (rssi >= -85) return WiFi1;
return WiFi0;
};
const SvelteComponent = $derived(getWiFiIcon());
</script>
<div class="indicator">
<div class="tooltip tooltip-left" data-tip={rssi + " dBm"}>
{#if showDBm}
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
{rssi} dBm
</span>
{/if}
<div class="h-7 w-7">
<SvelteComponent class="absolute inset-0 h-full w-full" />
</div>
</div>
</div>
@@ -0,0 +1,34 @@
<script lang="ts">
import { useFeatureFlags } from '$lib/stores';
import { modals } from 'svelte-modals';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import { api } from '$lib/api';
import { Cancel, Power } from '../icons';
const features = useFeatureFlags();
const postSleep = async () => await api.post('/api/system/sleep');
const confirmSleep = () => {
modals.open(ConfirmDialog, {
title: 'Confirm Power Down',
message: 'Are you sure you want to switch off the device?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Switch Off', icon: Power }
},
onConfirm: () => {
modals.close();
postSleep();
}
});
};
</script>
{#if $features.sleep}
<div class="flex-none">
<button class="btn btn-square btn-ghost h-9 w-10" onclick={confirmSleep}>
<Power class="text-error h-9 w-9" />
</button>
</div>
{/if}
@@ -0,0 +1,10 @@
<script lang="ts">
import { mode, modes } from "$lib/stores";
const deactivate = async () => {
mode.set(modes.indexOf('deactivated'));
};
</script>
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
@@ -0,0 +1,9 @@
<script lang="ts">
import { MdiWeatherSunny, MdiMoonAndStars } from "../icons";
</script>
<label class="swap swap-rotate">
<input type="checkbox" value="light" class="theme-controller" />
<MdiWeatherSunny class="swap-off h-7 w-7" />
<MdiMoonAndStars class="swap-on h-7 w-7" />
</label>
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import MdiHamburgerMenu from '~icons/mdi/hamburger-menu'; import {Hamburger} from '../icons'
</script> </script>
<div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800"> <div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800">
<div class="flex gap-2 p-2"> <div class="flex gap-2 p-2">
<a href="/"> <a href="/">
<svelte:component this={MdiHamburgerMenu} class="h-8 w-8"/> <Hamburger class="h-8 w-8"/>
</a> </a>
</div> </div>
</div> </div>
@@ -0,0 +1,111 @@
<script lang="ts">
import { page } from '$app/state';
import { modals } from 'svelte-modals';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/types/models';
import { useFeatureFlags } from '$lib/stores/featureFlags';
import { Cancel, CloudDown, Firmware } from '../icons';
const features = useFeatureFlags();
interface Props {
update?: boolean;
}
let { update = $bindable(false) }: Props = $props();
let firmwareVersion: string = $state('');
let firmwareDownloadLink: string = $state('');
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};
const result = await api.get<GithubRelease>(
`https://api.github.com/repos/${page.data.github}/releases/latest`,
{ headers }
);
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
console.warn('Error: Could not find releases in the repository');
return;
}
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
const results = result.inner;
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($features.firmware_built_target)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
}
}
async function postGithubDownload(url: string) {
const result = await api.post('/api/downloadUpdate', { download_url: url });
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
}
onMount(async () => {
if ($features.download_firmware) {
await getGithubAPI();
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
}
});
function confirmGithubUpdate(url: string) {
modals.open(ConfirmDialog, {
title: 'Confirm flashing new firmware to the device',
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Update', icon: CloudDown }
},
onConfirm: () => {
postGithubDownload(url);
modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll()
});
}
});
}
</script>
{#if update}
<div class="indicator flex-none">
<button
class="btn btn-square btn-ghost h-9 w-9"
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>
{firmwareVersion}
</span>
<Firmware class="h-7 w-7" />
</button>
</div>
{/if}
@@ -0,0 +1,6 @@
<script lang="ts">
import { selectedView, views } from "$lib/stores/application";
import Selector from "../widget/Selector.svelte";
</script>
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} />
@@ -0,0 +1,38 @@
<script lang="ts">
import { page } from '$app/state'
import { telemetry } from '$lib/stores/telemetry'
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte'
import UpdateIndicator from '$lib/components/statusbar/UpdateIndicator.svelte'
import SleepButton from './SleepButton.svelte'
import ThemeButton from './ThemeButton.svelte'
import FullscreenButton from './FullscreenButton.svelte'
import StopButton from './StopButton.svelte'
import ViewSelector from './ViewSelector.svelte'
import { Hamburger } from '../icons'
</script>
<div class="navbar bg-base-300 sticky top-0 z-10 h-12 min-h-fit drop-shadow-lg lg:h-16 gap-2 pr-0">
<div class="flex flex-1 gap-2">
<label for="main-menu" class="btn btn-ghost btn-circle btn-sm drawer-button">
<Hamburger class="h-6 w-auto" />
</label>
{#if page.data.title === 'Controller'}
<ViewSelector />
{:else}
<h1 class="px-2 text-xl font-bold lg:text-2xl">{page.data.title}</h1>
{/if}
</div>
<UpdateIndicator />
<FullscreenButton />
<ThemeButton />
<RssiIndicator rssi={$telemetry.rssi.rssi} />
<SleepButton />
<StopButton />
</div>
+8 -10
View File
@@ -2,35 +2,33 @@
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import error from '~icons/tabler/circle-x'; import { error, info, success, warning } from '../icons';
import success from '~icons/tabler/circle-check';
import warning from '~icons/tabler/alert-triangle';
import info from '~icons/tabler/info-circle';
export let theme = {
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}; }, icon = {
export let icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
}; } } = $props();
</script> </script>
<div class="toast toast-end mr-4 z-20"> <div class="toast toast-end mr-4 z-20">
{#each $notifications as notification (notification.id)} {#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div <div
animate:flip={{ duration: 400 }} animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}" class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }} in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }} out:fly={{ x: 100, duration: 400 }}
> >
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" /> <SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span> <span>{notification.message}</span>
</div> </div>
{/each} {/each}
@@ -0,0 +1,103 @@
<script lang="ts">
import { daisyColor } from "$lib/utilities";
import { Chart, registerables } from "chart.js";
import { onMount } from "svelte";
import { cubicOut } from "svelte/easing";
import { slide } from "svelte/transition";
let chartElement: HTMLCanvasElement = $state();
let chart: Chart;
interface Props {
label: any;
data: number[];
title: any;
}
let { label, data, title }: Props = $props();
Chart.register(...registerables);
onMount(() => {
chart = new Chart(chartElement, {
type: 'line',
data: {
labels: data,
datasets: [
{
label,
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
data,
yAxisID: 'y'
},
]
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
},
elements: {
point: {
radius: 0
}
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
},
ticks: {
color: daisyColor('--bc')
},
display: false
},
y: {
type: 'linear',
title: {
display: true,
text: title,
color: daisyColor('--bc'),
font: {
size: 16,
weight: 'bold'
}
},
position: 'left',
min: 0,
max: 100,
grid: { color: daisyColor('--bc', 10) },
ticks: {
color: daisyColor('--bc')
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
setInterval(() => {
chart.data.labels = data
chart.data.datasets[0].data = data
}, 500);
})
</script>
<div class="w-full h-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={chartElement}></canvas>
</div>
</div>
@@ -0,0 +1,20 @@
<script lang="ts">
interface Props {
options?: string[];
selectedOption?: string;
change: () => void;
[key: string]: any;
}
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
</script>
<select
bind:value={selectedOption}
{...rest}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
+391 -186
View File
@@ -1,242 +1,447 @@
import type { body_state_t } from './kinematic'; import type { body_state_t } from './kinematic'
import { fromInt8 } from './utilities'; import Kinematic from './kinematic'
import { fromInt8 } from './utilities'
const { sin } = Math; const { sin } = Math
export interface gait_state_t { export interface gait_state_t {
step_height: number; step_height: number
step_x: number; step_x: number
step_z: number; step_z: number
step_angle: number; step_angle: number
step_velocity: number; step_velocity: number
step_depth: number; step_depth: number
} }
export interface ControllerCommand { export interface ControllerCommand {
stop: number; stop: number
lx: number; lx: number
ly: number; ly: number
rx: number; rx: number
ry: number; ry: number
h: number; h: number
s: number; s: number
s1: number
} }
export abstract class GaitState { export abstract class GaitState {
protected abstract name: string; protected abstract name: string
protected static body_state: body_state_t; protected dt = 0.02
protected body_state!: body_state_t
protected gait_state: gait_state_t = {
step_height: 0.4,
step_x: 0,
step_z: 0,
step_angle: 0,
step_velocity: 1,
step_depth: 0.002
}
public get default_feet_pos() { public get default_feet_pos() {
return [ return new Kinematic().getDefaultFeetPos()
[1, -1, 1, 1], }
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
];
}
protected get default_height() { protected get default_height() {
return 0.5; return 0.5
} }
begin() { begin() {
console.log('Starting', this.name); console.log('Starting', this.name)
} }
end() { end() {
console.log('Ending', this.name); console.log('Ending', this.name)
} }
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return body_state; this.map_command(command)
} this.body_state = body_state
this.dt = dt / 1000
return body_state
}
map_command(command: ControllerCommand): gait_state_t { map_command(command: ControllerCommand) {
return { const newCommand = {
step_height: 0.4 + Math.abs(command.ry / 128), step_height: 0.4 + (command.s1 / 128 + 1) / 2,
step_x: (Math.floor(fromInt8(command.ly, -1, 1) * 10) / 10) * 3, step_x: Math.floor(fromInt8(command.ly, -1, 1) * 10) / 10,
step_z: -(Math.floor(fromInt8(command.lx, -1, 1) * 10) / 10) * 3, step_z: -(Math.floor(fromInt8(command.lx, -1, 1) * 10) / 10),
step_velocity: command.s / 128 + 1, step_velocity: command.s / 128 + 1,
step_angle: 0, step_angle: command.rx / 128,
step_depth: 0.2 step_depth: 0.002
}; }
}
this.gait_state = newCommand
}
} }
export class IdleState extends GaitState { export class IdleState extends GaitState {
protected name = 'Idle'; protected name = 'Idle'
}
export class CalibrationState extends GaitState {
protected name = 'Calibration'
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
body_state.omega = 0
body_state.phi = 0
body_state.psi = 0
body_state.xm = 0
body_state.ym = this.default_height * 10
body_state.zm = 0
body_state.feet = this.default_feet_pos
return body_state
}
} }
export class RestState extends GaitState { export class RestState extends GaitState {
protected name = 'Rest'; protected name = 'Rest'
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
body_state.omega = 0; body_state.omega = 0
body_state.phi = 0; body_state.phi = 0
body_state.psi = 0; body_state.psi = 0
body_state.xm = 0; body_state.xm = 0
body_state.ym = this.default_height / 2; body_state.ym = this.default_height / 2
body_state.zm = 0; body_state.zm = 0
body_state.feet = this.default_feet_pos; body_state.feet = this.default_feet_pos
return body_state; return body_state
} }
} }
export class StandState extends GaitState { export class StandState extends GaitState {
protected name = 'Stand'; protected name = 'Stand'
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
body_state.omega = 0; body_state.omega = 0
body_state.phi = command.rx / 8; body_state.phi = command.rx / 8
body_state.psi = command.ry / 8; body_state.psi = command.ry / 8
body_state.xm = command.ly / 2 / 100; body_state.xm = command.ly / 2 / 100
body_state.zm = command.lx / 2 / 100; body_state.zm = command.lx / 2 / 100
body_state.feet = this.default_feet_pos; body_state.feet = this.default_feet_pos
return body_state; return body_state
} }
} }
abstract class PhaseGaitState extends GaitState { abstract class PhaseGaitState extends GaitState {
protected tick = 0; protected tick = 0
protected phase = 0; protected phase = 0
protected phase_time = 0; protected phase_time = 0
protected abstract num_phases: number; protected abstract num_phases: number
protected abstract phase_speed_factor: number; protected abstract phase_speed_factor: number
protected abstract swing_stand_ratio: number; protected abstract swing_stand_ratio: number
protected contact_phases!: number[][]; protected contact_phases!: number[][]
protected shifts!: number[][]; protected shifts!: number[][]
protected body_state!: body_state_t; step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
protected gait_state!: gait_state_t; super.step(body_state, command, dt)
protected dt = 0.02; this.update_phase()
this.update_body_position()
this.update_feet_positions()
return this.body_state
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { update_phase() {
this.body_state = body_state; this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity
this.gait_state = this.map_command(command);
this.dt = dt / 1000;
this.update_phase();
this.update_body_position();
this.update_feet_positions();
return this.body_state;
}
update_phase() { if (this.phase_time >= 1) {
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity; this.phase += 1
if (this.phase == this.num_phases) this.phase = 0
this.phase_time = 0
}
}
if (this.phase_time >= 1) { update_body_position() {
this.phase += 1; if (this.num_phases === 4) return
if (this.phase == this.num_phases) this.phase = 0;
this.phase_time = 0;
}
}
update_body_position() { const shift = this.shifts[Math.floor(this.phase / 2)]
if (this.num_phases === 4) return;
const shift = this.shifts[Math.floor(this.phase / 2)]; this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4
}
this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4; update_feet_positions() {
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4; for (let i = 0; i < 4; i++) {
} this.body_state.feet[i] = this.update_foot_position(i)
}
}
update_feet_positions() { update_foot_position(index: number): number[] {
for (let i = 0; i < 4; i++) { const contact = this.contact_phases[index][this.phase]
this.body_state.feet[i] = this.update_foot_position(i); return contact ? this.stand(index) : this.swing(index)
} }
}
update_foot_position(index: number): number[] { stand(index: number): number[] {
const contact = this.contact_phases[index][this.phase]; const delta_pos = [
return contact ? this.stand(index) : this.swing(index); -this.gait_state.step_x * this.dt * this.swing_stand_ratio,
} 0,
-this.gait_state.step_z * this.dt * this.swing_stand_ratio
]
stand(index: number): number[] { this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0]
const delta_pos = [ this.body_state.feet[index][1] = this.default_feet_pos[index][1]
-this.gait_state.step_x * this.dt * this.swing_stand_ratio, this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2]
0, return this.body_state.feet[index]
-this.gait_state.step_z * this.dt * this.swing_stand_ratio }
];
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0]; swing(index: number): number[] {
this.body_state.feet[index][1] = this.default_feet_pos[index][1]; const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt]
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
return this.body_state.feet[index];
}
swing(index: number): number[] { if (this.gait_state.step_x == 0) {
const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt]; delta_pos[0] =
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8
}
if (this.gait_state.step_x == 0) { if (this.gait_state.step_z == 0) {
delta_pos[0] = delta_pos[2] =
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8; (this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8
} }
if (this.gait_state.step_z == 0) { this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0]
delta_pos[2] = this.body_state.feet[index][1] =
(this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8; this.default_feet_pos[index][1] + sin(this.phase_time * Math.PI) * this.gait_state.step_height
} this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2]
return this.body_state.feet[index]
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0]; }
this.body_state.feet[index][1] =
this.default_feet_pos[index][1] +
sin(this.phase_time * Math.PI) * this.gait_state.step_height;
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
return this.body_state.feet[index];
}
} }
export class FourPhaseWalkState extends PhaseGaitState { export class FourPhaseWalkState extends PhaseGaitState {
protected name = 'Four phase walk'; protected name = 'Four phase walk'
protected num_phases = 4; protected num_phases = 4
protected phase_speed_factor = 2.5; protected phase_speed_factor = 6
protected contact_phases = [ protected contact_phases = [
[1, 0, 1, 1], [1, 0, 1, 1],
[1, 1, 1, 0], [1, 1, 1, 0],
[1, 1, 1, 0], [1, 1, 1, 0],
[1, 0, 1, 1] [1, 0, 1, 1]
]; ]
protected swing_stand_ratio = 1 / (this.num_phases - 1); protected swing_stand_ratio = 1 / (this.num_phases - 1)
begin() { begin() {
super.begin(); super.begin()
} }
end() { end() {
super.end(); super.end()
} }
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt); return super.step(body_state, command, dt)
} }
} }
export class EightPhaseWalkState extends PhaseGaitState { export class EightPhaseWalkState extends PhaseGaitState {
protected name = 'Eight phase walk'; protected name = 'Eight phase walk'
protected num_phases = 8; protected num_phases = 8
protected phase_speed_factor = 1.5; protected phase_speed_factor = 4
protected contact_phases = [ protected contact_phases = [
[1, 0, 1, 1, 1, 1, 1, 1], [1, 0, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1, 1], [1, 1, 1, 1, 1, 0, 1, 1],
[1, 1, 1, 1, 1, 0, 1, 1], [1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 1, 1, 1, 1, 0] [1, 1, 1, 0, 1, 1, 1, 1]
]; ]
protected shifts = [ protected shifts = [
[-0.3, 0, -0.2], [-0.05, 0, -0.2],
[-0.3, 0, 0.2], [0.3, 0, 0.2],
[0.3, 0, -0.2], [-0.05, 0, 0.2],
[0.3, 0, 0.2] [0.3, 0, -0.2]
]; ]
protected swing_stand_ratio = 1 / (this.num_phases - 1); protected swing_stand_ratio = 1 / (this.num_phases - 1)
begin() { begin() {
super.begin(); super.begin()
} }
end() { end() {
super.end(); super.end()
} }
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt); return super.step(body_state, command, dt)
} }
}
export class BezierState extends GaitState {
protected name = 'Bezier'
protected phase = 0
protected phase_num = 0
protected step_length: number = 0
offset = [0, 0.5, 0.5, 0]
begin() {
super.begin()
}
end() {
super.end()
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
super.step(body_state, command, dt)
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2)
if (this.gait_state.step_x < 0) {
this.step_length = -this.step_length
}
this.update_phase()
this.update_feet_positions()
return this.body_state
}
update_phase() {
this.phase += this.dt * this.gait_state.step_velocity * 2
if (this.phase >= 1) {
this.phase_num += 1
this.phase_num %= 2
this.phase = 0
}
}
update_feet_positions() {
for (let i = 0; i < 4; i++) {
this.body_state.feet[i] = this.update_foot_position(i)
}
}
update_foot_position(index: number): number[] {
let phase = this.phase + this.offset[index]
if (phase >= 1) {
phase -= 1
}
this.body_state.feet[index][0] = this.default_feet_pos[index][0]
this.body_state.feet[index][1] = this.default_feet_pos[index][1]
this.body_state.feet[index][2] = this.default_feet_pos[index][2]
return phase <= 0.75 ?
this.stand_controller(index, phase / 0.75)
: this.swing_controller(index, (phase - 0.75) / (1 - 0.75))
}
stand_controller(index: number, phase: number) {
let depth = this.gait_state.step_depth
return this.controller(index, phase, stance_curve, depth)
}
swing_controller(index: number, phase: number) {
let height = this.gait_state.step_height
return this.controller(index, phase, bezier_curve, height)
}
controller(
index: number,
phase: number,
controller: (length: number, angle: number, ...args: number[]) => number[],
...args: number[]
) {
let length = this.step_length / 2
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2
const delta_pos = controller(length, angle, ...args, phase)
length = this.gait_state.step_angle * 2
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index])
const delta_rot = controller(length, angle, ...args, phase)
this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2
this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2
if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle)
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2
return this.body_state.feet[index]
}
}
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
const X_POLAR = Math.cos(angle)
const Y_POLAR = Math.sin(angle)
const step = length * (1 - 2 * phase)
const X = step * X_POLAR
const Z = step * Y_POLAR
let Y = 0
if (length !== 0) {
Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length))
}
return [X, Y, Z]
}
const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => {
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2)
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0])
const offsets = [
current_foot_pos[0] - default_foot_pos[0],
current_foot_pos[2] - default_foot_pos[2],
current_foot_pos[1] - default_foot_pos[1]
]
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2)
const offset_mod = Math.atan2(offset_mag, foot_mag)
return Math.PI / 2.0 + foot_dir + offset_mod
}
const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => {
const control_points = get_control_points(length, angle, height)
const n = control_points.length - 1
const point = [0, 0, 0]
for (let i = 0; i <= n; i++) {
const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i)
point[0] += bernstein_poly * control_points[i][0]
point[1] += bernstein_poly * control_points[i][1]
point[2] += bernstein_poly * control_points[i][2]
}
return point
}
const get_control_points = (length: number, angle: number, height: number): number[][] => {
const X_POLAR = Math.cos(angle)
const Z_POLAR = Math.sin(angle)
const STEP = [
-length,
-length * 1.4,
-length * 1.5,
-length * 1.5,
-length * 1.5,
0.0,
0.0,
0.0,
length * 1.5,
length * 1.5,
length * 1.4,
length
]
const Y = [
0.0,
0.0,
height * 0.9,
height * 0.9,
height * 0.9,
height * 0.9,
height * 0.9,
height * 1.1,
height * 1.1,
height * 1.1,
0.0,
0.0
]
const control_points: number[][] = []
for (let i = 0; i < STEP.length; i++) {
const X = STEP[i] * X_POLAR
const Z = STEP[i] * Z_POLAR
control_points.push([X, Y[i], Z])
}
return control_points
}
const comb = (n: number, k: number): number => {
if (k < 0 || k > n) return 0
if (k === 0 || k === n) return 1
k = Math.min(k, n - k)
let c = 1
for (let i = 0; i < k; i++) {
c = (c * (n - i)) / (i + 1)
}
return c
} }
+118 -307
View File
@@ -1,320 +1,131 @@
export interface body_state_t { export interface body_state_t {
omega: number; omega: number
phi: number; phi: number
psi: number; psi: number
xm: number; xm: number
ym: number; ym: number
zm: number; zm: number
feet: number[][]; feet: number[][]
} }
export interface position { export interface position {
x: number; x: number
y: number; y: number
z: number; z: number
} }
export interface target_position { export interface target_position {
x: number; x: number
z: number; z: number
yaw: number; yaw: number
} }
const { cos, sin, atan2, sqrt } = Math; const { cos, sin, atan2, acos, sqrt, max, min } = Math
const DEG2RAD = 0.017453292519943; const DEG2RAD = 0.017453292519943
export default class Kinematic { export default class Kinematic {
l1: number; l1: number
l2: number; l2: number
l3: number; l3: number
l4: number; l4: number
L: number; L: number
W: number; W: number
DEG2RAD = DEG2RAD; DEG2RAD = DEG2RAD
sHp = sin(Math.PI / 2); mountOffsets: number[][]
cHp = cos(Math.PI / 2);
invMountRot = [
Tlf: number[][] = []; [0, 0, -1],
Trf: number[][] = []; [0, 1, 0],
Tlb: number[][] = []; [1, 0, 0]
Trb: number[][] = []; ]
point_lf: number[][]; constructor() {
point_rf: number[][]; this.l1 = 60.5 / 100
point_lb: number[][]; this.l2 = 10 / 100
point_rb: number[][]; this.l3 = 111.7 / 100
Ix: number[][]; this.l4 = 118.5 / 100
constructor() { this.L = 207.5 / 100
this.l1 = 60.5 / 100; this.W = 78 / 100
this.l2 = 10 / 100;
this.l3 = 100.7 / 100; this.mountOffsets = [
this.l4 = 118.5 / 100; [this.L / 2, 0, this.W / 2],
[this.L / 2, 0, -this.W / 2],
this.L = 207.5 / 100; [-this.L / 2, 0, this.W / 2],
this.W = 78 / 100; [-this.L / 2, 0, -this.W / 2]
]
this.point_lf = [ }
[this.cHp, 0, this.sHp, this.L / 2],
[0, 1, 0, 0], getDefaultFeetPos(): number[][] {
[-this.sHp, 0, this.cHp, this.W / 2], return this.mountOffsets.map((offset, i) => {
[0, 0, 0, 1] return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.l1 : this.l1)]
]; })
}
this.point_rf = [
[this.cHp, 0, this.sHp, this.L / 2], calcIK(p: body_state_t): number[] {
[0, 1, 0, 0], const roll = p.omega * this.DEG2RAD
[-this.sHp, 0, this.cHp, -this.W / 2], const pitch = p.phi * this.DEG2RAD
[0, 0, 0, 1] const yaw = p.psi * this.DEG2RAD
]; const rot = this.euler2R(roll, pitch, yaw)
const inv_rot = [
this.point_lb = [ [rot[0][0], rot[1][0], rot[2][0]],
[this.cHp, 0, this.sHp, -this.L / 2], [rot[0][1], rot[1][1], rot[2][1]],
[0, 1, 0, 0], [rot[0][2], rot[1][2], rot[2][2]]
[-this.sHp, 0, this.cHp, this.W / 2], ]
[0, 0, 0, 1] const inv_trans = [
]; -inv_rot[0][0] * p.xm - inv_rot[0][1] * p.ym - inv_rot[0][2] * p.zm,
-inv_rot[1][0] * p.xm - inv_rot[1][1] * p.ym - inv_rot[1][2] * p.zm,
this.point_rb = [ -inv_rot[2][0] * p.xm - inv_rot[2][1] * p.ym - inv_rot[2][2] * p.zm
[this.cHp, 0, this.sHp, -this.L / 2], ]
[0, 1, 0, 0], return p.feet.flatMap((foot, i) => {
[-this.sHp, 0, this.cHp, -this.W / 2], const [wx, wy, wz] = foot
[0, 0, 0, 1] const bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0]
]; const by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1]
this.Ix = [ const bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2]
[-1, 0, 0, 0],
[0, 1, 0, 0], const [mx, my, mz] = this.mountOffsets[i]
[0, 0, 1, 0], const px = bx - mx,
[0, 0, 0, 1] py = by - my,
]; pz = bz - mz
}
const lx =
public calcIK(body_state: body_state_t): number[] { this.invMountRot[0][0] * px + this.invMountRot[0][1] * py + this.invMountRot[0][2] * pz
this.bodyIK(body_state); const ly =
this.invMountRot[1][0] * px + this.invMountRot[1][1] * py + this.invMountRot[1][2] * pz
return [ const lz =
...this.legIK(this.multiplyVector(this.inverse(this.Tlf), body_state.feet[0])), this.invMountRot[2][0] * px + this.invMountRot[2][1] * py + this.invMountRot[2][2] * pz
...this.legIK(
this.multiplyVector( const xLocal = i % 2 === 1 ? -lx : lx
this.Ix, return this.legIK(xLocal, ly, lz)
this.multiplyVector(this.inverse(this.Trf), body_state.feet[1]) })
) }
),
...this.legIK(this.multiplyVector(this.inverse(this.Tlb), body_state.feet[2])), private legIK(x: number, y: number, z: number): [number, number, number] {
...this.legIK( const F = sqrt(max(0, x * x + y * y - this.l1 * this.l1))
this.multiplyVector( const G = F - this.l2
this.Ix, const H = sqrt(G * G + z * z)
this.multiplyVector(this.inverse(this.Trb), body_state.feet[3]) const t1 = -atan2(y, x) - atan2(F, -this.l1)
) const D = (H * H - this.l3 * this.l3 - this.l4 * this.l4) / (2 * this.l3 * this.l4)
) const t3 = acos(max(-1, min(1, D)))
]; const t2 = atan2(z, G) - atan2(this.l4 * sin(t3), this.l3 + this.l4 * cos(t3))
} return [t1, t2, t3]
}
bodyIK(p: body_state_t) {
const cos_omega = cos(p.omega * this.DEG2RAD); private euler2R(roll: number, pitch: number, yaw: number): number[][] {
const sin_omega = sin(p.omega * this.DEG2RAD); const cr = cos(roll),
const cos_phi = cos(p.phi * this.DEG2RAD); sr = sin(roll)
const sin_phi = sin(p.phi * this.DEG2RAD); const cp = cos(pitch),
const cos_psi = cos(p.psi * this.DEG2RAD); sp = sin(pitch)
const sin_psi = sin(p.psi * this.DEG2RAD); const cy = cos(yaw),
sy = sin(yaw)
const Tm: number[][] = [ return [
[cos_phi * cos_psi, -sin_psi * cos_phi, sin_phi, p.xm], [cp * cy, -cp * sy, sp],
[ [sr * sp * cy + sy * cr, -sr * sp * sy + cr * cy, -sr * cp],
sin_omega * sin_phi * cos_psi + sin_psi * cos_omega, [sr * sy - sp * cr * cy, sr * cy + sp * sy * cr, cr * cp]
-sin_omega * sin_phi * sin_psi + cos_omega * cos_psi, ]
-sin_omega * cos_phi, }
p.ym
],
[
sin_omega * sin_psi - sin_phi * cos_omega * cos_psi,
sin_omega * cos_psi + sin_phi * sin_psi * cos_omega,
cos_omega * cos_phi,
p.zm
],
[0, 0, 0, 1]
];
this.Tlf = this.matrixMultiply(Tm, this.point_lf);
this.Trf = this.matrixMultiply(Tm, this.point_rf);
this.Tlb = this.matrixMultiply(Tm, this.point_lb);
this.Trb = this.matrixMultiply(Tm, this.point_rb);
}
public legIK(point: number[]): number[] {
const [x, y, z] = point;
let F = sqrt(x ** 2 + y ** 2 - this.l1 ** 2);
if (isNaN(F)) F = this.l1;
const G = F - this.l2;
const H = sqrt(G ** 2 + z ** 2);
const theta1 = -atan2(y, x) - atan2(F, -this.l1);
const D = (H ** 2 - this.l3 ** 2 - this.l4 ** 2) / (2 * this.l3 * this.l4);
let theta3 = atan2(sqrt(1 - D ** 2), D);
if (isNaN(theta3)) theta3 = 0;
const theta2 = atan2(z, G) - atan2(this.l4 * sin(theta3), this.l3 + this.l4 * cos(theta3));
return [theta1, theta2, theta3];
}
matrixMultiply(a: number[][], b: number[][]): number[][] {
const result: number[][] = [];
for (let i = 0; i < a.length; i++) {
const row: number[] = [];
for (let j = 0; j < b[0].length; j++) {
let sum = 0;
for (let k = 0; k < a[i].length; k++) {
sum += a[i][k] * b[k][j];
}
row.push(sum);
}
result.push(row);
}
return result;
}
multiplyVector(matrix: number[][], vector: number[]): number[] {
const rows = matrix.length;
const cols = matrix[0].length;
const vectorLength = vector.length;
if (cols !== vectorLength) {
throw new Error('Matrix and vector dimensions do not match for multiplication.');
}
const result = [];
for (let i = 0; i < rows; i++) {
let sum = 0;
for (let j = 0; j < cols; j++) {
sum += matrix[i][j] * vector[j];
}
result.push(sum);
}
return result;
}
private inverse(matrix: number[][]): number[][] {
const det = this.determinant(matrix);
const adjugate = this.adjugate(matrix);
const scalar = 1 / det;
const inverse: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
row.push(adjugate[i][j] * scalar);
}
inverse.push(row);
}
return inverse;
}
private determinant(matrix: number[][]): number {
if (matrix.length !== matrix[0].length) {
throw new Error('The matrix is not square.');
}
if (matrix.length === 2) {
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
}
let det = 0;
for (let i = 0; i < matrix.length; i++) {
const sign = i % 2 === 0 ? 1 : -1;
const subMatrix: number[][] = [];
for (let j = 1; j < matrix.length; j++) {
const row: number[] = [];
for (let k = 0; k < matrix.length; k++) {
if (k !== i) {
row.push(matrix[j][k]);
}
}
subMatrix.push(row);
}
det += sign * matrix[0][i] * this.determinant(subMatrix);
}
return det;
}
private adjugate(matrix: number[][]): number[][] {
if (matrix.length !== matrix[0].length) {
throw new Error('The matrix is not square.');
}
const adjugate: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
const sign = (i + j) % 2 === 0 ? 1 : -1;
const subMatrix: number[][] = [];
for (let k = 0; k < matrix.length; k++) {
if (k !== i) {
const subRow: number[] = [];
for (let l = 0; l < matrix.length; l++) {
if (l !== j) {
subRow.push(matrix[k][l]);
}
}
subMatrix.push(subRow);
}
}
const cofactor = sign * this.determinant(subMatrix);
row.push(cofactor);
}
adjugate.push(row);
}
return this.transpose(adjugate);
}
private transpose(matrix: number[][]): number[][] {
const transposed: number[][] = [];
for (let i = 0; i < matrix.length; i++) {
const row: number[] = [];
for (let j = 0; j < matrix[i].length; j++) {
row.push(matrix[j][i]);
}
transposed.push(row);
}
return transposed;
}
} }
-169
View File
@@ -1,169 +0,0 @@
export type vector = { x: number; y: number };
export interface ControllerInput {
left: vector;
right: vector;
height: number;
speed: number;
s1: number;
}
export type GithubRelease = {
message: string;
tag_name: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
};
export type JWT = { access_token: string };
export type angles = number[] | Int16Array;
export type WifiStatus = {
status: number;
local_ip: string;
mac_address: string;
rssi: number;
ssid: string;
bssid: string;
channel: number;
subnet_mask: string;
gateway_ip: string;
dns_ip_1: string;
dns_ip_2?: string;
};
export type WifiSettings = {
hostname: string;
priority_RSSI: boolean;
wifi_networks: NetworkItem[];
};
export type NetworkList = {
networks: NetworkItem[];
};
export type KnownNetworkItem = {
ssid: string;
password: string;
static_ip_config: boolean;
local_ip?: string;
subnet_mask?: string;
gateway_ip?: string;
dns_ip_1?: string;
dns_ip_2?: string;
};
export type NetworkItem = {
rssi: number;
ssid: string;
bssid: string;
channel: number;
encryption_type: number;
};
export type ApStatus = {
status: number;
ip_address: string;
mac_address: string;
station_num: number;
};
export type ApSettings = {
provision_mode: number;
ssid: string;
password: string;
channel: number;
ssid_hidden: boolean;
max_clients: number;
local_ip: string;
gateway_ip: string;
subnet_mask: string;
};
export type LightState = {
led_on: boolean;
};
export type NTPStatus = {
status: number;
utc_time: string;
local_time: string;
server: string;
uptime: number;
};
export type NTPSettings = {
enabled: boolean;
server: string;
tz_label: string;
tz_format: string;
};
export type Analytics = {
max_alloc_heap: number;
psram_size: number;
free_psram: number;
free_heap: number;
total_heap: number;
min_free_heap: number;
core_temp: number;
fs_total: number;
fs_used: number;
uptime: number;
};
export type StaticSystemInformation = {
esp_platform: string;
firmware_version: string;
cpu_freq_mhz: number;
cpu_type: string;
cpu_rev: number;
cpu_cores: number;
sketch_size: number;
free_sketch_space: number;
sdk_version: string;
arduino_version: string;
flash_chip_size: number;
flash_chip_speed: number;
cpu_reset_reason: string;
};
export type SystemInformation = Analytics & StaticSystemInformation;
export type CameraSettings = {
framesize: number;
quality: number;
brightness: number;
contrast: number;
saturation: number;
sharpness: number;
denoise: number;
special_effect: number;
wb_mode: number;
vflip: boolean;
hmirror: boolean;
};
export type File = number;
export interface Directory {
[key: string]: File | Directory;
}
export type Servo = {
name: string;
channel: number;
inverted: boolean;
angle: number;
center_angle: number;
};
export type ServoConfiguration = {
is_active: boolean;
servo_pwm_frequency: number;
servo_oscillator_frequency: number;
servos: Servo[];
};
+327 -299
View File
@@ -1,29 +1,31 @@
import { import {
Mesh, Mesh,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Scene, Scene,
WebGLRenderer, WebGLRenderer,
AmbientLight, AmbientLight,
DirectionalLight, DirectionalLight,
PCFSoftShadowMap, PCFSoftShadowMap,
GridHelper, type GridHelper,
ArrowHelper, ArrowHelper,
Vector3, Vector3,
FogExp2, FogExp2,
CanvasTexture, CanvasTexture,
type ColorRepresentation, type ColorRepresentation,
type WebGLRendererParameters, type WebGLRendererParameters,
MeshPhongMaterial, MeshPhongMaterial,
EquirectangularReflectionMapping, EquirectangularReflectionMapping,
ACESFilmicToneMapping, ACESFilmicToneMapping,
MathUtils, MathUtils,
MeshStandardMaterial, Group,
Group MeshBasicMaterial,
RepeatWrapping
} from 'three'; } from 'three';
import { Sky } from 'three/addons/objects/Sky.js'; import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { Reflector } from 'three/examples/jsm/objects/Reflector.js';
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'; import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader';
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'; import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls';
import { sunCalculator } from './utilities/position-utilities'; import { sunCalculator } from './utilities/position-utilities';
@@ -31,321 +33,347 @@ import { sunCalculator } from './utilities/position-utilities';
export const addScene = () => new Scene(); export const addScene = () => new Scene();
interface position { interface position {
x?: number; x?: number;
y?: number; y?: number;
z?: number; z?: number;
} }
interface light { interface light {
color?: ColorRepresentation; color?: ColorRepresentation;
intensity?: number; intensity?: number;
}
interface gridOptions {
divisions?: number;
size?: number;
} }
interface arrowOptions { interface arrowOptions {
origin: position; origin: position;
direction: position; direction: position;
length?: number; length?: number;
color?: ColorRepresentation; color?: ColorRepresentation;
} }
type directionalLight = position & light; type directionalLight = position & light;
type gridHelperOptions = gridOptions & position;
export default class SceneBuilder { export default class SceneBuilder {
public scene: Scene; public scene: Scene;
public camera!: PerspectiveCamera; public camera!: PerspectiveCamera;
public ground!: Mesh; public ground!: Mesh;
public renderer!: WebGLRenderer; public renderer!: WebGLRenderer;
public orbit: OrbitControls; public orbit: OrbitControls;
public callback: Function | undefined; public callback: Function | undefined;
public gridHelper!: GridHelper; public gridHelper!: GridHelper;
public model!: URDFRobot; public model!: URDFRobot;
public liveStreamTexture!: CanvasTexture; public liveStreamTexture!: CanvasTexture;
private fog!: FogExp2; private fog!: FogExp2;
private isLoaded: boolean = false; private isLoaded: boolean = false;
public isDragging: boolean = false; public isDragging: boolean = false;
highlightMaterial: any; highlightMaterial: any;
sky!: Sky; sky!: Sky;
transformControl: TransformControls; transformControl: TransformControls;
public modelGroup!: Group; public modelGroup!: Group;
constructor() { constructor() {
this.scene = new Scene(); this.scene = new Scene();
if (this.scene.environment?.mapping) { if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping; this.scene.environment.mapping = EquirectangularReflectionMapping;
} }
return this; return this;
} }
public addRenderer = (parameters?: WebGLRendererParameters) => { public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters); this.renderer = new WebGLRenderer(parameters);
this.renderer.outputColorSpace = 'srgb'; this.renderer.outputColorSpace = 'srgb';
this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = PCFSoftShadowMap; this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping; this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85; this.renderer.toneMappingExposure = 0.85;
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement); if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
return this; return this;
}; };
public addSky = () => { public addSky = () => {
this.sky = new Sky(); this.sky = new Sky();
this.sky.scale.setScalar(450000); this.sky.scale.setScalar(450000);
this.scene.add(this.sky); this.scene.add(this.sky);
const effectController = { const effectController = {
turbidity: 10, turbidity: 10,
rayleigh: 3, rayleigh: 3,
mieCoefficient: 0.005, mieCoefficient: 0.005,
mieDirectionalG: 0.7, mieDirectionalG: 0.7,
elevation: sunCalculator.calculateSunElevation(), elevation: sunCalculator.calculateSunElevation(),
azimuth: 180, azimuth: 200,
exposure: this.renderer.toneMappingExposure exposure: this.renderer.toneMappingExposure
}; };
const uniforms = this.sky.material.uniforms; const uniforms = this.sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity; uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh; uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient; uniforms['mieCoefficient'].value = effectController.mieCoefficient;
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
this.renderer.toneMappingExposure = 0.5; this.renderer.toneMappingExposure = 0.5;
const phi = MathUtils.degToRad(90 - effectController.elevation); const phi = MathUtils.degToRad(90 - effectController.elevation);
const theta = MathUtils.degToRad(effectController.azimuth); const theta = MathUtils.degToRad(effectController.azimuth);
const sun = new Vector3(); const sun = new Vector3();
sun.setFromSphericalCoords(1, phi, theta); sun.setFromSphericalCoords(1, phi, theta);
uniforms['sunPosition'].value.copy(sun); uniforms['sunPosition'].value.copy(sun);
return this; return this;
}; };
public addPerspectiveCamera = (options: position) => { public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera(); this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0); this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
this.scene.add(this.camera); this.scene.add(this.camera);
return this; return this;
}; };
public addGroundPlane = (options?: position) => { public addGroundPlane = (options?: position) => {
var planeMaterial = new MeshStandardMaterial({ color: 0x808080, side: 2, opacity: 0.5 }); const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
this.ground = new Mesh(new PlaneGeometry(), planeMaterial); checkerboardTexture.wrapS = RepeatWrapping;
this.ground.rotation.x = -Math.PI / 2; checkerboardTexture.wrapT = RepeatWrapping;
this.ground.scale.setScalar(30); checkerboardTexture.repeat.set(100, 100);
this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0); const checkerboardMat = new MeshBasicMaterial({
this.ground.receiveShadow = true; map: checkerboardTexture,
this.scene.add(this.ground); opacity: 0.1,
return this; transparent: true
}; });
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => { const plane = new PlaneGeometry(400, 400);
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
this.orbit.minDistance = minDistance;
this.orbit.maxDistance = maxDistance;
this.orbit.autoRotate = autoRotate;
this.orbit.update();
return this;
};
public addAmbientLight = (options: light) => { this.ground = new Mesh(plane, checkerboardMat);
const ambientLight = new AmbientLight(options.color, options.intensity); this.ground.rotation.x = -Math.PI / 2;
this.scene.add(ambientLight); this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0);
return this; this.ground.receiveShadow = true;
}; this.scene.add(this.ground);
public addDirectionalLight = (options: directionalLight) => { const mirror = new Reflector(plane, {
const directionalLight = new DirectionalLight(options.color, options.intensity); clipBias: 0.003,
directionalLight.castShadow = true; textureWidth: window.innerWidth * window.devicePixelRatio,
directionalLight.shadow.camera.top = 10; textureHeight: window.innerHeight * window.devicePixelRatio,
directionalLight.shadow.camera.bottom = -10; color: 0x00bfff
directionalLight.shadow.camera.right = 10; });
directionalLight.shadow.camera.left = -10; mirror.rotateX(-Math.PI / 2);
directionalLight.shadow.mapSize.set(4096, 4096); this.scene.add(mirror);
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); return this;
this.scene.add(directionalLight); };
return this;
};
public addGridHelper = (options: gridHelperOptions) => { public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.gridHelper = new GridHelper(options.size, options.divisions); this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.orbit.minDistance = minDistance;
this.gridHelper.material.opacity = 0.2; this.orbit.maxDistance = maxDistance;
this.gridHelper.material.depthWrite = false; this.orbit.autoRotate = autoRotate;
this.gridHelper.material.transparent = true; this.orbit.update();
this.scene.add(this.gridHelper); return this;
return this; };
};
public addFogExp2 = (color: ColorRepresentation, density?: number) => { public addAmbientLight = (options: light) => {
this.scene.fog = new FogExp2(color, density); const ambientLight = new AmbientLight(options.color, options.intensity);
return this; this.scene.add(ambientLight);
}; return this;
};
public fillParent = () => { public addDirectionalLight = (options: directionalLight) => {
const parentElement = this.renderer.domElement.parentElement; const directionalLight = new DirectionalLight(options.color, options.intensity);
if (parentElement) { directionalLight.castShadow = true;
const width = parentElement.clientWidth; directionalLight.shadow.camera.top = 10;
const height = parentElement.clientHeight; directionalLight.shadow.camera.bottom = -10;
this.handleResize(width, height); directionalLight.shadow.camera.right = 10;
} directionalLight.shadow.camera.left = -10;
return this; directionalLight.shadow.mapSize.set(4096, 4096);
};
public handleResize = (width = window.innerWidth, height = window.innerHeight) => { directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.renderer.setSize(width, height); this.scene.add(directionalLight);
this.renderer.setPixelRatio(window.devicePixelRatio); return this;
this.camera.aspect = width / height; };
this.camera.updateProjectionMatrix();
return this;
};
public addRenderCb = (callback: Function) => { private createCheckerboardTexture = (size: number, squares: number) => {
this.callback = callback; const canvas = document.createElement('canvas');
return this; canvas.width = size;
}; canvas.height = size;
const context = canvas.getContext('2d');
public startRenderLoop = () => { const squareSize = size / squares;
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera);
this.orbit.update();
this.handleRobotShadow();
if (this.callback) this.callback();
if (!this.liveStreamTexture) return;
});
return this;
};
public addArrowHelper = (options?: arrowOptions) => { for (let y = 0; y < squares; y++) {
const dir = new Vector3( for (let x = 0; x < squares; x++) {
options?.direction.x ?? 0, context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
options?.direction.y ?? 0, context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
options?.direction.z ?? 0 }
); }
const origin = new Vector3(
options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
);
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
);
this.scene.add(arrowHelper);
return this;
};
private setJointValue(jointName: string, angle: number) { const texture = new CanvasTexture(canvas);
if (!this.model) return; texture.wrapS = texture.wrapT = RepeatWrapping;
if (!this.model.joints[jointName]) return; texture.anisotropy = 16;
this.model.joints[jointName].setJointValue(angle); return texture;
} };
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'; public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density);
return this;
};
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { public fillParent = () => {
const traverse = (c: any) => { const parentElement = this.renderer.domElement.parentElement;
if (c.type === 'Mesh') { if (parentElement) {
if (revert) { const width = parentElement.clientWidth;
c.material = c.__origMaterial; const height = parentElement.clientHeight;
delete c.__origMaterial; this.handleResize(width, height);
} else { }
c.__origMaterial = c.material; return this;
c.material = material; };
}
}
if (c === m || !this.isJoint(c)) { public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
for (let i = 0; i < c.children.length; i++) { this.renderer.setSize(width, height);
const child = c.children[i]; this.renderer.setPixelRatio(window.devicePixelRatio);
if (!child.isURDFCollider) { this.camera.aspect = width / height;
traverse(c.children[i]); this.camera.updateProjectionMatrix();
} return this;
} };
}
};
traverse(m);
};
public addTransformControls = (model: any) => { public addRenderCb = (callback: Function) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement); this.callback = callback;
this.transformControl.addEventListener('dragging-changed', (event: any) => { return this;
this.orbit.enabled = !event.value; };
this.isDragging = !event.value;
});
this.transformControl.attach(model);
this.scene.add(this.transformControl);
this.transformControl.setMode('rotate');
return this;
};
public addModel = (model: any) => { public startRenderLoop = () => {
this.modelGroup = new Group(); this.renderer.setAnimationLoop(() => {
this.modelGroup.add(model); this.renderer.render(this.scene, this.camera);
this.model = model; this.orbit.update();
this.scene.add(this.modelGroup); this.handleRobotShadow();
return this; if (this.callback) this.callback();
}; if (!this.liveStreamTexture) return;
});
return this;
};
public addDragControl = (updateAngle: any) => { public addArrowHelper = (options?: arrowOptions) => {
const highlightColor = '#FFFFFF'; const dir = new Vector3(
const highlightMaterial = new MeshPhongMaterial({ options?.direction.x ?? 0,
shininess: 10, options?.direction.y ?? 0,
color: highlightColor, options?.direction.z ?? 0
emissive: highlightColor, );
emissiveIntensity: 0.25 const origin = new Vector3(
}); options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
);
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
);
this.scene.add(arrowHelper);
return this;
};
const dragControls = new PointerURDFDragControls( private setJointValue(jointName: string, angle: number) {
this.scene, if (!this.model) return;
this.camera, if (!this.model.joints[jointName]) return;
this.renderer.domElement this.model.joints[jointName].setJointValue(angle);
); }
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle);
};
dragControls.onDragStart = () => {
this.orbit.enabled = false;
this.isDragging = true;
};
dragControls.onDragEnd = () => {
this.orbit.enabled = true;
this.isDragging = false;
};
dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
this.renderer.domElement.addEventListener('touchstart', (data) => isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
dragControls._mouseDown(data.touches[0])
);
this.renderer.domElement.addEventListener('touchmove', (data) =>
dragControls._mouseMove(data.touches[0])
);
this.renderer.domElement.addEventListener('touchend', (data) =>
dragControls._mouseUp(data.touches[0])
);
return this;
};
public toggleFog = () => { highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
this.scene.fog = this.scene.fog ? null : this.fog; const traverse = (c: any) => {
}; if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial;
delete c.__origMaterial;
} else {
c.__origMaterial = c.material;
c.material = material;
}
}
private handleRobotShadow = () => { if (c === m || !this.isJoint(c)) {
if (this.isLoaded) return; for (let i = 0; i < c.children.length; i++) {
const intervalId = setInterval(() => { const child = c.children[i];
this.model?.traverse((c) => (c.castShadow = true)); if (!child.isURDFCollider) {
}, 10); traverse(c.children[i]);
setTimeout(() => { }
clearInterval(intervalId); }
}, 1000); }
this.isLoaded = true; };
}; traverse(m);
};
public addTransformControls = (model: any) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
this.transformControl.addEventListener('dragging-changed', (event: any) => {
this.orbit.enabled = !event.value;
this.isDragging = !event.value;
});
this.transformControl.attach(model);
this.scene.add(this.transformControl);
this.transformControl.setMode('rotate');
return this;
};
public addModel = (model: any) => {
this.modelGroup = new Group();
this.modelGroup.add(model);
this.model = model;
this.scene.add(this.modelGroup);
return this;
};
public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF';
const highlightMaterial = new MeshPhongMaterial({
shininess: 10,
color: highlightColor,
emissive: highlightColor,
emissiveIntensity: 0.9
});
const dragControls = new PointerURDFDragControls(
this.scene,
this.camera,
this.renderer.domElement
);
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle);
};
dragControls.onDragStart = () => {
this.orbit.enabled = false;
this.isDragging = true;
};
dragControls.onDragEnd = () => {
this.orbit.enabled = true;
this.isDragging = false;
};
dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
this.renderer.domElement.addEventListener(
'touchstart',
data => dragControls._mouseDown(data.touches[0]),
{ passive: true }
);
this.renderer.domElement.addEventListener(
'touchmove',
data => dragControls._mouseMove(data.touches[0]),
{ passive: true }
);
this.renderer.domElement.addEventListener(
'touchend',
data => dragControls._mouseUp(data.touches[0]),
{ passive: true }
);
return this;
};
public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog;
};
private handleRobotShadow = () => {
if (this.isLoaded) return;
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10);
setTimeout(() => clearInterval(intervalId), 1000);
this.isLoaded = true;
};
} }
+20 -37
View File
@@ -1,51 +1,38 @@
import { Result } from '$lib/utilities/result'; import { Result } from '$lib/utilities/result';
import { browser } from '$app/environment';
class FileService { class FileService {
private dbName = 'fileStorageDB'; private dbPromise: Promise<Result<IDBDatabase, string>> | null = browser
private dbVersion = 1; ? this.openDatabase()
private storeName = 'files'; : null;
private dbPromise: Promise<Result<IDBDatabase, string>>;
constructor() {
this.dbPromise = this.openDatabase();
}
private async openDatabase(): Promise<Result<IDBDatabase, string>> { private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => { return new Promise((resolve) => {
const request = indexedDB.open(this.dbName, this.dbVersion); const request = indexedDB.open('fileStorageDB', 1);
request.onerror = () => resolve(Result.err('Error opening database')); request.onupgradeneeded = () => {
request.result.createObjectStore('files');
request.onsuccess = () => resolve(Result.ok(request.result));
request.onupgradeneeded = (event) => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
}; };
request.onsuccess = () => resolve(Result.ok(request.result));
request.onerror = () => resolve(Result.err('Error opening database'));
}); });
} }
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> { private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
if (!browser || !this.dbPromise)
return Result.err('Not running in browser or DB not initialized');
const dbResult = await this.dbPromise; const dbResult = await this.dbPromise;
if (dbResult.isErr()) { if (dbResult.isErr()) return Result.err('Database not initialized');
return Result.err('Database not initialized properly'); const store = dbResult.inner.transaction('files', mode).objectStore('files');
} return Result.ok(store);
const db = dbResult.inner;
const transaction = db.transaction(this.storeName, mode);
return Result.ok(transaction.objectStore(this.storeName));
} }
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> { public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
const storeResult = await this.getStore('readwrite'); const storeResult = await this.getStore('readwrite');
if (storeResult.isErr()) { if (storeResult.isErr()) return Result.err('Failed to access store');
return Result.err('Failed to access object store for writing');
}
const store = storeResult.inner;
return new Promise((resolve) => { return new Promise((resolve) => {
const request = store.put(file, key); const request = storeResult.inner.put(file, key);
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result));
request.onerror = () => resolve(Result.err('Failed to save file')); request.onerror = () => resolve(Result.err('Failed to save file'));
}); });
@@ -53,19 +40,15 @@ class FileService {
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> { public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore('readonly'); const storeResult = await this.getStore('readonly');
if (storeResult.isErr()) { if (storeResult.isErr()) return Result.err('Failed to access store');
return Result.err('Failed to access object store for reading');
}
const store = storeResult.inner;
return new Promise((resolve) => { return new Promise((resolve) => {
const request = store.get(key); const request = storeResult.inner.get(key);
request.onsuccess = () => request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File content not found')); resolve(request.result ? Result.ok(request.result) : Result.err('File not found'));
request.onerror = () => resolve(Result.err('Failed to retrieve file')); request.onerror = () => resolve(Result.err('Failed to retrieve file'));
}); });
} }
} }
export default new FileService(); export default browser ? new FileService() : null;
+67
View File
@@ -0,0 +1,67 @@
import { persistentStore } from '$lib/utilities';
import { get, type Writable } from 'svelte/store';
import Visualization from '$lib/components/Visualization.svelte';
import Stream from '$lib/components/Stream.svelte';
import ChartWidget from '$lib/components/widget/ChartWidget.svelte';
export interface WidgetConfig {
id: string | number;
component: keyof typeof WidgetComponents;
props?: Record<string, any>;
}
export interface WidgetContainerConfig {
id: string | number;
layout?: 'row' | 'column' | 'wrap';
header?: string;
widgets: Array<WidgetConfig | WidgetContainerConfig>;
}
export const isWidgetConfig = (
widget: WidgetConfig | WidgetContainerConfig
): widget is WidgetConfig => 'component' in widget;
export const WidgetComponents = {
Visualization,
Stream,
ChartWidget
};
interface View {
name: string;
content: WidgetContainerConfig;
}
const defaultViews: View[] = [
{
name: 'Stream',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Stream' }]
}
},
{
name: '3D representation',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
}
},
{
name: 'Split screen',
content: {
id: 'root',
widgets: [
{ id: 2, component: 'Stream' },
{ id: 2, component: 'Visualization', props: { debug: true } }
]
}
}
];
export const views: Writable<View[]> = persistentStore('views', defaultViews);
export const selectedView = persistentStore('selected_view', get(views)[0].name);
+20
View File
@@ -0,0 +1,20 @@
import { api } from '$lib/api';
import { notifications } from '$lib/components/toasts/notifications';
import { writable, type Writable } from 'svelte/store';
let featureFlagsStore: Writable<Record<string, boolean>>;
export function useFeatureFlags() {
if (!featureFlagsStore) {
featureFlagsStore = writable<Record<string, boolean>>({});
api.get<Record<string, boolean>>('/api/features').then((result) => {
if (result.isOk()) featureFlagsStore.set(result.inner);
else {
notifications.error('Feature flag could not be fetched', 2500);
}
});
}
return featureFlagsStore;
}
+24
View File
@@ -0,0 +1,24 @@
import { writable } from 'svelte/store';
export const isFullscreen = writable(false);
export function toggleFullscreen() {
isFullscreen.update((state) => {
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen();
return !state;
});
}
export function enterFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
isFullscreen.set(true);
}
}
export function exitFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
isFullscreen.set(false);
}
}
+47
View File
@@ -0,0 +1,47 @@
import { readable, derived } from 'svelte/store'
export type GamepadState = {
available: boolean
gamepads: Gamepad[]
}
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
const update = () => {
const hasGamepadAPI = 'getGamepads' in navigator
if (!hasGamepadAPI) {
set({ available: false, gamepads: [] })
return
}
const gps = navigator.getGamepads?.() ?? []
const validGamepads = gps.filter(Boolean) as Gamepad[]
set({
available: true,
gamepads: validGamepads
})
raf = requestAnimationFrame(update)
}
window.addEventListener('gamepadconnected', update)
window.addEventListener('gamepaddisconnected', update)
let raf = requestAnimationFrame(update)
return () => {
cancelAnimationFrame(raf)
window.removeEventListener('gamepadconnected', update)
window.removeEventListener('gamepaddisconnected', update)
}
})
export const gamepad = derived(gamepads, $gamepads =>
$gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null
)
export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0])
export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? [])
export const hasGamepad = derived(
gamepads,
$gamepads => $gamepads.available && $gamepads.gamepads.length > 0
)
+21 -30
View File
@@ -1,36 +1,27 @@
import { type IMU } from '$lib/types/models';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { IMU } from '$lib/types/models';
let imu_data = {
x: <number[]>[],
y: <number[]>[],
z: <number[]>[],
imu_temp: <number[]>[],
altitude: <number[]>[],
pressure: <number[]>[],
bmp_temp: <number[]>[]
};
const maxIMUData = 100; const maxIMUData = 100;
function createIMU() { export const imu = (() => {
const { subscribe, update } = writable(imu_data); const { subscribe, update } = writable({
x: [] as number[],
y: [] as number[],
z: [] as number[],
heading: [] as number[],
altitude: [] as number[],
pressure: [] as number[],
bmp_temp: [] as number[]
});
return { const addData = (content: IMU) => {
subscribe, update(data => {
addData: (content: IMU) => { (Object.keys(content) as (keyof IMU)[]).forEach(key => {
update((imu_data) => ({ data[key] = [...data[key], content[key]].slice(-maxIMUData);
...imu_data, });
x: [...imu_data.x, content.x].slice(-maxIMUData), return data;
y: [...imu_data.y, content.y].slice(-maxIMUData), });
z: [...imu_data.z, content.z].slice(-maxIMUData), };
imu_temp: [...imu_data.imu_temp, content.imu_temp].slice(-maxIMUData),
altitude: [...imu_data.altitude, content.altitude].slice(-maxIMUData),
pressure: [...imu_data.pressure, content.pressure].slice(-maxIMUData),
bmp_temp: [...imu_data.bmp_temp, content.bmp_temp].slice(-maxIMUData)
}));
}
};
}
export const imu = createIMU(); return { subscribe, addData };
})();
+5
View File
@@ -2,3 +2,8 @@ export * from './socket-store';
export * from './logging-store'; export * from './logging-store';
export * from './model-store'; export * from './model-store';
export * from './socket'; export * from './socket';
export * from './fullscreen';
export * from './telemetry';
export * from './analytics';
export * from './featureFlags';
export * from './location-store';
+5
View File
@@ -0,0 +1,5 @@
import { persistentStore } from '$lib/utilities';
import { writable } from 'svelte/store';
import appEnv from 'app-env';
export const location = appEnv.VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
+12 -4
View File
@@ -1,14 +1,22 @@
import type { ControllerInput } from '$lib/models'; import type { ControllerInput } from '$lib/types/models';
import { persistentStore } from '$lib/utilities/svelte-utilities'; import { persistentStore } from '$lib/utilities/svelte-utilities';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
export const emulateModel = writable(true); export const emulateModel = writable(true);
export const jointNames = persistentStore('joint_names', []); export const jointNames = persistentStore('joint_names', <string[]>[]);
export const model = writable(); export const model = writable();
export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'crawl', 'walk'] as const; export const modes = [
'deactivated',
'idle',
'calibration',
'rest',
'stand',
'crawl',
'walk'
] as const;
export type Modes = (typeof modes)[number]; export type Modes = (typeof modes)[number];
@@ -22,7 +30,7 @@ export enum ModesEnum {
Walk Walk
} }
export const mode: Writable<ModesEnum> = writable(ModesEnum.Walk); export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated);
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]); export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]);
+1 -4
View File
@@ -1,5 +1,5 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
import { type angles } from '$lib/models'; import { type angles } from '$lib/types/models';
export const servoAnglesOut: Writable<number[]> = writable([ export const servoAnglesOut: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
@@ -8,7 +8,6 @@ export const servoAngles: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]); ]);
export const logs = writable([] as string[]); export const logs = writable([] as string[]);
export const battery = writable({});
export const mpu = writable({ heading: 0 }); export const mpu = writable({ heading: 0 });
export const sonar = writable([0, 0]); export const sonar = writable([0, 0]);
export const distances = writable({}); export const distances = writable({});
@@ -16,7 +15,6 @@ export const distances = writable({});
export interface socketDataCollection { export interface socketDataCollection {
angles: Writable<angles>; angles: Writable<angles>;
logs: Writable<string[]>; logs: Writable<string[]>;
battery: Writable<unknown>;
mpu: Writable<unknown>; mpu: Writable<unknown>;
distances: Writable<unknown>; distances: Writable<unknown>;
} }
@@ -24,7 +22,6 @@ export interface socketDataCollection {
export const socketData = { export const socketData = {
angles: servoAngles, angles: servoAngles,
logs, logs,
battery,
mpu, mpu,
distances distances
}; };
+128 -118
View File
@@ -1,122 +1,132 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const; const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number]; type SocketEvent = (typeof socketEvents)[number]
function createWebSocket() { export enum Topics {
let listeners = new Map<string, Set<(data?: unknown) => void>>(); imu = 0,
const { subscribe, set } = writable(false); mode = 1,
const reconnectTimeoutTime = 5000; command = 2,
let unresponsiveTimeoutId: number; servo = 3,
let reconnectTimeoutId: number; input = 4,
let ws: WebSocket; angles = 5,
let socketUrl: string | URL; position = 6
function init(url: string | URL) {
socketUrl = url;
connect();
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close();
set(false);
clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach((listener) => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
}
function connect() {
ws = new WebSocket(socketUrl);
ws.onopen = (ev) => {
set(true);
clearTimeout(reconnectTimeoutId);
listeners.get('open')?.forEach((listener) => listener(ev));
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue;
subscribeToEvent(event);
}
};
ws.onmessage = (message) => {
resetUnresponsiveCheck();
let data = message.data;
if (data instanceof ArrayBuffer) {
listeners.get('binary')?.forEach((listener) => listener(data));
return;
}
data = data.substring(1);
if (!data) return;
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
try {
payload = JSON.parse(payload);
} catch (error) {}
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
};
ws.onerror = (ev) => disconnect('error', ev);
ws.onclose = (ev) => disconnect('close', ev);
}
function unsubscribe(event: string, listener?: (data: any) => void) {
let eventListeners = listeners.get(event);
if (!eventListeners) return;
if (!eventListeners.size) {
unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId);
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
}
function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(`2/${event}[${JSON.stringify(data)}]`);
}
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('1/' + event);
}
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('0/' + event);
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: any) => void);
return () => {
unsubscribe(event, listener);
};
},
off: (event: string, listener?: (data: any) => void) => {
unsubscribe(event, listener);
}
};
} }
export const socket = createWebSocket(); function createWebSocket() {
let listeners = new Map<string | Topics, Set<(data?: unknown) => void>>()
const { subscribe, set } = writable(false)
const reconnectTimeoutTime = 5000
let unresponsiveTimeoutId: number
let reconnectTimeoutId: number
let ws: WebSocket
let socketUrl: string | URL
function init(url: string | URL) {
socketUrl = url
connect()
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close()
set(false)
clearTimeout(unresponsiveTimeoutId)
clearTimeout(reconnectTimeoutId)
listeners.get(reason)?.forEach(listener => listener(event))
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
}
function connect() {
ws = new WebSocket(socketUrl)
ws.onopen = ev => {
set(true)
clearTimeout(reconnectTimeoutId)
listeners.get('open')?.forEach(listener => listener(ev))
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue
subscribeToEvent(event as unknown as Topics)
}
}
ws.onmessage = message => {
resetUnresponsiveCheck()
let data = message.data
if (data instanceof ArrayBuffer) {
listeners.get('binary')?.forEach(listener => listener(data))
return
}
data = data.substring(1)
if (!data) return
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['))
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'))
try {
payload = JSON.parse(payload)
} catch (error) {}
if (event) listeners.get(event)?.forEach(listener => listener(payload))
}
ws.onerror = ev => disconnect('error', ev)
ws.onclose = ev => disconnect('close', ev)
}
function unsubscribe(event: Topics, listener?: (data: any) => void) {
let eventListeners = listeners.get(event)
if (!eventListeners) return
if (!eventListeners.size) {
unsubscribeToEvent(event)
}
if (listener) {
eventListeners?.delete(listener)
} else {
listeners.delete(event)
}
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId)
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
}
function sendEvent(event: Topics, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(JSON.stringify([2, event, data]))
}
function unsubscribeToEvent(event: Topics) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(`[1,${event}]`)
}
function subscribeToEvent(event: Topics) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(`[0,${event}]`)
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: Topics | SocketEvent, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event)
if (!eventListeners) {
if (!socketEvents.includes(event)) {
subscribeToEvent(event)
}
eventListeners = new Set()
listeners.set(event, eventListeners)
}
eventListeners.add(listener as (data: any) => void)
return () => {
unsubscribe(event, listener)
}
},
off: (event: Topics, listener?: (data: any) => void) => {
unsubscribe(event, listener)
}
}
}
export const socket = createWebSocket()
+25 -35
View File
@@ -1,45 +1,35 @@
import type { Battery, DownloadOTA } from '$lib/types/models'; import type { DownloadOTA } from '$lib/types/models';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
let telemetry_data = { let telemetry_data = {
rssi: { rssi: {
rssi: 0 rssi: 0
}, },
battery: { download_ota: {
voltage: 100, status: 'none',
current: false progress: 0,
}, error: ''
download_ota: { }
status: 'none',
progress: 0,
error: ''
}
}; };
function createTelemetry() { function createTelemetry() {
const { subscribe, set, update } = writable(telemetry_data); const { subscribe, set, update } = writable(telemetry_data);
return { return {
subscribe, subscribe,
setRSSI: (data: number) => { setRSSI: (data: number) => {
update((telemetry_data) => ({ update(telemetry_data => ({
...telemetry_data, ...telemetry_data,
rssi: { rssi: data } rssi: { rssi: data }
})); }));
}, },
setBattery: (data: Battery) => { setDownloadOTA: (data: DownloadOTA) => {
update((telemetry_data) => ({ update(telemetry_data => ({
...telemetry_data, ...telemetry_data,
battery: { voltage: data.voltage, current: data.current } download_ota: { status: data.status, progress: data.progress, error: data.error }
})); }));
}, }
setDownloadOTA: (data: DownloadOTA) => { };
update((telemetry_data) => ({
...telemetry_data,
download_ota: { status: data.status, progress: data.progress, error: data.error }
}));
}
};
} }
export const telemetry = createTelemetry(); export const telemetry = createTelemetry();
-55
View File
@@ -1,55 +0,0 @@
import { writable } from 'svelte/store';
import { goto } from '$app/navigation';
import { jwtDecode } from 'jwt-decode';
export type userProfile = {
username: string;
admin: boolean;
bearer_token: string;
};
type decodedJWT = {
username: string;
admin: boolean;
};
let empty = {
username: '',
admin: false,
bearer_token: ''
};
function createStore() {
const { subscribe, set } = writable(empty);
// retrieve store from sessionStorage / localStorage if available
const userdata = localStorage.getItem('user');
if (userdata) {
set(JSON.parse(userdata));
}
return {
subscribe,
init: (access_token: string) => {
const decoded: decodedJWT = jwtDecode(access_token);
const userdata = {
bearer_token: access_token,
username: decoded.username,
admin: decoded.admin
};
set(userdata);
// persist store in sessionStorage / localStorage
localStorage.setItem('user', JSON.stringify(userdata));
},
invalidate: () => {
console.log('Log out user');
set(empty);
// remove localStorage "user"
localStorage.removeItem('user');
// redirect to login page
goto('/');
}
};
}
export const user = createStore();
+17
View File
@@ -0,0 +1,17 @@
declare module 'three/src/math/MathUtils' {
export function generateUUID(): string;
export function clamp(value: number, min: number, max: number): number;
export function euclideanModulo(n: number, m: number): number;
export function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number;
export function lerp(x: number, y: number, t: number): number;
export function smoothstep(x: number, min: number, max: number): number;
export function smootherstep(x: number, min: number, max: number): number;
export function randInt(low: number, high: number): number;
export function randFloat(low: number, high: number): number;
export function randFloatSpread(range: number): number;
export function degToRad(degrees: number): number;
export function radToDeg(radians: number): number;
export function isPowerOfTwo(value: number): boolean;
export function ceilPowerOfTwo(value: number): number;
export function floorPowerOfTwo(value: number): number;
}
+197 -119
View File
@@ -1,143 +1,221 @@
export type vector = { x: number; y: number }
export interface ControllerInput {
left: vector
right: vector
height: number
speed: number
s1: number
}
export type GithubRelease = {
message: string
tag_name: string
assets: Array<{
name: string
browser_download_url: string
}>
}
export type angles = number[] | Int16Array
export type WifiStatus = { export type WifiStatus = {
status: number; status: number
local_ip: string; local_ip: string
mac_address: string; mac_address: string
rssi: number; rssi: number
ssid: string; ssid: string
bssid: string; bssid: string
channel: number; channel: number
subnet_mask: string; subnet_mask: string
gateway_ip: string; gateway_ip: string
dns_ip_1: string; dns_ip_1: string
dns_ip_2?: string; dns_ip_2?: string
}; }
export type WifiSettings = { export type WifiSettings = {
hostname: string; hostname: string
priority_RSSI: boolean; priority_RSSI: boolean
wifi_networks: KnownNetworkItem[]; wifi_networks: KnownNetworkItem[]
}; }
export type NetworkList = {
networks: NetworkItem[]
}
export type KnownNetworkItem = { export type KnownNetworkItem = {
ssid: string; ssid: string
password: string; password: string
static_ip_config: boolean; static_ip_config: boolean
local_ip?: string; local_ip?: string
subnet_mask?: string; subnet_mask?: string
gateway_ip?: string; gateway_ip?: string
dns_ip_1?: string; dns_ip_1?: string
dns_ip_2?: string; dns_ip_2?: string
}; }
export type NetworkItem = { export type NetworkItem = {
rssi: number; rssi: number
ssid: string; ssid: string
bssid: string; bssid: string
channel: number; channel: number
encryption_type: number; encryption_type: number
}; }
export type ApStatus = { export type ApStatus = {
status: number; status: number
ip_address: string; ip_address: string
mac_address: string; mac_address: string
station_num: number; station_num: number
}; }
export type ApSettings = { export type ApSettings = {
provision_mode: number; provision_mode: number
ssid: string; ssid: string
password: string; password: string
channel: number; channel: number
ssid_hidden: boolean; ssid_hidden: boolean
max_clients: number; max_clients: number
local_ip: string; local_ip: string
gateway_ip: string; gateway_ip: string
subnet_mask: string; subnet_mask: string
}; }
export type NTPStatus = {
status: number;
utc_time: string;
local_time: string;
server: string;
uptime: number;
};
export type RSSI = {
rssi: number;
ssid: string;
};
export type Battery = {
voltage: number;
current: boolean;
};
export type DownloadOTA = { export type DownloadOTA = {
status: string; status: string
progress: number; progress: number
error: string; error: string
}; }
export type NTPSettings = {
enabled: boolean;
server: string;
tz_label: string;
tz_format: string;
};
export type Analytics = { export type Analytics = {
max_alloc_heap: number; max_alloc_heap: number
psram_size: number; psram_size: number
free_psram: number; free_psram: number
free_heap: number; free_heap: number
total_heap: number; total_heap: number
min_free_heap: number; min_free_heap: number
core_temp: number; core_temp: number
fs_total: number; fs_total: number
fs_used: number; fs_used: number
uptime: number; uptime: number
cpu0_usage: number; cpu0_usage: number
cpu1_usage: number; cpu1_usage: number
cpu_usage: number; cpu_usage: number
}; }
export type Rssi = { export type Rssi = {
rssi: number; rssi: number
ssid: string; ssid: string
}; }
export type StaticSystemInformation = { export type StaticSystemInformation = {
esp_platform: string; esp_platform: string
firmware_version: string; firmware_version: string
cpu_freq_mhz: number; cpu_freq_mhz: number
cpu_type: string; cpu_type: string
cpu_rev: number; cpu_rev: number
cpu_cores: number; cpu_cores: number
sketch_size: number; sketch_size: number
free_sketch_space: number; free_sketch_space: number
sdk_version: string; sdk_version: string
arduino_version: string; arduino_version: string
flash_chip_size: number; flash_chip_size: number
flash_chip_speed: number; flash_chip_speed: number
cpu_reset_reason: string; cpu_reset_reason: string
}; }
export type SystemInformation = Analytics & StaticSystemInformation; export type SystemInformation = Analytics & StaticSystemInformation
export type IMU = { export type IMU = {
x: number; x: number
y: number; y: number
z: number; z: number
imu_temp: number; heading: number
altitude: number; altitude: number
bmp_temp: number; bmp_temp: number
pressure: number; pressure: number
}; }
export interface I2CDevice { export interface I2CDevice {
address: number; address: number
part_number: string; part_number: string
name: string; name: string
}; }
export type PinConfig = {
pin: number
mode: string
type: string
role: string
}
export type PeripheralsConfiguration = {
sda: number
scl: number
frequency: number
pins: PinConfig[]
}
export type CameraSettings = {
framesize: number
quality: number
brightness: number
contrast: number
saturation: number
sharpness: number
denoise: number
special_effect: number
wb_mode: number
vflip: boolean
hmirror: boolean
}
export type File = number
export interface Directory {
[key: string]: File | Directory
}
export type Servo = {
name: string
channel: number
inverted: boolean
angle: number
center_angle: number
}
export type ServoConfiguration = {
is_active: boolean
servo_pwm_frequency: number
servo_oscillator_frequency: number
servos: Servo[]
}
export interface MDNSServiceQuery {
services: MDNSServiceItem[]
}
export interface MDNSServiceItem {
ip: string
port: number
name: string
}
export interface MDNSService {
service: string
protocol: string
port: number
}
export interface MDNSTxtRecord {
key: string
value: string
}
export interface MDNSStatus {
started: boolean
hostname: string
instance: string
services: MDNSService[]
global_txt_records: MDNSTxtRecord[]
}
+14
View File
@@ -0,0 +1,14 @@
declare module 'uzip' {
interface UZIP {
parse(data: Uint8Array | ArrayBuffer): any;
compress(data: any): Uint8Array | ArrayBuffer;
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer;
decompress(data: Uint8Array | ArrayBuffer): any;
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer;
encode(data: any): Uint8Array | ArrayBuffer;
decode(data: Uint8Array | ArrayBuffer): any;
}
const uzip: UZIP;
export default uzip;
}
@@ -1,4 +1,4 @@
export function daisyColor(name: string, opacity: number = 100) { export const daisyColor = (name: string, opacity: number = 100) => {
const color = getComputedStyle(document.documentElement).getPropertyValue(name); const color = getComputedStyle(document.documentElement).getPropertyValue(name);
return `oklch(${color} / ${opacity}%)`; return `oklch(${color} / ${opacity}%)`;
} };
+3 -1
View File
@@ -4,4 +4,6 @@ export * from './svelte-utilities';
export * from './math-utilities'; export * from './math-utilities';
export * from './buffer-utilities'; export * from './buffer-utilities';
export * from './model-utilities'; export * from './model-utilities';
export * from './location-utilities'; export * from './position-utilities';
export * from './string-utilities';
export * from './color-utilities';
@@ -1,9 +0,0 @@
export const hostname = 'localhost'; //window.location.hostname;
export const isSecure = true; // window.location.protocol === 'https:';
export const location = 'localhost:5173'; //window.location; //import.meta.env.VITE_API_URL.replace('hostname', hostname);
const socketScheme = isSecure ? 'wss://' : 'ws://';
export const socketLocation = socketScheme + location; // import.meta.env.VITE_SOCKET_URL.replace('hostname', hostname);
+85 -56
View File
@@ -1,63 +1,92 @@
import { Color, LoaderUtils, Vector3 } from 'three'; import { Color, LoaderUtils, Vector3 } from 'three'
import URDFLoader, { type URDFRobot } from 'urdf-loader'; import URDFLoader, { type URDFRobot } from 'urdf-loader'
import { XacroLoader } from 'xacro-parser'; import { XacroLoader } from 'xacro-parser'
import { Result } from '$lib/utilities'; import { Result } from '$lib/utilities'
import { jointNames, model } from '$lib/stores'
import uzip from 'uzip'
import { fileService } from '$lib/services'
let model_xml: XMLDocument; let model_xml: XMLDocument
export const loadModelAsync = async ( export const populateModelCache = async () => {
url: string await cacheModelFiles()
): Promise<Result<[URDFRobot, string[]], string>> => { const modelRes = await loadModel('/yertle.URDF')
return new Promise((resolve, reject) => { if (modelRes.isOk()) {
const xacroLoader = new XacroLoader(); const [urdf, JOINT_NAME] = modelRes.inner
const urdfLoader = new URDFLoader(); jointNames.set(JOINT_NAME)
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url); model.set(urdf)
} else {
console.error(modelRes.inner, { exception: modelRes.exception })
}
}
xacroLoader.load( export const cacheModelFiles = async () => {
url, const data = await fetch('/URDF.zip')
async (xml) => {
model_xml = xml;
try {
const model = urdfLoader.parse(xml);
model.rotation.x = -Math.PI / 2;
model.rotation.z = Math.PI / 2;
model.traverse((c) => (c.castShadow = true));
model.updateMatrixWorld(true);
model.scale.setScalar(10);
const joints = Object.entries(model.joints)
.filter((joint) => joint[1].jointType !== 'fixed')
.map((joint) => joint[0]);
resolve(Result.ok([model, joints])); const files = uzip.parse(await data.arrayBuffer())
} catch (error) {
resolve(Result.err('Failed to load model', error));
}
},
(error) => resolve(Result.err('Failed to load model', error))
);
});
};
export const toeWorldPositions = (robot: URDFRobot) => { for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const toe_positions: Vector3[] = []; const url = new URL(path, window.location.href)
robot.traverse((child) => { fileService?.saveFile(url.toString(), data)
if (child.name.includes('toe') && !child.name.includes('_link')) { }
const worldPosition = new Vector3(); }
child.getWorldPosition(worldPosition);
toe_positions.push(worldPosition);
}
});
return toe_positions;
};
export const footColor = () => { export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element; const urdfLoader = new URDFLoader()
const colorAttrStr = colorElem.getAttribute('rgba') as string; urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
const colorStr = colorAttrStr
.split(' ')
.slice(0, 3)
.map((val) => Math.floor(+val * 255))
.join(', ');
return new Color(`rgb(${colorStr})`); let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
};
if (typeof xml === 'string') {
xml = new window.DOMParser().parseFromString(xml, 'text/xml')
}
return new Promise(resolve => {
model_xml = xml
try {
const model = urdfLoader.parse(xml)
setupRobot(model)
const joints = Object.entries(model.joints)
.filter(joint => joint[1].jointType !== 'fixed')
.map(joint => joint[0])
resolve(Result.ok([model, joints]))
} catch (error) {
resolve(Result.err('Failed to load model', error))
}
})
}
const loadXacro = async (url: string): Promise<XMLDocument> =>
new Promise((resolve, reject) => {
new XacroLoader().load(url, resolve, reject)
})
function setupRobot(robot: URDFRobot) {
robot.rotation.x = -Math.PI / 2
robot.rotation.z = Math.PI / 2
robot.scale.setScalar(10)
robot.traverse(c => (c.castShadow = true))
robot.updateMatrixWorld(true)
}
export function getToeWorldPositions(robot: URDFRobot): Vector3[] {
const toes: Vector3[] = []
robot.traverse(c => {
if (c.name.includes('toe') && !c.name.includes('_link'))
toes.push(c.getWorldPosition(new Vector3()))
})
return toes
}
export const extractFootColor = () => {
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element
const colorAttrStr = colorElem.getAttribute('rgba') as string
const colorStr = colorAttrStr
.split(' ')
.slice(0, 3)
.map(val => Math.floor(+val * 255))
.join(', ')
return new Color(`rgb(${colorStr})`)
}
+39 -28
View File
@@ -1,36 +1,47 @@
export const humanFileSize = (size: number): string => { export const humanFileSize = (size: number): string => {
const units = ['B', 'kB', 'MB', 'GB', 'TB']; const units = ['B', 'kB', 'MB', 'GB', 'TB']
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)); const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i]; return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i]
}; }
export const capitalize = (str: string): string => { export const capitalize = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
}; }
export const convertSeconds = (seconds: number) => { export const convertSeconds = (seconds: number) => {
// Calculate the number of seconds, minutes, hours, and days // Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60); let minutes = Math.floor(seconds / 60)
let hours = Math.floor(minutes / 60); let hours = Math.floor(minutes / 60)
let days = Math.floor(hours / 24); const days = Math.floor(hours / 24)
// Calculate the remaining hours, minutes, and seconds // Calculate the remaining hours, minutes, and seconds
hours = hours % 24; hours = hours % 24
minutes = minutes % 60; minutes = minutes % 60
seconds = seconds % 60; seconds = seconds % 60
// Create the formatted string // Create the formatted string
let result = ''; let result = ''
if (days > 0) { if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' '; result += days + ' day' + (days > 1 ? 's' : '') + ' '
} }
if (hours > 0) { if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' '; result += hours + ' hour' + (hours > 1 ? 's' : '') + ' '
} }
if (minutes > 0) { if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' '; result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' '
} }
result += seconds + ' second' + (seconds > 1 ? 's' : ''); result += seconds + ' second' + (seconds > 1 ? 's' : '')
return result; return result
}; }
export const compareIp = (ip1: string, ip2: string) => {
const ip1Parts = ip1.split('.').map(Number)
const ip2Parts = ip2.split('.').map(Number)
for (let i = 0; i < 4; i++) {
if (ip1Parts[i] !== ip2Parts[i]) {
return ip1Parts[i] > ip2Parts[i] ? 1 : -1
}
}
return 0
}
+8 -8
View File
@@ -1,16 +1,16 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
export const isEmbeddedApp = import.meta.env.VITE_EMBEDDED_BUILD === 'true'; export const persistentStore = <T>(key: string, initialValue: T) => {
const savedValue = browser ? localStorage.getItem(key) : null;
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue;
const store = writable<T>();
export const persistentStore = (key: string, initialValue: any) => { store.subscribe(value => {
const savedValue = browser ? JSON.parse(localStorage.getItem(key) as string) : null; if (browser) localStorage.setItem(key, JSON.stringify(value));
const data = savedValue !== null ? savedValue : initialValue;
const store = writable(data);
store.subscribe((value) => {
browser && localStorage.setItem(key, JSON.stringify(value));
}); });
store.set(data);
return store; return store;
}; };
+95 -99
View File
@@ -1,129 +1,125 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { user } from '$lib/stores/user'; import { page } from '$app/state';
import { telemetry } from '$lib/stores/telemetry'; import { Modals, modals } from 'svelte-modals';
import { analytics } from '$lib/stores/analytics'; import Toast from '$lib/components/toasts/Toast.svelte';
import type { userProfile } from '$lib/stores/user'; import { notifications } from '$lib/components/toasts/notifications';
import { page } from '$app/stores'; import { fade } from 'svelte/transition';
import { Modals, closeModal } from 'svelte-modals'; import '../app.css';
import Toast from '$lib/components/toasts/Toast.svelte'; import Menu from '../lib/components/menu/Menu.svelte';
import { notifications } from '$lib/components/toasts/notifications'; import Statusbar from '../lib/components/statusbar/statusbar.svelte';
import { fade } from 'svelte/transition'; import {
import '../app.css'; telemetry,
import Menu from './menu.svelte'; analytics,
import Statusbar from './statusbar.svelte'; ModesEnum,
import Login from './login.svelte'; kinematicData,
import { ModesEnum, kinematicData, mode, outControllerData, servoAngles, servoAnglesOut, socket } from '$lib/stores'; mode,
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models'; outControllerData,
import { api } from '$lib/api'; servoAngles,
servoAnglesOut,
socket,
location,
useFeatureFlags
} from '$lib/stores';
import type { Analytics, DownloadOTA } from '$lib/types/models';
interface Props {
children?: import('svelte').Snippet;
}
onMount(async () => { let { children }: Props = $props();
if ($user.bearer_token !== '') {
await validateUser($user);
}
const ws_token = $page.data.features.security ? '?access_token=' + $user.bearer_token : '';
socket.init(`ws://${window.location.host}/ws/events${ws_token}`);
addEventListeners(); const features = useFeatureFlags();
outControllerData.subscribe((data) => socket.sendEvent("input", {data})); onMount(async () => {
mode.subscribe((data) => socket.sendEvent("mode", {data})); const ws = $location ? $location : window.location.host;
servoAnglesOut.subscribe((data) => socket.sendEvent("angles", {data})); socket.init(`ws://${ws}/api/ws/events`);
kinematicData.subscribe((data) => socket.sendEvent("position", {data}));
}); addEventListeners();
outControllerData.subscribe(data => socket.sendEvent('input', { data }));
mode.subscribe(data => socket.sendEvent('mode', { data }));
servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data }));
kinematicData.subscribe(data => socket.sendEvent('position', { data }));
});
onDestroy(() => { onDestroy(() => {
removeEventListeners(); removeEventListeners();
}); });
const addEventListeners = () => { const addEventListeners = () => {
socket.on('open', handleOpen); socket.on('open', handleOpen);
socket.on('close', handleClose); socket.on('close', handleClose);
socket.on('error', handleError); socket.on('error', handleError);
socket.on('rssi', handleNetworkStatus); socket.on('rssi', handleNetworkStatus);
socket.on('mode', (data:ModesEnum) => mode.set(data)); socket.on('mode', (data: ModesEnum) => mode.set(data));
socket.on('angles', (angles:number[]) => { if (angles.length) servoAngles.set(angles)}); socket.on('analytics', handleAnalytics);
if ($page.data.features.analytics) socket.on('analytics', handleAnalytics); socket.on('angles', (angles: number[]) => {
if ($page.data.features.battery) socket.on('battery', handleBattery); if (angles.length) servoAngles.set(angles);
if ($page.data.features.download_firmware) socket.on('otastatus', handleOAT); });
if ($page.data.features.sonar) socket.on('sonar', data => console.log(data)) features.subscribe(data => {
}; if (data?.download_firmware) socket.on('otastatus', handleOAT);
if (data?.sonar) socket.on('sonar', data => console.log(data));
});
};
const removeEventListeners = () => { const removeEventListeners = () => {
socket.off('analytics', handleAnalytics); socket.off('analytics', handleAnalytics);
socket.off('open', handleOpen); socket.off('open', handleOpen);
socket.off('close', handleClose); socket.off('close', handleClose);
socket.off('rssi', handleNetworkStatus); socket.off('rssi', handleNetworkStatus);
socket.off('battery', handleBattery); socket.off('otastatus', handleOAT);
socket.off('otastatus', handleOAT); };
};
async function validateUser(userdata: userProfile) { const handleOpen = () => {
const result = await api.get('/api/verifyAuthorization') notifications.success('Connection to device established', 5000);
if (result.isErr()){ };
user.invalidate();
console.error('Error:', result.inner);
}
}
const handleOpen = () => { const handleClose = () => {
notifications.success('Connection to device established', 5000); notifications.error('Connection to device lost', 5000);
}; telemetry.setRSSI(0);
};
const handleClose = () => { const handleError = (data: any) => console.error(data);
notifications.error('Connection to device lost', 5000);
telemetry.setRSSI(0);
};
const handleError = (data: any) => console.error(data); const handleAnalytics = (data: Analytics) => analytics.addData(data);
const handleAnalytics = (data: Analytics) => analytics.addData(data); const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data); const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
const handleBattery = (data: Battery) => telemetry.setBattery(data);
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
let menuOpen = false;
let menuOpen = $state(false);
</script> </script>
<svelte:head> <svelte:head>
<title>{$page.data.title}</title> <title>{page.data.title}</title>
</svelte:head> </svelte:head>
{#if $page.data.features.security && $user.bearer_token === ''} <div class="drawer">
<Login /> <input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
{:else} <div class="drawer-content flex flex-col">
<div class="drawer"> <!-- Status bar content here -->
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} /> <Statusbar />
<div class="drawer-content flex flex-col">
<!-- Status bar content here -->
<Statusbar />
<!-- Main page content here --> <!-- Main page content here -->
<slot /> {@render children?.()}
</div> </div>
<!-- Side Navigation --> <!-- Side Navigation -->
<div class="drawer-side z-30 shadow-lg"> <div class="drawer-side z-30 shadow-lg">
<label for="main-menu" class="drawer-overlay" /> <label for="main-menu" class="drawer-overlay"></label>
<Menu <Menu menuClicked={() => (menuOpen = false)} />
on:menuClicked={() => menuOpen = false} </div>
/> </div>
</div>
</div>
{/if}
<Modals> <Modals>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div {#snippet backdrop()}
slot="backdrop" <div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur" class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade transition:fade
on:click={closeModal} onclick={modals.closeAll}
/> ></div>
{/snippet}
</Modals> </Modals>
<Toast /> <Toast />
+2 -20
View File
@@ -1,7 +1,4 @@
import { jointNames, model } from '$lib/stores'; export const prerender = false;
import { loadModelAsync } from '$lib/utilities/model-utilities';
export const prerender = true;
export const ssr = false; export const ssr = false;
const registerFetchIntercept = async () => { const registerFetchIntercept = async () => {
@@ -14,24 +11,9 @@ const registerFetchIntercept = async () => {
}; };
}; };
const loadModelFiles = async () => { export const load = async () => {
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner;
jointNames.set(JOINT_NAME);
model.set(urdf);
} else {
console.error(modelRes.inner, { exception: modelRes.exception });
}
};
export const load = async ({ fetch }) => {
await registerFetchIntercept(); await registerFetchIntercept();
await loadModelFiles();
const result = await fetch('/api/features');
const features = await result.json();
return { return {
features,
title: 'Spot micro controller', title: 'Spot micro controller',
github: 'runeharlyk/SpotMicroESP32-Leika', github: 'runeharlyk/SpotMicroESP32-Leika',
app_name: 'Spot Micro Controller', app_name: 'Spot Micro Controller',
+21 -18
View File
@@ -1,24 +1,27 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import { goto } from '$app/navigation'
import { notifications } from '$lib/components/toasts/notifications'; import Visualization from '$lib/components/Visualization.svelte'
import Visualization from '$lib/components/Visualization.svelte'; import { socket } from '$lib/stores'
import { onMount } from 'svelte'
export let data: PageData; onMount(() => {
socket.subscribe(isConnected => {
if (isConnected) {
goto('/controller')
}
})
})
</script> </script>
<div class="hero bg-base-100 h-screen"> <div class="hero bg-base-100 h-screen">
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center"> <div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
<div class="w-64 h-64"> <div class="w-64 h-64">
<Visualization sky={false} orbit panel={false} ground={false}/> <Visualization sky={false} orbit panel={false} ground={false} />
</div> </div>
<div class="card-body w-80"> <div class="card-body w-80">
<h2 class="card-title text-center text-2xl">Welcome to {data.app_name}</h2> <h2 class="card-title text-center text-2xl">Begin you journey</h2>
<p class="py-6 text-center"></p> <p class="py-6 text-center"></p>
<a <a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}> Add Robot Dog </a>
class="btn btn-primary" </div>
href="/controller" </div>
on:click={() => notifications.success('You did it!', 1000)}>Begin</a
>
</div>
</div>
</div> </div>
+7
View File
@@ -0,0 +1,7 @@
<script lang="ts">
import Connection from './Connection.svelte';
</script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
<Connection />
</div>
@@ -2,6 +2,6 @@ import type { PageLoad } from './$types';
export const load = (async () => { export const load = (async () => {
return { return {
title: 'NTP' title: 'Connection'
}; };
}) satisfies PageLoad; }) satisfies PageLoad;
@@ -0,0 +1,28 @@
<script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte';
import { WiFi } from '$lib/components/icons';
import { location, socket, useFeatureFlags } from '$lib/stores';
const features = useFeatureFlags();
const update = () => {
const ws = $location ? $location : window.location.host;
socket.init(`ws://${ws}/api/ws/events`);
};
</script>
<SettingsCard collapsible={false}>
{#snippet icon()}
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Connection</span>
{/snippet}
<div class="flex">
<label class="label w-32" for="server">Address:</label>
<input class="input" bind:value={$location} />
</div>
<button class="btn btn-primary" onclick={update}>Update</button>
</SettingsCard>
-7
View File
@@ -1,7 +0,0 @@
import type { PageLoad } from './$types';
import { goto } from '$app/navigation';
export const load = (async () => {
goto('/');
return;
}) satisfies PageLoad;
-263
View File
@@ -1,263 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import Collapsible from '$lib/components/Collapsible.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import { notifications } from '$lib/components/toasts/notifications';
import { TIME_ZONES } from './timezones';
import NTP from '~icons/tabler/clock-check';
import Server from '~icons/tabler/server';
import Clock from '~icons/tabler/clock';
import UTC from '~icons/tabler/clock-pin';
import Stopwatch from '~icons/tabler/24-hours';
import type { NTPSettings, NTPStatus } from '$lib/types/models';
import { api } from '$lib/api';
let ntpSettings: NTPSettings;
let ntpStatus: NTPStatus;
async function getNTPStatus() {
const result = await api.get<NTPStatus>('/api/ntpStatus');
if (result.isErr()){
console.error('Error:', result.inner);
return
}
ntpStatus = result.inner
}
async function getNTPSettings() {
const result = await api.get<NTPSettings>('/api/ntpSettings');
if (result.isErr()){
console.error('Error:', result.inner);
return
}
ntpSettings = result.inner
}
const interval = setInterval(async () => {
getNTPStatus();
}, 5000);
onDestroy(() => clearInterval(interval));
onMount(() => {
if (!$page.data.features.security || $user.admin) {
getNTPSettings();
}
});
let formField: any;
let formErrors = {
server: false
};
async function postNTPSettings(data: NTPSettings) {
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
if (result.isErr()){
notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner);
return
}
ntpSettings = result.inner
}
function handleSubmitNTP() {
let valid = true;
// Validate Server
// RegEx for IPv4
const regexExpIPv4 =
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
const regexExpURL =
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
valid = false;
formErrors.server = true;
} else {
formErrors.server = false;
}
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
// Submit JSON to REST API
if (valid) {
postNTPSettings(ntpSettings);
//alert('Form Valid');
}
}
function convertSeconds(seconds: number) {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
}
</script>
<SettingsCard collapsible={false}>
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
<span slot="title">Network Time</span>
<div class="w-full overflow-x-auto">
{#await getNTPStatus()}
<Spinner />
{:then nothing}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
? 'bg-success'
: 'bg-error'}"
>
<NTP
class="h-auto w-full scale-75 {ntpStatus.status === 1
? 'text-success-content'
: 'text-error-content'}"
/>
</div>
<div>
<div class="font-bold">Status</div>
<div class="text-sm opacity-75">
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Server class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">NTP Server</div>
<div class="text-sm opacity-75">
{ntpStatus.server}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Clock class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Local Time</div>
<div class="text-sm opacity-75">
{new Intl.DateTimeFormat('en-GB', {
dateStyle: 'long',
timeStyle: 'long'
}).format(new Date(ntpStatus.local_time))}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<UTC class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">UTC Time</div>
<div class="text-sm opacity-75">
{new Intl.DateTimeFormat('en-GB', {
dateStyle: 'long',
timeStyle: 'long',
timeZone: 'UTC'
}).format(new Date(ntpStatus.utc_time))}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Uptime</div>
<div class="text-sm opacity-75">
{convertSeconds(ntpStatus.uptime)}
</div>
</div>
</div>
</div>
{/await}
</div>
{#if !$page.data.features.security || $user.admin}
<Collapsible open={false} on:closed={getNTPSettings}>
<span slot="title">Change NTP Settings</span>
<form
class="form-control w-full"
on:submit|preventDefault={handleSubmitNTP}
novalidate
bind:this={formField}
>
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
bind:checked={ntpSettings.enabled}
class="checkbox checkbox-primary"
/>
<span class="">Enable NTP</span>
</label>
<label class="label" for="server">
<span class="label-text text-md">Server</span>
</label>
<input
type="text"
min="3"
max="64"
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
? 'border-error border-2'
: ''}"
bind:value={ntpSettings.server}
id="server"
required
/>
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
>Must be a valid IPv4 address or URL</span
>
</label>
<label class="label" for="tz">
<span class="label-text text-md">Pick Time Zone</span>
</label>
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
<option value={tz_label}>{tz_label}</option>
{/each}
</select>
<div class="mt-6 place-self-end">
<button class="btn btn-primary" type="submit">Apply Settings</button>
</div>
</form>
</Collapsible>
{/if}
</SettingsCard>
-466
View File
@@ -1,466 +0,0 @@
export type TimeZones = {
[name: string]: string
};
export const TIME_ZONES: TimeZones = {
"Africa/Abidjan": "GMT0",
"Africa/Accra": "GMT0",
"Africa/Addis_Ababa": "EAT-3",
"Africa/Algiers": "CET-1",
"Africa/Asmara": "EAT-3",
"Africa/Bamako": "GMT0",
"Africa/Bangui": "WAT-1",
"Africa/Banjul": "GMT0",
"Africa/Bissau": "GMT0",
"Africa/Blantyre": "CAT-2",
"Africa/Brazzaville": "WAT-1",
"Africa/Bujumbura": "CAT-2",
"Africa/Cairo": "EET-2",
"Africa/Casablanca": "UNK-1",
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Africa/Conakry": "GMT0",
"Africa/Dakar": "GMT0",
"Africa/Dar_es_Salaam": "EAT-3",
"Africa/Djibouti": "EAT-3",
"Africa/Douala": "WAT-1",
"Africa/El_Aaiun": "UNK-1",
"Africa/Freetown": "GMT0",
"Africa/Gaborone": "CAT-2",
"Africa/Harare": "CAT-2",
"Africa/Johannesburg": "SAST-2",
"Africa/Juba": "EAT-3",
"Africa/Kampala": "EAT-3",
"Africa/Khartoum": "CAT-2",
"Africa/Kigali": "CAT-2",
"Africa/Kinshasa": "WAT-1",
"Africa/Lagos": "WAT-1",
"Africa/Libreville": "WAT-1",
"Africa/Lome": "GMT0",
"Africa/Luanda": "WAT-1",
"Africa/Lubumbashi": "CAT-2",
"Africa/Lusaka": "CAT-2",
"Africa/Malabo": "WAT-1",
"Africa/Maputo": "CAT-2",
"Africa/Maseru": "SAST-2",
"Africa/Mbabane": "SAST-2",
"Africa/Mogadishu": "EAT-3",
"Africa/Monrovia": "GMT0",
"Africa/Nairobi": "EAT-3",
"Africa/Ndjamena": "WAT-1",
"Africa/Niamey": "WAT-1",
"Africa/Nouakchott": "GMT0",
"Africa/Ouagadougou": "GMT0",
"Africa/Porto-Novo": "WAT-1",
"Africa/Sao_Tome": "GMT0",
"Africa/Tripoli": "EET-2",
"Africa/Tunis": "CET-1",
"Africa/Windhoek": "CAT-2",
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla": "AST4",
"America/Antigua": "AST4",
"America/Araguaina": "UNK3",
"America/Argentina/Buenos_Aires": "UNK3",
"America/Argentina/Catamarca": "UNK3",
"America/Argentina/Cordoba": "UNK3",
"America/Argentina/Jujuy": "UNK3",
"America/Argentina/La_Rioja": "UNK3",
"America/Argentina/Mendoza": "UNK3",
"America/Argentina/Rio_Gallegos": "UNK3",
"America/Argentina/Salta": "UNK3",
"America/Argentina/San_Juan": "UNK3",
"America/Argentina/San_Luis": "UNK3",
"America/Argentina/Tucuman": "UNK3",
"America/Argentina/Ushuaia": "UNK3",
"America/Aruba": "AST4",
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
"America/Atikokan": "EST5",
"America/Bahia": "UNK3",
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
"America/Barbados": "AST4",
"America/Belem": "UNK3",
"America/Belize": "CST6",
"America/Blanc-Sablon": "AST4",
"America/Boa_Vista": "UNK4",
"America/Bogota": "UNK5",
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande": "UNK4",
"America/Cancun": "EST5",
"America/Caracas": "UNK4",
"America/Cayenne": "UNK3",
"America/Cayman": "EST5",
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
"America/Costa_Rica": "CST6",
"America/Creston": "MST7",
"America/Cuiaba": "UNK4",
"America/Curacao": "AST4",
"America/Danmarkshavn": "GMT0",
"America/Dawson": "MST7",
"America/Dawson_Creek": "MST7",
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
"America/Dominica": "AST4",
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
"America/Eirunepe": "UNK5",
"America/El_Salvador": "CST6",
"America/Fort_Nelson": "MST7",
"America/Fortaleza": "UNK3",
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
"America/Grenada": "AST4",
"America/Guadeloupe": "AST4",
"America/Guatemala": "CST6",
"America/Guayaquil": "UNK5",
"America/Guyana": "UNK4",
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo": "MST7",
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica": "EST5",
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk": "AST4",
"America/La_Paz": "UNK4",
"America/Lima": "UNK5",
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes": "AST4",
"America/Maceio": "UNK3",
"America/Managua": "CST6",
"America/Manaus": "UNK4",
"America/Marigot": "AST4",
"America/Martinique": "AST4",
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
"America/Montevideo": "UNK3",
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat": "AST4",
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha": "UNK2",
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
"America/Panama": "EST5",
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo": "UNK3",
"America/Phoenix": "MST7",
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain": "AST4",
"America/Porto_Velho": "UNK4",
"America/Puerto_Rico": "AST4",
"America/Punta_Arenas": "UNK3",
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
"America/Recife": "UNK3",
"America/Regina": "CST6",
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco": "UNK5",
"America/Santarem": "UNK3",
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo": "AST4",
"America/Sao_Paulo": "UNK3",
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy": "AST4",
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts": "AST4",
"America/St_Lucia": "AST4",
"America/St_Thomas": "AST4",
"America/St_Vincent": "AST4",
"America/Swift_Current": "CST6",
"America/Tegucigalpa": "CST6",
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
"America/Tortola": "AST4",
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
"America/Whitehorse": "MST7",
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey": "UNK-8",
"Antarctica/Davis": "UNK-7",
"Antarctica/DumontDUrville": "UNK-10",
"Antarctica/Macquarie": "UNK-11",
"Antarctica/Mawson": "UNK-5",
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer": "UNK3",
"Antarctica/Rothera": "UNK3",
"Antarctica/Syowa": "UNK-3",
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok": "UNK-6",
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden": "UNK-3",
"Asia/Almaty": "UNK-6",
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
"Asia/Anadyr": "UNK-12",
"Asia/Aqtau": "UNK-5",
"Asia/Aqtobe": "UNK-5",
"Asia/Ashgabat": "UNK-5",
"Asia/Atyrau": "UNK-5",
"Asia/Baghdad": "UNK-3",
"Asia/Bahrain": "UNK-3",
"Asia/Baku": "UNK-4",
"Asia/Bangkok": "UNK-7",
"Asia/Barnaul": "UNK-7",
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek": "UNK-6",
"Asia/Brunei": "UNK-8",
"Asia/Chita": "UNK-9",
"Asia/Choibalsan": "UNK-8",
"Asia/Colombo": "UNK-5:30",
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
"Asia/Dhaka": "UNK-6",
"Asia/Dili": "UNK-9",
"Asia/Dubai": "UNK-4",
"Asia/Dushanbe": "UNK-5",
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Ho_Chi_Minh": "UNK-7",
"Asia/Hong_Kong": "HKT-8",
"Asia/Hovd": "UNK-7",
"Asia/Irkutsk": "UNK-8",
"Asia/Jakarta": "WIB-7",
"Asia/Jayapura": "WIT-9",
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul": "UNK-4:30",
"Asia/Kamchatka": "UNK-12",
"Asia/Karachi": "PKT-5",
"Asia/Kathmandu": "UNK-5:45",
"Asia/Khandyga": "UNK-9",
"Asia/Kolkata": "IST-5:30",
"Asia/Krasnoyarsk": "UNK-7",
"Asia/Kuala_Lumpur": "UNK-8",
"Asia/Kuching": "UNK-8",
"Asia/Kuwait": "UNK-3",
"Asia/Macau": "CST-8",
"Asia/Magadan": "UNK-11",
"Asia/Makassar": "WITA-8",
"Asia/Manila": "PST-8",
"Asia/Muscat": "UNK-4",
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk": "UNK-7",
"Asia/Novosibirsk": "UNK-7",
"Asia/Omsk": "UNK-6",
"Asia/Oral": "UNK-5",
"Asia/Phnom_Penh": "UNK-7",
"Asia/Pontianak": "WIB-7",
"Asia/Pyongyang": "KST-9",
"Asia/Qatar": "UNK-3",
"Asia/Qyzylorda": "UNK-5",
"Asia/Riyadh": "UNK-3",
"Asia/Sakhalin": "UNK-11",
"Asia/Samarkand": "UNK-5",
"Asia/Seoul": "KST-9",
"Asia/Shanghai": "CST-8",
"Asia/Singapore": "UNK-8",
"Asia/Srednekolymsk": "UNK-11",
"Asia/Taipei": "CST-8",
"Asia/Tashkent": "UNK-5",
"Asia/Tbilisi": "UNK-4",
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
"Asia/Thimphu": "UNK-6",
"Asia/Tokyo": "JST-9",
"Asia/Tomsk": "UNK-7",
"Asia/Ulaanbaatar": "UNK-8",
"Asia/Urumqi": "UNK-6",
"Asia/Ust-Nera": "UNK-10",
"Asia/Vientiane": "UNK-7",
"Asia/Vladivostok": "UNK-10",
"Asia/Yakutsk": "UNK-9",
"Asia/Yangon": "UNK-6:30",
"Asia/Yekaterinburg": "UNK-5",
"Asia/Yerevan": "UNK-4",
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde": "UNK1",
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik": "GMT0",
"Atlantic/South_Georgia": "UNK2",
"Atlantic/St_Helena": "GMT0",
"Atlantic/Stanley": "UNK3",
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane": "AEST-10",
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin": "ACST-9:30",
"Australia/Eucla": "UNK-8:45",
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman": "AEST-10",
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth": "AWST-8",
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Etc/GMT": "GMT0",
"Etc/GMT+0": "GMT0",
"Etc/GMT+1": "UNK1",
"Etc/GMT+10": "UNK10",
"Etc/GMT+11": "UNK11",
"Etc/GMT+12": "UNK12",
"Etc/GMT+2": "UNK2",
"Etc/GMT+3": "UNK3",
"Etc/GMT+4": "UNK4",
"Etc/GMT+5": "UNK5",
"Etc/GMT+6": "UNK6",
"Etc/GMT+7": "UNK7",
"Etc/GMT+8": "UNK8",
"Etc/GMT+9": "UNK9",
"Etc/GMT-0": "GMT0",
"Etc/GMT-1": "UNK-1",
"Etc/GMT-10": "UNK-10",
"Etc/GMT-11": "UNK-11",
"Etc/GMT-12": "UNK-12",
"Etc/GMT-13": "UNK-13",
"Etc/GMT-14": "UNK-14",
"Etc/GMT-2": "UNK-2",
"Etc/GMT-3": "UNK-3",
"Etc/GMT-4": "UNK-4",
"Etc/GMT-5": "UNK-5",
"Etc/GMT-6": "UNK-6",
"Etc/GMT-7": "UNK-7",
"Etc/GMT-8": "UNK-8",
"Etc/GMT-9": "UNK-9",
"Etc/GMT0": "GMT0",
"Etc/Greenwich": "GMT0",
"Etc/UCT": "UTC0",
"Etc/UTC": "UTC0",
"Etc/Universal": "UTC0",
"Etc/Zulu": "UTC0",
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan": "UNK-4",
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul": "UNK-3",
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad": "EET-2",
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov": "UNK-3",
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk": "UNK-3",
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow": "MSK-3",
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara": "UNK-4",
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov": "UNK-4",
"Europe/Simferopol": "MSK-3",
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk": "UNK-4",
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd": "UNK-4",
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo": "EAT-3",
"Indian/Chagos": "UNK-6",
"Indian/Christmas": "UNK-7",
"Indian/Cocos": "UNK-6:30",
"Indian/Comoro": "EAT-3",
"Indian/Kerguelen": "UNK-5",
"Indian/Mahe": "UNK-4",
"Indian/Maldives": "UNK-5",
"Indian/Mauritius": "UNK-4",
"Indian/Mayotte": "EAT-3",
"Indian/Reunion": "UNK-4",
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville": "UNK-11",
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk": "UNK-10",
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
"Pacific/Efate": "UNK-11",
"Pacific/Enderbury": "UNK-13",
"Pacific/Fakaofo": "UNK-13",
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
"Pacific/Funafuti": "UNK-12",
"Pacific/Galapagos": "UNK6",
"Pacific/Gambier": "UNK9",
"Pacific/Guadalcanal": "UNK-11",
"Pacific/Guam": "ChST-10",
"Pacific/Honolulu": "HST10",
"Pacific/Kiritimati": "UNK-14",
"Pacific/Kosrae": "UNK-11",
"Pacific/Kwajalein": "UNK-12",
"Pacific/Majuro": "UNK-12",
"Pacific/Marquesas": "UNK9:30",
"Pacific/Midway": "SST11",
"Pacific/Nauru": "UNK-12",
"Pacific/Niue": "UNK11",
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
"Pacific/Noumea": "UNK-11",
"Pacific/Pago_Pago": "SST11",
"Pacific/Palau": "UNK-9",
"Pacific/Pitcairn": "UNK8",
"Pacific/Pohnpei": "UNK-11",
"Pacific/Port_Moresby": "UNK-10",
"Pacific/Rarotonga": "UNK10",
"Pacific/Saipan": "ChST-10",
"Pacific/Tahiti": "UNK10",
"Pacific/Tarawa": "UNK-12",
"Pacific/Tongatapu": "UNK-13",
"Pacific/Wake": "UNK-12",
"Pacific/Wallis": "UNK-12"
};
-15
View File
@@ -1,15 +0,0 @@
<script lang="ts">
import Controls from './Controls.svelte';
import { socket } from '$lib/stores';
import Spinner from '$lib/components/Spinner.svelte';
</script>
<div class="select-none">
{#if !$socket}
<div class="absolute left-0 flex flex-col w-screen h-screen justify-center items-center backdrop-blur-sm z-10">
<Spinner/>
<h2>Waiting for connection</h2>
</div>
{/if}
<Controls />
<slot/>
</div>
+26 -4
View File
@@ -1,9 +1,31 @@
<script lang="ts"> <script lang="ts">
import Visualization from "$lib/components/Visualization.svelte"; import Controls from './Controls.svelte';
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte';
import { selectedView, views } from '$lib/stores/application';
import { onMount } from 'svelte';
import { mpu, socket } from '$lib/stores';
import { imu } from '$lib/stores/imu';
import type { IMU } from '$lib/types/models';
let layout = $derived($views.find(v => v.name === $selectedView)!);
onMount(() => {
socket.on('imu', (data: IMU) => {
imu.addData(data);
if (data.heading)
mpu.update(mpuData => {
mpuData.heading = data.heading;
console.log(data.heading);
return mpuData;
});
});
});
</script> </script>
<div class="grow flex"> <div class="absolute top-0 select-none w-screen h-screen">
<div class="absolute h-screen w-full top-0"> <Controls />
<Visualization debug /> <div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
<WidgetContainer container={layout.content} />
</div> </div>
</div> </div>
+156 -110
View File
@@ -1,123 +1,169 @@
<script lang="ts"> <script lang="ts">
import nipplejs from 'nipplejs'; import nipplejs from 'nipplejs'
import { onMount } from 'svelte'; import { onMount } from 'svelte'
import { capitalize, throttler, toInt8 } from '$lib/utilities'; import { capitalize, throttler, toInt8 } from '$lib/utilities'
import { input, outControllerData, mode, modes, type Modes, ModesEnum, socket } from '$lib/stores'; import { input, outControllerData, mode, modes, type Modes, ModesEnum } from '$lib/stores'
import type { vector } from '$lib/models'; import type { vector } from '$lib/types/models'
import { VerticalSlider } from '$lib/components/input'
import { gamepadAxes, gamepadButtons, hasGamepad } from '$lib/stores/gamepad'
import { notifications } from '$lib/components/toasts/notifications'
let throttle = new throttler(); let throttle = new throttler()
let left: nipplejs.JoystickManager; let left: nipplejs.JoystickManager
let right: nipplejs.JoystickManager; let right: nipplejs.JoystickManager
let throttle_timing = 40; let throttle_timing = 40
let data = new Array(8); let data = new Array(8)
onMount(() => { $effect(() => {
left = nipplejs.create({ if ($hasGamepad) {
zone: document.getElementById('left') as HTMLElement, notifications.success('🎮 Gamepad connected', 3000)
color: 'grey',
dynamicPage: true,
mode: 'static',
restOpacity: 0.3
});
right = nipplejs.create({
zone: document.getElementById('right') as HTMLElement,
color: 'grey',
dynamicPage: true,
mode: 'static',
restOpacity: 0.3
});
left.on('move', (_, data) => handleJoyMove('left', data.vector));
left.on('end', (_, __) => handleJoyMove('left', { x: 0, y: 0 }));
right.on('move', (_, data) => handleJoyMove('right', data.vector));
right.on('end', (_, __) => handleJoyMove('right', { x: 0, y: 0 }));
});
const handleJoyMove = (key: 'left' | 'right', data: vector) => {
input.update((inputData) => {
inputData[key] = data;
return inputData;
});
throttle.throttle(updateData, throttle_timing);
};
const updateData = () => {
data[0] = 0;
data[1] = toInt8($input.left.x, -1, 1);
data[2] = toInt8($input.left.y, -1, 1);
data[3] = toInt8($input.right.x, -1, 1);
data[4] = toInt8($input.right.y, -1, 1);
data[5] = toInt8($input.height, 0, 100);
data[6] = toInt8($input.speed, 0, 100);
data[7] = toInt8($input.s1, 0, 100);
outControllerData.set(data);
};
const handleKeyup = (event: KeyboardEvent) => {
const down = event.type === 'keydown';
input.update((data) => {
if (event.key === 'w') data.left.y = down ? -1 : 0;
if (event.key === 'a') data.left.x = down ? -1 : 0;
if (event.key === 's') data.left.y = down ? 1 : 0;
if (event.key === 'd') data.left.x = down ? 1 : 0;
return data;
});
throttle.throttle(updateData, throttle_timing);
};
const handleRange = (event:Event, key: 'speed' | 'height' | 's1') => {
const value:number = event.target?.value
input.update((inputData) => {
inputData[key] = value;
return inputData;
});
throttle.throttle(updateData, throttle_timing);
} }
})
const changeMode = (modeValue: Modes) => { $effect(() => {
mode.set(modes.indexOf(modeValue)); handleJoyMove('left', { x: $gamepadAxes[0], y: -$gamepadAxes[1] })
}; handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] })
})
// TODO React to button press
// $effect(() => {
// if ($gamepadButtons.length === 0) return
//
// })
onMount(() => {
left = nipplejs.create({
zone: document.getElementById('left') as HTMLElement,
color: '#15191e80',
dynamicPage: true,
mode: 'static',
restOpacity: 1
})
right = nipplejs.create({
zone: document.getElementById('right') as HTMLElement,
color: '#15191e80',
dynamicPage: true,
mode: 'static',
restOpacity: 1
})
left.on('move', (_, data) => handleJoyMove('left', data.vector))
left.on('end', (_, __) => handleJoyMove('left', { x: 0, y: 0 }))
right.on('move', (_, data) => handleJoyMove('right', data.vector))
right.on('end', (_, __) => handleJoyMove('right', { x: 0, y: 0 }))
})
const handleJoyMove = (key: 'left' | 'right', data: vector) => {
input.update(inputData => {
inputData[key] = data
return inputData
})
throttle.throttle(updateData, throttle_timing)
}
const updateData = () => {
data[0] = 0
data[1] = toInt8($input.left.x, -1, 1)
data[2] = toInt8($input.left.y, -1, 1)
data[3] = toInt8($input.right.x, -1, 1)
data[4] = toInt8($input.right.y, -1, 1)
data[5] = toInt8($input.height, 0, 100)
data[6] = toInt8($input.speed, 0, 100)
data[7] = toInt8($input.s1, 0, 100)
outControllerData.set(data)
}
const handleKeyup = (event: KeyboardEvent) => {
const down = event.type === 'keydown'
input.update(data => {
if (event.key === 'w') data.left.y = down ? 1 : 0
if (event.key === 'a') data.left.x = down ? 1 : 0
if (event.key === 's') data.left.y = down ? -1 : 0
if (event.key === 'd') data.left.x = down ? -1 : 0
return data
})
throttle.throttle(updateData, throttle_timing)
}
const handleRange = (event: Event, key: 'speed' | 'height' | 's1') => {
const value: number = Number((event.target as HTMLInputElement).value)
input.update(inputData => {
inputData[key] = value
return inputData
})
throttle.throttle(updateData, throttle_timing)
}
const changeMode = (modeValue: Modes) => {
mode.set(modes.indexOf(modeValue))
}
</script> </script>
<div class="absolute top-0 left-0 w-screen h-screen"> <div class="absolute top-0 left-0 w-screen h-screen">
<div class="absolute top-0 left-0 h-full w-full flex portrait:hidden"> <div class="absolute top-0 left-0 h-full w-full flex portrait:hidden">
<div id="left" class="flex w-60 items-center justify-end" /> <div id="left" class="flex w-60 items-center justify-end"></div>
<div class="flex-1" /> <div class="flex-1"></div>
<div id="right" class="flex w-60 items-center" /> <div id="right" class="flex w-60 items-center"></div>
</div> </div>
<div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex"> <div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex">
<div class="flex justify-center w-full"> <div class="flex justify-center w-full">
<kbd class="kbd">W</kbd> <kbd class="kbd">W</kbd>
</div>
<div class="flex justify-center gap-2 w-full">
<kbd class="kbd">A</kbd>
<kbd class="kbd">S</kbd>
<kbd class="kbd">D</kbd>
</div>
<div class="flex justify-center w-full">
</div>
</div> </div>
<div class="absolute bottom-0 z-10 p-4 gap-4 flex items-end"> <div class="flex justify-center gap-2 w-full">
{#each modes as modeValue} <kbd class="kbd">A</kbd>
<button class="btn btn-outline" class:btn-active={$mode === modes.indexOf(modeValue)} on:click={() => changeMode(modeValue)}> <kbd class="kbd">S</kbd>
{capitalize(modeValue)} <kbd class="kbd">D</kbd>
</button> </div>
{/each} <div class="flex justify-center w-full"></div>
<div> </div>
{#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl} <div class="absolute bottom-0 z-10 flex items-end">
<label for="s1">S1</label> <div class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl">
<input type="range" name="s1" min="0" max="100" on:input={(e) => handleRange(e, 's1')} class="range range-sm" /> <VerticalSlider min={0} max={100} oninput={(e: Event) => handleRange(e, 'height')} />
<label for="speed">Speed</label> <label for="height">Ht</label>
<input type="range" name="speed" min="0" max="100" on:input={(e) => handleRange(e, 'speed')} class="range range-sm" /> </div>
{/if} <div
<label for="height">Height</label> class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden">
<input type="range" name="height" min="0" max="100" on:input={(e) => handleRange(e, 'height')} class="range range-sm" /> <div class="join">
{#each modes as modeValue}
<button
class="btn join-item"
class:btn-primary={$mode === modes.indexOf(modeValue)}
onclick={() => changeMode(modeValue)}>
{capitalize(modeValue)}
</button>
{/each}
</div>
{#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl}
<div class="flex gap-4">
<div>
<label for="s1">S1</label>
<input
type="range"
name="s1"
min="0"
max="25"
oninput={e => handleRange(e, 's1')}
class="range range-sm range-primary" />
</div>
<div>
<label for="speed">Speed</label>
<input
type="range"
name="speed"
min="0"
max="25"
oninput={e => handleRange(e, 'speed')}
class="range range-sm range-primary" />
</div>
</div> </div>
</div> {/if}
</div>
</div>
</div> </div>
<svelte:window on:keyup={handleKeyup} on:keydown={handleKeyup} /> <svelte:window onkeyup={handleKeyup} onkeydown={handleKeyup} />
@@ -1,19 +0,0 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { location } from '$lib/utilities';
let videoStream = `//${location}/api/stream`;
onDestroy(() => {
videoStream = '#';
});
</script>
<div class="w-full h-full">
<img
src={videoStream}
class="absolute object-cover blur-3xl w-full h-full -z-10"
alt="Live stream is down"
/>
<img src={videoStream} class="object-contain w-full h-full" alt="Live stream is down" />
</div>
-111
View File
@@ -1,111 +0,0 @@
<script lang="ts">
import logo from '$lib/assets/logo512.png';
import InputPassword from '$lib/components/InputPassword.svelte';
import { user } from '$lib/stores/user';
import { notifications } from '$lib/components/toasts/notifications';
import { fade, fly } from 'svelte/transition';
import Login from '~icons/tabler/login';
import { api } from '$lib/api';
import type { JWT } from '$lib/models';
type SignInData = {
password: string;
username: string;
};
let username = '';
let password = '';
let loginFailed = false;
let token = { access_token: '' };
async function signInUser(data: SignInData) {
const result = await api.post<JWT>('/api/signIn', data)
if (result.isErr()){
username = '';
password = '';
notifications.error('Wrong Username or Password!', 5000);
loginFailed = true;
setTimeout(() => {
loginFailed = false;
}, 1500);
return
}
token = result.inner;
user.init(token.access_token);
username = $user.username;
notifications.success('User ' + username + ' signed in', 5000);
}
</script>
<div class="hero from-primary/30 to-secondary/30 min-h-screen bg-gradient-to-br">
<div
class="card lg:card-side bg-base-100 face shadow-2xl {loginFailed
? 'failure border-error border-2'
: ''}"
in:fly={{ delay: 200, y: 100, duration: 500 }}
out:fade={{ duration: 200 }}
>
<figure class="bg-base-200"><img src={logo} alt="Logo" class="h-auto w-48 lg:w-64" /></figure>
<div class="card-body w-80">
<h2 class="card-title text-2xl">Login</h2>
<form class="form-control w-full max-w-xs">
<label class="label" for="user">
<span class="label-text text-md">Username</span>
</label>
<input
type="text"
class="input input-bordered w-full max-w-xs"
id="user"
bind:value={username}
/>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<InputPassword id="pwd" bind:value={password} />
<div class="card-actions mt-4 justify-end">
<button
class="btn btn-primary inline-flex items-center"
on:click={() => {
signInUser({ username, password });
}}><Login class="mr-2 h-5 w-5" /><span>Login</span></button
>
</div>
</form>
</div>
</div>
</div>
<style>
.failure {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translatex(-1px);
}
20%,
80% {
transform: translatex(2px);
}
30%,
50%,
70% {
transform: translatex(-4px);
}
40%,
60% {
transform: translatex(4px);
}
}
</style>
-276
View File
@@ -1,276 +0,0 @@
<script lang="ts">
import logo from '$lib/assets/logo512.png';
import MdiGithub from '~icons/mdi/github';
import MdiConnection from '~icons/mdi/connection';
import Users from '~icons/mdi/users';
import Settings from '~icons/mdi/settings';
import MdiController from '~icons/mdi/controller';
import Devices from '~icons/mdi/devices'
import Camera from '~icons/mdi/camera-outline';
import Rotate3d from '~icons/mdi/rotate-3d';
import MotorOutline from '~icons/mdi/motor-outline';
import Health from '~icons/mdi/stethoscope';
import Folder from '~icons/mdi/folder-outline';
import Update from '~icons/mdi/reload';
import WiFi from '~icons/mdi/wifi';
import Router from '~icons/mdi/router';
import AP from '~icons/mdi/access-point';
import Remote from '~icons/mdi/network';
import Avatar from '~icons/mdi/user-circle';
import Logout from '~icons/mdi/logout';
import Copyright from '~icons/mdi/copyright';
import NTP from '~icons/mdi/clock-check';
import Metrics from '~icons/mdi/report-bar';
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { createEventDispatcher } from 'svelte';
const appName = $page.data.app_name;
const copyright = $page.data.copyright;
const github = { href: 'https://github.com/' + $page.data.github, active: true };
type menuItem = {
title: string;
icon: ConstructorOfATypedSvelteComponent;
href?: string;
feature: boolean;
active?: boolean;
submenu?: subMenuItem[];
};
type subMenuItem = {
title: string;
icon: ConstructorOfATypedSvelteComponent;
href: string;
feature: boolean;
active: boolean;
};
let menuItems = [
{
title: 'Controller',
icon: MdiController,
href: '/controller',
feature: true,
},
{
title: 'Peripherals',
icon: Devices,
feature: true,
submenu: [
{
title: 'I2C',
icon: MdiConnection,
href: '/peripherals/i2c',
feature: true,
},
{
title: 'Camera',
icon: Camera,
href: '/peripherals/camera',
feature: $page.data.features.camera,
},
{
title: 'Servo',
icon: MotorOutline,
href: '/peripherals/servo',
feature: true,
},
{
title: 'IMU',
icon: Rotate3d,
href: '/peripherals/imu',
feature: $page.data.features.imu || $page.data.features.mag || $page.data.features.bmp,
}
]
},
{
title: 'Connections',
icon: Remote,
feature: $page.data.features.ntp,
submenu: [
{
title: 'NTP',
icon: NTP,
href: '/connections/ntp',
feature: $page.data.features.ntp,
}
]
},
{
title: 'WiFi',
icon: WiFi,
feature: true,
submenu: [
{
title: 'WiFi Station',
icon: Router,
href: '/wifi/sta',
feature: true,
},
{
title: 'Access Point',
icon: AP,
href: '/wifi/ap',
feature: true,
}
]
},
{
title: 'Users',
icon: Users,
href: '/user',
feature: $page.data.features.security && $user.admin,
},
{
title: 'System',
icon: Settings,
feature: true,
submenu: [
{
title: 'System Status',
icon: Health,
href: '/system/status',
feature: true,
},
{
title: 'File System',
icon: Folder,
href: '/system/filesystem',
feature: true,
},
{
title: 'System Metrics',
icon: Metrics,
href: '/system/metrics',
feature: $page.data.features.analytics,
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
feature:
($page.data.features.ota ||
$page.data.features.upload_firmware ||
$page.data.features.download_firmware) &&
(!$page.data.features.security || $user.admin),
}
]
}
] as menuItem[];
const dispatch = createEventDispatcher();
function setActiveMenuItem(targetTitle: string) {
menuItems.forEach(item => {
item.active = item.title === targetTitle;
item.submenu?.forEach(subItem => {
subItem.active = subItem.title === targetTitle;
});
});
menuItems = menuItems
dispatch('menuClicked');
}
$: setActiveMenuItem($page.data.title);
</script>
<div class="bg-base-200 text-base-content flex h-full w-80 flex-col p-4">
<!-- Sidebar content here -->
<a
href="/"
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
on:click={() => setActiveMenuItem('')}
>
<img src={logo} alt="Logo" class="h-12 w-12" />
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
</a>
<ul class="menu rounded-box menu-vertical flex-nowrap overflow-y-auto">
{#each menuItems as menuItem, i (menuItem.title)}
{#if menuItem.feature}
<li>
{#if menuItem.submenu}
<details>
<summary class="text-lg font-bold">
<svelte:component this={menuItem.icon} class="h-6 w-6" />
{menuItem.title}
</summary>
<ul>
{#each menuItem.submenu as subMenuItem}
{#if subMenuItem.feature}
<li class="hover-bordered">
<a
href={subMenuItem.href}
class:bg-base-100={subMenuItem.active}
class="text-ml font-bold"
on:click={() => {
setActiveMenuItem(subMenuItem.title);
menuItems = menuItems;
}}
><svelte:component
this={subMenuItem.icon}
class="h-5 w-5"
/>{subMenuItem.title}</a
>
</li>
{/if}
{/each}
</ul>
</details>
{:else}
<a
href={menuItem.href}
class:bg-base-100={menuItem.active}
class="text-lg font-bold"
on:click={() => {
setActiveMenuItem(menuItem.title);
menuItems = menuItems;
}}><svelte:component this={menuItem.icon} class="h-6 w-6" />{menuItem.title}</a
>
{/if}
</li>
{/if}
{/each}
</ul>
<div class="flex-col" />
<div class="flex-grow" />
{#if $page.data.features.security}
<div class="flex items-center">
<Avatar class="h-8 w-8" />
<span class="flex-grow px-4 text-xl font-bold">{$user.username}</span>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="btn btn-ghost"
on:click={() => {
user.invalidate();
}}
>
<Logout class="h-8 w-8 rotate-180" />
</div>
</div>
{/if}
<div class="divider my-0" />
<div class="flex items-center">
{#if github.active}
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer"
><MdiGithub class="h-5 w-5" /></a
>
{/if}
<div class="inline-flex flex-grow items-center justify-end text-sm">
<Copyright class="h-4 w-4" /><span class="px-2">{copyright}</span>
</div>
</div>
</div>
@@ -1,14 +1,17 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from "$lib/components/SettingsCard.svelte"; import SettingsCard from "$lib/components/SettingsCard.svelte";
import Camera from '~icons/mdi/camera-outline'
import Record from '~icons/mdi/radio-button-unchecked'
import CameraSetting from './CameraSetting.svelte'; import CameraSetting from './CameraSetting.svelte';
import Stream from '$lib/components/Stream.svelte'; import Stream from '$lib/components/Stream.svelte';
import { Camera } from "$lib/components/icons";
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Camera slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">Camera</span> <Camera class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Camera</span>
{/snippet}
<Stream /> <Stream />
<CameraSetting /> <CameraSetting />
</SettingsCard> </SettingsCard>
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api'; import { api } from '$lib/api';
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte';
import type { CameraSettings } from '$lib/models'; import type { CameraSettings } from '$lib/types/models';
let settings:CameraSettings let settings:CameraSettings = $state()
const getCameraSettings = async () => { const getCameraSettings = async () => {
const result = await api.get<CameraSettings>('/api/camera/settings') const result = await api.get<CameraSettings>('/api/camera/settings')
@@ -27,7 +27,7 @@
<Spinner /> <Spinner />
{:then _} {:then _}
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<button class="btn btn-primary" type="button" on:click={updateCameraSettings}>Update camera settings</button> <button class="btn btn-primary" type="button" onclick={updateCameraSettings}>Update camera settings</button>
<label for="brightness"> <label for="brightness">
Brightness {settings.brightness} Brightness {settings.brightness}
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import I2C from './i2c.svelte'; import I2C from './i2c.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
<I2C /> <I2C />
</div> </div>
+5 -5
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'I2C' title: 'I2C'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
+67 -30
View File
@@ -1,41 +1,78 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from "$lib/components/SettingsCard.svelte"; import SettingsCard from '$lib/components/SettingsCard.svelte'
import MdiConnection from '~icons/mdi/connection'; import { onMount } from 'svelte'
import { onDestroy, onMount } from "svelte"; import { socket } from '$lib/stores'
import { socket } from "$lib/stores"; import type { I2CDevice } from '$lib/types/models'
import type { I2CDevice } from "$lib/types/models"; import { Connection } from '$lib/components/icons'
import I2CSetting from './i2cSetting.svelte'
const i2cDevices = [ const i2cDevices = [
{address:30, part_number: "HMC5883", name: "3-Axis Digital Compass/Magnetometer IC"}, { address: 30, part_number: 'HMC5883', name: '3-Axis Digital Compass/Magnetometer IC' },
{address:64, part_number: "PCA9685", name: "16-channel PWM driver default address"}, { address: 41, part_number: 'BNO055', name: '9-Axis Absolute Orientation Sensor' },
{address:72, part_number: "ADS1115", name: "4-channel 16-bit ADC"}, { address: 64, part_number: 'PCA9685', name: '16-channel PWM driver default address' },
{address:104, part_number: "MPU6050", name: "Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices"}, { address: 72, part_number: 'ADS1115', name: '4-channel 16-bit ADC' },
{address:119, part_number: "BMP085", name: "Temp/Barometric"}, {
]; address: 104,
part_number: 'MPU6050',
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
},
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
]
let active_devices:I2CDevice[] = []; let active_devices: I2CDevice[] = $state([])
onMount(() => { let isLoading = $state(false)
socket.on('i2cScan', handleScan);
socket.sendEvent('i2cScan', "");
})
onDestroy(() => { onMount(() => {
socket.off('i2cScan', handleScan); socket.on('i2cScan', handleScan)
}) triggerScan()
return () => socket.off('i2cScan', handleScan)
})
const handleScan = (data: any) => { const handleScan = (data: any) => {
active_devices = data.addresses.map((address:number) => i2cDevices.find(device => device.address === address)) active_devices = data.addresses.map(
} (address: number) =>
i2cDevices.find(device => device.address === address) || {
address,
part_number: 'Unknown',
name: 'Unknown'
}
)
isLoading = false
}
const triggerScan = () => {
isLoading = true
socket.sendEvent('i2cScan', '')
}
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<MdiConnection slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">I<sup>2</sup>C</span> <Connection class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span>I<sup>2</sup>C</span>
{/snippet}
{#snippet right()}
<button class="btn btn-primary" onclick={triggerScan} disabled={isLoading}>
{#if isLoading}
<span class="loading loading-ring loading-xs"></span>
{:else}
Scan
{/if}
</button>
{/snippet}
<div class="grid"> <I2CSetting />
{#each active_devices as device }
<div>[{device.address.toString(16)}] {device.part_number} - {device.name}</div> <div class="grid">
{/each} {#if active_devices.length === 0}
</div> <div>No I2C devices found</div>
{:else}
{#each active_devices as device}
<div>[{device.address.toString(16)}] {device.part_number} - {device.name}</div>
{/each}
{/if}
</div>
</SettingsCard> </SettingsCard>
@@ -0,0 +1,99 @@
<script lang="ts">
import { Cancel, Edit, EditOff, Power } from '$lib/components/icons'
import { socket } from '$lib/stores'
import type { PeripheralsConfiguration } from '$lib/types/models'
import { onMount } from 'svelte'
import { modals } from 'svelte-modals'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
let settings: PeripheralsConfiguration | null = $state(null)
let isEditing = $state(false)
onMount(() => {
socket.on('peripheralSettings', handleSettings)
socket.sendEvent('peripheralSettings', '')
return () => socket.off('peripheralSettings', handleSettings)
})
const handleSettings = (data: any) => {
settings = data
}
const handleSave = () => {
modals.open(ConfirmDialog, {
title: 'Confirm configuration',
message:
'Are you sure you want to save this configuration? The operation cannot be undone. Please make sure you have the correct settings.',
labels: {
cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'Confirm', icon: Power }
},
onConfirm: () => {
modals.close()
socket.sendEvent('peripheralSettings', settings)
}
})
}
const Icon = $derived(isEditing ? EditOff : Edit)
</script>
{#if settings}
<div class="collapse bg-base-100 border-base-300 border">
<input type="checkbox" />
<div class="collapse-title font-semibold">Configuration</div>
<div class="collapse-content text-sm">
<div class="flex flex-col gap-2">
<label for="sda" class="input validator">
SDA
<input
id="sda"
type="number"
required
placeholder="Type a number between 1 to 48"
min="0"
max="48"
title="SDA pin number (0-48)"
disabled={!isEditing}
bind:value={settings.sda} />
</label>
<label for="scl" class="input validator">
SCL
<input
id="scl"
type="number"
required
placeholder="Type a number between 1 to 48"
min="1"
max="48"
title="SCL pin number (0-48)"
disabled={!isEditing}
bind:value={settings.scl} />
</label>
<label class="input validator" for="frequency">
Frequency
<input
id="frequency"
type="number"
required
placeholder="Type a number between 100000 to 430000"
min="100000"
max="430000"
title="I2C frequency in Hz"
disabled={!isEditing}
bind:value={settings.frequency} />
</label>
<div>
<button class="btn btn-outline btn-primary" onclick={() => (isEditing = !isEditing)}>
<Icon class="h-6 w-6" />
</button>
{#if isEditing}
<button class="btn btn-outline btn-primary" onclick={handleSave}>Save</button>
{/if}
</div>
</div>
</div>
</div>
{/if}
+231 -282
View File
@@ -1,304 +1,253 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from "$lib/components/SettingsCard.svelte"; import SettingsCard from '$lib/components/SettingsCard.svelte'
import Rotate3d from '~icons/mdi/rotate-3d'; import { imu } from '$lib/stores/imu'
import { imu } from '$lib/stores/imu'; import { Chart, registerables } from 'chart.js'
import { Chart, registerables } from 'chart.js'; import { cubicOut } from 'svelte/easing'
import { cubicOut } from "svelte/easing"; import { slide } from 'svelte/transition'
import { slide } from "svelte/transition"; import { onDestroy, onMount } from 'svelte'
import { onDestroy, onMount } from "svelte"; import { socket } from '$lib/stores'
import { daisyColor } from "$lib/DaisyUiHelper"; import type { IMU } from '$lib/types/models'
import { socket } from "$lib/stores"; import { useFeatureFlags } from '$lib/stores/featureFlags'
import type { IMU } from "$lib/types/models"; import { Rotate3d } from '$lib/components/icons'
import { page } from "$app/stores";
Chart.register(...registerables); Chart.register(...registerables)
let angleChartElement: HTMLCanvasElement; const features = useFeatureFlags()
let angleChart: Chart; let intervalId: number
let tempChartElement: HTMLCanvasElement; let angleChartElement: HTMLCanvasElement = $state()
let tempChart: Chart; let tempChartElement: HTMLCanvasElement = $state()
let altitudeChartElement: HTMLCanvasElement = $state()
let altitudeChartElement: HTMLCanvasElement; let angleChart: Chart
let altitudeChart: Chart; let tempChart: Chart
let altitudeChart: Chart
const handleImu = (data: IMU) => { const getChartColors = () => {
console.log(data); const style = getComputedStyle(document.body)
return {
imu.addData(data); primary: style.getPropertyValue('--color-primary'),
secondary: style.getPropertyValue('--color-secondary'),
accent: style.getPropertyValue('--color-accent'),
background: style.getPropertyValue('--color-background')
} }
}
onMount(() => { const createBaseChartConfig = (bgColor: string) => ({
socket.on('imu', handleImu); maintainAspectRatio: false,
angleChart = new Chart(angleChartElement, { responsive: true,
type: 'line', plugins: {
data: { legend: { display: true },
datasets: [ tooltip: { mode: 'index', intersect: false }
{ },
label: 'x', elements: { point: { radius: 1 } },
borderColor: daisyColor('--p'), scales: {
backgroundColor: daisyColor('--p', 50), x: {
borderWidth: 2, grid: { color: bgColor },
data: $imu.x, ticks: { color: bgColor },
yAxisID: 'y' display: false
}, },
{ y: {
label: 'y', type: 'linear',
borderColor: daisyColor('--s'), position: 'left',
backgroundColor: daisyColor('--s', 50), min: 0,
borderWidth: 2, max: 10,
data: $imu.y, grid: { color: bgColor },
yAxisID: 'y' ticks: { color: bgColor },
}, border: { color: bgColor }
{ }
label: 'z', }
borderColor: daisyColor('--a'), })
backgroundColor: daisyColor('--a', 50),
borderWidth: 2, const initializeCharts = () => {
data: $imu.z, const colors = getChartColors()
yAxisID: 'y' const baseConfig = createBaseChartConfig(colors.background)
}
] angleChart = new Chart(angleChartElement, {
}, type: 'line',
options: { data: {
maintainAspectRatio: false, datasets: [
responsive: true, {
plugins: { label: 'x',
legend: { borderColor: colors.primary,
display: true backgroundColor: colors.primary,
}, borderWidth: 2,
tooltip: { data: $imu.x,
mode: 'index', yAxisID: 'y'
intersect: false },
} {
}, label: 'y',
elements: { borderColor: colors.secondary,
point: { backgroundColor: colors.secondary,
radius: 1 borderWidth: 2,
} data: $imu.y,
}, yAxisID: 'y'
scales: { },
x: { {
grid: { label: 'z',
color: daisyColor('--bc', 10) borderColor: colors.accent,
}, backgroundColor: colors.accent,
ticks: { borderWidth: 2,
color: daisyColor('--bc') data: $imu.z,
}, yAxisID: 'y'
display: false }
}, ]
y: { },
type: 'linear', options: {
title: { ...baseConfig,
display: true, scales: {
text: 'Angle [°]', ...baseConfig.scales,
color: daisyColor('--bc'), y: {
font: { ...baseConfig.scales.y,
size: 16, title: {
weight: 'bold' display: true,
} text: 'Angle [°]',
}, color: colors.background,
position: 'left', font: { size: 16, weight: 'bold' }
min: 0, }
max: 10, }
grid: { color: daisyColor('--bc', 10) }, }
ticks: { color: daisyColor('--bc') }, }
border: { color: daisyColor('--bc', 10) }
}
}
}
});
tempChart = new Chart(tempChartElement, {
type: 'line',
data: {
datasets: [
{
label: 'Barometer temperature',
borderColor: daisyColor('--s'),
backgroundColor: daisyColor('--s', 50),
borderWidth: 2,
data: $imu.bmp_temp,
yAxisID: 'y'
}
]
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
},
elements: {
point: {
radius: 1
}
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
},
ticks: {
color: daisyColor('--bc')
},
display: false
},
y: {
type: 'linear',
title: {
display: true,
text: 'Temperature [C°]',
color: daisyColor('--bc'),
font: {
size: 16,
weight: 'bold'
}
},
position: 'left',
min: 0,
max: 10,
grid: { color: daisyColor('--bc', 10) },
ticks: { color: daisyColor('--bc') },
border: { color: daisyColor('--bc', 10) }
}
}
}
});
altitudeChart = new Chart(altitudeChartElement, {
type: 'line',
data: {
datasets: [
{
label: 'Altitude',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
data: $imu.altitude,
yAxisID: 'y'
}
]
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
},
elements: {
point: {
radius: 1
}
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
},
ticks: {
color: daisyColor('--bc')
},
display: false
},
y: {
type: 'linear',
title: {
display: true,
text: 'Altitude [M]',
color: daisyColor('--bc'),
font: {
size: 16,
weight: 'bold'
}
},
position: 'left',
min: 0,
max: 10,
grid: { color: daisyColor('--bc', 10) },
ticks: { color: daisyColor('--bc') },
border: { color: daisyColor('--bc', 10) }
}
}
}
});
setInterval(() => {
updateData(), 200;
});
}) })
onDestroy(() => { tempChart = new Chart(tempChartElement, {
socket.off('imu', handleImu); type: 'line',
data: {
datasets: [
{
label: 'Barometer temperature',
borderColor: colors.secondary,
backgroundColor: colors.secondary,
borderWidth: 2,
data: $imu.bmp_temp,
yAxisID: 'y'
}
]
},
options: {
...baseConfig,
scales: {
...baseConfig.scales,
y: {
...baseConfig.scales.y,
title: {
display: true,
text: 'Temperature [C°]',
color: colors.background,
font: { size: 16, weight: 'bold' }
}
}
}
}
}) })
const updateData = () => { altitudeChart = new Chart(altitudeChartElement, {
if ($page.data.features.imu) { type: 'line',
angleChart.data.labels = $imu.x; data: {
angleChart.data.datasets[0].data = $imu.x; datasets: [
angleChart.data.datasets[1].data = $imu.y; {
angleChart.data.datasets[2].data = $imu.z; label: 'Altitude',
angleChart.options.scales!.y!.min = Math.min(Math.min(...$imu.x), Math.min(...$imu.y), Math.min(...$imu.z)) - 1; borderColor: colors.primary,
angleChart.options.scales!.y!.max = Math.max(Math.max(...$imu.x), Math.max(...$imu.y), Math.max(...$imu.z)) + 1; backgroundColor: colors.primary,
angleChart.update('none'); borderWidth: 2,
data: $imu.altitude,
yAxisID: 'y'
}
]
},
options: {
...baseConfig,
scales: {
...baseConfig.scales,
y: {
...baseConfig.scales.y,
title: {
display: true,
text: 'Altitude [M]',
color: colors.background,
font: { size: 16, weight: 'bold' }
}
}
} }
}
})
}
if ($page.data.features.bmp) { const updateChartData = (chart: Chart, data: number[], label: string) => {
tempChart.data.labels = $imu.bmp_temp; chart.data.labels = data
tempChart.data.datasets[0].data = $imu.bmp_temp; chart.data.datasets[0].data = data
tempChart.options.scales!.y!.min = Math.min(...$imu.bmp_temp) - 1; chart.options.scales!.y!.min = Math.min(...data) - 1
tempChart.options.scales!.y!.max = Math.max(...$imu.bmp_temp) + 1; chart.options.scales!.y!.max = Math.max(...data) + 1
tempChart.update('none'); chart.update('none')
}
altitudeChart.data.labels = $imu.altitude; const updateData = () => {
altitudeChart.data.datasets[0].data = $imu.altitude; if ($features.imu) {
altitudeChart.options.scales!.y!.min = Math.min(Math.min(...$imu.altitude)) - 1; angleChart.data.labels = $imu.x
altitudeChart.options.scales!.y!.max = Math.max(Math.max(...$imu.altitude)) + 1; angleChart.data.datasets[0].data = $imu.x
altitudeChart.update('none'); angleChart.data.datasets[1].data = $imu.y
} angleChart.data.datasets[2].data = $imu.z
const allValues = [...$imu.x, ...$imu.y, ...$imu.z]
angleChart.options.scales!.y!.min = Math.min(...allValues) - 1
angleChart.options.scales!.y!.max = Math.max(...allValues) + 1
angleChart.update('none')
} }
if ($features.bmp) {
updateChartData(tempChart, $imu.bmp_temp, 'Temperature')
updateChartData(altitudeChart, $imu.altitude, 'Altitude')
}
}
onMount(() => {
socket.on('imu', (data: IMU) => {
console.log(data)
imu.addData(data)
})
initializeCharts()
intervalId = setInterval(updateData, 200)
})
onDestroy(() => {
socket.off('imu')
clearInterval(intervalId)
})
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Rotate3d slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">IMU</span> <Rotate3d class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{#if $page.data.features.imu} {/snippet}
<div class="w-full overflow-x-auto"> {#snippet title()}
<div <span>IMU</span>
class="flex w-full flex-col space-y-1 h-60" {/snippet}
transition:slide|local={{ duration: 300, easing: cubicOut }}
> {#if $features.imu}
<canvas bind:this={angleChartElement} /> <div class="w-full overflow-x-auto">
</div> <div
</div> class="flex w-full flex-col space-y-1 h-60"
{/if} transition:slide|local={{ duration: 300, easing: cubicOut }}>
{#if $page.data.features.bmp} <canvas bind:this={angleChartElement}></canvas>
<div class="w-full overflow-x-auto"> </div>
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={tempChartElement} />
</div>
</div>
<div class="w-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={altitudeChartElement} />
</div>
</div> </div>
{/if} {/if}
<!-- <IMUSetting /> -->
{#if $features.bmp}
<div class="w-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={tempChartElement}></canvas>
</div>
</div>
<div class="w-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={altitudeChartElement}></canvas>
</div>
</div>
{/if}
</SettingsCard> </SettingsCard>
@@ -1,7 +1,12 @@
<script lang="ts"> <script lang="ts">
import Servos from './servos.svelte'; import Servos from './servos.svelte'
import ServoTable from './ServoTable.svelte'
let servoId = $state(0)
let pwm = $state(306)
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
<Servos /> <Servos bind:servoId bind:pwm />
<ServoTable {servoId} {pwm} />
</div> </div>
@@ -0,0 +1,113 @@
<script lang="ts">
import { api } from '$lib/api'
import { onMount } from 'svelte'
import { RotateCw, RotateCcw } from '$lib/components/icons'
interface Props {
data?: any
servoId?: number
pwm?: number
}
let {
data = $bindable({
servos: []
}),
pwm = $bindable(306),
servoId = $bindable(0)
}: Props = $props()
const updateValue = (event: Event, index: number, key: string) => {
data.servos[index][key] = Number((event.target as HTMLInputElement).value)
}
const syncConfig = async () => {
await api.post('/api/servo/config', data)
}
const toggleDirection = async (index: number) => {
data.servos[index].direction = data.servos[index].direction === 1 ? -1 : 1
await syncConfig()
}
onMount(async () => {
const result = await api.get('/api/servo/config')
if (result.isOk()) {
data = result.inner
}
})
const setCenterPWM = async () => {
console.log('setCenterPWM', servoId, pwm)
data.servos[servoId]['center_pwm'] = pwm
await syncConfig()
}
</script>
<div>
<button class="btn btn-sm btn-primary" onclick={() => setCenterPWM()}>Set center pwm</button>
</div>
<div class="overflow-x-auto">
<table class="table table-xs">
<thead>
<tr>
<th>Servo</th>
<th>Center PWM</th>
<th>Center Angle</th>
<th>Direction</th>
<th>Conversion</th>
</tr>
</thead>
<tbody>
{#each data.servos as servo, index}
<tr class="hover:bg-base-200">
<td class="font-medium">Servo {index}</td>
<td>
<input
type="number"
class="input input-sm input-bordered w-20"
value={servo.center_pwm}
onblur={syncConfig}
oninput={event => updateValue(event, index, 'center_pwm')}
min="80"
max="600" />
</td>
<td>
<input
type="number"
step="0.1"
class="input input-sm input-bordered w-20"
value={servo.center_angle}
onblur={syncConfig}
oninput={event => updateValue(event, index, 'center_angle')}
min="-90"
max="90" />
</td>
<td>
<button
class="btn btn-sm btn-ghost"
title="Toggle direction {servo.direction}"
onclick={() => toggleDirection(index)}>
{#if servo.direction === 1}
<RotateCw class="w-4 h-4 text-green-500" />
{:else}
<RotateCcw class="w-4 h-4" />
{/if}
</button>
</td>
<td>
<input
type="number"
step="0.01"
class="input input-sm input-bordered w-20"
value={servo.conversion}
onblur={syncConfig}
oninput={event => updateValue(event, index, 'conversion')}
min="0"
max="10" />
</td>
</tr>
{/each}
</tbody>
</table>
</div>
@@ -1,30 +0,0 @@
<script lang="ts">
import type { Servo } from "$lib/models";
import { createEventDispatcher } from "svelte";
export let servo: Servo;
const dispatch = createEventDispatcher();
const sweep = () => {
dispatch('sweep', {channel: servo.channel});
};
</script>
<div>
<h2 class="text-lg">{ servo.name }</h2>
<div class="flex gap-2 items-center">
Is inverted <input type="checkbox" bind:checked={servo.inverted} class="toggle"/>
</div>
<div>
Middle position <input type="number" bind:value={servo.center_angle} class="input input-bordered input-sm max-w-xs"/>
</div>
<div class="relative mb-6">
<label for="labels-range-input" class="sr-only">Labels range</label>
<input id="labels-range-input" type="range" bind:value={servo.angle} min="0" max="180" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700">
<span class="text-sm text-gray-500 dark:text-gray-400 absolute start-0 -bottom-6">0</span>
<span class="text-sm text-gray-500 dark:text-gray-400 absolute start-1/2 -translate-x-1/2 rtl:translate-x-1/2 -bottom-6">90</span>
<span class="text-sm text-gray-500 dark:text-gray-400 absolute end-0 -bottom-6">180</span>
</div>
<button class="btn btn-neutral btn-sm" on:click={sweep}>Sweep range</button>
</div>

Some files were not shown because too many files have changed in this diff Show More