Compare commits
73 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1aed91d9 | |||
| 923ea17702 | |||
| 3a401abfab | |||
| 26c36b8302 | |||
| bfc259e660 | |||
| 6368bf9213 | |||
| cd802f1c22 | |||
| 59bb1d9579 | |||
| ae98ba76f7 | |||
| bd8c8fd988 | |||
| 7de5a1aa7c | |||
| a3e4fdd8a5 | |||
| f82fa051f2 | |||
| b66ddc3e81 | |||
| c85ac41ebc | |||
| 78d01533f4 | |||
| 18d4d66758 | |||
| 1b9dc9bb9e | |||
| 767d1157df | |||
| 1799889712 | |||
| 0b5d7b1534 | |||
| 10b78e6919 | |||
| 3fd72d081e | |||
| 1f3a465d3e | |||
| cddb6023e7 | |||
| 2f46484e0a | |||
| 4fcaf5d77d | |||
| ea8ddb43ef | |||
| 774c546487 | |||
| 6f46c1f598 | |||
| bc810ee2dd | |||
| 54a0419770 | |||
| d7a6bffe0a | |||
| df087decdb | |||
| 527764b0b5 | |||
| 8c97c68d11 | |||
| e5bf10cdb0 | |||
| de3912ff10 | |||
| 251a791876 | |||
| e36365ead6 | |||
| cb5c095888 | |||
| 281fa32c89 | |||
| d899701195 | |||
| 7061166fcd | |||
| 36b39d41ba | |||
| 7d0a7861ea | |||
| bf8c9bce95 | |||
| 9c984d3215 | |||
| 43e76770a8 | |||
| 6e10eabd9f | |||
| 922a4e3665 | |||
| 5e162ffb71 | |||
| f21ce92d43 | |||
| 98f3fc674b | |||
| c5901c65b3 | |||
| 2eab893dd7 | |||
| a3be035f98 | |||
| 743aa073b7 | |||
| a3de13c619 | |||
| 90be771211 | |||
| 7d79ec39ab | |||
| 211ff7205b | |||
| d0aa3b7b42 | |||
| d529eaa201 | |||
| c8ee64d7f4 | |||
| ec4c3fd98e | |||
| 0cc372cd36 | |||
| 9be405a89d | |||
| e3cfe89e19 | |||
| 144b99c180 | |||
| c788e118e3 | |||
| aae16335b3 | |||
| a43c250ed1 |
@@ -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
|
||||
@@ -2,7 +2,6 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
Vendored
+4
-4
@@ -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"
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode",
|
||||
"platformio.platformio-ide",
|
||||
"svelte.svelte-vscode"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
PUBLIC_VITE_USE_HOST_NAME=true
|
||||
PUBLIC_USE_JSON=true
|
||||
PUBLIC_USE_MSGPACK=true
|
||||
@@ -3,7 +3,6 @@ node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
+2
-4
@@ -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"
|
||||
}
|
||||
|
||||
Generated
+50
-92
@@ -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
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Check } from './icons';
|
||||
import { exitBeforeEnter } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Check } from './icons';
|
||||
import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onDismiss: any;
|
||||
dismiss?: any;
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
dismiss = { label: 'Dismiss', icon: Check }
|
||||
}: Props = $props();
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
labels = {
|
||||
dismiss: { label: 'Dismiss', icon: Check },
|
||||
},
|
||||
}: ModalProps = $props();
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onDismiss}
|
||||
>
|
||||
<dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onDismiss}>
|
||||
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,37 +1,40 @@
|
||||
<script lang="ts">
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import {
|
||||
WidgetComponents,
|
||||
type WidgetContainerConfig,
|
||||
isWidgetConfig,
|
||||
} from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
|
||||
interface Props {
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
interface Props {
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
|
||||
let { container }: Props = $props();
|
||||
let { container }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}
|
||||
>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,111 +1,109 @@
|
||||
<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;
|
||||
interface Props {
|
||||
update?: boolean
|
||||
}
|
||||
|
||||
let { update = $bindable(false) }: Props = $props()
|
||||
|
||||
let firmwareVersion: string = $state('')
|
||||
let firmwareDownloadLink: string = $state('')
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
)
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository')
|
||||
return
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
|
||||
let { update = $bindable(false) }: Props = $props();
|
||||
const results = result.inner
|
||||
update = false
|
||||
firmwareVersion = ''
|
||||
|
||||
let firmwareVersion: string = $state('');
|
||||
let firmwareDownloadLink: string = $state('');
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
};
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
);
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository');
|
||||
return;
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
|
||||
const results = result.inner;
|
||||
update = false;
|
||||
firmwareVersion = '';
|
||||
|
||||
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
update = true;
|
||||
firmwareVersion = results.tag_name;
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||
notifications.info('Firmware update available.', 5000);
|
||||
}
|
||||
}
|
||||
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target as string)
|
||||
) {
|
||||
update = true
|
||||
firmwareVersion = results.tag_name
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url
|
||||
notifications.info('Firmware update available.', 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url })
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI();
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
|
||||
}
|
||||
});
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
});
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI()
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000) // once per hour
|
||||
}
|
||||
})
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url)
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1">
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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();
|
||||
setTimeout(() => {
|
||||
notifications.update((state) => {
|
||||
return state.filter((n) => n.id !== id);
|
||||
});
|
||||
}, timeout);
|
||||
notifications.update((state) => {
|
||||
return [...state, { id, type, message }];
|
||||
});
|
||||
}
|
||||
function send(message: string, type: StateType = 'info', timeout: number) {
|
||||
const id = generateId()
|
||||
setTimeout(() => {
|
||||
notifications.update(state => {
|
||||
return state.filter(n => n.id !== id)
|
||||
})
|
||||
}, timeout)
|
||||
notifications.update(state => {
|
||||
return [...state, { id, type, message }]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
|
||||
}
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9);
|
||||
return '_' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore();
|
||||
export const notifications = createNotificationStore()
|
||||
|
||||
@@ -1,103 +1,101 @@
|
||||
<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 chart: Chart;
|
||||
let chartElement: HTMLCanvasElement;
|
||||
let chart: Chart;
|
||||
|
||||
interface Props {
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
interface Props {
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
|
||||
let { label, data, title }: Props = $props();
|
||||
let { label, data, title }: Props = $props();
|
||||
|
||||
Chart.register(...registerables);
|
||||
Chart.register(...registerables);
|
||||
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
}, 500);
|
||||
})
|
||||
setInterval(() => {
|
||||
chart.data.labels = data;
|
||||
chart.data.datasets[0].data = data;
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class="w-full h-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface Props {
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change?: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:value={selectedOption}
|
||||
{...rest}
|
||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
bind:value={selectedOption}
|
||||
{...rest}
|
||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}">
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
+161
-183
@@ -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
@@ -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]
|
||||
}
|
||||
|
||||
|
||||
+349
-348
@@ -1,379 +1,380 @@
|
||||
import {
|
||||
Mesh,
|
||||
PerspectiveCamera,
|
||||
PlaneGeometry,
|
||||
Scene,
|
||||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
DirectionalLight,
|
||||
PCFSoftShadowMap,
|
||||
type GridHelper,
|
||||
ArrowHelper,
|
||||
Vector3,
|
||||
FogExp2,
|
||||
CanvasTexture,
|
||||
type ColorRepresentation,
|
||||
type WebGLRendererParameters,
|
||||
MeshPhongMaterial,
|
||||
EquirectangularReflectionMapping,
|
||||
ACESFilmicToneMapping,
|
||||
MathUtils,
|
||||
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';
|
||||
Mesh,
|
||||
PerspectiveCamera,
|
||||
PlaneGeometry,
|
||||
Scene,
|
||||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
DirectionalLight,
|
||||
PCFSoftShadowMap,
|
||||
type GridHelper,
|
||||
ArrowHelper,
|
||||
Vector3,
|
||||
FogExp2,
|
||||
CanvasTexture,
|
||||
type ColorRepresentation,
|
||||
type WebGLRendererParameters,
|
||||
MeshPhongMaterial,
|
||||
EquirectangularReflectionMapping,
|
||||
ACESFilmicToneMapping,
|
||||
MathUtils,
|
||||
Group,
|
||||
MeshBasicMaterial,
|
||||
RepeatWrapping
|
||||
} from 'three'
|
||||
import { Sky } from 'three/addons/objects/Sky.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
|
||||
import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
|
||||
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'
|
||||
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
|
||||
import { sunCalculator } from './utilities/position-utilities'
|
||||
|
||||
export const addScene = () => new Scene();
|
||||
export const addScene = () => new Scene()
|
||||
|
||||
interface position {
|
||||
x?: number;
|
||||
y?: number;
|
||||
z?: number;
|
||||
x?: number
|
||||
y?: number
|
||||
z?: number
|
||||
}
|
||||
|
||||
interface light {
|
||||
color?: ColorRepresentation;
|
||||
intensity?: number;
|
||||
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();
|
||||
if (this.scene.environment?.mapping) {
|
||||
this.scene.environment.mapping = EquirectangularReflectionMapping;
|
||||
}
|
||||
return this;
|
||||
constructor() {
|
||||
this.scene = new Scene()
|
||||
if (this.scene.environment?.mapping) {
|
||||
this.scene.environment.mapping = EquirectangularReflectionMapping
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public addRenderer = (parameters?: WebGLRendererParameters) => {
|
||||
this.renderer = new WebGLRenderer(parameters)
|
||||
this.renderer.outputColorSpace = 'srgb'
|
||||
this.renderer.shadowMap.enabled = true
|
||||
this.renderer.shadowMap.type = PCFSoftShadowMap
|
||||
this.renderer.toneMapping = ACESFilmicToneMapping
|
||||
this.renderer.toneMappingExposure = 0.85
|
||||
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)
|
||||
const effectController = {
|
||||
turbidity: 10,
|
||||
rayleigh: 3,
|
||||
mieCoefficient: 0.005,
|
||||
mieDirectionalG: 0.7,
|
||||
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()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
public addGroundPlane = (options?: position) => {
|
||||
const checkerboardTexture = this.createCheckerboardTexture(1024, 2)
|
||||
checkerboardTexture.wrapS = RepeatWrapping
|
||||
checkerboardTexture.wrapT = RepeatWrapping
|
||||
checkerboardTexture.repeat.set(100, 100)
|
||||
const checkerboardMat = new MeshBasicMaterial({
|
||||
map: checkerboardTexture,
|
||||
opacity: 0.1,
|
||||
transparent: true
|
||||
})
|
||||
|
||||
const plane = new PlaneGeometry(400, 400)
|
||||
|
||||
this.ground = new Mesh(plane, checkerboardMat)
|
||||
this.ground.rotation.x = -Math.PI / 2
|
||||
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0)
|
||||
this.ground.receiveShadow = true
|
||||
this.scene.add(this.ground)
|
||||
|
||||
const mirror = new Reflector(plane, {
|
||||
clipBias: 0.003,
|
||||
textureWidth: window.innerWidth * window.devicePixelRatio,
|
||||
textureHeight: window.innerHeight * window.devicePixelRatio,
|
||||
color: 0x00bfff
|
||||
})
|
||||
mirror.rotateX(-Math.PI / 2)
|
||||
this.scene.add(mirror)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
|
||||
this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
this.orbit.minDistance = minDistance + (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
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
const texture = new CanvasTexture(canvas)
|
||||
texture.wrapS = texture.wrapT = RepeatWrapping
|
||||
texture.anisotropy = 16
|
||||
return texture
|
||||
}
|
||||
|
||||
public addSky = () => {
|
||||
this.sky = new Sky();
|
||||
this.sky.scale.setScalar(450000);
|
||||
this.scene.add(this.sky);
|
||||
const effectController = {
|
||||
turbidity: 10,
|
||||
rayleigh: 3,
|
||||
mieCoefficient: 0.005,
|
||||
mieDirectionalG: 0.7,
|
||||
elevation: sunCalculator.calculateSunElevation(),
|
||||
azimuth: 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();
|
||||
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
|
||||
this.scene.fog = new FogExp2(color, density)
|
||||
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;
|
||||
};
|
||||
|
||||
public addGroundPlane = (options?: position) => {
|
||||
const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
|
||||
checkerboardTexture.wrapS = RepeatWrapping;
|
||||
checkerboardTexture.wrapT = RepeatWrapping;
|
||||
checkerboardTexture.repeat.set(100, 100);
|
||||
const checkerboardMat = new MeshBasicMaterial({
|
||||
map: checkerboardTexture,
|
||||
opacity: 0.1,
|
||||
transparent: true
|
||||
});
|
||||
|
||||
const plane = new PlaneGeometry(400, 400);
|
||||
|
||||
this.ground = new Mesh(plane, checkerboardMat);
|
||||
this.ground.rotation.x = -Math.PI / 2;
|
||||
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0);
|
||||
this.ground.receiveShadow = true;
|
||||
this.scene.add(this.ground);
|
||||
|
||||
const mirror = new Reflector(plane, {
|
||||
clipBias: 0.003,
|
||||
textureWidth: window.innerWidth * window.devicePixelRatio,
|
||||
textureHeight: window.innerHeight * window.devicePixelRatio,
|
||||
color: 0x00bfff
|
||||
});
|
||||
mirror.rotateX(-Math.PI / 2);
|
||||
this.scene.add(mirror);
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
|
||||
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.orbit.minDistance = minDistance;
|
||||
this.orbit.maxDistance = maxDistance;
|
||||
this.orbit.autoRotate = autoRotate;
|
||||
this.orbit.update();
|
||||
return this;
|
||||
};
|
||||
|
||||
public addAmbientLight = (options: light) => {
|
||||
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);
|
||||
|
||||
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 squareSize = size / squares;
|
||||
|
||||
for (let y = 0; y < squares; y++) {
|
||||
for (let x = 0; x < squares; x++) {
|
||||
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
|
||||
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
const texture = new CanvasTexture(canvas);
|
||||
texture.wrapS = texture.wrapT = RepeatWrapping;
|
||||
texture.anisotropy = 16;
|
||||
return texture;
|
||||
};
|
||||
|
||||
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
|
||||
this.scene.fog = new FogExp2(color, density);
|
||||
return this;
|
||||
};
|
||||
|
||||
public fillParent = () => {
|
||||
const parentElement = this.renderer.domElement.parentElement;
|
||||
if (parentElement) {
|
||||
const width = parentElement.clientWidth;
|
||||
const height = parentElement.clientHeight;
|
||||
this.handleResize(width, height);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
|
||||
this.renderer.setSize(width, height);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
return this;
|
||||
};
|
||||
|
||||
public addRenderCb = (callback: Function) => {
|
||||
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;
|
||||
};
|
||||
|
||||
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);
|
||||
public fillParent = () => {
|
||||
const parentElement = this.renderer.domElement.parentElement
|
||||
if (parentElement) {
|
||||
const width = parentElement.clientWidth
|
||||
const height = parentElement.clientHeight
|
||||
this.handleResize(width, height)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
|
||||
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
|
||||
}
|
||||
|
||||
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
|
||||
const traverse = (c: any) => {
|
||||
if (c.type === 'Mesh') {
|
||||
if (revert) {
|
||||
c.material = c.__origMaterial;
|
||||
delete c.__origMaterial;
|
||||
} else {
|
||||
c.__origMaterial = c.material;
|
||||
c.material = material;
|
||||
}
|
||||
}
|
||||
public addRenderCb = (callback: Function) => {
|
||||
this.callback = callback
|
||||
return this
|
||||
}
|
||||
|
||||
if (c === m || !this.isJoint(c)) {
|
||||
for (let i = 0; i < c.children.length; i++) {
|
||||
const child = c.children[i];
|
||||
if (!child.isURDFCollider) {
|
||||
traverse(c.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
traverse(m);
|
||||
};
|
||||
public 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
|
||||
}
|
||||
|
||||
public addTransformControls = (model: any) => {
|
||||
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
|
||||
this.transformControl.addEventListener('dragging-changed', (event: any) => {
|
||||
this.orbit.enabled = !event.value;
|
||||
this.isDragging = !event.value;
|
||||
});
|
||||
this.transformControl.attach(model);
|
||||
this.scene.add(this.transformControl);
|
||||
this.transformControl.setMode('rotate');
|
||||
return this;
|
||||
};
|
||||
public 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
|
||||
}
|
||||
|
||||
public addModel = (model: any) => {
|
||||
this.modelGroup = new Group();
|
||||
this.modelGroup.add(model);
|
||||
this.model = model;
|
||||
this.scene.add(this.modelGroup);
|
||||
return this;
|
||||
};
|
||||
private setJointValue(jointName: string, angle: number) {
|
||||
if (!this.model) return
|
||||
if (!this.model.joints[jointName]) return
|
||||
this.model.joints[jointName].setJointValue(angle)
|
||||
}
|
||||
|
||||
public addDragControl = (updateAngle: any) => {
|
||||
const highlightColor = '#FFFFFF';
|
||||
const highlightMaterial = new MeshPhongMaterial({
|
||||
shininess: 10,
|
||||
color: highlightColor,
|
||||
emissive: highlightColor,
|
||||
emissiveIntensity: 0.9
|
||||
});
|
||||
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
|
||||
|
||||
const dragControls = new PointerURDFDragControls(
|
||||
this.scene,
|
||||
this.camera,
|
||||
this.renderer.domElement
|
||||
);
|
||||
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
|
||||
this.setJointValue(joint.name, angle);
|
||||
updateAngle(joint.name, angle);
|
||||
};
|
||||
dragControls.onDragStart = () => {
|
||||
this.orbit.enabled = false;
|
||||
this.isDragging = true;
|
||||
};
|
||||
dragControls.onDragEnd = () => {
|
||||
this.orbit.enabled = true;
|
||||
this.isDragging = false;
|
||||
};
|
||||
dragControls.onHover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, false, highlightMaterial);
|
||||
dragControls.onUnhover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, true, highlightMaterial);
|
||||
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
|
||||
const traverse = (c: any) => {
|
||||
if (c.type === 'Mesh') {
|
||||
if (revert) {
|
||||
c.material = c.__origMaterial
|
||||
delete c.__origMaterial
|
||||
} else {
|
||||
c.__origMaterial = c.material
|
||||
c.material = material
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
if (c === m || !this.isJoint(c)) {
|
||||
for (let i = 0; i < c.children.length; i++) {
|
||||
const child = c.children[i]
|
||||
if (!child.isURDFCollider) {
|
||||
traverse(c.children[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
traverse(m)
|
||||
}
|
||||
|
||||
public toggleFog = () => {
|
||||
this.scene.fog = this.scene.fog ? null : this.fog;
|
||||
};
|
||||
public addTransformControls = (model: any) => {
|
||||
this.transformControl = new TransformControls(this.camera, this.renderer.domElement)
|
||||
this.transformControl.addEventListener('dragging-changed', (event: any) => {
|
||||
this.orbit.enabled = !event.value
|
||||
this.isDragging = !event.value
|
||||
})
|
||||
this.transformControl.attach(model)
|
||||
this.scene.add(this.transformControl)
|
||||
this.transformControl.setMode('rotate')
|
||||
return this
|
||||
}
|
||||
|
||||
private handleRobotShadow = () => {
|
||||
if (this.isLoaded) return;
|
||||
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10);
|
||||
setTimeout(() => clearInterval(intervalId), 1000);
|
||||
this.isLoaded = true;
|
||||
};
|
||||
public addModel = (model: any) => {
|
||||
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 highlightMaterial = new MeshPhongMaterial({
|
||||
shininess: 10,
|
||||
color: highlightColor,
|
||||
emissive: highlightColor,
|
||||
emissiveIntensity: 0.9
|
||||
})
|
||||
|
||||
const dragControls = new PointerURDFDragControls(
|
||||
this.scene,
|
||||
this.camera,
|
||||
this.renderer.domElement
|
||||
)
|
||||
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
|
||||
this.setJointValue(joint.name, angle)
|
||||
updateAngle(joint.name, angle)
|
||||
}
|
||||
dragControls.onDragStart = () => {
|
||||
this.orbit.enabled = false
|
||||
this.isDragging = true
|
||||
}
|
||||
dragControls.onDragEnd = () => {
|
||||
this.orbit.enabled = true
|
||||
this.isDragging = false
|
||||
}
|
||||
dragControls.onHover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, false, highlightMaterial)
|
||||
dragControls.onUnhover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, true, highlightMaterial)
|
||||
|
||||
this.renderer.domElement.addEventListener(
|
||||
'touchstart',
|
||||
data => dragControls._mouseDown(data.touches[0]),
|
||||
{ passive: true }
|
||||
)
|
||||
this.renderer.domElement.addEventListener(
|
||||
'touchmove',
|
||||
data => dragControls._mouseMove(data.touches[0]),
|
||||
{ passive: true }
|
||||
)
|
||||
this.renderer.domElement.addEventListener(
|
||||
'touchend',
|
||||
data => dragControls._mouseUp(data.touches[0]),
|
||||
{ passive: true }
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
public toggleFog = () => {
|
||||
this.scene.fog = this.scene.fog ? null : this.fog
|
||||
}
|
||||
|
||||
private handleRobotShadow = () => {
|
||||
if (this.isLoaded) return
|
||||
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10)
|
||||
setTimeout(() => clearInterval(intervalId), 1000)
|
||||
this.isLoaded = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>>({});
|
||||
if (!featureFlagsStore) {
|
||||
featureFlagsStore = persistentStore<Record<string, boolean | string>>('FeatureFlags', {})
|
||||
|
||||
api.get<Record<string, boolean>>('/api/features').then((result) => {
|
||||
if (result.isOk()) featureFlagsStore.set(result.inner);
|
||||
else {
|
||||
notifications.error('Feature flag could not be fetched', 2500);
|
||||
}
|
||||
});
|
||||
}
|
||||
api.get<Record<string, boolean>>('/api/features').then(result => {
|
||||
if (result.isOk()) featureFlagsStore.set(result.inner)
|
||||
else {
|
||||
notifications.error('Feature flag could not be fetched', 2500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return featureFlagsStore;
|
||||
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)
|
||||
)
|
||||
|
||||
@@ -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', '');
|
||||
|
||||
@@ -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
|
||||
});
|
||||
left: { x: 0, y: 0 },
|
||||
right: { x: 0, y: 0 },
|
||||
height: 0.5,
|
||||
speed: 0.5,
|
||||
s1: 0.05
|
||||
})
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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})`;
|
||||
};
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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>
|
||||
+100
-96
@@ -1,125 +1,129 @@
|
||||
<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 {
|
||||
telemetry,
|
||||
analytics,
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
outControllerData,
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location,
|
||||
useFeatureFlags
|
||||
} from '$lib/stores';
|
||||
import type { Analytics, DownloadOTA } from '$lib/types/models';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { page } from '$app/state'
|
||||
import { Modals, modals } from 'svelte-modals'
|
||||
import Toast from '$lib/components/toasts/Toast.svelte'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import { fade } from 'svelte/transition'
|
||||
import '../app.css'
|
||||
import Menu from '../lib/components/menu/Menu.svelte'
|
||||
import Statusbar from '../lib/components/statusbar/statusbar.svelte'
|
||||
import {
|
||||
telemetry,
|
||||
analytics,
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
outControllerData,
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location,
|
||||
useFeatureFlags,
|
||||
walkGait
|
||||
} from '$lib/stores'
|
||||
import { type Analytics, type DownloadOTA } from '$lib/types/models'
|
||||
import { MessageTopic } from '$lib/types/models'
|
||||
|
||||
let { children }: Props = $props();
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
const features = useFeatureFlags();
|
||||
let { children }: Props = $props()
|
||||
|
||||
onMount(async () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
const features = useFeatureFlags()
|
||||
|
||||
addEventListeners();
|
||||
onMount(async () => {
|
||||
const ws = $location ? $location : window.location.host
|
||||
socket.init(`ws://${ws}/api/ws/events`)
|
||||
|
||||
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 }));
|
||||
});
|
||||
addEventListeners()
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListeners();
|
||||
});
|
||||
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))
|
||||
})
|
||||
|
||||
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);
|
||||
});
|
||||
features.subscribe(data => {
|
||||
if (data?.download_firmware) socket.on('otastatus', handleOAT);
|
||||
if (data?.sonar) socket.on('sonar', data => console.log(data));
|
||||
});
|
||||
};
|
||||
onDestroy(() => {
|
||||
removeEventListeners()
|
||||
})
|
||||
|
||||
const removeEventListeners = () => {
|
||||
socket.off('analytics', handleAnalytics);
|
||||
socket.off('open', handleOpen);
|
||||
socket.off('close', handleClose);
|
||||
socket.off('rssi', handleNetworkStatus);
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
const addEventListeners = () => {
|
||||
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(MessageTopic.otastatus, handleOAT)
|
||||
if (data?.sonar) socket.on(MessageTopic.sonar, data => console.log(data))
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
const removeEventListeners = () => {
|
||||
socket.off(MessageTopic.analytics, handleAnalytics)
|
||||
socket.off('open', handleOpen)
|
||||
socket.off('close', handleClose)
|
||||
socket.off(MessageTopic.rssi, handleNetworkStatus)
|
||||
socket.off(MessageTopic.otastatus, handleOAT)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000);
|
||||
telemetry.setRSSI(0);
|
||||
};
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000)
|
||||
}
|
||||
|
||||
const handleError = (data: any) => console.error(data);
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000)
|
||||
telemetry.setRSSI(0)
|
||||
}
|
||||
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data);
|
||||
const handleError = (data: any) => console.error(data)
|
||||
|
||||
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data)
|
||||
|
||||
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
|
||||
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data)
|
||||
|
||||
let menuOpen = $state(false);
|
||||
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data)
|
||||
|
||||
let menuOpen = $state(false)
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{page.data.title}</title>
|
||||
<title>{page.data.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
|
||||
<!-- Main page content here -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay"></label>
|
||||
<Menu menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
<!-- Main page content here -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay"></label>
|
||||
<Menu menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modals>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
{#snippet backdrop()}
|
||||
<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>
|
||||
{/snippet}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
{#snippet backdrop()}
|
||||
<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>
|
||||
{/snippet}
|
||||
</Modals>
|
||||
|
||||
<Toast />
|
||||
|
||||
+18
-18
@@ -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;
|
||||
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 { fetch: originalFetch } = window
|
||||
const fileService = (await import('$lib/services/file-service')).default
|
||||
window.fetch = async (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();
|
||||
return {
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2024 Rune Harlyk'
|
||||
};
|
||||
};
|
||||
await registerFetchIntercept()
|
||||
return {
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2025 Rune Harlyk'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket, useFeatureFlags } from '$lib/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket } from '$lib/stores';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Connection</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Connection</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<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);
|
||||
if (data.heading)
|
||||
mpu.update(mpuData => {
|
||||
mpuData.heading = data.heading;
|
||||
console.log(data.heading);
|
||||
onMount(() => {
|
||||
socket.on(MessageTopic.imu, (data: IMU) => {
|
||||
imu.addData(data)
|
||||
if (data.heading)
|
||||
mpu.update(mpuData => {
|
||||
mpuData.heading = data.heading
|
||||
console.log(data.heading)
|
||||
|
||||
return mpuData;
|
||||
});
|
||||
});
|
||||
});
|
||||
return mpuData
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 select-none w-screen h-screen">
|
||||
<Controls />
|
||||
<div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
|
||||
<WidgetContainer container={layout.content} />
|
||||
</div>
|
||||
<Controls />
|
||||
<div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
|
||||
<WidgetContainer container={layout.content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,15 +1,7 @@
|
||||
<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('/');
|
||||
}
|
||||
import SystemMetrics from './SystemMetrics.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<SystemMetrics />
|
||||
<SystemMetrics />
|
||||
</div>
|
||||
|
||||
@@ -1,373 +1,369 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import { Metrics } from '$lib/components/icons';
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import { Metrics } from '$lib/components/icons';
|
||||
|
||||
Chart.register(...registerables);
|
||||
Chart.register(...registerables);
|
||||
|
||||
let cpuChartElement: HTMLCanvasElement = $state();
|
||||
let cpuChart: Chart;
|
||||
let cpuChartElement: HTMLCanvasElement;
|
||||
let cpuChart: Chart;
|
||||
|
||||
let heapChartElement: HTMLCanvasElement = $state();
|
||||
let heapChart: Chart;
|
||||
let heapChartElement: HTMLCanvasElement;
|
||||
let heapChart: Chart;
|
||||
|
||||
let filesystemChartElement: HTMLCanvasElement = $state();
|
||||
let filesystemChart: Chart;
|
||||
let filesystemChartElement: HTMLCanvasElement;
|
||||
let filesystemChart: Chart;
|
||||
|
||||
let temperatureChartElement: HTMLCanvasElement = $state();
|
||||
let temperatureChart: Chart;
|
||||
let temperatureChartElement: HTMLCanvasElement;
|
||||
let temperatureChart: Chart;
|
||||
|
||||
onMount(() => {
|
||||
cpuChart = new Chart(cpuChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.cpu_usage,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cpu usage core 0',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu0_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage core 1',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu1_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage total',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cpu usage [%]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Used Heap',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.used_heap,
|
||||
fill:true,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
fill:true,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setInterval(updateData, 500);
|
||||
});
|
||||
onMount(() => {
|
||||
cpuChart = new Chart(cpuChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.cpu_usage,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cpu usage core 0',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu0_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage core 1',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu1_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage total',
|
||||
borderColor: daisyColor('--color-secondary'),
|
||||
backgroundColor: daisyColor('--color-secondary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cpu usage [%]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Used Heap',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.used_heap,
|
||||
fill: true,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
fill: true,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setInterval(updateData, 500);
|
||||
});
|
||||
|
||||
function updateData() {
|
||||
cpuChart.data.labels = $analytics.cpu_usage;
|
||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||
cpuChart.update('none');
|
||||
function updateData() {
|
||||
cpuChart.data.labels = $analytics.cpu_usage;
|
||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||
cpuChart.update('none');
|
||||
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||
heapChart.update('none');
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||
heapChart.update('none');
|
||||
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||
filesystemChart.update('none');
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||
filesystemChart.update('none');
|
||||
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >System Metrics</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<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 }}
|
||||
>
|
||||
<canvas bind:this={cpuChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={cpuChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={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 }}
|
||||
>
|
||||
<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 }}
|
||||
>
|
||||
<canvas bind:this={temperatureChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={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 }}>
|
||||
<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 }}>
|
||||
<canvas bind:this={temperatureChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -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,89 +149,92 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getSystemStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={CPU}
|
||||
title="Chip"
|
||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||
{:then}
|
||||
{#if systemInformation}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={CPU}
|
||||
title="Chip"
|
||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={SDK}
|
||||
title="SDK Version"
|
||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||
<StatusItem
|
||||
icon={SDK}
|
||||
title="SDK Version"
|
||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={CPP}
|
||||
title="Firmware Version"
|
||||
description={systemInformation.firmware_version} />
|
||||
<StatusItem
|
||||
icon={CPP}
|
||||
title="Firmware Version"
|
||||
description={systemInformation.firmware_version} />
|
||||
|
||||
<StatusItem
|
||||
icon={Speed}
|
||||
title="CPU Frequency"
|
||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||
}`} />
|
||||
<StatusItem
|
||||
icon={Speed}
|
||||
title="CPU Frequency"
|
||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||
}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Heap}
|
||||
title="Heap (Free / Max Alloc)"
|
||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||
<StatusItem
|
||||
icon={Heap}
|
||||
title="Heap (Free / Max Alloc)"
|
||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Pyramid}
|
||||
title="PSRAM (Size / Free)"
|
||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||
<StatusItem
|
||||
icon={Pyramid}
|
||||
title="PSRAM (Size / Free)"
|
||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Sketch}
|
||||
title="Sketch (Used / Free)"
|
||||
description={`${(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
<StatusItem
|
||||
icon={Sketch}
|
||||
title="Sketch (Used / Free)"
|
||||
description={`${(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
${systemInformation.free_sketch_space / 1000000} MB used (${
|
||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000
|
||||
} MB free)`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Flash}
|
||||
title="Flash Chip (Size / Speed)"
|
||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
} MHz`} />
|
||||
<StatusItem
|
||||
icon={Flash}
|
||||
title="Flash Chip (Size / Speed)"
|
||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
} MHz`} />
|
||||
|
||||
<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 (${
|
||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||
}
|
||||
<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 (${
|
||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||
}
|
||||
MB free)`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Temperature}
|
||||
title="Core Temperature"
|
||||
description={`${
|
||||
systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||
}`} />
|
||||
<StatusItem
|
||||
icon={Temperature}
|
||||
title="Core Temperature"
|
||||
description={`${
|
||||
systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||
}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Stopwatch}
|
||||
title="Uptime"
|
||||
description={convertSeconds(systemInformation.uptime)} />
|
||||
<StatusItem
|
||||
icon={Stopwatch}
|
||||
title="Uptime"
|
||||
description={convertSeconds(systemInformation.uptime)} />
|
||||
|
||||
<StatusItem
|
||||
icon={Power}
|
||||
title="Reset Reason"
|
||||
description={systemInformation.cpu_reset_reason} />
|
||||
</div>
|
||||
<StatusItem
|
||||
icon={Power}
|
||||
title="Reset Reason"
|
||||
description={systemInformation.cpu_reset_reason} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,165 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
};
|
||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||
headers
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
};
|
||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||
headers,
|
||||
});
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
return result.inner as any;
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(assets: any) {
|
||||
let url = '';
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
assets[i].name.includes('.bin') &&
|
||||
assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
url = assets[i].browser_download_url;
|
||||
}
|
||||
}
|
||||
if (url === '') {
|
||||
// if no asset was found, use the first one
|
||||
modals.open(InfoDialog, {
|
||||
title: 'No matching firmware found',
|
||||
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(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown },
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll(),
|
||||
});
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
return result.inner as any;
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(assets: any) {
|
||||
let url = '';
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
assets[i].name.includes('.bin') &&
|
||||
assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
url = assets[i].browser_download_url;
|
||||
}
|
||||
}
|
||||
if (url === '') {
|
||||
// if no asset was found, use the first one
|
||||
modals.open(InfoDialog, {
|
||||
title: 'No matching firmware found',
|
||||
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()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Github Firmware Manager</span>
|
||||
{/snippet}
|
||||
{#await getGithubAPI()}
|
||||
<Spinner />
|
||||
{:then githubReleases}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<div
|
||||
class="overflow-x-auto"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<table class="table w-full table-auto">
|
||||
<thead>
|
||||
<tr class="font-bold">
|
||||
<th align="left">Release</th>
|
||||
<th align="center" class="hidden sm:block">Release Date</th>
|
||||
<th align="center">Experimental</th>
|
||||
<th align="center">Install</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each githubReleases as release}
|
||||
<tr
|
||||
class={(
|
||||
compareVersions(
|
||||
$features.firmware_version,
|
||||
release.tag_name
|
||||
) === 0
|
||||
) ?
|
||||
'bg-primary text-primary-content'
|
||||
: '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 align="center" class="hidden min-h-full align-middle sm:block">
|
||||
<div class="my-2">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium'
|
||||
}).format(new Date(release.published_at))}
|
||||
</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if release.prerelease}
|
||||
<Prerelease class="text-accent h-5 w-5" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if compareVersions($features.firmware_version, 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}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{: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
|
||||
>
|
||||
</div>
|
||||
{/await}
|
||||
{#snippet icon()}
|
||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Github Firmware Manager</span>
|
||||
{/snippet}
|
||||
{#await getGithubAPI()}
|
||||
<Spinner />
|
||||
{:then githubReleases}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<table class="table w-full table-auto">
|
||||
<thead>
|
||||
<tr class="font-bold">
|
||||
<th align="left">Release</th>
|
||||
<th align="center" class="hidden sm:block">Release Date</th>
|
||||
<th align="center">Experimental</th>
|
||||
<th align="center">Install</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each githubReleases as release}
|
||||
<tr
|
||||
class={(
|
||||
compareVersions($features.firmware_version as string, release.tag_name) === 0
|
||||
) ?
|
||||
'bg-primary text-primary-content'
|
||||
: '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 align="center" class="hidden min-h-full align-middle sm:block">
|
||||
<div class="my-2">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium',
|
||||
}).format(new Date(release.published_at))}
|
||||
</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if release.prerelease}
|
||||
<Prerelease class="text-accent h-5 w-5" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="center">
|
||||
{#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}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{: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>
|
||||
</div>
|
||||
{/await}
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,57 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
||||
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]);
|
||||
const result = await api.post('/api/firmware', formData);
|
||||
if (result.isErr()) console.error('Error:', result.inner);
|
||||
}
|
||||
async function uploadBIN() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files![0]);
|
||||
const result = await api.post('/api/firmware', formData);
|
||||
if (result.isErr()) console.error('Error:', result.inner);
|
||||
}
|
||||
|
||||
function confirmBinUpload() {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Flashing the Device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Upload', icon: OTA }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
uploadBIN();
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmBinUpload() {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Flashing the Device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Upload', icon: OTA },
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
uploadBIN();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Upload Firmware</span>
|
||||
{/snippet}
|
||||
<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.
|
||||
</span>
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Upload Firmware</span>
|
||||
{/snippet}
|
||||
<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.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="binFile"
|
||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||
bind:files
|
||||
accept=".bin,.md5"
|
||||
onchange={confirmBinUpload}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
id="binFile"
|
||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||
bind:files
|
||||
accept=".bin,.md5"
|
||||
onchange={confirmBinUpload} />
|
||||
</SettingsCard>
|
||||
|
||||
@@ -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,22 +162,24 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getAPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={apStatusVariant[apStatus.status]}
|
||||
description={apStatusDescription[apStatus.status]} />
|
||||
{:then}
|
||||
{#if apStatus}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={apStatusVariant[apStatus.status]}
|
||||
description={apStatusDescription[apStatus.status]} />
|
||||
|
||||
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||
|
||||
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||
|
||||
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
|
||||
</div>
|
||||
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
@@ -190,175 +190,176 @@
|
||||
</div>
|
||||
{#await getAPSettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||
onsubmit={preventDefault(handleSubmitAP)}
|
||||
novalidate
|
||||
bind:this={formField}>
|
||||
<div>
|
||||
<label class="label" for="apmode">
|
||||
<span class="label-text">Provide Access Point ...</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
id="apmode"
|
||||
bind:value={apSettings.provision_mode}>
|
||||
{#each provisionMode as mode}
|
||||
<option value={mode.id}>
|
||||
{mode.text}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
{:then}
|
||||
{#if apSettings}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||
onsubmit={preventDefault(handleSubmitAP)}
|
||||
novalidate
|
||||
bind:this={formField}>
|
||||
<div>
|
||||
<label class="label" for="apmode">
|
||||
<span class="label-text">Provide Access Point ...</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
id="apmode"
|
||||
bind:value={apSettings.provision_mode}>
|
||||
{#each provisionMode as mode}
|
||||
<option value={mode.id}>
|
||||
{mode.text}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Preferred Channel</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="13"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.channel
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.channel}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||
>Must be channel 1 to 13</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Preferred Channel</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="13"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.channel
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.channel}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||
>Must be channel 1 to 13</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text text-md">Max Clients</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="8"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.max_clients
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.max_clients}
|
||||
id="clients"
|
||||
required />
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||
>Maximum 8 clients allowed</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text text-md">Max Clients</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="8"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.max_clients
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.max_clients}
|
||||
id="clients"
|
||||
required />
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||
>Maximum 8 clients allowed</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
|
||||
''
|
||||
)}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.gateway_ip}
|
||||
id="gateway"
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.subnet_mask}
|
||||
id="subnet"
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.gateway_ip}
|
||||
id="gateway"
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.subnet_mask}
|
||||
id="subnet"
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={apSettings.ssid_hidden}
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="">Hide SSID</span>
|
||||
</label>
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={apSettings.ssid_hidden}
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="">Hide SSID</span>
|
||||
</label>
|
||||
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
+115
-131
@@ -1,147 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||
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 { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||
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, type ModalProps } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
storeNetwork: any;
|
||||
let { isOpen, storeNetwork }: ModalProps = $props();
|
||||
|
||||
const encryptionType = [
|
||||
'Open',
|
||||
'WEP',
|
||||
'WPA PSK',
|
||||
'WPA2 PSK',
|
||||
'WPA WPA2 PSK',
|
||||
'WPA2 Enterprise',
|
||||
'WPA3 PSK',
|
||||
'WPA2 WPA3 PSK',
|
||||
'WAPI PSK'
|
||||
]
|
||||
|
||||
let listOfNetworks: NetworkItem[] = $state([])
|
||||
|
||||
let scanActive = $state(false)
|
||||
|
||||
let pollingId: ReturnType<typeof setTimeout> | number
|
||||
|
||||
async function scanNetworks() {
|
||||
scanActive = true
|
||||
await api.get('/api/wifi/scan')
|
||||
if ((await pollingResults()) == false) {
|
||||
pollingId = setInterval(() => pollingResults(), 1000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let { isOpen, storeNetwork }: Props = $props();
|
||||
|
||||
const encryptionType = [
|
||||
'Open',
|
||||
'WEP',
|
||||
'WPA PSK',
|
||||
'WPA2 PSK',
|
||||
'WPA WPA2 PSK',
|
||||
'WPA2 Enterprise',
|
||||
'WPA3 PSK',
|
||||
'WPA2 WPA3 PSK',
|
||||
'WAPI PSK'
|
||||
];
|
||||
|
||||
let listOfNetworks: NetworkItem[] = $state([]);
|
||||
|
||||
let scanActive = $state(false);
|
||||
|
||||
let pollingId: number;
|
||||
|
||||
async function scanNetworks() {
|
||||
scanActive = true;
|
||||
await api.get('/api/wifi/scan');
|
||||
if ((await pollingResults()) == false) {
|
||||
pollingId = setInterval(() => pollingResults(), 1000);
|
||||
}
|
||||
return;
|
||||
async function pollingResults() {
|
||||
const result = await api.get<NetworkList>('/api/wifi/networks')
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner)
|
||||
return false
|
||||
}
|
||||
|
||||
async function pollingResults() {
|
||||
const result = await api.get<NetworkList>('/api/wifi/networks');
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
return false;
|
||||
}
|
||||
let response = result.inner;
|
||||
listOfNetworks = response.networks;
|
||||
scanActive = false;
|
||||
if (listOfNetworks.length) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
return listOfNetworks.length;
|
||||
let response = result.inner
|
||||
listOfNetworks = response.networks
|
||||
scanActive = false
|
||||
if (listOfNetworks.length) {
|
||||
clearInterval(pollingId)
|
||||
pollingId = 0
|
||||
}
|
||||
return listOfNetworks.length
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
scanNetworks();
|
||||
});
|
||||
onMount(() => {
|
||||
scanNetworks()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollingId) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (pollingId) {
|
||||
clearInterval(pollingId)
|
||||
pollingId = 0
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">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"
|
||||
>
|
||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||
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">
|
||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="menu">
|
||||
{#each listOfNetworks as network, i}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<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)
|
||||
}}
|
||||
role="button"
|
||||
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" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{network.ssid}</div>
|
||||
<div class="text-sm opacity-75">
|
||||
Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="menu">
|
||||
{#each listOfNetworks as network, i}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<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);
|
||||
}}
|
||||
role="button"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{network.ssid}</div>
|
||||
<div class="text-sm opacity-75">
|
||||
Security: {encryptionType[network.encryption_type]},
|
||||
Channel: {network.channel}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex flex-none items-center"
|
||||
disabled={scanActive}
|
||||
onclick={scanNetworks}
|
||||
>
|
||||
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex flex-none items-center"
|
||||
disabled={scanActive}
|
||||
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()}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
onclick={() => modals.close()}>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+268
-257
@@ -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,53 +297,55 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getWifiStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
||||
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
||||
|
||||
{#if wifiStatus.status === 3}
|
||||
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
||||
|
||||
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
||||
|
||||
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm modal-button"
|
||||
onclick={() => {
|
||||
showWifiDetails = !showWifiDetails
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
showWifiDetails
|
||||
) ?
|
||||
'rotate-180'
|
||||
: ''}" />
|
||||
</button>
|
||||
</StatusItem>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Folds open -->
|
||||
{#if showWifiDetails}
|
||||
{:then}
|
||||
{#if wifiStatus}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 pt-1"
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
||||
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
||||
|
||||
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
||||
{#if wifiStatus.status === 3}
|
||||
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
||||
|
||||
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
||||
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
||||
|
||||
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
||||
|
||||
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
||||
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm modal-button"
|
||||
onclick={() => {
|
||||
showWifiDetails = !showWifiDetails
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
showWifiDetails
|
||||
) ?
|
||||
'rotate-180'
|
||||
: ''}" />
|
||||
</button>
|
||||
</StatusItem>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Folds open -->
|
||||
{#if showWifiDetails}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 pt-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
||||
|
||||
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
||||
|
||||
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
||||
|
||||
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
||||
|
||||
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
@@ -349,254 +357,257 @@
|
||||
</div>
|
||||
{#await getWifiSettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
addNetwork()
|
||||
showNetworkEditor = true
|
||||
}
|
||||
}}>
|
||||
<Add class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
scanForNetworks()
|
||||
showNetworkEditor = true
|
||||
}
|
||||
}}>
|
||||
<Scan class="h-6 w-6" /></button>
|
||||
{: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"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
addNetwork()
|
||||
showNetworkEditor = true
|
||||
}
|
||||
}}>
|
||||
<Add class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||
onclick={() => {
|
||||
if (checkNetworkList()) {
|
||||
scanForNetworks()
|
||||
showNetworkEditor = true
|
||||
}
|
||||
}}>
|
||||
<Scan class="h-6 w-6" /></button>
|
||||
|
||||
<div
|
||||
class="overflow-x-auto space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<DragDropList
|
||||
id="networks"
|
||||
type={VerticalDropZone}
|
||||
itemSize={60}
|
||||
itemCount={dndNetworkList.length}
|
||||
on:drop={onDrop}>
|
||||
{#snippet children({ index })}
|
||||
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
|
||||
<div class="space-x-0 px-0 mx-0">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
handleEdit(index)
|
||||
}}>
|
||||
<Edit class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
confirmDelete(index)
|
||||
}}>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</StatusItem>
|
||||
{/snippet}
|
||||
</DragDropList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="divider mb-0"></div>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Host Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="1"
|
||||
max="32"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrorhostname
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={wifiSettings.hostname}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
||||
>Host name must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={wifiSettings.priority_RSSI}
|
||||
class="checkbox checkbox-primary sm:-mb-5" />
|
||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||
</label>
|
||||
<div
|
||||
class="overflow-x-auto space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<DragDropList
|
||||
id="networks"
|
||||
type={VerticalDropZone}
|
||||
itemSize={60}
|
||||
itemCount={dndNetworkList.length}
|
||||
on:drop={onDrop}>
|
||||
{#snippet children({ index }: { index: number })}
|
||||
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
|
||||
<div class="space-x-0 px-0 mx-0">
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
handleEdit(index)
|
||||
}}>
|
||||
<Edit class="h-6 w-6" /></button>
|
||||
<button
|
||||
class="btn btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
confirmDelete(index)
|
||||
}}>
|
||||
<Delete class="text-error h-6 w-6" />
|
||||
</button>
|
||||
</div>
|
||||
</StatusItem>
|
||||
{/snippet}
|
||||
</DragDropList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showNetworkEditor}
|
||||
<div class="divider my-0"></div>
|
||||
<div
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<div class="divider mb-0"></div>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Host Name</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="1"
|
||||
max="32"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
formErrorhostname
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={networkEditable.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
bind:value={wifiSettings.hostname}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 3 and 32 characters long</span>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
||||
>Host name must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
||||
</div>
|
||||
<label
|
||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={static_ip_config}
|
||||
bind:checked={wifiSettings.priority_RSSI}
|
||||
class="checkbox checkbox-primary sm:-mb-5" />
|
||||
<span class="sm:-mb-5">Static IP Config?</span>
|
||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||
</label>
|
||||
</div>
|
||||
{#if static_ip_config}
|
||||
|
||||
{#if showNetworkEditor}
|
||||
<div class="divider my-0"></div>
|
||||
<div
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ?
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.local_ip}
|
||||
id="localIP"
|
||||
bind:value={networkEditable.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.gateway_ip}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 3 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.subnet_mask}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">DNS 1</span>
|
||||
</label>
|
||||
<label
|
||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_1}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">DNS 2</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_2}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
type="checkbox"
|
||||
bind:checked={static_ip_config}
|
||||
class="checkbox checkbox-primary sm:-mb-5" />
|
||||
<span class="sm:-mb-5">Static IP Config?</span>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if static_ip_config}
|
||||
<div
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="divider mb-2 mt-0"></div>
|
||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
||||
{newNetwork ? 'Add Network' : 'Update Network'}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" onclick={validateHostName}>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.gateway_ip}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.subnet_mask}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">DNS 1</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_1}
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">DNS 2</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={networkEditable.dns_ip_2}
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
||||
Must be a valid IPv4 address
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="divider mb-2 mt-0"></div>
|
||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
||||
{newNetwork ? 'Add Network' : 'Update Network'}
|
||||
</button>
|
||||
<button class="btn btn-primary" type="button" onclick={validateHostName}>
|
||||
Apply Settings
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
Binary file not shown.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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.
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
+43
-25
@@ -1,31 +1,49 @@
|
||||
import type { UserConfig, Plugin } from 'vite';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export default function viteLittleFS(): Plugin[] {
|
||||
return [
|
||||
{
|
||||
name: 'vite-plugin-littlefs',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
return [
|
||||
{
|
||||
name: 'vite-plugin-littlefs',
|
||||
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
|
||||
config.build.rollupOptions.output = {
|
||||
...config.build?.rollupOptions?.output,
|
||||
assetFileNames: assetFileNames.replace('.[hash]', '')
|
||||
};
|
||||
if (!output || !config.build?.rollupOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Client-build
|
||||
if (config.build?.rollupOptions?.output.chunkFileNames.includes('hash')) {
|
||||
config.build.rollupOptions.output = {
|
||||
...config.build?.rollupOptions?.output,
|
||||
chunkFileNames: chunkFileNames.replace('.[hash]', ''),
|
||||
entryFileNames: entryFileNames.replace('.[hash]', '')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
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 = {
|
||||
...outputOptions,
|
||||
assetFileNames: assetFileNames.replace('.[hash]', ''),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
chunkFileNames &&
|
||||
typeof chunkFileNames === 'string' &&
|
||||
chunkFileNames.includes('hash')
|
||||
) {
|
||||
config.build.rollupOptions.output = {
|
||||
...config.build.rollupOptions.output,
|
||||
chunkFileNames: chunkFileNames.replace('.[hash]', ''),
|
||||
...(entryFileNames &&
|
||||
typeof entryFileNames === 'string' && {
|
||||
entryFileNames: entryFileNames.replace('.[hash]', ''),
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+3
-3
@@ -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': {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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; }
|
||||
};
|
||||
@@ -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
|
||||
@@ -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();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <ArduinoJson.h>
|
||||
#include <event_bus.hpp>
|
||||
#include <event_socket.h>
|
||||
#include <PsychicHttp.h>
|
||||
|
||||
#include <HTTPClient.h>
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <gait/state.h>
|
||||
|
||||
class IdleState : public GaitState {
|
||||
protected:
|
||||
const char *name() const override { return "Idle"; }
|
||||
};
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
+143
-115
@@ -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 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 syncAngles(const String &originId = "", bool sync = false) {
|
||||
emitAngles(originId, sync);
|
||||
_servoController->setAngles(angles);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
MotionAnglesMsg anglesMsg;
|
||||
for (int i = 0; i < 12; i++) {
|
||||
anglesMsg.angles[i] = angles[i];
|
||||
}
|
||||
EventBus<MotionAnglesMsg>::publishAsync(anglesMsg, _anglesHandle);
|
||||
}
|
||||
|
||||
void syncAngles() {
|
||||
emitAngles();
|
||||
_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;
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user