683 Commits

Author SHA1 Message Date
Niklas Jensen 7376ecf270 Fix IMU ang MAG, added MAG chart to svelte 2025-11-30 12:43:05 +01:00
Niklas Jensen 5481a598d9 Early stages of magnetometer from ICM20948 2025-11-30 12:43:05 +01:00
Niklas Jensen 0d379a8013 Add void pointer for initializing sensors 2025-11-30 12:43:05 +01:00
Rune Harlyk 868ff0446a 🐛 Fix imu and magnotometer 2025-11-30 12:43:04 +01:00
Niklas Jensen 081c1e7046 Ignore weird success flag on IMU update 2025-11-30 12:41:52 +01:00
Rune Harlyk 042548412d 🐛 Imu temp in message 2025-11-30 12:41:52 +01:00
Niklas Jensen 5c4dc51093 Added PAJ7620U2 defs, set mag for ICM20948 temp 2025-11-30 12:41:52 +01:00
Rune Harlyk 94a50302cc 🐛 Fix socket deadlock 2025-11-30 12:41:52 +01:00
Rune Harlyk e17382c505 Emit imu, mag and bmp data 2025-11-30 12:41:51 +01:00
Rune Harlyk 106c20418c 🐛 Fix system metric emit 2025-11-30 12:41:04 +01:00
Rune Harlyk 413097db1c 🐛 Call begin on camera service 2025-11-30 12:40:44 +01:00
Niklas Jensen f9c28ed42a Fix USE_ICM20948 checks in peripherals.cpp 2025-11-30 12:40:14 +01:00
Rune Harlyk 69dbea3fae 🐛 Call begin on camera service 2025-11-30 12:40:14 +01:00
Niklas Jensen a24ab44b17 Added ICM20948 support 2025-11-30 12:40:14 +01:00
Rune Harlyk 9e02f8b8ee Remove psychichttp 2025-11-27 16:45:30 +01:00
Niklas Jensen 7c3dd2d15b Fixed ordering of readme (developing->running) 2025-11-27 14:29:47 +01:00
Niklas Jensen 135c7b0c94 Fixed esp32 prebuild FS directory location 2025-11-27 14:29:47 +01:00
Rune Harlyk 06d457f4e5 🐛 Fixes barometer 2025-11-04 20:03:07 +01:00
Rune Harlyk 67c5936399 🎨 Update observation space to match real world 2025-10-23 15:41:02 +02:00
Rune Harlyk f1751f2589 🐛 Fixes drag angles handling 2025-10-20 21:12:39 +02:00
Rune Harlyk 48c0b01f93 🎨 Remove sky in favor of static background color 2025-10-20 21:07:05 +02:00
Rune Harlyk 64ef3d31eb 🐛 Fix relative path in app 2025-10-20 20:34:33 +02:00
Rune Harlyk b14f005b22 🐛 Fix model loading on github pages 2025-10-20 20:17:57 +02:00
Rune Harlyk 72a288145d 🎨 Set 3D representation as default view 2025-10-20 19:22:23 +02:00
Rune Harlyk af0815b01f 🎨 Reduce stand offset 2025-10-20 19:08:09 +02:00
Rune Harlyk df3e813470 🎨 Improve rotation handling 2025-10-20 19:08:09 +02:00
Rune Harlyk 1b28b8b7fd 🐛 Fix stl relative model path 2025-10-20 19:08:09 +02:00
Rune Harlyk c449cb3390 🎨 Adds rotation keyboard controls 2025-10-20 19:08:09 +02:00
Rune Harlyk 05a420f345 Adds cumulative displacement of the robot 2025-10-20 19:08:09 +02:00
Rune Harlyk df395657e3 🎨 Removes deprecated base 2025-10-20 17:35:39 +02:00
Rune Harlyk 8970457353 🎨 Fix different typing problems 2025-10-14 20:07:12 +02:00
Rune Harlyk 0aab42f0e9 🎮 Maps controller buttons to modes 2025-10-14 19:41:40 +02:00
Rune Harlyk 76d965ff43 🎨 Updates defaults motion smoothing 2025-10-11 20:51:02 +02:00
Rune Harlyk 0b9921e592 🎨 Updates duty and fixes direction angle 2025-10-11 19:16:30 +02:00
Rune Harlyk aee29c47e4 🎨 Improves mode handling 2025-10-11 15:29:22 +02:00
Rune Harlyk f2ee454b89 ⬆️ Upgrade frontend dependencies 2025-10-11 11:02:17 +02:00
Rune Harlyk a77eb0b1e0 🎨 Lint project 2025-10-11 10:54:07 +02:00
Rune Harlyk 91a7b170fe 🎨 format 2025-10-11 10:42:32 +02:00
Rune Harlyk 4d51b9f556 🎨 Adds kinematics config to readme 2025-10-10 22:23:51 +02:00
Rune Harlyk 92a98064c3 🎨 Updates readme 2025-10-10 22:05:27 +02:00
Rune Harlyk 1fbddd483c Adds option to control sim using web app 2025-10-10 22:05:27 +02:00
Rune Harlyk d47ce02cc6 ️ Makes training parallelized 2025-10-10 22:05:27 +02:00
Rune Harlyk 01c4a80c8f 🔥 Clean up gitignore 2025-10-10 22:05:27 +02:00
Rune Harlyk 174d77a9fd Updates training script with stablebaseline 2025-10-10 22:05:27 +02:00
Rune Harlyk a078f28a82 🎨 Use real variables 2025-10-10 22:05:27 +02:00
Rune Harlyk f3f3864b83 🔥 Remove simple play kinematics 2025-10-10 22:05:27 +02:00
Rune Harlyk 46bb5f74b1 🎨 Fixes gait in sim 2025-10-10 22:05:27 +02:00
Rune Harlyk 89a0316fb4 Adds script to play with kinematics 2025-10-10 22:05:27 +02:00
Rune Harlyk 51ee910fb6 🐛 Fixes many smaller simulation pains 2025-10-10 22:05:27 +02:00
Rune Harlyk a198de05c2 Fixes body kin rot 2025-10-10 22:05:27 +02:00
Rune Harlyk d3db2b3650 ♻️ Update sim structure 2025-10-10 22:05:27 +02:00
Rune Harlyk 5a6f195f56 🫐 Updates foot color for urdf 2025-10-10 22:05:27 +02:00
Rune Harlyk 0cae981779 🧁 Simplifies backpart stl 2025-10-10 22:05:27 +02:00
Rune Harlyk c541b3f474 🧼 Removes print 2025-10-10 22:05:27 +02:00
Rune Harlyk ceccb2c901 🪇 Adds git input function to GUI 2025-10-10 22:05:27 +02:00
Rune Harlyk 8c21f3e2e4 🎯 Updates number of solve iterations 2025-10-10 22:05:27 +02:00
Rune Harlyk 55eecdc8d7 🛹 Adds static gui to env 2025-10-10 22:05:27 +02:00
Rune Harlyk b98c0e866b 🍒 Saves the initial state for faster reload 2025-10-10 22:05:27 +02:00
Rune Harlyk 3d294f38c2 🪴 Adds gitignore for python 2025-10-10 22:05:27 +02:00
Rune Harlyk a237dc3995 📏 Tries to rebuild kinematics in python 2025-10-10 22:05:27 +02:00
Rune Harlyk 80c74dc745 🧹 Formats urdf 2025-10-10 22:05:27 +02:00
Rune Harlyk fb9313913d 🤖 Adds plane 2025-10-10 22:05:27 +02:00
Rune Harlyk 33e7fac74c 🤖 Adds initial sim structure 2025-10-10 22:05:27 +02:00
Rune Harlyk 2face72aee 🎨 Clamp servo pwm 2025-10-09 18:33:17 +02:00
Rune Harlyk 1f8e7efdb2 Adds option to rotate gesture sensor 2025-10-09 18:33:04 +02:00
Rune Harlyk b184449e7b 🔥 Clean up arduino libs 2025-10-09 18:31:40 +02:00
Rune Harlyk bc31b1b2dd Replace millis with esp timer 2025-10-09 17:49:36 +02:00
Rune Harlyk 12e1f80830 🐛 Adds missing function definitions in socket adapter 2025-09-18 18:50:04 +02:00
Rune Harlyk 1cadcf8bdb 🎨 Pull subscribe logic out from websocket 2025-09-18 18:50:04 +02:00
Rune Harlyk 06d27e0644 🎨 Renames event socket to websocket adapter 2025-09-18 18:50:04 +02:00
Rune Harlyk 98b519dee8 🐛 Adds servo config over http 2025-09-18 18:50:04 +02:00
Rune Harlyk 4da2d7fa20 🔥 Cleans up build flags 2025-09-18 18:50:04 +02:00
Rune Harlyk 0f992b26e9 🔥 Removes unused feature flags 2025-09-18 18:50:04 +02:00
Rune Harlyk 2a57d1ecc3 🔥 Removes firmware rename script 2025-09-18 18:50:04 +02:00
Rune Harlyk fd3180d08b 🔥 Removes unused libs 2025-09-18 18:50:04 +02:00
Rune Harlyk 43b5216d9f ️ Removes task manager dependency 2025-09-18 18:50:04 +02:00
Rune Harlyk e1e11346b4 🔥 Removes unused functions and constants 2025-09-18 18:50:04 +02:00
Rune Harlyk 3ce8c88a84 🎨 Replace Arduino String with std::string 2025-09-18 18:50:04 +02:00
Rune Harlyk 0285b522f1 🎨 Replaces delay with vTaskDelay 2025-09-18 18:50:04 +02:00
Rune Harlyk 4ea287b162 🐛 Fixes table linking 2025-09-14 19:43:34 +02:00
Rune Harlyk c2d52449b4 🎨 Makes file system service use define var 2025-09-14 19:43:34 +02:00
Rune Harlyk f9a0880cd9 Moves servo event to main 2025-09-14 19:43:34 +02:00
Rune Harlyk 1bb098e952 ⬇️ Downgrades fastled version 2025-09-14 19:43:34 +02:00
Rune Harlyk 9c74c8e87b 🚨 Fixes build error for esp-idf 2025-09-14 19:43:34 +02:00
Rune Harlyk 3f4d956903 Adds partion tables 2025-09-14 19:43:34 +02:00
Rune Harlyk a5371c36b9 ♻️ Moves peripherals to source file, add sensor base 2025-09-14 19:43:34 +02:00
Rune Harlyk 41b863a0eb ♻️ Moves motion implementation to source file 2025-09-14 19:43:34 +02:00
Rune Harlyk 7fd35f3f48 ♻️ Major clean up of project structure 2025-09-14 19:43:34 +02:00
Rune Harlyk 26c36b8302 🎨 Makes gesture sensor more readable and motion take last gesture 2025-09-10 15:16:00 +02:00
Rune Harlyk bfc259e660 Adds gesture controls 2025-09-10 15:16:00 +02:00
Rune Harlyk 6368bf9213 🎨 Makes use of msg type for sensors 2025-09-10 11:15:44 +02:00
Rune Harlyk cd802f1c22 Makes fsm states by time aware 2025-09-08 22:39:53 +02:00
Rune Harlyk 59bb1d9579 ️ Improves imu speed by making it non blocking and run faster 2025-09-08 22:37:57 +02:00
Rune Harlyk ae98ba76f7 Makes stand imu compensating 2025-09-06 21:02:28 +02:00
Rune Harlyk bd8c8fd988 🐛 Fixes imu handling 2025-09-06 19:55:57 +02:00
Rune Harlyk 7de5a1aa7c 🎨 Lerp gait params to target 2025-09-05 15:22:47 +02:00
Rune Harlyk a3e4fdd8a5 🎨 Moves kinematics config to kinematics file 2025-09-05 14:55:02 +02:00
Rune Harlyk f82fa051f2 🎨 Renames states folder 2025-09-04 23:33:45 +02:00
Rune Harlyk b66ddc3e81 Introduces motion as a state machine 2025-09-04 23:33:45 +02:00
Rune Harlyk c85ac41ebc 🐛 Makes step height dynamic 2025-09-04 21:03:09 +02:00
Rune Harlyk 78d01533f4 Makes body rotation controllable 2025-09-04 19:31:45 +02:00
Rune Harlyk 18d4d66758 Makes robot stand compensate imu 2025-09-04 19:27:48 +02:00
Rune Harlyk 1b9dc9bb9e Makes motion use target position for body state 2025-09-04 19:27:17 +02:00
Rune Harlyk 767d1157df Makes kinematics params be based on config 2025-09-04 19:08:54 +02:00
Rune Harlyk 1799889712 Introduces kinmatics config to sync mapping between variants 2025-09-04 18:02:38 +02:00
Rune Harlyk 0b5d7b1534 Fixes gait into bezier 2025-09-04 17:33:25 +02:00
Rune Harlyk 10b78e6919 🎨 Smoother crawl body shift 2025-09-04 17:33:25 +02:00
Rune Harlyk 3fd72d081e 🎨 Correct behavoir 2025-09-04 17:33:25 +02:00
Rune Harlyk 1f3a465d3e 🎨 Adds speed factor to frontend 2025-09-04 17:33:25 +02:00
Rune Harlyk cddb6023e7 🎨 Better base walking speed 2025-09-04 17:33:25 +02:00
Rune Harlyk 2f46484e0a 🎨 Simplifies gait 2025-09-04 17:33:25 +02:00
Rune Harlyk 4fcaf5d77d 🐛 Try to handle body shifting 2025-09-04 17:33:25 +02:00
Rune Harlyk ea8ddb43ef 🎨 Adds speed factor between gaits 2025-09-04 17:33:25 +02:00
Rune Harlyk 774c546487 🎨 Cleanup crawl 2025-09-04 17:33:25 +02:00
Rune Harlyk 6f46c1f598 🎨 Renames kinematics config 2025-09-04 17:33:25 +02:00
Rune Harlyk bc810ee2dd 🎨 Adds defaults to notification service 2025-09-04 17:33:25 +02:00
Rune Harlyk 54a0419770 🎨 Cleans up gait handling code 2025-09-04 17:33:25 +02:00
Rune Harlyk d7a6bffe0a 🎨 Update the rotation command handling 2025-09-01 22:53:14 +02:00
Rune Harlyk df087decdb 🎨 Renames topics 2025-09-01 18:48:27 +02:00
Rune Harlyk 527764b0b5 🐛 Expands number of endpoints 2025-09-01 18:43:12 +02:00
Rune Harlyk 8c97c68d11 🚩 Add feature flag for spot pico 2025-09-01 18:42:51 +02:00
Rune Harlyk e5bf10cdb0 🎨 Updates and simplifies command handling 2025-09-01 18:41:59 +02:00
Rune Harlyk de3912ff10 Adds kinematics for spot pico 2025-08-22 12:31:22 +02:00
Rune Harlyk 251a791876 Enables better zoom for viz 2025-08-21 23:13:50 +02:00
Rune Harlyk e36365ead6 Adds gif of short walk 2025-08-11 14:40:49 +02:00
Rune Harlyk cb5c095888 🐛 Removes camera endpoint using feature flag 2025-08-03 15:53:49 +02:00
Rune Harlyk 281fa32c89 🐛 Fixes the relative paths 2025-08-02 16:43:45 +02:00
Rune Harlyk d899701195 Simplifies frontend test 2025-07-16 21:58:39 +02:00
Rune Harlyk 7061166fcd 🎨 Matches command mapping in frontend 2025-07-16 21:47:24 +02:00
Rune Harlyk 36b39d41ba 🎨 Replace magic number for stand_frac 2025-07-16 21:44:55 +02:00
Rune Harlyk 7d0a7861ea 🎨 Formats extensions.json 2025-07-16 20:41:28 +02:00
Rune Harlyk bf8c9bce95 📝 Updates readme 2025-07-16 20:40:34 +02:00
Rune Harlyk 9c984d3215 🎨 Inlines cors wildcard 2025-07-16 20:33:12 +02:00
Rune Harlyk 43e76770a8 ️ Removes unnecessary lerp 2025-07-16 20:32:46 +02:00
Rune Harlyk 6e10eabd9f 🔥 Cleans up peripherals service 2025-07-16 20:32:19 +02:00
Rune Harlyk 922a4e3665 🔥 Removes certs 2025-07-16 20:27:33 +02:00
Rune Harlyk 5e162ffb71 ️ Adds build flags for speed and gc 2025-07-16 20:26:21 +02:00
Rune Harlyk f21ce92d43 🐛 Excludes models files for other variants when building 2025-07-12 12:43:07 +02:00
Rune Harlyk 98f3fc674b Makes socket messages event typed 2025-07-11 18:59:07 +02:00
Rune Harlyk c5901c65b3 Adds yertle model visulization 2025-07-11 15:16:47 +02:00
Rune Harlyk 2eab893dd7 🚩 Expands feature flag handling with persistence 2025-07-11 15:16:47 +02:00
Rune Harlyk a3be035f98 🚚 Moves firmware to src and include 2025-07-11 12:16:23 +02:00
Rune Harlyk 743aa073b7 🚀 Makes deploy action run 2025-07-10 23:18:15 +02:00
Rune Harlyk a3de13c619 🔧 Makes default visualization be spot micro 2025-07-10 22:32:27 +02:00
Rune Harlyk 90be771211 🚀 Deploys app 2025-07-10 22:28:05 +02:00
Rune Harlyk 7d79ec39ab Fixes more linter errors 2025-07-10 21:54:38 +02:00
Rune Harlyk 211ff7205b 🔧 Adds env with default variables 2025-07-10 21:54:38 +02:00
Rune Harlyk d0aa3b7b42 💄 Updates colors for metrics chart 2025-07-10 21:54:38 +02:00
Rune Harlyk d529eaa201 Fixes build warning and errors 2025-07-10 21:54:38 +02:00
Rune Harlyk c8ee64d7f4 🐛 Fixes event socket binary serialization buffer length 2025-07-10 20:44:04 +02:00
Rune Harlyk ec4c3fd98e Changes mgspack dependency 2025-07-10 19:04:39 +02:00
Rune Harlyk 0cc372cd36 🐛 Fixes some linting errors 2025-07-10 19:04:39 +02:00
Rune Harlyk 9be405a89d 🐛 Maps frontend gait params same as backend 2025-07-10 19:04:39 +02:00
Rune Harlyk e3cfe89e19 ♻️ Replaces JsonObject with JsonVariant 2025-07-10 19:04:39 +02:00
Rune Harlyk 144b99c180 🔥 Removes debug logging 2025-07-10 19:04:39 +02:00
Rune Harlyk c788e118e3 ️ Adds O3 build flag 2025-07-10 19:04:39 +02:00
Rune Harlyk aae16335b3 ♻️ Centralizes socket serialization 2025-07-10 19:04:39 +02:00
Rune Harlyk a43c250ed1 Adds msgPack and update message protocol 2025-07-10 19:04:39 +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
Rune Harlyk 0ccd54ba53 🌍 Makes dev server available network wide 2024-08-03 18:21:58 +02:00
Rune Harlyk 278061bd7c 🚨 Updates led service to show isConnected 2024-08-03 17:16:51 +01:00
Rune Harlyk 4c05ba695b 📏 Adds step length slider 2024-08-03 17:16:51 +01:00
Rune Harlyk 8d2ca13b51 🪝 Removes user selection in controller view 2024-08-03 17:16:51 +01:00
Rune Harlyk 5b6f27d692 🐕 Makes gaits speed variable 2024-08-03 17:16:51 +01:00
Rune Harlyk e532ae7929 🎉 Connects motion service with servo controller 2024-08-03 17:16:51 +01:00
Rune Harlyk 4e75952f57 🏎️ Updates servo controller to reflect rl angles 2024-08-03 17:16:51 +01:00
Rune Harlyk 5ecb2eb9b5 ⚖️ Adds equality functions 2024-08-03 11:49:53 +02:00
Rune Harlyk 588952496b 🧄 Updates gait members to use body_state refference 2024-08-03 11:44:49 +02:00
Rune Harlyk 4d5ea77909 🚦 Removes cone visulization 2024-08-03 01:57:49 +01:00
Rune Harlyk 70fe15054a 🐋 Adds sonar functionality 2024-08-03 01:57:49 +01:00
Rune Harlyk 069f14ddf7 🙊 Adds sonar support 2024-08-03 01:57:49 +01:00
Rune Harlyk 1f30b919f5 🌆 Removes log for onSubscribe event 2024-08-03 02:52:19 +02:00
Rune Harlyk f229d0b3e3 🐾 Updates the default feet positions 2024-08-02 21:13:04 +02:00
Rune Harlyk fe920ca939 🎓 Updates task manager 2024-08-02 19:23:47 +02:00
Rune Harlyk 5bced012ca 📈 Updates the metric graph scales 2024-08-02 18:55:42 +02:00
Rune Harlyk b3b7eb10c2 🫀 Adds task to analytics 2024-08-02 18:55:23 +02:00
Rune Harlyk 10c0e28ecd 📈 Updates system metric charts 2024-08-02 18:29:36 +02:00
Rune Harlyk 0854061e36 ⏱️ Updates timing macro 2024-08-02 17:30:06 +02:00
Rune Harlyk e7f78c52da 🚇 Adds hasSubcribers to event socket for early return 2024-08-02 14:54:00 +01:00
Rune Harlyk d182e9e925 👨‍💻 Updates c++ version to 23 2024-08-01 23:47:36 +01:00
Rune Harlyk 63816ba4cf 🦘 Formats gait code 2024-08-01 22:09:38 +01:00
Rune Harlyk 1bcebf8e00 🦘 Adds gait comment in changelog 2024-08-01 22:09:38 +01:00
Rune Harlyk abefdd6c21 🪘 Adds translated c++ Gait code 2024-08-01 22:09:38 +01:00
Rune Harlyk 0e59ee93f8 🦘 Cleans gait source 2024-08-01 22:09:38 +01:00
Rune Harlyk 215bfdf582 🎲 Adds all gait planners for internal kinematics 2024-08-01 22:09:38 +01:00
Rune Harlyk 46a7dbd8f2 🪅 Adds 8 phase gait with body shift 2024-08-01 22:09:38 +01:00
Rune Harlyk 5d9343989b 🧹 Cleans up gait code 2024-08-01 22:09:38 +01:00
Rune Harlyk 8a7bbb90d7 🪅 Adds motion smoothing option for debug 2024-08-01 22:09:38 +01:00
Rune Harlyk c93d3a030d 🐎 Adds four phase gait 2024-08-01 22:09:38 +01:00
Rune Harlyk c2e80f99c3 🦋 Adds phase controller 2024-08-01 22:09:38 +01:00
Rune Harlyk bd7fef7c46 🐏 Adds gait structures 2024-08-01 22:09:38 +01:00
Rune Harlyk f8e52bf4c0 📃 Update 6_developing.md 2024-07-24 20:44:02 +02:00
Rune Harlyk 9e58939dfd 🧵 Takes semaphore onWSClose 2024-07-22 23:31:33 +02:00
Rune Harlyk b204e49e36 🐏 Adds support for esp32s3 psram 2024-07-22 14:08:03 +02:00
Rune Harlyk 634fb62913 🛜 Updates wifi RSSI buffer size 2024-07-22 14:00:22 +02:00
Rune Harlyk 431487a328 📷 Returns camera sensor after sending frame 2024-07-22 13:59:46 +02:00
Rune Harlyk ae75d4011b ⏱️ Adds time it macro 2024-07-22 13:55:37 +02:00
Rune Harlyk 168585c89d ✂️ Removes mock server 2024-07-18 20:26:32 +02:00
Rune Harlyk 5017e20871 ☀️ Adds sun elevation calculator 2024-07-18 00:35:23 +02:00
Rune Harlyk b61223ea81 🎮 Adds controller demo to readme.md 2024-07-18 00:29:55 +02:00
Rune Harlyk 162c69fc7c 🎮 Adds demo of controller and kinematics 2024-07-18 00:29:55 +02:00
Rune Harlyk f76d57f331 🐍 Restricts angular movement 2024-07-17 23:54:13 +02:00
Rune Harlyk 080c18cf19 ⬅️ Fixes direction for the kinematic result 2024-07-17 23:54:13 +02:00
Rune Harlyk 1ce4da5d11 👨‍🔬 Adds math macros 2024-07-14 23:59:27 +02:00
Rune Harlyk d6df900a49 🍬 Renames servo event config 2024-07-14 23:32:49 +02:00
Rune Harlyk 200ea62d95 Adds timing macro 2024-07-14 23:32:49 +02:00
Rune Harlyk c783793b5c 🔋 Adds battery model 2024-07-14 23:16:31 +02:00
Rune Harlyk cfa3e58d09 Moves common math functions to own file 2024-07-12 13:40:49 +02:00
Rune Harlyk c432792300 🐍 Makes sveltekit run arduino task loop 2024-07-11 23:11:12 +02:00
Rune Harlyk 6c257784ca 🪇 Updates task manager to accurate task cpu usage 2024-07-11 23:11:12 +02:00
Rune Harlyk ba8295dc57 💊 Updates camera task priority 2024-07-11 23:11:12 +02:00
Rune Harlyk 2872354a67 📶 Makes WiFiIcon tooltip be the RSSI value 2024-07-09 21:42:03 +02:00
Rune Harlyk ef2ffa0f78 🎏 Updates logging level in PsychicWebSocket.cpp 2024-07-09 20:45:09 +02:00
Rune Harlyk 12fc57af1f 🐇 Adds clang as a software requirement 2024-07-09 20:31:31 +02:00
Rune Harlyk 0e29dba043 🪄 Formats ESP32SvelteKit 2024-07-09 20:31:31 +02:00
Rune Harlyk b75c3bc251 🪄 Formats Peripherals 2024-07-09 20:31:31 +02:00
Rune Harlyk 1aba163b60 🪄 Formats MotionService 2024-07-09 20:31:31 +02:00
Rune Harlyk 9ea6eb2b5d 🪄 Formats LEDService 2024-07-09 20:31:31 +02:00
Rune Harlyk e8e4e4c953 🪄 Formats Kinematics 2024-07-09 20:31:31 +02:00
Rune Harlyk cee796c705 🪄 Formats JsonUtils 2024-07-09 20:31:31 +02:00
Rune Harlyk 2d57fc5fee 🪄 Formats IPUtils 2024-07-09 20:31:31 +02:00
Rune Harlyk 6ee9100fdc 🪄 Formats HttpEndpoint 2024-07-09 20:31:31 +02:00
Rune Harlyk 6a6fb74229 🪄 Formats FileExplorer 2024-07-09 20:31:31 +02:00
Rune Harlyk 181788ee46 🪄 Formats display service 2024-07-09 20:31:31 +02:00
Rune Harlyk 42eafde631 🪄 Formats NTP related service 2024-07-09 20:31:31 +02:00
Rune Harlyk 33e1a28223 🪄 Formats RestartService 2024-07-09 20:31:31 +02:00
Rune Harlyk c47a7bc02f 🪄 Formats SecurityManager 2024-07-09 20:31:31 +02:00
Rune Harlyk 4e5f582978 🪄 Formats SecuritySettingsService 2024-07-09 20:31:31 +02:00
Rune Harlyk 7d586eec90 🪄 Formats StatefulService 2024-07-09 20:31:31 +02:00
Rune Harlyk 49e4291f2d 🪄 Formats FSPersistence 2024-07-09 20:31:31 +02:00
Rune Harlyk a19d789174 🪄 Formats FeatureService 2024-07-09 20:31:31 +02:00
Rune Harlyk 38288a47e5 🪄 Formats FactoryResetService 2024-07-09 20:31:31 +02:00
Rune Harlyk 2478e9a77b 🪄 Formats EventSocket 2024-07-09 20:31:31 +02:00
Rune Harlyk 3c8775de3d 🪄 Formats EventEndpoint 2024-07-09 20:31:31 +02:00
Rune Harlyk 23a2ea566d 🪄 Formats DownloadFirmwareService 2024-07-09 20:31:31 +02:00
Rune Harlyk bbc7498653 🪄 Formats CameraService 2024-07-09 20:31:31 +02:00
Rune Harlyk 74c2285800 🪄 Formats BatteryService 2024-07-09 20:31:31 +02:00
Rune Harlyk 227610fcb9 🪄 Formats AuthenticationService 2024-07-09 20:31:31 +02:00
Rune Harlyk 4952be1b47 🪄 Formats ArduinoJsonJWT 2024-07-09 20:31:31 +02:00
Rune Harlyk ac022094ed 🪄 Formats APStatus 2024-07-09 20:31:31 +02:00
Rune Harlyk aa23377774 🪄 Formats APSettingsService 2024-07-09 20:31:31 +02:00
Rune Harlyk 9b56b257b7 🪄 Formats Analytics service 2024-07-09 20:31:31 +02:00
Rune Harlyk 227cbd536f 🪄 Formats StatefulService 2024-07-09 20:31:31 +02:00
Rune Harlyk ba41f520b0 🪄 Formats SleepService 2024-07-09 20:31:31 +02:00
Rune Harlyk 03e21beddd 🪄 Formats SystemStatus 2024-07-09 20:31:31 +02:00
Rune Harlyk 0ba9ad75b0 🪄 Formats TaskManager 2024-07-09 20:31:31 +02:00
Rune Harlyk 6a0ff5cd80 🪄 Formats UploadFirmwareService 2024-07-09 20:31:31 +02:00
Rune Harlyk 3ff5384b42 🪄 Formats WiFiScanner 2024-07-09 20:31:31 +02:00
Rune Harlyk bec053ad18 🪄 Formats WiFiSettingsService 2024-07-09 20:31:31 +02:00
Rune Harlyk a4b41e845b 🪄 Formats wifiStatus 2024-07-09 20:31:31 +02:00
Rune Harlyk 97201c0f73 🪄 Formats main 2024-07-09 20:31:31 +02:00
Rune Harlyk 767e828332 🪮 Adds clang format 2024-07-09 20:31:31 +02:00
Rune Harlyk 68789de008 🎏 Adds project directory description 2024-07-09 20:24:53 +02:00
Rune Harlyk d0fa715dee 🎚️ Updates logging level for onSubcribe 2024-07-08 21:55:00 +02:00
Rune Harlyk 10d4b75b05 🐍 Adds python version 2024-07-08 21:53:05 +02:00
Rune Harlyk 5645736256 🗿 Adds i2c scanner and page 2024-07-08 21:44:18 +02:00
Rune Harlyk c400660a6f 🖱️ Combines deviceConfig and IMU service to peripherals 2024-07-08 21:44:18 +02:00
Rune Harlyk 81f69631f9 🪇 Adds early return for kinematics to reduce calc 2024-07-08 21:40:31 +02:00
Rune Harlyk 2689093485 🪮 Add c_str() to origin id 2024-07-05 20:28:51 +02:00
Rune Harlyk d977aa0a70 🎥 Moves camera stream to component 2024-07-05 11:41:03 +02:00
Rune Harlyk 73019c008b 💡 Adds led service 2024-07-01 21:20:55 +02:00
Rune Harlyk 9d127230ca 🐢 Fixes links in readme.md 2024-06-30 12:52:21 +02:00
Rune Harlyk 909f947407 🦕 Fixes links in readme.md 2024-06-30 12:50:22 +02:00
Rune Harlyk 43eba6b642 ✒️ Updates readme.md 2024-06-29 22:31:48 +02:00
Rune Harlyk 3fed74ea00 🎍 Updates readme.md 2024-06-29 22:22:41 +02:00
Rune Harlyk 33422faf30 Cleans up documentation 2024-06-29 22:09:22 +02:00
Rune Harlyk 99de6a01ce 🍦 Adds software_description.md 2024-06-29 22:04:29 +02:00
Rune Harlyk 13e38e8d5e 🛜 Removes leftover mqtt 2024-06-28 13:31:34 +02:00
Rune Harlyk e8f48f7427 🪰 Fixes payload offset bug 2024-06-28 02:22:00 +02:00
Rune Harlyk adf71187c6 🔋 Adds battery event interval 2024-06-25 21:32:57 +02:00
Rune Harlyk 0bc844d6c5 🪰 Adds orientation store 2024-06-25 21:18:23 +02:00
Rune Harlyk d489759087 🔋 Updates battery service with voltage and current 2024-06-25 21:16:31 +02:00
Rune Harlyk e0096e53a9 🧭 Hides charts base on sensors available 2024-06-25 20:26:24 +02:00
Rune Harlyk 68d7568dd7 🏮 Updates readme 2024-06-18 21:11:24 +02:00
Rune Harlyk 8574c4e14d 🔭 Adds platformio build badge status 2024-06-18 20:52:07 +02:00
Rune Harlyk c9626dfa44 📜 Adds changelog 2024-06-18 16:39:36 +02:00
Rune Harlyk 283c420f98 🪄 Updates eventsocket protocol 2024-06-18 16:39:36 +02:00
Rune Harlyk e4ea3992b3 🪄 Adds servo angles 2024-06-18 16:39:36 +02:00
Rune Harlyk b4a106e7bc 🛸 Use char* in favour of String 2024-06-18 16:39:36 +02:00
Rune Harlyk b7f4e9c043 💣 Cleans up event socket interface 2024-06-18 16:39:36 +02:00
Rune Harlyk 6a638d2eeb 💣 Removes websocketServer 2024-06-18 16:39:36 +02:00
Rune Harlyk cac70f5707 🖥️ Removes event registration 2024-06-18 16:39:36 +02:00
Rune Harlyk efb45218af 📷 Adds support for esp32-wroom-camera 2024-06-17 22:27:12 +02:00
Rune Harlyk 0880f569b7 💣 Removes mqtt support 2024-06-17 22:05:48 +02:00
Rune Harlyk 4e69ff1572 🧹 Makes sweeping faster 2024-06-10 21:45:22 +02:00
Rune Harlyk 8045edac87 👑 Updates internal kinematics params 2024-06-10 21:45:22 +02:00
Rune Harlyk 944ef033a0 🚚 Adds function to delete files 2024-06-10 21:45:22 +02:00
Rune Harlyk 813dde318c 🍡 Adds sweeping for servo 2024-06-10 21:45:22 +02:00
Rune Harlyk d951bc13c8 🦕 Update embedded-build.yml 2024-06-10 21:45:22 +02:00
Rune Harlyk 59eac0569d 🐝 Update embedded-build.yml 2024-06-10 21:45:22 +02:00
Rune Harlyk 515ce57c18 🦔 Update embedded-build.yml 2024-06-10 21:45:22 +02:00
Rune Harlyk 88f9c0e5fb 🎩 Adds lots of magic 2024-06-10 21:45:22 +02:00
Rune Harlyk 69733beb5e 📂 Adds file selection 2024-06-10 21:45:22 +02:00
Rune Harlyk 55347f1cac 🪵 Removes serial logging 2024-06-10 21:45:22 +02:00
Rune Harlyk f62a8a38cb 🕸️ Enables servo config endpoint and persistence 2024-06-10 21:45:22 +02:00
Rune Harlyk 81792f3dd5 ⛱️ Updates logging to use ESP_LOG in favor Serial 2024-06-10 21:45:22 +02:00
Rune Harlyk 2e370ea217 🎮 Updates embedded build workflow 2024-06-10 21:45:22 +02:00
Rune Harlyk 45ffc31dfd ⛱️ Updates wwwdata 2024-06-10 21:45:22 +02:00
Rune Harlyk 5d28dafb68 🛸 Adds embedded build workflow 2024-06-10 21:45:22 +02:00
Rune Harlyk 7005ae7e15 🎩 Elevates ui components 2024-06-10 21:45:22 +02:00
Rune Harlyk 421c7a908b 🧹 Removes duplicated file 2024-06-10 21:45:22 +02:00
Rune Harlyk f29700dcd6 🔮 Restructes platformio.ini 2024-06-10 21:45:22 +02:00
Rune Harlyk 6e02d7bddb ⛱️ Removes std warnings 2024-06-10 21:45:22 +02:00
Rune Harlyk 5e946343f2 ⚠️ Removes redefines warning 2024-06-10 21:45:22 +02:00
Rune Harlyk 42597da736 🔮 Refactors platformio configuration 2024-06-10 21:45:22 +02:00
Rune Harlyk 4b76e90db3 🤖 Adds small fixes to psychic 2024-06-10 21:45:22 +02:00
Rune Harlyk e81beeb36b 🛸 Adds servo config 2024-06-10 21:45:22 +02:00
Rune Harlyk f5d9cea236 🌗 Make more service feature togglable 2024-06-10 21:45:22 +02:00
Rune Harlyk c9be4873f4 🦼 Updates servoController 2024-06-10 21:45:22 +02:00
Rune Harlyk e2b54cdf5e 🦼 Adds servo calibration UI 2024-06-10 21:45:22 +02:00
Rune Harlyk c96703538c ⌨️ Adds KDE 2024-06-10 21:45:22 +02:00
Rune Harlyk f95fdf02a5 🍒 Makes feature toggles work independent 2024-06-10 21:45:22 +02:00
Rune Harlyk 5f5edcff2c 📏 Adds more precision to shared angles 2024-06-04 17:42:31 +02:00
Rune Harlyk a7efb274b8 🔮 Reduces flash size 2024-06-04 17:42:31 +02:00
Rune Harlyk b560aacd7f 🛸 Gitignores build and launch.json 2024-06-04 17:42:31 +02:00
Rune Harlyk c61d761773 ⛱️ Removes kinematics notebook 2024-06-04 17:42:31 +02:00
Rune Harlyk cf1036b572 Ignores c_cpp_properties 2024-06-04 17:42:31 +02:00
Rune Harlyk 4bf630edd3 🪠 Removes feature code 2024-06-04 17:42:31 +02:00
Rune Harlyk bfca33e55d 🗿 Hides position target 2024-06-04 17:42:31 +02:00
Rune Harlyk 2a42eb5f3c 🪠 Adds GaitPlanner interface 2024-06-04 17:42:31 +02:00
Rune Harlyk 1b2d6a9850 🛸 Adds tranformcontroller for body 2024-06-04 17:42:31 +02:00
Rune Harlyk 379091433c 🥣 Updates filesystem partition 2024-06-04 17:42:31 +02:00
Rune Harlyk b338ec0316 🪄 Update kinematic interface 2024-06-04 17:42:31 +02:00
Rune Harlyk ccf6f01e4d ♣️ Simplifices angle updating 2024-06-04 17:42:31 +02:00
Rune Harlyk 7482752698 🦾 Fixes kinematic leg orientation 2024-06-04 17:42:31 +02:00
Rune Harlyk 05cf4fc138 ✂️ Removes min spiffs config 2024-06-04 17:42:31 +02:00
Rune Harlyk eb609e9873 🏍️ Adds position event for updating kinematics 2024-06-04 17:42:31 +02:00
Rune Harlyk addf57b2a6 🥣 Adds setting for playing with the kinematics 2024-06-04 17:42:31 +02:00
Rune Harlyk d2d1c85f50 🦾 Makes embedded kinematics kinda work 2024-06-04 17:42:31 +02:00
Rune Harlyk 68d319e022 🌬️ Optimized kinematric.ts with pre computation 2024-06-04 17:42:31 +02:00
Rune Harlyk 6626c2e274 🪄 Simplifies kin code 2024-06-04 17:42:31 +02:00
Rune Harlyk 90e72cbacc 🔦 Adds min spiffs config 2024-06-04 17:42:31 +02:00
Rune Harlyk 1b75de0376 ✂️ Removes unused events 2024-06-04 17:42:31 +02:00
Rune Harlyk 0ae82776e1 🦾 Adds kinematics 2024-06-04 17:42:31 +02:00
Rune Harlyk c2d5195243 🪰 Adds angle, heading, altitude, pressure and temperature 2024-06-02 22:05:45 +02:00
Rune Harlyk f6ca10846f ✈️ Adds barometer to imu service 2024-06-02 22:05:45 +02:00
Rune Harlyk d1567fa2dd 💫 Initial plans for device configuration service 2024-06-02 22:05:45 +02:00
Rune Harlyk 83a9007b51 ✒️ Rename Spot.md to spot.md 2024-05-29 12:31:49 +02:00
Rune Harlyk 869614fedb 📃 Updates documentation with a guide 2024-05-28 20:55:45 +02:00
Rune Harlyk c9ccb914bf 📃 Adds starting documentation 2024-05-28 20:55:45 +02:00
Rune Harlyk e50c9052ec 💫 Adds a getting started guide 2024-05-28 20:55:45 +02:00
Rune Harlyk 84c9b99097 ⛱️ Updates dev proxy to used factory mdns 2024-05-28 20:55:45 +02:00
Rune Harlyk 482a8ed50c 👾 Adds the recommended extensions 2024-05-28 20:55:45 +02:00
Rune Harlyk 0122491367 📜 Adds the initial docs 2024-05-28 20:55:45 +02:00
Rune Harlyk 17b805a964 🪄 Updates readme to reflect new template 2024-05-25 12:40:16 +02:00
Rune Harlyk 9821713309 📶 Disables OAT until flash requirements are met 2024-05-25 12:40:16 +02:00
Rune Harlyk b2b5a2fcb4 🪄 Updates app data 2024-05-25 12:40:16 +02:00
Rune Harlyk b980a76ca2 🪔 Unregistres build flags 2024-05-25 12:40:16 +02:00
Rune Harlyk d17c30c314 ✂️ Removes cors preflight from sleep service 2024-05-25 12:40:16 +02:00
Rune Harlyk dec60bc7d1 ✂️ Cleans up back files 2024-05-25 12:40:16 +02:00
Rune Harlyk d5c198c186 ⛱️ Removes phycic stream 2024-05-25 12:40:16 +02:00
Rune Harlyk dc2a639aff 🌬️ Moves website to progmem 2024-05-25 12:40:16 +02:00
Rune Harlyk 122093885d 🗿 Remove comments 2024-05-25 12:40:16 +02:00
Rune Harlyk 78d280eb37 🗿 Removes unused features 2024-05-25 12:40:16 +02:00
Rune Harlyk 38a9f0011a 🦾 Updates wwwData with newest build 2024-05-25 12:40:16 +02:00
Rune Harlyk 19e5cbd2da 🗿 Updates factory ap password 2024-05-25 12:40:16 +02:00
Rune Harlyk 3be4b97c17 ✂️ Removes old filesystem config 2024-05-25 12:40:16 +02:00
Rune Harlyk adf4a10375 📂 Updates app build output destination 2024-05-25 12:40:16 +02:00
Rune Harlyk 2c90293fc5 📂 Adds pre build step to ensure project structure 2024-05-22 17:20:13 +02:00
Rune Harlyk 67cb048d71 📹 Adds camera services 2024-05-17 14:17:34 +02:00
Rune Harlyk 9991b69471 🌋 Moves defines to header 2024-05-17 14:17:34 +02:00
Rune Harlyk ba12a52224 🌬️ Updates logging level 2024-05-17 14:17:34 +02:00
Rune Harlyk c0fa16dd71 📷 Adds camera service 2024-05-17 14:17:34 +02:00
Rune Harlyk b7ae17f3bf 🪄 Adds api service with updates 2024-05-08 19:53:55 +02:00
Rune Harlyk 4c66c428e6 Removes shadow on scan modal 2024-05-07 11:00:03 +02:00
Rune Harlyk a150caad9d Upgrades ArduinoJson from version 6 to 7 2024-05-07 11:00:03 +02:00
Rune Harlyk 2b4d196e7c 👽 Configures frontend tests suit 2024-05-03 16:23:03 +02:00
Rune Harlyk 00381579db 🏍️ Updates build file 2024-05-03 16:23:03 +02:00
Rune Harlyk ecfc0ac413 📜 Updates readme to reflect ongoing refactoring 2024-05-03 16:23:03 +02:00
Rune Harlyk b7a4568f07 🧹 Removes notification event service 2024-05-03 16:23:03 +02:00
Rune Harlyk 9dee0e1bb1 🏍️ Adds motionservice with data sync 2024-05-03 16:23:03 +02:00
Rune Harlyk ae1cb70710 🔦 Reduces flash size by making analytic synchronous 2024-05-03 16:23:03 +02:00
Rune Harlyk 8f87a1304b 🥣 Updates types and depedencies 2024-05-03 16:23:03 +02:00
Rune Harlyk fac760b709 🔔 Fixes notification bug 2024-05-03 16:23:03 +02:00
Rune Harlyk 95914ec334 Handles svelte warnings 2024-05-03 16:23:03 +02:00
Rune Harlyk b0590e52e8 🧹 Removes unused svelte properties 2024-05-03 16:23:03 +02:00
Rune Harlyk f7a51d1077 📈 Updates cpu chart scale 2024-05-03 16:23:03 +02:00
Rune Harlyk a82f7bcb46 📂 Adds filesystem service 2024-05-03 16:23:03 +02:00
Rune Harlyk 16481b4054 Updates readme to reflect template 2024-05-03 16:23:03 +02:00
Rune Harlyk c8e972f72d Adds taskManager cpu usage 2024-05-03 16:23:03 +02:00
Rune Harlyk d13a9d2b80 Removes shadow 2024-05-03 16:23:03 +02:00
Rune Harlyk f11b4b0c35 Updates rssi 2024-05-03 16:23:03 +02:00
Rune Harlyk a706a377b2 Fixes ui warnings 2024-05-03 16:23:03 +02:00
Rune Harlyk 0a144a7473 Adds navigation menu persistence 2024-05-03 16:23:03 +02:00
Rune Harlyk 028beabb5d Adds wip task manager 2024-05-03 16:23:03 +02:00
Rune Harlyk 35acb958cf Disables sleep and MQQT as default for space saving 2024-05-03 16:23:03 +02:00
Rune Harlyk 5dc80e74d5 Updates lockfile 2024-05-03 16:23:03 +02:00
Rune Harlyk fb42c39b2d Updates the build script 2024-05-03 16:23:03 +02:00
Rune Harlyk 027d5eebc7 Deletes old project 2024-05-03 16:23:03 +02:00
Rune Harlyk 0b4fe8a0ef Updates wifi.svelte 2024-05-03 16:23:03 +02:00
Rune Harlyk dc6e5daf65 Updates the types 2024-05-03 16:23:03 +02:00
Rune Harlyk 28e33dd396 Updates the building workflow to build firmware 2024-05-03 16:23:03 +02:00
Rune Harlyk 48e96d5775 Moves the MQTT types to models 2024-05-03 16:23:03 +02:00
Rune Harlyk 0abe0b530c Updates api path to /api 2024-05-03 16:23:03 +02:00
Rune Harlyk 9ca4381442 Updates daisy ui 2024-05-03 16:23:03 +02:00
Rune Harlyk 5609fe35d7 Makes wifi settings you eventsocket 2024-05-03 16:23:03 +02:00
Rune Harlyk fb9d5637be Removes the lightdemo 2024-05-03 16:23:03 +02:00
Rune Harlyk 81335c59f8 Adds monitor flags for esp32cam 2024-05-03 16:23:03 +02:00
Rune Harlyk 3649d53b04 Removes the in card shadow 2024-05-03 16:23:03 +02:00
Rune Harlyk 5148891fc4 Updates the esp32 template to use eventsocket 2024-05-03 16:23:03 +02:00
Rune Harlyk 7e521235f4 Cleans up ESP32Sveltekit header 2024-05-03 16:23:03 +02:00
Rune Harlyk d800c8612f Adds default header for server name 2024-05-03 16:23:03 +02:00
Rune Harlyk 32352962ef Adds simple display, uss and imu service 2024-05-03 16:23:03 +02:00
Rune Harlyk 0085add674 Makes class live in DRAM_ATTR 2024-05-03 16:23:03 +02:00
Rune Harlyk 9d6815cb05 Restuctures template 2024-05-03 16:23:03 +02:00
Rune Harlyk b804b9df1f Adds template libs 2024-05-03 16:23:03 +02:00
Rune Harlyk 0724705939 Replace eventSource reconnect interval with timeout 2024-05-03 16:23:03 +02:00
Rune Harlyk b8c28fc545 Adds obsidian vault 2024-05-03 16:23:03 +02:00
Rune Harlyk 91d94ca9ac Remove import 2024-05-03 16:23:03 +02:00
Rune Harlyk c5deaa56e9 update actuator state service 2024-05-03 16:23:03 +02:00
Rune Harlyk c71f0a702d Adds vscode settings 2024-05-03 16:23:03 +02:00
Rune Harlyk 1550bb192a Adds www to gitignore 2024-05-03 16:23:03 +02:00
Rune Harlyk 4018c07faf Updates shadows and conditional ground plane 2024-05-03 16:23:03 +02:00
Rune Harlyk e71fc68652 Adds option to change theme 2024-05-03 16:23:03 +02:00
Rune Harlyk e0fa434aeb Allow both way sync of angles 2024-05-03 16:23:03 +02:00
Rune Harlyk 14c38a1700 Clean up app 2024-05-03 16:23:03 +02:00
Rune Harlyk dc7689793d More sync 2024-05-03 16:23:03 +02:00
Rune Harlyk 0fb2387e30 Adds simple data sync 2024-05-03 16:23:03 +02:00
Rune Harlyk 259bc0b5eb Adds actuator service to sync angles 2024-05-03 16:23:03 +02:00
Rune Harlyk f67071fd74 Allows for all template features 2024-05-03 16:23:03 +02:00
Rune Harlyk f3e5a66589 Updates build script 2024-05-03 16:23:03 +02:00
Rune Harlyk cde36ffda5 Adds almost complete use of ESP32-sveltekit template 2024-05-03 16:23:03 +02:00
Rune Harlyk 290f678253 📜 Simplifies buildapp step 2024-05-03 16:23:03 +02:00
Rune Harlyk 3eb8190cda 🚀 Builds with new ESP32-Sveltekit template 2024-05-03 16:23:03 +02:00
Rune Harlyk 6b47100f3f Adds new way to use feature flags 2024-05-03 16:23:03 +02:00
Rune Harlyk 0acbb4c83a 🚀 Initial sveltekit app 2024-05-03 16:23:03 +02:00
Rune Harlyk 23806e366b 🪶 Adds monitor flags 2024-04-30 20:00:32 +02:00
Rune Harlyk 09f9649d6f 🍎 Makes app installable 2024-03-17 22:06:11 +01:00
Rune Harlyk 8528f3400f 🐩 Adds favicon 2024-03-17 21:23:15 +01:00
Rune Harlyk 39675081a0 Adds overall connection diagram 2024-03-17 21:23:15 +01:00
Rune Harlyk 1d4f43d7ae 🧪 Adds platformio build gate 2024-03-17 21:23:15 +01:00
Rune Harlyk 7edf8792f8 🧪 Makes test only run if relevant code is touched 2024-03-17 21:23:15 +01:00
Rune Harlyk 085091d8c2 🐾 Updates readme 2024-03-17 16:04:05 +01:00
Rune Harlyk 19450ec104 🛝 Removes comment from action 2024-03-17 16:04:05 +01:00
Rune Harlyk 63acf93d20 🖖 Updates import of topbar 2024-03-17 16:04:05 +01:00
Rune Harlyk a297c65937 🤏 Add gated check for embedded app build 2024-03-17 16:04:05 +01:00
Rune Harlyk b0aff0f61b 📨 Updates the moved gitignores 2024-03-17 16:04:05 +01:00
Rune Harlyk 5b8ae1d020 🤌 Update naming of frontend test gate 2024-03-17 16:04:05 +01:00
821 changed files with 291627 additions and 27659 deletions
+61
View File
@@ -0,0 +1,61 @@
name: Deploy GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./app
env:
BASE_PATH: /SpotMicroESP32-Leika
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "./app/pnpm-lock.yaml"
- run: pnpm install
- run: pnpm run build
- name: Setup Pages
uses: actions/configure-pages@v4
with:
static_site_generator: "sveltekit"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: app/build/
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
+42
View File
@@ -0,0 +1,42 @@
name: PlatformIO CI
on:
push:
branches: [master]
paths:
- "esp32/**"
- "platformio.ini"
pull_request:
branches: [master]
paths:
- "esp32/**"
- "platformio.ini"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/cache@v3
with:
path: |
~/.cache/pip
~/.platformio/.cache
key: ${{ runner.os }}-pio
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- run: pip install -r esp32/scripts/requirements.txt
- name: Install PlatformIO Core
run: pip install --upgrade platformio
- name: Build PlatformIO Project
run: pio run
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: esp32/build/firmware
@@ -1,10 +1,14 @@
name: CI name: Frontend Tests
on: on:
push: push:
branches: [ master ] branches: [ master ]
paths:
- 'app/**'
pull_request: pull_request:
branches: [ master ] branches: [ master ]
paths:
- 'app/**'
permissions: permissions:
contents: read contents: read
@@ -19,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
@@ -30,6 +34,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run tests - name: Run tests
run: pnpm test run: pnpm test
+8 -2
View File
@@ -1,2 +1,8 @@
*.pyc .vscode/.browse.c_cpp.db*
spot_env .vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
__pycache__/
*.py[cod]
*$py.class
.pio
+4 -1
View File
@@ -2,7 +2,10 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846 // See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format // for the documentation about the extensions.json format
"recommendations": [ "recommendations": [
"platformio.platformio-ide" "bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"platformio.platformio-ide",
"svelte.svelte-vscode"
], ],
"unwantedRecommendations": [ "unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack" "ms-vscode.cpptools-extension-pack"
+10 -1
View File
@@ -1,6 +1,15 @@
{ {
"files.associations": { "files.associations": {
"cmath": "cpp" "cmath": "cpp",
"array": "cpp",
"deque": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"string_view": "cpp",
"initializer_list": "cpp",
"regex": "cpp"
}, },
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.detectIndentation": false, "editor.detectIndentation": false,
+3
View File
@@ -0,0 +1,3 @@
PUBLIC_VITE_USE_HOST_NAME=true
PUBLIC_USE_JSON=true
PUBLIC_USE_MSGPACK=true
-3
View File
@@ -1,3 +0,0 @@
VITE_API_URL="leika.local"
VITE_SOCKET_URL="leika.local"
VITE_EMBEDDED_BUILD=true
-3
View File
@@ -1,3 +0,0 @@
VITE_API_URL="hostname"
VITE_SOCKET_URL="hostname:2096"
VITE_EMBEDDED_BUILD=true
-3
View File
@@ -1,3 +0,0 @@
VITE_API_URL="hostname"
VITE_SOCKET_URL="hostname:2096"
VITE_EMBEDDED_BUILD=false
-3
View File
@@ -1,3 +0,0 @@
VITE_API_URL="leika.local"
VITE_SOCKET_URL="leika.local"
VITE_EMBEDDED_BUILD=false
+30 -19
View File
@@ -1,20 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = { module.exports = {
root: true, root: true,
parser: '@typescript-eslint/parser', extends: [
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], 'eslint:recommended',
plugins: ['svelte3', '@typescript-eslint'], 'plugin:@typescript-eslint/recommended',
ignorePatterns: ['*.cjs'], 'plugin:svelte/recommended',
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 'prettier'
settings: { ],
'svelte3/typescript': () => require('typescript') parser: '@typescript-eslint/parser',
}, plugins: ['@typescript-eslint'],
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020 ecmaVersion: 2020,
}, extraFileExtensions: ['.svelte']
env: { },
browser: true, env: {
es2017: true, browser: true,
node: true es2017: true,
} node: true
}; },
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
}
-15
View File
@@ -1,15 +0,0 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"overrides": [],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}
+8 -23
View File
@@ -1,24 +1,9 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store .DS_Store
*.suo node_modules
*.ntvs* /build
*.njsproj /.svelte-kit
*.sln /package
*.sw? .env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
-9
View File
@@ -1,12 +1,3 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
+10 -7
View File
@@ -1,9 +1,12 @@
{ {
"useTabs": true, "useTabs": false,
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "tabWidth": 4,
"printWidth": 100, "trailingComma": "none",
"plugins": ["prettier-plugin-svelte"], "arrowParens": "avoid",
"pluginSearchDirs": ["."], "experimentalTernaries": true,
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] "printWidth": 100,
"semi": false,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }
+5 -1
View File
@@ -1,3 +1,7 @@
{ {
"recommendations": ["svelte.svelte-vscode"] "recommendations": [
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
]
} }
+28 -2
View File
@@ -1,3 +1,29 @@
# Controller App # create-svelte
This is the controller for my spot micro Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
## Creating a project
If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
```
## Developing
Once you've created your project, follow these steps:
1: Delete package-lock.json
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
Running `git status` should show:
[![example.png](https://i.postimg.cc/yddM3hH3/example.png)](https://postimg.cc/7CFsp2bq)
You can preview the production build with `npm run preview`.
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
+8
View File
@@ -0,0 +1,8 @@
declare module 'app-env' {
interface ENV {
VITE_USE_HOST_NAME: boolean
}
const appEnv: ENV
export default appEnv
}
-15
View File
@@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"
/>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
BIN
View File
Binary file not shown.
+63 -53
View File
@@ -1,55 +1,65 @@
{ {
"name": "app", "name": "spot_micro_controller",
"private": true, "version": "0.0.1",
"version": "0.0.0", "private": true,
"type": "module", "scripts": {
"scripts": { "dev": "vite dev --host",
"dev": "vite --mode embedded", "build": "vite build",
"dev:mock_embedded": "vite --mode mock_embedded", "build:embedded": "cross-env VITE_USE_HOST_NAME=true vite build",
"dev:mock_web": "vite --mode mock_web", "preview": "vite preview",
"build": "vite build --mode embedded", "test": "pnpm run test:integration && pnpm run test:unit",
"build:mock_web": "vite build --mode mock_web", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"build:web": "vite build --mode web", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"preview": "vite preview", "lint": "prettier --check . && eslint .",
"test": "vitest --environment jsdom", "format": "prettier --write .",
"check": "svelte-check --tsconfig ./tsconfig.json", "test:integration": "playwright test",
"format": "prettier --plugin-search-dir . --write ." "test:unit": "vitest"
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@iconify-json/mdi": "^1.2.3",
"@tsconfig/svelte": "^5.0.2", "@iconify-json/tabler": "^1.2.23",
"@types/three": "^0.160.0", "@playwright/test": "^1.56.0",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@sveltejs/adapter-static": "^3.0.10",
"@typescript-eslint/parser": "^6.20.0", "@sveltejs/kit": "^2.46.4",
"autoprefixer": "^10.4.17", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"cross-env": "^7.0.3", "@types/eslint": "^9.6.1",
"husky": "^9.0.7", "@types/three": "^0.180.0",
"jsdom": "^24.0.0", "@typescript-eslint/eslint-plugin": "^8.46.0",
"lint-staged": "^15.2.0", "@typescript-eslint/parser": "^8.46.0",
"postcss": "^8.4.33", "autoprefixer": "^10.4.21",
"prettier": "3.2.4", "eslint": "^9.37.0",
"svelte": "^4.2.9", "eslint-config-prettier": "^10.1.8",
"svelte-check": "^3.6.3", "eslint-plugin-svelte": "^3.12.4",
"svelte-hero-icons": "^5.0.0", "jsdom": "^27.0.0",
"tailwindcss": "^3.4.1", "prettier": "^3.6.2",
"tslib": "^2.6.2", "prettier-plugin-svelte": "^3.4.0",
"typescript": "^5.3.3", "svelte": "^5.39.11",
"vite": "^5.0.12", "svelte-check": "^4.3.3",
"vite-plugin-compression": "^0.5.1", "svelte-focus-trap": "^1.2.0",
"vite-plugin-singlefile": "^1.0.0", "tailwindcss": "^4.1.14",
"vitest": "^1.3.1" "tslib": "^2.8.1",
}, "typescript": "^5.9.3",
"dependencies": { "unplugin-icons": "^22.4.2",
"nipplejs": "^0.10.1", "vite": "^7.1.9",
"prettier-plugin-svelte": "^3.2.1", "vitest": "^3.2.4"
"svelte-routing": "^2.11.0", },
"three": "^0.160.1", "type": "module",
"urdf-loader": "^0.12.1", "dependencies": {
"uzip": "^0.20201231.0", "@msgpack/msgpack": "^3.1.2",
"xacro-parser": "^0.3.9" "@niku/vite-env-caster": "^1.1.2",
}, "@sveltejs/adapter-auto": "^6.1.1",
"lint-staged": { "@tailwindcss/vite": "^4.1.14",
"*.js": "eslint --cache --fix", "chart.js": "^4.5.0",
"*.{js,css,md,ts,svelte}": "prettier --write" "compare-versions": "^6.1.1",
} "cross-env": "^10.1.0",
"daisyui": "^5.2.0",
"nipplejs": "^0.10.2",
"svelte-dnd-list": "^0.1.8",
"svelte-modals": "^2.0.1",
"three": "^0.180.0",
"urdf-loader": "^0.12.6",
"uzip": "^0.20201231.0",
"xacro-parser": "^0.3.10"
},
"packageManager": "pnpm@9.3.0"
} }
+12
View File
@@ -0,0 +1,12 @@
import type { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
webServer: {
command: 'pnpm run build && pnpm run preview',
port: 4173
},
testDir: 'tests/integration',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
}
export default config
+3264 -2562
View File
File diff suppressed because it is too large Load Diff
-6
View File
@@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
Binary file not shown.
-48
View File
@@ -1,48 +0,0 @@
<script lang="ts">
import { Router, Route } from 'svelte-routing';
import { onMount } from 'svelte';
import TopBar from './components/TopBar.svelte';
import socketService from '$lib/services/socket-service';
import Controller from './routes/Controller.svelte';
import { fileService } from '$lib/services';
import Settings from './routes/Settings.svelte';
import { jointNames, model, outControllerData, mode } from '$lib/stores';
import { loadModelAsync, socketLocation } from '$lib/utilities';
import type { Result } from '$lib/utilities/result';
export let url = window.location.pathname;
onMount(async () => {
socketService.connect(socketLocation);
socketService.addPublisher(outControllerData);
socketService.addPublisher(mode, 'mode');
registerFetchIntercept();
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 });
}
});
const registerFetchIntercept = () => {
const { fetch: originalFetch } = window;
window.fetch = async (...args) => {
const [resource, config] = args;
let file: Result<Uint8Array | undefined, string>;
file = await fileService.getFile(resource.toString());
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
};
};
</script>
<Router {url}>
<TopBar />
<div class="absolute w-full h-full bg-background text-on-background">
<Route path="/" component={Controller} />
<Route path="/settings/*page" component={Settings} />
</div>
</Router>
+41 -13
View File
@@ -1,20 +1,48 @@
:root { @import 'tailwindcss';
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; @plugin "daisyui";
line-height: 1.5;
font-weight: 400;
font-synthesis: none; @plugin "daisyui" {
text-rendering: optimizeLegibility; themes:
-webkit-font-smoothing: antialiased; light --default,
-moz-osx-font-smoothing: grayscale; dark --prefersdark;
-webkit-text-size-adjust: 100%;
} }
body { @plugin "daisyui/theme" {
margin: 0; 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);
}
button {
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
}
#nipple_0_0,
#nipple_1_1 {
z-index: 10 !important;
} }
#three-gui-panel { #three-gui-panel {
top: 50px; top: 64px;
right:0px right: 0px;
}
@media (max-width: 1023px) {
#three-gui-panel {
top: 48px;
}
} }
+13
View File
@@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {}
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo512.png" />
<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%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
-113
View File
@@ -1,113 +0,0 @@
<script lang="ts">
import nipplejs from 'nipplejs';
import { onMount } from 'svelte';
import { capitalize, throttler, toInt8 } from '$lib/utilities';
import { input, outControllerData, mode, modes, type Modes } from '$lib/stores';
import type { vector } from '$lib/models';
import Range from './input/Range.svelte';
import Button from './input/Button.svelte';
let throttle = new throttler();
let left: nipplejs.JoystickManager;
let right: nipplejs.JoystickManager;
let throttle_timing = 40;
let data = new Int8Array($outControllerData.length);
onMount(() => {
left = nipplejs.create({
zone: document.getElementById('left') as HTMLElement,
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] = 1;
data[1] = 0;
data[2] = toInt8($input.left.x, -1, 1);
data[3] = toInt8($input.left.y, -1, 1);
data[4] = toInt8($input.right.x, -1, 1);
data[5] = toInt8($input.right.y, -1, 1);
data[6] = toInt8($input.height, 0, 100);
data[7] = toInt8($input.speed, 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:CustomEvent, key: 'speed' | 'height') => {
const value:number = event.detail
input.update((inputData) => {
inputData[key] = value;
return inputData;
});
throttle.throttle(updateData, throttle_timing);
}
const changeMode = (modeValue: Modes) => {
mode.set(modeValue);
};
</script>
<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 id="left" class="flex w-60 items-center justify-end" />
<div class="flex-1" />
<div id="right" class="flex w-60 items-center" />
</div>
<div class="absolute bottom-0 z-10 p-4 gap-4 flex items-end">
{#each modes as modeValue}
<div>
<Button
on:click={() => changeMode(modeValue)}
active={$mode === modeValue}
>
{capitalize(modeValue)}
</Button>
</div>
{/each}
<div>
{#if $mode === 'walk'}
<Range label="Speed" on:value={(e) => handleRange(e, 'speed')}></Range>
{/if}
<Range label="Height" on:value={(e) => handleRange(e, 'height')}></Range>
</div>
</div>
</div>
<svelte:window on:keyup={handleKeyup} on:keydown={handleKeyup} />
-84
View File
@@ -1,84 +0,0 @@
<script lang="ts">
import socketService from '$lib/services/socket-service';
import { Icon, Bars3, XMark, Power, Battery100, Signal, SignalSlash } from 'svelte-hero-icons';
import { emulateModel } from '$lib/stores';
import { Link, useLocation } from 'svelte-routing';
import { isConnected } from '$lib/stores';
const views = ['Virtual environment', 'Robot camera'];
const modes = ['Drive', 'Choreography'];
const location = useLocation();
let selected_view = views[0];
let selected_modes = modes[0];
let settingOpen = window.location.pathname.includes('/settings');
$: emulateModel.set(selected_view === views[0]);
$: settingOpen = $location.pathname.includes('/settings');
const stop = () => {
if ($isConnected) {
socketService.send(JSON.stringify({ type: 'system/stop' }));
}
};
</script>
<div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800">
<div class="flex gap-2 p-2">
{#if settingOpen}
<Link to="/">
<Icon src={XMark} size="32" />
</Link>
{:else}
<Link to="/settings">
<Icon src={Bars3} size="32" />
</Link>
{/if}
<select
bind:value={selected_modes}
class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"
>
{#each modes as mode}
<option>{mode}</option>
{/each}
</select>
<select
bind:value={selected_view}
class="rounded-md outline outline-2 text-zinc-200 outline-zinc-600 bg-zinc-800"
>
{#each views as view}
<option>{view}</option>
{/each}
</select>
</div>
<div class="flex gap-2 p-2">
<button class="action_button bg-zinc-600">
<Icon src={Power} size="24" />
</button>
<button class="action_button"><Icon src={Battery100} size="24" /></button>
<button class="action_button"
><Icon src={$isConnected ? Signal : SignalSlash} size="24" /></button
>
</div>
<div>
<button class="h-full w-20 bg-red-600 text-white" on:click={stop}>STOP</button>
</div>
</div>
<style>
.topbar {
height: 50px;
}
.action_button {
border-radius: 4px;
width: 34px;
height: 34px;
display: flex;
justify-content: center;
align-items: center;
outline: 1px solid #52525b;
}
</style>
-180
View File
@@ -1,180 +0,0 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { BufferGeometry, CanvasTexture, CircleGeometry, CubicBezierCurve3, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, Vector3, type NormalBufferAttributes } from 'three';
import socketService from '$lib/services/socket-service';
import uzip from 'uzip';
import { model } from '$lib/stores';
import { footColor, isEmbeddedApp, location, toeWorldPositions } from '$lib/utilities';
import { fileService } from '$lib/services';
import { servoAngles, mpu, jointNames } from '$lib/stores';
import SceneBuilder from '$lib/sceneBuilder';
import { lerp, degToRad } from 'three/src/math/MathUtils';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let sceneManager = new SceneBuilder();
let canvas: HTMLCanvasElement, streamCanvas: HTMLCanvasElement, stream: HTMLImageElement;
let context: CanvasRenderingContext2D, texture: CanvasTexture;
let modelAngles: number[] | Int16Array = new Array(12).fill(0);
let modelTargetAngles: number[] | Int16Array = new Array(12).fill(0);
let feet_trace = new Array(4).fill([]);
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
const videoStream = `//${location}/api/stream`;
let showStream = false;
let settings = {
'Trace feet':true,
'Trace points': 30,
'Fix camera on robot': true
}
onMount(async () => {
await cacheModelFiles()
await createScene();
if (!isEmbeddedApp) createPanel();
});
onDestroy(() => {
canvas.remove();
});
const createPanel = () => {
const panel = new GUI({width: 310});
panel.close();
panel.domElement.id = 'three-gui-panel';
const visibility = panel.addFolder('Visualization');
visibility.add(settings, 'Trace feet')
visibility.add(settings, 'Trace points', 1, 1000, 1)
visibility.add(settings, 'Fix camera on robot')
}
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);
socketService.send(
JSON.stringify({
type: 'kinematic/angle',
angle: angle * (180 / Math.PI),
id: $jointNames.indexOf(name)
})
);
};
const createScene = async () => {
sceneManager
.addRenderer({ antialias: true, canvas: canvas, alpha: true })
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
.addOrbitControls(8, 30)
.addSky()
.addGroundPlane()
.addGridHelper({ size: 250, divisions: 125 })
.addAmbientLight({ color: 0xffffff, intensity: 0.7 })
.addDirectionalLight({ x: 10, y: 100, z: 10, color: 0xffffff, intensity: 1 })
.addArrowHelper({ origin: { x: 0, y: 0, z: 0 }, direction: { x: 0, y: -2, z: 0 } })
.addFogExp2(0xcccccc, 0.015)
.addModel($model)
.addDragControl(updateAngles)
.handleResize()
.addRenderCb(render)
.startRenderLoop();
addVideoStream();
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 addVideoStream = () => {
context = streamCanvas.getContext('2d')!;
texture = new CanvasTexture(stream);
const liveStream = new Mesh(
new CircleGeometry(35, 32),
new MeshBasicMaterial({ map: texture })
);
liveStream.position.z = -50;
liveStream.visible = showStream;
sceneManager.scene.add(liveStream);
};
const handleVideoStream = () => {
if (!showStream) return;
context.drawImage(stream, 0, 0);
texture.needsUpdate = true;
};
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 render = () => {
const robot = sceneManager.model;
if (!robot) return;
const toes = toeWorldPositions(robot)
renderTraceLines(toes)
if (settings['Fix camera on robot']) {
sceneManager.controls.target = robot.position.clone()
}
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1);
modelTargetAngles = $servoAngles;
handleVideoStream();
for (let i = 0; i < $jointNames.length; i++) {
modelAngles[i] = lerp(
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
modelTargetAngles[i],
0.1
);
robot.joints[$jointNames[i]].setJointValue(degToRad(modelAngles[i]));
}
};
</script>
<svelte:window on:resize={sceneManager.handleResize} />
{#if showStream}
<img
bind:this={stream}
src={videoStream}
class="hidden"
alt="Live stream is down"
crossorigin="anonymous"
/>
{/if}
<canvas bind:this={streamCanvas} class="hidden"></canvas>
<canvas bind:this={canvas} class="absolute"></canvas>
-19
View File
@@ -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>
-11
View File
@@ -1,11 +0,0 @@
<script lang="ts">
export let active = false
</script>
<button
on:click
class={$$restProps.class + ' rounded-md outline outline-2 text-zinc-200 outline-zinc-600 p-2' +
(active ? ' bg-zinc-600' : '')}
>
<slot/>
</button>
-29
View File
@@ -1,29 +0,0 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
export let value = 50;
export let min = 0;
export let max = 100;
export let label = '';
const dispatchValueInput = () => {
dispatch('value', value)
}
</script>
<div class="">
<input
id="range"
type="range"
{min}
{max}
bind:value
on:change
on:input={dispatchValueInput}
class="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
</div>
<label for="range" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{label}</label
>
@@ -1,86 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { jointNames } from '../../lib/stores';
type Servo = {
id: number;
name: string;
minPWM: number;
maxPWM: number;
pwmFor180: number;
};
let servos: Servo[] = [];
onMount(() => {
jointNames.subscribe((data) => {
servos = data.map((name: string, i: number) => {
return {
id: i,
name,
minPWM: 0,
maxPWM: 0,
pwmFor180: 0
};
});
});
});
let selectedServo: number | null = null;
function updateServoValue(index: number, field: keyof Servo, value: number): void {
servos[index] = { ...servos[index], [field]: value };
}
const formatServo = (servo: Servo) => {
const string = servo.name;
const name = string.charAt(0).toUpperCase() + string.split('_').join(' ').slice(1);
return `${servo.id} ${name}`;
};
</script>
<div>
<div class="servo-selector">
<label for="servo-select">Select Servo:</label>
<select id="servo-select" class="bg-zinc-800" bind:value={selectedServo}>
{#each servos as servo}
<option value={servo.id}>{formatServo(servo)}</option>
{/each}
</select>
</div>
{#if selectedServo !== null}
<div class="mt-5">
<h2>Servo {formatServo(servos[selectedServo])} Calibration</h2>
<label for="minPWM">Min PWM:</label>
<input
type="number"
id="minPWM"
class="bg-zinc-800"
value={servos[selectedServo].minPWM}
on:blur={(event) =>
updateServoValue(selectedServo ?? 0, 'minPWM', Number(event.target?.value))}
/>
<label for="maxPWM">Max PWM:</label>
<input
type="number"
id="maxPWM"
class="bg-zinc-800"
value={servos[selectedServo].maxPWM}
on:blur={(event) =>
updateServoValue(selectedServo ?? 0, 'maxPWM', Number(event.target?.value))}
/>
<label for="pwmFor180">PWM for 180°:</label>
<input
type="number"
id="pwmFor180"
class="bg-zinc-800"
value={servos[selectedServo].pwmFor180}
on:blur={(event) =>
updateServoValue(selectedServo ?? 0, 'pwmFor180', Number(event.target?.value))}
/>
</div>
{/if}
</div>
@@ -1,23 +0,0 @@
<script lang="ts">
import { socketService } from '$lib/services';
import { isConnected, settings } from '$lib/stores';
import { onMount } from 'svelte';
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({ type: 'system/settings' });
socketService.send(message);
}
});
</script>
<div class="w-full h-full">
<div>
{#each Object.entries($settings) as entry}
<div class="flex gap-8">
<div class="w-32">{entry[0]}:</div>
<div>{entry[1]}</div>
</div>
{/each}
</div>
</div>
-28
View File
@@ -1,28 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { humanFileSize } from '$lib/utilities';
import socketService from '$lib/services/socket-service';
import { isConnected, systemInfo } from '$lib/stores';
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({ type: 'system/info' });
socketService.send(message);
}
});
</script>
<div class="w-full h-full">
<div class="w-1/3">
{#each Object.entries($systemInfo ?? {}) as entry}
<div class="flex gap-8">
<div class="w-32">{entry[0]}:</div>
{#if entry[0].includes('Size') || entry[0].includes('Free') || entry[0].includes('Min')}
<div>{humanFileSize(entry[1])}</div>
{:else}
<div>{entry[1]}</div>
{/if}
</div>
{/each}
</div>
</div>
-18
View File
@@ -1,18 +0,0 @@
<script lang="ts">
import socketService from '$lib/services/socket-service';
import { isConnected, logs } from '$lib/stores';
import { onMount } from 'svelte';
onMount(() => {
if ($isConnected) {
const message = JSON.stringify({ type: 'system/logs' });
socketService.send(message);
}
});
</script>
<div class="w-full h-full">
{#each $logs as entry}
<div>{entry}</div>
{/each}
</div>
-35
View File
@@ -1,35 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* @layer base {
:root {
--primary: 98 0 238;
--primary-variant: 55 0 179;
--secondary: 55 0 179;
--secondary-variant: 55 0 179;
--background: 255 255 255;
--surface: 251 251 250;
--error: 176 0 32;
--on-primary: 255 255 255;
--on-secondary: 0 0 0;
--on-background: 0 0 0;
--on-surface: 0 0 0;
--on-error: 255 255 255;
}
:root[class~="dark"] {
--primary: 98 0 238;
--primary-variant: 55 0 179;
--secondary: 55 0 179;
--secondary-variant: 55 0 179;
--background: 30 30 30;
--surface: 36 36 36;
--error: 176 0 32;
--on-primary: 255 255 255;
--on-secondary: 255 255 255;
--on-background: 255 255 255;
--on-surface: 255 255 255;
--on-error: 255 255 255;
}
} */
+79
View File
@@ -0,0 +1,79 @@
import { get } from 'svelte/store'
import { Err, Ok, type Result } from './utilities'
import { apiLocation } from './stores'
export const api = {
get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params)
},
post<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'POST', data)
},
put<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'PUT', data)
},
remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE')
}
}
async function sendRequest<TResponse>(
endpoint: string,
method: string,
data?: unknown,
params?: RequestInit
): Promise<Result<TResponse, Error>> {
endpoint = resolveUrl(endpoint)
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined
const request = {
...params,
method,
body,
headers: {
...params?.headers,
Authorization: 'Basic',
'Content-Type': 'application/json'
}
}
let response
try {
response = await fetch(endpoint, request)
} catch {
return Err.new(new Error(), 'An error has occurred')
}
const isResponseOk = response.status >= 200 && response.status < 400
if (!isResponseOk) {
if (response.status === 401) {
return Err.new(new ApiError(response), 'User was not authorized')
}
return Err.new(new ApiError(response), 'An error has occurred')
}
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type')
if (contentType && contentType.includes('application/json')) {
const data = await response.json()
return Ok.new(data as TResponse)
} else {
// Handle empty object as response
return Ok.new(null as TResponse)
}
}
function resolveUrl(url: string): string {
if (url.startsWith('http') || !get(apiLocation)) return url
const protocol = window.location.protocol
return `${protocol}//${get(apiLocation)}${url.startsWith('/') ? '' : '/'}${url}`
}
export class ApiError extends Error {
constructor(public readonly response: Response) {
super(`${response.status}`)
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+44
View File
@@ -0,0 +1,44 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { Down } from './icons'
function openCollapsible() {
open = !open
if (open) {
opened()
} else {
closed()
}
}
let { icon, title, children, open, opened, closed, class: klass } = $props()
</script>
<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"
>
<span class="inline-flex items-baseline">
{@render icon?.()}
{@render title?.()}
</span>
<button class="btn btn-circle btn-ghost btn-sm" onclick={() => openCollapsible()}>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
open
) ?
'rotate-180'
: ''}"
/>
</button>
</div>
{#if open}
<div
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div>
@@ -0,0 +1,48 @@
<script lang="ts">
import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'
import { Cancel, Check } from '$lib/components/icons'
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals'
let {
isOpen,
title,
message,
onConfirm,
labels = {
cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'OK', icon: Check }
}
}: ModalProps = $props()
</script>
{#if isOpen}
{@const SvelteComponent = labels?.confirm.icon}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
<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"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button
class="btn btn-error inline-flex items-center"
onclick={() => modals.close()}
>
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
</button>
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
</button>
</div>
</div>
</div>
{/if}
@@ -0,0 +1,101 @@
<script lang="ts">
import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'
import { telemetry } from '$lib/stores/telemetry'
import { Cancel } from './icons'
import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals'
// provided by <Modals />
interface Props {
isOpen: boolean
}
let { isOpen }: Props = $props()
let updating = $state(true)
let progress = $state(0)
$effect(() => {
if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress
}
})
$effect(() => {
if ($telemetry.download_ota.status == 'error') {
updating = false
}
})
let message = $state('Preparing ...')
$effect(() => {
if ($telemetry.download_ota.status == 'progress') {
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
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>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
<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"
>
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
<div class="divider my-2"></div>
<div class="overflow-y-auto">
<div class="bg-base-100 flex flex-col items-center justify-center p-6">
{#if $telemetry.download_ota.status == 'progress'}
<progress class="progress progress-primary w-56" value={progress} max="100"
></progress>
{:else}
<progress class="progress progress-primary w-56"></progress>
{/if}
<p class="mt-8 text-2xl">{message}</p>
</div>
</div>
<div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2">
<div class="grow"></div>
<button
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating}
onclick={() => {
modals.closeAll()
location.reload()
}}
>
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
>
</div>
</div>
</div>
{/if}
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'
import { Check } from './icons'
import { exitBeforeEnter, type ModalProps } from 'svelte-modals'
let {
isOpen,
title,
message,
onDismiss,
labels = {
dismiss: { label: 'Dismiss', icon: Check }
}
}: ModalProps = $props()
</script>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
<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"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button
class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}
>
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
</button>
</div>
</div>
</div>
{/if}
@@ -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
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>
@@ -0,0 +1,76 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { Down } from './icons'
interface Props {
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>
{#if collapsible}
<div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div
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?.()}
{@render title?.()}
</span>
<button
class="btn btn-circle btn-ghost btn-sm"
onclick={() => {
open = !open
}}
>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
open
) ?
'rotate-180'
: ''}"
/>
</button>
</div>
{#if open}
<div
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div>
{:else}
<div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div
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?.()}
{@render title?.()}
</span>
{@render right?.()}
</div>
<div class="flex flex-col gap-2 p-4 pt-0">
{@render children?.()}
</div>
</div>
{/if}
+8
View File
@@ -0,0 +1,8 @@
<script lang="ts">
import { Loader } from './icons'
</script>
<div class="flex h-full w-full flex-col items-center justify-center p-6">
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
<p class="text-xl">Loading...</p>
</div>
+47
View File
@@ -0,0 +1,47 @@
<script lang="ts">
import type { ComponentType } from 'svelte'
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
const {
icon,
title,
description = '',
variant = 'primary',
class: klass = '',
children = null
} = $props<{
icon?: ComponentType
title: string
description?: string | number
variant?: Variant
class?: string
children?: () => ComponentType
}>()
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>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import { onDestroy } from 'svelte'
import { apiLocation } from '$lib/stores'
let source = $state(`${$apiLocation}/api/camera/stream`)
onDestroy(() => (source = '#'))
</script>
<div class="w-full h-full">
<img
src={source}
class="absolute object-cover blur-3xl w-full h-full -z-10"
alt="Live stream is down"
/>
<img src={source} class="object-contain w-full h-full" alt="Live stream is down" />
</div>
+37
View File
@@ -0,0 +1,37 @@
<script>
import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from './icons'
/** @type {{theme?: any, icon?: any}} */
let {
theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
},
icon = {
error: error,
success: success,
warning: warning,
info: info
}
} = $props()
</script>
<div class="toast toast-end mr-4">
{#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div
animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }}
>
<SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
</div>
+340
View File
@@ -0,0 +1,340 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import {
Mesh,
MeshBasicMaterial,
type Object3D,
SphereGeometry,
Vector3,
type Object3DEventMap,
Color
} from 'three'
import {
ModesEnum,
kinematicData,
mode,
model,
outControllerData,
servoAnglesOut,
servoAngles,
mpu,
jointNames,
currentKinematic,
walkGait,
walkGaitToMode
} from '$lib/stores'
import { 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 { type body_state_t } from '$lib/kinematic'
import { BezierState, CalibrationState, 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'
interface Props {
defaultColor?: string | null
orbit?: boolean
panel?: boolean
debug?: boolean
ground?: boolean
}
let {
defaultColor = '#0091ff',
orbit = false,
panel = true,
debug = false,
ground = true
}: Props = $props()
let sceneManager = $state(new SceneBuilder())
let canvas: HTMLCanvasElement
let currentModelAngles: number[] = new Array(12).fill(0)
let modelTargetAngles: number[] = new Array(12).fill(0)
let gui_panel: GUI
let Throttler = new throttler()
let target: Object3D<Object3DEventMap>
let target_position = { x: 0, z: 0, yaw: 0 }
let kinematic = get(currentKinematic)
let planners = {
[ModesEnum.Deactivated]: new IdleState(),
[ModesEnum.Idle]: new IdleState(),
[ModesEnum.Calibration]: new CalibrationState(),
[ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(),
[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(),
cumulative_x: 0,
cumulative_y: 0,
cumulative_z: 0,
cumulative_roll: 0,
cumulative_pitch: 0,
cumulative_yaw: 0
}
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: defaultColor
}
onMount(async () => {
await populateModelCache()
await createScene()
servoAngles.subscribe(updateAnglesFromStore)
walkGait.subscribe(gait => planners[ModesEnum.Walk].set_mode(walkGaitToMode(gait)))
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').onChange(setSceneBackground).listen()
}
const updateKinematicPosition = () => {
kinematicData.set([
settings.omega,
settings.phi,
settings.psi,
settings.xm,
settings.ym,
settings.zm
])
}
const setSceneBackground = (c: string | null) => (sceneManager.scene.background = new Color(c!))
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(2, 20, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
.addFogExp2(0xcccccc, 0.015)
.addModel($model as URDFRobot)
.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(angles => {
Object.entries(angles).forEach(([name, angle]) => {
updateAngles(name, angle)
})
})
}
if (defaultColor) setSceneBackground(settings['Background'] || defaultColor)
}
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,
cumulative_x: body_state.cumulative_x,
cumulative_y: body_state.cumulative_y,
cumulative_z: body_state.cumulative_z,
cumulative_roll: body_state.cumulative_roll,
cumulative_pitch: body_state.cumulative_pitch,
cumulative_yaw: body_state.cumulative_yaw
}
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))
const cumulativeYaw = body_state.cumulative_yaw
const cosYaw = Math.cos(cumulativeYaw)
const sinYaw = Math.sin(cumulativeYaw)
const rotatedXm = settings.xm * cosYaw - settings.zm * sinYaw
const rotatedZm = settings.xm * sinYaw + settings.zm * cosYaw
robot.position.x = smooth(robot.position.x, -rotatedZm - body_state.cumulative_z * 1.2, 0.1)
robot.position.z = smooth(robot.position.z, -rotatedXm - body_state.cumulative_x * 1.2, 0.1)
const pitch = degToRad(settings.psi - 90) + body_state.cumulative_pitch
const roll = degToRad(settings.omega) + body_state.cumulative_roll
robot.rotation.z = smooth(
robot.rotation.z,
degToRad(-settings.phi + $mpu.heading + 90) + cumulativeYaw,
0.1
)
robot.rotation.y = smooth(robot.rotation.y, roll, 0.1)
robot.rotation.x = smooth(robot.rotation.x, pitch, 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 = {
lx: controlData[0],
ly: controlData[1],
rx: controlData[2],
ry: controlData[3],
h: controlData[4],
s: controlData[5],
s1: controlData[6]
}
body_state.ym = data.h
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)
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>
<svelte:window onresize={sceneManager.fillParent} />
<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: Event) => (value = (e.target as HTMLInputElement).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,35 @@
<script lang="ts">
interface Props {
min?: number
max?: number
step?: number
value?: number
oninput?: (value: number) => void
}
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,41 @@
<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: { url: string; version: string; active?: boolean; href?: string }
}
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,15 @@
<script>
import logo from '$lib/assets/logo512.png'
import { resolve } from '$app/paths'
/** @type {{appName: any}} */
let { appName } = $props()
</script>
<a
href={resolve('/')}
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>
+200
View File
@@ -0,0 +1,200 @@
<script lang="ts">
import { page } from '$app/state'
import { base } from '$app/paths'
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 { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
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 }
import type { ComponentType } from 'svelte'
type menuItem = {
title: string
icon: ComponentType
href?: string
feature: boolean
active?: boolean
submenu?: menuItem[]
}
function withBase(path: string) {
return `${base}${path.startsWith('/') ? path : '/' + path}`
}
let menuItems = $state<menuItem[]>([])
$effect(() => {
menuItems = [
{
title: 'Connection',
icon: WiFi,
href: withBase('/connection'),
feature: !PUBLIC_VITE_USE_HOST_NAME
},
{
title: 'Controller',
icon: MdiController,
href: withBase('/controller'),
feature: true
},
{
title: 'Peripherals',
icon: Devices,
feature: true,
submenu: [
{
title: 'I2C',
icon: Connection,
href: withBase('/peripherals/i2c'),
feature: true
},
{
title: 'Camera',
icon: Camera,
href: withBase('/peripherals/camera'),
feature: $features.camera
},
{
title: 'Servo',
icon: MotorOutline,
href: withBase('/peripherals/servo'),
feature: true
},
{
title: 'IMU',
icon: Rotate3d,
href: withBase('/peripherals/imu'),
feature: $features.imu || $features.mag || $features.bmp
}
]
},
{
title: 'WiFi',
icon: WiFi,
feature: true,
submenu: [
{
title: 'WiFi Station',
icon: Router,
href: withBase('/wifi/sta'),
feature: true
},
{
title: 'Access Point',
icon: AP,
href: withBase('/wifi/ap'),
feature: true
},
{
title: 'mDNS',
icon: DNS,
href: withBase('/wifi/mdns'),
feature: true
}
]
},
{
title: 'System',
icon: Settings,
feature: true,
submenu: [
{
title: 'System Status',
icon: Health,
href: withBase('/system/status'),
feature: true
},
{
title: 'File System',
icon: Folder,
href: withBase('/system/filesystem'),
feature: true
},
{
title: 'System Metrics',
icon: Metrics,
href: withBase('/system/metrics'),
feature: true
},
{
title: 'Firmware Update',
icon: Update,
href: withBase('/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: CustomEvent) => {
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,56 @@
<script lang="ts">
import MenuList from './MenuList.svelte'
import type { ComponentType } from 'svelte'
type MenuItem = {
title: string
icon: ComponentType
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 (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,9 @@
<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>
@@ -0,0 +1,18 @@
<script lang="ts">
import { Hamburger } from '../icons'
import { resolve } from '$app/paths'
</script>
<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">
<a href={resolve('/')}>
<Hamburger class="h-8 w-8" />
</a>
</div>
</div>
<style>
.topbar {
height: 50px;
}
</style>
@@ -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 as string) === 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 as string)
) {
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>
@@ -0,0 +1,37 @@
<script>
import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from '../icons'
/** @type {{theme?: any, icon?: any}} */
let {
theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
},
icon = {
error: error,
success: success,
warning: warning,
info: info
}
} = $props()
</script>
<div class="toast toast-end mr-4 z-20">
{#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div
animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }}
>
<SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
</div>
@@ -0,0 +1,42 @@
import { writable } from 'svelte/store'
type StateType = 'info' | 'success' | 'warning' | 'error'
type State = {
id: string
type: StateType
message: string
}
function createNotificationStore() {
const state: State[] = []
const notifications = writable(state)
const { subscribe } = notifications
function send(message: string, type: StateType = 'info', timeout: number) {
const id = generateId()
setTimeout(() => {
notifications.update(state => {
return state.filter(n => n.id !== id)
})
}, timeout)
notifications.update(state => {
return [...state, { id, type, message }]
})
}
return {
subscribe,
send,
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
}
}
function generateId() {
return '_' + Math.random().toString(36).substr(2, 9)
}
export const notifications = createNotificationStore()
@@ -0,0 +1,102 @@
<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
let chart: Chart<'line', number[], number>
interface Props {
label: string
data: number[]
title: string
}
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]: unknown
}
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>
+493
View File
@@ -0,0 +1,493 @@
import { get } from 'svelte/store'
import type { body_state_t } from './kinematic'
import { currentKinematic } from './stores/featureFlags'
export interface gait_state_t {
step_height: number
step_x: number
step_z: number
step_angle: number
step_velocity: number
step_depth: number
}
export interface ControllerCommand {
lx: number
ly: number
rx: number
ry: number
h: number
s: number
s1: number
}
export abstract class GaitState {
protected abstract name: string
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() {
return get(currentKinematic).getDefaultFeetPos()
}
protected get default_height() {
return 0.5
}
begin() {
console.log('Starting', this.name)
}
end() {
console.log('Ending', this.name)
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
this.map_command(command)
this.body_state = body_state
this.dt = dt / 1000
if (body_state.cumulative_x === undefined) {
body_state.cumulative_x = 0
body_state.cumulative_y = 0
body_state.cumulative_z = 0
body_state.cumulative_roll = 0
body_state.cumulative_pitch = 0
body_state.cumulative_yaw = 0
}
return body_state
}
map_command(command: ControllerCommand) {
const newCommand = {
step_height: 0.4 + (command.s1 + 1) / 2,
step_x: command.ly,
step_z: -command.lx,
step_velocity: command.s,
step_angle: command.rx,
step_depth: 0.002
}
this.gait_state = newCommand
}
}
export class IdleState extends GaitState {
protected name = 'Idle'
step(body_state: body_state_t, command: ControllerCommand) {
super.step(body_state, command)
return body_state
}
}
export class CalibrationState extends GaitState {
protected name = 'Calibration'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command)
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 {
protected name = 'Rest'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command)
body_state.omega = 0
body_state.phi = 0
body_state.psi = 0
body_state.xm = 0
body_state.ym = this.default_height / 2
body_state.zm = 0
body_state.feet = this.default_feet_pos
return body_state
}
}
export class StandState extends GaitState {
protected name = 'Stand'
step(body_state: body_state_t, command: ControllerCommand) {
super.step(body_state, command)
body_state.omega = 0
body_state.phi = command.rx * 10 * (Math.PI / 2)
body_state.psi = command.ry * 10 * (Math.PI / 2)
body_state.xm = command.ly / 4
body_state.zm = command.lx / 4
body_state.feet = this.default_feet_pos
return body_state
}
}
export class BezierState extends GaitState {
protected name = 'Bezier'
protected phase = 0
protected phase_num = 0
protected step_length = 0
protected stand_offset = 0.75
protected mode: 'crawl' | 'trot' = 'trot'
protected speed_factor = 1
offset = [0, 0.5, 0.75, 0.25]
protected shift_start_pos = { x: 0, z: 0 }
protected shift_target_pos = { x: 0, z: 0 }
protected shift_start_time = 0
protected current_shift_leg = -1
protected last_body_state: body_state_t | null = null
protected cumulative_position = { x: 0, y: 0, z: 0 }
protected cumulative_orientation = { roll: 0, pitch: 0, yaw: 0 }
constructor() {
super()
this.set_mode(this.mode)
}
begin() {
super.begin()
}
set_mode(mode: 'crawl' | 'trot', duty?: number, order?: [number, number, number, number]) {
console.log('BezierState set_mode', mode)
this.mode = mode
if (mode === 'crawl') {
this.speed_factor = 0.5
this.stand_offset = duty ?? 0.85
const o = order ?? [3, 0, 2, 1]
const base = [0, 0.25, 0.5, 0.75]
const offsets = new Array(4).fill(0)
for (let i = 0; i < 4; i++) offsets[o[i]] = base[i]
this.offset = offsets
} else {
this.speed_factor = 2
this.stand_offset = duty ?? 0.6
this.offset = order ? (order.map(v => v % 1) as number[]) : [0, 0.5, 0.5, 0]
}
}
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_body_position()
this.update_feet_positions()
this.update_cumulative_position()
return this.body_state
}
update_phase() {
const m = this.gait_state
if (m.step_x === 0 && m.step_z === 0 && m.step_angle === 0) {
this.phase = 0
return
}
this.phase += this.dt * m.step_velocity * this.speed_factor
if (this.phase >= 1) {
this.phase_num = (this.phase_num + 1) % 2
this.phase = 0
}
}
update_body_position() {
const m = this.gait_state
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
if (!moving) return
if (this.mode !== 'crawl') return
const { stance, swing, next_swing, time_to_lift } = this.get_leg_states()
if (stance.length >= 3 && swing.length === 0 && next_swing !== -1) {
if (this.current_shift_leg !== next_swing) {
this.current_shift_leg = next_swing
this.shift_start_pos.x = this.body_state.xm
this.shift_start_pos.z = this.body_state.zm
const remaining_legs = stance.filter(leg => leg !== next_swing)
const target = this.stance_centroid(remaining_legs)
this.shift_target_pos.x = target[0]
this.shift_target_pos.z = target[2]
this.shift_start_time = time_to_lift
}
const total_time = this.shift_start_time
const progress = total_time > 0 ? 1 - time_to_lift / total_time : 1
const smooth_progress = this.smoothstep01(Math.max(0, Math.min(1, progress)))
this.body_state.xm = this.lerp(
this.shift_start_pos.x,
this.shift_target_pos.x,
smooth_progress
)
this.body_state.zm = this.lerp(
this.shift_start_pos.z,
this.shift_target_pos.z,
smooth_progress
)
}
}
protected lerp(a: number, b: number, t: number): number {
return a + (b - a) * t
}
protected stance_centroid(legs: number[]): number[] {
if (legs.length === 0) return [this.body_state.xm, 0, this.body_state.zm]
let sx = 0,
sz = 0
for (const i of legs) {
sx += this.body_state.feet[i][0]
sz += this.body_state.feet[i][2]
}
return [sx / legs.length, 0, sz / legs.length]
}
protected get_leg_states(): {
stance: number[]
swing: number[]
next_swing: number
time_to_lift: number
} {
const stance: number[] = []
const swing: number[] = []
let next_swing = -1
let min_time_to_swing = Infinity
for (let i = 0; i < 4; i++) {
let phase = this.phase + this.offset[i]
if (phase >= 1) phase -= 1
if (phase <= this.stand_offset) {
stance.push(i)
const time_to_swing = this.stand_offset - phase
if (time_to_swing < min_time_to_swing) {
min_time_to_swing = time_to_swing
next_swing = i
}
} else {
swing.push(i)
}
}
return { stance, swing, next_swing, time_to_lift: min_time_to_swing }
}
protected smoothstep01(t: number): number {
const x = Math.max(0, Math.min(1, t))
return x * x * (3 - 2 * x)
}
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 <= this.stand_offset ?
this.stand_controller(index, phase / this.stand_offset)
: this.swing_controller(index, (phase - this.stand_offset) / (1 - this.stand_offset))
}
stand_controller(index: number, phase: number) {
const depth = this.gait_state.step_depth
return this.controller(index, phase, stance_curve, depth)
}
swing_controller(index: number, phase: number) {
const 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]
}
update_cumulative_position() {
if (this.last_body_state === null) {
this.last_body_state = { ...this.body_state }
this.body_state.cumulative_x = 0
this.body_state.cumulative_y = 0
this.body_state.cumulative_z = 0
this.body_state.cumulative_roll = 0
this.body_state.cumulative_pitch = 0
this.body_state.cumulative_yaw = 0
return
}
const m = this.gait_state
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
if (moving) {
const step_displacement_x_local =
m.step_x * m.step_velocity * this.dt * this.speed_factor
const step_displacement_z_local =
m.step_z * m.step_velocity * this.dt * this.speed_factor
const step_displacement_yaw =
m.step_angle * m.step_velocity * this.dt * this.speed_factor
const cos_yaw = Math.cos(this.cumulative_orientation.yaw)
const sin_yaw = Math.sin(this.cumulative_orientation.yaw)
const step_displacement_x =
step_displacement_x_local * cos_yaw - step_displacement_z_local * sin_yaw
const step_displacement_z =
step_displacement_x_local * sin_yaw + step_displacement_z_local * cos_yaw
this.cumulative_position.x += step_displacement_x
this.cumulative_position.z += step_displacement_z
this.cumulative_orientation.yaw += step_displacement_yaw
}
this.body_state.cumulative_x = this.cumulative_position.x
this.body_state.cumulative_y = this.cumulative_position.y
this.body_state.cumulative_z = this.cumulative_position.z
this.body_state.cumulative_roll = this.cumulative_orientation.roll
this.body_state.cumulative_pitch = this.cumulative_orientation.pitch
this.body_state.cumulative_yaw = this.cumulative_orientation.yaw
this.last_body_state = { ...this.body_state }
}
}
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
}
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+151 -391
View File
@@ -1,393 +1,153 @@
export interface body_state_t {
omega: number
phi: number
psi: number
xm: number
ym: number
zm: number
feet: number[][]
cumulative_x: number
cumulative_y: number
cumulative_z: number
cumulative_roll: number
cumulative_pitch: number
cumulative_yaw: number
}
export interface position {
x: number
y: number
z: number
}
export interface target_position {
x: number
z: number
yaw: number
}
export interface KinematicParams {
coxa: number
coxa_offset: number
femur: number
tibia: number
L: number
W: number
}
const { cos, sin, atan2, acos, sqrt, max, min } = Math
const DEG2RAD = 0.017453292519943
export default class Kinematic { export default class Kinematic {
private l1: number; coxa: number
private l2: number; coxa_offset: number
private l3: number; femur: number
private l4: number; tibia: number
private L: number; L: number
private W: number; W: number
constructor() { DEG2RAD = DEG2RAD
this.l1 = 50;
this.l2 = 20; mountOffsets: number[][]
this.l3 = 120;
this.l4 = 155; invMountRot = [
[0, 0, -1],
this.L = 140; [0, 1, 0],
this.W = 75; [1, 0, 0]
} ]
bodyIK( constructor(params: KinematicParams) {
omega: number, this.coxa = params.coxa
phi: number, this.coxa_offset = params.coxa_offset
psi: number, this.femur = params.femur
xm: number, this.tibia = params.tibia
ym: number, this.L = params.L
zm: number this.W = params.W
): number[][][] {
const { cos, sin } = Math; this.mountOffsets = [
[this.L / 2, 0, this.W / 2],
const Rx: number[][] = [ [this.L / 2, 0, -this.W / 2],
[1, 0, 0, 0], [-this.L / 2, 0, this.W / 2],
[0, cos(omega), -sin(omega), 0], [-this.L / 2, 0, -this.W / 2]
[0, sin(omega), cos(omega), 0], ]
[0, 0, 0, 1] }
];
const Ry: number[][] = [ getDefaultFeetPos(): number[][] {
[cos(phi), 0, sin(phi), 0], return this.mountOffsets.map((offset, i) => {
[0, 1, 0, 0], return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
[-sin(phi), 0, cos(phi), 0], })
[0, 0, 0, 1] }
];
const Rz: number[][] = [ calcIK(p: body_state_t): number[] {
[cos(psi), -sin(psi), 0, 0], const roll = p.omega * this.DEG2RAD
[sin(psi), cos(psi), 0, 0], const pitch = p.phi * this.DEG2RAD
[0, 0, 1, 0], const yaw = p.psi * this.DEG2RAD
[0, 0, 0, 1] const rot = this.euler2R(roll, pitch, yaw)
]; const inv_rot = [
const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz); [rot[0][0], rot[1][0], rot[2][0]],
[rot[0][1], rot[1][1], rot[2][1]],
const T: number[][] = [ [rot[0][2], rot[1][2], rot[2][2]]
[0, 0, 0, xm], ]
[0, 0, 0, ym], const inv_trans = [
[0, 0, 0, zm], -inv_rot[0][0] * p.xm - inv_rot[0][1] * p.ym - inv_rot[0][2] * p.zm,
[0, 0, 0, 0] -inv_rot[1][0] * p.xm - inv_rot[1][1] * p.ym - inv_rot[1][2] * p.zm,
]; -inv_rot[2][0] * p.xm - inv_rot[2][1] * p.ym - inv_rot[2][2] * p.zm
const Tm: number[][] = this.matrixAdd(T, Rxyz); ]
return p.feet.flatMap((foot, i) => {
const sHp = sin(Math.PI / 2); const [wx, wy, wz] = foot
const cHp = cos(Math.PI / 2); const bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0]
const L = this.L; const by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1]
const W = this.W; const bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2]
return [ const [mx, my, mz] = this.mountOffsets[i]
this.matrixMultiply(Tm, [ const px = bx - mx,
[cHp, 0, sHp, L / 2], py = by - my,
[0, 1, 0, 0], pz = bz - mz
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1] const lx =
]), this.invMountRot[0][0] * px +
this.matrixMultiply(Tm, [ this.invMountRot[0][1] * py +
[cHp, 0, sHp, L / 2], this.invMountRot[0][2] * pz
[0, 1, 0, 0], const ly =
[-sHp, 0, cHp, -W / 2], this.invMountRot[1][0] * px +
[0, 0, 0, 1] this.invMountRot[1][1] * py +
]), this.invMountRot[1][2] * pz
this.matrixMultiply(Tm, [ const lz =
[cHp, 0, sHp, -L / 2], this.invMountRot[2][0] * px +
[0, 1, 0, 0], this.invMountRot[2][1] * py +
[-sHp, 0, cHp, W / 2], this.invMountRot[2][2] * pz
[0, 0, 0, 1]
]), const xLocal = i % 2 === 1 ? -lx : lx
this.matrixMultiply(Tm, [ return this.legIK(xLocal, ly, lz)
[cHp, 0, sHp, -L / 2], })
[0, 1, 0, 0], }
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1] private legIK(x: number, y: number, z: number): [number, number, number] {
]) const F = sqrt(max(0, x * x + y * y - this.coxa * this.coxa))
]; const G = F - this.coxa_offset
} const H = sqrt(G * G + z * z)
const t1 = -atan2(y, x) - atan2(F, -this.coxa)
private legIK(point: number[]): number[] { const D =
const [x, y, z] = point; (H * H - this.femur * this.femur - this.tibia * this.tibia) /
const { atan2, cos, sin, sqrt, acos } = Math; (2 * this.femur * this.tibia)
const { l1, l2, l3, l4 } = this; const t3 = acos(max(-1, min(1, D)))
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
let F; return [t1, t2, t3]
}
try {
F = sqrt(x ** 2 + y ** 2 - l1 ** 2); private euler2R(roll: number, pitch: number, yaw: number): number[][] {
if (isNaN(F)) throw new Error('F is NaN'); const cr = cos(roll),
} catch (error) { sr = sin(roll)
//console.log(error) const cp = cos(pitch),
F = l1; sp = sin(pitch)
} const cy = cos(yaw),
const G = F - l2; sy = sin(yaw)
const H = sqrt(G ** 2 + z ** 2); return [
[cp * cy, -cp * sy, sp],
const theta1 = -atan2(y, x) - atan2(F, -l1); [sr * sp * cy + sy * cr, -sr * sp * sy + cr * cy, -sr * cp],
const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4); [sr * sy - sp * cr * cy, sr * cy + sp * sy * cr, cr * cp]
let theta3: number; ]
try { }
theta3 = acos(D);
if (isNaN(theta3)) throw new Error('theta3 is NaN');
} catch (error) {
theta3 = 0;
}
const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + 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 matrixAdd(a: number[][], b: number[][]): number[][] {
const result: number[][] = [];
for (let i = 0; i < a.length; i++) {
const row: number[] = [];
for (let j = 0; j < a[i].length; j++) {
row.push(a[i][j] + b[i][j]);
}
result.push(row);
}
return result;
}
public calcLegPoints(angles: number[]): number[][] {
const [theta1, theta2, theta3] = angles;
const theta23 = theta2 + theta3;
const T0: number[] = [0, 0, 0, 1];
const T1: number[] = this.vectorAdd(T0, [
-this.l1 * Math.cos(theta1),
this.l1 * Math.sin(theta1),
0,
0
]);
const T2: number[] = this.vectorAdd(T1, [
-this.l2 * Math.sin(theta1),
-this.l2 * Math.cos(theta1),
0,
0
]);
const T3: number[] = this.vectorAdd(T2, [
-this.l3 * Math.sin(theta1) * Math.cos(theta2),
-this.l3 * Math.cos(theta1) * Math.cos(theta2),
this.l3 * Math.sin(theta2),
0
]);
const T4: number[] = this.vectorAdd(T3, [
-this.l4 * Math.sin(theta1) * Math.cos(theta23),
-this.l4 * Math.cos(theta1) * Math.cos(theta23),
this.l4 * Math.sin(theta23),
0
]);
return [T0, T1, T2, T3, T4];
}
public calcIK(Lp: number[][], angles: number[], center: number[]): number[][] {
const [omega, phi, psi] = angles;
const [xm, ym, zm] = center;
const [Tlf, Trf, Tlb, Trb] = this.bodyIK(omega, phi, psi, xm, ym, zm);
const Ix: number[][] = [
[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
return [
this.legIK(this.multiplyVector(this.matrixInverse(Tlf), Lp[0])),
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trf), Lp[1]))),
this.legIK(this.multiplyVector(this.matrixInverse(Tlb), Lp[2])),
this.legIK(this.multiplyVector(Ix, this.multiplyVector(this.matrixInverse(Trb), Lp[3])))
];
}
private vectorAdd(a: number[], b: number[]): number[] {
return a.map((val, index) => val + b[index]);
}
private matrixInverse(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;
}
}
export class ForwardKinematics {
private l1: number;
private l2: number;
private l3: number;
private l4: number;
constructor() {
this.l1 = 50;
this.l2 = 20;
this.l3 = 120;
this.l4 = 155;
}
public calculateFootpoint(theta1: number, theta2: number, theta3: number): number[] {
const { cos, sin } = Math;
const x =
this.l1 * cos(theta1) +
this.l2 * cos(theta1) +
this.l3 * cos(theta1 + theta2) +
this.l4 * cos(theta1 + theta2 + theta3);
const y =
this.l1 * sin(theta1) +
this.l2 * sin(theta1) +
this.l3 * sin(theta1 + theta2) +
this.l4 * sin(theta1 + theta2 + theta3);
const z = 0;
return [x, y, z];
}
public calculateFootpoints(angles: number[]): number[][] {
const footpoints: number[][] = [];
for (let i = 0; i < angles.length; i += 3) {
const theta1 = angles[i];
const theta2 = angles[i + 1];
const theta3 = angles[i + 2];
const footpoint = this.calculateFootpoint(theta1, theta2, theta3);
footpoints.push(footpoint);
}
return footpoints;
}
} }
-22
View File
@@ -1,22 +0,0 @@
export type vector = { x: number; y: number };
export interface ControllerInput {
left: vector;
right: vector;
height: number;
speed: number;
}
export type angles = number[] | Int16Array;
export type AnglesData = {
type: 'angles';
data: angles;
};
export type LogData = {
type: 'log';
data: string;
};
export type WebSocketJsonMsg = AnglesData | LogData;
+308 -279
View File
@@ -1,319 +1,348 @@
import { import {
Mesh, Mesh,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Scene, Scene,
ShadowMaterial, WebGLRenderer,
WebGLRenderer, AmbientLight,
AmbientLight, DirectionalLight,
DirectionalLight, PCFSoftShadowMap,
PCFSoftShadowMap, type GridHelper,
GridHelper, ArrowHelper,
ArrowHelper, Vector3,
Vector3, FogExp2,
LoaderUtils, CanvasTexture,
Object3D, type ColorRepresentation,
FogExp2, type WebGLRendererParameters,
CanvasTexture, MeshPhongMaterial,
type ColorRepresentation, EquirectangularReflectionMapping,
type WebGLRendererParameters, ACESFilmicToneMapping,
MeshPhongMaterial, Group,
EquirectangularReflectionMapping, MeshBasicMaterial,
ACESFilmicToneMapping, RepeatWrapping,
MathUtils Object3D
} from 'three'; } from 'three'
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 { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'; import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'; import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
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;
function calculateCurrentSunElevation() {
let now = new Date();
let decimalTime = now.getHours() + now.getMinutes() / 60;
let normalizedTime = (decimalTime % 12) / 6 - 1;
return 10 * Math.sin(normalizedTime * Math.PI);
}
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 controls: OrbitControls; public orbit: OrbitControls
public callback: Function; public callback: (() => void) | 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
highlightMaterial: any; public isDragging: boolean = false
transformControl: TransformControls
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
document.body.appendChild(this.renderer.domElement); if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement)
return this; return this
}; }
public addSky = () => { public addPerspectiveCamera = (options: position) => {
const sky = new Sky(); this.camera = new PerspectiveCamera()
sky.scale.setScalar(450000); this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0)
this.scene.add(sky); this.scene.add(this.camera)
const effectController = { return this
turbidity: 10, }
rayleigh: 3,
mieCoefficient: 0.005,
mieDirectionalG: 0.7,
elevation: calculateCurrentSunElevation(),
azimuth: 180,
exposure: this.renderer.toneMappingExposure
};
const uniforms = sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient;
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
this.renderer.toneMappingExposure = 0.5;
const phi = MathUtils.degToRad(90 - effectController.elevation);
const theta = MathUtils.degToRad(effectController.azimuth);
const sun = new Vector3();
sun.setFromSphericalCoords(1, phi, theta); public addGroundPlane = (options?: position) => {
uniforms['sunPosition'].value.copy(sun); const checkerboardTexture = this.createCheckerboardTexture(1024, 2)
return this; checkerboardTexture.wrapS = RepeatWrapping
}; checkerboardTexture.wrapT = RepeatWrapping
checkerboardTexture.repeat.set(100, 100)
const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture,
opacity: 0.1,
transparent: true
})
public addPerspectiveCamera = (options: position) => { const plane = new PlaneGeometry(400, 400)
this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(this.camera);
return this;
};
public addGroundPlane = (options?: position) => { this.ground = new Mesh(plane, checkerboardMat)
this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 })); this.ground.rotation.x = -Math.PI / 2
this.ground.rotation.x = -Math.PI / 2; this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0)
this.ground.scale.setScalar(30); this.ground.receiveShadow = true
this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0); this.scene.add(this.ground)
this.ground.receiveShadow = true;
this.scene.add(this.ground);
return this;
};
public addOrbitControls = (minDistance: number, maxDistance: number) => { const mirror = new Reflector(plane, {
this.controls = new OrbitControls(this.camera, this.renderer.domElement); clipBias: 0.003,
this.controls.minDistance = minDistance; textureWidth: window.innerWidth * window.devicePixelRatio,
this.controls.maxDistance = maxDistance; textureHeight: window.innerHeight * window.devicePixelRatio,
this.controls.update(); color: 0x00bfff
return this; })
}; mirror.rotateX(-Math.PI / 2)
this.scene.add(mirror)
public addAmbientLight = (options: light) => { return this
const ambientLight = new AmbientLight(options.color, options.intensity); }
this.scene.add(ambientLight);
return this;
};
public addDirectionalLight = (options: directionalLight) => { public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
const directionalLight = new DirectionalLight(options.color, options.intensity); this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
directionalLight.castShadow = true; this.orbit.minDistance = minDistance + (maxDistance - minDistance) / 2
directionalLight.shadow.mapSize.setScalar(2048); this.orbit.maxDistance = maxDistance
directionalLight.shadow.mapSize.width = 1024; this.orbit.autoRotate = autoRotate
directionalLight.shadow.mapSize.height = 1024; this.orbit.update()
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.orbit.minDistance = minDistance
directionalLight.shadow.radius = 5; return this
this.scene.add(directionalLight); }
return this;
};
public addGridHelper = (options: gridHelperOptions) => { public addAmbientLight = (options: light) => {
this.gridHelper = new GridHelper(options.size, options.divisions); const ambientLight = new AmbientLight(options.color, options.intensity)
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.scene.add(ambientLight)
this.gridHelper.material.opacity = 0.2; return this
this.gridHelper.material.depthWrite = false; }
this.gridHelper.material.transparent = true;
this.scene.add(this.gridHelper);
return this;
};
public addFogExp2 = (color: ColorRepresentation, density?: number) => { public addDirectionalLight = (options: directionalLight) => {
this.scene.fog = new FogExp2(color, density); const directionalLight = new DirectionalLight(options.color, options.intensity)
return this; directionalLight.castShadow = true
}; directionalLight.shadow.camera.top = 10
directionalLight.shadow.camera.bottom = -10
directionalLight.shadow.camera.right = 10
directionalLight.shadow.camera.left = -10
directionalLight.shadow.mapSize.set(4096, 4096)
public handleResize = () => { directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0)
this.renderer.setSize(window.innerWidth, window.innerHeight); this.scene.add(directionalLight)
this.renderer.setPixelRatio(window.devicePixelRatio); return this
this.camera.aspect = window.innerWidth / window.innerHeight; }
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.controls.update();
this.renderer.render(this.scene, this.camera);
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 addModel = (model: any) => { public addRenderCb = (callback: () => void) => {
this.model = model; this.callback = callback
this.scene.add(model); return this
return this; }
};
public addDragControl = (updateAngle: any) => { public startRenderLoop = () => {
const highlightColor = '#FFFFFF'; this.renderer.setAnimationLoop(() => {
const highlightMaterial = new MeshPhongMaterial({ this.renderer.render(this.scene, this.camera)
shininess: 10, this.orbit.update()
color: highlightColor, this.handleRobotShadow()
emissive: highlightColor, if (this.callback) this.callback()
emissiveIntensity: 0.25 if (!this.liveStreamTexture) return
}); })
return this
}
const dragControls = new PointerURDFDragControls( public addArrowHelper = (options?: arrowOptions) => {
this.scene, const dir = new Vector3(
this.camera, options?.direction.x ?? 0,
this.renderer.domElement options?.direction.y ?? 0,
); options?.direction.z ?? 0
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { )
this.setJointValue(joint.name, angle); const origin = new Vector3(
updateAngle(joint.name, angle); options?.origin.x ?? 0,
}; options?.origin.y ?? 0,
dragControls.onDragStart = () => (this.controls.enabled = false); options?.origin.z ?? 0
dragControls.onDragEnd = () => (this.controls.enabled = true); )
dragControls.onHover = (joint: URDFMimicJoint) => const arrowHelper = new ArrowHelper(
this.highlightLinkGeometry(joint, false, highlightMaterial); dir,
dragControls.onUnhover = (joint: URDFMimicJoint) => origin,
this.highlightLinkGeometry(joint, true, highlightMaterial); options?.length ?? 1.5,
options?.color ?? 0xff0000
)
this.scene.add(arrowHelper)
return this
}
this.renderer.domElement.addEventListener('touchstart', (data) => private setJointValue(jointName: string, angle: number) {
dragControls._mouseDown(data.touches[0]) if (!this.model) return
); if (!this.model.joints[jointName]) return
this.renderer.domElement.addEventListener('touchmove', (data) => this.model.joints[jointName].setJointValue(angle)
dragControls._mouseMove(data.touches[0]) }
);
this.renderer.domElement.addEventListener('touchend', (data) =>
dragControls._mouseUp(data.touches[0])
);
return this;
};
public toggleFog = () => { isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
this.scene.fog = this.scene.fog ? null : this.fog;
};
private handleRobotShadow = () => { highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
if (this.isLoaded) return; const traverse = (c: Object3D) => {
const intervalId = setInterval(() => { if (c.type === 'Mesh') {
this.model?.traverse((c) => (c.castShadow = true)); if (revert) {
}, 10); c.material = c.__origMaterial
setTimeout(() => { delete c.__origMaterial
clearInterval(intervalId); } else {
}, 1000); c.__origMaterial = c.material
this.isLoaded = true; c.material = material
}; }
}
if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) {
const child = c.children[i]
if (!child.isURDFCollider) {
traverse(c.children[i])
}
}
}
}
traverse(m)
}
public addTransformControls = (model: Object3D) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement)
this.transformControl.addEventListener('dragging-changed', (event: { value: boolean }) => {
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: URDFRobot) => {
this.modelGroup = new Group()
this.modelGroup.add(model)
this.model = model
this.scene.add(this.modelGroup)
return this
}
public addDragControl = (updateAngle: (angles: Record<string, number>) => void) => {
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
}
} }
+42 -60
View File
@@ -1,71 +1,53 @@
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 =
private dbVersion = 1; browser ? this.openDatabase() : null
private storeName = 'files';
private dbPromise: Promise<Result<IDBDatabase, string>>;
constructor() { private async openDatabase(): Promise<Result<IDBDatabase, string>> {
this.dbPromise = this.openDatabase(); return new Promise(resolve => {
} const request = indexedDB.open('fileStorageDB', 1)
private async openDatabase(): Promise<Result<IDBDatabase, string>> { request.onupgradeneeded = () => {
return new Promise((resolve) => { request.result.createObjectStore('files')
const request = indexedDB.open(this.dbName, this.dbVersion); }
request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Error opening database'))
})
}
request.onerror = () => resolve(Result.err('Error opening database')); 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
if (dbResult.isErr()) return Result.err('Database not initialized')
const store = dbResult.inner.transaction('files', mode).objectStore('files')
return Result.ok(store)
}
request.onsuccess = () => resolve(Result.ok(request.result)); public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
const storeResult = await this.getStore('readwrite')
if (storeResult.isErr()) return Result.err('Failed to access store')
request.onupgradeneeded = (event) => { return new Promise(resolve => {
const db = request.result; const request = storeResult.inner.put(file, key)
if (!db.objectStoreNames.contains(this.storeName)) { request.onsuccess = () => resolve(Result.ok(request.result))
db.createObjectStore(this.storeName); request.onerror = () => resolve(Result.err('Failed to save file'))
} })
}; }
});
}
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> { public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const dbResult = await this.dbPromise; const storeResult = await this.getStore('readonly')
if (dbResult.isErr()) { if (storeResult.isErr()) return Result.err('Failed to access store')
return Result.err('Database not initialized properly');
}
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>> { return new Promise(resolve => {
const storeResult = await this.getStore('readwrite'); const request = storeResult.inner.get(key)
if (storeResult.isErr()) { request.onsuccess = () =>
return Result.err('Failed to access object store for writing'); resolve(request.result ? Result.ok(request.result) : Result.err('File not found'))
} request.onerror = () => resolve(Result.err('Failed to retrieve file'))
const store = storeResult.inner; })
}
return new Promise((resolve) => {
const request = store.put(file, key);
request.onsuccess = () => resolve(Result.ok(request.result));
request.onerror = () => resolve(Result.err('Failed to save file'));
});
}
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore('readonly');
if (storeResult.isErr()) {
return Result.err('Failed to access object store for reading');
}
const store = storeResult.inner;
return new Promise((resolve) => {
const request = store.get(key);
request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File content not found'));
request.onerror = () => resolve(Result.err('Failed to retrieve file'));
});
}
} }
export default new FileService(); export default browser ? new FileService() : null
+2 -3
View File
@@ -1,3 +1,2 @@
export { default as fileService } from './file-service'; export { default as fileService } from './file-service'
export { default as socketService } from './socket-service'; export { default as resultService } from './result-service'
export { default as resultService } from './result-service';
+14 -14
View File
@@ -1,19 +1,19 @@
import { errorLogs, latestErrorLog } from '$lib/stores'; import { errorLogs, latestErrorLog } from '$lib/stores'
import type { Result } from '$lib/utilities'; import type { Result } from '$lib/utilities'
class ResultService { class ResultService {
public handleResult(result: Result<unknown, string>, tag?: string) { public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) { if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception }; const errorLogEntry = { tag, message: result.inner, exception: result.exception }
latestErrorLog.set(errorLogEntry); latestErrorLog.set(errorLogEntry)
errorLogs.update((entries) => { errorLogs.update(entries => {
entries.push(errorLogEntry); entries.push(errorLogEntry)
return entries; return entries
}); })
} }
return result; return result
} }
} }
export default new ResultService(); export default new ResultService()
-96
View File
@@ -1,96 +0,0 @@
import { isConnected, socketData } from '$lib/stores';
import { Result, Ok } from '$lib/utilities';
import { resultService } from '$lib/services';
import { type WebSocketJsonMsg } from '$lib/models';
import type { Writable } from 'svelte/store';
type WebsocketOutData = string | ArrayBufferLike | Blob | ArrayBufferView;
// TODO
/**
* MOVE THE store to a store.ts file
*
* Make an object on the class that encapsulate all the stores
*
* Make the handle message function look up the type and set the value, to simplify the code
*/
class SocketService {
private socket!: WebSocket;
private url?:string
constructor() {}
public connect(url: string): void {
this.url = url
this.socket = new WebSocket(url);
this.socket.binaryType = 'arraybuffer';
this.socket.onopen = () => this.handleConnected();
this.socket.onclose = () => this.handleDisconnected();
this.socket.onmessage = (event: MessageEvent) =>
resultService.handleResult(this.handleMessage(event), 'SocketService');
this.socket.onerror = (error: Event) => console.log(error);
}
public send(data: WebsocketOutData): Result<void, string> {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(data);
return Ok.void();
}
return Result.err('The connection is not open');
}
public addPublisher(store: Writable<WebsocketOutData>, type?: string) {
const publish = (data: WebsocketOutData) =>
this.send(type ? JSON.stringify({ type, data }) : data);
store.subscribe(publish);
}
private handleConnected(): void {
isConnected.set(true);
}
private handleDisconnected(): void {
isConnected.set(false);
setTimeout(() => this.connect(this.url as string), 500)
}
private getJsonFromMessage(msg: string): Result<WebSocketJsonMsg, string> {
try {
return Result.ok(JSON.parse(msg) as WebSocketJsonMsg);
} catch (error) {
return Result.err('Failed to parse socket message', error);
}
}
private handleBufferMessage(buffer: ArrayBuffer): Result<void, string> {
console.log(buffer);
return Ok.void();
}
private handleMessage(event: MessageEvent): Result<void, string> {
if (event.data instanceof ArrayBuffer) {
return this.handleBufferMessage(event.data);
}
let msgRes = this.getJsonFromMessage(event.data);
if (msgRes.isErr()) {
return msgRes;
}
const msg = msgRes.inner;
if (msg.type === 'log') {
socketData.logs.update((entries) => {
entries.push(msg.data);
return entries;
});
return Ok.void();
} else if (msg.data && msg.type in socketData) {
socketData[msg.type].set(msg.data);
return Ok.void();
}
return Result.err(`Got invalid msg: ${JSON.stringify(msg)}`);
}
}
export default new SocketService();
+69
View File
@@ -0,0 +1,69 @@
import { type Analytics } from '$lib/types/models'
import { writable } from 'svelte/store'
const analytics_data = {
uptime: <number[]>[],
free_heap: <number[]>[],
total_heap: <number[]>[],
used_heap: <number[]>[],
min_free_heap: <number[]>[],
max_alloc_heap: <number[]>[],
fs_used: <number[]>[],
fs_total: <number[]>[],
core_temp: <number[]>[],
cpu0_usage: <number[]>[],
cpu1_usage: <number[]>[],
cpu_usage: <number[]>[]
}
const maxAnalyticsData = 100
function createAnalytics() {
const { subscribe, update } = writable(analytics_data)
return {
subscribe,
addData: (content: Analytics) => {
update(analytics_data => ({
...analytics_data,
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(
-maxAnalyticsData
),
total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
-maxAnalyticsData
),
used_heap: [
...analytics_data.used_heap,
(content.total_heap - content.free_heap) / 1000
].slice(-maxAnalyticsData),
min_free_heap: [
...analytics_data.min_free_heap,
content.min_free_heap / 1000
].slice(-maxAnalyticsData),
max_alloc_heap: [
...analytics_data.max_alloc_heap,
content.max_alloc_heap / 1000
].slice(-maxAnalyticsData),
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(
-maxAnalyticsData
),
fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(
-maxAnalyticsData
),
core_temp: [...analytics_data.core_temp, content.core_temp].slice(
-maxAnalyticsData
),
cpu0_usage: [...analytics_data.cpu0_usage, content.cpu0_usage].slice(
-maxAnalyticsData
),
cpu1_usage: [...analytics_data.cpu1_usage, content.cpu1_usage].slice(
-maxAnalyticsData
),
cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData)
}))
}
}
}
export const analytics = createAnalytics()
+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, unknown>
}
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: '3D representation',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
}
},
{
name: 'Stream',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Stream' }]
}
},
{
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)
+64
View File
@@ -0,0 +1,64 @@
import { api } from '$lib/api'
import { notifications } from '$lib/components/toasts/notifications'
import Kinematic from '$lib/kinematic'
import { persistentStore } from '$lib/utilities'
import { derived, type Writable } from 'svelte/store'
import { resolve } from '$app/paths'
let featureFlagsStore: Writable<Record<string, boolean | string>>
export function useFeatureFlags() {
if (!featureFlagsStore) {
featureFlagsStore = persistentStore<Record<string, boolean | string>>('FeatureFlags', {})
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
}
const base = resolve('/')
export const variants = {
SPOTMICRO_ESP32: {
model: `${base}spot_micro.urdf.xacro`,
stl: `${base}stl.zip`,
kinematics: {
coxa: 60.5 / 100,
coxa_offset: 10 / 100,
femur: 111.7 / 100,
tibia: 118.5 / 100,
L: 207.5 / 100,
W: 78 / 100
}
},
SPOTMICRO_YERTLE: {
model: `${base}yertle.URDF`,
stl: `${base}URDF.zip`,
kinematics: {
coxa: 35 / 100,
coxa_offset: 0 / 100,
femur: 130 / 100,
tibia: 130 / 100,
L: 240 / 100,
W: 78 / 100
}
}
}
export const currentVariant = derived(useFeatureFlags(), $flagStore => {
const variantFlag = $flagStore['variant'] as string
return variantFlag && variants[variantFlag as keyof typeof variants] ?
variants[variantFlag as keyof typeof variants]
: variants.SPOTMICRO_ESP32
})
export const currentKinematic = derived(
currentVariant,
$variant => new Kinematic($variant.kinematics)
)
+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)
}
}
+87
View File
@@ -0,0 +1,87 @@
import { readable, derived } from 'svelte/store'
export type GamepadState = {
available: boolean
gamepads: Gamepad[]
}
const DEADZONE = 0.15
const dz = (x: number) => {
const a = Math.abs(x)
if (a < DEADZONE) return 0
return ((a - DEADZONE) / (1 - DEADZONE)) * Math.sign(x)
}
let raf = 0
let running = false
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
const update = () => {
const pads = navigator.getGamepads?.() ?? []
const list = Array.from(pads)
.map(p => p || null)
.filter(Boolean) as Gamepad[]
set({ available: 'getGamepads' in navigator, gamepads: list })
raf = requestAnimationFrame(update)
}
const onConnect = () => update()
const onDisconnect = () => update()
const onVis = () => {
if (document.hidden) {
running = false
cancelAnimationFrame(raf)
} else if (!running) {
running = true
raf = requestAnimationFrame(update)
}
}
window.addEventListener('gamepadconnected', onConnect)
window.addEventListener('gamepaddisconnected', onDisconnect)
document.addEventListener('visibilitychange', onVis)
running = true
raf = requestAnimationFrame(update)
return () => {
running = false
cancelAnimationFrame(raf)
window.removeEventListener('gamepadconnected', onConnect)
window.removeEventListener('gamepaddisconnected', onDisconnect)
document.removeEventListener('visibilitychange', onVis)
}
})
export const gamepad = derived(gamepads, s =>
s.available && s.gamepads.length ? s.gamepads[0] : null
)
export const hasGamepad = derived(gamepads, s => s.available && s.gamepads.length > 0)
export const gamepadAxes = derived(gamepad, g => (g ? g.axes.map(dz) : [0, 0, 0, 0]))
type ButtonEdge = { pressed: boolean; value: number; justPressed: boolean; justReleased: boolean }
const prev = new Map<number, { pressed: boolean; value: number }[]>()
export const gamepadButtons = derived(gamepad, g => g?.buttons ?? [])
export const gamepadButtonsEdges = derived(gamepad, g => {
if (!g) return [] as ButtonEdge[]
const p = prev.get(g.index) || []
const out = g.buttons.map((b, i): ButtonEdge => {
const pr = p[i] || { pressed: false, value: 0 }
const pressed = !!b.pressed || b.value > 0.5
return {
pressed,
value: b.value,
justPressed: pressed && !pr.pressed,
justReleased: !pressed && pr.pressed
}
})
prev.set(
g.index,
out.map(x => ({ pressed: x.pressed, value: x.value }))
)
return out
})
+40
View File
@@ -0,0 +1,40 @@
import { writable } from 'svelte/store'
import type { IMUMsg } from '$lib/types/models'
const maxIMUData = 100
export const imu = (() => {
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[]
})
const addData = (content: IMUMsg) => {
update(data => {
if (content.imu && content.imu[4]) {
data.x = [...data.x, content.imu[0]].slice(-maxIMUData)
data.y = [...data.y, content.imu[1]].slice(-maxIMUData)
data.z = [...data.z, content.imu[2]].slice(-maxIMUData)
}
if (content.mag && content.mag[4]) {
data.heading = [...data.heading, content.mag[3]].slice(-maxIMUData)
}
if (content.bmp && content.bmp[3]) {
data.pressure = [...data.pressure, content.bmp[0]].slice(-maxIMUData)
data.altitude = [...data.altitude, content.bmp[1]].slice(-maxIMUData)
data.bmp_temp = [...data.bmp_temp, content.bmp[2]].slice(-maxIMUData)
}
return data
})
}
return { subscribe, addData }
})()
+9 -3
View File
@@ -1,3 +1,9 @@
export * from './socket-store'; 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 './fullscreen'
export * from './telemetry'
export * from './analytics'
export * from './featureFlags'
export * from './location-store'
+6
View File
@@ -0,0 +1,6 @@
import { persistentStore } from '$lib/utilities'
import { writable } from 'svelte/store'
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
export const apiLocation =
PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '')
+6 -6
View File
@@ -1,11 +1,11 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store'
export interface errorLog { export interface errorLog {
message: unknown; message: unknown
tag?: string; tag?: string
exception?: unknown; exception?: unknown
} }
export const latestErrorLog: Writable<errorLog> = writable(); export const latestErrorLog: Writable<errorLog> = writable()
export const errorLogs: Writable<errorLog[]> = writable([]); export const errorLogs: Writable<errorLog[]> = writable([])
+45 -15
View File
@@ -1,24 +1,54 @@
import type { ControllerInput } from '$lib/models'; import type { ControllerInput } from '$lib/types/models'
import { persistentStore } from '$lib/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 = ['idle', 'rest', 'stand', 'walk'] as const; export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'walk'] as const
export type Modes = (typeof modes)[number]; export type Modes = (typeof modes)[number]
export const mode: Writable<Modes> = writable('idle'); export enum ModesEnum {
Deactivated = 0,
Idle = 1,
Calibration = 2,
Rest = 3,
Stand = 4,
Walk = 5
}
export const outControllerData = writable(new Int8Array([0, 0, 0, 0, 0, 0, 70, 0])); export enum WalkGaits {
Trot = 0,
Crawl = 1
}
export const walkGaits = ['trot', 'crawl'] as const
export const walkGaitLabels: Record<WalkGaits, string> = {
[WalkGaits.Trot]: 'Trot',
[WalkGaits.Crawl]: 'Crawl'
}
export const walkGaitToMode = (gait: WalkGaits): 'trot' | 'crawl' => {
return gait === WalkGaits.Trot ? 'trot' : 'crawl'
}
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated)
export const walkGait: Writable<WalkGaits> = writable(WalkGaits.Trot)
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0])
export const kinematicData = writable([0, 0, 0, 0, 1, 0])
export const input: Writable<ControllerInput> = writable({ export const input: Writable<ControllerInput> = writable({
left: { x: 0, y: 0 }, left: { x: 0, y: 0 },
right: { x: 0, y: 0 }, right: { x: 0, y: 0 },
height: 70, height: 0.5,
speed: 0 speed: 0.5,
}); s1: 0.05
})
+21 -25
View File
@@ -1,31 +1,27 @@
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 isConnected = writable(false); export const servoAnglesOut: Writable<number[]> = writable([
export const servoAngles: Writable<angles> = writable(new Int16Array(12).fill(0)); 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
export const logs = writable([] as string[]); ])
export const battery = writable({}); export const servoAngles: Writable<number[]> = writable([
export const mpu = writable({ heading: 0 }); 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
export const distances = writable({}); ])
export const settings = writable({}); export const logs = writable([] as string[])
export const systemInfo = writable({} as number); export const mpu = writable({ heading: 0 })
export const sonar = writable([0, 0])
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>;
settings: Writable<unknown>;
systemInfo: Writable<unknown>;
} }
export const socketData = { export const socketData = {
angles: servoAngles, angles: servoAngles,
logs, logs,
battery, mpu,
mpu, distances
distances, }
settings,
systemInfo
};
+160
View File
@@ -0,0 +1,160 @@
import { writable } from 'svelte/store'
import { encode, decode } from '@msgpack/msgpack'
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number]
type SocketMessage = [number, string?, unknown?]
let useBinary = false
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer
try {
if (useBinary) {
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage
}
return JSON.parse(data as string)
} catch (error) {
console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`)
}
return null
}
const encodeMessage = (data: unknown) => {
try {
return useBinary ? encode(data) : JSON.stringify(data)
} catch (error) {
console.error(`Could not encode data: ${data} - ${error}`)
}
}
function createWebSocket() {
const listeners = new Map<string, Set<(data?: unknown) => void>>()
const { subscribe, set } = writable(false)
const reconnectTimeoutTime = 5000
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
let reconnectTimeoutId: ReturnType<typeof setTimeout>
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.binaryType = 'arraybuffer'
ws.onopen = ev => {
ping()
useBinary = true
ping()
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 = frame => {
resetUnresponsiveCheck()
const message = decodeMessage(frame.data)
if (!message) return
const [, event, payload = undefined] = message
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: unknown) => void) {
const 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
send([2, event, data])
}
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
send([1, event])
}
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
send([0, event])
}
function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
const serialized = encodeMessage(data)
if (!serialized) {
console.error('Could not serialize data:', data)
return
}
ws.send(serialized)
}
function ping() {
const serialized = encodeMessage([4])
if (!serialized) {
console.error('Could not serialize message')
return
}
ws.send(serialized)
}
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: unknown) => void)
return () => {
unsubscribe(event, listener as (data: unknown) => void)
}
},
off: <T>(event: string, listener?: (data: T) => void) => {
unsubscribe(event, listener as (data: unknown) => void)
}
}
}
export const socket = createWebSocket()
+35
View File
@@ -0,0 +1,35 @@
import type { DownloadOTA } from '$lib/types/models'
import { writable } from 'svelte/store'
const telemetry_data = {
rssi: {
rssi: 0
},
download_ota: {
status: 'none',
progress: 0,
error: ''
}
}
function createTelemetry() {
const { subscribe, update } = writable(telemetry_data)
return {
subscribe,
setRSSI: (data: number) => {
update(telemetry_data => ({
...telemetry_data,
rssi: { rssi: data }
}))
},
setDownloadOTA: (data: DownloadOTA) => {
update(telemetry_data => ({
...telemetry_data,
download_ota: { status: data.status, progress: data.progress, error: data.error }
}))
}
}
}
export const telemetry = createTelemetry()

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