204 Commits

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

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