73 Commits

Author SHA1 Message Date
Rune Harlyk 2b1aed91d9 🎨 Simplifies spin skill 2025-09-10 21:07:38 +02:00
Rune Harlyk 923ea17702 🎨 Use std for min and max 2025-09-10 20:21:56 +02:00
Rune Harlyk 3a401abfab Adds skilmanager and spin skill 2025-09-10 15:59:41 +02:00
Rune Harlyk 26c36b8302 🎨 Makes gesture sensor more readable and motion take last gesture 2025-09-10 15:16:00 +02:00
Rune Harlyk bfc259e660 Adds gesture controls 2025-09-10 15:16:00 +02:00
Rune Harlyk 6368bf9213 🎨 Makes use of msg type for sensors 2025-09-10 11:15:44 +02:00
Rune Harlyk cd802f1c22 Makes fsm states by time aware 2025-09-08 22:39:53 +02:00
Rune Harlyk 59bb1d9579 ️ Improves imu speed by making it non blocking and run faster 2025-09-08 22:37:57 +02:00
Rune Harlyk ae98ba76f7 Makes stand imu compensating 2025-09-06 21:02:28 +02:00
Rune Harlyk bd8c8fd988 🐛 Fixes imu handling 2025-09-06 19:55:57 +02:00
Rune Harlyk 7de5a1aa7c 🎨 Lerp gait params to target 2025-09-05 15:22:47 +02:00
Rune Harlyk a3e4fdd8a5 🎨 Moves kinematics config to kinematics file 2025-09-05 14:55:02 +02:00
Rune Harlyk f82fa051f2 🎨 Renames states folder 2025-09-04 23:33:45 +02:00
Rune Harlyk b66ddc3e81 Introduces motion as a state machine 2025-09-04 23:33:45 +02:00
Rune Harlyk c85ac41ebc 🐛 Makes step height dynamic 2025-09-04 21:03:09 +02:00
Rune Harlyk 78d01533f4 Makes body rotation controllable 2025-09-04 19:31:45 +02:00
Rune Harlyk 18d4d66758 Makes robot stand compensate imu 2025-09-04 19:27:48 +02:00
Rune Harlyk 1b9dc9bb9e Makes motion use target position for body state 2025-09-04 19:27:17 +02:00
Rune Harlyk 767d1157df Makes kinematics params be based on config 2025-09-04 19:08:54 +02:00
Rune Harlyk 1799889712 Introduces kinmatics config to sync mapping between variants 2025-09-04 18:02:38 +02:00
Rune Harlyk 0b5d7b1534 Fixes gait into bezier 2025-09-04 17:33:25 +02:00
Rune Harlyk 10b78e6919 🎨 Smoother crawl body shift 2025-09-04 17:33:25 +02:00
Rune Harlyk 3fd72d081e 🎨 Correct behavoir 2025-09-04 17:33:25 +02:00
Rune Harlyk 1f3a465d3e 🎨 Adds speed factor to frontend 2025-09-04 17:33:25 +02:00
Rune Harlyk cddb6023e7 🎨 Better base walking speed 2025-09-04 17:33:25 +02:00
Rune Harlyk 2f46484e0a 🎨 Simplifies gait 2025-09-04 17:33:25 +02:00
Rune Harlyk 4fcaf5d77d 🐛 Try to handle body shifting 2025-09-04 17:33:25 +02:00
Rune Harlyk ea8ddb43ef 🎨 Adds speed factor between gaits 2025-09-04 17:33:25 +02:00
Rune Harlyk 774c546487 🎨 Cleanup crawl 2025-09-04 17:33:25 +02:00
Rune Harlyk 6f46c1f598 🎨 Renames kinematics config 2025-09-04 17:33:25 +02:00
Rune Harlyk bc810ee2dd 🎨 Adds defaults to notification service 2025-09-04 17:33:25 +02:00
Rune Harlyk 54a0419770 🎨 Cleans up gait handling code 2025-09-04 17:33:25 +02:00
Rune Harlyk d7a6bffe0a 🎨 Update the rotation command handling 2025-09-01 22:53:14 +02:00
Rune Harlyk df087decdb 🎨 Renames topics 2025-09-01 18:48:27 +02:00
Rune Harlyk 527764b0b5 🐛 Expands number of endpoints 2025-09-01 18:43:12 +02:00
Rune Harlyk 8c97c68d11 🚩 Add feature flag for spot pico 2025-09-01 18:42:51 +02:00
Rune Harlyk e5bf10cdb0 🎨 Updates and simplifies command handling 2025-09-01 18:41:59 +02:00
Rune Harlyk de3912ff10 Adds kinematics for spot pico 2025-08-22 12:31:22 +02:00
Rune Harlyk 251a791876 Enables better zoom for viz 2025-08-21 23:13:50 +02:00
Rune Harlyk e36365ead6 Adds gif of short walk 2025-08-11 14:40:49 +02:00
Rune Harlyk cb5c095888 🐛 Removes camera endpoint using feature flag 2025-08-03 15:53:49 +02:00
Rune Harlyk 281fa32c89 🐛 Fixes the relative paths 2025-08-02 16:43:45 +02:00
Rune Harlyk d899701195 Simplifies frontend test 2025-07-16 21:58:39 +02:00
Rune Harlyk 7061166fcd 🎨 Matches command mapping in frontend 2025-07-16 21:47:24 +02:00
Rune Harlyk 36b39d41ba 🎨 Replace magic number for stand_frac 2025-07-16 21:44:55 +02:00
Rune Harlyk 7d0a7861ea 🎨 Formats extensions.json 2025-07-16 20:41:28 +02:00
Rune Harlyk bf8c9bce95 📝 Updates readme 2025-07-16 20:40:34 +02:00
Rune Harlyk 9c984d3215 🎨 Inlines cors wildcard 2025-07-16 20:33:12 +02:00
Rune Harlyk 43e76770a8 ️ Removes unnecessary lerp 2025-07-16 20:32:46 +02:00
Rune Harlyk 6e10eabd9f 🔥 Cleans up peripherals service 2025-07-16 20:32:19 +02:00
Rune Harlyk 922a4e3665 🔥 Removes certs 2025-07-16 20:27:33 +02:00
Rune Harlyk 5e162ffb71 ️ Adds build flags for speed and gc 2025-07-16 20:26:21 +02:00
Rune Harlyk f21ce92d43 🐛 Excludes models files for other variants when building 2025-07-12 12:43:07 +02:00
Rune Harlyk 98f3fc674b Makes socket messages event typed 2025-07-11 18:59:07 +02:00
Rune Harlyk c5901c65b3 Adds yertle model visulization 2025-07-11 15:16:47 +02:00
Rune Harlyk 2eab893dd7 🚩 Expands feature flag handling with persistence 2025-07-11 15:16:47 +02:00
Rune Harlyk a3be035f98 🚚 Moves firmware to src and include 2025-07-11 12:16:23 +02:00
Rune Harlyk 743aa073b7 🚀 Makes deploy action run 2025-07-10 23:18:15 +02:00
Rune Harlyk a3de13c619 🔧 Makes default visualization be spot micro 2025-07-10 22:32:27 +02:00
Rune Harlyk 90be771211 🚀 Deploys app 2025-07-10 22:28:05 +02:00
Rune Harlyk 7d79ec39ab Fixes more linter errors 2025-07-10 21:54:38 +02:00
Rune Harlyk 211ff7205b 🔧 Adds env with default variables 2025-07-10 21:54:38 +02:00
Rune Harlyk d0aa3b7b42 💄 Updates colors for metrics chart 2025-07-10 21:54:38 +02:00
Rune Harlyk d529eaa201 Fixes build warning and errors 2025-07-10 21:54:38 +02:00
Rune Harlyk c8ee64d7f4 🐛 Fixes event socket binary serialization buffer length 2025-07-10 20:44:04 +02:00
Rune Harlyk ec4c3fd98e Changes mgspack dependency 2025-07-10 19:04:39 +02:00
Rune Harlyk 0cc372cd36 🐛 Fixes some linting errors 2025-07-10 19:04:39 +02:00
Rune Harlyk 9be405a89d 🐛 Maps frontend gait params same as backend 2025-07-10 19:04:39 +02:00
Rune Harlyk e3cfe89e19 ♻️ Replaces JsonObject with JsonVariant 2025-07-10 19:04:39 +02:00
Rune Harlyk 144b99c180 🔥 Removes debug logging 2025-07-10 19:04:39 +02:00
Rune Harlyk c788e118e3 ️ Adds O3 build flag 2025-07-10 19:04:39 +02:00
Rune Harlyk aae16335b3 ♻️ Centralizes socket serialization 2025-07-10 19:04:39 +02:00
Rune Harlyk a43c250ed1 Adds msgPack and update message protocol 2025-07-10 19:04:39 +02:00
153 changed files with 216637 additions and 5730 deletions
+61
View File
@@ -0,0 +1,61 @@
name: Deploy GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./app
env:
BASE_PATH: /SpotMicroESP32-Leika
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "./app/pnpm-lock.yaml"
- run: pnpm install
- run: pnpm run build
- name: Setup Pages
uses: actions/configure-pages@v4
with:
static_site_generator: "sveltekit"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: app/build/
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
-1
View File
@@ -2,7 +2,6 @@
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
__pycache__/
*.py[cod]
*$py.class
+3 -3
View File
@@ -2,10 +2,10 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
"esbenp.prettier-vscode",
"platformio.platformio-ide",
"svelte.svelte-vscode"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
+3
View File
@@ -0,0 +1,3 @@
PUBLIC_VITE_USE_HOST_NAME=true
PUBLIC_USE_JSON=true
PUBLIC_USE_MSGPACK=true
-1
View File
@@ -3,7 +3,6 @@ node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
+2 -4
View File
@@ -45,6 +45,7 @@
},
"type": "module",
"dependencies": {
"@msgpack/msgpack": "^3.1.2",
"@niku/vite-env-caster": "^1.0.2",
"@sveltejs/adapter-auto": "^4.0.0",
"@tailwindcss/vite": "^4.0.12",
@@ -52,16 +53,13 @@
"compare-versions": "^6.1.0",
"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": "^2.0.0",
"three": "^0.162.0",
"urdf-loader": "^0.12.1",
"uzip": "^0.20201231.0",
"xacro-parser": "^0.3.9",
"@types/msgpack-lite": "^0.1.11",
"msgpack-lite": "^0.1.26"
"xacro-parser": "^0.3.9"
},
"packageManager": "pnpm@9.3.0"
}
+50 -92
View File
@@ -8,18 +8,18 @@ importers:
.:
dependencies:
'@msgpack/msgpack':
specifier: ^3.1.2
version: 3.1.2
'@niku/vite-env-caster':
specifier: ^1.0.2
version: 1.1.2
'@sveltejs/adapter-auto':
specifier: ^4.0.0
version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
'@tailwindcss/vite':
specifier: ^4.0.12
version: 4.0.12(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@types/msgpack-lite':
specifier: ^0.1.11
version: 0.1.11
version: 4.0.12(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
chart.js:
specifier: ^4.4.2
version: 4.4.2
@@ -32,12 +32,6 @@ importers:
daisyui:
specifier: ^5.0.0
version: 5.0.0
jwt-decode:
specifier: ^4.0.0
version: 4.0.0
msgpack-lite:
specifier: ^0.1.26
version: 0.1.26
nipplejs:
specifier: ^0.10.1
version: 0.10.1
@@ -71,13 +65,13 @@ importers:
version: 1.49.1
'@sveltejs/adapter-static':
specifier: ^3.0.1
version: 3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
version: 3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
'@sveltejs/kit':
specifier: ^2.5.27
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/vite-plugin-svelte':
specifier: ^5.0.3
version: 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
version: 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@types/eslint':
specifier: ^8.56.0
version: 8.56.0
@@ -134,10 +128,10 @@ importers:
version: 0.18.5
vite:
specifier: ^6.2.1
version: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
version: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitest:
specifier: ^1.2.0
version: 1.2.0(@types/node@24.0.10)(jsdom@24.0.0)(lightningcss@1.29.2)
version: 1.2.0(@types/node@24.0.12)(jsdom@24.0.0)(lightningcss@1.29.2)
packages:
@@ -515,6 +509,10 @@ packages:
'@kurkle/color@0.3.2':
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
'@msgpack/msgpack@3.1.2':
resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==}
engines: {node: '>= 18'}
'@niku/vite-env-caster@1.1.2':
resolution: {integrity: sha512-6I/8REFdmfeGnK92H3nYHGc6lExwjm72jLxAsDPlfji97Eej4rOMl6WuYGLgsQI0pl5RrMRMveeRdijdL6hW+Q==}
@@ -766,11 +764,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/msgpack-lite@0.1.11':
resolution: {integrity: sha512-cdCZS/gw+jIN22I4SUZUFf1ZZfVv5JM1//Br/MuZcI373sxiy3eSSoiyLu0oz+BPatTbGGGBO5jrcvd0siCdTQ==}
'@types/node@24.0.10':
resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==}
'@types/node@24.0.12':
resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==}
'@types/semver@7.5.8':
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
@@ -1202,9 +1197,6 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'}
event-lite@0.1.3:
resolution: {integrity: sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==}
execa@5.1.1:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'}
@@ -1350,9 +1342,6 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.1:
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
engines: {node: '>= 4'}
@@ -1374,9 +1363,6 @@ packages:
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
int64-buffer@0.1.10:
resolution: {integrity: sha512-v7cSY1J8ydZ0GyjUHqF+1bshJ6cnEVLo9EnjB8p+4HDRPZc9N5jjmvUV7NvEsqQOKyH0pmIBFWXVQbiS0+OBbA==}
is-binary-path@2.1.0:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
@@ -1415,9 +1401,6 @@ packages:
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@@ -1447,10 +1430,6 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
jwt-decode@4.0.0:
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
engines: {node: '>=18'}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@@ -1616,10 +1595,6 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
msgpack-lite@0.1.26:
resolution: {integrity: sha512-SZ2IxeqZ1oRFGo0xFGbvBJWMp3yLIY9rlIJyxy8CGrwZn1f0ZK4r6jV/AM1r0FZMDUkWkglOk/eeKIL9g77Nxw==}
hasBin: true
nanoid@3.3.7:
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -2563,6 +2538,8 @@ snapshots:
'@kurkle/color@0.3.2': {}
'@msgpack/msgpack@3.1.2': {}
'@niku/vite-env-caster@1.1.2':
dependencies:
chalk: 4.1.2
@@ -2644,18 +2621,18 @@ snapshots:
'@sinclair/typebox@0.27.8': {}
'@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
'@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
dependencies:
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
import-meta-resolve: 4.1.0
'@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
'@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
dependencies:
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
'@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@types/cookie': 0.6.0
cookie: 0.6.0
devalue: 5.1.1
@@ -2668,27 +2645,27 @@ snapshots:
set-cookie-parser: 2.6.0
sirv: 3.0.1
svelte: 5.20.4
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies:
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
debug: 4.4.0
svelte: 5.20.4
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
transitivePeerDependencies:
- supports-color
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
debug: 4.4.0
deepmerge: 4.3.1
kleur: 4.1.5
magic-string: 0.30.17
svelte: 5.20.4
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitefu: 1.0.6(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitefu: 1.0.6(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
transitivePeerDependencies:
- supports-color
@@ -2745,13 +2722,13 @@ snapshots:
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.12
'@tailwindcss/oxide-win32-x64-msvc': 4.0.12
'@tailwindcss/vite@4.0.12(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
'@tailwindcss/vite@4.0.12(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
dependencies:
'@tailwindcss/node': 4.0.12
'@tailwindcss/oxide': 4.0.12
lightningcss: 1.29.2
tailwindcss: 4.0.12
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
'@tweenjs/tween.js@23.1.2': {}
@@ -2768,13 +2745,10 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/msgpack-lite@0.1.11':
dependencies:
'@types/node': 24.0.10
'@types/node@24.0.10':
'@types/node@24.0.12':
dependencies:
undici-types: 7.8.0
optional: true
'@types/semver@7.5.8': {}
@@ -3298,8 +3272,6 @@ snapshots:
esutils@2.0.3: {}
event-lite@0.1.3: {}
execa@5.1.1:
dependencies:
cross-spawn: 7.0.3
@@ -3455,8 +3427,6 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.1: {}
import-fresh@3.3.0:
@@ -3475,8 +3445,6 @@ snapshots:
inherits@2.0.4: {}
int64-buffer@0.1.10: {}
is-binary-path@2.1.0:
dependencies:
binary-extensions: 2.3.0
@@ -3503,8 +3471,6 @@ snapshots:
is-stream@3.0.0: {}
isarray@1.0.0: {}
isexe@2.0.0: {}
jiti@2.4.2: {}
@@ -3547,8 +3513,6 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
jwt-decode@4.0.0: {}
keyv@4.5.4:
dependencies:
json-buffer: 3.0.1
@@ -3682,13 +3646,6 @@ snapshots:
ms@2.1.3: {}
msgpack-lite@0.1.26:
dependencies:
event-lite: 0.1.3
ieee754: 1.2.1
int64-buffer: 0.1.10
isarray: 1.0.0
nanoid@3.3.7: {}
nanoid@3.3.8: {}
@@ -4064,7 +4021,8 @@ snapshots:
ufo@1.5.3: {}
undici-types@7.8.0: {}
undici-types@7.8.0:
optional: true
universalify@0.2.0: {}
@@ -4110,13 +4068,13 @@ snapshots:
uzip@0.20201231.0: {}
vite-node@1.2.0(@types/node@24.0.10)(lightningcss@1.29.2):
vite-node@1.2.0(@types/node@24.0.12)(lightningcss@1.29.2):
dependencies:
cac: 6.7.14
debug: 4.4.0
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.4.14(@types/node@24.0.10)(lightningcss@1.29.2)
vite: 5.4.14(@types/node@24.0.12)(lightningcss@1.29.2)
transitivePeerDependencies:
- '@types/node'
- less
@@ -4128,33 +4086,33 @@ snapshots:
- supports-color
- terser
vite@5.4.14(@types/node@24.0.10)(lightningcss@1.29.2):
vite@5.4.14(@types/node@24.0.12)(lightningcss@1.29.2):
dependencies:
esbuild: 0.21.5
postcss: 8.5.3
rollup: 4.34.8
optionalDependencies:
'@types/node': 24.0.10
'@types/node': 24.0.12
fsevents: 2.3.3
lightningcss: 1.29.2
vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2):
vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2):
dependencies:
esbuild: 0.25.0
postcss: 8.5.3
rollup: 4.34.8
optionalDependencies:
'@types/node': 24.0.10
'@types/node': 24.0.12
fsevents: 2.3.3
jiti: 2.4.2
lightningcss: 1.29.2
yaml: 2.4.2
vitefu@1.0.6(vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)):
vitefu@1.0.6(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)):
optionalDependencies:
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
vitest@1.2.0(@types/node@24.0.10)(jsdom@24.0.0)(lightningcss@1.29.2):
vitest@1.2.0(@types/node@24.0.12)(jsdom@24.0.0)(lightningcss@1.29.2):
dependencies:
'@vitest/expect': 1.2.0
'@vitest/runner': 1.2.0
@@ -4174,11 +4132,11 @@ snapshots:
strip-literal: 1.3.0
tinybench: 2.8.0
tinypool: 0.8.4
vite: 5.4.14(@types/node@24.0.10)(lightningcss@1.29.2)
vite-node: 1.2.0(@types/node@24.0.10)(lightningcss@1.29.2)
vite: 5.4.14(@types/node@24.0.12)(lightningcss@1.29.2)
vite-node: 1.2.0(@types/node@24.0.12)(lightningcss@1.29.2)
why-is-node-running: 2.2.2
optionalDependencies:
'@types/node': 24.0.10
'@types/node': 24.0.12
jsdom: 24.0.0
transitivePeerDependencies:
- less
+9 -20
View File
@@ -2,25 +2,17 @@
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Check } from './icons';
import { exitBeforeEnter } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
title: string;
message: string;
onDismiss: any;
dismiss?: any;
}
import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
let {
isOpen,
title,
message,
onDismiss,
dismiss = { label: 'Dismiss', icon: Check }
}: Props = $props();
labels = {
dismiss: { label: 'Dismiss', icon: Check },
},
}: ModalProps = $props();
</script>
{#if isOpen}
@@ -29,11 +21,9 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
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"
>
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
@@ -41,9 +31,8 @@
<div class="flex justify-end gap-2">
<button
class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}
>
<dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
onclick={onDismiss}>
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
</button>
</div>
</div>
@@ -1,15 +1,15 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import * as THREE from 'three'
import { imu } from '$lib/stores/imu'
import SceneBuilder from '$lib/sceneBuilder'
import { onMount, onDestroy } from 'svelte';
import * as THREE from 'three';
import { imu } from '$lib/stores/imu';
import SceneBuilder from '$lib/sceneBuilder';
let canvas: HTMLCanvasElement = $state()
let sceneBuilder: SceneBuilder
let cube: THREE.Mesh
let targetRotation = new THREE.Euler()
let lastUpdateTime = 0
const LERP_SPEED = 5 // rotations per second
let canvas: HTMLCanvasElement;
let sceneBuilder: SceneBuilder;
let cube: THREE.Mesh;
let targetRotation = new THREE.Euler();
let lastUpdateTime = 0;
const LERP_SPEED = 5; // rotations per second
const initThreeJS = () => {
sceneBuilder = new SceneBuilder()
@@ -18,59 +18,59 @@
.addOrbitControls(1, 10, false)
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
.fillParent()
.fillParent();
const geometry = new THREE.BoxGeometry(1, 1, 1)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.8
})
cube = new THREE.Mesh(geometry, material)
sceneBuilder.scene.add(cube)
opacity: 0.8,
});
cube = new THREE.Mesh(geometry, material);
sceneBuilder.scene.add(cube);
sceneBuilder.addRenderCb(() => {
if (!cube) return
const currentTime = performance.now()
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
lastUpdateTime = currentTime
if (!cube) return;
const currentTime = performance.now();
const deltaTime = (currentTime - lastUpdateTime) / 1000; // convert to seconds
lastUpdateTime = currentTime;
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
})
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime);
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor);
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor);
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor);
});
sceneBuilder.startRenderLoop()
}
sceneBuilder.startRenderLoop();
};
const updateOrientation = () => {
if (!cube) return
if (!cube) return;
const y = -$imu.x[$imu.x.length - 1] || 0
const x = $imu.y[$imu.y.length - 1] || 0
const z = -$imu.z[$imu.z.length - 1] || 0
const y = -$imu.x[$imu.x.length - 1] || 0;
const x = $imu.y[$imu.y.length - 1] || 0;
const z = -$imu.z[$imu.z.length - 1] || 0;
targetRotation.set(
THREE.MathUtils.degToRad(x),
THREE.MathUtils.degToRad(y),
THREE.MathUtils.degToRad(z)
)
}
);
};
onMount(() => {
initThreeJS()
})
initThreeJS();
});
onDestroy(() => {
sceneBuilder?.renderer?.dispose()
})
sceneBuilder?.renderer?.dispose();
});
$effect(() => {
if ($imu) {
updateOrientation()
updateOrientation();
}
})
});
</script>
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
+20 -25
View File
@@ -6,7 +6,7 @@
LineBasicMaterial,
Mesh,
MeshBasicMaterial,
Object3D,
type Object3D,
SphereGeometry,
Vector3,
type NormalBufferAttributes,
@@ -21,7 +21,11 @@
servoAnglesOut,
servoAngles,
mpu,
jointNames
jointNames,
currentKinematic,
walkGait,
walkGaits,
walkGaitToMode
} from '$lib/stores'
import {
extractFootColor,
@@ -32,16 +36,8 @@
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 { type body_state_t } from '$lib/kinematic'
import { BezierState, CalibrationState, IdleState, RestState, StandState } from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store'
@@ -57,7 +53,7 @@
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
let sceneManager = $state(new SceneBuilder())
let canvas: HTMLCanvasElement = $state()
let canvas: HTMLCanvasElement
let currentModelAngles: number[] = new Array(12).fill(0)
let modelTargetAngles: number[] = new Array(12).fill(0)
@@ -70,7 +66,7 @@
let target_position = { x: 0, z: 0, yaw: 0 }
let kinematic = new Kinematic()
let kinematic = get(currentKinematic)
let planners = {
[ModesEnum.Deactivated]: new IdleState(),
@@ -78,7 +74,6 @@
[ModesEnum.Calibration]: new CalibrationState(),
[ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(),
[ModesEnum.Walk]: new BezierState()
}
let lastTick = performance.now()
@@ -117,6 +112,7 @@
await populateModelCache()
await createScene()
servoAngles.subscribe(updateAnglesFromStore)
walkGait.subscribe(gait => planners[ModesEnum.Walk].set_mode(walkGaitToMode(gait)))
if (panel) createPanel()
})
@@ -177,7 +173,7 @@
sceneManager
.addRenderer({ antialias: true, canvas, alpha: true })
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
.addOrbitControls(8, 30, orbit)
.addOrbitControls(2, 20, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
.addFogExp2(0xcccccc, 0.015)
@@ -264,16 +260,15 @@
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData)
const data = {
stop: controlData[0],
lx: controlData[1],
ly: controlData[2],
rx: controlData[3],
ry: controlData[4],
h: controlData[5],
s: controlData[6],
s1: controlData[7]
lx: controlData[0],
ly: controlData[1],
rx: controlData[2],
ry: controlData[3],
h: controlData[4],
s: controlData[5],
s1: controlData[6]
}
body_state.ym = ((data.h + 127) * 0.35) / 100
body_state.ym = data.h
let planner = planners[get(mode)]
const delta = performance.now() - lastTick
@@ -1,6 +1,10 @@
<script lang="ts">
import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
import {
WidgetComponents,
type WidgetContainerConfig,
isWidgetConfig,
} from '$lib/stores/application';
import Widget from './Widget.svelte';
interface Props {
@@ -15,8 +19,7 @@
class="flex w-full h-full"
class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}
>
class:flex-wrap={container.layout === 'wrap'}>
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
@@ -29,8 +32,8 @@
{#if index !== container.widgets.length - 1}
<div
class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}
></div>
class:divider-horizontal={container.layout === 'column'}>
</div>
{/if}
{/each}
</div>
+20 -15
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state'
import { base } from '$app/paths'
import { useFeatureFlags } from '$lib/stores/featureFlags'
import GithubButton from '../menu/GithubButton.svelte'
import LogoButton from '../menu/LogoButton.svelte'
@@ -22,7 +23,7 @@
Metrics,
DNS
} from '$lib/components/icons'
import appEnv from 'app-env'
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
const features = useFeatureFlags()
@@ -41,6 +42,10 @@
submenu?: menuItem[]
}
function withBase(path: string) {
return `${base}${path.startsWith('/') ? path : '/' + path}`
}
let menuItems = $state<menuItem[]>([])
$effect(() => {
@@ -48,13 +53,13 @@
{
title: 'Connection',
icon: WiFi,
href: '/connection',
feature: !appEnv.VITE_USE_HOST_NAME
href: withBase('/connection'),
feature: !PUBLIC_VITE_USE_HOST_NAME
},
{
title: 'Controller',
icon: MdiController,
href: '/controller',
href: withBase('/controller'),
feature: true
},
{
@@ -65,25 +70,25 @@
{
title: 'I2C',
icon: Connection,
href: '/peripherals/i2c',
href: withBase('/peripherals/i2c'),
feature: true
},
{
title: 'Camera',
icon: Camera,
href: '/peripherals/camera',
href: withBase('/peripherals/camera'),
feature: $features.camera
},
{
title: 'Servo',
icon: MotorOutline,
href: '/peripherals/servo',
href: withBase('/peripherals/servo'),
feature: true
},
{
title: 'IMU',
icon: Rotate3d,
href: '/peripherals/imu',
href: withBase('/peripherals/imu'),
feature: $features.imu || $features.mag || $features.bmp
}
]
@@ -96,19 +101,19 @@
{
title: 'WiFi Station',
icon: Router,
href: '/wifi/sta',
href: withBase('/wifi/sta'),
feature: true
},
{
title: 'Access Point',
icon: AP,
href: '/wifi/ap',
href: withBase('/wifi/ap'),
feature: true
},
{
title: 'mDNS',
icon: DNS,
href: '/wifi/mdns',
href: withBase('/wifi/mdns'),
feature: true
}
]
@@ -121,25 +126,25 @@
{
title: 'System Status',
icon: Health,
href: '/system/status',
href: withBase('/system/status'),
feature: true
},
{
title: 'File System',
icon: Folder,
href: '/system/filesystem',
href: withBase('/system/filesystem'),
feature: true
},
{
title: 'System Metrics',
icon: Metrics,
href: '/system/metrics',
href: withBase('/system/metrics'),
feature: true
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
href: withBase('/system/update'),
feature: $features.ota || $features.upload_firmware || $features.download_firmware
}
]
@@ -1,80 +1,80 @@
<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';
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();
const features = useFeatureFlags()
interface Props {
update?: boolean;
update?: boolean
}
let { update = $bindable(false) }: Props = $props();
let { update = $bindable(false) }: Props = $props()
let firmwareVersion: string = $state('');
let firmwareDownloadLink: string = $state('');
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;
console.warn('Error: Could not find releases in the repository')
return
}
if (result.isErr()) {
console.error('Error:', result.inner);
return;
console.error('Error:', result.inner)
return
}
const results = result.inner;
update = false;
firmwareVersion = '';
const results = result.inner
update = false
firmwareVersion = ''
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($features.firmware_built_target)
results.assets[i].name.includes($features.firmware_built_target as string)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
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 });
const result = await api.post('/api/downloadUpdate', { download_url: url })
if (result.isErr()) {
console.error('Error:', result.inner);
return;
console.error('Error:', result.inner)
return
}
}
onMount(async () => {
if ($features.download_firmware) {
await getGithubAPI();
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
await getGithubAPI()
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000) // once per hour
}
});
})
function confirmGithubUpdate(url: string) {
modals.open(ConfirmDialog, {
@@ -85,12 +85,12 @@
confirm: { label: 'Update', icon: CloudDown }
},
onConfirm: () => {
postGithubDownload(url);
postGithubDownload(url)
modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll()
});
})
}
});
})
}
</script>
@@ -98,11 +98,9 @@
<div class="indicator flex-none">
<button
class="btn btn-square btn-ghost h-9 w-9"
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
>
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>
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" />
+24 -24
View File
@@ -1,42 +1,42 @@
import { writable, derived, type Writable } from 'svelte/store';
import { writable, derived, type Writable } from 'svelte/store'
type StateType = 'info' | 'success' | 'warning' | 'error';
type StateType = 'info' | 'success' | 'warning' | 'error'
type State = {
id: string;
type: StateType;
message: string;
};
id: string
type: StateType
message: string
}
function createNotificationStore() {
const state: State[] = [];
const notifications = writable(state);
const { subscribe } = notifications;
const state: State[] = []
const notifications = writable(state)
const { subscribe } = notifications
function send(message: string, type: StateType = 'info', timeout: number) {
const id = generateId();
const id = generateId()
setTimeout(() => {
notifications.update((state) => {
return state.filter((n) => n.id !== id);
});
}, timeout);
notifications.update((state) => {
return [...state, { id, type, message }];
});
notifications.update(state => {
return state.filter(n => n.id !== id)
})
}, timeout)
notifications.update(state => {
return [...state, { id, type, message }]
})
}
return {
subscribe,
send,
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
};
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
}
}
function generateId() {
return '_' + Math.random().toString(36).substr(2, 9);
return '_' + Math.random().toString(36).substr(2, 9)
}
export const notifications = createNotificationStore();
export const notifications = createNotificationStore()
@@ -1,11 +1,11 @@
<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";
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 chartElement: HTMLCanvasElement;
let chart: Chart;
interface Props {
@@ -30,36 +30,36 @@
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
data,
yAxisID: 'y'
yAxisID: 'y',
},
]
],
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
display: true,
},
tooltip: {
mode: 'index',
intersect: false
}
intersect: false,
},
},
elements: {
point: {
radius: 0
}
radius: 0,
},
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
color: daisyColor('--bc', 10),
},
ticks: {
color: daisyColor('--bc')
color: daisyColor('--bc'),
},
display: false
display: false,
},
y: {
type: 'linear',
@@ -69,35 +69,33 @@
color: daisyColor('--bc'),
font: {
size: 16,
weight: 'bold'
}
weight: 'bold',
},
},
position: 'left',
min: 0,
max: 100,
grid: { color: daisyColor('--bc', 10) },
ticks: {
color: daisyColor('--bc')
color: daisyColor('--bc'),
},
border: { color: daisyColor('--bc', 10) },
},
},
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
setInterval(() => {
chart.data.labels = data
chart.data.datasets[0].data = data
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 }}
>
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={chartElement}></canvas>
</div>
</div>
@@ -2,7 +2,7 @@
interface Props {
options?: string[];
selectedOption?: string;
change: () => void;
change?: () => void;
[key: string]: any;
}
@@ -12,8 +12,7 @@
<select
bind:value={selectedOption}
{...rest}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
>
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}">
{#each options as option}
<option value={option}>{option}</option>
{/each}
+161 -183
View File
@@ -1,8 +1,6 @@
import { get } from 'svelte/store'
import type { body_state_t } from './kinematic'
import Kinematic from './kinematic'
import { fromInt8 } from './utilities'
const { sin } = Math
import { currentKinematic } from './stores/featureFlags'
export interface gait_state_t {
step_height: number
@@ -14,7 +12,6 @@ export interface gait_state_t {
}
export interface ControllerCommand {
stop: number
lx: number
ly: number
rx: number
@@ -39,7 +36,7 @@ export abstract class GaitState {
}
public get default_feet_pos() {
return new Kinematic().getDefaultFeetPos()
return get(currentKinematic).getDefaultFeetPos()
}
protected get default_height() {
@@ -61,11 +58,11 @@ export abstract class GaitState {
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: command.rx / 128,
step_height: 0.4 + (command.s1 + 1) / 2,
step_x: command.ly,
step_z: -command.lx,
step_velocity: command.s,
step_angle: command.rx,
step_depth: 0.002
}
@@ -80,7 +77,8 @@ export class IdleState extends GaitState {
export class CalibrationState extends GaitState {
protected name = 'Calibration'
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) {
body_state.omega = 0
body_state.phi = 0
body_state.psi = 0
@@ -95,7 +93,8 @@ export class CalibrationState extends GaitState {
export class RestState extends GaitState {
protected name = 'Rest'
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) {
body_state.omega = 0
body_state.phi = 0
body_state.psi = 0
@@ -110,167 +109,60 @@ export class RestState extends GaitState {
export class StandState extends GaitState {
protected name = 'Stand'
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
step(body_state: body_state_t, command: ControllerCommand) {
body_state.omega = 0
body_state.phi = command.rx / 8
body_state.psi = command.ry / 8
body_state.xm = command.ly / 2 / 100
body_state.zm = command.lx / 2 / 100
body_state.phi = command.rx * 10 * (Math.PI / 2)
body_state.psi = command.ry * 10 * (Math.PI / 2)
body_state.xm = command.ly / 4
body_state.zm = command.lx / 4
body_state.feet = this.default_feet_pos
return body_state
}
}
abstract class PhaseGaitState extends GaitState {
protected tick = 0
protected phase = 0
protected phase_time = 0
protected abstract num_phases: number
protected abstract phase_speed_factor: number
protected abstract swing_stand_ratio: number
protected contact_phases!: number[][]
protected shifts!: number[][]
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
super.step(body_state, command, dt)
this.update_phase()
this.update_body_position()
this.update_feet_positions()
return this.body_state
}
update_phase() {
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity
if (this.phase_time >= 1) {
this.phase += 1
if (this.phase == this.num_phases) this.phase = 0
this.phase_time = 0
}
}
update_body_position() {
if (this.num_phases === 4) return
const shift = this.shifts[Math.floor(this.phase / 2)]
this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4
}
update_feet_positions() {
for (let i = 0; i < 4; i++) {
this.body_state.feet[i] = this.update_foot_position(i)
}
}
update_foot_position(index: number): number[] {
const contact = this.contact_phases[index][this.phase]
return contact ? this.stand(index) : this.swing(index)
}
stand(index: number): number[] {
const delta_pos = [
-this.gait_state.step_x * this.dt * this.swing_stand_ratio,
0,
-this.gait_state.step_z * this.dt * this.swing_stand_ratio
]
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0]
this.body_state.feet[index][1] = this.default_feet_pos[index][1]
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2]
return this.body_state.feet[index]
}
swing(index: number): number[] {
const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt]
if (this.gait_state.step_x == 0) {
delta_pos[0] =
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8
}
if (this.gait_state.step_z == 0) {
delta_pos[2] =
(this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8
}
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0]
this.body_state.feet[index][1] =
this.default_feet_pos[index][1] + sin(this.phase_time * Math.PI) * this.gait_state.step_height
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2]
return this.body_state.feet[index]
}
}
export class FourPhaseWalkState extends PhaseGaitState {
protected name = 'Four phase walk'
protected num_phases = 4
protected phase_speed_factor = 6
protected contact_phases = [
[1, 0, 1, 1],
[1, 1, 1, 0],
[1, 1, 1, 0],
[1, 0, 1, 1]
]
protected swing_stand_ratio = 1 / (this.num_phases - 1)
begin() {
super.begin()
}
end() {
super.end()
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt)
}
}
export class EightPhaseWalkState extends PhaseGaitState {
protected name = 'Eight phase walk'
protected num_phases = 8
protected phase_speed_factor = 4
protected contact_phases = [
[1, 0, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 0, 1, 1, 1, 1]
]
protected shifts = [
[-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)
begin() {
super.begin()
}
end() {
super.end()
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt)
}
}
export class 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]
protected step_length = 0
protected stand_offset = 0.85
protected mode: 'crawl' | 'trot' = 'trot'
protected speed_factor = 1
offset = [0, 0.5, 0.75, 0.25]
protected shift_start_pos = { x: 0, z: 0 }
protected shift_target_pos = { x: 0, z: 0 }
protected shift_start_time = 0
protected current_shift_leg = -1
constructor() {
super()
this.set_mode(this.mode)
}
begin() {
super.begin()
}
set_mode(mode: 'crawl' | 'trot', duty?: number, order?: [number, number, number, number]) {
console.log('BezierState set_mode', mode)
this.mode = mode
if (mode === 'crawl') {
this.speed_factor = 0.5
this.stand_offset = duty ?? 0.85
const o = order ?? [3, 0, 2, 1]
const base = [0, 0.25, 0.5, 0.75]
const offsets = new Array(4).fill(0)
for (let i = 0; i < 4; i++) offsets[o[i]] = base[i]
this.offset = offsets
} else {
this.speed_factor = 2
this.stand_offset = duty ?? 0.6
this.offset = order ? (order.map(v => v % 1) as number[]) : [0, 0.5, 0.5, 0]
}
}
end() {
super.end()
}
@@ -278,49 +170,139 @@ export class BezierState extends GaitState {
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
}
if (this.gait_state.step_x < 0) this.step_length = -this.step_length
this.update_phase()
this.update_body_position()
this.update_feet_positions()
return this.body_state
}
update_phase() {
this.phase += this.dt * this.gait_state.step_velocity * 2
const m = this.gait_state
if (m.step_x === 0 && m.step_z === 0 && m.step_angle === 0) {
this.phase = 0
return
}
this.phase += this.dt * m.step_velocity * this.speed_factor
if (this.phase >= 1) {
this.phase_num += 1
this.phase_num %= 2
this.phase_num = (this.phase_num + 1) % 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_body_position() {
const m = this.gait_state
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
if (!moving) return
if (this.mode !== 'crawl') return
const { stance, swing, next_swing, time_to_lift } = this.get_leg_states()
if (stance.length >= 3 && swing.length === 0 && next_swing !== -1) {
if (this.current_shift_leg !== next_swing) {
this.current_shift_leg = next_swing
this.shift_start_pos.x = this.body_state.xm
this.shift_start_pos.z = this.body_state.zm
const remaining_legs = stance.filter(leg => leg !== next_swing)
const target = this.stance_centroid(remaining_legs)
this.shift_target_pos.x = target[0]
this.shift_target_pos.z = target[2]
this.shift_start_time = time_to_lift
}
const total_time = this.shift_start_time
const progress = total_time > 0 ? 1 - time_to_lift / total_time : 1
const smooth_progress = this.smoothstep01(Math.max(0, Math.min(1, progress)))
this.body_state.xm = this.lerp(
this.shift_start_pos.x,
this.shift_target_pos.x,
smooth_progress
)
this.body_state.zm = this.lerp(
this.shift_start_pos.z,
this.shift_target_pos.z,
smooth_progress
)
}
}
protected lerp(a: number, b: number, t: number): number {
return a + (b - a) * t
}
protected stance_centroid(legs: number[]): number[] {
if (legs.length === 0) return [this.body_state.xm, 0, this.body_state.zm]
let sx = 0,
sz = 0
for (const i of legs) {
sx += this.body_state.feet[i][0]
sz += this.body_state.feet[i][2]
}
return [sx / legs.length, 0, sz / legs.length]
}
protected get_leg_states(): {
stance: number[]
swing: number[]
next_swing: number
time_to_lift: number
} {
const stance: number[] = []
const swing: number[] = []
let next_swing = -1
let min_time_to_swing = Infinity
for (let i = 0; i < 4; i++) {
let phase = this.phase + this.offset[i]
if (phase >= 1) phase -= 1
if (phase <= this.stand_offset) {
stance.push(i)
const time_to_swing = this.stand_offset - phase
if (time_to_swing < min_time_to_swing) {
min_time_to_swing = time_to_swing
next_swing = i
}
} else {
swing.push(i)
}
}
return { stance, swing, next_swing, time_to_lift: min_time_to_swing }
}
protected smoothstep01(t: number): number {
const x = Math.max(0, Math.min(1, t))
return x * x * (3 - 2 * x)
}
update_feet_positions() {
for (let i = 0; i < 4; i++) this.body_state.feet[i] = this.update_foot_position(i)
}
update_foot_position(index: number): number[] {
let phase = this.phase + this.offset[index]
if (phase >= 1) {
phase -= 1
}
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))
return phase <= this.stand_offset ?
this.stand_controller(index, phase / this.stand_offset)
: this.swing_controller(index, (phase - this.stand_offset) / (1 - this.stand_offset))
}
stand_controller(index: number, phase: number) {
let depth = this.gait_state.step_depth
const 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
const height = this.gait_state.step_height
return this.controller(index, phase, bezier_curve, height)
}
@@ -356,10 +338,7 @@ const stance_curve = (length: number, angle: number, depth: number, phase: numbe
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))
}
if (length !== 0) Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length))
return [X, Y, Z]
}
@@ -390,6 +369,7 @@ const bezier_curve = (length: number, angle: number, height: number, phase: numb
}
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)
@@ -440,8 +420,6 @@ const comb = (n: number, k: number): number => {
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)
}
for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1)
return c
}
+27 -18
View File
@@ -20,15 +20,24 @@ export interface target_position {
yaw: number
}
export interface KinematicParams {
coxa: number
coxa_offset: number
femur: number
tibia: number
L: number
W: number
}
const { cos, sin, atan2, acos, sqrt, max, min } = Math
const DEG2RAD = 0.017453292519943
export default class Kinematic {
l1: number
l2: number
l3: number
l4: number
coxa: number
coxa_offset: number
femur: number
tibia: number
L: number
W: number
@@ -43,14 +52,13 @@ export default class Kinematic {
[1, 0, 0]
]
constructor() {
this.l1 = 60.5 / 100
this.l2 = 10 / 100
this.l3 = 111.7 / 100
this.l4 = 118.5 / 100
this.L = 207.5 / 100
this.W = 78 / 100
constructor(params: KinematicParams) {
this.coxa = params.coxa
this.coxa_offset = params.coxa_offset
this.femur = params.femur
this.tibia = params.tibia
this.L = params.L
this.W = params.W
this.mountOffsets = [
[this.L / 2, 0, this.W / 2],
@@ -62,7 +70,7 @@ export default class Kinematic {
getDefaultFeetPos(): number[][] {
return this.mountOffsets.map((offset, i) => {
return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.l1 : this.l1)]
return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
})
}
@@ -105,13 +113,14 @@ export default class Kinematic {
}
private legIK(x: number, y: number, z: number): [number, number, number] {
const F = sqrt(max(0, x * x + y * y - this.l1 * this.l1))
const G = F - this.l2
const F = sqrt(max(0, x * x + y * y - this.coxa * this.coxa))
const G = F - this.coxa_offset
const H = sqrt(G * G + z * z)
const t1 = -atan2(y, x) - atan2(F, -this.l1)
const D = (H * H - this.l3 * this.l3 - this.l4 * this.l4) / (2 * this.l3 * this.l4)
const t1 = -atan2(y, x) - atan2(F, -this.coxa)
const D =
(H * H - this.femur * this.femur - this.tibia * this.tibia) / (2 * this.femur * this.tibia)
const t3 = acos(max(-1, min(1, D)))
const t2 = atan2(z, G) - atan2(this.l4 * sin(t3), this.l3 + this.l4 * cos(t3))
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
return [t1, t2, t3]
}
+208 -207
View File
@@ -21,78 +21,78 @@ import {
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';
} 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;
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 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,
@@ -101,279 +101,280 @@ export default class SceneBuilder {
elevation: sunCalculator.calculateSunElevation(),
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) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
checkerboardTexture.wrapS = RepeatWrapping;
checkerboardTexture.wrapT = RepeatWrapping;
checkerboardTexture.repeat.set(100, 100);
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);
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);
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);
})
mirror.rotateX(-Math.PI / 2)
this.scene.add(mirror)
return this;
};
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 = minDistance + (maxDistance - minDistance) / 2
this.orbit.maxDistance = maxDistance
this.orbit.autoRotate = autoRotate
this.orbit.update()
this.orbit.minDistance = minDistance
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
}
private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const context = canvas.getContext('2d');
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const context = canvas.getContext('2d')
const squareSize = size / squares;
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);
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;
};
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.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]),
{ 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;
};
)
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
}
}
+52 -10
View File
@@ -1,20 +1,62 @@
import { api } from '$lib/api';
import { notifications } from '$lib/components/toasts/notifications';
import { writable, type Writable } from 'svelte/store';
import { api } from '$lib/api'
import { notifications } from '$lib/components/toasts/notifications'
import Kinematic from '$lib/kinematic'
import { persistentStore } from '$lib/utilities'
import { derived, type Writable } from 'svelte/store'
import { base } from '$app/paths'
let featureFlagsStore: Writable<Record<string, boolean>>;
let featureFlagsStore: Writable<Record<string, boolean | string>>
export function useFeatureFlags() {
if (!featureFlagsStore) {
featureFlagsStore = writable<Record<string, boolean>>({});
featureFlagsStore = persistentStore<Record<string, boolean | string>>('FeatureFlags', {})
api.get<Record<string, boolean>>('/api/features').then((result) => {
if (result.isOk()) featureFlagsStore.set(result.inner);
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);
notifications.error('Feature flag could not be fetched', 2500)
}
});
})
}
return featureFlagsStore;
return featureFlagsStore
}
export const variants = {
SPOTMICRO_ESP32: {
model: `${base}/spot_micro.urdf.xacro`,
stl: `${base}/stl.zip`,
kinematics: {
coxa: 60.5 / 100,
coxa_offset: 10 / 100,
femur: 111.7 / 100,
tibia: 118.5 / 100,
L: 207.5 / 100,
W: 78 / 100
}
},
SPOTMICRO_YERTLE: {
model: `${base}/yertle.URDF`,
stl: `${base}/URDF.zip`,
kinematics: {
coxa: 35 / 100,
coxa_offset: 0 / 100,
femur: 130 / 100,
tibia: 130 / 100,
L: 240 / 100,
W: 78 / 100
}
}
}
export const currentVariant = derived(useFeatureFlags(), $flagStore => {
const variantFlag = $flagStore['variant'] as string
return variantFlag && variants[variantFlag as keyof typeof variants] ?
variants[variantFlag as keyof typeof variants]
: variants.SPOTMICRO_ESP32
})
export const currentKinematic = derived(
currentVariant,
$variant => new Kinematic($variant.kinematics)
)
+2 -2
View File
@@ -1,5 +1,5 @@
import { persistentStore } from '$lib/utilities';
import { writable } from 'svelte/store';
import appEnv from 'app-env';
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public';
export const location = appEnv.VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
+39 -30
View File
@@ -1,45 +1,54 @@
import type { ControllerInput } from '$lib/types/models';
import { persistentStore } from '$lib/utilities/svelte-utilities';
import { writable, type Writable } from 'svelte/store';
import type { ControllerInput } from '$lib/types/models'
import { persistentStore } from '$lib/utilities/svelte-utilities'
import { writable, type Writable } from 'svelte/store'
export const emulateModel = writable(true);
export const emulateModel = writable(true)
export const jointNames = persistentStore('joint_names', <string[]>[]);
export const jointNames = persistentStore('joint_names', <string[]>[])
export const model = writable();
export const model = writable()
export const modes = [
'deactivated',
'idle',
'calibration',
'rest',
'stand',
'crawl',
'walk'
] as const;
export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'walk'] as const
export type Modes = (typeof modes)[number];
export type Modes = (typeof modes)[number]
export enum ModesEnum {
Deactivated,
Idle,
Calibration,
Rest,
Stand,
Crawl,
Walk
Deactivated = 0,
Idle = 1,
Calibration = 2,
Rest = 3,
Stand = 4,
Walk = 5
}
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated);
export enum WalkGaits {
Trot = 0,
Crawl = 1
}
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]);
export const walkGaits = ['trot', 'crawl'] as const
export const kinematicData = writable([0, 0, 0, 0, 1, 0]);
export const walkGaitLabels: Record<WalkGaits, string> = {
[WalkGaits.Trot]: 'Trot',
[WalkGaits.Crawl]: 'Crawl'
}
export const walkGaitToMode = (gait: WalkGaits): 'trot' | 'crawl' => {
return gait === WalkGaits.Trot ? 'trot' : 'crawl'
}
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated)
export const walkGait: Writable<WalkGaits> = writable(WalkGaits.Trot)
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0])
export const kinematicData = writable([0, 0, 0, 0, 1, 0])
export const input: Writable<ControllerInput> = writable({
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 50,
speed: 50,
s1: 50
});
height: 0.5,
speed: 0.5,
s1: 0.05
})
-27
View File
@@ -1,27 +0,0 @@
import { readable } from 'svelte/store';
export const heading = readable(0, (set) => {
const updateHeading = (e: any) => {
let alpha;
if (e.webkitCompassHeading) alpha = e.webkitCompassHeading;
else if (e.alpha) alpha = e.alpha;
else {
let q = e.target.quaternion;
alpha =
Math.atan2(2 * q[0] * q[1] + 2 * q[2] * q[3], 1 - 2 * q[1] * q[1] - 2 * q[2] * q[2]) *
(180 / Math.PI);
if (alpha < 0) alpha += 360;
}
set(alpha);
};
if ('AbsoluteOrientationSensor' in window) {
var sensor = new window.AbsoluteOrientationSensor({ frequency: 60 }) as any;
sensor.addEventListener('reading', updateHeading);
sensor.start();
} else if (window.DeviceMotionEvent) window.addEventListener('deviceorientation', updateHeading);
return () => {
if ('AbsoluteOrientationSensor' in window) sensor.removeEventListener('reading', updateHeading);
window.addEventListener('deviceorientation', updateHeading);
};
});
+114 -86
View File
@@ -1,132 +1,160 @@
import { writable } from 'svelte/store'
import { writable } from 'svelte/store';
import { encode, decode } from '@msgpack/msgpack';
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number]
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
type SocketEvent = (typeof socketEvents)[number];
export enum Topics {
imu = 0,
mode = 1,
command = 2,
servo = 3,
input = 4,
angles = 5,
position = 6
}
type SocketMessage = [number, string?, unknown?];
let useBinary = false;
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer;
try {
if (useBinary) {
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage;
}
return JSON.parse(data as string);
} catch (error) {
console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`);
}
return null;
};
const encodeMessage = (data: unknown) => {
try {
return useBinary ? encode(data) : JSON.stringify(data);
} catch (error) {
console.error(`Could not encode data: ${data} - ${error}`);
}
};
function createWebSocket() {
let listeners = new Map<string | Topics, Set<(data?: unknown) => void>>()
const { subscribe, set } = writable(false)
const reconnectTimeoutTime = 5000
let unresponsiveTimeoutId: number
let reconnectTimeoutId: number
let ws: WebSocket
let socketUrl: string | URL
const listeners = new Map<string, Set<(data?: unknown) => void>>();
const { subscribe, set } = writable(false);
const reconnectTimeoutTime = 5000;
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>;
let reconnectTimeoutId: ReturnType<typeof setTimeout>;
let ws: WebSocket;
let socketUrl: string | URL;
function init(url: string | URL) {
socketUrl = url
connect()
socketUrl = url;
connect();
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close()
set(false)
clearTimeout(unresponsiveTimeoutId)
clearTimeout(reconnectTimeoutId)
listeners.get(reason)?.forEach(listener => listener(event))
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
ws.close();
set(false);
clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach(listener => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
}
function connect() {
ws = new WebSocket(socketUrl)
ws = new WebSocket(socketUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = ev => {
set(true)
clearTimeout(reconnectTimeoutId)
listeners.get('open')?.forEach(listener => listener(ev))
ping();
useBinary = true;
ping();
set(true);
clearTimeout(reconnectTimeoutId);
listeners.get('open')?.forEach(listener => listener(ev));
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue
subscribeToEvent(event as unknown as Topics)
if (socketEvents.includes(event as SocketEvent)) continue;
subscribeToEvent(event);
}
}
ws.onmessage = message => {
resetUnresponsiveCheck()
let data = message.data
if (data instanceof ArrayBuffer) {
listeners.get('binary')?.forEach(listener => listener(data))
return
}
data = data.substring(1)
if (!data) return
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['))
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'))
try {
payload = JSON.parse(payload)
} catch (error) {}
if (event) listeners.get(event)?.forEach(listener => listener(payload))
}
ws.onerror = ev => disconnect('error', ev)
ws.onclose = ev => disconnect('close', ev)
};
ws.onmessage = frame => {
resetUnresponsiveCheck();
const message = decodeMessage(frame.data);
if (!message) return;
const [, event, payload = undefined] = message;
if (event) listeners.get(event)?.forEach(listener => listener(payload));
};
ws.onerror = ev => disconnect('error', ev);
ws.onclose = ev => disconnect('close', ev);
}
function unsubscribe(event: Topics, listener?: (data: any) => void) {
let eventListeners = listeners.get(event)
if (!eventListeners) return
function unsubscribe(event: string, listener?: (data: unknown) => void) {
const eventListeners = listeners.get(event);
if (!eventListeners) return;
if (!eventListeners.size) {
unsubscribeToEvent(event)
unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener)
eventListeners?.delete(listener);
} else {
listeners.delete(event)
listeners.delete(event);
}
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId)
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
clearTimeout(unresponsiveTimeoutId);
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
}
function sendEvent(event: Topics, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(JSON.stringify([2, event, data]))
function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
send([2, event, data]);
}
function unsubscribeToEvent(event: Topics) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(`[1,${event}]`)
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
send([1, event]);
}
function subscribeToEvent(event: Topics) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
ws.send(`[0,${event}]`)
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
send([0, event]);
}
function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
const serialized = encodeMessage(data);
if (!serialized) {
console.error('Could not serialize data:', data);
return;
}
ws.send(serialized);
}
function ping() {
const serialized = encodeMessage([4]);
if (!serialized) {
console.error('Could not serialize message');
return;
}
ws.send(serialized);
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: Topics | SocketEvent, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event)
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event)) {
subscribeToEvent(event)
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set()
listeners.set(event, eventListeners)
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: any) => void)
eventListeners.add(listener as (data: unknown) => void);
return () => {
unsubscribe(event, listener)
}
unsubscribe(event, listener as (data: unknown) => void);
};
},
off: (event: Topics, listener?: (data: any) => void) => {
unsubscribe(event, listener)
}
}
off: <T>(event: string, listener?: (data: T) => void) => {
unsubscribe(event, listener as (data: unknown) => void);
},
};
}
export const socket = createWebSocket()
export const socket = createWebSocket();
+18
View File
@@ -1,3 +1,21 @@
export enum MessageTopic {
imu = 'imu',
mode = 'mode',
input = 'input',
analytics = 'analytics',
position = 'position',
angles = 'angles',
i2cScan = 'i2cScan',
peripheralSettings = 'peripheralSettings',
otastatus = 'otastatus',
gait = 'walk_gait',
servoState = 'servoState',
servoPWM = 'servoPWM',
WiFiSettings = 'WiFiSettings',
sonar = 'sonar',
rssi = 'rssi'
}
export type vector = { x: number; y: number }
export interface ControllerInput {
+4 -2
View File
@@ -1,4 +1,6 @@
export const daisyColor = (name: string, opacity: number = 100) => {
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
return `oklch(${color} / ${opacity}%)`;
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
if (opacity >= 100) return color;
const alpha = Math.min(Math.max(opacity, 0), 100) / 100;
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`;
};
+4 -3
View File
@@ -2,15 +2,16 @@ 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 { currentVariant, jointNames, model } from '$lib/stores'
import uzip from 'uzip'
import { fileService } from '$lib/services'
import { get } from 'svelte/store'
let model_xml: XMLDocument
export const populateModelCache = async () => {
await cacheModelFiles()
const modelRes = await loadModel('/yertle.URDF')
const modelRes = await loadModel(get(currentVariant).model)
if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner
jointNames.set(JOINT_NAME)
@@ -21,7 +22,7 @@ export const populateModelCache = async () => {
}
export const cacheModelFiles = async () => {
const data = await fetch('/URDF.zip')
const data = await fetch(get(currentVariant).stl)
const files = uzip.parse(await data.arrayBuffer())
+3 -10
View File
@@ -1,15 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
setTimeout(() => {
goto('/');
}, 3000);
});
import { page } from '$app/state'
</script>
<div class="flex justify-center items-center w-full h-full">
<h1 class="text-4xl">404 - Page not found</h1>
<p>You will be redirected to the home page in 3 seconds</p>
<h1>{page.status} {page.error?.message}</h1>
<span>Go to <a class="btn btn-primary" href="/">Home page</a></span>
</div>
+60 -56
View File
@@ -1,13 +1,13 @@
<script lang="ts">
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 { 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,
@@ -19,75 +19,79 @@
servoAnglesOut,
socket,
location,
useFeatureFlags
} from '$lib/stores';
import type { Analytics, DownloadOTA } from '$lib/types/models';
useFeatureFlags,
walkGait
} from '$lib/stores'
import { type Analytics, type DownloadOTA } from '$lib/types/models'
import { MessageTopic } from '$lib/types/models'
interface Props {
children?: import('svelte').Snippet;
children?: import('svelte').Snippet
}
let { children }: Props = $props();
let { children }: Props = $props()
const features = useFeatureFlags();
const features = useFeatureFlags()
onMount(async () => {
const ws = $location ? $location : window.location.host;
socket.init(`ws://${ws}/api/ws/events`);
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(MessageTopic.input, data))
mode.subscribe(data => socket.sendEvent(MessageTopic.mode, data))
walkGait.subscribe(data => socket.sendEvent(MessageTopic.gait, data))
servoAnglesOut.subscribe(data => socket.sendEvent(MessageTopic.angles, data))
kinematicData.subscribe(data => socket.sendEvent(MessageTopic.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('analytics', handleAnalytics);
socket.on('angles', (angles: number[]) => {
if (angles.length) servoAngles.set(angles);
});
socket.on('open', handleOpen)
socket.on('close', handleClose)
socket.on('error', handleError)
socket.on(MessageTopic.rssi, handleNetworkStatus)
socket.on(MessageTopic.mode, (data: ModesEnum) => mode.set(data))
socket.on(MessageTopic.analytics, handleAnalytics)
socket.on(MessageTopic.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));
});
};
if (data?.download_firmware) socket.on(MessageTopic.otastatus, handleOAT)
if (data?.sonar) socket.on(MessageTopic.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('otastatus', handleOAT);
};
socket.off(MessageTopic.analytics, handleAnalytics)
socket.off('open', handleOpen)
socket.off('close', handleClose)
socket.off(MessageTopic.rssi, handleNetworkStatus)
socket.off(MessageTopic.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 handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data)
let menuOpen = $state(false);
let menuOpen = $state(false)
</script>
<svelte:head>
@@ -117,8 +121,8 @@
<div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade
onclick={modals.closeAll}
></div>
onclick={modals.closeAll}>
</div>
{/snippet}
</Modals>
+13 -13
View File
@@ -1,22 +1,22 @@
export const prerender = false;
export const ssr = false;
export const prerender = true
export const ssr = false
const registerFetchIntercept = async () => {
const { fetch: originalFetch } = window;
const fileService = (await import('$lib/services/file-service')).default;
const { fetch: originalFetch } = window
const fileService = (await import('$lib/services/file-service')).default
window.fetch = async (resource, config) => {
let url = resource instanceof Request ? resource.url : resource.toString();
let file = await fileService.getFile(url);
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
};
};
const url = resource instanceof Request ? resource.url : resource.toString()
const file = await fileService?.getFile(url)
return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config)
}
}
export const load = async () => {
await registerFetchIntercept();
await registerFetchIntercept()
return {
title: 'Spot micro controller',
github: 'runeharlyk/SpotMicroESP32-Leika',
app_name: 'Spot Micro Controller',
copyright: '2024 Rune Harlyk'
};
};
copyright: '2025 Rune Harlyk'
}
}
+2 -4
View File
@@ -1,9 +1,7 @@
<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();
import { location, socket } from '$lib/stores';
const update = () => {
const ws = $location ? $location : window.location.host;
@@ -16,7 +14,7 @@
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Connection</span>
<span>Connection</span>
{/snippet}
<div class="flex">
+16 -16
View File
@@ -1,26 +1,26 @@
<script lang="ts">
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';
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 { MessageTopic, type IMU } from '$lib/types/models'
let layout = $derived($views.find(v => v.name === $selectedView)!);
let layout = $derived($views.find(v => v.name === $selectedView)!)
onMount(() => {
socket.on('imu', (data: IMU) => {
imu.addData(data);
socket.on(MessageTopic.imu, (data: IMU) => {
imu.addData(data)
if (data.heading)
mpu.update(mpuData => {
mpuData.heading = data.heading;
console.log(data.heading);
mpuData.heading = data.heading
console.log(data.heading)
return mpuData;
});
});
});
return mpuData
})
})
})
</script>
<div class="absolute top-0 select-none w-screen h-screen">
+49 -16
View File
@@ -1,11 +1,22 @@
<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 } from '$lib/stores'
import { capitalize, throttler } from '$lib/utilities'
import {
input,
outControllerData,
mode,
modes,
type Modes,
ModesEnum,
walkGaits,
WalkGaits,
walkGait,
walkGaitLabels
} from '$lib/stores'
import type { vector } from '$lib/types/models'
import { VerticalSlider } from '$lib/components/input'
import { gamepadAxes, gamepadButtons, hasGamepad } from '$lib/stores/gamepad'
import { gamepadAxes, hasGamepad } from '$lib/stores/gamepad'
import { notifications } from '$lib/components/toasts/notifications'
let throttle = new throttler()
@@ -13,7 +24,7 @@
let right: nipplejs.JoystickManager
let throttle_timing = 40
let data = new Array(8)
let data = new Array(7)
$effect(() => {
if ($hasGamepad) {
@@ -64,14 +75,13 @@
}
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] = $input.left.x
data[1] = $input.left.y
data[2] = $input.right.x
data[3] = $input.right.y
data[4] = $input.height
data[5] = $input.speed
data[6] = $input.s1
outControllerData.set(data)
}
@@ -101,6 +111,10 @@
const changeMode = (modeValue: Modes) => {
mode.set(modes.indexOf(modeValue))
}
const changeWalkGait = (walkGaitValue: WalkGaits) => {
walkGait.set(walkGaitValue)
}
</script>
<div class="absolute top-0 left-0 w-screen h-screen">
@@ -122,7 +136,11 @@
</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')} />
<VerticalSlider
min={0}
max={1}
step={0.01}
oninput={(e: Event) => handleRange(e, 'height')} />
<label for="height">Ht</label>
</div>
<div
@@ -138,7 +156,20 @@
{/each}
</div>
{#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl}
{#if $mode === ModesEnum.Walk}
<div class="join">
{#each Object.values(WalkGaits) as gaitValue}
{#if typeof gaitValue === 'number'}
<button
class="btn join-item btn-sm"
class:btn-secondary={$walkGait === gaitValue}
onclick={() => changeWalkGait(gaitValue)}>
{walkGaitLabels[gaitValue]}
</button>
{/if}
{/each}
</div>
<div class="flex gap-4">
<div>
<label for="s1">S1</label>
@@ -146,7 +177,8 @@
type="range"
name="s1"
min="0"
max="25"
step="0.01"
max="1"
oninput={e => handleRange(e, 's1')}
class="range range-sm range-primary" />
</div>
@@ -156,7 +188,8 @@
type="range"
name="speed"
min="0"
max="25"
step="0.01"
max="1"
oninput={e => handleRange(e, 'speed')}
class="range range-sm range-primary" />
</div>
+5 -4
View File
@@ -2,7 +2,7 @@
import SettingsCard from '$lib/components/SettingsCard.svelte'
import { onMount } from 'svelte'
import { socket } from '$lib/stores'
import type { I2CDevice } from '$lib/types/models'
import { MessageTopic, type I2CDevice } from '$lib/types/models'
import { Connection } from '$lib/components/icons'
import I2CSetting from './i2cSetting.svelte'
@@ -16,6 +16,7 @@
part_number: 'MPU6050',
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
},
{ address: 115, part_number: 'PAJ7620U2', name: 'Gesture sensor' },
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
]
@@ -24,9 +25,9 @@
let isLoading = $state(false)
onMount(() => {
socket.on('i2cScan', handleScan)
socket.on(MessageTopic.i2cScan, handleScan)
triggerScan()
return () => socket.off('i2cScan', handleScan)
return () => socket.off(MessageTopic.i2cScan, handleScan)
})
const handleScan = (data: any) => {
@@ -43,7 +44,7 @@
const triggerScan = () => {
isLoading = true
socket.sendEvent('i2cScan', '')
socket.sendEvent(MessageTopic.i2cScan, '')
}
</script>
@@ -1,7 +1,7 @@
<script lang="ts">
import { Cancel, Edit, EditOff, Power } from '$lib/components/icons'
import { socket } from '$lib/stores'
import type { PeripheralsConfiguration } from '$lib/types/models'
import { MessageTopic, type PeripheralsConfiguration } from '$lib/types/models'
import { onMount } from 'svelte'
import { modals } from 'svelte-modals'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
@@ -10,9 +10,9 @@
let isEditing = $state(false)
onMount(() => {
socket.on('peripheralSettings', handleSettings)
socket.sendEvent('peripheralSettings', '')
return () => socket.off('peripheralSettings', handleSettings)
socket.on(MessageTopic.peripheralSettings, handleSettings)
socket.sendEvent(MessageTopic.peripheralSettings, '')
return () => socket.off(MessageTopic.peripheralSettings, handleSettings)
})
const handleSettings = (data: any) => {
@@ -30,7 +30,7 @@
},
onConfirm: () => {
modals.close()
socket.sendEvent('peripheralSettings', settings)
socket.sendEvent(MessageTopic.peripheralSettings, settings)
}
})
}
+11 -11
View File
@@ -6,18 +6,18 @@
import { slide } from 'svelte/transition'
import { onDestroy, onMount } from 'svelte'
import { socket } from '$lib/stores'
import type { IMU } from '$lib/types/models'
import { MessageTopic, type IMU } from '$lib/types/models'
import { useFeatureFlags } from '$lib/stores/featureFlags'
import { Rotate3d } from '$lib/components/icons'
Chart.register(...registerables)
const features = useFeatureFlags()
let intervalId: number
let intervalId: ReturnType<typeof setInterval> | number
let angleChartElement: HTMLCanvasElement = $state()
let tempChartElement: HTMLCanvasElement = $state()
let altitudeChartElement: HTMLCanvasElement = $state()
let angleChartElement: HTMLCanvasElement
let tempChartElement: HTMLCanvasElement
let altitudeChartElement: HTMLCanvasElement
let angleChart: Chart
let tempChart: Chart
@@ -38,7 +38,7 @@
responsive: true,
plugins: {
legend: { display: true },
tooltip: { mode: 'index', intersect: false }
tooltip: { mode: 'index' as const, intersect: false }
},
elements: { point: { radius: 1 } },
scales: {
@@ -48,8 +48,8 @@
display: false
},
y: {
type: 'linear',
position: 'left',
type: 'linear' as const,
position: 'left' as const,
min: 0,
max: 10,
grid: { color: bgColor },
@@ -201,7 +201,7 @@
}
onMount(() => {
socket.on('imu', (data: IMU) => {
socket.on(MessageTopic.imu, (data: IMU) => {
console.log(data)
imu.addData(data)
})
@@ -211,14 +211,14 @@
})
onDestroy(() => {
socket.off('imu')
socket.off(MessageTopic.imu)
clearInterval(intervalId)
})
</script>
<SettingsCard collapsible={false}>
{#snippet icon()}
<Rotate3d class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
<Rotate3d class="flex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span>IMU</span>
@@ -1,5 +1,6 @@
<script lang="ts">
import { socket } from '$lib/stores'
import { MessageTopic } from '$lib/types/models'
import { throttler as Throttler } from '$lib/utilities'
let { servoId = $bindable(0), pwm = $bindable(306) } = $props()
@@ -11,16 +12,16 @@
const throttler = new Throttler()
const activateServo = () => {
socket.sendEvent('servoState', { active: 1 })
socket.sendEvent(MessageTopic.servoState, { active: 1 })
}
const deactivateServo = () => {
socket.sendEvent('servoState', { active: 0 })
socket.sendEvent(MessageTopic.servoState, { active: 0 })
}
const updatePWM = () => {
throttler.throttle(() => {
socket.sendEvent('servoPWM', { servo_id: servoId, pwm })
socket.sendEvent(MessageTopic.servoPWM, { servo_id: servoId, pwm })
}, 10)
}
@@ -1,13 +1,5 @@
<script lang="ts">
import SystemMetrics from './SystemMetrics.svelte';
import { goto } from '$app/navigation';
import { useFeatureFlags } from '$lib/stores/featureFlags';
const features = useFeatureFlags();
if (!$features.analytics) {
goto('/');
}
</script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+104 -108
View File
@@ -12,16 +12,16 @@
Chart.register(...registerables);
let cpuChartElement: HTMLCanvasElement = $state();
let cpuChartElement: HTMLCanvasElement;
let cpuChart: Chart;
let heapChartElement: HTMLCanvasElement = $state();
let heapChartElement: HTMLCanvasElement;
let heapChart: Chart;
let filesystemChartElement: HTMLCanvasElement = $state();
let filesystemChartElement: HTMLCanvasElement;
let filesystemChart: Chart;
let temperatureChartElement: HTMLCanvasElement = $state();
let temperatureChartElement: HTMLCanvasElement;
let temperatureChart: Chart;
onMount(() => {
@@ -32,79 +32,79 @@
datasets: [
{
label: 'Cpu usage core 0',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderColor: daisyColor('--color-primary'),
backgroundColor: daisyColor('--color-primary', 50),
borderWidth: 2,
data: $analytics.cpu0_usage,
yAxisID: 'y'
yAxisID: 'y',
},
{
label: 'Cpu usage core 1',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderColor: daisyColor('--color-primary'),
backgroundColor: daisyColor('--color-primary', 50),
borderWidth: 2,
data: $analytics.cpu1_usage,
yAxisID: 'y'
yAxisID: 'y',
},
{
label: 'Cpu usage total',
borderColor: daisyColor('--s'),
backgroundColor: daisyColor('--s', 50),
borderColor: daisyColor('--color-secondary'),
backgroundColor: daisyColor('--color-secondary', 50),
borderWidth: 2,
data: $analytics.cpu_usage,
yAxisID: 'y'
yAxisID: 'y',
},
]
],
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
display: true,
},
tooltip: {
mode: 'index',
intersect: false
}
intersect: false,
},
},
elements: {
point: {
radius: 0
}
radius: 0,
},
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
color: daisyColor('--color-base-content', 10),
},
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
display: false
display: false,
},
y: {
type: 'linear',
title: {
display: true,
text: 'Cpu usage [%]',
color: daisyColor('--bc'),
color: daisyColor('--color-base-content'),
font: {
size: 16,
weight: 'bold'
}
weight: 'bold',
},
},
position: 'left',
min: 0,
max: 100,
grid: { color: daisyColor('--bc', 10) },
grid: { color: daisyColor('--color-base-content', 10) },
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
border: { color: daisyColor('--color-base-content', 10) },
},
},
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
heapChart = new Chart(heapChartElement, {
type: 'line',
@@ -113,64 +113,64 @@
datasets: [
{
label: 'Used Heap',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderColor: daisyColor('--color-primary'),
backgroundColor: daisyColor('--color-primary', 50),
borderWidth: 2,
data: $analytics.used_heap,
fill:true,
yAxisID: 'y'
}
]
fill: true,
yAxisID: 'y',
},
],
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
display: true,
},
tooltip: {
mode: 'index',
intersect: false
}
intersect: false,
},
},
elements: {
point: {
radius: 0
}
radius: 0,
},
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
color: daisyColor('--color-base-content', 10),
},
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
display: false
display: false,
},
y: {
type: 'linear',
title: {
display: true,
text: 'Heap [kb]',
color: daisyColor('--bc'),
color: daisyColor('--color-base-content'),
font: {
size: 16,
weight: 'bold'
}
weight: 'bold',
},
},
position: 'left',
min: 0,
max: Math.round($analytics.total_heap[0]),
grid: { color: daisyColor('--bc', 10) },
grid: { color: daisyColor('--color-base-content', 10) },
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
border: { color: daisyColor('--color-base-content', 10) },
},
},
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
filesystemChart = new Chart(filesystemChartElement, {
type: 'line',
@@ -179,64 +179,64 @@
datasets: [
{
label: 'File System Used',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderColor: daisyColor('--color-primary'),
backgroundColor: daisyColor('--color-primary', 50),
borderWidth: 2,
data: $analytics.fs_used,
fill:true,
yAxisID: 'y'
}
]
fill: true,
yAxisID: 'y',
},
],
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
display: true,
},
tooltip: {
mode: 'index',
intersect: false
}
intersect: false,
},
},
elements: {
point: {
radius: 0
}
radius: 0,
},
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
color: daisyColor('--color-base-content', 10),
},
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
display: false
display: false,
},
y: {
type: 'linear',
title: {
display: true,
text: 'File System [kb]',
color: daisyColor('--bc'),
color: daisyColor('--color-base-content'),
font: {
size: 16,
weight: 'bold'
}
weight: 'bold',
},
},
position: 'left',
min: 0,
max: Math.round($analytics.fs_total[0]),
grid: { color: daisyColor('--bc', 10) },
grid: { color: daisyColor('--color-base-content', 10) },
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
border: { color: daisyColor('--color-base-content', 10) },
},
},
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
temperatureChart = new Chart(temperatureChartElement, {
type: 'line',
@@ -245,63 +245,63 @@
datasets: [
{
label: 'Core Temperature',
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderColor: daisyColor('--color-primary'),
backgroundColor: daisyColor('--color-primary', 50),
borderWidth: 2,
data: $analytics.core_temp,
yAxisID: 'y'
}
]
yAxisID: 'y',
},
],
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
display: true,
},
tooltip: {
mode: 'index',
intersect: false
}
intersect: false,
},
},
elements: {
point: {
radius: 0
}
radius: 0,
},
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
color: daisyColor('--color-base-content', 10),
},
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
display: false
display: false,
},
y: {
type: 'linear',
title: {
display: true,
text: 'Core Temperature [°C]',
color: daisyColor('--bc'),
color: daisyColor('--color-base-content'),
font: {
size: 16,
weight: 'bold'
}
weight: 'bold',
},
},
position: 'left',
suggestedMin: 20,
suggestedMax: 100,
grid: { color: daisyColor('--bc', 10) },
grid: { color: daisyColor('--color-base-content', 10) },
ticks: {
color: daisyColor('--bc')
color: daisyColor('--color-base-content'),
},
border: { color: daisyColor('--color-base-content', 10) },
},
},
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
setInterval(updateData, 500);
});
@@ -334,14 +334,13 @@
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >System Metrics</span>
<span>System Metrics</span>
{/snippet}
<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 }}
>
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={cpuChartElement}></canvas>
</div>
</div>
@@ -349,24 +348,21 @@
<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 }}
>
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={heapChartElement}></canvas>
</div>
</div>
<div class="w-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-52"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={filesystemChartElement}></canvas>
</div>
</div>
<div class="w-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-52"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={temperatureChartElement}></canvas>
</div>
</div>
@@ -6,7 +6,7 @@
import Spinner from '$lib/components/Spinner.svelte'
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import type { SystemInformation, Analytics } from '$lib/types/models'
import { type SystemInformation, type Analytics, MessageTopic } from '$lib/types/models'
import { socket } from '$lib/stores/socket'
import { api } from '$lib/api'
import { convertSeconds } from '$lib/utilities'
@@ -34,7 +34,7 @@
const features = useFeatureFlags()
let systemInformation: SystemInformation = $state()
let systemInformation: SystemInformation | null = $state(null)
async function getSystemStatus() {
const result = await api.get<SystemInformation>('/api/system/status')
@@ -50,12 +50,17 @@
const postSleep = async () => await api.post('api/sleep')
onMount(() => socket.on('analytics', handleSystemData))
onMount(() => socket.on(MessageTopic.analytics, handleSystemData))
onDestroy(() => socket.off('analytics', handleSystemData))
const handleSystemData = (data: Analytics) =>
(systemInformation = { ...systemInformation, ...data })
onDestroy(() => socket.off(MessageTopic.analytics, handleSystemData))
const handleSystemData = (data: Analytics) => {
if (systemInformation) {
systemInformation = {
...systemInformation,
...(data as unknown as SystemInformation)
}
}
}
const postRestart = async () => await api.post('/api/system/restart')
@@ -144,7 +149,8 @@
<div class="w-full overflow-x-auto">
{#await getSystemStatus()}
<Spinner />
{:then nothing}
{:then}
{#if systemInformation}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
@@ -201,9 +207,10 @@
<StatusItem
icon={Folder}
title="File System (Used / Total)"
description={`${((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(
1
)} % of ${systemInformation.fs_total / 1000000} MB used (${
description={`${(
(systemInformation.fs_used / systemInformation.fs_total) *
100
).toFixed(1)} % of ${systemInformation.fs_total / 1000000} MB used (${
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
}
MB free)`} />
@@ -227,6 +234,7 @@
title="Reset Reason"
description={systemInformation.cpu_reset_reason} />
</div>
{/if}
{/await}
</div>
@@ -19,10 +19,10 @@
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
'X-GitHub-Api-Version': '2022-11-28',
};
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
headers
headers,
});
if (result.isErr()) {
console.error('Error:', result.inner);
@@ -58,7 +58,7 @@
message:
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
dismiss: { label: 'OK', icon: Check },
onDismiss: () => modals.close()
onDismiss: () => modals.close(),
});
return;
}
@@ -68,14 +68,14 @@
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 }
confirm: { label: 'Update', icon: CloudDown },
},
onConfirm: () => {
postGithubDownload(url);
modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll()
onConfirm: () => modals.closeAll(),
});
}
},
});
}
</script>
@@ -91,10 +91,7 @@
<Spinner />
{:then githubReleases}
<div class="relative w-full overflow-visible">
<div
class="overflow-x-auto"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}>
<table class="table w-full table-auto">
<thead>
<tr class="font-bold">
@@ -108,26 +105,21 @@
{#each githubReleases as release}
<tr
class={(
compareVersions(
$features.firmware_version,
release.tag_name
) === 0
compareVersions($features.firmware_version as string, release.tag_name) === 0
) ?
'bg-primary text-primary-content'
: 'bg-base-100 h-14'}
>
: 'bg-base-100 h-14'}>
<td align="left" class="text-base font-semibold">
<a
href={release.html_url}
class="link link-hover"
target="_blank"
rel="noopener noreferrer">{release.name}</a
></td
>
></td>
<td align="center" class="hidden min-h-full align-middle sm:block">
<div class="my-2">
{new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium'
dateStyle: 'medium',
}).format(new Date(release.published_at))}
</div>
</td>
@@ -137,13 +129,12 @@
{/if}
</td>
<td align="center">
{#if compareVersions($features.firmware_version, release.tag_name) != 0}
{#if compareVersions($features.firmware_version as string, release.tag_name) != 0}
<button
class="btn btn-ghost btn-circle btn-sm"
onclick={() => {
confirmGithubUpdate(release.assets);
}}
>
}}>
<CloudDown class="text-secondary h-6 w-6" />
</button>
{/if}
@@ -157,9 +148,7 @@
{:catch error}
<div class="alert alert-error shadow-lg">
<Error class="h-6 w-6 shrink-0" />
<span
>Please connect to a network with internet access to perform a firmware update.</span
>
<span>Please connect to a network with internet access to perform a firmware update.</span>
</div>
{/await}
</SettingsCard>
@@ -6,11 +6,11 @@
import { api } from '$lib/api';
import { Cancel, OTA, Warning } from '$lib/components/icons';
let files: FileList = $state();
let files: FileList | undefined = $state();
async function uploadBIN() {
const formData = new FormData();
formData.append('file', files[0]);
formData.append('file', files![0]);
const result = await api.post('/api/firmware', formData);
if (result.isErr()) console.error('Error:', result.inner);
}
@@ -21,12 +21,12 @@
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Upload', icon: OTA }
confirm: { label: 'Upload', icon: OTA },
},
onConfirm: () => {
modals.close();
uploadBIN();
}
},
});
}
</script>
@@ -41,8 +41,8 @@
<div class="alert alert-warning shadow-lg">
<Warning class="h-6 w-6 shrink-0" />
<span
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload
a (.md5) file first to verify the uploaded firmware.
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload a
(.md5) file first to verify the uploaded firmware.
</span>
</div>
@@ -52,6 +52,5 @@
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
bind:files
accept=".bin,.md5"
onchange={confirmBinUpload}
/>
onchange={confirmBinUpload} />
</SettingsCard>
+77 -76
View File
@@ -1,74 +1,71 @@
<script lang="ts">
import { preventDefault } from 'svelte/legacy'
import { preventDefault } from 'svelte/legacy';
import { onMount, onDestroy } from 'svelte'
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { PasswordInput } from '$lib/components/input'
import SettingsCard from '$lib/components/SettingsCard.svelte'
import { notifications } from '$lib/components/toasts/notifications'
import Spinner from '$lib/components/Spinner.svelte'
import type { ApSettings, ApStatus } from '$lib/types/models'
import { api } from '$lib/api'
import { useFeatureFlags } from '$lib/stores'
import { AP, Devices, Home, MAC } from '$lib/components/icons'
import StatusItem from '$lib/components/StatusItem.svelte'
import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { PasswordInput } from '$lib/components/input';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import { notifications } from '$lib/components/toasts/notifications';
import Spinner from '$lib/components/Spinner.svelte';
import type { ApSettings, ApStatus } from '$lib/types/models';
import { api } from '$lib/api';
import { AP, Devices, Home, MAC } from '$lib/components/icons';
import StatusItem from '$lib/components/StatusItem.svelte';
const features = useFeatureFlags()
let apSettings: ApSettings | null = $state(null);
let apStatus: ApStatus | null = $state(null);
let apSettings: ApSettings = $state()
let apStatus: ApStatus = $state()
let formField: any = $state()
let formField: any = $state();
async function getAPStatus() {
const result = await api.get<ApStatus>('/api/wifi/ap/status')
const result = await api.get<ApStatus>('/api/wifi/ap/status');
if (result.isErr()) {
console.error('Error:', result.inner)
return
console.error('Error:', result.inner);
return;
}
apStatus = result.inner
return apStatus
apStatus = result.inner;
return apStatus;
}
async function getAPSettings() {
const result = await api.get<ApSettings>('/api/wifi/ap/settings')
const result = await api.get<ApSettings>('/api/wifi/ap/settings');
if (result.isErr()) {
console.error('Error:', result.inner)
return
console.error('Error:', result.inner);
return;
}
apSettings = result.inner
return apSettings
apSettings = result.inner;
return apSettings;
}
const interval = setInterval(async () => {
getAPStatus()
}, 5000)
getAPStatus();
}, 5000);
onDestroy(() => clearInterval(interval))
onDestroy(() => clearInterval(interval));
onMount(getAPSettings)
onMount(getAPSettings);
let provisionMode = [
{
id: 0,
text: `Always`
text: `Always`,
},
{
id: 1,
text: `When WiFi Disconnected`
text: `When WiFi Disconnected`,
},
{
id: 2,
text: `Never`
}
]
text: `Never`,
},
];
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning';
let apStatusVariant: Variant[] = ['success', 'error', 'warning']
let apStatusVariant: Variant[] = ['success', 'error', 'warning'];
let apStatusDescription = ['Active', 'Inactive', 'Lingering']
let apStatusDescription = ['Active', 'Inactive', 'Lingering'];
let formErrors = $state({
ssid: false,
@@ -76,80 +73,81 @@
max_clients: false,
local_ip: false,
gateway_ip: false,
subnet_mask: false
})
subnet_mask: false,
});
async function postAPSettings(data: ApSettings) {
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data)
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
if (result.isErr()) {
notifications.error('User not authorized.', 3000)
console.error('Error:', result.inner)
return
notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner);
return;
}
notifications.success('Access Point settings updated.', 3000)
apSettings = result.inner
notifications.success('Access Point settings updated.', 3000);
apSettings = result.inner;
}
function handleSubmitAP() {
let valid = true
if (!apSettings) return;
let valid = true;
// Validate SSID
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
valid = false
formErrors.ssid = true
valid = false;
formErrors.ssid = true;
} else {
formErrors.ssid = false
formErrors.ssid = false;
}
// Validate Channel
let channel = Number(apSettings.channel)
let channel = Number(apSettings.channel);
if (1 > channel || channel > 13) {
valid = false
formErrors.channel = true
valid = false;
formErrors.channel = true;
} else {
formErrors.channel = false
formErrors.channel = false;
}
// Validate max_clients
let maxClients = Number(apSettings.max_clients)
let maxClients = Number(apSettings.max_clients);
if (1 > maxClients || maxClients > 8) {
valid = false
formErrors.max_clients = true
valid = false;
formErrors.max_clients = true;
} else {
formErrors.max_clients = false
formErrors.max_clients = false;
}
// RegEx for IPv4
const regexExp =
/\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/
/\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/;
// Validate gateway IP
if (!regexExp.test(apSettings.gateway_ip)) {
valid = false
formErrors.gateway_ip = true
valid = false;
formErrors.gateway_ip = true;
} else {
formErrors.gateway_ip = false
formErrors.gateway_ip = false;
}
// Validate Subnet Mask
if (!regexExp.test(apSettings.subnet_mask)) {
valid = false
formErrors.subnet_mask = true
valid = false;
formErrors.subnet_mask = true;
} else {
formErrors.subnet_mask = false
formErrors.subnet_mask = false;
}
// Validate local IP
if (!regexExp.test(apSettings.local_ip)) {
valid = false
formErrors.local_ip = true
valid = false;
formErrors.local_ip = true;
} else {
formErrors.local_ip = false
formErrors.local_ip = false;
}
// Submit JSON to REST API
if (valid) {
postAPSettings(apSettings)
postAPSettings(apSettings);
}
}
</script>
@@ -164,7 +162,8 @@
<div class="w-full overflow-x-auto">
{#await getAPStatus()}
<Spinner />
{:then nothing}
{:then}
{#if apStatus}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
@@ -180,6 +179,7 @@
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
</div>
{/if}
{/await}
</div>
@@ -190,7 +190,8 @@
</div>
{#await getAPSettings()}
<Spinner />
{:then nothing}
{:then}
{#if apSettings}
<div
class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
@@ -292,9 +293,8 @@
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
''
)}"
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
size="15"
@@ -359,6 +359,7 @@
</div>
</form>
</div>
{/if}
{/await}
</div>
</SettingsCard>
+33 -49
View File
@@ -6,15 +6,9 @@
import type { NetworkItem, NetworkList } from '$lib/types/models';
import { api } from '$lib/api';
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
import { modals, exitBeforeEnter } from 'svelte-modals';
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
storeNetwork: any;
}
let { isOpen, storeNetwork }: Props = $props();
let { isOpen, storeNetwork }: ModalProps = $props();
const encryptionType = [
'Open',
@@ -26,49 +20,49 @@
'WPA3 PSK',
'WPA2 WPA3 PSK',
'WAPI PSK'
];
]
let listOfNetworks: NetworkItem[] = $state([]);
let listOfNetworks: NetworkItem[] = $state([])
let scanActive = $state(false);
let scanActive = $state(false)
let pollingId: number;
let pollingId: ReturnType<typeof setTimeout> | number
async function scanNetworks() {
scanActive = true;
await api.get('/api/wifi/scan');
scanActive = true
await api.get('/api/wifi/scan')
if ((await pollingResults()) == false) {
pollingId = setInterval(() => pollingResults(), 1000);
pollingId = setInterval(() => pollingResults(), 1000)
}
return;
return
}
async function pollingResults() {
const result = await api.get<NetworkList>('/api/wifi/networks');
const result = await api.get<NetworkList>('/api/wifi/networks')
if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner);
return false;
console.error(`Error occurred while fetching: `, result.inner)
return false
}
let response = result.inner;
listOfNetworks = response.networks;
scanActive = false;
let response = result.inner
listOfNetworks = response.networks
scanActive = false
if (listOfNetworks.length) {
clearInterval(pollingId);
pollingId = 0;
clearInterval(pollingId)
pollingId = 0
}
return listOfNetworks.length;
return listOfNetworks.length
}
onMount(() => {
scanNetworks();
});
scanNetworks()
})
onDestroy(() => {
if (pollingId) {
clearInterval(pollingId);
pollingId = 0;
clearInterval(pollingId)
pollingId = 0
}
});
})
</script>
{#if isOpen}
@@ -77,17 +71,13 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
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"
>
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">Scan Networks</h2>
<div class="divider my-2"></div>
<div class="overflow-y-auto">
{#if scanActive}<div
class="bg-base-100 flex flex-col items-center justify-center p-6"
>
{#if scanActive}<div class="bg-base-100 flex flex-col items-center justify-center p-6">
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
<p class="mt-8 text-2xl">Scanning ...</p>
</div>
@@ -99,21 +89,17 @@
<div
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
onclick={() => {
storeNetwork(network.ssid);
storeNetwork(network.ssid)
}}
role="button"
tabindex="0"
>
tabindex="0">
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Network
class="text-primary-content h-auto w-full scale-75"
/>
<Network class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">{network.ssid}</div>
<div class="text-sm opacity-75">
Security: {encryptionType[network.encryption_type]},
Channel: {network.channel}
Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
</div>
</div>
<div class="grow"></div>
@@ -129,16 +115,14 @@
<button
class="btn btn-primary inline-flex flex-none items-center"
disabled={scanActive}
onclick={scanNetworks}
>
onclick={scanNetworks}>
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
</button>
<div class="grow"></div>
<button
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
onclick={() => modals.close()}
>
onclick={() => modals.close()}>
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
</button>
</div>
+20 -9
View File
@@ -11,7 +11,12 @@
import ScanNetworks from './Scan.svelte'
import Spinner from '$lib/components/Spinner.svelte'
import InfoDialog from '$lib/components/InfoDialog.svelte'
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'
import {
MessageTopic,
type KnownNetworkItem,
type WifiSettings,
type WifiStatus
} from '$lib/types/models'
import { socket } from '$lib/stores'
import { api } from '$lib/api'
import {
@@ -51,8 +56,8 @@
let newNetwork: boolean = $state(true)
let showNetworkEditor: boolean = $state(false)
let wifiStatus: WifiStatus = $state()
let wifiSettings: WifiSettings = $state()
let wifiStatus: WifiStatus | null = $state(null)
let wifiSettings: WifiSettings | null = $state(null)
let dndNetworkList: KnownNetworkItem[] = $state([])
@@ -92,10 +97,10 @@
return wifiSettings
}
onDestroy(() => socket.off('WiFiSettings'))
onDestroy(() => socket.off(MessageTopic.WiFiSettings))
onMount(() => {
socket.on<WifiSettings>('WiFiSettings', data => {
socket.on<WifiSettings>(MessageTopic.WiFiSettings, data => {
wifiSettings = data
dndNetworkList = wifiSettings.wifi_networks
})
@@ -113,6 +118,7 @@
}
function validateHostName() {
if (!wifiSettings) return false
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
formErrorhostname = true
} else {
@@ -291,7 +297,8 @@
<div class="w-full overflow-x-auto">
{#await getWifiStatus()}
<Spinner />
{:then nothing}
{:then}
{#if wifiStatus}
<div
class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
@@ -339,6 +346,7 @@
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
</div>
{/if}
{/if}
{/await}
</div>
@@ -349,7 +357,8 @@
</div>
{#await getWifiSettings()}
<Spinner />
{:then nothing}
{:then}
{#if wifiSettings}
<div class="relative w-full overflow-visible">
<button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
@@ -379,7 +388,7 @@
itemSize={60}
itemCount={dndNetworkList.length}
on:drop={onDrop}>
{#snippet children({ index })}
{#snippet children({ index }: { index: number })}
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
<div class="space-x-0 px-0 mx-0">
<button
@@ -519,7 +528,8 @@
bind:value={networkEditable.gateway_ip}
required />
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
<span
class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span>
</label>
</div>
@@ -597,6 +607,7 @@
</div>
</form>
</div>
{/if}
{/await}
</div>
</SettingsCard>
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Plastic_-_Matte_(Black)
Kd 0.098039 0.098039 0.098039
File diff suppressed because it is too large Load Diff
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
File diff suppressed because it is too large Load Diff
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Opaque(28,28,28)
Kd 0.109804 0.109804 0.109804
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
newmtl Opaque(0,0,255)
Kd 0.000000 0.000000 1.000000
newmtl Opaque(202,209,238)
Kd 0.792157 0.819608 0.933333
File diff suppressed because it is too large Load Diff
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Opaque(28,28,28)
Kd 0.109804 0.109804 0.109804
newmtl Opaque(0,0,255)
Kd 0.000000 0.000000 1.000000
newmtl Opaque(202,209,238)
Kd 0.792157 0.819608 0.933333
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
File diff suppressed because it is too large Load Diff
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Opaque(28,28,28)
Kd 0.109804 0.109804 0.109804
newmtl Opaque(0,0,255)
Kd 0.000000 0.000000 1.000000
newmtl Opaque(202,209,238)
Kd 0.792157 0.819608 0.933333
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
File diff suppressed because it is too large Load Diff
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Opaque(28,28,28)
Kd 0.109804 0.109804 0.109804
newmtl Opaque(0,0,255)
Kd 0.000000 0.000000 1.000000
newmtl Opaque(202,209,238)
Kd 0.792157 0.819608 0.933333
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
File diff suppressed because it is too large Load Diff
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
File diff suppressed because it is too large Load Diff
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
# WaveFront *.mtl file (generated by Autodesk ATF)
newmtl Stainless_Steel_-_Satin
Kd 0.796078 0.796078 0.796078
newmtl Steel_-_Satin
Kd 0.627451 0.627451 0.627451
newmtl Plastic_-_Matte_(Black)
Kd 0.098039 0.098039 0.098039
newmtl Rubber_-_Soft
Kd 0.152941 0.152941 0.152941
File diff suppressed because it is too large Load Diff
Binary file not shown.
+402
View File
@@ -0,0 +1,402 @@
<?xml version="1.0"?>
<robot name="Dog">
<material name="white">
<color rgba="1 1 1 1" />
</material>
<material name="black">
<color rgba="0 0 0 1" />
</material>
<material name="foot_color">
<color rgba="0 0.75 1 1" />
</material>
<link name="base_link">
<visual>
<geometry>
<mesh filename="package://URDF/frame.stl" />
</geometry>
<origin rpy="0 0 3.141" xyz="0 0 0.0" />
<material name="black" />
</visual>
<inertial>
<mass value="1.5" />
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0" />
</inertial>
</link>
<!-- shell -->
<link name="frame">
<visual>
<geometry>
<mesh filename="package://URDF/shell.stl" />
</geometry>
<origin rpy="0 0 3.141" xyz="0 0 0" />
<material name="white" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<mesh filename="package://URDF/shell.stl" />
</geometry>
<origin rpy="0 0 3.141" xyz="0 0 0.0" />
</collision>
</link>
<joint name="base_to_frame" type="fixed">
<parent link="base_link" />
<child link="frame" />
<origin xyz="0 0 0" />
</joint>
<!-- lf shoulder -->
<link name="lf_shoulder">
<visual>
<geometry>
<mesh filename="package://URDF/lf shoulder.stl" />
</geometry>
<origin rpy="3.141 3.141 0" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="lf_shoulder" type="continuous">
<parent link="base_link" />
<child link="lf_shoulder" />
<origin xyz="0.090 0.037 0" />
<axis xyz="1 0 0" />
</joint>
<link name="lf_thigh">
<visual>
<geometry>
<mesh filename="package://URDF/femur.stl" />
</geometry>
<origin rpy="1.5708 0 1.5708" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="lf_thigh" type="continuous">
<parent link="lf_shoulder" />
<child link="lf_thigh" />
<origin xyz="0.025 0.027 0 " />
<axis xyz="0 1 0" />
</joint>
<link name="lf_shin">
<visual>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
</collision>
</link>
<joint name="lf_shin" type="continuous">
<parent link="lf_thigh" />
<child link="lf_shin" />
<origin xyz="0 0 -0.130 " />
<axis xyz="0 1 0" />
</joint>
<link name="lf_toe">
<visual>
<geometry>
<sphere radius="0.005" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
<material name="foot_color" />
</visual>
<inertial>
<mass value="0.05" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<sphere radius="0.020" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
</collision>
</link>
<joint name="lf_toe" type="fixed">
<parent link="lf_shin" />
<child link="lf_toe" />
<origin xyz="0 0 -0.130" />
</joint>
<!-- rf shoulder -->
<link name="rf_shoulder">
<visual>
<geometry>
<mesh filename="package://URDF/rf shoulder.stl" />
</geometry>
<origin rpy="0 0 3.141" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="rf_shoulder" type="continuous">
<parent link="base_link" />
<child link="rf_shoulder" />
<origin xyz="0.090 -0.040 0" />
<axis xyz="1 0 0" />
</joint>
<link name="rf_thigh">
<visual>
<geometry>
<mesh filename="package://URDF/femur.stl" />
</geometry>
<origin rpy="4.71239 3.141 1.5708" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="rf_thigh" type="continuous">
<parent link="rf_shoulder" />
<child link="rf_thigh" />
<origin xyz="0.025 -0.027 0 " />
<axis xyz="0 1 0" />
</joint>
<link name="rf_shin">
<visual>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.0045 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.007 0" />
</collision>
</link>
<joint name="rf_shin" type="continuous">
<parent link="rf_thigh" />
<child link="rf_shin" />
<origin xyz="0 0 -0.130 " />
<axis xyz="0 1 0" />
</joint>
<link name="rf_toe">
<visual>
<geometry>
<sphere radius="0.005" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
<material name="foot_color" />
</visual>
<inertial>
<mass value="0.05" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<sphere radius="0.020" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
</collision>
</link>
<joint name="rf_toe" type="fixed">
<parent link="rf_shin" />
<child link="rf_toe" />
<origin xyz="0 0 -0.130" />
</joint>
<!-- lb shoulder -->
<link name="lb_shoulder">
<visual>
<geometry>
<mesh filename="package://URDF/lb shoulder.stl" />
</geometry>
<origin rpy="3.141 3.141 0" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="lb_shoulder" type="continuous">
<parent link="base_link" />
<child link="lb_shoulder" />
<origin xyz="-0.081 0.038 0" />
<axis xyz="1 0 0" />
</joint>
<link name="lb_thigh">
<visual>
<geometry>
<mesh filename="package://URDF/femur.stl" />
</geometry>
<origin rpy="1.5708 0 1.5708" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="lb_thigh" type="continuous">
<parent link="lb_shoulder" />
<child link="lb_thigh" />
<origin xyz="-0.043 0.027 0 " />
<axis xyz="0 1 0" />
</joint>
<link name="lb_shin">
<visual>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
</collision>
</link>
<joint name="lb_shin" type="continuous">
<parent link="lb_thigh" />
<child link="lb_shin" />
<origin xyz="0 0 -0.130 " />
<axis xyz="0 1 0" />
</joint>
<link name="lb_toe">
<visual>
<geometry>
<sphere radius="0.0005" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
<material name="foot_color" />
</visual>
<inertial>
<mass value="0.05" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<sphere radius="0.020" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
</collision>
</link>
<joint name="lb_toe" type="fixed">
<parent link="lb_shin" />
<child link="lb_toe" />
<origin xyz="0 0 -0.130" />
</joint>
<!-- rb arm -->
<link name="rb_shoulder">
<visual>
<geometry>
<mesh filename="package://URDF/rb shoulder.stl" />
</geometry>
<origin rpy="3.141 3.141 0" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="rb_shoulder" type="continuous">
<parent link="base_link" />
<child link="rb_shoulder" />
<origin xyz="-0.081 -0.040 0" />
<axis xyz="1 0 0" />
</joint>
<link name="rb_thigh">
<visual>
<geometry>
<mesh filename="package://URDF/femur.stl" />
</geometry>
<origin rpy="4.71239 3.141 1.5708" xyz="0 0 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
</link>
<joint name="rb_thigh" type="continuous">
<parent link="rb_shoulder" />
<child link="rb_thigh" />
<origin xyz="-0.043 -0.027 0 " />
<axis xyz="0 1 0" />
</joint>
<link name="rb_shin">
<visual>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.0045 0" />
<material name="black" />
</visual>
<inertial>
<mass value="0.025" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<mesh filename="package://URDF/tibia.stl" />
</geometry>
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.007 0" />
</collision>
</link>
<joint name="rb_shin" type="continuous">
<parent link="rb_thigh" />
<child link="rb_shin" />
<origin xyz="0 0 -0.130 " />
<axis xyz="0 1 0" />
</joint>
<link name="rb_toe">
<visual>
<geometry>
<sphere radius="0.0005" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
<material name="foot_color" />
</visual>
<inertial>
<mass value="0.05" />
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
</inertial>
<collision>
<geometry>
<sphere radius="0.020" />
</geometry>
<origin rpy="0 0 0" xyz="0 0 0" />
</collision>
</link>
<joint name="rb_toe" type="fixed">
<parent link="rb_shin" />
<child link="rb_toe" />
<origin xyz="0 0 -0.130" />
</joint>
</robot>
+6 -3
View File
@@ -1,10 +1,10 @@
import adapter from '@sveltejs/adapter-static'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
const basePath = process.env.BASE_PATH ?? ''
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
// for more information about preprocessors
preprocess: vitePreprocess(),
kit: {
@@ -14,7 +14,10 @@ const config = {
fallback: 'index.html',
precompress: false,
strict: true
})
}),
paths: {
base: basePath
}
}
}
+7 -20
View File
@@ -1,24 +1,11 @@
import { expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test'
test('has title', async ({ page }) => {
await page.goto('/');
await page.route('**/api/features', (route) =>
route.fulfill({
status: 200,
body: JSON.stringify({})
})
);
await expect(page).toHaveTitle(/Spot micro controller/);
});
await page.goto('/')
await expect(page).toHaveTitle(/Spot micro controller/)
})
test('index page has expected h1', async ({ page }) => {
await page.goto('/');
await page.route('**/api/features', (route) =>
route.fulfill({
status: 200,
body: JSON.stringify({})
})
);
await expect(page.getByRole('heading', { name: 'Spot micro controller' }).first()).toBeVisible();
});
await page.goto('/')
await expect(page.getByRole('heading', { name: 'Spot micro controller' }).first()).toBeVisible()
})
+31 -13
View File
@@ -1,4 +1,4 @@
import type { UserConfig, Plugin } from 'vite';
import type { Plugin } from 'vite';
export default function viteLittleFS(): Plugin[] {
return [
@@ -7,25 +7,43 @@ export default function viteLittleFS(): Plugin[] {
enforce: 'post',
apply: 'build',
async config(config, _configEnv) {
const { assetFileNames, chunkFileNames, entryFileNames } =
config.build?.rollupOptions?.output;
async config(config) {
const output = config.build?.rollupOptions?.output;
// Handle Server-build + Client Assets
if (!output || !config.build?.rollupOptions) {
return;
}
const outputOptions = Array.isArray(output) ? output[0] : output;
if (!outputOptions) {
return;
}
const { assetFileNames, chunkFileNames, entryFileNames } = outputOptions;
if (assetFileNames && typeof assetFileNames === 'string') {
config.build.rollupOptions.output = {
...config.build?.rollupOptions?.output,
assetFileNames: assetFileNames.replace('.[hash]', '')
...outputOptions,
assetFileNames: assetFileNames.replace('.[hash]', ''),
};
}
// Handle Client-build
if (config.build?.rollupOptions?.output.chunkFileNames.includes('hash')) {
if (
chunkFileNames &&
typeof chunkFileNames === 'string' &&
chunkFileNames.includes('hash')
) {
config.build.rollupOptions.output = {
...config.build?.rollupOptions?.output,
...config.build.rollupOptions.output,
chunkFileNames: chunkFileNames.replace('.[hash]', ''),
entryFileNames: entryFileNames.replace('.[hash]', '')
...(entryFileNames &&
typeof entryFileNames === 'string' && {
entryFileNames: entryFileNames.replace('.[hash]', ''),
}),
};
}
}
}
},
},
];
}
+3 -3
View File
@@ -5,7 +5,10 @@ import viteLittleFS from './vite-plugin-littlefs'
import EnvCaster from '@niku/vite-env-caster'
import tailwindcss from '@tailwindcss/vite'
const basePath = process.env.BASE_PATH ?? ''
export default defineConfig({
base: basePath,
plugins: [
tailwindcss(),
sveltekit(),
@@ -15,9 +18,6 @@ export default defineConfig({
viteLittleFS(),
EnvCaster()
],
test: {
include: ['src/**/*.{test,spec}.{js,ts}']
},
server: {
proxy: {
'/api': {
+1 -1
View File
@@ -4,7 +4,7 @@ build_flags =
-D APPLICATION_CORE=0
-D EMBED_WWW
-D SERVE_CONFIG_FILES
-D CORS_ORIGIN=\"*\"
-D ENABLE_CORS
-D USE_MSGPACK=1 ; Use either msgpack or json
-D USE_JSON=0 ; Use either msgpack or json
+3 -1
View File
@@ -2,7 +2,8 @@
build_flags =
; Kinematics - Choose only one
-D SPOTMICRO_ESP32
;-D SPOTMICRO_YERTLE
; -D SPOTMICRO_ESP32_MINI
; -D SPOTMICRO_YERTLE
; Firmware flags
-D USE_SLEEP=1
@@ -19,3 +20,4 @@ build_flags =
-D USE_BNO055=0
-D USE_USS=0
-D USE_PCA9685=1
-D USE_PAJ7620U2=0
-43
View File
@@ -1,43 +0,0 @@
#pragma once
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include "comm_base.hpp"
#include "event_bus.hpp"
#include "topic.hpp"
class BluetoothService : public CommBase<> {
BLEServer* bleServer {nullptr};
BLECharacteristic* txCharacteristic {nullptr};
BLECharacteristic* rxCharacteristic {nullptr};
bool connected {false};
public:
void begin(const char* name);
private:
void handleReceive(const std::string& data);
void send(size_t clientId, const char* data, size_t len) override;
struct ServerCb : BLEServerCallbacks {
BluetoothService* svc;
ServerCb(BluetoothService* s) : svc(s) {}
void onConnect(BLEServer*) override { svc->connected = true; }
void onDisconnect(BLEServer* s) override {
svc->connected = false;
for (size_t i = 0; i < static_cast<size_t>(Topic::COUNT); ++i) svc->unsubscribe(static_cast<Topic>(i), 0);
svc->bleServer->startAdvertising();
}
};
struct RxCb : BLECharacteristicCallbacks {
BluetoothService* svc;
RxCb(BluetoothService* s) : svc(s) {}
void onWrite(BLECharacteristic* c) override {
auto v = c->getValue();
if (!v.empty()) svc->handleReceive(v);
}
};
};
-95
View File
@@ -1,95 +0,0 @@
#pragma once
#include "event_bus.hpp"
#include <ArduinoJson.h>
#include <array>
#include <bitset>
#include <freertos/FreeRTOS.h>
#include "topic.hpp"
#ifndef MAX_CID
#define MAX_CID 64
#endif
enum class MsgKind : uint8_t { Connect = 0, Disconnect = 1, Event = 2, Ping = 3, Pong = 4 };
template <size_t MaxCid = MAX_CID, size_t NTopics = static_cast<size_t>(Topic::COUNT)>
class CommBase {
using Bits = std::bitset<MaxCid>;
std::array<Bits, NTopics> subs_;
portMUX_TYPE mux_ portMUX_INITIALIZER_UNLOCKED;
std::array<void*, NTopics> subscriptionHandle {};
static constexpr size_t invalid = SIZE_MAX;
static constexpr size_t idx(Topic t) {
size_t i = static_cast<size_t>(t);
return i < NTopics ? i : invalid;
}
template <Topic T>
void encode(JsonDocument& d, const typename TopicTraits<T>::Msg& m) {
auto a = d.to<JsonArray>();
a.add(static_cast<uint8_t>(MsgKind::Event));
a.add(static_cast<uint8_t>(T));
toJson(a.add<JsonVariant>(), m);
}
protected:
virtual void send(size_t cid, const char* data, size_t len) = 0;
template <class Msg>
auto& getHandle(Topic topic) {
using H = typename EventBus<Msg>::Handle;
static H dummy;
auto* p = static_cast<H*>(subscriptionHandle[size_t(topic)]);
return p ? *p : dummy;
}
template <class Msg>
void setHandle(Topic topic, typename EventBus<Msg>::Handle&& h) {
using H = typename EventBus<Msg>::Handle;
subscriptionHandle[size_t(topic)] = new H(std::move(h));
}
public:
void subscribe(Topic t, size_t cid) {
size_t i = idx(t);
if (i == invalid) return;
portENTER_CRITICAL(&mux_);
subs_[i].set(cid);
portEXIT_CRITICAL(&mux_);
}
void unsubscribe(Topic t, size_t cid) {
size_t i = idx(t);
if (i == invalid) return;
portENTER_CRITICAL(&mux_);
subs_[i].reset(cid);
portEXIT_CRITICAL(&mux_);
}
bool has(Topic t) const {
size_t i = idx(t);
return i == invalid ? false : subs_[i].any();
}
template <Topic T>
void emit(const typename TopicTraits<T>::Msg& m) {
constexpr size_t i = idx(T);
if (i == invalid) return;
if (!subs_[i].any()) return;
JsonDocument doc;
encode<T>(doc, m);
String out;
#if USE_MSGPACK
serializeMsgPack(doc, out);
#else
serializeJson(doc, out);
#endif
auto& b = subs_[i];
for (size_t cid = 0; cid < MaxCid; ++cid)
if (b.test(cid)) send(cid, out.c_str(), out.length());
}
};
-33
View File
@@ -1,33 +0,0 @@
#ifndef Socket_h
#define Socket_h
#include <PsychicHttp.h>
#include <ArduinoJson.h>
#include <map>
#include <list>
#include <functional>
#include "event_bus.hpp"
#include "adapters/comm_base.hpp"
#include "topic.hpp"
class EventSocket : public CommBase<> {
PsychicWebSocketHandler _socket;
public:
EventSocket();
PsychicWebSocketHandler *getHandler() { return &_socket; }
private:
void send(size_t clientId, const char *data, size_t len) override;
void handleReceive(const std::string &data);
void onWSOpen(PsychicWebSocketClient *client);
void onWSClose(PsychicWebSocketClient *client);
esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
};
extern EventSocket socket;
#endif
-259
View File
@@ -1,259 +0,0 @@
#pragma once
#include <array>
#include <optional>
#include <atomic>
#include <cstddef>
#include <cstring>
#include <type_traits>
#include <utility>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
template <typename Sig, size_t MaxSize>
class FixedFn;
template <typename R, typename... A, size_t MaxSize>
class FixedFn<R(A...), MaxSize> {
alignas(void*) std::byte buf[MaxSize];
void (*call)(void*, A&&...) {};
void (*moveFn)(void*, void*) {};
void (*destroy)(void*) {};
public:
template <typename Fun>
void set(Fun&& f) {
static_assert(sizeof(Fun) <= MaxSize);
new (buf) Fun(std::forward<Fun>(f));
call = [](void* p, A&&... as) { (*reinterpret_cast<Fun*>(p))(std::forward<A>(as)...); };
moveFn = [](void* d, void* s) { new (d) Fun(std::move(*reinterpret_cast<Fun*>(s))); };
destroy = [](void* p) { reinterpret_cast<Fun*>(p)->~Fun(); };
}
R operator()(A... as) const {
return call(const_cast<void*>(static_cast<const void*>(buf)), std::forward<A>(as)...);
}
FixedFn() = default;
FixedFn(FixedFn&& o) {
if (o.moveFn) o.moveFn(buf, o.buf);
call = o.call;
moveFn = o.moveFn;
destroy = o.destroy;
o.destroy = nullptr;
}
FixedFn(const FixedFn& o) {
std::memcpy(buf, o.buf, MaxSize);
call = o.call;
moveFn = o.moveFn;
destroy = o.destroy;
}
~FixedFn() {
if (destroy) destroy(buf);
}
FixedFn& operator=(const FixedFn&) = delete;
FixedFn& operator=(FixedFn&&) = delete;
};
enum class EmitMode { Latest, Batch };
template <typename Msg, size_t QueueDepth = 64, size_t MaxSubs = 8, size_t BatchSize = 16>
class EventBus {
struct Item {
Msg payload;
size_t exclude;
};
static constexpr size_t NO_EX = MaxSubs;
struct Sub {
FixedFn<void(const Msg*, size_t), 48> cb;
TickType_t interval;
TickType_t last;
EmitMode mode;
std::array<Msg, BatchSize> buf;
size_t cnt;
};
inline static StaticQueue_t qbuf;
inline static Item qStorage[QueueDepth];
inline static QueueHandle_t queue =
xQueueCreateStatic(QueueDepth, sizeof(Item), reinterpret_cast<uint8_t*>(qStorage), &qbuf);
inline static std::array<std::optional<Sub>, MaxSubs> subs {};
inline static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
inline static Msg latest {};
inline static std::atomic<bool> hasLatest {false};
inline static std::atomic<size_t> subCount {0};
static void store(const Msg& m) {
portENTER_CRITICAL(&mux);
latest = m;
hasLatest.store(true, std::memory_order_release);
portEXIT_CRITICAL(&mux);
}
static void storeISR(const Msg& m) {
UBaseType_t s = portSET_INTERRUPT_MASK_FROM_ISR();
latest = m;
hasLatest.store(true, std::memory_order_release);
portCLEAR_INTERRUPT_MASK_FROM_ISR(s);
}
static void dispatch(const Msg& m, size_t ex) {
TickType_t now = xTaskGetTickCount();
Sub* ready[MaxSubs];
size_t readyCnt = 0;
portENTER_CRITICAL(&mux);
for (size_t i = 0; i < MaxSubs; ++i) {
auto& opt = subs[i];
if (!opt || i == ex) continue;
Sub& s = *opt;
TickType_t dt = now - s.last;
if (s.interval && dt < s.interval) {
if (s.mode == EmitMode::Batch && s.cnt < BatchSize)
s.buf[s.cnt++] = m;
else if (s.mode == EmitMode::Latest) {
s.buf[0] = m;
s.cnt = 1;
}
} else {
s.buf[s.cnt++] = m;
s.last = now;
ready[readyCnt++] = &s;
}
}
portEXIT_CRITICAL(&mux);
for (size_t i = 0; i < readyCnt; ++i) {
Sub* s = ready[i];
s->cb(s->buf.data(), s->cnt);
s->cnt = 0;
}
}
static void worker(void*) {
Item it;
while (xQueueReceive(queue, &it, portMAX_DELAY) == pdTRUE) dispatch(it.payload, it.exclude);
}
static void ensureTask() {
static bool once = (xTaskCreatePinnedToCore(worker, "evtbus", 4096, nullptr, 6, nullptr, 1), true);
(void)once;
}
static bool push(const Msg& m, size_t ex = NO_EX, TickType_t to = 0) {
ensureTask();
Item it {m, ex};
return xQueueSend(queue, &it, to) == pdTRUE;
}
public:
class Handle {
size_t idx {NO_EX};
friend class EventBus;
explicit Handle(size_t i) : idx(i) {}
public:
Handle() = default;
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& o) noexcept : idx(o.idx) { o.idx = NO_EX; }
Handle& operator=(Handle&& o) noexcept {
if (this != &o) {
unsubscribe();
idx = o.idx;
o.idx = NO_EX;
}
return *this;
}
~Handle() { unsubscribe(); }
void unsubscribe() {
if (idx < MaxSubs) {
portENTER_CRITICAL(&mux);
subs[idx].reset();
portEXIT_CRITICAL(&mux);
subCount.fetch_sub(1, std::memory_order_acq_rel);
idx = NO_EX;
}
}
bool valid() const { return idx < MaxSubs; }
};
template <typename C>
static Handle subscribe(uint32_t ms, EmitMode mode, C fn) {
ensureTask();
portENTER_CRITICAL(&mux);
for (size_t i = 0; i < MaxSubs; ++i)
if (!subs[i]) {
subs[i].emplace();
Sub& s = *subs[i];
s.cb.set(std::move(fn));
s.interval = pdMS_TO_TICKS(ms);
s.last = xTaskGetTickCount();
s.mode = mode;
s.cnt = 0;
subCount.fetch_add(1, std::memory_order_acq_rel);
portEXIT_CRITICAL(&mux);
return Handle(i);
}
portEXIT_CRITICAL(&mux);
return Handle(NO_EX);
}
template <typename C>
static Handle subscribe(C fn) {
if constexpr (std::is_invocable_v<C, const Msg*, size_t>)
return subscribe(0, EmitMode::Latest, std::move(fn));
else
return subscribe(0, EmitMode::Latest, [fn = std::move(fn)](const Msg* p, size_t n) {
for (size_t i = 0; i < n; ++i) fn(p[i]);
});
}
template <typename C>
static Handle subscribe(uint32_t ms, C fn) {
if constexpr (std::is_invocable_v<C, const Msg*, size_t>)
return subscribe(ms, EmitMode::Batch, std::move(fn));
else
return subscribe(ms, EmitMode::Batch, [fn = std::move(fn)](const Msg* p, size_t n) {
for (size_t i = 0; i < n; ++i) fn(p[i]);
});
}
static void publish(const Msg& m) {
store(m);
push(m, NO_EX, portMAX_DELAY);
}
static void publish(const Msg& m, const Handle& h) {
if (h.valid())
dispatch(m, h.idx);
else
publish(m);
}
static bool publishAsync(const Msg& m) {
store(m);
return push(m);
}
static bool publishAsync(const Msg& m, const Handle& h) {
if (h.valid()) dispatch(m, h.idx);
return publishAsync(m);
}
static void publishISR(const Msg& m, BaseType_t* hpw = nullptr) {
storeISR(m);
Item it {m, NO_EX};
xQueueSendFromISR(queue, &it, hpw);
}
static bool peek(Msg& out) {
if (!hasLatest.load(std::memory_order_acquire)) return false;
portENTER_CRITICAL(&mux);
out = latest;
portEXIT_CRITICAL(&mux);
return true;
}
static bool take(Msg& out) {
if (!hasLatest.load(std::memory_order_acquire)) return false;
portENTER_CRITICAL(&mux);
out = latest;
hasLatest.store(false, std::memory_order_release);
portEXIT_CRITICAL(&mux);
return true;
}
static bool hasSubscribers() { return subCount.load(std::memory_order_acquire) > 0; }
};
+46
View File
@@ -0,0 +1,46 @@
#ifndef Socket_h
#define Socket_h
#include <PsychicHttp.h>
#include <template/stateful_service.h>
#include <list>
#include <map>
#include <vector>
enum message_type_t { CONNECT = 0, DISCONNECT = 1, EVENT = 2, PING = 3, PONG = 4, BINARY_EVENT = 5 };
typedef std::function<void(JsonVariant &root, int originId)> EventCallback;
typedef std::function<void(const String &originId, bool sync)> SubscribeCallback;
class EventSocket {
public:
EventSocket();
PsychicWebSocketHandler *getHandler() { return &_socket; }
bool hasSubscribers(const char *event);
void onEvent(String event, EventCallback callback);
void onSubscribe(String event, SubscribeCallback callback);
void emit(const char *event, JsonVariant &payload, const char *originId = "", bool onlyToSameOrigin = false);
private:
PsychicWebSocketHandler _socket;
std::map<String, std::list<int>> client_subscriptions;
std::map<String, std::list<EventCallback>> event_callbacks;
std::map<String, std::list<SubscribeCallback>> subscribe_callbacks;
void handleEventCallbacks(String event, JsonVariant &jsonObject, int originId);
void send(PsychicWebSocketClient *client, const char *data, size_t len);
void handleSubscribeCallbacks(String event, const String &originId);
void onWSOpen(PsychicWebSocketClient *client);
void onWSClose(PsychicWebSocketClient *client);
esp_err_t onFrame(PsychicWebSocketRequest *request, httpd_ws_frame *frame);
};
extern EventSocket socket;
#endif
+18
View File
@@ -79,6 +79,24 @@
static_assert(!(USE_JSON == 1 && USE_MSGPACK == 1), "Cannot set both USE_JSON and USE_MSGPACK to 1 simultaneously");
#if defined(SPOTMICRO_ESP32) && defined(SPOTMICRO_ESP32_MINI) && defined(SPOTMICRO_YERTLE)
#error "Only one kinematics variant must be defined"
#endif
#if !defined(SPOTMICRO_ESP32) && !defined(SPOTMICRO_ESP32_MINI) && !defined(SPOTMICRO_YERTLE)
#error "You must define one kinematics variant"
#endif
#if defined(SPOTMICRO_ESP32)
#define KINEMATICS_VARIANT_STR "SPOTMICRO_ESP32"
#elif defined(SPOTMICRO_ESP32_MINI)
#define KINEMATICS_VARIANT_STR "SPOTMICRO_ESP32_MINI"
#elif defined(SPOTMICRO_YERTLE)
#define KINEMATICS_VARIANT_STR "SPOTMICRO_YERTLE"
#else
#define KINEMATICS_VARIANT_STR "UNKNOWN"
#endif
namespace feature_service {
void printFeatureConfiguration();
+1 -1
View File
@@ -4,7 +4,7 @@
#include <WiFi.h>
#include <ArduinoJson.h>
#include <event_bus.hpp>
#include <event_socket.h>
#include <PsychicHttp.h>
#include <HTTPClient.h>
+1 -1
View File
@@ -8,7 +8,7 @@
#include <PsychicHttp.h>
#include <system_service.h>
#include <event_bus.hpp>
#include <event_socket.h>
enum FileType { ft_none = 0, ft_firmware = 1, ft_md5 = 2 };
-135
View File
@@ -1,135 +0,0 @@
#pragma once
#include <gait/state.h>
#include <utils/math_utils.h>
#include <array>
#include <functional>
class BezierState : public GaitState {
private:
float phase_time = 0.0f;
static constexpr float PHASE_OFFSET[4] = {0.f, 0.5f, 0.5f, 0.f};
static constexpr float STAND_OFFSET = 0.75f;
static constexpr uint8_t BEZIER_POINTS = 12;
float step_length = 0.0f;
static constexpr std::array<float, BEZIER_POINTS> COMBINATORIAL_VALUES = {
combinatorial_constexpr(11, 0), // 1
combinatorial_constexpr(11, 1), // 11
combinatorial_constexpr(11, 2), // 55
combinatorial_constexpr(11, 3), // 165
combinatorial_constexpr(11, 4), // 330
combinatorial_constexpr(11, 5), // 462
combinatorial_constexpr(11, 6), // 462
combinatorial_constexpr(11, 7), // 330
combinatorial_constexpr(11, 8), // 165
combinatorial_constexpr(11, 9), // 55
combinatorial_constexpr(11, 10), // 11
combinatorial_constexpr(11, 11) // 1
};
alignas(32) static constexpr float BEZIER_STEPS[12] = {-1.0f, -1.4f, -1.5f, -1.5f, -1.5f, 0.0f,
0.0f, 0.0f, 1.5f, 1.5f, 1.4f, 1.0f};
alignas(32) static constexpr float BEZIER_HEIGHTS[12] = {0.0f, 0.0f, 0.9f, 0.9f, 0.9f, 0.9f,
0.9f, 1.1f, 1.1f, 1.1f, 0.0f, 0.0f};
public:
const char *name() const override { return "Bezier"; }
void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) override {
this->mapCommand(command);
step_length = std::hypot(gait_state.step_x, gait_state.step_z);
if (gait_state.step_x < 0.0f) {
step_length = -step_length;
}
updatePhase(dt);
updateFeetPositions(body_state);
}
protected:
void updatePhase(float dt) { phase_time = std::fmod(phase_time + dt * gait_state.step_velocity * 2, 1.0f); }
void updateFeetPositions(body_state_t &body_state) {
for (int i = 0; i < 4; ++i) {
updateFootPosition(body_state, i);
}
}
void updateFootPosition(body_state_t &body_state, const int index) {
body_state.feet[index][0] = this->default_feet_pos[index][0];
body_state.feet[index][1] = this->default_feet_pos[index][1];
body_state.feet[index][2] = this->default_feet_pos[index][2];
const float leg_phase = std::fmod(phase_time + PHASE_OFFSET[index], 1.0f);
const bool contact = leg_phase <= STAND_OFFSET;
contact ? standController(body_state, index, leg_phase / 0.75)
: swingController(body_state, index, (leg_phase - 0.75) / (1 - 0.75));
}
void standController(body_state_t &body_state, const int index, const float phase) {
controller(index, body_state, phase, stanceCurve, &gait_state.step_depth);
}
void swingController(body_state_t &body_state, const int index, const float phase) {
controller(index, body_state, phase, bezierCurve, &gait_state.step_height);
}
void controller(const int index, body_state_t &body_state, const float phase,
std::function<void(float, float, float *, float, float *)> curve, float *arg) {
float delta_pos[3] = {0};
float delta_rot[3] = {0};
float length = step_length / 2.0f;
float angle = std::atan2(gait_state.step_z, step_length) * 2;
curve(length, angle, arg, phase, delta_pos);
length = gait_state.step_angle * 2.0f;
angle = yawArc(default_feet_pos[index], body_state.feet[index]);
curve(length, angle, arg, phase, delta_rot);
body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2;
if (step_length || gait_state.step_angle) body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2;
body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2;
}
static void stanceCurve(const float length, const float angle, const float *depth, const float phase,
float *point) {
float step = length * (1.0f - 2.0f * phase);
point[0] += step * std::cos(angle);
point[2] += step * std::sin(angle);
if (length != 0.0f) {
point[1] = -*depth * std::cos((M_PI * (point[0] + point[2])) / (2.f * length));
}
}
static void bezierCurve(const float length, const float angle, const float *height, const float phase,
float *point) {
const float X_POLAR = std::cos(angle);
const float Z_POLAR = std::sin(angle);
float phase_power = 1.0f;
float inv_phase_power = std::pow(1.0f - phase, 11);
const float one_minus_phase = 1.0f - phase;
for (int i = 0; i < 12; i++) {
float b = COMBINATORIAL_VALUES[i] * phase_power * inv_phase_power;
point[0] += b * BEZIER_STEPS[i] * length * X_POLAR;
point[1] += b * BEZIER_HEIGHTS[i] * *height;
point[2] += b * BEZIER_STEPS[i] * length * Z_POLAR;
phase_power *= phase;
inv_phase_power /= one_minus_phase;
}
}
static float yawArc(const float feet_pos[4], const float *current_pos) {
const float foot_mag = std::hypot(feet_pos[0], feet_pos[2]);
const float foot_dir = std::atan2(feet_pos[2], feet_pos[0]);
const float offsets[] = {current_pos[0] - feet_pos[0], current_pos[1] - feet_pos[1],
current_pos[2] - feet_pos[2]};
const float offset_mag = std::hypot(offsets[0], offsets[2]);
const float offset_mod = std::atan2(offset_mag, foot_mag);
return M_PI_2 + foot_dir + offset_mod;
}
};
-33
View File
@@ -1,33 +0,0 @@
#pragma once
#include <gait/phase_state_base.h>
class EightPhaseWalkState : public PhaseGaitState {
protected:
const char *name() const override { return "Eight phase walk"; }
int num_phases() const override { return 8; }
float phase_speed_factor() const override { return 4; }
float swing_stand_ratio() const override { return 1.0f / (num_phases() - 1); }
public:
EightPhaseWalkState() {
uint8_t contact[4][8] = {
{1, 0, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 0, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 0}, {1, 1, 1, 0, 1, 1, 1, 1}};
float shift_values[4][3] = {{-0.05f, 0, -0.2f}, {0.25f, 0, 0.2f}, {-0.05f, 0, 0.2f}, {0.25f, 0, -0.2f}};
for (uint8_t i = 0; i < 4; ++i) {
for (uint8_t j = 0; j < 8; ++j) {
contact_phases[i][j] = contact[i][j];
}
for (uint8_t j = 0; j < 3; ++j) {
shifts[i][j] = shift_values[i][j];
}
}
}
void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) override {
return PhaseGaitState::step(body_state, command, dt);
}
};
@@ -1,28 +0,0 @@
#pragma once
#include <gait/phase_state_base.h>
class FourPhaseWalkState : public PhaseGaitState {
protected:
const char *name() const override { return "Four phase walk"; }
int num_phases() const override { return 4; }
float phase_speed_factor() const override { return 6; }
float swing_stand_ratio() const override { return 1.0f / (num_phases() - 1); }
public:
FourPhaseWalkState() {
uint8_t contact[4][4] = {{1, 0, 1, 1}, {1, 1, 1, 0}, {1, 1, 1, 0}, {1, 0, 1, 1}};
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
contact_phases[i][j] = contact[i][j];
}
}
}
void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) override {
return PhaseGaitState::step(body_state, command, dt);
}
};
-8
View File
@@ -1,8 +0,0 @@
#pragma once
#include <gait/state.h>
class IdleState : public GaitState {
protected:
const char *name() const override { return "Idle"; }
};
-78
View File
@@ -1,78 +0,0 @@
#pragma once
#include <gait/state.h>
class PhaseGaitState : public GaitState {
protected:
int phase = 0;
float phase_time = 0;
virtual int num_phases() const = 0;
virtual float phase_speed_factor() const = 0;
virtual float swing_stand_ratio() const = 0;
float dt = 0.02f;
uint8_t contact_phases[4][8];
float shifts[4][3];
void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) override {
mapCommand(command);
this->dt = dt;
updatePhase();
updateBodyPosition(body_state);
updateFeetPositions(body_state);
}
void updatePhase() {
phase_time += dt * phase_speed_factor() * gait_state.step_velocity;
if (phase_time >= 1.0f) {
phase += 1;
if (phase == num_phases()) phase = 0;
phase_time = 0;
}
}
void updateBodyPosition(body_state_t &body_state) {
if (num_phases() == 4) return;
const auto &shift = shifts[phase / 2];
body_state.xm += (shift[0] - body_state.xm) * dt * 4;
body_state.zm += (shift[2] - body_state.zm) * dt * 4;
}
void updateFeetPositions(body_state_t &body_state) {
for (int i = 0; i < 4; ++i) {
updateFootPosition(body_state, i);
}
}
void updateFootPosition(body_state_t &body_state, int index) {
bool contact = contact_phases[index][phase];
contact ? stand(body_state, index) : swing(body_state, index);
}
void stand(body_state_t &body_state, int index) {
float delta_pos[3] = {-gait_state.step_x * dt * swing_stand_ratio(), 0,
-gait_state.step_z * dt * swing_stand_ratio()};
body_state.feet[index][0] += delta_pos[0];
body_state.feet[index][1] = default_feet_pos[index][1];
body_state.feet[index][2] += delta_pos[2];
}
void swing(body_state_t &body_state, int index) {
float delta_pos[3] = {gait_state.step_x * dt, 0, gait_state.step_z * dt};
if (std::fabs(gait_state.step_x) < 0.01) {
delta_pos[0] = (default_feet_pos[index][0] - body_state.feet[index][0]) * dt * 8;
}
if (std::fabs(gait_state.step_z) < 0.01) {
delta_pos[2] = (default_feet_pos[index][2] - body_state.feet[index][2]) * dt * 8;
}
body_state.feet[index][0] += delta_pos[0];
body_state.feet[index][1] = default_feet_pos[index][1] + std::sin(phase_time * M_PI) * gait_state.step_height;
body_state.feet[index][2] += delta_pos[2];
}
};
-18
View File
@@ -1,18 +0,0 @@
#pragma once
#include <gait/state.h>
class RestState : public GaitState {
protected:
const char *name() const override { return "Rest"; }
void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) override {
body_state.omega = 0;
body_state.phi = 0;
body_state.psi = 0;
body_state.xm = 0;
body_state.ym = getDefaultHeight() / 2;
body_state.zm = 0;
body_state.updateFeet(default_feet_pos);
}
};
-17
View File
@@ -1,17 +0,0 @@
#pragma once
#include <gait/state.h>
class StandState : public GaitState {
protected:
const char *name() const override { return "Stand"; }
void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) override {
body_state.omega = 0;
body_state.phi = command.rx / 8;
body_state.psi = command.ry / 8;
body_state.xm = command.ly / 2 / 100;
body_state.zm = command.lx / 2 / 100;
body_state.updateFeet(default_feet_pos);
}
};
-44
View File
@@ -1,44 +0,0 @@
#pragma once
#include <kinematics.h>
struct gait_state_t {
float step_height;
float step_x;
float step_z;
float step_angle;
float step_velocity;
float step_depth;
};
struct ControllerCommand {
int stop;
float lx, ly, rx, ry, h, s, s1;
};
class GaitState {
protected:
virtual const char *name() const = 0;
float default_feet_pos[4][4] = {{1, -1, 0.7, 1}, {1, -1, -0.7, 1}, {-1, -1, 0.7, 1}, {-1, -1, -0.7, 1}};
gait_state_t gait_state = {0.4, 0, 0, 0, 1, 0.002};
void mapCommand(ControllerCommand command) {
this->gait_state.step_height = 0.4 + (command.s1 / 128 + 1) / 2;
this->gait_state.step_x = command.ly / 128;
this->gait_state.step_z = -command.lx / 128;
this->gait_state.step_velocity = command.s / 128 + 1;
this->gait_state.step_angle = command.rx / 128;
this->gait_state.step_depth = 0.002;
}
public:
virtual float getDefaultHeight() const { return 0.5f; }
virtual void begin() { ESP_LOGI("Gait Planner", "Starting %s", name()); }
virtual void end() { ESP_LOGI("Gait Planner", "Ending %s", name()); }
virtual void step(body_state_t &body_state, ControllerCommand command, float dt = 0.02f) {
this->mapCommand(command);
}
};
+76 -38
View File
@@ -3,8 +3,65 @@
#include <utils/math_utils.h>
class KinConfig {
public:
#if defined(SPOTMICRO_ESP32)
static constexpr float coxa = 60.5f / 100.0f;
static constexpr float coxa_offset = 10.0f / 100.0f;
static constexpr float femur = 111.2f / 100.0f;
static constexpr float tibia = 118.5f / 100.0f;
static constexpr float L = 207.5f / 100.0f;
static constexpr float W = 78.0f / 100.0f;
#elif defined(SPOTMICRO_ESP32_MINI)
static constexpr float coxa = 35.0f / 100.0f;
static constexpr float coxa_offset = 0.0f / 100.0f;
static constexpr float femur = 60.0f / 100.0f;
static constexpr float tibia = 60.0f / 100.0f;
static constexpr float L = 160.0f / 100.0f;
static constexpr float W = 80.0f / 100.0f;
#elif defined(SPOTMICRO_YERTLE)
static constexpr float coxa = 35.0f / 100.0f;
static constexpr float coxa_offset = 0.0f;
static constexpr float femur = 130.0f / 100.0f;
static constexpr float tibia = 130.0f / 100.0f;
static constexpr float L = 240.0f / 100.0f;
static constexpr float W = 78.0f / 100.0f;
#endif
static constexpr float mountOffsets[4][3] = {
{L / 2, 0, W / 2}, {L / 2, 0, -W / 2}, {-L / 2, 0, W / 2}, {-L / 2, 0, -W / 2}};
static constexpr float default_feet_positions[4][4] = {
{mountOffsets[0][0], 0, mountOffsets[0][2] + coxa, 1},
{mountOffsets[1][0], 0, mountOffsets[1][2] - coxa, 1},
{mountOffsets[2][0], 0, mountOffsets[2][2] + coxa, 1},
{mountOffsets[3][0], 0, mountOffsets[3][2] - coxa, 1},
};
// Max constants
static constexpr float max_roll = 15 * (float)M_PI_2;
static constexpr float max_pitch = 15 * (float)M_PI_2;
static constexpr float max_body_shift_x = W / 3;
static constexpr float max_body_shift_z = W / 3;
static constexpr float max_leg_reach = femur + tibia - coxa_offset;
static constexpr float min_body_height = max_leg_reach * 0.45;
static constexpr float max_body_height = max_leg_reach * 0.9;
static constexpr float body_height_range = max_body_height - min_body_height;
static constexpr float max_step_length = max_leg_reach * 0.8;
static constexpr float max_step_height = max_leg_reach / 2;
// Default constant
static constexpr float default_step_depth = 0.002;
static constexpr float default_body_height = min_body_height + body_height_range / 2;
static constexpr float default_step_height = default_body_height / 2;
};
struct alignas(16) body_state_t {
float omega, phi, psi, xm, ym, zm;
float omega {0}, phi {0}, psi {0}, xm {0}, ym {KinConfig::default_body_height}, zm {0};
float feet[4][4];
void updateFeet(const float newFeet[4][4]) { COPY_2D_ARRAY_4x4(feet, newFeet); }
@@ -21,25 +78,13 @@ struct alignas(16) body_state_t {
class Kinematics {
private:
#if defined(SPOTMICRO_ESP32)
static constexpr float l1 = 60.5f / 100.0f;
static constexpr float l2 = 10.0f / 100.0f;
static constexpr float l3 = 111.2f / 100.0f;
static constexpr float l4 = 118.5f / 100.0f;
static constexpr float coxa = KinConfig::coxa;
static constexpr float coxa_offset = KinConfig::coxa_offset;
static constexpr float femur = KinConfig::femur;
static constexpr float tibia = KinConfig::tibia;
static constexpr float L = 207.5f / 100.0f;
static constexpr float W = 78.0f / 100.0f;
#elif defined(SPOTMICRO_YERTLE)
static constexpr float l1 = 35.0f / 100.0f;
static constexpr float l2 = 0.0f;
static constexpr float l3 = 130.0f / 100.0f;
static constexpr float l4 = 130.0f / 100.0f;
static constexpr float L = 240.0f / 100.0f;
static constexpr float W = 78.0f / 100.0f;
#else
#error "Must define either SPOTMICRO_ESP32 or SPOTMICRO_YERTLE"
#endif
static constexpr float L = KinConfig::L;
static constexpr float W = KinConfig::W;
static constexpr float mountOffsets[4][3] = {
{L / 2, 0, W / 2}, {L / 2, 0, -W / 2}, {-L / 2, 0, W / 2}, {-L / 2, 0, -W / 2}};
@@ -53,13 +98,6 @@ class Kinematics {
body_state_t currentState;
public:
static constexpr float default_feet_positions[4][4] = {
{mountOffsets[0][0], -1, mountOffsets[0][2] + l1, 1},
{mountOffsets[1][0], -1, mountOffsets[1][2] - l1, 1},
{mountOffsets[2][0], -1, mountOffsets[2][2] + l1, 1},
{mountOffsets[3][0], -1, mountOffsets[3][2] - l1, 1},
};
esp_err_t calculate_inverse_kinematics(const body_state_t body_state, float result[12]) {
esp_err_t ret = ESP_OK;
@@ -145,21 +183,21 @@ class Kinematics {
inv_rot[2][2] = rot[2][2];
}
inline void legIK(float x, float y, float z, float result[3]) {
float F = sqrt(max(0.0f, x * x + y * y - l1 * l1));
float G = F - l2;
inline void legIK(float x, float y, float z, float out[3]) {
float F = sqrt(std::max(0.0f, x * x + y * y - coxa * coxa));
float G = F - coxa_offset;
float H = sqrt(G * G + z * z);
float theta1 = -atan2f(y, x) - atan2f(F, -l1);
float D = (H * H - l3 * l3 - l4 * l4) / (2 * l3 * l4);
float theta3 = acosf(max(-1.0f, min(1.0f, D)));
float theta2 = atan2f(z, G) - atan2f(l4 * sinf(theta3), l3 + l4 * cosf(theta3));
result[0] = RAD_TO_DEG_F(theta1);
result[1] = RAD_TO_DEG_F(theta2);
#if defined(SPOTMICRO_ESP32)
result[2] = RAD_TO_DEG_F(theta3);
float theta1 = -atan2f(y, x) - atan2f(F, -coxa);
float D = (H * H - femur * femur - tibia * tibia) / (2 * femur * tibia);
float theta3 = acosf(std::max(-1.0f, std::min(1.0f, D)));
float theta2 = atan2f(z, G) - atan2f(tibia * sinf(theta3), femur + tibia * cosf(theta3));
out[0] = RAD_TO_DEG_F(theta1);
out[1] = RAD_TO_DEG_F(theta2);
#if defined(SPOTMICRO_ESP32) || defined(SPOTMICRO_ESP32_MINI)
out[2] = RAD_TO_DEG_F(theta3);
#elif defined(SPOTMICRO_YERTLE)
result[2] = RAD_TO_DEG_F(theta3 + theta2);
out[2] = RAD_TO_DEG_F(theta3 + theta2);
#endif
}
};
+1 -1
View File
@@ -25,7 +25,7 @@ class MDNSService : public StatefulService<MDNSSettings> {
void begin();
esp_err_t getStatus(PsychicRequest *request);
void getStatus(JsonObject &root);
void getStatus(JsonVariant &root);
static esp_err_t queryServices(PsychicRequest *request, JsonVariant &json);
@@ -1,18 +1,18 @@
#pragma once
#include <ArduinoJson.h>
struct MotionInputMsg {
struct CommandMsg {
float lx, ly, rx, ry, h, s, s1;
friend void toJson(JsonVariant v, MotionInputMsg const &m) {
friend void toJson(JsonVariant v, CommandMsg const &c) {
JsonArray arr = v.to<JsonArray>();
arr.add(m.lx);
arr.add(m.ly);
arr.add(m.rx);
arr.add(m.ry);
arr.add(m.h);
arr.add(m.s);
arr.add(m.s1);
arr.add(c.lx);
arr.add(c.ly);
arr.add(c.rx);
arr.add(c.ry);
arr.add(c.h);
arr.add(c.s);
arr.add(c.s1);
}
void fromJson(JsonVariantConst o) {
+140 -112
View File
@@ -1,120 +1,171 @@
#ifndef MotionService_h
#define MotionService_h
#include <event_bus.hpp>
#include <topic.hpp>
#include <event_socket.h>
#include <kinematics.h>
#include <peripherals/servo_controller.h>
#include <utils/timing.h>
#include <utils/math_utils.h>
#include <gait/state.h>
#include <gait/crawl_state.h>
#include <gait/bezier_state.h>
#include <motion_states/state.h>
#include <motion_states/walk_state.h>
#include <motion_states/stand_state.h>
#include <motion_states/rest_state.h>
#include <motion_skills/skill_manager.h>
#include <message_types.h>
#define DEFAULT_STATE false
#define ANGLES_EVENT "angles"
#define INPUT_EVENT "input"
#define POSITION_EVENT "position"
#define MODE_EVENT "mode"
#define WALK_GAIT_EVENT "walk_gait"
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, CRAWL, WALK };
enum class MOTION_STATE { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, WALK };
class MotionService {
public:
MotionService(ServoController* servoController) : _servoController(servoController) {}
MotionService(ServoController *servoController, Peripherals *peripherals)
: _servoController(servoController), _peripherals(peripherals) {}
void begin() {
setupEventBusSubscriptions();
body_state.updateFeet(kinematics.default_feet_positions);
socket.onEvent(INPUT_EVENT, [&](JsonVariant &root, int originId) { handleInput(root, originId); });
socket.onEvent(MODE_EVENT, [&](JsonVariant &root, int originId) { handleMode(root, originId); });
socket.onEvent(WALK_GAIT_EVENT, [&](JsonVariant &root, int originId) { handleWalkGait(root, originId); });
socket.onEvent(ANGLES_EVENT, [&](JsonVariant &root, int originId) { anglesEvent(root, originId); });
socket.onSubscribe(ANGLES_EVENT,
std::bind(&MotionService::syncAngles, this, std::placeholders::_1, std::placeholders::_2));
body_state.updateFeet(KinConfig::default_feet_positions);
skillManager.setWalkState(&walkState);
}
void anglesEvent(const MotionAnglesMsg& msg) {
void anglesEvent(JsonVariant &root, int originId) {
JsonArray array = root.as<JsonArray>();
for (int i = 0; i < 12; i++) {
angles[i] = msg.angles[i];
angles[i] = array[i];
}
syncAngles();
syncAngles(String(originId));
}
void positionEvent(const MotionPositionMsg& msg) {
body_state.omega = msg.omega;
body_state.phi = msg.phi;
body_state.psi = msg.psi;
body_state.xm = msg.xm;
body_state.ym = msg.ym;
body_state.zm = msg.zm;
void setState(MotionState *newState) {
_servoController->activate();
if (state) {
state->end();
}
state = newState;
if (state) state->begin();
}
void handleInput(const MotionInputMsg& msg) {
command.lx = msg.lx;
command.ly = msg.ly;
command.rx = msg.rx;
command.ry = msg.ry;
command.h = msg.h;
command.s = msg.s;
command.s1 = msg.s1;
void handleInput(JsonVariant &root, int originId) {
command.fromJson(root);
if (state) state->handleCommand(command);
}
body_state.ym = (command.h + 127.f) * 0.35f / 100;
void handleWalkGait(JsonVariant &root, int originId) {
ESP_LOGI("MotionService", "Walk Gait %d", root.as<int>());
switch (motionState) {
case MOTION_STATE::STAND: {
body_state.phi = command.rx / 8;
body_state.psi = command.ry / 8;
body_state.xm = command.ly / 2 / 100;
body_state.zm = command.lx / 2 / 100;
body_state.updateFeet(kinematics.default_feet_positions);
WALK_GAIT walkGait = static_cast<WALK_GAIT>(root.as<int>());
if (walkGait == WALK_GAIT::TROT)
walkState.set_mode_trot();
else
walkState.set_mode_crawl();
}
void handleMode(JsonVariant &root, int originId) {
MOTION_STATE mode = static_cast<MOTION_STATE>(root.as<int>());
ESP_LOGV("MotionService", "Mode %d", mode);
switch (mode) {
case MOTION_STATE::REST: setState(&restState); break;
case MOTION_STATE::STAND: setState(&standState); break;
case MOTION_STATE::WALK: setState(&walkState); break;
case MOTION_STATE::DEACTIVATED:
setState(nullptr);
_servoController->deactivate();
break;
default: setState(nullptr); break;
}
}
JsonDocument doc;
doc.set(static_cast<int>(mode));
JsonVariant data = doc.as<JsonVariant>();
socket.emit(MODE_EVENT, data, String(originId).c_str());
}
void handleMode(const MotionModeMsg& msg) {
motionState = (MOTION_STATE)msg.mode;
ESP_LOGV("MotionService", "Mode %d", motionState);
motionState == MOTION_STATE::DEACTIVATED ? _servoController->deactivate() : _servoController->activate();
MotionModeMsg response;
response.mode = msg.mode;
EventBus<MotionModeMsg>::publishAsync(response, _modeHandle);
void emitAngles(const String &originId = "", bool sync = false) {
JsonDocument doc;
auto arr = doc.to<JsonArray>();
for (int i = 0; i < 12; i++) arr.add(angles[i]);
JsonVariant data = doc.as<JsonVariant>();
socket.emit(ANGLES_EVENT, data, originId.c_str());
}
void emitAngles() {
MotionAnglesMsg anglesMsg;
for (int i = 0; i < 12; i++) {
anglesMsg.angles[i] = angles[i];
}
EventBus<MotionAnglesMsg>::publishAsync(anglesMsg, _anglesHandle);
}
void syncAngles() {
emitAngles();
void syncAngles(const String &originId = "", bool sync = false) {
emitAngles(originId, sync);
_servoController->setAngles(angles);
}
bool updateMotion() {
switch (motionState) {
case MOTION_STATE::DEACTIVATED: return false;
case MOTION_STATE::IDLE: return false;
case MOTION_STATE::CALIBRATION: update_angles(calibration_angles, new_angles, false); break;
case MOTION_STATE::REST: update_angles(rest_angles, new_angles, false); break;
case MOTION_STATE::STAND: kinematics.calculate_inverse_kinematics(body_state, new_angles); break;
case MOTION_STATE::CRAWL:
crawlGait->step(body_state, command);
kinematics.calculate_inverse_kinematics(body_state, new_angles);
break;
case MOTION_STATE::WALK:
walkGait->step(body_state, command);
kinematics.calculate_inverse_kinematics(body_state, new_angles);
break;
void handleGestures() {
const gesture_t ges = _peripherals->takeGesture();
if (ges != gesture_t::eGestureNone) {
ESP_LOGI("Motion", "Gesture: %d", ges);
if (ges == gesture_t::eGestureUp) {
skillManager.clearQueue();
if (!state) {
_servoController->activate();
setState(&restState);
} else if (state == &restState)
setState(&standState);
else if (state == &standState)
setState(&walkState);
} else if (ges == gesture_t::eGestureDown) {
skillManager.clearQueue();
if (state == &restState) {
_servoController->deactivate();
setState(nullptr);
} else if (state == &standState)
setState(&restState);
else if (state == &walkState)
setState(&standState);
} else {
skillManager.queueGestureSkill(ges);
}
}
}
bool updateMotion() {
handleGestures();
unsigned long now = millis();
float dt = (now - lastUpdate) / 1000.0f;
lastUpdate = now;
skillManager.update(body_state, state, _peripherals, dt);
if (skillManager.hasActiveSkill()) {
MotionState *requiredState = skillManager.getCurrentSkillRequiredState();
if (requiredState && state != requiredState) {
setState(requiredState);
}
}
if (!state) return false;
state->updateImuOffsets(_peripherals->angleY(), _peripherals->angleX());
state->step(body_state, dt);
kinematics.calculate_inverse_kinematics(body_state, new_angles);
return update_angles(new_angles, angles);
}
bool update_angles(float new_angles[12], float angles[12], bool useLerp = true) {
bool update_angles(float new_angles[12], float angles[12]) {
bool updated = false;
for (int i = 0; i < 12; i++) {
float new_angle = useLerp ? lerp(angles[i], new_angles[i] * dir[i], 0.3) : new_angles[i] * dir[i];
const float new_angle = new_angles[i] * dir[i];
if (!isEqual(new_angle, angles[i], 0.1)) {
angles[i] = new_angle;
updated = true;
@@ -123,56 +174,33 @@ class MotionService {
return updated;
}
float* getAngles() { return angles; }
float *getAngles() { return angles; }
private:
void setupEventBusSubscriptions() {
_inputHandle = EventBus<MotionInputMsg>::subscribe([this](const MotionInputMsg* msg, size_t n) {
if (n > 0) handleInput(msg[0]);
});
_modeHandle = EventBus<MotionModeMsg>::subscribe([this](const MotionModeMsg* msg, size_t n) {
if (n > 0) handleMode(msg[0]);
});
_anglesHandle = EventBus<MotionAnglesMsg>::subscribe([this](const MotionAnglesMsg* msg, size_t n) {
if (n > 0) anglesEvent(msg[0]);
});
_positionHandle = EventBus<MotionPositionMsg>::subscribe([this](const MotionPositionMsg* msg, size_t n) {
if (n > 0) positionEvent(msg[0]);
});
}
EventBus<MotionInputMsg>::Handle _inputHandle;
EventBus<MotionModeMsg>::Handle _modeHandle;
EventBus<MotionAnglesMsg>::Handle _anglesHandle;
EventBus<MotionPositionMsg>::Handle _positionHandle;
ServoController* _servoController;
ServoController *_servoController;
Peripherals *_peripherals;
Kinematics kinematics;
ControllerCommand command = {0, 0, 0, 0, 0, 0, 0, 0};
friend class GaitState;
CommandMsg command = {0, 0, 0, 0, 0, 0, 0};
std::unique_ptr<GaitState> crawlGait = std::make_unique<EightPhaseWalkState>();
std::unique_ptr<GaitState> walkGait = std::make_unique<BezierState>();
friend class MotionState;
MOTION_STATE motionState = MOTION_STATE::DEACTIVATED;
unsigned long _lastUpdate;
MotionState *state = nullptr;
RestState restState;
StandState standState;
WalkState walkState;
SkillManager skillManager;
body_state_t body_state;
body_state_t body_state = {0, 0, 0, 0, 0, 0};
float new_angles[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
float angles[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
float dir[12] = {1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1};
#if defined(SPOTMICRO_ESP32)
float rest_angles[12] = {0, 90, -145, 0, 90, -145, 0, 90, -145, 0, 90, -145};
float calibration_angles[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
#elif defined(SPOTMICRO_YERTLE)
float rest_angles[12] = {0, 45, -45, 0, 45, -45, 0, 45, -45, 0, 45, -45};
float calibration_angles[12] = {0, 90, 0, 0, 90, 0, 0, 90, 0, 0, 90, 0};
#endif
unsigned long lastUpdate = millis();
};
#endif
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <kinematics.h>
#include <message_types.h>
#include <peripherals/peripherals.h>
#include <motion_states/state.h>
class Skill {
public:
virtual ~Skill() = default;
virtual const char* getName() const = 0;
virtual void begin(body_state_t& body_state, Peripherals* peripherals) {}
virtual void execute(body_state_t& body_state, MotionState* currentState, Peripherals* peripherals, float dt) = 0;
virtual bool isComplete() const = 0;
virtual void reset() = 0;
virtual MotionState* getRequiredState() = 0;
protected:
bool _isActive = false;
bool _isComplete = false;
};
+123
View File
@@ -0,0 +1,123 @@
#pragma once
#include <motion_skills/skill.h>
#include <motion_skills/spin_skill.h>
#include <motion_skills/walk_skill.h>
#include <motion_states/walk_state.h>
#include <peripherals/gesture.h>
#include <esp_log.h>
#include <queue>
#include <memory>
class SkillManager {
private:
std::queue<std::unique_ptr<Skill>> _skillQueue;
std::unique_ptr<Skill> _currentSkill;
WalkState* _walkState = nullptr;
public:
SkillManager() = default;
void setWalkState(WalkState* walkState) { _walkState = walkState; }
void queueSkill(std::unique_ptr<Skill> skill) {
_skillQueue.push(std::move(skill));
ESP_LOGI("SkillManager", "Queued skill. Queue size: %d", _skillQueue.size());
}
void queueGestureSkill(gesture_t gesture) {
std::unique_ptr<Skill> skill = nullptr;
switch (gesture) {
case gesture_t::eGestureLeft:
// Walk 1m left (90 degrees heading)
skill = std::make_unique<WalkSkill>(1.0f, 90.0f, 0.0f);
static_cast<WalkSkill*>(skill.get())->setWalkState(_walkState);
break;
case gesture_t::eGestureRight:
// Walk 1m right (-90 degrees heading)
skill = std::make_unique<WalkSkill>(1.0f, -90.0f, 0.0f);
static_cast<WalkSkill*>(skill.get())->setWalkState(_walkState);
break;
case gesture_t::eGestureUp:
// Walk 1m forward (0 degrees heading)
skill = std::make_unique<WalkSkill>(1.0f, 0.0f, 0.0f);
static_cast<WalkSkill*>(skill.get())->setWalkState(_walkState);
break;
case gesture_t::eGestureDown:
// Walk 1m backward (180 degrees heading)
skill = std::make_unique<WalkSkill>(-1.0f, 0.0f, 0.0f);
static_cast<WalkSkill*>(skill.get())->setWalkState(_walkState);
break;
case gesture_t::eGestureClockwise:
// Rotate 90 degrees clockwise
skill = std::make_unique<WalkSkill>(0.0f, 0.0f, 90.0f);
static_cast<WalkSkill*>(skill.get())->setWalkState(_walkState);
break;
case gesture_t::eGestureAntiClockwise:
// Rotate 90 degrees counter-clockwise
skill = std::make_unique<WalkSkill>(0.0f, 0.0f, -90.0f);
static_cast<WalkSkill*>(skill.get())->setWalkState(_walkState);
break;
default: return; // No skill mapped to this gesture
}
if (skill) {
ESP_LOGI("SkillManager", "Mapping gesture %d to skill: %s", gesture, skill->getName());
queueSkill(std::move(skill));
}
}
void update(body_state_t& body_state, MotionState* currentState, Peripherals* peripherals, float dt) {
// Check if current skill is complete
if (_currentSkill && _currentSkill->isComplete()) {
ESP_LOGI("SkillManager", "Skill '%s' completed", _currentSkill->getName());
_currentSkill.reset();
}
// Start next skill if no current skill and queue has skills
if (!_currentSkill && !_skillQueue.empty()) {
_currentSkill = std::move(_skillQueue.front());
_skillQueue.pop();
_currentSkill->begin(body_state, peripherals);
ESP_LOGI("SkillManager", "Started skill: %s", _currentSkill->getName());
}
// Execute current skill
if (_currentSkill && !_currentSkill->isComplete()) {
_currentSkill->execute(body_state, currentState, peripherals, dt);
}
}
bool hasActiveSkill() const { return _currentSkill && !_currentSkill->isComplete(); }
bool hasQueuedSkills() const { return !_skillQueue.empty(); }
void clearQueue() {
while (!_skillQueue.empty()) {
_skillQueue.pop();
}
if (_currentSkill) {
_currentSkill->reset();
_currentSkill.reset();
}
ESP_LOGI("SkillManager", "Cleared all skills");
}
const char* getCurrentSkillName() const { return _currentSkill ? _currentSkill->getName() : "None"; }
MotionState* getCurrentSkillRequiredState() const {
return _currentSkill ? _currentSkill->getRequiredState() : nullptr;
}
void logStatus() const {
ESP_LOGI("SkillManager", "Status: active=%s, queued=%d, current=%s", hasActiveSkill() ? "yes" : "no",
_skillQueue.size(), getCurrentSkillName());
}
};
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <motion_skills/walk_skill.h>
class SpinAroundSkill : public WalkSkill {
private:
bool _clockwise = true;
public:
SpinAroundSkill(bool clockwise = true) : WalkSkill(0.0f, 0.0f, clockwise ? 90.0f : -90.0f), _clockwise(clockwise) {}
const char* getName() const override { return _clockwise ? "Spin Clockwise" : "Spin Counter-Clockwise"; }
};

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