281 Commits

Author SHA1 Message Date
Rune Harlyk 04fecf33f8 🪴 Adds webserial lidar support 2024-08-04 13:53:53 +02:00
Rune Harlyk acf4efde4c 🗺️ Adds lidar visualization 2024-08-04 00:02:17 +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
642 changed files with 72031 additions and 26193 deletions
+43
View File
@@ -0,0 +1,43 @@
name: PlatformIO CI
on:
push:
branches: [ master ]
paths:
- 'esp32/**'
pull_request:
branches: [ master ]
paths:
- 'esp32/**'
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./esp32
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 ./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@v3
with:
name: build-artifacts
path: esp32/build/firmware
@@ -1,10 +1,14 @@
name: CI name: Frontend Tests
on: on:
push: push:
branches: [ master ] branches: [ master ]
paths:
- 'app/**'
pull_request: pull_request:
branches: [ master ] branches: [ master ]
paths:
- 'app/**'
permissions: permissions:
contents: read contents: read
@@ -30,6 +34,8 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run tests - name: Run tests
run: pnpm test run: pnpm test
+4 -2
View File
@@ -1,2 +1,4 @@
*.pyc .vscode/.browse.c_cpp.db*
spot_env .vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
+4 -1
View File
@@ -2,7 +2,10 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846 // See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format // for the documentation about the extensions.json format
"recommendations": [ "recommendations": [
"platformio.platformio-ide" "platformio.platformio-ide",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
], ],
"unwantedRecommendations": [ "unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack" "ms-vscode.cpptools-extension-pack"
+10 -1
View File
@@ -1,6 +1,15 @@
{ {
"files.associations": { "files.associations": {
"cmath": "cpp" "cmath": "cpp",
"array": "cpp",
"deque": "cpp",
"string": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"string_view": "cpp",
"initializer_list": "cpp",
"regex": "cpp"
}, },
"editor.tabSize": 4, "editor.tabSize": 4,
"editor.detectIndentation": false, "editor.detectIndentation": false,
View File
-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
+19 -8
View File
@@ -1,20 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = { module.exports = {
root: true, root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], plugins: ['@typescript-eslint'],
plugins: ['svelte3', '@typescript-eslint'],
ignorePatterns: ['*.cjs'],
overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
settings: {
'svelte3/typescript': () => require('typescript')
},
parserOptions: { parserOptions: {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2020 ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
}, },
env: { env: {
browser: true, browser: true,
es2017: true, es2017: true,
node: 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": {}
}
+9 -23
View File
@@ -1,24 +1,10 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store .DS_Store
*.suo node_modules
*.ntvs* /build
*.njsproj /.svelte-kit
*.sln /package
*.sw? .env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
+1
View File
@@ -0,0 +1 @@
engine-strict=true
-9
View File
@@ -1,12 +1,3 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
# Ignore files for PNPM, NPM and YARN # Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml pnpm-lock.yaml
package-lock.json package-lock.json
-1
View File
@@ -4,6 +4,5 @@
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": ["prettier-plugin-svelte"], "plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"recommendations": ["svelte.svelte-vscode"] "recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
} }
+37 -2
View File
@@ -1,3 +1,38 @@
# 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
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
```bash
npm run dev
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
## Building
To create a production version of your app:
```bash
npm run build
```
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.
-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.
+46 -40
View File
@@ -1,55 +1,61 @@
{ {
"name": "app", "name": "spot_micro_controller",
"version": "0.0.1",
"private": true, "private": true,
"version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"dev": "vite --mode embedded", "dev": "vite dev --host",
"dev:mock_embedded": "vite --mode mock_embedded", "build": "vite build",
"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", "preview": "vite preview",
"test": "vitest --environment jsdom", "test": "npm run test:integration && npm run test:unit",
"check": "svelte-check --tsconfig ./tsconfig.json", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"format": "prettier --plugin-search-dir . --write ." "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": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.2", "@iconify-json/mdi": "^1.1.64",
"@tsconfig/svelte": "^5.0.2", "@iconify-json/tabler": "^1.1.109",
"@types/three": "^0.160.0", "@playwright/test": "^1.28.1",
"@typescript-eslint/eslint-plugin": "^6.20.0", "@sveltejs/adapter-auto": "^3.0.0",
"@typescript-eslint/parser": "^6.20.0", "@sveltejs/adapter-static": "^3.0.1",
"autoprefixer": "^10.4.17", "@sveltejs/kit": "^2.5.5",
"cross-env": "^7.0.3", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"husky": "^9.0.7", "@types/eslint": "^8.56.0",
"@types/three": "^0.162.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lint-staged": "^15.2.0", "postcss": "^8.4.38",
"postcss": "^8.4.33", "prettier": "^3.1.1",
"prettier": "3.2.4", "prettier-plugin-svelte": "^3.1.2",
"svelte": "^4.2.9", "svelte": "^4.2.7",
"svelte-check": "^3.6.3", "svelte-check": "^3.6.0",
"svelte-hero-icons": "^5.0.0", "svelte-focus-trap": "^1.2.0",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.3",
"tslib": "^2.6.2", "tslib": "^2.6.1",
"typescript": "^5.3.3", "typescript": "^5.1.6",
"vite": "^5.0.12", "unplugin-icons": "^0.18.5",
"vite-plugin-compression": "^0.5.1", "vite": "^5.0.3",
"vite-plugin-singlefile": "^1.0.0", "vitest": "^1.2.0"
"vitest": "^1.3.1"
}, },
"type": "module",
"dependencies": { "dependencies": {
"chart.js": "^4.4.2",
"compare-versions": "^6.1.0",
"daisyui": "^4.10.2",
"jwt-decode": "^4.0.0",
"nipplejs": "^0.10.1", "nipplejs": "^0.10.1",
"prettier-plugin-svelte": "^3.2.1", "svelte-dnd-list": "^0.1.8",
"svelte-routing": "^2.11.0", "svelte-modals": "^1.3.0",
"three": "^0.160.1", "three": "^0.162.0",
"urdf-loader": "^0.12.1", "urdf-loader": "^0.12.1",
"uzip": "^0.20201231.0", "uzip": "^0.20201231.0",
"xacro-parser": "^0.3.9" "xacro-parser": "^0.3.9"
},
"lint-staged": {
"*.js": "eslint --cache --fix",
"*.{js,css,md,ts,svelte}": "prettier --write"
} }
} }
+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;
+2799 -2007
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -1,6 +1,5 @@
import tailwindcss from 'tailwindcss';
import autoprefixer from 'autoprefixer';
export default { export default {
plugins: { plugins: [tailwindcss(), autoprefixer()]
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>
+13 -15
View File
@@ -1,20 +1,18 @@
:root { @tailwind base;
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; @tailwind components;
line-height: 1.5; @tailwind utilities;
font-weight: 400;
font-synthesis: none; #nipple_0_0, #nipple_1_1 {
text-rendering: optimizeLegibility; z-index: 10!important;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
} }
#three-gui-panel { #three-gui-panel {
top: 50px; top: 64px;
right:0px right: 0px;
}
@media (max-width: 1023px) {
#three-gui-panel {
top: 48px;
}
} }
+13
View File
@@ -0,0 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {};
+12
View File
@@ -0,0 +1,12 @@
<!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" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
-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>
-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;
}
} */
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+4
View File
@@ -0,0 +1,4 @@
export function daisyColor(name: string, opacity: number = 100) {
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
return `oklch(${color} / ${opacity}%)`;
}
+73
View File
@@ -0,0 +1,73 @@
import { user } from '$lib/stores/user';
import { get } from 'svelte/store';
import { Err, Ok, type Result } from './utilities';
export namespace api {
export function get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params);
}
export function post<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'POST', data);
}
export function put<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'PUT', data);
}
export function 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>> {
const user_token = get(user).bearer_token;
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
const request = {
...params,
method,
body,
headers: {
...params?.headers,
Authorization: user_token ? 'Bearer ' + user_token : 'Basic',
'Content-Type': 'application/json'
}
};
let response;
try {
response = await fetch(endpoint, request);
} catch (error) {
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);
}
}
export class ApiError extends Error {
constructor(public readonly response: Response) {
super(`${response.status}`);
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,27 @@
<script lang="ts">
import Battery0 from '~icons/tabler/battery';
import Battery25 from '~icons/tabler/battery-1';
import Battery50 from '~icons/tabler/battery-2';
import Battery75 from '~icons/tabler/battery-3';
import Battery100 from '~icons/tabler/battery-4';
import BatteryCharging from '~icons/tabler/battery-charging-2';
export let current = 0;
export let voltage = 0;
</script>
<div class="tooltip tooltip-left z-10" data-tip="{voltage}V {Math.floor(current*10)/10} mA">
{#if voltage == 0}
<BatteryCharging class="{$$props.class || ''} -rotate-90 animate-pulse" />
{:else if voltage > 8.2}
<Battery100 class="{$$props.class || ''} -rotate-90" />
{:else if voltage > 8}
<Battery75 class="{$$props.class || ''} -rotate-90" />
{:else if voltage > 7.8}
<Battery50 class="{$$props.class || ''} -rotate-90" />
{:else if voltage > 7.6}
<Battery25 class="{$$props.class || ''} -rotate-90" />
{:else}
<Battery0 class="{$$props.class || ''} text-error -rotate-90 animate-pulse" />
{/if}
</div>
+43
View File
@@ -0,0 +1,43 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import Down from '~icons/tabler/chevron-down';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function openCollapsible() {
open = !open;
if (open) {
dispatch('opened');
} else {
dispatch('closed');
}
}
export let open = false;
</script>
<div class="{$$props.class || ''} 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">
<slot name="icon" />
<slot name="title" />
</span>
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => 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 }}
>
<slot />
</div>
{/if}
</div>
@@ -0,0 +1,52 @@
<script lang="ts">
import { closeModal } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import Cancel from '~icons/tabler/x';
import Check from '~icons/tabler/check';
// provided by <Modals />
export let isOpen: boolean;
export let title: string;
export let message: string;
export let onConfirm: any;
export let labels = {
cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'OK', icon: Check }
};
</script>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
on:introstart
on:outroend
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" />
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" />
<div class="flex justify-end gap-2">
<button class="btn btn-primary inline-flex items-center" on:click={closeModal}
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span
>{labels?.cancel.label}</span
></button
>
<button
class="btn btn-warning text-warning-content inline-flex items-center"
on:click={onConfirm}
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span
>{labels?.confirm.label}</span
></button
>
</div>
</div>
</div>
{/if}
@@ -0,0 +1,92 @@
<script lang="ts">
import { closeAllModals, onBeforeClose } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { telemetry } from '$lib/stores/telemetry';
import Cancel from '~icons/tabler/x';
// provided by <Modals />
export let isOpen: boolean;
let updating = true;
let progress = 0;
$: if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress;
}
$: if ($telemetry.download_ota.status == 'error') {
updating = false;
}
let message = 'Preparing ...';
let timerId: number;
$: 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
timerId = setTimeout(() => {
closeAllModals();
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 }}
on:introstart
on:outroend
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 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" />
{:else}
<progress class="progress progress-primary w-56" />
{/if}
<p class="mt-8 text-2xl">{message}</p>
</div>
</div>
<div class="divider my-2" />
<div class="flex flex-wrap justify-end gap-2">
<div class="flex-grow" />
<button
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating}
on:click={() => {
closeAllModals();
location.reload();
}}
>
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
>
</div>
</div>
</div>
{/if}
+42
View File
@@ -0,0 +1,42 @@
<script lang="ts">
import { closeModal } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import Check from '~icons/tabler/check';
// provided by <Modals />
export let isOpen: boolean;
export let title: string;
export let message: string;
export let onDismiss: any;
export let dismiss = { label: 'Dismiss', icon: Check };
</script>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
on:introstart
on:outroend
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" />
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" />
<div class="flex justify-end gap-2">
<button
class="btn btn-warning text-warning-content inline-flex items-center"
on:click={onDismiss}
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
></button
>
</div>
</div>
</div>
{/if}
@@ -0,0 +1,60 @@
<script lang="ts">
let show = false;
$: type = show ? 'text' : 'password';
export let value = '';
export let id = '';
function handleInput(e: any) {
value = e.target.value;
}
</script>
<div class="relative">
<input {type} class="input input-bordered w-full" {value} on:input={handleInput} {id} />
<div class="absolute inset-y-0 right-0 flex items-center pr-1">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}"
on:click={() => (show = false)}
width="40"
height="40"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
role="button"
tabindex="0"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
<path
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
/>
<path d="M3 3l18 18" />
</svg>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}"
on:click={() => (show = true)}
width="40"
height="40"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
role="button"
tabindex="0"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
</svg>
</div>
</div>
+77
View File
@@ -0,0 +1,77 @@
<script lang="ts">
import { onMount } from "svelte";
import { lidar, type LidarPoint } from '$lib/stores/lidar'
function getIntersection(angle:number, size:number):number {
const sinAngle = Math.sin(angle);
const cosAngle = Math.cos(angle);
let x, y;
if (Math.abs(cosAngle) > Math.abs(sinAngle)) {
x = size * Math.sign(cosAngle);
y = x * sinAngle / cosAngle;
} else {
y = size * Math.sign(sinAngle);
x = y * cosAngle / sinAngle;
}
return Math.sqrt(x**2 + y**2);
}
let canvas:HTMLCanvasElement
let ctx
const DEG2RAD = 0.017453292519943;
onMount(() => {
ctx = canvas.getContext("2d")
resize()
lidar.subscribe(lidar => {
draw(lidar.points)
})
})
const draw = (points:LidarPoint[]) => {
if(!points) return
const centerX = canvas.width / 2
const centerY = canvas.height / 2
const scale = 0.01//Math.max(centerX, centerY) / Math.max(...points.map((point) => point.distance))
if (!ctx) return
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = 0; i < points.length; i++){
const angle = points[i].angle
const distance = points[i].distance
const quality = points[i].quality
const endX = centerX + (distance * scale) * Math.cos(angle * DEG2RAD);
const endY = centerY - (distance * scale) * Math.sin(angle * DEG2RAD);
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(endX, endY);
ctx.strokeStyle = "grey"
ctx.stroke();
ctx.beginPath();
ctx.arc(endX, endY, 3, 0, Math.PI * 2);
ctx.fillStyle = "#1bfc06"
ctx.fill();
}
}
const resize = () => {
const parentElement = canvas.parentElement;
if (parentElement) {
canvas.width = parentElement.clientWidth
canvas.height = parentElement.clientHeight
}
}
</script>
<svelte:window on:resize={resize}></svelte:window>
<canvas bind:this={canvas} class="w-full h-full"></canvas>
@@ -0,0 +1,40 @@
<script lang="ts">
import WiFi from '~icons/tabler/wifi';
import WiFi0 from '~icons/tabler/wifi-0';
import WiFi1 from '~icons/tabler/wifi-1';
import WiFi2 from '~icons/tabler/wifi-2';
import WifiOff from '~icons/tabler/wifi-off';
export let showDBm = true;
export let rssi_dbm = 0;
</script>
<div class="indicator">
<div class="tooltip tooltip-left" data-tip={rssi_dbm + " dBm"}>
{#if showDBm}
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
{rssi_dbm} dBm
</span>
{/if}
{#if rssi_dbm >= -55}
<WiFi class={$$props.class || ''} />
{:else if rssi_dbm >= -75}
<div class="{$$props.class || ''} relative">
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
<WiFi2 class="absolute inset-0 h-full w-full" />
</div>
{:else if rssi_dbm >= -85}
<div class="{$$props.class || ''} relative">
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
<WiFi1 class="absolute inset-0 h-full w-full" />
</div>
{:else if rssi_dbm === 0}
<WifiOff class={$$props.class || ''} />
{:else}
<div class="{$$props.class || ''} relative">
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
<WiFi0 class="absolute inset-0 h-full w-full" />
</div>
{/if}
</div>
</div>
@@ -0,0 +1,56 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import Down from '~icons/tabler/chevron-down';
export let open = true;
export let collapsible = true;
</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">
<slot name="icon" />
<slot name="title" />
</span>
<button
class="btn btn-circle btn-ghost btn-sm"
on:click={() => {
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 }}
>
<slot />
</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 w-full p-4 text-xl font-medium">
<span class="inline-flex items-baseline">
<slot name="icon" />
<slot name="title" />
</span>
</div>
<div class="flex flex-col gap-2 p-4 pt-0">
<slot />
</div>
</div>
{/if}
+8
View File
@@ -0,0 +1,8 @@
<script lang="ts">
import Loader from '~icons/tabler/loader-2';
</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>
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import { user } from '$lib/stores/user';
import { onDestroy } from 'svelte';
const ws_token = `?access_token=${$user.bearer_token}`
let source = "/api/camera/stream"+ ws_token;
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 from '~icons/tabler/circle-x';
import success from '~icons/tabler/circle-check';
import warning from '~icons/tabler/alert-triangle';
import info from '~icons/tabler/info-circle';
export let theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
};
export let icon = {
error: error,
success: success,
warning: warning,
info: info
};
</script>
<div class="toast toast-end mr-4">
{#each $notifications as notification (notification.id)}
<div
animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }}
>
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
</div>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import MdiHamburgerMenu from '~icons/mdi/hamburger-menu';
</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="/">
<svelte:component this={MdiHamburgerMenu} class="h-8 w-8"/>
</a>
</div>
</div>
<style>
.topbar {
height: 50px;
}
</style>
@@ -0,0 +1,106 @@
<script lang="ts">
import { page } from '$app/stores';
import { openModal, closeAllModals } from 'svelte-modals';
import { user } from '$lib/stores/user';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import Firmware from '~icons/tabler/refresh-alert';
import Cancel from '~icons/tabler/x';
import CloudDown from '~icons/tabler/cloud-download';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/models';
export let update = false;
let firmwareVersion: string;
let firmwareDownloadLink: string;
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
const result = await api.get<GithubRelease>(`https://api.github.com/repos/${$page.data.github}/releases/latest`, {headers})
if (result.inner.message === "404" || result.inner.message == "Not Found") {
console.warn('Error: Could not find releases in the repository');
return
}
if (result.isErr()) {
console.error('Error:', result.inner);
return
}
const results = result.inner;
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($page.data.features.firmware_built_target)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
}
}
async function postGithubDownload(url: string) {
const result = await api.post('/api/downloadUpdate', { download_url: url });
if (result.isErr()){
console.error('Error:', result.inner);
return
}
}
onMount(async () => {
if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) {
await getGithubAPI();
const interval = setInterval(
async () => {
await getGithubAPI();
},
60 * 60 * 1000
); // once per hour
}
});
function confirmGithubUpdate(url: string) {
openModal(ConfirmDialog, {
title: 'Confirm flashing new firmware to the device',
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Update', icon: CloudDown }
},
onConfirm: () => {
postGithubDownload(url);
openModal(GithubUpdateDialog, {
onConfirm: () => closeAllModals()
});
}
});
}
</script>
{#if update}
<button
class="btn btn-square btn-ghost h-9 w-9"
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>{firmwareVersion}</span
>
<Firmware class="h-7 w-7" />
</button>
{/if}
+308
View File
@@ -0,0 +1,308 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { BufferGeometry, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, Object3D, SphereGeometry, Vector3, type NormalBufferAttributes, type Object3DEventMap } from 'three';
import uzip from 'uzip';
import { ModesEnum, kinematicData, mode, model, outControllerData, servoAnglesOut, servoAngles, mpu, jointNames } from '$lib/stores';
import { footColor, isEmbeddedApp, throttler, toeWorldPositions } from '$lib/utilities';
import { fileService } from '$lib/services';
import SceneBuilder from '$lib/sceneBuilder';
import { lerp, degToRad } from 'three/src/math/MathUtils';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import Kinematic, { type body_state_t } from '$lib/kinematic';
import {EightPhaseWalkState, FourPhaseWalkState, IdleState, RestState, StandState} from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js';
import type { URDFRobot } from 'urdf-loader';
import { get } from 'svelte/store';
export let sky = true
export let orbit = false
export let panel = true
export let debug = false
export let ground = true
let sceneManager = 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 feet_trace = new Array(4).fill([]);
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
let target: Object3D<Object3DEventMap>;
let target_position = {x: 0, z: 0, yaw: 0}
let kinematic = new Kinematic()
let planners = {
[ModesEnum.Idle]: new IdleState(),
[ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(),
[ModesEnum.Walk]: new FourPhaseWalkState()
}
let lastTick = performance.now()
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
let body_state = {
omega: 0,
phi: 0,
psi: 0,
xm: 0,
ym: 0.5,
zm: 0,
feet: planners[ModesEnum.Idle].default_feet_pos
}
let settings = {
'Internal kinematic':false,
'Robot transform controls':false,
'Auto orient robot':true,
'Trace feet':debug,
'Trace points': 30,
'Fix camera on robot': true,
'Smooth motion': true,
'omega': 0,
'phi': 0,
'psi': 0,
'xm': 0,
'ym': 0.7,
'zm': 0,
'Background': "black"
}
onMount(async () => {
await cacheModelFiles()
await createScene();
if (!isEmbeddedApp && panel) createPanel();
servoAngles.subscribe(updateAnglesFromStore)
});
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, 'Smooth motion')
visibility.addColor(settings, 'Background')
}
const updateKinematicPosition = () => {
kinematicData.set([
settings.omega,
settings.phi,
settings.psi,
settings.xm,
settings.ym,
settings.zm
])
}
const cacheModelFiles = async () => {
let data = await fetch('/stl.zip').then((data) => data.arrayBuffer());
var files = uzip.parse(data);
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const url = new URL(path, window.location.href);
fileService.saveFile(url.toString(), data);
}
};
const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
};
const createScene = async () => {
sceneManager
.addRenderer({ antialias: true, canvas, alpha: true })
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
.addOrbitControls(8, 30, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 0.9 })
.addAmbientLight({ color: 0xffffff, intensity: 0.6 })
.addFogExp2(0xcccccc, 0.015)
.addModel($model)
.addTransformControls(sceneManager.model)
.fillParent()
.addRenderCb(render)
.startRenderLoop();
if (ground) sceneManager
.addGroundPlane()
.addGridHelper({ size: 30, divisions: 25 })
const geometry = new SphereGeometry(0.1, 32, 16 );
const material = new MeshBasicMaterial( { color: 0xffff00 } );
target = new Mesh(geometry, material);
if (debug) {
sceneManager.scene.add(target);
sceneManager.addDragControl(updateAngles)
}
if (sky) sceneManager.addSky()
for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry();
const material = new LineBasicMaterial({ color: footColor() });
const line = new Line(geometry, material);
trace_lines.push(geometry);
sceneManager.scene.add(line);
}
};
const renderTraceLines = (foot_positions: Vector3[]) => {
if (!settings['Trace feet']) {
if (!feet_trace.length) return
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
feet_trace = new Array(4).fill([])
return
}
trace_lines.forEach((line, i) => {
feet_trace[i].push(foot_positions[i])
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
line.setFromPoints(feet_trace[i]);
})
}
const calculate_kinematics = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const position:body_state_t = {
omega: settings.omega,
phi: settings.phi,
psi: settings.psi,
xm: settings.xm,
ym: settings.ym,
zm: settings.zm,
feet: body_state.feet
}
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]));
modelTargetAngles = new_angles;
}
const orient_robot = (robot: URDFRobot, toes:Vector3[]) => {
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1);
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1);
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1);
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1);
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1);
}
const update_camera = (robot:URDFRobot) => {
if (!settings['Fix camera on robot']) return
sceneManager.orbit.target = robot.position.clone()
}
const smooth = (start:number, end:number, amount:number) => {
return settings['Smooth motion'] ? lerp(start, end, amount) : end
}
const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData)
const data = {
stop: controlData[0],
lx: controlData[1],
ly: controlData[2],
rx: controlData[3],
ry: controlData[4],
h: controlData[5],
s: controlData[6],
};
body_state.ym = ((data.h + 127) * 0.75) / 100;
let planner = planners[get(mode)]
const delta = performance.now() - lastTick
lastTick = performance.now()
body_state = planner.step(body_state, data, delta);
settings.omega = body_state.omega
settings.phi = body_state.phi
settings.psi = body_state.psi
settings.xm = body_state.xm
settings.ym = body_state.ym
settings.zm = body_state.zm
}
const update_robot_position = (robot:URDFRobot) => {
if (!settings['Robot transform controls']) return
settings.omega = radToDeg(robot.rotation.y)
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading -90
settings.psi = radToDeg(robot.rotation.x) + 90
settings.xm = robot.position.z * 100
settings.zm = -robot.position.x * 100
}
const updateTargetPosition = () => {
target.position.x = smooth(target.position.x, target_position.x, 0.5)
target.position.z = smooth(target.position.z, target_position.z, 0.5)
}
const render = () => {
const robot = sceneManager.model;
if (!robot) return;
const toes = toeWorldPositions(robot)
renderTraceLines(toes)
update_camera(robot)
update_gait()
calculate_kinematics()
update_robot_position(robot)
sceneManager.transformControl.showX = settings['Robot transform controls']
sceneManager.transformControl.showY = settings['Robot transform controls']
sceneManager.transformControl.showZ = settings['Robot transform controls']
for (let i = 0; i < $jointNames.length; i++) {
currentModelAngles[i] = smooth((robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI), modelTargetAngles[i], 0.1);
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
}
orient_robot(robot, toes)
updateTargetPosition();
};
</script>
<svelte:window on:resize={sceneManager.fillParent} />
<canvas bind:this={canvas}></canvas>
@@ -0,0 +1,37 @@
<script>
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { notifications } from '$lib/components/toasts/notifications';
import error from '~icons/tabler/circle-x';
import success from '~icons/tabler/circle-check';
import warning from '~icons/tabler/alert-triangle';
import info from '~icons/tabler/info-circle';
export let theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
};
export let icon = {
error: error,
success: success,
warning: warning,
info: info
};
</script>
<div class="toast toast-end mr-4 z-20">
{#each $notifications as notification (notification.id)}
<div
animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }}
>
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
</div>
@@ -0,0 +1,42 @@
import { writable, derived, type 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) => send(msg, 'error', timeout),
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
};
}
function generateId() {
return '_' + Math.random().toString(36).substr(2, 9);
}
export const notifications = createNotificationStore();
+242
View File
@@ -0,0 +1,242 @@
import type { body_state_t } from './kinematic';
import { fromInt8 } from './utilities';
const { sin } = Math;
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 {
stop: number;
lx: number;
ly: number;
rx: number;
ry: number;
h: number;
s: number;
}
export abstract class GaitState {
protected abstract name: string;
protected static body_state: body_state_t;
public get default_feet_pos() {
return [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
];
}
protected get default_height() {
return 0.5;
}
begin() {
console.log('Starting', this.name);
}
end() {
console.log('Ending', this.name);
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return body_state;
}
map_command(command: ControllerCommand): gait_state_t {
return {
step_height: 0.4 + Math.abs(command.ry / 128),
step_x: (Math.floor(fromInt8(command.ly, -1, 1) * 10) / 10) * 3,
step_z: -(Math.floor(fromInt8(command.lx, -1, 1) * 10) / 10) * 3,
step_velocity: command.s / 128 + 1,
step_angle: 0,
step_depth: 0.2
};
}
}
export class IdleState extends GaitState {
protected name = 'Idle';
}
export class RestState extends GaitState {
protected name = 'Rest';
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
body_state.omega = 0;
body_state.phi = 0;
body_state.psi = 0;
body_state.xm = 0;
body_state.ym = this.default_height / 2;
body_state.zm = 0;
body_state.feet = this.default_feet_pos;
return body_state;
}
}
export class StandState extends GaitState {
protected name = 'Stand';
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
body_state.omega = 0;
body_state.phi = command.rx / 8;
body_state.psi = command.ry / 8;
body_state.xm = command.ly / 2 / 100;
body_state.zm = command.lx / 2 / 100;
body_state.feet = this.default_feet_pos;
return body_state;
}
}
abstract class PhaseGaitState extends GaitState {
protected tick = 0;
protected phase = 0;
protected phase_time = 0;
protected abstract num_phases: number;
protected abstract phase_speed_factor: number;
protected abstract swing_stand_ratio: number;
protected contact_phases!: number[][];
protected shifts!: number[][];
protected body_state!: body_state_t;
protected gait_state!: gait_state_t;
protected dt = 0.02;
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
this.body_state = body_state;
this.gait_state = this.map_command(command);
this.dt = dt / 1000;
this.update_phase();
this.update_body_position();
this.update_feet_positions();
return this.body_state;
}
update_phase() {
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity;
if (this.phase_time >= 1) {
this.phase += 1;
if (this.phase == this.num_phases) this.phase = 0;
this.phase_time = 0;
}
}
update_body_position() {
if (this.num_phases === 4) return;
const shift = this.shifts[Math.floor(this.phase / 2)];
this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4;
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4;
}
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[] {
const contact = this.contact_phases[index][this.phase];
return contact ? this.stand(index) : this.swing(index);
}
stand(index: number): number[] {
const delta_pos = [
-this.gait_state.step_x * this.dt * this.swing_stand_ratio,
0,
-this.gait_state.step_z * this.dt * this.swing_stand_ratio
];
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0];
this.body_state.feet[index][1] = this.default_feet_pos[index][1];
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
return this.body_state.feet[index];
}
swing(index: number): number[] {
const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt];
if (this.gait_state.step_x == 0) {
delta_pos[0] =
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8;
}
if (this.gait_state.step_z == 0) {
delta_pos[2] =
(this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8;
}
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0];
this.body_state.feet[index][1] =
this.default_feet_pos[index][1] +
sin(this.phase_time * Math.PI) * this.gait_state.step_height;
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
return this.body_state.feet[index];
}
}
export class FourPhaseWalkState extends PhaseGaitState {
protected name = 'Four phase walk';
protected num_phases = 4;
protected phase_speed_factor = 2.5;
protected contact_phases = [
[1, 0, 1, 1],
[1, 1, 1, 0],
[1, 1, 1, 0],
[1, 0, 1, 1]
];
protected swing_stand_ratio = 1 / (this.num_phases - 1);
begin() {
super.begin();
}
end() {
super.end();
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt);
}
}
export class EightPhaseWalkState extends PhaseGaitState {
protected name = 'Eight phase walk';
protected num_phases = 8;
protected phase_speed_factor = 1.5;
protected contact_phases = [
[1, 0, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 0, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0]
];
protected shifts = [
[-0.3, 0, -0.2],
[-0.3, 0, 0.2],
[0.3, 0, -0.2],
[0.3, 0, 0.2]
];
protected swing_stand_ratio = 1 / (this.num_phases - 1);
begin() {
super.begin();
}
end() {
super.end();
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt);
}
}
+1
View File
@@ -0,0 +1 @@
// place files you want to import through the `$lib` alias in this folder.
+143 -216
View File
@@ -1,120 +1,166 @@
export default class Kinematic {
private l1: number;
private l2: number;
private l3: number;
private l4: number;
private L: number; export interface body_state_t {
private W: number; omega: number;
phi: number;
constructor() { psi: number;
this.l1 = 50; xm: number;
this.l2 = 20; ym: number;
this.l3 = 120; zm: number;
this.l4 = 155; feet: number[][];
this.L = 140;
this.W = 75;
} }
bodyIK( export interface position {
omega: number, x: number;
phi: number, y: number;
psi: number, z: number;
xm: number, }
ym: number,
zm: number
): number[][][] {
const { cos, sin } = Math;
const Rx: number[][] = [ export interface target_position {
[1, 0, 0, 0], x: number;
[0, cos(omega), -sin(omega), 0], z: number;
[0, sin(omega), cos(omega), 0], yaw: number;
[0, 0, 0, 1] }
];
const Ry: number[][] = [ const { cos, sin, atan2, sqrt } = Math;
[cos(phi), 0, sin(phi), 0],
const DEG2RAD = 0.017453292519943;
export default class Kinematic {
l1: number;
l2: number;
l3: number;
l4: number;
L: number;
W: number;
DEG2RAD = DEG2RAD;
sHp = sin(Math.PI / 2);
cHp = cos(Math.PI / 2);
Tlf: number[][] = [];
Trf: number[][] = [];
Tlb: number[][] = [];
Trb: number[][] = [];
point_lf: number[][];
point_rf: number[][];
point_lb: number[][];
point_rb: number[][];
Ix: number[][];
constructor() {
this.l1 = 60.5 / 100;
this.l2 = 10 / 100;
this.l3 = 100.7 / 100;
this.l4 = 118.5 / 100;
this.L = 207.5 / 100;
this.W = 78 / 100;
this.point_lf = [
[this.cHp, 0, this.sHp, this.L / 2],
[0, 1, 0, 0], [0, 1, 0, 0],
[-sin(phi), 0, cos(phi), 0], [-this.sHp, 0, this.cHp, this.W / 2],
[0, 0, 0, 1] [0, 0, 0, 1]
]; ];
const Rz: number[][] = [
[cos(psi), -sin(psi), 0, 0], this.point_rf = [
[sin(psi), cos(psi), 0, 0], [this.cHp, 0, this.sHp, this.L / 2],
[0, 1, 0, 0],
[-this.sHp, 0, this.cHp, -this.W / 2],
[0, 0, 0, 1]
];
this.point_lb = [
[this.cHp, 0, this.sHp, -this.L / 2],
[0, 1, 0, 0],
[-this.sHp, 0, this.cHp, this.W / 2],
[0, 0, 0, 1]
];
this.point_rb = [
[this.cHp, 0, this.sHp, -this.L / 2],
[0, 1, 0, 0],
[-this.sHp, 0, this.cHp, -this.W / 2],
[0, 0, 0, 1]
];
this.Ix = [
[-1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 1, 0], [0, 0, 1, 0],
[0, 0, 0, 1] [0, 0, 0, 1]
]; ];
const Rxyz: number[][] = this.matrixMultiply(this.matrixMultiply(Rx, Ry), Rz); }
const T: number[][] = [ public calcIK(body_state: body_state_t): number[] {
[0, 0, 0, xm], this.bodyIK(body_state);
[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 [ return [
this.matrixMultiply(Tm, [ ...this.legIK(this.multiplyVector(this.inverse(this.Tlf), body_state.feet[0])),
[cHp, 0, sHp, L / 2], ...this.legIK(
[0, 1, 0, 0], this.multiplyVector(
[-sHp, 0, cHp, W / 2], this.Ix,
[0, 0, 0, 1] this.multiplyVector(this.inverse(this.Trf), body_state.feet[1])
]), )
this.matrixMultiply(Tm, [ ),
[cHp, 0, sHp, L / 2], ...this.legIK(this.multiplyVector(this.inverse(this.Tlb), body_state.feet[2])),
[0, 1, 0, 0], ...this.legIK(
[-sHp, 0, cHp, -W / 2], this.multiplyVector(
[0, 0, 0, 1] this.Ix,
]), this.multiplyVector(this.inverse(this.Trb), body_state.feet[3])
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[] { bodyIK(p: body_state_t) {
const [x, y, z] = point; const cos_omega = cos(p.omega * this.DEG2RAD);
const { atan2, cos, sin, sqrt, acos } = Math; const sin_omega = sin(p.omega * this.DEG2RAD);
const { l1, l2, l3, l4 } = this; const cos_phi = cos(p.phi * this.DEG2RAD);
const sin_phi = sin(p.phi * this.DEG2RAD);
const cos_psi = cos(p.psi * this.DEG2RAD);
const sin_psi = sin(p.psi * this.DEG2RAD);
let F; const Tm: number[][] = [
[cos_phi * cos_psi, -sin_psi * cos_phi, sin_phi, p.xm],
[
sin_omega * sin_phi * cos_psi + sin_psi * cos_omega,
-sin_omega * sin_phi * sin_psi + cos_omega * cos_psi,
-sin_omega * cos_phi,
p.ym
],
[
sin_omega * sin_psi - sin_phi * cos_omega * cos_psi,
sin_omega * cos_psi + sin_phi * sin_psi * cos_omega,
cos_omega * cos_phi,
p.zm
],
[0, 0, 0, 1]
];
try { this.Tlf = this.matrixMultiply(Tm, this.point_lf);
F = sqrt(x ** 2 + y ** 2 - l1 ** 2); this.Trf = this.matrixMultiply(Tm, this.point_rf);
if (isNaN(F)) throw new Error('F is NaN'); this.Tlb = this.matrixMultiply(Tm, this.point_lb);
} catch (error) { this.Trb = this.matrixMultiply(Tm, this.point_rb);
//console.log(error)
F = l1;
} }
const G = F - l2;
public legIK(point: number[]): number[] {
const [x, y, z] = point;
let F = sqrt(x ** 2 + y ** 2 - this.l1 ** 2);
if (isNaN(F)) F = this.l1;
const G = F - this.l2;
const H = sqrt(G ** 2 + z ** 2); const H = sqrt(G ** 2 + z ** 2);
const theta1 = -atan2(y, x) - atan2(F, -l1); const theta1 = -atan2(y, x) - atan2(F, -this.l1);
const D = (H ** 2 - l3 ** 2 - l4 ** 2) / (2 * l3 * l4); const D = (H ** 2 - this.l3 ** 2 - this.l4 ** 2) / (2 * this.l3 * this.l4);
let theta3: number; let theta3 = atan2(sqrt(1 - D ** 2), D);
try { if (isNaN(theta3)) theta3 = 0;
theta3 = acos(D);
if (isNaN(theta3)) throw new Error('theta3 is NaN'); const theta2 = atan2(z, G) - atan2(this.l4 * sin(theta3), this.l3 + this.l4 * cos(theta3));
} catch (error) {
theta3 = 0;
}
const theta2 = atan2(z, G) - atan2(l4 * sin(theta3), l3 + l4 * cos(theta3));
return [theta1, theta2, theta3]; return [theta1, theta2, theta3];
} }
@@ -165,81 +211,7 @@ export default class Kinematic {
return result; return result;
} }
private matrixAdd(a: number[][], b: number[][]): number[][] { private inverse(matrix: 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 det = this.determinant(matrix);
const adjugate = this.adjugate(matrix); const adjugate = this.adjugate(matrix);
const scalar = 1 / det; const scalar = 1 / det;
@@ -346,48 +318,3 @@ export default class Kinematic {
} }
} }
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;
}
}
+154 -7
View File
@@ -5,18 +5,165 @@ export interface ControllerInput {
right: vector; right: vector;
height: number; height: number;
speed: number; speed: number;
s1: number;
} }
export type GithubRelease = {
message: string;
tag_name: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
};
export type JWT = { access_token: string };
export type angles = number[] | Int16Array; export type angles = number[] | Int16Array;
export type AnglesData = { export type WifiStatus = {
type: 'angles'; status: number;
data: angles; local_ip: string;
mac_address: string;
rssi: number;
ssid: string;
bssid: string;
channel: number;
subnet_mask: string;
gateway_ip: string;
dns_ip_1: string;
dns_ip_2?: string;
}; };
export type LogData = { export type WifiSettings = {
type: 'log'; hostname: string;
data: string; priority_RSSI: boolean;
wifi_networks: NetworkItem[];
}; };
export type WebSocketJsonMsg = AnglesData | LogData; export type NetworkList = {
networks: NetworkItem[];
};
export type KnownNetworkItem = {
ssid: string;
password: string;
static_ip_config: boolean;
local_ip?: string;
subnet_mask?: string;
gateway_ip?: string;
dns_ip_1?: string;
dns_ip_2?: string;
};
export type NetworkItem = {
rssi: number;
ssid: string;
bssid: string;
channel: number;
encryption_type: number;
};
export type ApStatus = {
status: number;
ip_address: string;
mac_address: string;
station_num: number;
};
export type ApSettings = {
provision_mode: number;
ssid: string;
password: string;
channel: number;
ssid_hidden: boolean;
max_clients: number;
local_ip: string;
gateway_ip: string;
subnet_mask: string;
};
export type LightState = {
led_on: boolean;
};
export type NTPStatus = {
status: number;
utc_time: string;
local_time: string;
server: string;
uptime: number;
};
export type NTPSettings = {
enabled: boolean;
server: string;
tz_label: string;
tz_format: string;
};
export type Analytics = {
max_alloc_heap: number;
psram_size: number;
free_psram: number;
free_heap: number;
total_heap: number;
min_free_heap: number;
core_temp: number;
fs_total: number;
fs_used: number;
uptime: number;
};
export type StaticSystemInformation = {
esp_platform: string;
firmware_version: string;
cpu_freq_mhz: number;
cpu_type: string;
cpu_rev: number;
cpu_cores: number;
sketch_size: number;
free_sketch_space: number;
sdk_version: string;
arduino_version: string;
flash_chip_size: number;
flash_chip_speed: number;
cpu_reset_reason: string;
};
export type SystemInformation = Analytics & StaticSystemInformation;
export type CameraSettings = {
framesize: number;
quality: number;
brightness: number;
contrast: number;
saturation: number;
sharpness: number;
denoise: number;
special_effect: number;
wb_mode: number;
vflip: boolean;
hmirror: boolean;
};
export type File = number;
export interface Directory {
[key: string]: File | Directory;
}
export type Servo = {
name: string;
channel: number;
inverted: boolean;
angle: number;
center_angle: number;
};
export type ServoConfiguration = {
is_active: boolean;
servo_pwm_frequency: number;
servo_oscillator_frequency: number;
servos: Servo[];
};
+76 -44
View File
@@ -3,7 +3,6 @@ import {
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Scene, Scene,
ShadowMaterial,
WebGLRenderer, WebGLRenderer,
AmbientLight, AmbientLight,
DirectionalLight, DirectionalLight,
@@ -11,8 +10,6 @@ import {
GridHelper, GridHelper,
ArrowHelper, ArrowHelper,
Vector3, Vector3,
LoaderUtils,
Object3D,
FogExp2, FogExp2,
CanvasTexture, CanvasTexture,
type ColorRepresentation, type ColorRepresentation,
@@ -20,12 +17,16 @@ import {
MeshPhongMaterial, MeshPhongMaterial,
EquirectangularReflectionMapping, EquirectangularReflectionMapping,
ACESFilmicToneMapping, ACESFilmicToneMapping,
MathUtils MathUtils,
MeshStandardMaterial,
Group
} from 'three'; } from 'three';
import { Sky } from 'three/addons/objects/Sky.js'; import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'; import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader';
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'; import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls';
import { sunCalculator } from './utilities/position-utilities';
export const addScene = () => new Scene(); export const addScene = () => new Scene();
@@ -56,26 +57,23 @@ type directionalLight = position & light;
type gridHelperOptions = gridOptions & position; type gridHelperOptions = gridOptions & position;
function calculateCurrentSunElevation() {
let now = new Date();
let decimalTime = now.getHours() + now.getMinutes() / 60;
let normalizedTime = (decimalTime % 12) / 6 - 1;
return 10 * Math.sin(normalizedTime * Math.PI);
}
export default class SceneBuilder { export default class SceneBuilder {
public scene: Scene; public scene: Scene;
public camera: PerspectiveCamera; public camera!: PerspectiveCamera;
public ground: Mesh; public ground!: Mesh;
public renderer: WebGLRenderer; public renderer!: WebGLRenderer;
public controls: OrbitControls; public orbit: OrbitControls;
public callback: Function; public callback: Function | undefined;
public gridHelper: GridHelper; public gridHelper!: GridHelper;
public model: URDFRobot; public model!: URDFRobot;
public liveStreamTexture: CanvasTexture; public liveStreamTexture!: CanvasTexture;
private fog: FogExp2; private fog!: FogExp2;
private isLoaded: boolean = false; private isLoaded: boolean = false;
public isDragging: boolean = false;
highlightMaterial: any; highlightMaterial: any;
sky!: Sky;
transformControl: TransformControls;
public modelGroup!: Group;
constructor() { constructor() {
this.scene = new Scene(); this.scene = new Scene();
@@ -92,24 +90,24 @@ export default class SceneBuilder {
this.renderer.shadowMap.type = PCFSoftShadowMap; this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping; this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85; this.renderer.toneMappingExposure = 0.85;
document.body.appendChild(this.renderer.domElement); if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
return this; return this;
}; };
public addSky = () => { public addSky = () => {
const sky = new Sky(); this.sky = new Sky();
sky.scale.setScalar(450000); this.sky.scale.setScalar(450000);
this.scene.add(sky); this.scene.add(this.sky);
const effectController = { const effectController = {
turbidity: 10, turbidity: 10,
rayleigh: 3, rayleigh: 3,
mieCoefficient: 0.005, mieCoefficient: 0.005,
mieDirectionalG: 0.7, mieDirectionalG: 0.7,
elevation: calculateCurrentSunElevation(), elevation: sunCalculator.calculateSunElevation(),
azimuth: 180, azimuth: 180,
exposure: this.renderer.toneMappingExposure exposure: this.renderer.toneMappingExposure
}; };
const uniforms = sky.material.uniforms; const uniforms = this.sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity; uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh; uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient; uniforms['mieCoefficient'].value = effectController.mieCoefficient;
@@ -126,13 +124,14 @@ export default class SceneBuilder {
public addPerspectiveCamera = (options: position) => { public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera(); this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
this.scene.add(this.camera); this.scene.add(this.camera);
return this; return this;
}; };
public addGroundPlane = (options?: position) => { public addGroundPlane = (options?: position) => {
this.ground = new Mesh(new PlaneGeometry(), new ShadowMaterial({ side: 2 })); var planeMaterial = new MeshStandardMaterial({ color: 0x808080, side: 2, opacity: 0.5 });
this.ground = new Mesh(new PlaneGeometry(), planeMaterial);
this.ground.rotation.x = -Math.PI / 2; this.ground.rotation.x = -Math.PI / 2;
this.ground.scale.setScalar(30); this.ground.scale.setScalar(30);
this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0); this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0);
@@ -141,11 +140,12 @@ export default class SceneBuilder {
return this; return this;
}; };
public addOrbitControls = (minDistance: number, maxDistance: number) => { public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.controls = new OrbitControls(this.camera, this.renderer.domElement); this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.minDistance = minDistance; this.orbit.minDistance = minDistance;
this.controls.maxDistance = maxDistance; this.orbit.maxDistance = maxDistance;
this.controls.update(); this.orbit.autoRotate = autoRotate;
this.orbit.update();
return this; return this;
}; };
@@ -158,11 +158,13 @@ export default class SceneBuilder {
public addDirectionalLight = (options: directionalLight) => { public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity); const directionalLight = new DirectionalLight(options.color, options.intensity);
directionalLight.castShadow = true; directionalLight.castShadow = true;
directionalLight.shadow.mapSize.setScalar(2048); directionalLight.shadow.camera.top = 10;
directionalLight.shadow.mapSize.width = 1024; directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.mapSize.height = 1024; directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.left = -10;
directionalLight.shadow.mapSize.set(4096, 4096);
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
directionalLight.shadow.radius = 5;
this.scene.add(directionalLight); this.scene.add(directionalLight);
return this; return this;
}; };
@@ -182,10 +184,20 @@ export default class SceneBuilder {
return this; return this;
}; };
public handleResize = () => { public fillParent = () => {
this.renderer.setSize(window.innerWidth, window.innerHeight); const parentElement = this.renderer.domElement.parentElement;
if (parentElement) {
const width = parentElement.clientWidth;
const height = parentElement.clientHeight;
this.handleResize(width, height);
}
return this;
};
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height);
this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = window.innerWidth / window.innerHeight; this.camera.aspect = width / height;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
return this; return this;
}; };
@@ -197,8 +209,8 @@ export default class SceneBuilder {
public startRenderLoop = () => { public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => { this.renderer.setAnimationLoop(() => {
this.controls.update();
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
this.orbit.update();
this.handleRobotShadow(); this.handleRobotShadow();
if (this.callback) this.callback(); if (this.callback) this.callback();
if (!this.liveStreamTexture) return; if (!this.liveStreamTexture) return;
@@ -259,9 +271,23 @@ export default class SceneBuilder {
traverse(m); traverse(m);
}; };
public addTransformControls = (model: any) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
this.transformControl.addEventListener('dragging-changed', (event: any) => {
this.orbit.enabled = !event.value;
this.isDragging = !event.value;
});
this.transformControl.attach(model);
this.scene.add(this.transformControl);
this.transformControl.setMode('rotate');
return this;
};
public addModel = (model: any) => { public addModel = (model: any) => {
this.modelGroup = new Group();
this.modelGroup.add(model);
this.model = model; this.model = model;
this.scene.add(model); this.scene.add(this.modelGroup);
return this; return this;
}; };
@@ -283,8 +309,14 @@ export default class SceneBuilder {
this.setJointValue(joint.name, angle); this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle); updateAngle(joint.name, angle);
}; };
dragControls.onDragStart = () => (this.controls.enabled = false); dragControls.onDragStart = () => {
dragControls.onDragEnd = () => (this.controls.enabled = true); this.orbit.enabled = false;
this.isDragging = true;
};
dragControls.onDragEnd = () => {
this.orbit.enabled = true;
this.isDragging = false;
};
dragControls.onHover = (joint: URDFMimicJoint) => dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial); this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) => dragControls.onUnhover = (joint: URDFMimicJoint) =>
-1
View File
@@ -1,3 +1,2 @@
export { default as fileService } from './file-service'; export { default as fileService } from './file-service';
export { default as socketService } from './socket-service';
export { default as resultService } from './result-service'; export { default as resultService } from './result-service';
-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();
+55
View File
@@ -0,0 +1,55 @@
import { type Analytics } from '$lib/types/models';
import { writable } from 'svelte/store';
let 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();
+36
View File
@@ -0,0 +1,36 @@
import { type IMU } from '$lib/types/models';
import { writable } from 'svelte/store';
let imu_data = {
x: <number[]>[],
y: <number[]>[],
z: <number[]>[],
imu_temp: <number[]>[],
altitude: <number[]>[],
pressure: <number[]>[],
bmp_temp: <number[]>[]
};
const maxIMUData = 100;
function createIMU() {
const { subscribe, update } = writable(imu_data);
return {
subscribe,
addData: (content: IMU) => {
update((imu_data) => ({
...imu_data,
x: [...imu_data.x, content.x].slice(-maxIMUData),
y: [...imu_data.y, content.y].slice(-maxIMUData),
z: [...imu_data.z, content.z].slice(-maxIMUData),
imu_temp: [...imu_data.imu_temp, content.imu_temp].slice(-maxIMUData),
altitude: [...imu_data.altitude, content.altitude].slice(-maxIMUData),
pressure: [...imu_data.pressure, content.pressure].slice(-maxIMUData),
bmp_temp: [...imu_data.bmp_temp, content.bmp_temp].slice(-maxIMUData)
}));
}
};
}
export const imu = createIMU();
+1
View File
@@ -1,3 +1,4 @@
export * from './socket-store'; export * from './socket-store';
export * from './logging-store'; export * from './logging-store';
export * from './model-store'; export * from './model-store';
export * from './socket';
+29
View File
@@ -0,0 +1,29 @@
import { writable } from 'svelte/store';
export type LidarPoint = {
distance: number;
angle: number;
quality: number;
};
let lidar_data = {
points: <LidarPoint[]>[]
};
const maxLidarData = 600;
function createLidar() {
const { subscribe, update } = writable(lidar_data);
return {
subscribe,
addData: (lidarPoint: LidarPoint) => {
update((lidar_data) => ({
...lidar_data,
points: [...lidar_data.points, lidarPoint].slice(-maxLidarData)
}));
}
};
}
export const lidar = createLidar();
+19 -6
View File
@@ -1,5 +1,5 @@
import type { ControllerInput } from '$lib/models'; import type { ControllerInput } from '$lib/models';
import { persistentStore } from '$lib/utilities'; import { persistentStore } from '$lib/utilities/svelte-utilities';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
export const emulateModel = writable(true); export const emulateModel = writable(true);
@@ -8,17 +8,30 @@ export const jointNames = persistentStore('joint_names', []);
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', 'crawl', '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,
Idle,
Calibration,
Rest,
Stand,
Crawl,
Walk
}
export const outControllerData = writable(new Int8Array([0, 0, 0, 0, 0, 0, 70, 0])); export const mode: Writable<ModesEnum> = writable(ModesEnum.Walk);
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]);
export const kinematicData = writable([0, 0, 0, 0, 1, 0]);
export const input: Writable<ControllerInput> = writable({ export const input: Writable<ControllerInput> = writable({
left: { x: 0, y: 0 }, left: { x: 0, y: 0 },
right: { x: 0, y: 0 }, right: { x: 0, y: 0 },
height: 70, height: 50,
speed: 0 speed: 50,
s1: 50
}); });
+27
View File
@@ -0,0 +1,27 @@
import { readable } from 'svelte/store';
export const heading = readable(0, (set) => {
const updateHeading = (e: any) => {
let alpha;
if (e.webkitCompassHeading) alpha = e.webkitCompassHeading;
else if (e.alpha) alpha = e.alpha;
else {
let q = e.target.quaternion;
alpha =
Math.atan2(2 * q[0] * q[1] + 2 * q[2] * q[3], 1 - 2 * q[1] * q[1] - 2 * q[2] * q[2]) *
(180 / Math.PI);
if (alpha < 0) alpha += 360;
}
set(alpha);
};
if ('AbsoluteOrientationSensor' in window) {
var sensor = new window.AbsoluteOrientationSensor({ frequency: 60 }) as any;
sensor.addEventListener('reading', updateHeading);
sensor.start();
} else if (window.DeviceMotionEvent) window.addEventListener('deviceorientation', updateHeading);
return () => {
if ('AbsoluteOrientationSensor' in window) sensor.removeEventListener('reading', updateHeading);
window.addEventListener('deviceorientation', updateHeading);
};
});
+8 -9
View File
@@ -1,14 +1,17 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
import { type angles } from '$lib/models'; import { type angles } from '$lib/models';
export const isConnected = writable(false); export const servoAnglesOut: Writable<number[]> = writable([
export const servoAngles: Writable<angles> = writable(new Int16Array(12).fill(0)); 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]);
export const servoAngles: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]);
export const logs = writable([] as string[]); export const logs = writable([] as string[]);
export const battery = writable({}); export const battery = writable({});
export const mpu = writable({ heading: 0 }); export const mpu = writable({ heading: 0 });
export const sonar = writable([0, 0]);
export const distances = writable({}); export const distances = writable({});
export const settings = writable({});
export const systemInfo = writable({} as number);
export interface socketDataCollection { export interface socketDataCollection {
angles: Writable<angles>; angles: Writable<angles>;
@@ -16,8 +19,6 @@ export interface socketDataCollection {
battery: Writable<unknown>; battery: Writable<unknown>;
mpu: Writable<unknown>; mpu: Writable<unknown>;
distances: Writable<unknown>; distances: Writable<unknown>;
settings: Writable<unknown>;
systemInfo: Writable<unknown>;
} }
export const socketData = { export const socketData = {
@@ -25,7 +26,5 @@ export const socketData = {
logs, logs,
battery, battery,
mpu, mpu,
distances, distances
settings,
systemInfo
}; };
+122
View File
@@ -0,0 +1,122 @@
import { writable } from 'svelte/store';
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
type SocketEvent = (typeof socketEvents)[number];
function createWebSocket() {
let listeners = new Map<string, Set<(data?: unknown) => void>>();
const { subscribe, set } = writable(false);
const reconnectTimeoutTime = 5000;
let unresponsiveTimeoutId: number;
let reconnectTimeoutId: number;
let ws: WebSocket;
let socketUrl: string | URL;
function init(url: string | URL) {
socketUrl = url;
connect();
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close();
set(false);
clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach((listener) => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
}
function connect() {
ws = new WebSocket(socketUrl);
ws.onopen = (ev) => {
set(true);
clearTimeout(reconnectTimeoutId);
listeners.get('open')?.forEach((listener) => listener(ev));
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue;
subscribeToEvent(event);
}
};
ws.onmessage = (message) => {
resetUnresponsiveCheck();
let data = message.data;
if (data instanceof ArrayBuffer) {
listeners.get('binary')?.forEach((listener) => listener(data));
return;
}
data = data.substring(1);
if (!data) return;
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
try {
payload = JSON.parse(payload);
} catch (error) {}
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
};
ws.onerror = (ev) => disconnect('error', ev);
ws.onclose = (ev) => disconnect('close', ev);
}
function unsubscribe(event: string, listener?: (data: any) => void) {
let eventListeners = listeners.get(event);
if (!eventListeners) return;
if (!eventListeners.size) {
unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId);
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
}
function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(`2/${event}[${JSON.stringify(data)}]`);
}
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('1/' + event);
}
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('0/' + event);
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: any) => void);
return () => {
unsubscribe(event, listener);
};
},
off: (event: string, listener?: (data: any) => void) => {
unsubscribe(event, listener);
}
};
}
export const socket = createWebSocket();
+45
View File
@@ -0,0 +1,45 @@
import type { Battery, DownloadOTA } from '$lib/types/models';
import { writable } from 'svelte/store';
let telemetry_data = {
rssi: {
rssi: 0
},
battery: {
voltage: 100,
current: false
},
download_ota: {
status: 'none',
progress: 0,
error: ''
}
};
function createTelemetry() {
const { subscribe, set, update } = writable(telemetry_data);
return {
subscribe,
setRSSI: (data: number) => {
update((telemetry_data) => ({
...telemetry_data,
rssi: { rssi: data }
}));
},
setBattery: (data: Battery) => {
update((telemetry_data) => ({
...telemetry_data,
battery: { voltage: data.voltage, current: data.current }
}));
},
setDownloadOTA: (data: DownloadOTA) => {
update((telemetry_data) => ({
...telemetry_data,
download_ota: { status: data.status, progress: data.progress, error: data.error }
}));
}
};
}
export const telemetry = createTelemetry();
+55
View File
@@ -0,0 +1,55 @@
import { writable } from 'svelte/store';
import { goto } from '$app/navigation';
import { jwtDecode } from 'jwt-decode';
export type userProfile = {
username: string;
admin: boolean;
bearer_token: string;
};
type decodedJWT = {
username: string;
admin: boolean;
};
let empty = {
username: '',
admin: false,
bearer_token: ''
};
function createStore() {
const { subscribe, set } = writable(empty);
// retrieve store from sessionStorage / localStorage if available
const userdata = localStorage.getItem('user');
if (userdata) {
set(JSON.parse(userdata));
}
return {
subscribe,
init: (access_token: string) => {
const decoded: decodedJWT = jwtDecode(access_token);
const userdata = {
bearer_token: access_token,
username: decoded.username,
admin: decoded.admin
};
set(userdata);
// persist store in sessionStorage / localStorage
localStorage.setItem('user', JSON.stringify(userdata));
},
invalidate: () => {
console.log('Log out user');
set(empty);
// remove localStorage "user"
localStorage.removeItem('user');
// redirect to login page
goto('/');
}
};
}
export const user = createStore();
+143
View File
@@ -0,0 +1,143 @@
export type WifiStatus = {
status: number;
local_ip: string;
mac_address: string;
rssi: number;
ssid: string;
bssid: string;
channel: number;
subnet_mask: string;
gateway_ip: string;
dns_ip_1: string;
dns_ip_2?: string;
};
export type WifiSettings = {
hostname: string;
priority_RSSI: boolean;
wifi_networks: KnownNetworkItem[];
};
export type KnownNetworkItem = {
ssid: string;
password: string;
static_ip_config: boolean;
local_ip?: string;
subnet_mask?: string;
gateway_ip?: string;
dns_ip_1?: string;
dns_ip_2?: string;
};
export type NetworkItem = {
rssi: number;
ssid: string;
bssid: string;
channel: number;
encryption_type: number;
};
export type ApStatus = {
status: number;
ip_address: string;
mac_address: string;
station_num: number;
};
export type ApSettings = {
provision_mode: number;
ssid: string;
password: string;
channel: number;
ssid_hidden: boolean;
max_clients: number;
local_ip: string;
gateway_ip: string;
subnet_mask: string;
};
export type NTPStatus = {
status: number;
utc_time: string;
local_time: string;
server: string;
uptime: number;
};
export type RSSI = {
rssi: number;
ssid: string;
};
export type Battery = {
voltage: number;
current: boolean;
};
export type DownloadOTA = {
status: string;
progress: number;
error: string;
};
export type NTPSettings = {
enabled: boolean;
server: string;
tz_label: string;
tz_format: string;
};
export type Analytics = {
max_alloc_heap: number;
psram_size: number;
free_psram: number;
free_heap: number;
total_heap: number;
min_free_heap: number;
core_temp: number;
fs_total: number;
fs_used: number;
uptime: number;
cpu0_usage: number;
cpu1_usage: number;
cpu_usage: number;
};
export type Rssi = {
rssi: number;
ssid: string;
};
export type StaticSystemInformation = {
esp_platform: string;
firmware_version: string;
cpu_freq_mhz: number;
cpu_type: string;
cpu_rev: number;
cpu_cores: number;
sketch_size: number;
free_sketch_space: number;
sdk_version: string;
arduino_version: string;
flash_chip_size: number;
flash_chip_speed: number;
cpu_reset_reason: string;
};
export type SystemInformation = Analytics & StaticSystemInformation;
export type IMU = {
x: number;
y: number;
z: number;
imu_temp: number;
altitude: number;
bmp_temp: number;
pressure: number;
};
export interface I2CDevice {
address: number;
part_number: string;
name: string;
};
+4 -5
View File
@@ -1,10 +1,9 @@
export const hostname = window.location.hostname; export const hostname = 'localhost'; //window.location.hostname;
export const isSecure = window.location.protocol === 'https:'; export const isSecure = true; // window.location.protocol === 'https:';
export const location = import.meta.env.VITE_API_URL.replace('hostname', hostname); export const location = 'localhost:5173'; //window.location; //import.meta.env.VITE_API_URL.replace('hostname', hostname);
const socketScheme = isSecure ? 'wss://' : 'ws://'; const socketScheme = isSecure ? 'wss://' : 'ws://';
export const socketLocation = export const socketLocation = socketScheme + location; // import.meta.env.VITE_SOCKET_URL.replace('hostname', hostname);
socketScheme + import.meta.env.VITE_SOCKET_URL.replace('hostname', hostname);
+7
View File
@@ -9,3 +9,10 @@ export const toInt8 = (number: number, min: number, max: number) => {
let scaled = ((number - min) / (max - min)) * 255 - 128; let scaled = ((number - min) / (max - min)) * 255 - 128;
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0; return Math.max(-128, Math.min(127, Math.round(scaled))) | 0;
}; };
export const fromInt8 = (int8: number, min: number, max: number) => {
int8 = Math.max(-128, Math.min(127, int8));
const scaled = (int8 + 128) / 255;
const number = scaled * (max - min) + min;
return number;
};
+1 -1
View File
@@ -33,7 +33,7 @@ export const loadModelAsync = async (
resolve(Result.err('Failed to load model', error)); resolve(Result.err('Failed to load model', error));
} }
}, },
(error) => reject(error) (error) => resolve(Result.err('Failed to load model', error))
); );
}); });
}; };
@@ -0,0 +1,84 @@
class SunCalculator {
calculateSunElevation(lat: number = 55, lon: number = 12) {
const now = new Date();
const JD = this.getJulianDate(now);
const solarDec = this.getSolarDeclination(JD);
const solarTime = this.getSolarTime(now, lon);
const hourAngle = (solarTime - 12) * 15;
const elevation = Math.asin(
Math.sin(this.degToRad(lat)) * Math.sin(solarDec) +
Math.cos(this.degToRad(lat)) * Math.cos(solarDec) * Math.cos(this.degToRad(hourAngle))
);
return this.radToDeg(elevation);
}
getJulianDate(date: Date) {
const Y = date.getUTCFullYear();
const M = date.getUTCMonth() + 1;
const D =
date.getUTCDate() +
date.getUTCHours() / 24 +
date.getUTCMinutes() / 1440 +
date.getUTCSeconds() / 86400;
const A = Math.floor((14 - M) / 12);
const Y1 = Y + 4800 - A;
const M1 = M + 12 * A - 3;
return (
D +
Math.floor((153 * M1 + 2) / 5) +
365 * Y1 +
Math.floor(Y1 / 4) -
Math.floor(Y1 / 100) +
Math.floor(Y1 / 400) -
32045
);
}
getSolarDeclination(JulianDate: number) {
const n = JulianDate - 2451545;
const L = (280.46 + 0.9856474 * n) % 360;
const g = this.degToRad((357.528 + 0.9856003 * n) % 360);
const lambda = this.degToRad(L + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g));
return Math.asin(Math.sin(lambda) * Math.sin(this.degToRad(23.44)));
}
getSolarTime(date: Date, lon: number) {
const EoT = this.getEquationOfTime(date);
const offset = date.getTimezoneOffset() / 60;
const standardMeridian = Math.round(lon / 15) * 15;
const solarTime =
date.getUTCHours() +
(date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 -
offset;
return (solarTime + 24) % 24;
}
getEquationOfTime(date: Date) {
const JD = this.getJulianDate(date);
const n = JD - 2451545;
const g = this.degToRad((357.528 + 0.9856003 * n) % 360);
const q = this.degToRad((280.46 + 0.9856474 * n) % 360);
return (
4 *
this.radToDeg(
0.000075 +
0.001868 * Math.cos(q) -
0.032077 * Math.sin(g) -
0.014615 * Math.cos(2 * q) -
0.040849 * Math.sin(2 * g)
)
);
}
degToRad(deg: number) {
return deg * (Math.PI / 180);
}
radToDeg(rad: number) {
return rad * (180 / Math.PI);
}
}
export const sunCalculator = new SunCalculator();
+27
View File
@@ -7,3 +7,30 @@ export const humanFileSize = (size: number): string => {
export const capitalize = (str: string): string => { export const capitalize = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}; };
export const convertSeconds = (seconds: number) => {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
};
+3 -2
View File
@@ -1,14 +1,15 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { browser } from '$app/environment';
export const isEmbeddedApp = import.meta.env.VITE_EMBEDDED_BUILD === 'true'; export const isEmbeddedApp = import.meta.env.VITE_EMBEDDED_BUILD === 'true';
export const persistentStore = (key: string, initialValue: any) => { export const persistentStore = (key: string, initialValue: any) => {
const savedValue = JSON.parse(localStorage.getItem(key) as string); const savedValue = browser ? JSON.parse(localStorage.getItem(key) as string) : null;
const data = savedValue !== null ? savedValue : initialValue; const data = savedValue !== null ? savedValue : initialValue;
const store = writable(data); const store = writable(data);
store.subscribe((value) => { store.subscribe((value) => {
localStorage.setItem(key, JSON.stringify(value)); browser && localStorage.setItem(key, JSON.stringify(value));
}); });
return store; return store;
-9
View File
@@ -1,9 +0,0 @@
import './app.css';
import './index.css';
import App from './App.svelte';
const app = new App({
target: document.getElementById('app') as HTMLElement
});
export default app;
+15
View File
@@ -0,0 +1,15 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
setTimeout(() => {
goto('/');
}, 3000);
});
</script>
<div class="flex justify-center items-center w-full h-full">
<h1 class="text-4xl">404 - Page not found</h1>
<p>You will be redirected to the home page in 3 seconds</p>
</div>
+129
View File
@@ -0,0 +1,129 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { user } from '$lib/stores/user';
import { telemetry } from '$lib/stores/telemetry';
import { analytics } from '$lib/stores/analytics';
import type { userProfile } from '$lib/stores/user';
import { page } from '$app/stores';
import { Modals, closeModal } from 'svelte-modals';
import Toast from '$lib/components/toasts/Toast.svelte';
import { notifications } from '$lib/components/toasts/notifications';
import { fade } from 'svelte/transition';
import '../app.css';
import Menu from './menu.svelte';
import Statusbar from './statusbar.svelte';
import Login from './login.svelte';
import { ModesEnum, kinematicData, mode, outControllerData, servoAngles, servoAnglesOut, socket } from '$lib/stores';
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
import { api } from '$lib/api';
onMount(async () => {
if ($user.bearer_token !== '') {
await validateUser($user);
}
const ws_token = $page.data.features.security ? '?access_token=' + $user.bearer_token : '';
socket.init(`ws://${window.location.host}/ws/events${ws_token}`);
addEventListeners();
outControllerData.subscribe((data) => socket.sendEvent("input", {data}));
mode.subscribe((data) => socket.sendEvent("mode", {data}));
servoAnglesOut.subscribe((data) => socket.sendEvent("angles", {data}));
kinematicData.subscribe((data) => socket.sendEvent("position", {data}));
});
onDestroy(() => {
removeEventListeners();
});
const addEventListeners = () => {
socket.on('open', handleOpen);
socket.on('close', handleClose);
socket.on('error', handleError);
socket.on('rssi', handleNetworkStatus);
socket.on('mode', (data:ModesEnum) => mode.set(data));
socket.on('angles', (angles:number[]) => { if (angles.length) servoAngles.set(angles)});
if ($page.data.features.analytics) socket.on('analytics', handleAnalytics);
if ($page.data.features.battery) socket.on('battery', handleBattery);
if ($page.data.features.download_firmware) socket.on('otastatus', handleOAT);
if ($page.data.features.sonar) socket.on('sonar', data => console.log(data))
};
const removeEventListeners = () => {
socket.off('analytics', handleAnalytics);
socket.off('open', handleOpen);
socket.off('close', handleClose);
socket.off('rssi', handleNetworkStatus);
socket.off('battery', handleBattery);
socket.off('otastatus', handleOAT);
};
async function validateUser(userdata: userProfile) {
const result = await api.get('/api/verifyAuthorization')
if (result.isErr()){
user.invalidate();
console.error('Error:', result.inner);
}
}
const handleOpen = () => {
notifications.success('Connection to device established', 5000);
};
const handleClose = () => {
notifications.error('Connection to device lost', 5000);
telemetry.setRSSI(0);
};
const handleError = (data: any) => console.error(data);
const handleAnalytics = (data: Analytics) => analytics.addData(data);
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
const handleBattery = (data: Battery) => telemetry.setBattery(data);
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
let menuOpen = false;
</script>
<svelte:head>
<title>{$page.data.title}</title>
</svelte:head>
{#if $page.data.features.security && $user.bearer_token === ''}
<Login />
{:else}
<div class="drawer">
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
<div class="drawer-content flex flex-col">
<!-- Status bar content here -->
<Statusbar />
<!-- Main page content here -->
<slot />
</div>
<!-- Side Navigation -->
<div class="drawer-side z-30 shadow-lg">
<label for="main-menu" class="drawer-overlay" />
<Menu
on:menuClicked={() => menuOpen = false}
/>
</div>
</div>
{/if}
<Modals>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
slot="backdrop"
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur"
transition:fade
on:click={closeModal}
/>
</Modals>
<Toast />
+40
View File
@@ -0,0 +1,40 @@
import { jointNames, model } from '$lib/stores';
import { loadModelAsync } from '$lib/utilities/model-utilities';
export const prerender = true;
export const ssr = false;
const registerFetchIntercept = async () => {
const { fetch: originalFetch } = window;
const fileService = (await import('$lib/services/file-service')).default;
window.fetch = async (resource, config) => {
let url = resource instanceof Request ? resource.url : resource.toString();
let file = await fileService.getFile(url);
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
};
};
const loadModelFiles = async () => {
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner;
jointNames.set(JOINT_NAME);
model.set(urdf);
} else {
console.error(modelRes.inner, { exception: modelRes.exception });
}
};
export const load = async ({ fetch }) => {
await registerFetchIntercept();
await loadModelFiles();
const result = await fetch('/api/features');
const features = await result.json();
return {
features,
title: 'Spot micro controller',
github: 'runeharlyk/SpotMicroESP32-Leika',
app_name: 'Spot Micro Controller',
copyright: '2024 Rune Harlyk'
};
};
+24
View File
@@ -0,0 +1,24 @@
<script lang="ts">
import type { PageData } from './$types';
import { notifications } from '$lib/components/toasts/notifications';
import Visualization from '$lib/components/Visualization.svelte';
export let data: PageData;
</script>
<div class="hero bg-base-100 h-screen">
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
<div class="w-64 h-64">
<Visualization sky={false} orbit panel={false} ground={false}/>
</div>
<div class="card-body w-80">
<h2 class="card-title text-center text-2xl">Welcome to {data.app_name}</h2>
<p class="py-6 text-center"></p>
<a
class="btn btn-primary"
href="/controller"
on:click={() => notifications.success('You did it!', 1000)}>Begin</a
>
</div>
</div>
</div>
-15
View File
@@ -1,15 +0,0 @@
<script lang="ts">
import Stream from '$components/Views/Stream.svelte';
import Model from '$components/Views/Model.svelte';
import Controls from '$components/Controls.svelte';
import { emulateModel } from '$lib/stores';
</script>
<div class="flex justify-center items-center w-full h-full">
{#if $emulateModel}
<Model />
{:else}
<Stream />
{/if}
<Controls />
</div>
-62
View File
@@ -1,62 +0,0 @@
<script lang="ts">
import { Link, Route, Router } from 'svelte-routing';
import Info from '../components/settings/Info.svelte';
import Log from '../components/settings/Log.svelte';
import Configuration from '../components/settings/Configuration.svelte';
import {
Icon,
InformationCircle,
BookOpen,
AdjustmentsVertical,
Cog6Tooth
} from 'svelte-hero-icons';
import Calibration from '../components/settings/Calibration.svelte';
export const page = '';
const menu = [
{
title: 'Calibration',
path: '/calibration',
icon: AdjustmentsVertical,
component: Calibration
},
{
title: 'System info',
path: '/info',
icon: InformationCircle,
component: Info
},
{
title: 'Log',
path: '/log',
icon: BookOpen,
component: Log
},
{
title: 'Settings',
path: '/settings',
icon: Cog6Tooth,
component: Configuration
}
];
</script>
<div class="pt-14 flex h-full">
<nav class="w-1/6 flex flex-col">
{#each menu as link}
<Link to={'/settings' + link.path}>
<div class="px-4 py-2 flex gap-2 items-center">
<Icon src={link.icon} size="24" />{link.title}
</div>
</Link>
{/each}
</nav>
<main class="w-full h-full">
<Router>
{#each menu as link}
<Route path={link.path} component={link.component}></Route>
{/each}
</Router>
</main>
</div>
+7
View File
@@ -0,0 +1,7 @@
import type { PageLoad } from './$types';
import { goto } from '$app/navigation';
export const load = (async () => {
goto('/');
return;
}) satisfies PageLoad;
@@ -0,0 +1,7 @@
<script lang="ts">
import NTP from './NTP.svelte';
</script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
<NTP />
</div>
+7
View File
@@ -0,0 +1,7 @@
import type { PageLoad } from './$types';
export const load = (async () => {
return {
title: 'NTP'
};
}) satisfies PageLoad;
+263
View File
@@ -0,0 +1,263 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import Collapsible from '$lib/components/Collapsible.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import { notifications } from '$lib/components/toasts/notifications';
import { TIME_ZONES } from './timezones';
import NTP from '~icons/tabler/clock-check';
import Server from '~icons/tabler/server';
import Clock from '~icons/tabler/clock';
import UTC from '~icons/tabler/clock-pin';
import Stopwatch from '~icons/tabler/24-hours';
import type { NTPSettings, NTPStatus } from '$lib/types/models';
import { api } from '$lib/api';
let ntpSettings: NTPSettings;
let ntpStatus: NTPStatus;
async function getNTPStatus() {
const result = await api.get<NTPStatus>('/api/ntpStatus');
if (result.isErr()){
console.error('Error:', result.inner);
return
}
ntpStatus = result.inner
}
async function getNTPSettings() {
const result = await api.get<NTPSettings>('/api/ntpSettings');
if (result.isErr()){
console.error('Error:', result.inner);
return
}
ntpSettings = result.inner
}
const interval = setInterval(async () => {
getNTPStatus();
}, 5000);
onDestroy(() => clearInterval(interval));
onMount(() => {
if (!$page.data.features.security || $user.admin) {
getNTPSettings();
}
});
let formField: any;
let formErrors = {
server: false
};
async function postNTPSettings(data: NTPSettings) {
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
if (result.isErr()){
notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner);
return
}
ntpSettings = result.inner
}
function handleSubmitNTP() {
let valid = true;
// Validate Server
// RegEx for IPv4
const regexExpIPv4 =
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
const regexExpURL =
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
valid = false;
formErrors.server = true;
} else {
formErrors.server = false;
}
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
// Submit JSON to REST API
if (valid) {
postNTPSettings(ntpSettings);
//alert('Form Valid');
}
}
function convertSeconds(seconds: number) {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
}
</script>
<SettingsCard collapsible={false}>
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
<span slot="title">Network Time</span>
<div class="w-full overflow-x-auto">
{#await getNTPStatus()}
<Spinner />
{:then nothing}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
? 'bg-success'
: 'bg-error'}"
>
<NTP
class="h-auto w-full scale-75 {ntpStatus.status === 1
? 'text-success-content'
: 'text-error-content'}"
/>
</div>
<div>
<div class="font-bold">Status</div>
<div class="text-sm opacity-75">
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Server class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">NTP Server</div>
<div class="text-sm opacity-75">
{ntpStatus.server}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Clock class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Local Time</div>
<div class="text-sm opacity-75">
{new Intl.DateTimeFormat('en-GB', {
dateStyle: 'long',
timeStyle: 'long'
}).format(new Date(ntpStatus.local_time))}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<UTC class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">UTC Time</div>
<div class="text-sm opacity-75">
{new Intl.DateTimeFormat('en-GB', {
dateStyle: 'long',
timeStyle: 'long',
timeZone: 'UTC'
}).format(new Date(ntpStatus.utc_time))}
</div>
</div>
</div>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10">
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">Uptime</div>
<div class="text-sm opacity-75">
{convertSeconds(ntpStatus.uptime)}
</div>
</div>
</div>
</div>
{/await}
</div>
{#if !$page.data.features.security || $user.admin}
<Collapsible open={false} on:closed={getNTPSettings}>
<span slot="title">Change NTP Settings</span>
<form
class="form-control w-full"
on:submit|preventDefault={handleSubmitNTP}
novalidate
bind:this={formField}
>
<label class="label cursor-pointer justify-start gap-4">
<input
type="checkbox"
bind:checked={ntpSettings.enabled}
class="checkbox checkbox-primary"
/>
<span class="">Enable NTP</span>
</label>
<label class="label" for="server">
<span class="label-text text-md">Server</span>
</label>
<input
type="text"
min="3"
max="64"
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
? 'border-error border-2'
: ''}"
bind:value={ntpSettings.server}
id="server"
required
/>
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
>Must be a valid IPv4 address or URL</span
>
</label>
<label class="label" for="tz">
<span class="label-text text-md">Pick Time Zone</span>
</label>
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
<option value={tz_label}>{tz_label}</option>
{/each}
</select>
<div class="mt-6 place-self-end">
<button class="btn btn-primary" type="submit">Apply Settings</button>
</div>
</form>
</Collapsible>
{/if}
</SettingsCard>
+466
View File
@@ -0,0 +1,466 @@
export type TimeZones = {
[name: string]: string
};
export const TIME_ZONES: TimeZones = {
"Africa/Abidjan": "GMT0",
"Africa/Accra": "GMT0",
"Africa/Addis_Ababa": "EAT-3",
"Africa/Algiers": "CET-1",
"Africa/Asmara": "EAT-3",
"Africa/Bamako": "GMT0",
"Africa/Bangui": "WAT-1",
"Africa/Banjul": "GMT0",
"Africa/Bissau": "GMT0",
"Africa/Blantyre": "CAT-2",
"Africa/Brazzaville": "WAT-1",
"Africa/Bujumbura": "CAT-2",
"Africa/Cairo": "EET-2",
"Africa/Casablanca": "UNK-1",
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Africa/Conakry": "GMT0",
"Africa/Dakar": "GMT0",
"Africa/Dar_es_Salaam": "EAT-3",
"Africa/Djibouti": "EAT-3",
"Africa/Douala": "WAT-1",
"Africa/El_Aaiun": "UNK-1",
"Africa/Freetown": "GMT0",
"Africa/Gaborone": "CAT-2",
"Africa/Harare": "CAT-2",
"Africa/Johannesburg": "SAST-2",
"Africa/Juba": "EAT-3",
"Africa/Kampala": "EAT-3",
"Africa/Khartoum": "CAT-2",
"Africa/Kigali": "CAT-2",
"Africa/Kinshasa": "WAT-1",
"Africa/Lagos": "WAT-1",
"Africa/Libreville": "WAT-1",
"Africa/Lome": "GMT0",
"Africa/Luanda": "WAT-1",
"Africa/Lubumbashi": "CAT-2",
"Africa/Lusaka": "CAT-2",
"Africa/Malabo": "WAT-1",
"Africa/Maputo": "CAT-2",
"Africa/Maseru": "SAST-2",
"Africa/Mbabane": "SAST-2",
"Africa/Mogadishu": "EAT-3",
"Africa/Monrovia": "GMT0",
"Africa/Nairobi": "EAT-3",
"Africa/Ndjamena": "WAT-1",
"Africa/Niamey": "WAT-1",
"Africa/Nouakchott": "GMT0",
"Africa/Ouagadougou": "GMT0",
"Africa/Porto-Novo": "WAT-1",
"Africa/Sao_Tome": "GMT0",
"Africa/Tripoli": "EET-2",
"Africa/Tunis": "CET-1",
"Africa/Windhoek": "CAT-2",
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Anguilla": "AST4",
"America/Antigua": "AST4",
"America/Araguaina": "UNK3",
"America/Argentina/Buenos_Aires": "UNK3",
"America/Argentina/Catamarca": "UNK3",
"America/Argentina/Cordoba": "UNK3",
"America/Argentina/Jujuy": "UNK3",
"America/Argentina/La_Rioja": "UNK3",
"America/Argentina/Mendoza": "UNK3",
"America/Argentina/Rio_Gallegos": "UNK3",
"America/Argentina/Salta": "UNK3",
"America/Argentina/San_Juan": "UNK3",
"America/Argentina/San_Luis": "UNK3",
"America/Argentina/Tucuman": "UNK3",
"America/Argentina/Ushuaia": "UNK3",
"America/Aruba": "AST4",
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
"America/Atikokan": "EST5",
"America/Bahia": "UNK3",
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
"America/Barbados": "AST4",
"America/Belem": "UNK3",
"America/Belize": "CST6",
"America/Blanc-Sablon": "AST4",
"America/Boa_Vista": "UNK4",
"America/Bogota": "UNK5",
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
"America/Campo_Grande": "UNK4",
"America/Cancun": "EST5",
"America/Caracas": "UNK4",
"America/Cayenne": "UNK3",
"America/Cayman": "EST5",
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
"America/Costa_Rica": "CST6",
"America/Creston": "MST7",
"America/Cuiaba": "UNK4",
"America/Curacao": "AST4",
"America/Danmarkshavn": "GMT0",
"America/Dawson": "MST7",
"America/Dawson_Creek": "MST7",
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
"America/Dominica": "AST4",
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
"America/Eirunepe": "UNK5",
"America/El_Salvador": "CST6",
"America/Fort_Nelson": "MST7",
"America/Fortaleza": "UNK3",
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
"America/Grenada": "AST4",
"America/Guadeloupe": "AST4",
"America/Guatemala": "CST6",
"America/Guayaquil": "UNK5",
"America/Guyana": "UNK4",
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
"America/Hermosillo": "MST7",
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
"America/Jamaica": "EST5",
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
"America/Kralendijk": "AST4",
"America/La_Paz": "UNK4",
"America/Lima": "UNK5",
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
"America/Lower_Princes": "AST4",
"America/Maceio": "UNK3",
"America/Managua": "CST6",
"America/Manaus": "UNK4",
"America/Marigot": "AST4",
"America/Martinique": "AST4",
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
"America/Montevideo": "UNK3",
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
"America/Montserrat": "AST4",
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Noronha": "UNK2",
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
"America/Panama": "EST5",
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
"America/Paramaribo": "UNK3",
"America/Phoenix": "MST7",
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
"America/Port_of_Spain": "AST4",
"America/Porto_Velho": "UNK4",
"America/Puerto_Rico": "AST4",
"America/Punta_Arenas": "UNK3",
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
"America/Recife": "UNK3",
"America/Regina": "CST6",
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
"America/Rio_Branco": "UNK5",
"America/Santarem": "UNK3",
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
"America/Santo_Domingo": "AST4",
"America/Sao_Paulo": "UNK3",
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
"America/St_Barthelemy": "AST4",
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
"America/St_Kitts": "AST4",
"America/St_Lucia": "AST4",
"America/St_Thomas": "AST4",
"America/St_Vincent": "AST4",
"America/Swift_Current": "CST6",
"America/Tegucigalpa": "CST6",
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
"America/Tortola": "AST4",
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
"America/Whitehorse": "MST7",
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
"Antarctica/Casey": "UNK-8",
"Antarctica/Davis": "UNK-7",
"Antarctica/DumontDUrville": "UNK-10",
"Antarctica/Macquarie": "UNK-11",
"Antarctica/Mawson": "UNK-5",
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Antarctica/Palmer": "UNK3",
"Antarctica/Rothera": "UNK3",
"Antarctica/Syowa": "UNK-3",
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
"Antarctica/Vostok": "UNK-6",
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Asia/Aden": "UNK-3",
"Asia/Almaty": "UNK-6",
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
"Asia/Anadyr": "UNK-12",
"Asia/Aqtau": "UNK-5",
"Asia/Aqtobe": "UNK-5",
"Asia/Ashgabat": "UNK-5",
"Asia/Atyrau": "UNK-5",
"Asia/Baghdad": "UNK-3",
"Asia/Bahrain": "UNK-3",
"Asia/Baku": "UNK-4",
"Asia/Bangkok": "UNK-7",
"Asia/Barnaul": "UNK-7",
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
"Asia/Bishkek": "UNK-6",
"Asia/Brunei": "UNK-8",
"Asia/Chita": "UNK-9",
"Asia/Choibalsan": "UNK-8",
"Asia/Colombo": "UNK-5:30",
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
"Asia/Dhaka": "UNK-6",
"Asia/Dili": "UNK-9",
"Asia/Dubai": "UNK-4",
"Asia/Dushanbe": "UNK-5",
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
"Asia/Ho_Chi_Minh": "UNK-7",
"Asia/Hong_Kong": "HKT-8",
"Asia/Hovd": "UNK-7",
"Asia/Irkutsk": "UNK-8",
"Asia/Jakarta": "WIB-7",
"Asia/Jayapura": "WIT-9",
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
"Asia/Kabul": "UNK-4:30",
"Asia/Kamchatka": "UNK-12",
"Asia/Karachi": "PKT-5",
"Asia/Kathmandu": "UNK-5:45",
"Asia/Khandyga": "UNK-9",
"Asia/Kolkata": "IST-5:30",
"Asia/Krasnoyarsk": "UNK-7",
"Asia/Kuala_Lumpur": "UNK-8",
"Asia/Kuching": "UNK-8",
"Asia/Kuwait": "UNK-3",
"Asia/Macau": "CST-8",
"Asia/Magadan": "UNK-11",
"Asia/Makassar": "WITA-8",
"Asia/Manila": "PST-8",
"Asia/Muscat": "UNK-4",
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Asia/Novokuznetsk": "UNK-7",
"Asia/Novosibirsk": "UNK-7",
"Asia/Omsk": "UNK-6",
"Asia/Oral": "UNK-5",
"Asia/Phnom_Penh": "UNK-7",
"Asia/Pontianak": "WIB-7",
"Asia/Pyongyang": "KST-9",
"Asia/Qatar": "UNK-3",
"Asia/Qyzylorda": "UNK-5",
"Asia/Riyadh": "UNK-3",
"Asia/Sakhalin": "UNK-11",
"Asia/Samarkand": "UNK-5",
"Asia/Seoul": "KST-9",
"Asia/Shanghai": "CST-8",
"Asia/Singapore": "UNK-8",
"Asia/Srednekolymsk": "UNK-11",
"Asia/Taipei": "CST-8",
"Asia/Tashkent": "UNK-5",
"Asia/Tbilisi": "UNK-4",
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
"Asia/Thimphu": "UNK-6",
"Asia/Tokyo": "JST-9",
"Asia/Tomsk": "UNK-7",
"Asia/Ulaanbaatar": "UNK-8",
"Asia/Urumqi": "UNK-6",
"Asia/Ust-Nera": "UNK-10",
"Asia/Vientiane": "UNK-7",
"Asia/Vladivostok": "UNK-10",
"Asia/Yakutsk": "UNK-9",
"Asia/Yangon": "UNK-6:30",
"Asia/Yekaterinburg": "UNK-5",
"Asia/Yerevan": "UNK-4",
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Cape_Verde": "UNK1",
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
"Atlantic/Reykjavik": "GMT0",
"Atlantic/South_Georgia": "UNK2",
"Atlantic/St_Helena": "GMT0",
"Atlantic/Stanley": "UNK3",
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Brisbane": "AEST-10",
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Darwin": "ACST-9:30",
"Australia/Eucla": "UNK-8:45",
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Lindeman": "AEST-10",
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Australia/Perth": "AWST-8",
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
"Etc/GMT": "GMT0",
"Etc/GMT+0": "GMT0",
"Etc/GMT+1": "UNK1",
"Etc/GMT+10": "UNK10",
"Etc/GMT+11": "UNK11",
"Etc/GMT+12": "UNK12",
"Etc/GMT+2": "UNK2",
"Etc/GMT+3": "UNK3",
"Etc/GMT+4": "UNK4",
"Etc/GMT+5": "UNK5",
"Etc/GMT+6": "UNK6",
"Etc/GMT+7": "UNK7",
"Etc/GMT+8": "UNK8",
"Etc/GMT+9": "UNK9",
"Etc/GMT-0": "GMT0",
"Etc/GMT-1": "UNK-1",
"Etc/GMT-10": "UNK-10",
"Etc/GMT-11": "UNK-11",
"Etc/GMT-12": "UNK-12",
"Etc/GMT-13": "UNK-13",
"Etc/GMT-14": "UNK-14",
"Etc/GMT-2": "UNK-2",
"Etc/GMT-3": "UNK-3",
"Etc/GMT-4": "UNK-4",
"Etc/GMT-5": "UNK-5",
"Etc/GMT-6": "UNK-6",
"Etc/GMT-7": "UNK-7",
"Etc/GMT-8": "UNK-8",
"Etc/GMT-9": "UNK-9",
"Etc/GMT0": "GMT0",
"Etc/Greenwich": "GMT0",
"Etc/UCT": "UTC0",
"Etc/UTC": "UTC0",
"Etc/Universal": "UTC0",
"Etc/Zulu": "UTC0",
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Astrakhan": "UNK-4",
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Istanbul": "UNK-3",
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Kaliningrad": "EET-2",
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Kirov": "UNK-3",
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Minsk": "UNK-3",
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Moscow": "MSK-3",
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Samara": "UNK-4",
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Saratov": "UNK-4",
"Europe/Simferopol": "MSK-3",
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Ulyanovsk": "UNK-4",
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Volgograd": "UNK-4",
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
"Indian/Antananarivo": "EAT-3",
"Indian/Chagos": "UNK-6",
"Indian/Christmas": "UNK-7",
"Indian/Cocos": "UNK-6:30",
"Indian/Comoro": "EAT-3",
"Indian/Kerguelen": "UNK-5",
"Indian/Mahe": "UNK-4",
"Indian/Maldives": "UNK-5",
"Indian/Mauritius": "UNK-4",
"Indian/Mayotte": "EAT-3",
"Indian/Reunion": "UNK-4",
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
"Pacific/Bougainville": "UNK-11",
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
"Pacific/Chuuk": "UNK-10",
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
"Pacific/Efate": "UNK-11",
"Pacific/Enderbury": "UNK-13",
"Pacific/Fakaofo": "UNK-13",
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
"Pacific/Funafuti": "UNK-12",
"Pacific/Galapagos": "UNK6",
"Pacific/Gambier": "UNK9",
"Pacific/Guadalcanal": "UNK-11",
"Pacific/Guam": "ChST-10",
"Pacific/Honolulu": "HST10",
"Pacific/Kiritimati": "UNK-14",
"Pacific/Kosrae": "UNK-11",
"Pacific/Kwajalein": "UNK-12",
"Pacific/Majuro": "UNK-12",
"Pacific/Marquesas": "UNK9:30",
"Pacific/Midway": "SST11",
"Pacific/Nauru": "UNK-12",
"Pacific/Niue": "UNK11",
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
"Pacific/Noumea": "UNK-11",
"Pacific/Pago_Pago": "SST11",
"Pacific/Palau": "UNK-9",
"Pacific/Pitcairn": "UNK8",
"Pacific/Pohnpei": "UNK-11",
"Pacific/Port_Moresby": "UNK-10",
"Pacific/Rarotonga": "UNK10",
"Pacific/Saipan": "ChST-10",
"Pacific/Tahiti": "UNK10",
"Pacific/Tarawa": "UNK-12",
"Pacific/Tongatapu": "UNK-13",
"Pacific/Wake": "UNK-12",
"Pacific/Wallis": "UNK-12"
};
+15
View File
@@ -0,0 +1,15 @@
<script lang="ts">
import Controls from './Controls.svelte';
import { socket } from '$lib/stores';
import Spinner from '$lib/components/Spinner.svelte';
</script>
<div class="select-none">
{#if !$socket}
<div class="absolute left-0 flex flex-col w-screen h-screen justify-center items-center backdrop-blur-sm z-10">
<Spinner/>
<h2>Waiting for connection</h2>
</div>
{/if}
<Controls />
<slot/>
</div>
+15
View File
@@ -0,0 +1,15 @@
<script lang="ts">
import Visualization from "$lib/components/Visualization.svelte";
import Lidar from "$lib/components/Lidar.svelte";
</script>
<div class="grow flex">
<div class="absolute h-screen w-full top-0 flex">
<div class="flex-1 overflow-hidden">
<Visualization debug />
</div>
<div class="flex-1">
<Lidar />
</div>
</div>
</div>
+3
View File
@@ -0,0 +1,3 @@
export const load = async () => {
return { title: 'Controller' };
};
@@ -2,17 +2,15 @@
import nipplejs from 'nipplejs'; import nipplejs from 'nipplejs';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { capitalize, throttler, toInt8 } from '$lib/utilities'; import { capitalize, throttler, toInt8 } from '$lib/utilities';
import { input, outControllerData, mode, modes, type Modes } from '$lib/stores'; import { input, outControllerData, mode, modes, type Modes, ModesEnum, socket } from '$lib/stores';
import type { vector } from '$lib/models'; import type { vector } from '$lib/models';
import Range from './input/Range.svelte';
import Button from './input/Button.svelte';
let throttle = new throttler(); let throttle = new throttler();
let left: nipplejs.JoystickManager; let left: nipplejs.JoystickManager;
let right: nipplejs.JoystickManager; let right: nipplejs.JoystickManager;
let throttle_timing = 40; let throttle_timing = 40;
let data = new Int8Array($outControllerData.length); let data = new Array(8);
onMount(() => { onMount(() => {
left = nipplejs.create({ left = nipplejs.create({
@@ -46,14 +44,14 @@
}; };
const updateData = () => { const updateData = () => {
data[0] = 1; data[0] = 0;
data[1] = 0; data[1] = toInt8($input.left.x, -1, 1);
data[2] = toInt8($input.left.x, -1, 1); data[2] = toInt8($input.left.y, -1, 1);
data[3] = toInt8($input.left.y, -1, 1); data[3] = toInt8($input.right.x, -1, 1);
data[4] = toInt8($input.right.x, -1, 1); data[4] = toInt8($input.right.y, -1, 1);
data[5] = toInt8($input.right.y, -1, 1); data[5] = toInt8($input.height, 0, 100);
data[6] = toInt8($input.height, 0, 100); data[6] = toInt8($input.speed, 0, 100);
data[7] = toInt8($input.speed, 0, 100); data[7] = toInt8($input.s1, 0, 100);
outControllerData.set(data); outControllerData.set(data);
}; };
@@ -70,8 +68,9 @@
throttle.throttle(updateData, throttle_timing); throttle.throttle(updateData, throttle_timing);
}; };
const handleRange = (event:CustomEvent, key: 'speed' | 'height') => { const handleRange = (event:Event, key: 'speed' | 'height' | 's1') => {
const value:number = event.detail const value:number = event.target?.value
input.update((inputData) => { input.update((inputData) => {
inputData[key] = value; inputData[key] = value;
return inputData; return inputData;
@@ -80,7 +79,7 @@
} }
const changeMode = (modeValue: Modes) => { const changeMode = (modeValue: Modes) => {
mode.set(modeValue); mode.set(modes.indexOf(modeValue));
}; };
</script> </script>
@@ -90,22 +89,33 @@
<div class="flex-1" /> <div class="flex-1" />
<div id="right" class="flex w-60 items-center" /> <div id="right" class="flex w-60 items-center" />
</div> </div>
<div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex">
<div class="flex justify-center w-full">
<kbd class="kbd">W</kbd>
</div>
<div class="flex justify-center gap-2 w-full">
<kbd class="kbd">A</kbd>
<kbd class="kbd">S</kbd>
<kbd class="kbd">D</kbd>
</div>
<div class="flex justify-center w-full">
</div>
</div>
<div class="absolute bottom-0 z-10 p-4 gap-4 flex items-end"> <div class="absolute bottom-0 z-10 p-4 gap-4 flex items-end">
{#each modes as modeValue} {#each modes as modeValue}
<div> <button class="btn btn-outline" class:btn-active={$mode === modes.indexOf(modeValue)} on:click={() => changeMode(modeValue)}>
<Button
on:click={() => changeMode(modeValue)}
active={$mode === modeValue}
>
{capitalize(modeValue)} {capitalize(modeValue)}
</Button> </button>
</div>
{/each} {/each}
<div> <div>
{#if $mode === 'walk'} {#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl}
<Range label="Speed" on:value={(e) => handleRange(e, 'speed')}></Range> <label for="s1">S1</label>
<input type="range" name="s1" min="0" max="100" on:input={(e) => handleRange(e, 's1')} class="range range-sm" />
<label for="speed">Speed</label>
<input type="range" name="speed" min="0" max="100" on:input={(e) => handleRange(e, 'speed')} class="range range-sm" />
{/if} {/if}
<Range label="Height" on:value={(e) => handleRange(e, 'height')}></Range> <label for="height">Height</label>
<input type="range" name="height" min="0" max="100" on:input={(e) => handleRange(e, 'height')} class="range range-sm" />
</div> </div>
</div> </div>
</div> </div>

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