689 Commits

Author SHA1 Message Date
Rune Harlyk cbfd7aa354 Adds skill system 2025-12-25 14:03:39 +01:00
Rune Harlyk bc27e5000a 🎨 Update connection url 2025-12-25 14:02:21 +01:00
Rune Harlyk a67d4643b0 🎨 Handle static config files 2025-12-25 13:39:15 +01:00
Rune Harlyk 4e24d87e4b Make read imu and mag be timing based 2025-12-25 13:38:50 +01:00
Rune Harlyk 630bab7678 ♻️ Remove duplicate ping pong handling 2025-12-25 13:37:05 +01:00
Rune Harlyk f54c957be8 🐛 Secure sub and unsub with mutex 2025-12-25 13:36:49 +01:00
Rune Harlyk ed88e47944 🐛 Map rotation bound correct in rad 2025-12-25 13:36:24 +01:00
Rune Harlyk ba36bcc5a5 ♻️ Adds filesystems endpoints back 2025-12-25 13:36:01 +01:00
Rune Harlyk 5e2e29d2a4 ♻️ Change kinematics units to SI 2025-12-24 13:44:45 +01:00
Rune Harlyk 3be08a31ed Adds imu calibration 2025-12-24 13:44:43 +01:00
Rune Harlyk e22ac69e9b 🚨 Fix warnings 2025-12-24 12:34:36 +01:00
Rune Harlyk 0e54f0430f Adds rest of missing api endpoints 2025-12-23 22:28:23 +01:00
Rune Harlyk 0556f86473 🎨 Adds endpoints for wifi and ap 2025-12-20 21:07:32 +01:00
Rune Harlyk 097cc0e33e 🐛 Update base step height in visualizer 2025-12-08 22:30:56 +01:00
Rune Harlyk fe76f2d7dd Adds system metrics endpoints 2025-11-27 21:15:47 +01:00
Rune Harlyk f78a0f50bd 🐛 Fix imu and magnotometer 2025-11-27 20:59:11 +01:00
Rune Harlyk d43e98d06b 🐛 Imu temp in message 2025-11-27 18:44:36 +01:00
Rune Harlyk ffb2bc8749 🐛 Fix socket deadlock 2025-11-27 18:44:12 +01:00
Rune Harlyk 6c61227623 Emit imu, mag and bmp data 2025-11-27 17:38:37 +01:00
Rune Harlyk 7d2f384898 🐛 Fix system metric emit 2025-11-27 17:32:48 +01:00
Rune Harlyk 8a80559ea7 🐛 Call begin on camera service 2025-11-27 17:28:49 +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
819 changed files with 291745 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:
push:
branches: [ master ]
paths:
- 'app/**'
pull_request:
branches: [ master ]
paths:
- 'app/**'
permissions:
contents: read
@@ -19,7 +23,7 @@ jobs:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 8
version: 9
- name: Setup Node.js
uses: actions/setup-node@v4
@@ -30,6 +34,8 @@ jobs:
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run tests
run: pnpm test
run: pnpm test
+8 -2
View File
@@ -1,2 +1,8 @@
*.pyc
spot_env
.vscode/.browse.c_cpp.db*
.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
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"platformio.platformio-ide",
"svelte.svelte-vscode"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
+10 -1
View File
@@ -1,6 +1,15 @@
{
"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.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
+1 -1
View File
@@ -10,4 +10,4 @@ node_modules
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
yarn.lock
+30 -19
View File
@@ -1,20 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020
},
env: {
browser: true,
es2017: true,
node: true
}
};
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: 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
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
node_modules
/build
/.svelte-kit
/package
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1 -10
View File
@@ -1,13 +1,4 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock
yarn.lock
+10 -7
View File
@@ -1,9 +1,12 @@
{
"useTabs": true,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
"useTabs": false,
"singleQuote": true,
"tabWidth": 4,
"trailingComma": "none",
"arrowParens": "avoid",
"experimentalTernaries": true,
"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.
+64 -54
View File
@@ -1,55 +1,65 @@
{
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --mode embedded",
"dev:mock_embedded": "vite --mode mock_embedded",
"dev:mock_web": "vite --mode mock_web",
"build": "vite build --mode embedded",
"build:mock_web": "vite build --mode mock_web",
"build:web": "vite build --mode web",
"preview": "vite preview",
"test": "vitest --environment jsdom",
"check": "svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2",
"@tsconfig/svelte": "^5.0.2",
"@types/three": "^0.160.0",
"@typescript-eslint/eslint-plugin": "^6.20.0",
"@typescript-eslint/parser": "^6.20.0",
"autoprefixer": "^10.4.17",
"cross-env": "^7.0.3",
"husky": "^9.0.7",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.0",
"postcss": "^8.4.33",
"prettier": "3.2.4",
"svelte": "^4.2.9",
"svelte-check": "^3.6.3",
"svelte-hero-icons": "^5.0.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-singlefile": "^1.0.0",
"vitest": "^1.3.1"
},
"dependencies": {
"nipplejs": "^0.10.1",
"prettier-plugin-svelte": "^3.2.1",
"svelte-routing": "^2.11.0",
"three": "^0.160.1",
"urdf-loader": "^0.12.1",
"uzip": "^0.20201231.0",
"xacro-parser": "^0.3.9"
},
"lint-staged": {
"*.js": "eslint --cache --fix",
"*.{js,css,md,ts,svelte}": "prettier --write"
}
}
"name": "spot_micro_controller",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev --host",
"build": "vite build",
"build:embedded": "cross-env VITE_USE_HOST_NAME=true vite build",
"preview": "vite preview",
"test": "pnpm run test:integration && pnpm run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write .",
"test:integration": "playwright test",
"test:unit": "vitest"
},
"devDependencies": {
"@iconify-json/mdi": "^1.2.3",
"@iconify-json/tabler": "^1.2.23",
"@playwright/test": "^1.56.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.46.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.1",
"@types/three": "^0.180.0",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"jsdom": "^27.0.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.39.11",
"svelte-check": "^4.3.3",
"svelte-focus-trap": "^1.2.0",
"tailwindcss": "^4.1.14",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"unplugin-icons": "^22.4.2",
"vite": "^7.1.9",
"vitest": "^3.2.4"
},
"type": "module",
"dependencies": {
"@msgpack/msgpack": "^3.1.2",
"@niku/vite-env-caster": "^1.1.2",
"@sveltejs/adapter-auto": "^6.1.1",
"@tailwindcss/vite": "^4.1.14",
"chart.js": "^4.5.0",
"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>
+42 -14
View File
@@ -1,20 +1,48 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
@import 'tailwindcss';
@plugin "daisyui";
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
@plugin "daisyui" {
themes:
light --default,
dark --prefersdark;
}
body {
margin: 0;
@plugin "daisyui/theme" {
name: 'light';
default: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: white;
}
@plugin "daisyui/theme" {
name: 'dark';
prefersdark: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: oklch(0.3 0.012 256);
}
button {
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
}
#nipple_0_0,
#nipple_1_1 {
z-index: 10 !important;
}
#three-gui-panel {
top: 50px;
right:0px
}
top: 64px;
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}
+156
View File
@@ -0,0 +1,156 @@
<script lang="ts">
import { skill } from '$lib/stores'
import { onMount, onDestroy } from 'svelte'
let targetX = $state(0.5)
let targetZ = $state(0)
let targetYaw = $state(0)
let speed = $state(0.5)
const status = skill.status
const isActive = skill.isActive
const progress = skill.progress
const presets = [
{ name: 'Forward 0.5m', x: 0.5, z: 0, yaw: 0 },
{ name: 'Forward 1m', x: 1, z: 0, yaw: 0 },
{ name: 'Back 0.5m', x: -0.5, z: 0, yaw: 0 },
{ name: 'Left 0.5m', x: 0, z: 0.5, yaw: 0 },
{ name: 'Right 0.5m', x: 0, z: -0.5, yaw: 0 },
{ name: 'Turn Left 90°', x: 0, z: 0, yaw: 1.57 },
{ name: 'Turn Right 90°', x: 0, z: 0, yaw: -1.57 },
{ name: 'Turn 180°', x: 0, z: 0, yaw: 3.14 }
]
onMount(() => skill.init())
onDestroy(() => skill.destroy())
function executeSkill() {
skill.walk(targetX, targetZ, targetYaw, speed)
}
function runPreset(preset: (typeof presets)[0]) {
skill.walk(preset.x, preset.z, preset.yaw, speed)
}
function formatMeters(val: number): string {
return val.toFixed(3) + 'm'
}
function formatDegrees(rad: number): string {
return ((rad * 180) / Math.PI).toFixed(1) + '°'
}
</script>
<div class="card bg-base-200 shadow-xl">
<div class="card-body p-4">
<h2 class="card-title text-sm flex justify-between">
Skill Control
<span class="badge" class:badge-success={$isActive} class:badge-ghost={!$isActive}>
{$isActive ? 'Active' : 'Idle'}
</span>
</h2>
<div class="grid grid-cols-2 gap-2 text-xs mb-2">
<div class="stat bg-base-300 rounded-lg p-2">
<div class="stat-title text-xs">Position</div>
<div class="stat-value text-sm">
{formatMeters($status.x)}, {formatMeters($status.z)}
</div>
<div class="stat-desc">Yaw: {formatDegrees($status.yaw)}</div>
</div>
<div class="stat bg-base-300 rounded-lg p-2">
<div class="stat-title text-xs">Distance</div>
<div class="stat-value text-sm">{formatMeters($status.distance)}</div>
<div class="stat-desc">Total traveled</div>
</div>
</div>
{#if $isActive}
<div class="mb-2">
<div class="flex justify-between text-xs mb-1">
<span>Progress</span>
<span>{($progress * 100).toFixed(0)}%</span>
</div>
<progress class="progress progress-primary w-full" value={$progress} max="1"></progress>
<div class="text-xs text-base-content/60 mt-1">
Target: ({$status.skill_target_x.toFixed(2)}, {$status.skill_target_z.toFixed(2)}, {formatDegrees(
$status.skill_target_yaw
)})
</div>
</div>
{/if}
<div class="divider my-1 text-xs">Presets</div>
<div class="grid grid-cols-4 gap-1">
{#each presets as preset}
<button class="btn btn-xs btn-outline" onclick={() => runPreset(preset)}>
{preset.name}
</button>
{/each}
</div>
<div class="divider my-1 text-xs">Custom</div>
<div class="grid grid-cols-3 gap-2">
<div class="form-control">
<label class="label py-0" for="skill-x">
<span class="label-text text-xs">X (m)</span>
</label>
<input
id="skill-x"
type="number"
step="0.1"
bind:value={targetX}
class="input input-bordered input-xs w-full"
/>
</div>
<div class="form-control">
<label class="label py-0" for="skill-z">
<span class="label-text text-xs">Z (m)</span>
</label>
<input
id="skill-z"
type="number"
step="0.1"
bind:value={targetZ}
class="input input-bordered input-xs w-full"
/>
</div>
<div class="form-control">
<label class="label py-0" for="skill-yaw">
<span class="label-text text-xs">Yaw (rad)</span>
</label>
<input
id="skill-yaw"
type="number"
step="0.1"
bind:value={targetYaw}
class="input input-bordered input-xs w-full"
/>
</div>
</div>
<div class="form-control mt-2">
<label class="label py-0" for="skill-speed">
<span class="label-text text-xs">Speed: {speed.toFixed(2)}</span>
</label>
<input id="skill-speed" type="range" min="0.1" max="1" step="0.05" bind:value={speed} class="range range-xs range-primary" />
</div>
<div class="card-actions justify-between mt-2">
<div class="flex gap-1">
<button class="btn btn-xs btn-ghost" onclick={() => skill.resetPosition()}>Reset Pos</button>
</div>
<div class="flex gap-1">
<button class="btn btn-xs btn-error" onclick={() => skill.stop()} disabled={!$isActive}>
Stop
</button>
<button class="btn btn-xs btn-primary" onclick={executeSkill} disabled={$isActive}>
Execute
</button>
</div>
</div>
</div>
</div>
+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>
+348
View File
@@ -0,0 +1,348 @@
<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]
const THREEJS_SCALE = 10
let body_state = {
omega: 0,
phi: 0,
psi: 0,
xm: 0,
ym: 0.15,
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.15,
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) * THREEJS_SCALE,
0.1
)
robot.position.z = smooth(
robot.position.z,
(-rotatedXm - body_state.cumulative_x) * THREEJS_SCALE,
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]
}
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 / THREEJS_SCALE
settings.zm = -robot.position.x / THREEJS_SCALE
}
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>
+508
View File
@@ -0,0 +1,508 @@
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 get kinematic() {
return get(currentKinematic)
}
protected gait_state: gait_state_t = {
step_height: 0,
step_x: 0,
step_z: 0,
step_angle: 0,
step_velocity: 1,
step_depth: 0
}
public get default_feet_pos() {
return this.kinematic.getDefaultFeetPos()
}
protected get default_height() {
return this.kinematic.default_body_height
}
protected get default_step_depth() {
return this.kinematic.default_step_depth
}
protected get default_step_height() {
return this.kinematic.default_step_height
}
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 kin = this.kinematic
this.gait_state = {
step_height: command.s1 * kin.max_step_height,
step_x: command.ly * kin.max_step_length,
step_z: -command.lx * kin.max_step_length,
step_velocity: command.s,
step_angle: command.rx,
step_depth: kin.default_step_depth
}
}
}
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'
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.kinematic.max_body_height
body_state.zm = 0
body_state.feet = this.default_feet_pos
return body_state
}
}
export class RestState extends GaitState {
protected name = 'Rest'
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.kinematic.min_body_height
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)
const kin = this.kinematic
body_state.omega = 0
body_state.ym = kin.min_body_height + command.h * kin.body_height_range
body_state.psi = command.ry * kin.max_pitch
body_state.phi = command.rx * kin.max_roll
body_state.xm = command.ly * kin.max_body_shift_x
body_state.zm = command.lx * kin.max_body_shift_z
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)
const kin = this.kinematic
this.body_state.ym = kin.min_body_height + command.h * kin.body_height_range
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)
const kin = this.kinematic
length = this.gait_state.step_angle * kin.max_step_length
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.
+182 -391
View File
@@ -1,393 +1,184 @@
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 {
private l1: number;
private l2: number;
private l3: number;
private l4: number;
private L: number;
private W: number;
constructor() {
this.l1 = 50;
this.l2 = 20;
this.l3 = 120;
this.l4 = 155;
this.L = 140;
this.W = 75;
}
bodyIK(
omega: number,
phi: number,
psi: number,
xm: number,
ym: number,
zm: number
): number[][][] {
const { cos, sin } = Math;
const Rx: number[][] = [
[1, 0, 0, 0],
[0, cos(omega), -sin(omega), 0],
[0, sin(omega), cos(omega), 0],
[0, 0, 0, 1]
];
const Ry: number[][] = [
[cos(phi), 0, sin(phi), 0],
[0, 1, 0, 0],
[-sin(phi), 0, cos(phi), 0],
[0, 0, 0, 1]
];
const Rz: number[][] = [
[cos(psi), -sin(psi), 0, 0],
[sin(psi), cos(psi), 0, 0],
[0, 0, 1, 0],
[0, 0, 0, 1]
];
const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz);
const T: number[][] = [
[0, 0, 0, xm],
[0, 0, 0, ym],
[0, 0, 0, zm],
[0, 0, 0, 0]
];
const Tm: number[][] = this.matrixAdd(T, Rxyz);
const sHp = sin(Math.PI / 2);
const cHp = cos(Math.PI / 2);
const L = this.L;
const W = this.W;
return [
this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1]
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1]
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, W / 2],
[0, 0, 0, 1]
]),
this.matrixMultiply(Tm, [
[cHp, 0, sHp, -L / 2],
[0, 1, 0, 0],
[-sHp, 0, cHp, -W / 2],
[0, 0, 0, 1]
])
];
}
private legIK(point: number[]): number[] {
const [x, y, z] = point;
const { atan2, cos, sin, sqrt, acos } = Math;
const { l1, l2, l3, l4 } = this;
let F;
try {
F = sqrt(x ** 2 + y ** 2 - l1 ** 2);
if (isNaN(F)) throw new Error('F is NaN');
} catch (error) {
//console.log(error)
F = l1;
}
const G = F - l2;
const H = sqrt(G ** 2 + z ** 2);
const theta1 = -atan2(y, x) - atan2(F, -l1);
const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4);
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;
}
coxa: number
coxa_offset: number
femur: number
tibia: number
L: number
W: number
DEG2RAD = DEG2RAD
max_roll: number
max_pitch: number
max_body_shift_x: number
max_body_shift_z: number
max_leg_reach: number
min_body_height: number
max_body_height: number
body_height_range: number
max_step_length: number
max_step_height: number
default_step_depth: number
default_body_height: number
default_step_height: number
mountOffsets: number[][]
default_feet_positions: number[][]
invMountRot = [
[0, 0, -1],
[0, 1, 0],
[1, 0, 0]
]
constructor(params: KinematicParams) {
this.coxa = params.coxa
this.coxa_offset = params.coxa_offset
this.femur = params.femur
this.tibia = params.tibia
this.L = params.L
this.W = params.W
this.max_roll = 15 * (Math.PI / 2)
this.max_pitch = 15 * (Math.PI / 2)
this.max_body_shift_x = this.W / 3
this.max_body_shift_z = this.W / 3
this.max_leg_reach = this.femur + this.tibia - this.coxa_offset
this.min_body_height = this.max_leg_reach * 0.45
this.max_body_height = this.max_leg_reach * 1
this.body_height_range = this.max_body_height - this.min_body_height
this.max_step_length = this.max_leg_reach * 0.8
this.max_step_height = this.max_leg_reach / 2
this.default_step_depth = 0.002
this.default_body_height = this.min_body_height + this.body_height_range / 2
this.default_step_height = this.default_body_height / 2
this.mountOffsets = [
[this.L / 2, 0, this.W / 2],
[this.L / 2, 0, -this.W / 2],
[-this.L / 2, 0, this.W / 2],
[-this.L / 2, 0, -this.W / 2]
]
this.default_feet_positions = this.mountOffsets.map((offset, i) => {
return [offset[0], 0, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
})
}
getDefaultFeetPos(): number[][] {
return this.default_feet_positions.map(pos => [...pos])
}
calcIK(p: body_state_t): number[] {
const roll = p.omega * this.DEG2RAD
const pitch = p.phi * this.DEG2RAD
const yaw = p.psi * this.DEG2RAD
const rot = this.euler2R(roll, pitch, yaw)
const inv_rot = [
[rot[0][0], rot[1][0], rot[2][0]],
[rot[0][1], rot[1][1], rot[2][1]],
[rot[0][2], rot[1][2], rot[2][2]]
]
const inv_trans = [
-inv_rot[0][0] * p.xm - inv_rot[0][1] * p.ym - inv_rot[0][2] * p.zm,
-inv_rot[1][0] * p.xm - inv_rot[1][1] * p.ym - inv_rot[1][2] * p.zm,
-inv_rot[2][0] * p.xm - inv_rot[2][1] * p.ym - inv_rot[2][2] * p.zm
]
return p.feet.flatMap((foot, i) => {
const [wx, wy, wz] = foot
const bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0]
const by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1]
const bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2]
const [mx, my, mz] = this.mountOffsets[i]
const px = bx - mx,
py = by - my,
pz = bz - mz
const lx =
this.invMountRot[0][0] * px +
this.invMountRot[0][1] * py +
this.invMountRot[0][2] * pz
const ly =
this.invMountRot[1][0] * px +
this.invMountRot[1][1] * py +
this.invMountRot[1][2] * pz
const lz =
this.invMountRot[2][0] * px +
this.invMountRot[2][1] * py +
this.invMountRot[2][2] * pz
const xLocal = i % 2 === 1 ? -lx : lx
return this.legIK(xLocal, ly, lz)
})
}
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)
const D =
(H * H - this.femur * this.femur - this.tibia * this.tibia) /
(2 * this.femur * this.tibia)
const t3 = acos(max(-1, min(1, D)))
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
return [t1, t2, t3]
}
private euler2R(roll: number, pitch: number, yaw: number): number[][] {
const cr = cos(roll),
sr = sin(roll)
const cp = cos(pitch),
sp = sin(pitch)
const cy = cos(yaw),
sy = sin(yaw)
return [
[cp * cy, -cp * sy, sp],
[sr * sp * cy + sy * cr, -sr * sp * sy + cr * cy, -sr * cp],
[sr * sy - sp * cr * cy, sr * cy + sp * sy * cr, cr * cp]
]
}
}
-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 {
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
ShadowMaterial,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
GridHelper,
ArrowHelper,
Vector3,
LoaderUtils,
Object3D,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
MeshPhongMaterial,
EquirectangularReflectionMapping,
ACESFilmicToneMapping,
MathUtils
} from 'three';
import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader';
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls';
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
type GridHelper,
ArrowHelper,
Vector3,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
MeshPhongMaterial,
EquirectangularReflectionMapping,
ACESFilmicToneMapping,
Group,
MeshBasicMaterial,
RepeatWrapping,
type Object3D
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
export const addScene = () => new Scene();
export const addScene = () => new Scene()
interface position {
x?: number;
y?: number;
z?: number;
x?: number
y?: number
z?: number
}
interface light {
color?: ColorRepresentation;
intensity?: number;
}
interface gridOptions {
divisions?: number;
size?: number;
color?: ColorRepresentation
intensity?: number
}
interface arrowOptions {
origin: position;
direction: position;
length?: number;
color?: ColorRepresentation;
origin: position
direction: position
length?: number
color?: ColorRepresentation
}
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);
}
type directionalLight = position & light
export default class SceneBuilder {
public scene: Scene;
public camera: PerspectiveCamera;
public ground: Mesh;
public renderer: WebGLRenderer;
public controls: OrbitControls;
public callback: Function;
public gridHelper: GridHelper;
public model: URDFRobot;
public liveStreamTexture: CanvasTexture;
private fog: FogExp2;
private isLoaded: boolean = false;
highlightMaterial: any;
public scene: Scene
public camera!: PerspectiveCamera
public ground!: Mesh
public renderer!: WebGLRenderer
public orbit: OrbitControls
public callback: (() => void) | undefined
public gridHelper!: GridHelper
public model!: URDFRobot
public liveStreamTexture!: CanvasTexture
private fog!: FogExp2
private isLoaded: boolean = false
public isDragging: boolean = false
transformControl: TransformControls
public modelGroup!: Group
constructor() {
this.scene = new Scene();
if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping;
}
return this;
}
constructor() {
this.scene = new Scene()
if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping
}
return this
}
public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters);
this.renderer.outputColorSpace = 'srgb';
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85;
document.body.appendChild(this.renderer.domElement);
return this;
};
public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters)
this.renderer.outputColorSpace = 'srgb'
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = PCFSoftShadowMap
this.renderer.toneMapping = ACESFilmicToneMapping
this.renderer.toneMappingExposure = 0.85
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement)
return this
}
public addSky = () => {
const sky = new Sky();
sky.scale.setScalar(450000);
this.scene.add(sky);
const effectController = {
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();
public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera()
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0)
this.scene.add(this.camera)
return this
}
sun.setFromSphericalCoords(1, phi, theta);
uniforms['sunPosition'].value.copy(sun);
return this;
};
public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2)
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) => {
this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(this.camera);
return this;
};
const plane = new PlaneGeometry(400, 400)
public addGroundPlane = (options?: position) => {
this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 }));
this.ground.rotation.x = -Math.PI / 2;
this.ground.scale.setScalar(30);
this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0);
this.ground.receiveShadow = true;
this.scene.add(this.ground);
return this;
};
this.ground = new Mesh(plane, checkerboardMat)
this.ground.rotation.x = -Math.PI / 2
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0)
this.ground.receiveShadow = true
this.scene.add(this.ground)
public addOrbitControls = (minDistance: number, maxDistance: number) => {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = minDistance;
this.controls.maxDistance = maxDistance;
this.controls.update();
return this;
};
const mirror = new Reflector(plane, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff
})
mirror.rotateX(-Math.PI / 2)
this.scene.add(mirror)
public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity);
this.scene.add(ambientLight);
return this;
};
return this
}
public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.setScalar(2048);
directionalLight.shadow.mapSize.width = 1024;
directionalLight.shadow.mapSize.height = 1024;
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
directionalLight.shadow.radius = 5;
this.scene.add(directionalLight);
return this;
};
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
this.orbit.minDistance = minDistance + (maxDistance - minDistance) / 2
this.orbit.maxDistance = maxDistance
this.orbit.autoRotate = autoRotate
this.orbit.update()
this.orbit.minDistance = minDistance
return this
}
public addGridHelper = (options: gridHelperOptions) => {
this.gridHelper = new GridHelper(options.size, options.divisions);
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.gridHelper.material.opacity = 0.2;
this.gridHelper.material.depthWrite = false;
this.gridHelper.material.transparent = true;
this.scene.add(this.gridHelper);
return this;
};
public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity)
this.scene.add(ambientLight)
return this
}
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density);
return this;
};
public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity)
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 = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
return this;
};
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0)
this.scene.add(directionalLight)
return this
}
public addRenderCb = (callback: Function) => {
this.callback = callback;
return this;
};
private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const context = canvas.getContext('2d')
public startRenderLoop = () => {
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;
};
const squareSize = size / squares
public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(
options?.direction.x ?? 0,
options?.direction.y ?? 0,
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;
};
for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize)
}
}
private setJointValue(jointName: string, angle: number) {
if (!this.model) return;
if (!this.model.joints[jointName]) return;
this.model.joints[jointName].setJointValue(angle);
}
const texture = new CanvasTexture(canvas)
texture.wrapS = texture.wrapT = RepeatWrapping
texture.anisotropy = 16
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) => {
const traverse = (c: any) => {
if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial;
delete c.__origMaterial;
} else {
c.__origMaterial = c.material;
c.material = material;
}
}
public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement
if (parentElement) {
const width = parentElement.clientWidth
const height = parentElement.clientHeight
this.handleResize(width, height)
}
return this
}
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 handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.camera.aspect = width / height
this.camera.updateProjectionMatrix()
return this
}
public addModel = (model: any) => {
this.model = model;
this.scene.add(model);
return this;
};
public addRenderCb = (callback: () => void) => {
this.callback = callback
return this
}
public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF';
const highlightMaterial = new MeshPhongMaterial({
shininess: 10,
color: highlightColor,
emissive: highlightColor,
emissiveIntensity: 0.25
});
public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera)
this.orbit.update()
this.handleRobotShadow()
if (this.callback) this.callback()
if (!this.liveStreamTexture) return
})
return this
}
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.controls.enabled = false);
dragControls.onDragEnd = () => (this.controls.enabled = true);
dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial);
public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(
options?.direction.x ?? 0,
options?.direction.y ?? 0,
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
}
this.renderer.domElement.addEventListener('touchstart', (data) =>
dragControls._mouseDown(data.touches[0])
);
this.renderer.domElement.addEventListener('touchmove', (data) =>
dragControls._mouseMove(data.touches[0])
);
this.renderer.domElement.addEventListener('touchend', (data) =>
dragControls._mouseUp(data.touches[0])
);
return this;
};
private setJointValue(jointName: string, angle: number) {
if (!this.model) return
if (!this.model.joints[jointName]) return
this.model.joints[jointName].setJointValue(angle)
}
public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog;
};
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
private handleRobotShadow = () => {
if (this.isLoaded) return;
const intervalId = setInterval(() => {
this.model?.traverse((c) => (c.castShadow = true));
}, 10);
setTimeout(() => {
clearInterval(intervalId);
}, 1000);
this.isLoaded = true;
};
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: Object3D) => {
if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial
delete c.__origMaterial
} else {
c.__origMaterial = c.material
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 {
private dbName = 'fileStorageDB';
private dbVersion = 1;
private storeName = 'files';
private dbPromise: Promise<Result<IDBDatabase, string>>;
private dbPromise: Promise<Result<IDBDatabase, string>> | null =
browser ? this.openDatabase() : null
constructor() {
this.dbPromise = this.openDatabase();
}
private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise(resolve => {
const request = indexedDB.open('fileStorageDB', 1)
private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onupgradeneeded = () => {
request.result.createObjectStore('files')
}
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) => {
const db = request.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
return new Promise(resolve => {
const request = storeResult.inner.put(file, key)
request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Failed to save file'))
})
}
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
const dbResult = await this.dbPromise;
if (dbResult.isErr()) {
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 getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore('readonly')
if (storeResult.isErr()) return Result.err('Failed to access store')
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 object store for writing');
}
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'));
});
}
return new Promise(resolve => {
const request = storeResult.inner.get(key)
request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File 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 socketService } from './socket-service';
export { default as resultService } from './result-service';
export { default as fileService } from './file-service'
export { default as resultService } from './result-service'
+14 -14
View File
@@ -1,19 +1,19 @@
import { errorLogs, latestErrorLog } from '$lib/stores';
import type { Result } from '$lib/utilities';
import { errorLogs, latestErrorLog } from '$lib/stores'
import type { Result } from '$lib/utilities'
class ResultService {
public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception };
latestErrorLog.set(errorLogEntry);
errorLogs.update((entries) => {
entries.push(errorLogEntry);
return entries;
});
}
public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception }
latestErrorLog.set(errorLogEntry)
errorLogs.update(entries => {
entries.push(errorLogEntry)
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()
+79
View File
@@ -0,0 +1,79 @@
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'
import SkillPanel from '$lib/components/SkillPanel.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,
SkillPanel
}
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 } }
]
}
},
{
name: 'Skills',
content: {
id: 'root',
widgets: [
{ id: 1, component: 'Visualization', props: { debug: true } },
{ id: 2, component: 'SkillPanel' }
]
}
}
]
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: 0.0605,
coxa_offset: 0.01,
femur: 0.1112,
tibia: 0.1185,
L: 0.2075,
W: 0.078
}
},
SPOTMICRO_YERTLE: {
model: `${base}yertle.URDF`,
stl: `${base}URDF.zip`,
kinematics: {
coxa: 0.035,
coxa_offset: 0.0,
femur: 0.13,
tibia: 0.13,
L: 0.24,
W: 0.078
}
}
}
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 }
})()
+10 -3
View File
@@ -1,3 +1,10 @@
export * from './socket-store';
export * from './logging-store';
export * from './model-store';
export * from './socket-store'
export * from './logging-store'
export * from './model-store'
export * from './socket'
export * from './fullscreen'
export * from './telemetry'
export * from './analytics'
export * from './featureFlags'
export * from './location-store'
export * from './skill'
+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 {
message: unknown;
tag?: string;
exception?: unknown;
message: unknown
tag?: string
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 { persistentStore } from '$lib/utilities';
import { writable, type Writable } from 'svelte/store';
import type { ControllerInput } from '$lib/types/models'
import { persistentStore } from '$lib/utilities/svelte-utilities'
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({
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 70,
speed: 0
});
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 0.5,
speed: 0.5,
s1: 0.5
})
+85
View File
@@ -0,0 +1,85 @@
import { writable, derived } from 'svelte/store'
import { socket } from './socket'
import { MessageTopic, type SkillStatus, type SkillCommand } from '$lib/types/models'
const defaultStatus: SkillStatus = {
x: 0,
y: 0,
z: 0,
yaw: 0,
distance: 0,
skill_active: false,
skill_target_x: 0,
skill_target_z: 0,
skill_target_yaw: 0,
skill_traveled_x: 0,
skill_traveled_z: 0,
skill_rotated: 0,
skill_progress: 0,
skill_complete: false
}
function createSkillStore() {
const status = writable<SkillStatus>(defaultStatus)
const history = writable<SkillCommand[]>([])
let unsubscribe: (() => void) | null = null
function init() {
if (unsubscribe) return
unsubscribe = socket.on<SkillStatus>(MessageTopic.skillStatus, data => {
status.set(data)
if (data.event === 'complete') {
history.update(h => [...h.slice(-9), getCurrentTarget(data)])
}
})
}
function getCurrentTarget(s: SkillStatus): SkillCommand {
return { x: s.skill_target_x, z: s.skill_target_z, yaw: s.skill_target_yaw }
}
function execute(cmd: SkillCommand) {
socket.sendEvent(MessageTopic.skill, cmd)
}
function walk(x: number, z: number = 0, yaw: number = 0, speed: number = 0.5) {
execute({ x, z, yaw, speed })
}
function turn(yaw: number, speed: number = 0.5) {
execute({ x: 0, z: 0, yaw, speed })
}
function stop() {
socket.sendEvent(MessageTopic.displacement, { action: 'clear' })
}
function resetPosition() {
socket.sendEvent(MessageTopic.displacement, { action: 'reset' })
}
function destroy() {
if (unsubscribe) {
unsubscribe()
unsubscribe = null
}
}
return {
status,
history,
init,
destroy,
execute,
walk,
turn,
stop,
resetPosition,
isActive: derived(status, $s => $s.skill_active),
progress: derived(status, $s => $s.skill_progress),
position: derived(status, $s => ({ x: $s.x, y: $s.y, z: $s.z, yaw: $s.yaw }))
}
}
export const skill = createSkillStore()

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