Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c12ef332e | |||
| 481dfaf8e5 | |||
| 6769ffeb20 | |||
| 0586775849 | |||
| 4766f47e7e | |||
| d2d7d8e323 | |||
| c5155fe641 | |||
| f1312fb5c6 | |||
| a592848f34 | |||
| 06b05b2dc1 | |||
| 01d46f283b | |||
| 7c8c5b40a1 | |||
| 632f603fda | |||
| 4101ad033c | |||
| 3ee096bfab | |||
| 753e692fe2 | |||
| 40025a55c3 | |||
| 98262b2efc | |||
| 01e174f337 | |||
| a9fea7fd56 | |||
| e09ec81f1d | |||
| ee17f6862c | |||
| 8be7546eba | |||
| e156b732eb | |||
| 20c5a8ee92 | |||
| dac21a499f | |||
| 9a6c240140 | |||
| 8733ecd9b7 | |||
| fba531d3e8 | |||
| fc04d1b8d6 | |||
| 4c33a75164 | |||
| 6015e67d05 | |||
| f59f32ce26 | |||
| 3671610860 | |||
| c346f7f553 | |||
| f864616303 | |||
| ad2d28c9ba | |||
| 967923321f | |||
| 6b7e3281cf | |||
| fdf70f7eb8 | |||
| e4cb035ad9 | |||
| c02938b567 | |||
| c24740e8ec | |||
| e0d3912d83 | |||
| b113a30942 | |||
| 9534529e50 | |||
| 23a41d26b1 | |||
| 569c19ad1d | |||
| 17e30ebfe9 | |||
| 170e180c11 | |||
| 5a24038d68 | |||
| 99660b9a23 | |||
| 72f3bcfd78 |
@@ -4,18 +4,17 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
paths:
|
paths:
|
||||||
- 'esp32/**'
|
- "esp32/**"
|
||||||
|
- "platformio.ini"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
paths:
|
paths:
|
||||||
- 'esp32/**'
|
- "esp32/**"
|
||||||
|
- "platformio.ini"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./esp32
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -28,8 +27,8 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: "3.x"
|
||||||
- run: pip install -r ./scripts/requirements.txt
|
- run: pip install -r esp32/scripts/requirements.txt
|
||||||
- name: Install PlatformIO Core
|
- name: Install PlatformIO Core
|
||||||
run: pip install --upgrade platformio
|
run: pip install --upgrade platformio
|
||||||
|
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
.pio
|
||||||
|
|||||||
+8
-17
@@ -8,30 +8,21 @@ If you're seeing this, you've probably already done this step. Congrats!
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# create a new project in the current directory
|
# create a new project in the current directory
|
||||||
npm create svelte@latest
|
npx sv create
|
||||||
|
|
||||||
# create a new project in my-app
|
|
||||||
npm create svelte@latest my-app
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
Once you've created your project, follow these steps:
|
||||||
|
|
||||||
```bash
|
1: Delete package-lock.json
|
||||||
npm run dev
|
2: Check `git status`. If you see any changes other than package-lock.json or favicon.ico, run the command `git restore ./` (See below)
|
||||||
|
3: Run `npm install` or `pnpm install` or `yarn` to install the dependencies
|
||||||
|
4: Run `npm run build` to build the project
|
||||||
|
|
||||||
# or start the server and open the app in a new browser tab
|
Running `git status` should show:
|
||||||
npm run dev -- --open
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
[](https://postimg.cc/7CFsp2bq)
|
||||||
|
|
||||||
To create a production version of your app:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
You can preview the production build with `npm run preview`.
|
You can preview the production build with `npm run preview`.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -59,7 +59,9 @@
|
|||||||
"three": "^0.162.0",
|
"three": "^0.162.0",
|
||||||
"urdf-loader": "^0.12.1",
|
"urdf-loader": "^0.12.1",
|
||||||
"uzip": "^0.20201231.0",
|
"uzip": "^0.20201231.0",
|
||||||
"xacro-parser": "^0.3.9"
|
"xacro-parser": "^0.3.9",
|
||||||
|
"@types/msgpack-lite": "^0.1.11",
|
||||||
|
"msgpack-lite": "^0.1.26"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.3.0"
|
"packageManager": "pnpm@9.3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+91
-32
@@ -13,10 +13,13 @@ importers:
|
|||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
'@sveltejs/adapter-auto':
|
'@sveltejs/adapter-auto':
|
||||||
specifier: ^4.0.0
|
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(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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.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)))
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.0.12
|
specifier: ^4.0.12
|
||||||
version: 4.0.12(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
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
|
||||||
chart.js:
|
chart.js:
|
||||||
specifier: ^4.4.2
|
specifier: ^4.4.2
|
||||||
version: 4.4.2
|
version: 4.4.2
|
||||||
@@ -32,6 +35,9 @@ importers:
|
|||||||
jwt-decode:
|
jwt-decode:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.0.0
|
version: 4.0.0
|
||||||
|
msgpack-lite:
|
||||||
|
specifier: ^0.1.26
|
||||||
|
version: 0.1.26
|
||||||
nipplejs:
|
nipplejs:
|
||||||
specifier: ^0.10.1
|
specifier: ^0.10.1
|
||||||
version: 0.10.1
|
version: 0.10.1
|
||||||
@@ -65,13 +71,13 @@ importers:
|
|||||||
version: 1.49.1
|
version: 1.49.1
|
||||||
'@sveltejs/adapter-static':
|
'@sveltejs/adapter-static':
|
||||||
specifier: ^3.0.1
|
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(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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.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':
|
'@sveltejs/kit':
|
||||||
specifier: ^2.5.27
|
specifier: ^2.5.27
|
||||||
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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.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':
|
'@sveltejs/vite-plugin-svelte':
|
||||||
specifier: ^5.0.3
|
specifier: ^5.0.3
|
||||||
version: 5.0.3(svelte@5.20.4)(vite@6.2.1(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.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||||
'@types/eslint':
|
'@types/eslint':
|
||||||
specifier: ^8.56.0
|
specifier: ^8.56.0
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
@@ -128,10 +134,10 @@ importers:
|
|||||||
version: 0.18.5
|
version: 0.18.5
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.2.1
|
specifier: ^6.2.1
|
||||||
version: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
version: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^1.2.0
|
specifier: ^1.2.0
|
||||||
version: 1.2.0(jsdom@24.0.0)(lightningcss@1.29.2)
|
version: 1.2.0(@types/node@24.0.10)(jsdom@24.0.0)(lightningcss@1.29.2)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -760,6 +766,12 @@ packages:
|
|||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
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/semver@7.5.8':
|
'@types/semver@7.5.8':
|
||||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||||
|
|
||||||
@@ -1190,6 +1202,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
event-lite@0.1.3:
|
||||||
|
resolution: {integrity: sha512-8qz9nOz5VeD2z96elrEKD2U433+L3DWdUdDkOINLGOJvx1GsMBbMn0aCeu28y8/e85A6mCigBiFlYMnTBEGlSw==}
|
||||||
|
|
||||||
execa@5.1.1:
|
execa@5.1.1:
|
||||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1335,6 +1350,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
ignore@5.3.1:
|
ignore@5.3.1:
|
||||||
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
|
resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
@@ -1356,6 +1374,9 @@ packages:
|
|||||||
inherits@2.0.4:
|
inherits@2.0.4:
|
||||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
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:
|
is-binary-path@2.1.0:
|
||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1394,6 +1415,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
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:
|
isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
@@ -1592,6 +1616,10 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
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:
|
nanoid@3.3.7:
|
||||||
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -2019,6 +2047,9 @@ packages:
|
|||||||
ufo@1.5.3:
|
ufo@1.5.3:
|
||||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||||
|
|
||||||
|
undici-types@7.8.0:
|
||||||
|
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||||
|
|
||||||
universalify@0.2.0:
|
universalify@0.2.0:
|
||||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
@@ -2613,18 +2644,18 @@ snapshots:
|
|||||||
|
|
||||||
'@sinclair/typebox@0.27.8': {}
|
'@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(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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.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)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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))
|
||||||
import-meta-resolve: 4.1.0
|
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(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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.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)))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(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.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||||
'@types/cookie': 0.6.0
|
'@types/cookie': 0.6.0
|
||||||
cookie: 0.6.0
|
cookie: 0.6.0
|
||||||
devalue: 5.1.1
|
devalue: 5.1.1
|
||||||
@@ -2637,27 +2668,27 @@ snapshots:
|
|||||||
set-cookie-parser: 2.6.0
|
set-cookie-parser: 2.6.0
|
||||||
sirv: 3.0.1
|
sirv: 3.0.1
|
||||||
svelte: 5.20.4
|
svelte: 5.20.4
|
||||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
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(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(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.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
svelte: 5.20.4
|
svelte: 5.20.4
|
||||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(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))
|
||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
deepmerge: 4.3.1
|
deepmerge: 4.3.1
|
||||||
kleur: 4.1.5
|
kleur: 4.1.5
|
||||||
magic-string: 0.30.17
|
magic-string: 0.30.17
|
||||||
svelte: 5.20.4
|
svelte: 5.20.4
|
||||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
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(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))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -2714,13 +2745,13 @@ snapshots:
|
|||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.12
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.12
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.0.12
|
'@tailwindcss/oxide-win32-x64-msvc': 4.0.12
|
||||||
|
|
||||||
'@tailwindcss/vite@4.0.12(vite@6.2.1(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.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.0.12
|
'@tailwindcss/node': 4.0.12
|
||||||
'@tailwindcss/oxide': 4.0.12
|
'@tailwindcss/oxide': 4.0.12
|
||||||
lightningcss: 1.29.2
|
lightningcss: 1.29.2
|
||||||
tailwindcss: 4.0.12
|
tailwindcss: 4.0.12
|
||||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||||
|
|
||||||
'@tweenjs/tween.js@23.1.2': {}
|
'@tweenjs/tween.js@23.1.2': {}
|
||||||
|
|
||||||
@@ -2737,6 +2768,14 @@ snapshots:
|
|||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
|
'@types/msgpack-lite@0.1.11':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 24.0.10
|
||||||
|
|
||||||
|
'@types/node@24.0.10':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 7.8.0
|
||||||
|
|
||||||
'@types/semver@7.5.8': {}
|
'@types/semver@7.5.8': {}
|
||||||
|
|
||||||
'@types/stats.js@0.17.3': {}
|
'@types/stats.js@0.17.3': {}
|
||||||
@@ -3259,6 +3298,8 @@ snapshots:
|
|||||||
|
|
||||||
esutils@2.0.3: {}
|
esutils@2.0.3: {}
|
||||||
|
|
||||||
|
event-lite@0.1.3: {}
|
||||||
|
|
||||||
execa@5.1.1:
|
execa@5.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.3
|
cross-spawn: 7.0.3
|
||||||
@@ -3414,6 +3455,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
safer-buffer: 2.1.2
|
safer-buffer: 2.1.2
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
ignore@5.3.1: {}
|
ignore@5.3.1: {}
|
||||||
|
|
||||||
import-fresh@3.3.0:
|
import-fresh@3.3.0:
|
||||||
@@ -3432,6 +3475,8 @@ snapshots:
|
|||||||
|
|
||||||
inherits@2.0.4: {}
|
inherits@2.0.4: {}
|
||||||
|
|
||||||
|
int64-buffer@0.1.10: {}
|
||||||
|
|
||||||
is-binary-path@2.1.0:
|
is-binary-path@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.3.0
|
binary-extensions: 2.3.0
|
||||||
@@ -3458,6 +3503,8 @@ snapshots:
|
|||||||
|
|
||||||
is-stream@3.0.0: {}
|
is-stream@3.0.0: {}
|
||||||
|
|
||||||
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isexe@2.0.0: {}
|
isexe@2.0.0: {}
|
||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
@@ -3635,6 +3682,13 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
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.7: {}
|
||||||
|
|
||||||
nanoid@3.3.8: {}
|
nanoid@3.3.8: {}
|
||||||
@@ -4010,6 +4064,8 @@ snapshots:
|
|||||||
|
|
||||||
ufo@1.5.3: {}
|
ufo@1.5.3: {}
|
||||||
|
|
||||||
|
undici-types@7.8.0: {}
|
||||||
|
|
||||||
universalify@0.2.0: {}
|
universalify@0.2.0: {}
|
||||||
|
|
||||||
unplugin-icons@0.18.5:
|
unplugin-icons@0.18.5:
|
||||||
@@ -4054,13 +4110,13 @@ snapshots:
|
|||||||
|
|
||||||
uzip@0.20201231.0: {}
|
uzip@0.20201231.0: {}
|
||||||
|
|
||||||
vite-node@1.2.0(lightningcss@1.29.2):
|
vite-node@1.2.0(@types/node@24.0.10)(lightningcss@1.29.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.0
|
debug: 4.4.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
picocolors: 1.0.1
|
picocolors: 1.0.1
|
||||||
vite: 5.4.14(lightningcss@1.29.2)
|
vite: 5.4.14(@types/node@24.0.10)(lightningcss@1.29.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@types/node'
|
- '@types/node'
|
||||||
- less
|
- less
|
||||||
@@ -4072,31 +4128,33 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vite@5.4.14(lightningcss@1.29.2):
|
vite@5.4.14(@types/node@24.0.10)(lightningcss@1.29.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.21.5
|
esbuild: 0.21.5
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
rollup: 4.34.8
|
rollup: 4.34.8
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 24.0.10
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
lightningcss: 1.29.2
|
lightningcss: 1.29.2
|
||||||
|
|
||||||
vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2):
|
vite@6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.0
|
esbuild: 0.25.0
|
||||||
postcss: 8.5.3
|
postcss: 8.5.3
|
||||||
rollup: 4.34.8
|
rollup: 4.34.8
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 24.0.10
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.4.2
|
jiti: 2.4.2
|
||||||
lightningcss: 1.29.2
|
lightningcss: 1.29.2
|
||||||
yaml: 2.4.2
|
yaml: 2.4.2
|
||||||
|
|
||||||
vitefu@1.0.6(vite@6.2.1(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)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
vite: 6.2.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||||
|
|
||||||
vitest@1.2.0(jsdom@24.0.0)(lightningcss@1.29.2):
|
vitest@1.2.0(@types/node@24.0.10)(jsdom@24.0.0)(lightningcss@1.29.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 1.2.0
|
'@vitest/expect': 1.2.0
|
||||||
'@vitest/runner': 1.2.0
|
'@vitest/runner': 1.2.0
|
||||||
@@ -4116,10 +4174,11 @@ snapshots:
|
|||||||
strip-literal: 1.3.0
|
strip-literal: 1.3.0
|
||||||
tinybench: 2.8.0
|
tinybench: 2.8.0
|
||||||
tinypool: 0.8.4
|
tinypool: 0.8.4
|
||||||
vite: 5.4.14(lightningcss@1.29.2)
|
vite: 5.4.14(@types/node@24.0.10)(lightningcss@1.29.2)
|
||||||
vite-node: 1.2.0(lightningcss@1.29.2)
|
vite-node: 1.2.0(@types/node@24.0.10)(lightningcss@1.29.2)
|
||||||
why-is-node-running: 2.2.2
|
why-is-node-running: 2.2.2
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
|
'@types/node': 24.0.10
|
||||||
jsdom: 24.0.0
|
jsdom: 24.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -1,18 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { focusTrap } from 'svelte-focus-trap';
|
import { focusTrap } from 'svelte-focus-trap'
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition'
|
||||||
import { Cancel, Check } from '$lib/components/icons';
|
import { Cancel, Check } from '$lib/components/icons'
|
||||||
import { modals, exitBeforeEnter } from 'svelte-modals';
|
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals'
|
||||||
|
|
||||||
// provided by <Modals />
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isOpen: boolean;
|
|
||||||
title: string;
|
|
||||||
message: string;
|
|
||||||
onConfirm: any;
|
|
||||||
labels?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
let {
|
let {
|
||||||
isOpen,
|
isOpen,
|
||||||
@@ -23,7 +13,7 @@
|
|||||||
cancel: { label: 'Cancel', icon: Cancel },
|
cancel: { label: 'Cancel', icon: Cancel },
|
||||||
confirm: { label: 'OK', icon: Check }
|
confirm: { label: 'OK', icon: Check }
|
||||||
}
|
}
|
||||||
}: Props = $props();
|
}: ModalProps = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
@@ -33,26 +23,18 @@
|
|||||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||||
transition:fly={{ y: 50 }}
|
transition:fly={{ y: 50 }}
|
||||||
use:exitBeforeEnter
|
use:exitBeforeEnter
|
||||||
use:focusTrap
|
use:focusTrap>
|
||||||
>
|
|
||||||
<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"
|
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>
|
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||||
<div class="divider my-2"></div>
|
<div class="divider my-2"></div>
|
||||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||||
<div class="divider my-2"></div>
|
<div class="divider my-2"></div>
|
||||||
<div class="flex justify-end gap-2">
|
<div class="flex justify-end gap-2">
|
||||||
<button
|
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
|
||||||
class="btn btn-primary inline-flex items-center"
|
|
||||||
onclick={() => modals.close()}
|
|
||||||
>
|
|
||||||
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
|
||||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
|
||||||
onclick={onConfirm}
|
|
||||||
>
|
|
||||||
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
|
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte'
|
||||||
|
import * as THREE from 'three'
|
||||||
|
import { imu } from '$lib/stores/imu'
|
||||||
|
import SceneBuilder from '$lib/sceneBuilder'
|
||||||
|
|
||||||
|
let canvas: HTMLCanvasElement = $state()
|
||||||
|
let sceneBuilder: SceneBuilder
|
||||||
|
let cube: THREE.Mesh
|
||||||
|
let targetRotation = new THREE.Euler()
|
||||||
|
let lastUpdateTime = 0
|
||||||
|
const LERP_SPEED = 5 // rotations per second
|
||||||
|
|
||||||
|
const initThreeJS = () => {
|
||||||
|
sceneBuilder = new SceneBuilder()
|
||||||
|
.addRenderer({ canvas: canvas, antialias: true, alpha: true })
|
||||||
|
.addPerspectiveCamera({ x: 2, y: 0, z: 2 })
|
||||||
|
.addOrbitControls(1, 10, false)
|
||||||
|
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
|
||||||
|
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
|
||||||
|
.fillParent()
|
||||||
|
|
||||||
|
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||||
|
const material = new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x00ff00,
|
||||||
|
transparent: true,
|
||||||
|
opacity: 0.8
|
||||||
|
})
|
||||||
|
cube = new THREE.Mesh(geometry, material)
|
||||||
|
sceneBuilder.scene.add(cube)
|
||||||
|
|
||||||
|
sceneBuilder.addRenderCb(() => {
|
||||||
|
if (!cube) return
|
||||||
|
const currentTime = performance.now()
|
||||||
|
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
|
||||||
|
lastUpdateTime = currentTime
|
||||||
|
|
||||||
|
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
|
||||||
|
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
|
||||||
|
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
|
||||||
|
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
|
||||||
|
})
|
||||||
|
|
||||||
|
sceneBuilder.startRenderLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOrientation = () => {
|
||||||
|
if (!cube) return
|
||||||
|
|
||||||
|
const y = -$imu.x[$imu.x.length - 1] || 0
|
||||||
|
const x = $imu.y[$imu.y.length - 1] || 0
|
||||||
|
const z = -$imu.z[$imu.z.length - 1] || 0
|
||||||
|
|
||||||
|
targetRotation.set(
|
||||||
|
THREE.MathUtils.degToRad(x),
|
||||||
|
THREE.MathUtils.degToRad(y),
|
||||||
|
THREE.MathUtils.degToRad(z)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
initThreeJS()
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
sceneBuilder?.renderer?.dispose()
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($imu) {
|
||||||
|
updateOrientation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
|
||||||
|
<canvas class="w-full h-full" bind:this={canvas}></canvas>
|
||||||
|
</div>
|
||||||
@@ -1,31 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition'
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing'
|
||||||
import { Down } from './icons';
|
import { Down } from './icons'
|
||||||
interface Props {
|
interface Props {
|
||||||
open?: boolean;
|
open?: boolean
|
||||||
collapsible?: boolean;
|
collapsible?: boolean
|
||||||
icon?: import('svelte').Snippet;
|
icon?: import('svelte').Snippet
|
||||||
title?: import('svelte').Snippet;
|
title?: import('svelte').Snippet
|
||||||
children?: import('svelte').Snippet;
|
children?: import('svelte').Snippet
|
||||||
|
right?: import('svelte').Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { open = $bindable(true), collapsible = true, icon, title, children, right }: Props = $props()
|
||||||
open = $bindable(true),
|
|
||||||
collapsible = true,
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
children
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if collapsible}
|
{#if collapsible}
|
||||||
<div
|
<div
|
||||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||||
>
|
|
||||||
<span class="inline-flex items-baseline">
|
<span class="inline-flex items-baseline">
|
||||||
{@render icon?.()}
|
{@render icon?.()}
|
||||||
{@render title?.()}
|
{@render title?.()}
|
||||||
@@ -33,34 +26,32 @@
|
|||||||
<button
|
<button
|
||||||
class="btn btn-circle btn-ghost btn-sm"
|
class="btn btn-circle btn-ghost btn-sm"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
open = !open;
|
open = !open
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Down
|
<Down
|
||||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
|
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ?
|
||||||
? 'rotate-180'
|
'rotate-180'
|
||||||
: ''}"
|
: ''}" />
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 p-4 pt-0"
|
class="flex flex-col gap-2 p-4 pt-0"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div
|
<div
|
||||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
|
||||||
>
|
<div
|
||||||
<div class="min-h-16 w-full p-4 text-xl font-medium">
|
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||||
<span class="inline-flex items-baseline">
|
<span class="inline-flex items-baseline">
|
||||||
{@render icon?.()}
|
{@render icon?.()}
|
||||||
{@render title?.()}
|
{@render title?.()}
|
||||||
</span>
|
</span>
|
||||||
|
{@render right?.()}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||||
|
|
||||||
|
const {
|
||||||
|
icon,
|
||||||
|
title,
|
||||||
|
description = '',
|
||||||
|
variant = 'primary',
|
||||||
|
class: klass = '',
|
||||||
|
children = null
|
||||||
|
} = $props<{
|
||||||
|
icon?: any
|
||||||
|
title: string
|
||||||
|
description?: string | number
|
||||||
|
variant?: Variant
|
||||||
|
class?: string
|
||||||
|
children?: () => any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const Icon = $derived(icon)
|
||||||
|
|
||||||
|
const variants: Record<Variant, [string, string]> = {
|
||||||
|
success: ['bg-success', 'text-success-content'],
|
||||||
|
error: ['bg-error', 'text-error-content'],
|
||||||
|
primary: ['bg-primary', 'text-primary-content'],
|
||||||
|
info: ['bg-info', 'text-info-content'],
|
||||||
|
warning: ['bg-warning', 'text-warning-content']
|
||||||
|
}
|
||||||
|
|
||||||
|
const variantKey: Variant = (variant as Variant) in variants ? (variant as Variant) : 'primary'
|
||||||
|
const [bgColor, textColor] = variants[variantKey]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2 {klass}">
|
||||||
|
{#if icon}
|
||||||
|
<div class="mask mask-hexagon {bgColor} h-auto w-10 flex-none">
|
||||||
|
<Icon class="{textColor} h-auto w-full scale-75" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="grow">
|
||||||
|
<div class="font-bold">{title}</div>
|
||||||
|
<div class="text-sm opacity-75 grow">{description}</div>
|
||||||
|
</div>
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte'
|
||||||
import {
|
import {
|
||||||
BufferGeometry,
|
BufferGeometry,
|
||||||
Line,
|
Line,
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
Vector3,
|
Vector3,
|
||||||
type NormalBufferAttributes,
|
type NormalBufferAttributes,
|
||||||
type Object3DEventMap
|
type Object3DEventMap
|
||||||
} from 'three';
|
} from 'three'
|
||||||
import {
|
import {
|
||||||
ModesEnum,
|
ModesEnum,
|
||||||
kinematicData,
|
kinematicData,
|
||||||
@@ -22,12 +22,17 @@
|
|||||||
servoAngles,
|
servoAngles,
|
||||||
mpu,
|
mpu,
|
||||||
jointNames
|
jointNames
|
||||||
} from '$lib/stores';
|
} from '$lib/stores'
|
||||||
import { footColor, populateModelCache, throttler, toeWorldPositions } from '$lib/utilities';
|
import {
|
||||||
import SceneBuilder from '$lib/sceneBuilder';
|
extractFootColor,
|
||||||
import { lerp, degToRad } from 'three/src/math/MathUtils';
|
populateModelCache,
|
||||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
throttler,
|
||||||
import Kinematic, { type body_state_t } from '$lib/kinematic';
|
getToeWorldPositions
|
||||||
|
} from '$lib/utilities'
|
||||||
|
import SceneBuilder from '$lib/sceneBuilder'
|
||||||
|
import { lerp, degToRad } from 'three/src/math/MathUtils'
|
||||||
|
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
|
||||||
|
import Kinematic, { type body_state_t } from '$lib/kinematic'
|
||||||
import {
|
import {
|
||||||
BezierState,
|
BezierState,
|
||||||
CalibrationState,
|
CalibrationState,
|
||||||
@@ -36,42 +41,36 @@
|
|||||||
IdleState,
|
IdleState,
|
||||||
RestState,
|
RestState,
|
||||||
StandState
|
StandState
|
||||||
} from '$lib/gait';
|
} from '$lib/gait'
|
||||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
import { radToDeg } from 'three/src/math/MathUtils.js'
|
||||||
import type { URDFRobot } from 'urdf-loader';
|
import type { URDFRobot } from 'urdf-loader'
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
sky?: boolean;
|
sky?: boolean
|
||||||
orbit?: boolean;
|
orbit?: boolean
|
||||||
panel?: boolean;
|
panel?: boolean
|
||||||
debug?: boolean;
|
debug?: boolean
|
||||||
ground?: boolean;
|
ground?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
|
||||||
sky = true,
|
|
||||||
orbit = false,
|
|
||||||
panel = true,
|
|
||||||
debug = false,
|
|
||||||
ground = true
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let sceneManager = $state(new SceneBuilder());
|
let sceneManager = $state(new SceneBuilder())
|
||||||
let canvas: HTMLCanvasElement = $state();
|
let canvas: HTMLCanvasElement = $state()
|
||||||
|
|
||||||
let currentModelAngles: number[] = new Array(12).fill(0);
|
let currentModelAngles: number[] = new Array(12).fill(0)
|
||||||
let modelTargetAngles: number[] = new Array(12).fill(0);
|
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||||
let gui_panel: GUI;
|
let gui_panel: GUI
|
||||||
let Throttler = new throttler();
|
let Throttler = new throttler()
|
||||||
|
|
||||||
let feet_trace = new Array(4).fill([]);
|
let feet_trace = new Array(4).fill([])
|
||||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = [];
|
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||||
let target: Object3D<Object3DEventMap>;
|
let target: Object3D<Object3DEventMap>
|
||||||
|
|
||||||
let target_position = { x: 0, z: 0, yaw: 0 };
|
let target_position = { x: 0, z: 0, yaw: 0 }
|
||||||
|
|
||||||
let kinematic = new Kinematic();
|
let kinematic = new Kinematic()
|
||||||
|
|
||||||
let planners = {
|
let planners = {
|
||||||
[ModesEnum.Deactivated]: new IdleState(),
|
[ModesEnum.Deactivated]: new IdleState(),
|
||||||
@@ -81,10 +80,10 @@
|
|||||||
[ModesEnum.Stand]: new StandState(),
|
[ModesEnum.Stand]: new StandState(),
|
||||||
[ModesEnum.Crawl]: new EightPhaseWalkState(),
|
[ModesEnum.Crawl]: new EightPhaseWalkState(),
|
||||||
[ModesEnum.Walk]: new BezierState()
|
[ModesEnum.Walk]: new BezierState()
|
||||||
};
|
}
|
||||||
let lastTick = performance.now();
|
let lastTick = performance.now()
|
||||||
|
|
||||||
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1];
|
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
|
||||||
|
|
||||||
let body_state = {
|
let body_state = {
|
||||||
omega: 0,
|
omega: 0,
|
||||||
@@ -93,8 +92,8 @@
|
|||||||
xm: 0,
|
xm: 0,
|
||||||
ym: 0.5,
|
ym: 0.5,
|
||||||
zm: 0,
|
zm: 0,
|
||||||
feet: planners[ModesEnum.Idle].default_feet_pos
|
feet: kinematic.getDefaultFeetPos()
|
||||||
};
|
}
|
||||||
|
|
||||||
let settings = {
|
let settings = {
|
||||||
'Internal kinematic': true,
|
'Internal kinematic': true,
|
||||||
@@ -112,51 +111,51 @@
|
|||||||
ym: 0.7,
|
ym: 0.7,
|
||||||
zm: 0,
|
zm: 0,
|
||||||
Background: 'black'
|
Background: 'black'
|
||||||
};
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await populateModelCache();
|
await populateModelCache()
|
||||||
await createScene();
|
await createScene()
|
||||||
servoAngles.subscribe(updateAnglesFromStore);
|
servoAngles.subscribe(updateAnglesFromStore)
|
||||||
if (panel) createPanel();
|
if (panel) createPanel()
|
||||||
});
|
})
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
canvas.remove();
|
canvas.remove()
|
||||||
gui_panel?.destroy();
|
gui_panel?.destroy()
|
||||||
});
|
})
|
||||||
|
|
||||||
const updateAnglesFromStore = (angles: number[]) => {
|
const updateAnglesFromStore = (angles: number[]) => {
|
||||||
if (sceneManager.isDragging) return;
|
if (sceneManager.isDragging) return
|
||||||
if (settings['Internal kinematic']) return;
|
if (settings['Internal kinematic']) return
|
||||||
modelTargetAngles = angles;
|
modelTargetAngles = angles
|
||||||
};
|
}
|
||||||
|
|
||||||
const createPanel = () => {
|
const createPanel = () => {
|
||||||
gui_panel = new GUI({ width: 310 });
|
gui_panel = new GUI({ width: 310 })
|
||||||
gui_panel.close();
|
gui_panel.close()
|
||||||
gui_panel.domElement.id = 'three-gui-panel';
|
gui_panel.domElement.id = 'three-gui-panel'
|
||||||
|
|
||||||
const general = gui_panel.addFolder('General');
|
const general = gui_panel.addFolder('General')
|
||||||
general.add(settings, 'Internal kinematic');
|
general.add(settings, 'Internal kinematic')
|
||||||
general.add(settings, 'Robot transform controls');
|
general.add(settings, 'Robot transform controls')
|
||||||
general.add(settings, 'Auto orient robot');
|
general.add(settings, 'Auto orient robot')
|
||||||
|
|
||||||
const kinematic = gui_panel.addFolder('Kinematics');
|
const kinematic = gui_panel.addFolder('Kinematics')
|
||||||
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen();
|
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
|
||||||
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen();
|
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
|
||||||
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen();
|
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
|
||||||
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen();
|
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
|
||||||
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen();
|
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
|
||||||
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen();
|
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
|
||||||
|
|
||||||
const visibility = gui_panel.addFolder('Visualization');
|
const visibility = gui_panel.addFolder('Visualization')
|
||||||
visibility.add(settings, 'Trace feet');
|
visibility.add(settings, 'Trace feet')
|
||||||
visibility.add(settings, 'Trace points', 1, 1000, 1);
|
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||||
visibility.add(settings, 'Target position');
|
visibility.add(settings, 'Target position')
|
||||||
visibility.add(settings, 'Smooth motion');
|
visibility.add(settings, 'Smooth motion')
|
||||||
visibility.addColor(settings, 'Background');
|
visibility.addColor(settings, 'Background')
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateKinematicPosition = () => {
|
const updateKinematicPosition = () => {
|
||||||
kinematicData.set([
|
kinematicData.set([
|
||||||
@@ -166,16 +165,13 @@
|
|||||||
settings.xm,
|
settings.xm,
|
||||||
settings.ym,
|
settings.ym,
|
||||||
settings.zm
|
settings.zm
|
||||||
]);
|
])
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateAngles = (name: string, angle: number) => {
|
const updateAngles = (name: string, angle: number) => {
|
||||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
|
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
|
||||||
Throttler.throttle(
|
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
|
||||||
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))),
|
}
|
||||||
100
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createScene = async () => {
|
const createScene = async () => {
|
||||||
sceneManager
|
sceneManager
|
||||||
@@ -189,46 +185,46 @@
|
|||||||
.addTransformControls(sceneManager.model)
|
.addTransformControls(sceneManager.model)
|
||||||
.fillParent()
|
.fillParent()
|
||||||
.addRenderCb(render)
|
.addRenderCb(render)
|
||||||
.startRenderLoop();
|
.startRenderLoop()
|
||||||
|
|
||||||
if (ground) sceneManager.addGroundPlane();
|
if (ground) sceneManager.addGroundPlane()
|
||||||
|
|
||||||
const geometry = new SphereGeometry(0.1, 32, 16);
|
const geometry = new SphereGeometry(0.1, 32, 16)
|
||||||
const material = new MeshBasicMaterial({ color: 0xffff00 });
|
const material = new MeshBasicMaterial({ color: 0xffff00 })
|
||||||
target = new Mesh(geometry, material);
|
target = new Mesh(geometry, material)
|
||||||
sceneManager.scene.add(target);
|
sceneManager.scene.add(target)
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
sceneManager.addDragControl(updateAngles);
|
sceneManager.addDragControl(updateAngles)
|
||||||
}
|
}
|
||||||
if (sky) sceneManager.addSky();
|
if (sky) sceneManager.addSky()
|
||||||
|
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
const geometry = new BufferGeometry();
|
const geometry = new BufferGeometry()
|
||||||
const material = new LineBasicMaterial({ color: footColor() });
|
const material = new LineBasicMaterial({ color: extractFootColor() })
|
||||||
const line = new Line(geometry, material);
|
const line = new Line(geometry, material)
|
||||||
trace_lines.push(geometry);
|
trace_lines.push(geometry)
|
||||||
sceneManager.scene.add(line);
|
sceneManager.scene.add(line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const renderTraceLines = (foot_positions: Vector3[]) => {
|
const renderTraceLines = (foot_positions: Vector3[]) => {
|
||||||
if (!settings['Trace feet']) {
|
if (!settings['Trace feet']) {
|
||||||
if (!feet_trace.length) return;
|
if (!feet_trace.length) return
|
||||||
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)));
|
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
|
||||||
feet_trace = new Array(4).fill([]);
|
feet_trace = new Array(4).fill([])
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
trace_lines.forEach((line, i) => {
|
trace_lines.forEach((line, i) => {
|
||||||
feet_trace[i].push(foot_positions[i]);
|
feet_trace[i].push(foot_positions[i])
|
||||||
feet_trace[i] = feet_trace[i].slice(-settings['Trace points']);
|
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
|
||||||
line.setFromPoints(feet_trace[i]);
|
line.setFromPoints(feet_trace[i])
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const calculate_kinematics = () => {
|
const calculate_kinematics = () => {
|
||||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return;
|
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||||
const position: body_state_t = {
|
const position: body_state_t = {
|
||||||
omega: settings.omega,
|
omega: settings.omega,
|
||||||
phi: settings.phi,
|
phi: settings.phi,
|
||||||
@@ -237,40 +233,36 @@
|
|||||||
ym: settings.ym,
|
ym: settings.ym,
|
||||||
zm: settings.zm,
|
zm: settings.zm,
|
||||||
feet: body_state.feet
|
feet: body_state.feet
|
||||||
};
|
}
|
||||||
|
|
||||||
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]));
|
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
|
||||||
modelTargetAngles = new_angles;
|
modelTargetAngles = new_angles
|
||||||
};
|
}
|
||||||
|
|
||||||
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
|
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
|
||||||
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return;
|
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
|
||||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
|
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
|
||||||
|
|
||||||
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1);
|
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
|
||||||
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1);
|
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
|
||||||
|
|
||||||
robot.rotation.z = smooth(
|
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
|
||||||
robot.rotation.z,
|
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
|
||||||
degToRad(-settings.phi + $mpu.heading + 90),
|
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
|
||||||
0.1
|
}
|
||||||
);
|
|
||||||
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1);
|
|
||||||
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1);
|
|
||||||
};
|
|
||||||
|
|
||||||
const update_camera = (robot: URDFRobot) => {
|
const update_camera = (robot: URDFRobot) => {
|
||||||
if (!settings['Fix camera on robot']) return;
|
if (!settings['Fix camera on robot']) return
|
||||||
sceneManager.orbit.target = robot.position.clone();
|
sceneManager.orbit.target = robot.position.clone()
|
||||||
};
|
}
|
||||||
|
|
||||||
const smooth = (start: number, end: number, amount: number) => {
|
const smooth = (start: number, end: number, amount: number) => {
|
||||||
return settings['Smooth motion'] ? lerp(start, end, amount) : end;
|
return settings['Smooth motion'] ? lerp(start, end, amount) : end
|
||||||
};
|
}
|
||||||
|
|
||||||
const update_gait = () => {
|
const update_gait = () => {
|
||||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return;
|
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||||
const controlData = get(outControllerData);
|
const controlData = get(outControllerData)
|
||||||
const data = {
|
const data = {
|
||||||
stop: controlData[0],
|
stop: controlData[0],
|
||||||
lx: controlData[1],
|
lx: controlData[1],
|
||||||
@@ -280,66 +272,66 @@
|
|||||||
h: controlData[5],
|
h: controlData[5],
|
||||||
s: controlData[6],
|
s: controlData[6],
|
||||||
s1: controlData[7]
|
s1: controlData[7]
|
||||||
};
|
}
|
||||||
body_state.ym = ((data.h + 127) * 0.75) / 100;
|
body_state.ym = ((data.h + 127) * 0.35) / 100
|
||||||
|
|
||||||
let planner = planners[get(mode)];
|
let planner = planners[get(mode)]
|
||||||
const delta = performance.now() - lastTick;
|
const delta = performance.now() - lastTick
|
||||||
lastTick = performance.now();
|
lastTick = performance.now()
|
||||||
|
|
||||||
body_state = planner.step(body_state, data, delta);
|
body_state = planner.step(body_state, data, delta)
|
||||||
|
|
||||||
settings.omega = body_state.omega;
|
settings.omega = body_state.omega
|
||||||
settings.phi = body_state.phi;
|
settings.phi = body_state.phi
|
||||||
settings.psi = body_state.psi;
|
settings.psi = body_state.psi
|
||||||
settings.xm = body_state.xm;
|
settings.xm = body_state.xm
|
||||||
settings.ym = body_state.ym;
|
settings.ym = body_state.ym
|
||||||
settings.zm = body_state.zm;
|
settings.zm = body_state.zm
|
||||||
};
|
}
|
||||||
|
|
||||||
const update_robot_position = (robot: URDFRobot) => {
|
const update_robot_position = (robot: URDFRobot) => {
|
||||||
if (!settings['Robot transform controls']) return;
|
if (!settings['Robot transform controls']) return
|
||||||
settings.omega = radToDeg(robot.rotation.y);
|
settings.omega = radToDeg(robot.rotation.y)
|
||||||
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90;
|
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
|
||||||
settings.psi = radToDeg(robot.rotation.x) + 90;
|
settings.psi = radToDeg(robot.rotation.x) + 90
|
||||||
settings.xm = robot.position.z * 100;
|
settings.xm = robot.position.z * 100
|
||||||
settings.zm = -robot.position.x * 100;
|
settings.zm = -robot.position.x * 100
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateTargetPosition = () => {
|
const updateTargetPosition = () => {
|
||||||
target.visible = settings['Target position'];
|
target.visible = settings['Target position']
|
||||||
target.position.x = smooth(target.position.x, target_position.x, 0.5);
|
target.position.x = smooth(target.position.x, target_position.x, 0.5)
|
||||||
target.position.z = smooth(target.position.z, target_position.z, 0.5);
|
target.position.z = smooth(target.position.z, target_position.z, 0.5)
|
||||||
};
|
}
|
||||||
|
|
||||||
const render = () => {
|
const render = () => {
|
||||||
const robot = sceneManager.model;
|
const robot = sceneManager.model
|
||||||
if (!robot) return;
|
if (!robot) return
|
||||||
|
|
||||||
const toes = toeWorldPositions(robot);
|
const toes = getToeWorldPositions(robot)
|
||||||
|
|
||||||
renderTraceLines(toes);
|
renderTraceLines(toes)
|
||||||
update_camera(robot);
|
update_camera(robot)
|
||||||
update_gait();
|
update_gait()
|
||||||
calculate_kinematics();
|
calculate_kinematics()
|
||||||
update_robot_position(robot);
|
update_robot_position(robot)
|
||||||
|
|
||||||
sceneManager.transformControl.showX = settings['Robot transform controls'];
|
sceneManager.transformControl.showX = settings['Robot transform controls']
|
||||||
sceneManager.transformControl.showY = settings['Robot transform controls'];
|
sceneManager.transformControl.showY = settings['Robot transform controls']
|
||||||
sceneManager.transformControl.showZ = settings['Robot transform controls'];
|
sceneManager.transformControl.showZ = settings['Robot transform controls']
|
||||||
|
|
||||||
for (let i = 0; i < $jointNames.length; i++) {
|
for (let i = 0; i < $jointNames.length; i++) {
|
||||||
currentModelAngles[i] = smooth(
|
currentModelAngles[i] = smooth(
|
||||||
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
||||||
modelTargetAngles[i],
|
modelTargetAngles[i],
|
||||||
0.1
|
0.1
|
||||||
);
|
)
|
||||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
|
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
|
||||||
}
|
}
|
||||||
|
|
||||||
orient_robot(robot, toes);
|
orient_robot(robot, toes)
|
||||||
updateTargetPosition();
|
updateTargetPosition()
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window onresize={sceneManager.fillParent} />
|
<svelte:window onresize={sceneManager.fillParent} />
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export { default as Hamburger } from '~icons/mdi/hamburger-menu'
|
|||||||
export { default as FileIcon } from '~icons/mdi/file'
|
export { default as FileIcon } from '~icons/mdi/file'
|
||||||
export { default as FolderIcon } from '~icons/mdi/folder-outline'
|
export { default as FolderIcon } from '~icons/mdi/folder-outline'
|
||||||
export { default as FolderOpenOutline } from '~icons/mdi/folder-open-outline'
|
export { default as FolderOpenOutline } from '~icons/mdi/folder-open-outline'
|
||||||
|
export { default as TrashIcon } from '~icons/mdi/trash'
|
||||||
|
export { default as RotateCcw } from '~icons/mdi/rotate-left'
|
||||||
|
export { default as RotateCw } from '~icons/mdi/rotate-right'
|
||||||
|
|
||||||
export { default as Down } from '~icons/tabler/chevron-down'
|
export { default as Down } from '~icons/tabler/chevron-down'
|
||||||
export { default as Cancel } from '~icons/tabler/x'
|
export { default as Cancel } from '~icons/tabler/x'
|
||||||
@@ -50,13 +53,14 @@ export { default as Power } from '~icons/tabler/power'
|
|||||||
export { default as MAC } from '~icons/tabler/dna-2'
|
export { default as MAC } from '~icons/tabler/dna-2'
|
||||||
export { default as Home } from '~icons/tabler/home'
|
export { default as Home } from '~icons/tabler/home'
|
||||||
export { default as SSID } from '~icons/tabler/router'
|
export { default as SSID } from '~icons/tabler/router'
|
||||||
export { default as DNS } from '~icons/tabler/address-book'
|
export { default as DNS } from '~icons/mdi/dns'
|
||||||
export { default as Gateway } from '~icons/tabler/torii'
|
export { default as Gateway } from '~icons/tabler/torii'
|
||||||
export { default as Subnet } from '~icons/tabler/grid-dots'
|
export { default as Subnet } from '~icons/tabler/grid-dots'
|
||||||
export { default as Channel } from '~icons/tabler/antenna'
|
export { default as Channel } from '~icons/tabler/antenna'
|
||||||
export { default as Scan } from '~icons/tabler/radar-2'
|
export { default as Scan } from '~icons/tabler/radar-2'
|
||||||
export { default as Add } from '~icons/tabler/circle-plus'
|
export { default as Add } from '~icons/tabler/circle-plus'
|
||||||
export { default as Edit } from '~icons/tabler/pencil'
|
export { default as Edit } from '~icons/mdi/edit'
|
||||||
|
export { default as EditOff } from '~icons/mdi/edit-off'
|
||||||
export { default as Delete } from '~icons/tabler/trash'
|
export { default as Delete } from '~icons/tabler/trash'
|
||||||
|
|
||||||
export { default as Network } from '~icons/tabler/router'
|
export { default as Network } from '~icons/tabler/router'
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
Router,
|
Router,
|
||||||
AP,
|
AP,
|
||||||
Copyright,
|
Copyright,
|
||||||
Metrics
|
Metrics,
|
||||||
|
DNS
|
||||||
} from '$lib/components/icons'
|
} from '$lib/components/icons'
|
||||||
import appEnv from 'app-env'
|
import appEnv from 'app-env'
|
||||||
|
|
||||||
@@ -103,6 +104,12 @@
|
|||||||
icon: AP,
|
icon: AP,
|
||||||
href: '/wifi/ap',
|
href: '/wifi/ap',
|
||||||
feature: true
|
feature: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'mDNS',
|
||||||
|
icon: DNS,
|
||||||
|
href: '/wifi/mdns',
|
||||||
|
feature: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -127,7 +134,7 @@
|
|||||||
title: 'System Metrics',
|
title: 'System Metrics',
|
||||||
icon: Metrics,
|
icon: Metrics,
|
||||||
href: '/system/metrics',
|
href: '/system/metrics',
|
||||||
feature: $features.analytics
|
feature: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Firmware Update',
|
title: 'Firmware Update',
|
||||||
@@ -165,7 +172,11 @@
|
|||||||
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
|
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
|
||||||
<LogoButton {appName} />
|
<LogoButton {appName} />
|
||||||
|
|
||||||
<MenuList {menuItems} select={updateMenu} class="grow flex-nowrap overflow-y-auto" level="0" />
|
<MenuList
|
||||||
|
{menuItems}
|
||||||
|
select={updateMenu}
|
||||||
|
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
|
||||||
|
level="0" />
|
||||||
|
|
||||||
<div class="divider my-0"></div>
|
<div class="divider my-0"></div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,13 +16,13 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ul class={klass + ' menu'}>
|
<ul class={klass + ' menu w-full'}>
|
||||||
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
|
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
|
||||||
{#if menuItem.feature}
|
{#if menuItem.feature}
|
||||||
<li>
|
<li>
|
||||||
{#if menuItem.submenu}
|
{#if menuItem.submenu}
|
||||||
<details open={menuItem.submenu.some(subItem => subItem.active)}>
|
<details open={menuItem.submenu.some(subItem => subItem.active)}>
|
||||||
<summary class="text-lg font-bold">
|
<summary class="font-bold">
|
||||||
<menuItem.icon class="h-6 w-6" />
|
<menuItem.icon class="h-6 w-6" />
|
||||||
{menuItem.title}
|
{menuItem.title}
|
||||||
</summary>
|
</summary>
|
||||||
|
|||||||
+192
-197
@@ -1,33 +1,34 @@
|
|||||||
import type { body_state_t } from './kinematic';
|
import type { body_state_t } from './kinematic'
|
||||||
import { fromInt8 } from './utilities';
|
import Kinematic from './kinematic'
|
||||||
|
import { fromInt8 } from './utilities'
|
||||||
|
|
||||||
const { sin } = Math;
|
const { sin } = Math
|
||||||
|
|
||||||
export interface gait_state_t {
|
export interface gait_state_t {
|
||||||
step_height: number;
|
step_height: number
|
||||||
step_x: number;
|
step_x: number
|
||||||
step_z: number;
|
step_z: number
|
||||||
step_angle: number;
|
step_angle: number
|
||||||
step_velocity: number;
|
step_velocity: number
|
||||||
step_depth: number;
|
step_depth: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ControllerCommand {
|
export interface ControllerCommand {
|
||||||
stop: number;
|
stop: number
|
||||||
lx: number;
|
lx: number
|
||||||
ly: number;
|
ly: number
|
||||||
rx: number;
|
rx: number
|
||||||
ry: number;
|
ry: number
|
||||||
h: number;
|
h: number
|
||||||
s: number;
|
s: number
|
||||||
s1: number;
|
s1: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class GaitState {
|
export abstract class GaitState {
|
||||||
protected abstract name: string;
|
protected abstract name: string
|
||||||
|
|
||||||
protected dt = 0.02;
|
protected dt = 0.02
|
||||||
protected body_state!: body_state_t;
|
protected body_state!: body_state_t
|
||||||
protected gait_state: gait_state_t = {
|
protected gait_state: gait_state_t = {
|
||||||
step_height: 0.4,
|
step_height: 0.4,
|
||||||
step_x: 0,
|
step_x: 0,
|
||||||
@@ -35,32 +36,27 @@ export abstract class GaitState {
|
|||||||
step_angle: 0,
|
step_angle: 0,
|
||||||
step_velocity: 1,
|
step_velocity: 1,
|
||||||
step_depth: 0.002
|
step_depth: 0.002
|
||||||
};
|
}
|
||||||
|
|
||||||
public get default_feet_pos() {
|
public get default_feet_pos() {
|
||||||
return [
|
return new Kinematic().getDefaultFeetPos()
|
||||||
[1, -1, 1, 1],
|
|
||||||
[1, -1, -1, 1],
|
|
||||||
[-1, -1, 1, 1],
|
|
||||||
[-1, -1, -1, 1]
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected get default_height() {
|
protected get default_height() {
|
||||||
return 0.5;
|
return 0.5
|
||||||
}
|
}
|
||||||
|
|
||||||
begin() {
|
begin() {
|
||||||
console.log('Starting', this.name);
|
console.log('Starting', this.name)
|
||||||
}
|
}
|
||||||
end() {
|
end() {
|
||||||
console.log('Ending', this.name);
|
console.log('Ending', this.name)
|
||||||
}
|
}
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
this.map_command(command);
|
this.map_command(command)
|
||||||
this.body_state = body_state;
|
this.body_state = body_state
|
||||||
this.dt = dt / 1000;
|
this.dt = dt / 1000
|
||||||
return body_state;
|
return body_state
|
||||||
}
|
}
|
||||||
|
|
||||||
map_command(command: ControllerCommand) {
|
map_command(command: ControllerCommand) {
|
||||||
@@ -71,107 +67,107 @@ export abstract class GaitState {
|
|||||||
step_velocity: command.s / 128 + 1,
|
step_velocity: command.s / 128 + 1,
|
||||||
step_angle: command.rx / 128,
|
step_angle: command.rx / 128,
|
||||||
step_depth: 0.002
|
step_depth: 0.002
|
||||||
};
|
}
|
||||||
|
|
||||||
this.gait_state = newCommand;
|
this.gait_state = newCommand
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IdleState extends GaitState {
|
export class IdleState extends GaitState {
|
||||||
protected name = 'Idle';
|
protected name = 'Idle'
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CalibrationState extends GaitState {
|
export class CalibrationState extends GaitState {
|
||||||
protected name = 'Calibration';
|
protected name = 'Calibration'
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
body_state.omega = 0;
|
body_state.omega = 0
|
||||||
body_state.phi = 0;
|
body_state.phi = 0
|
||||||
body_state.psi = 0;
|
body_state.psi = 0
|
||||||
body_state.xm = 0;
|
body_state.xm = 0
|
||||||
body_state.ym = this.default_height * 10;
|
body_state.ym = this.default_height * 10
|
||||||
body_state.zm = 0;
|
body_state.zm = 0
|
||||||
body_state.feet = this.default_feet_pos;
|
body_state.feet = this.default_feet_pos
|
||||||
return body_state;
|
return body_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RestState extends GaitState {
|
export class RestState extends GaitState {
|
||||||
protected name = 'Rest';
|
protected name = 'Rest'
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
body_state.omega = 0;
|
body_state.omega = 0
|
||||||
body_state.phi = 0;
|
body_state.phi = 0
|
||||||
body_state.psi = 0;
|
body_state.psi = 0
|
||||||
body_state.xm = 0;
|
body_state.xm = 0
|
||||||
body_state.ym = this.default_height / 2;
|
body_state.ym = this.default_height / 2
|
||||||
body_state.zm = 0;
|
body_state.zm = 0
|
||||||
body_state.feet = this.default_feet_pos;
|
body_state.feet = this.default_feet_pos
|
||||||
return body_state;
|
return body_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StandState extends GaitState {
|
export class StandState extends GaitState {
|
||||||
protected name = 'Stand';
|
protected name = 'Stand'
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
body_state.omega = 0;
|
body_state.omega = 0
|
||||||
body_state.phi = command.rx / 8;
|
body_state.phi = command.rx / 8
|
||||||
body_state.psi = command.ry / 8;
|
body_state.psi = command.ry / 8
|
||||||
body_state.xm = command.ly / 2 / 100;
|
body_state.xm = command.ly / 2 / 100
|
||||||
body_state.zm = command.lx / 2 / 100;
|
body_state.zm = command.lx / 2 / 100
|
||||||
body_state.feet = this.default_feet_pos;
|
body_state.feet = this.default_feet_pos
|
||||||
return body_state;
|
return body_state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class PhaseGaitState extends GaitState {
|
abstract class PhaseGaitState extends GaitState {
|
||||||
protected tick = 0;
|
protected tick = 0
|
||||||
protected phase = 0;
|
protected phase = 0
|
||||||
protected phase_time = 0;
|
protected phase_time = 0
|
||||||
protected abstract num_phases: number;
|
protected abstract num_phases: number
|
||||||
protected abstract phase_speed_factor: number;
|
protected abstract phase_speed_factor: number
|
||||||
protected abstract swing_stand_ratio: number;
|
protected abstract swing_stand_ratio: number
|
||||||
|
|
||||||
protected contact_phases!: number[][];
|
protected contact_phases!: number[][]
|
||||||
protected shifts!: number[][];
|
protected shifts!: number[][]
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
super.step(body_state, command, dt);
|
super.step(body_state, command, dt)
|
||||||
this.update_phase();
|
this.update_phase()
|
||||||
this.update_body_position();
|
this.update_body_position()
|
||||||
this.update_feet_positions();
|
this.update_feet_positions()
|
||||||
return this.body_state;
|
return this.body_state
|
||||||
}
|
}
|
||||||
|
|
||||||
update_phase() {
|
update_phase() {
|
||||||
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity;
|
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity
|
||||||
|
|
||||||
if (this.phase_time >= 1) {
|
if (this.phase_time >= 1) {
|
||||||
this.phase += 1;
|
this.phase += 1
|
||||||
if (this.phase == this.num_phases) this.phase = 0;
|
if (this.phase == this.num_phases) this.phase = 0
|
||||||
this.phase_time = 0;
|
this.phase_time = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_body_position() {
|
update_body_position() {
|
||||||
if (this.num_phases === 4) return;
|
if (this.num_phases === 4) return
|
||||||
|
|
||||||
const shift = this.shifts[Math.floor(this.phase / 2)];
|
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.xm += (shift[0] - this.body_state.xm) * this.dt * 4
|
||||||
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4;
|
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4
|
||||||
}
|
}
|
||||||
|
|
||||||
update_feet_positions() {
|
update_feet_positions() {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.body_state.feet[i] = this.update_foot_position(i);
|
this.body_state.feet[i] = this.update_foot_position(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_foot_position(index: number): number[] {
|
update_foot_position(index: number): number[] {
|
||||||
const contact = this.contact_phases[index][this.phase];
|
const contact = this.contact_phases[index][this.phase]
|
||||||
return contact ? this.stand(index) : this.swing(index);
|
return contact ? this.stand(index) : this.swing(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
stand(index: number): number[] {
|
stand(index: number): number[] {
|
||||||
@@ -179,154 +175,153 @@ abstract class PhaseGaitState extends GaitState {
|
|||||||
-this.gait_state.step_x * this.dt * this.swing_stand_ratio,
|
-this.gait_state.step_x * this.dt * this.swing_stand_ratio,
|
||||||
0,
|
0,
|
||||||
-this.gait_state.step_z * this.dt * this.swing_stand_ratio
|
-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][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][1] = this.default_feet_pos[index][1]
|
||||||
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
|
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2]
|
||||||
return this.body_state.feet[index];
|
return this.body_state.feet[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
swing(index: number): number[] {
|
swing(index: number): number[] {
|
||||||
const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt];
|
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) {
|
if (this.gait_state.step_x == 0) {
|
||||||
delta_pos[0] =
|
delta_pos[0] =
|
||||||
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8;
|
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.gait_state.step_z == 0) {
|
if (this.gait_state.step_z == 0) {
|
||||||
delta_pos[2] =
|
delta_pos[2] =
|
||||||
(this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8;
|
(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][0] = this.body_state.feet[index][0] + delta_pos[0]
|
||||||
this.body_state.feet[index][1] =
|
this.body_state.feet[index][1] =
|
||||||
this.default_feet_pos[index][1] +
|
this.default_feet_pos[index][1] + sin(this.phase_time * Math.PI) * this.gait_state.step_height
|
||||||
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]
|
||||||
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
|
return this.body_state.feet[index]
|
||||||
return this.body_state.feet[index];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FourPhaseWalkState extends PhaseGaitState {
|
export class FourPhaseWalkState extends PhaseGaitState {
|
||||||
protected name = 'Four phase walk';
|
protected name = 'Four phase walk'
|
||||||
protected num_phases = 4;
|
protected num_phases = 4
|
||||||
protected phase_speed_factor = 6;
|
protected phase_speed_factor = 6
|
||||||
protected contact_phases = [
|
protected contact_phases = [
|
||||||
[1, 0, 1, 1],
|
[1, 0, 1, 1],
|
||||||
[1, 1, 1, 0],
|
[1, 1, 1, 0],
|
||||||
[1, 1, 1, 0],
|
[1, 1, 1, 0],
|
||||||
[1, 0, 1, 1]
|
[1, 0, 1, 1]
|
||||||
];
|
]
|
||||||
protected swing_stand_ratio = 1 / (this.num_phases - 1);
|
protected swing_stand_ratio = 1 / (this.num_phases - 1)
|
||||||
|
|
||||||
begin() {
|
begin() {
|
||||||
super.begin();
|
super.begin()
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
super.end();
|
super.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
return super.step(body_state, command, dt);
|
return super.step(body_state, command, dt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EightPhaseWalkState extends PhaseGaitState {
|
export class EightPhaseWalkState extends PhaseGaitState {
|
||||||
protected name = 'Eight phase walk';
|
protected name = 'Eight phase walk'
|
||||||
protected num_phases = 8;
|
protected num_phases = 8
|
||||||
protected phase_speed_factor = 4;
|
protected phase_speed_factor = 4
|
||||||
protected contact_phases = [
|
protected contact_phases = [
|
||||||
[1, 0, 1, 1, 1, 1, 1, 1],
|
[1, 0, 1, 1, 1, 1, 1, 1],
|
||||||
[1, 1, 1, 1, 1, 0, 1, 1],
|
[1, 1, 1, 1, 1, 0, 1, 1],
|
||||||
[1, 1, 1, 1, 1, 1, 1, 0],
|
[1, 1, 1, 1, 1, 1, 1, 0],
|
||||||
[1, 1, 1, 0, 1, 1, 1, 1]
|
[1, 1, 1, 0, 1, 1, 1, 1]
|
||||||
];
|
]
|
||||||
protected shifts = [
|
protected shifts = [
|
||||||
[-0.05, 0, -0.2],
|
[-0.05, 0, -0.2],
|
||||||
[0.3, 0, 0.2],
|
[0.3, 0, 0.2],
|
||||||
[-0.05, 0, 0.2],
|
[-0.05, 0, 0.2],
|
||||||
[0.3, 0, -0.2]
|
[0.3, 0, -0.2]
|
||||||
];
|
]
|
||||||
protected swing_stand_ratio = 1 / (this.num_phases - 1);
|
protected swing_stand_ratio = 1 / (this.num_phases - 1)
|
||||||
|
|
||||||
begin() {
|
begin() {
|
||||||
super.begin();
|
super.begin()
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
super.end();
|
super.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
return super.step(body_state, command, dt);
|
return super.step(body_state, command, dt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BezierState extends GaitState {
|
export class BezierState extends GaitState {
|
||||||
protected name = 'Bezier';
|
protected name = 'Bezier'
|
||||||
protected phase = 0;
|
protected phase = 0
|
||||||
protected phase_num = 0;
|
protected phase_num = 0
|
||||||
protected step_length: number = 0;
|
protected step_length: number = 0
|
||||||
offset = [0, 0.5, 0.5, 0];
|
offset = [0, 0.5, 0.5, 0]
|
||||||
|
|
||||||
begin() {
|
begin() {
|
||||||
super.begin();
|
super.begin()
|
||||||
}
|
}
|
||||||
|
|
||||||
end() {
|
end() {
|
||||||
super.end();
|
super.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||||
super.step(body_state, command, dt);
|
super.step(body_state, command, dt)
|
||||||
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2);
|
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2)
|
||||||
if (this.gait_state.step_x < 0) {
|
if (this.gait_state.step_x < 0) {
|
||||||
this.step_length = -this.step_length;
|
this.step_length = -this.step_length
|
||||||
}
|
}
|
||||||
this.update_phase();
|
this.update_phase()
|
||||||
this.update_feet_positions();
|
this.update_feet_positions()
|
||||||
return this.body_state;
|
return this.body_state
|
||||||
}
|
}
|
||||||
|
|
||||||
update_phase() {
|
update_phase() {
|
||||||
this.phase += this.dt * this.gait_state.step_velocity * 2;
|
this.phase += this.dt * this.gait_state.step_velocity * 2
|
||||||
if (this.phase >= 1) {
|
if (this.phase >= 1) {
|
||||||
this.phase_num += 1;
|
this.phase_num += 1
|
||||||
this.phase_num %= 2;
|
this.phase_num %= 2
|
||||||
this.phase = 0;
|
this.phase = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_feet_positions() {
|
update_feet_positions() {
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < 4; i++) {
|
||||||
this.body_state.feet[i] = this.update_foot_position(i);
|
this.body_state.feet[i] = this.update_foot_position(i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_foot_position(index: number): number[] {
|
update_foot_position(index: number): number[] {
|
||||||
let phase = this.phase + this.offset[index];
|
let phase = this.phase + this.offset[index]
|
||||||
if (phase >= 1) {
|
if (phase >= 1) {
|
||||||
phase -= 1;
|
phase -= 1
|
||||||
}
|
}
|
||||||
this.body_state.feet[index][0] = this.default_feet_pos[index][0];
|
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][1] = this.default_feet_pos[index][1]
|
||||||
this.body_state.feet[index][2] = this.default_feet_pos[index][2];
|
this.body_state.feet[index][2] = this.default_feet_pos[index][2]
|
||||||
return phase <= 0.75 ?
|
return phase <= 0.75 ?
|
||||||
this.stand_controller(index, phase / 0.75)
|
this.stand_controller(index, phase / 0.75)
|
||||||
: this.swing_controller(index, (phase - 0.75) / (1 - 0.75));
|
: this.swing_controller(index, (phase - 0.75) / (1 - 0.75))
|
||||||
}
|
}
|
||||||
|
|
||||||
stand_controller(index: number, phase: number) {
|
stand_controller(index: number, phase: number) {
|
||||||
let depth = this.gait_state.step_depth;
|
let depth = this.gait_state.step_depth
|
||||||
return this.controller(index, phase, stance_curve, depth);
|
return this.controller(index, phase, stance_curve, depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
swing_controller(index: number, phase: number) {
|
swing_controller(index: number, phase: number) {
|
||||||
let height = this.gait_state.step_height;
|
let height = this.gait_state.step_height
|
||||||
return this.controller(index, phase, bezier_curve, height);
|
return this.controller(index, phase, bezier_curve, height)
|
||||||
}
|
}
|
||||||
|
|
||||||
controller(
|
controller(
|
||||||
@@ -335,69 +330,69 @@ export class BezierState extends GaitState {
|
|||||||
controller: (length: number, angle: number, ...args: number[]) => number[],
|
controller: (length: number, angle: number, ...args: number[]) => number[],
|
||||||
...args: number[]
|
...args: number[]
|
||||||
) {
|
) {
|
||||||
let length = this.step_length / 2;
|
let length = this.step_length / 2
|
||||||
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2;
|
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2
|
||||||
const delta_pos = controller(length, angle, ...args, phase);
|
const delta_pos = controller(length, angle, ...args, phase)
|
||||||
|
|
||||||
length = this.gait_state.step_angle * 2;
|
length = this.gait_state.step_angle * 2
|
||||||
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index]);
|
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index])
|
||||||
|
|
||||||
const delta_rot = controller(length, angle, ...args, phase);
|
const delta_rot = controller(length, angle, ...args, phase)
|
||||||
|
|
||||||
this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2;
|
this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2
|
||||||
this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2;
|
this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2
|
||||||
if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle)
|
if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle)
|
||||||
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2;
|
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2
|
||||||
|
|
||||||
return this.body_state.feet[index];
|
return this.body_state.feet[index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
|
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
|
||||||
const X_POLAR = Math.cos(angle);
|
const X_POLAR = Math.cos(angle)
|
||||||
const Y_POLAR = Math.sin(angle);
|
const Y_POLAR = Math.sin(angle)
|
||||||
|
|
||||||
const step = length * (1 - 2 * phase);
|
const step = length * (1 - 2 * phase)
|
||||||
const X = step * X_POLAR;
|
const X = step * X_POLAR
|
||||||
const Z = step * Y_POLAR;
|
const Z = step * Y_POLAR
|
||||||
let Y = 0;
|
let Y = 0
|
||||||
|
|
||||||
if (length !== 0) {
|
if (length !== 0) {
|
||||||
Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length));
|
Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length))
|
||||||
|
}
|
||||||
|
return [X, Y, Z]
|
||||||
}
|
}
|
||||||
return [X, Y, Z];
|
|
||||||
};
|
|
||||||
|
|
||||||
const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => {
|
const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => {
|
||||||
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2);
|
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2)
|
||||||
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0]);
|
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0])
|
||||||
const offsets = [
|
const offsets = [
|
||||||
current_foot_pos[0] - default_foot_pos[0],
|
current_foot_pos[0] - default_foot_pos[0],
|
||||||
current_foot_pos[2] - default_foot_pos[2],
|
current_foot_pos[2] - default_foot_pos[2],
|
||||||
current_foot_pos[1] - default_foot_pos[1]
|
current_foot_pos[1] - default_foot_pos[1]
|
||||||
];
|
]
|
||||||
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2);
|
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2)
|
||||||
const offset_mod = Math.atan2(offset_mag, foot_mag);
|
const offset_mod = Math.atan2(offset_mag, foot_mag)
|
||||||
|
|
||||||
return Math.PI / 2.0 + foot_dir + offset_mod;
|
return Math.PI / 2.0 + foot_dir + offset_mod
|
||||||
};
|
}
|
||||||
|
|
||||||
const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => {
|
const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => {
|
||||||
const control_points = get_control_points(length, angle, height);
|
const control_points = get_control_points(length, angle, height)
|
||||||
const n = control_points.length - 1;
|
const n = control_points.length - 1
|
||||||
|
|
||||||
const point = [0, 0, 0];
|
const point = [0, 0, 0]
|
||||||
for (let i = 0; i <= n; i++) {
|
for (let i = 0; i <= n; i++) {
|
||||||
const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i);
|
const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i)
|
||||||
point[0] += bernstein_poly * control_points[i][0];
|
point[0] += bernstein_poly * control_points[i][0]
|
||||||
point[1] += bernstein_poly * control_points[i][1];
|
point[1] += bernstein_poly * control_points[i][1]
|
||||||
point[2] += bernstein_poly * control_points[i][2];
|
point[2] += bernstein_poly * control_points[i][2]
|
||||||
|
}
|
||||||
|
return point
|
||||||
}
|
}
|
||||||
return point;
|
|
||||||
};
|
|
||||||
const get_control_points = (length: number, angle: number, height: number): number[][] => {
|
const get_control_points = (length: number, angle: number, height: number): number[][] => {
|
||||||
const X_POLAR = Math.cos(angle);
|
const X_POLAR = Math.cos(angle)
|
||||||
const Z_POLAR = Math.sin(angle);
|
const Z_POLAR = Math.sin(angle)
|
||||||
|
|
||||||
const STEP = [
|
const STEP = [
|
||||||
-length,
|
-length,
|
||||||
@@ -412,7 +407,7 @@ const get_control_points = (length: number, angle: number, height: number): numb
|
|||||||
length * 1.5,
|
length * 1.5,
|
||||||
length * 1.4,
|
length * 1.4,
|
||||||
length
|
length
|
||||||
];
|
]
|
||||||
|
|
||||||
const Y = [
|
const Y = [
|
||||||
0.0,
|
0.0,
|
||||||
@@ -427,26 +422,26 @@ const get_control_points = (length: number, angle: number, height: number): numb
|
|||||||
height * 1.1,
|
height * 1.1,
|
||||||
0.0,
|
0.0,
|
||||||
0.0
|
0.0
|
||||||
];
|
]
|
||||||
|
|
||||||
const control_points: number[][] = [];
|
const control_points: number[][] = []
|
||||||
|
|
||||||
for (let i = 0; i < STEP.length; i++) {
|
for (let i = 0; i < STEP.length; i++) {
|
||||||
const X = STEP[i] * X_POLAR;
|
const X = STEP[i] * X_POLAR
|
||||||
const Z = STEP[i] * Z_POLAR;
|
const Z = STEP[i] * Z_POLAR
|
||||||
control_points.push([X, Y[i], Z]);
|
control_points.push([X, Y[i], Z])
|
||||||
}
|
}
|
||||||
|
|
||||||
return control_points;
|
return control_points
|
||||||
};
|
}
|
||||||
|
|
||||||
const comb = (n: number, k: number): number => {
|
const comb = (n: number, k: number): number => {
|
||||||
if (k < 0 || k > n) return 0;
|
if (k < 0 || k > n) return 0
|
||||||
if (k === 0 || k === n) return 1;
|
if (k === 0 || k === n) return 1
|
||||||
k = Math.min(k, n - k);
|
k = Math.min(k, n - k)
|
||||||
let c = 1;
|
let c = 1
|
||||||
for (let i = 0; i < k; i++) {
|
for (let i = 0; i < k; i++) {
|
||||||
c = (c * (n - i)) / (i + 1);
|
c = (c * (n - i)) / (i + 1)
|
||||||
|
}
|
||||||
|
return c
|
||||||
}
|
}
|
||||||
return c;
|
|
||||||
};
|
|
||||||
|
|||||||
+105
-294
@@ -1,320 +1,131 @@
|
|||||||
|
|
||||||
export interface body_state_t {
|
export interface body_state_t {
|
||||||
omega: number;
|
omega: number
|
||||||
phi: number;
|
phi: number
|
||||||
psi: number;
|
psi: number
|
||||||
xm: number;
|
xm: number
|
||||||
ym: number;
|
ym: number
|
||||||
zm: number;
|
zm: number
|
||||||
feet: number[][];
|
feet: number[][]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface position {
|
export interface position {
|
||||||
x: number;
|
x: number
|
||||||
y: number;
|
y: number
|
||||||
z: number;
|
z: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface target_position {
|
export interface target_position {
|
||||||
x: number;
|
x: number
|
||||||
z: number;
|
z: number
|
||||||
yaw: number;
|
yaw: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const { cos, sin, atan2, sqrt } = Math;
|
const { cos, sin, atan2, acos, sqrt, max, min } = Math
|
||||||
|
|
||||||
const DEG2RAD = 0.017453292519943;
|
const DEG2RAD = 0.017453292519943
|
||||||
|
|
||||||
export default class Kinematic {
|
export default class Kinematic {
|
||||||
l1: number;
|
l1: number
|
||||||
l2: number;
|
l2: number
|
||||||
l3: number;
|
l3: number
|
||||||
l4: number;
|
l4: number
|
||||||
|
|
||||||
L: number;
|
L: number
|
||||||
W: number;
|
W: number
|
||||||
|
|
||||||
DEG2RAD = DEG2RAD;
|
DEG2RAD = DEG2RAD
|
||||||
|
|
||||||
sHp = sin(Math.PI / 2);
|
mountOffsets: number[][]
|
||||||
cHp = cos(Math.PI / 2);
|
|
||||||
|
|
||||||
Tlf: number[][] = [];
|
invMountRot = [
|
||||||
Trf: number[][] = [];
|
[0, 0, -1],
|
||||||
Tlb: number[][] = [];
|
[0, 1, 0],
|
||||||
Trb: number[][] = [];
|
[1, 0, 0]
|
||||||
|
]
|
||||||
point_lf: number[][];
|
|
||||||
point_rf: number[][];
|
|
||||||
point_lb: number[][];
|
|
||||||
point_rb: number[][];
|
|
||||||
Ix: number[][];
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.l1 = 60.5 / 100;
|
this.l1 = 60.5 / 100
|
||||||
this.l2 = 10 / 100;
|
this.l2 = 10 / 100
|
||||||
this.l3 = 100.7 / 100;
|
this.l3 = 111.7 / 100
|
||||||
this.l4 = 118.5 / 100;
|
this.l4 = 118.5 / 100
|
||||||
|
|
||||||
this.L = 207.5 / 100;
|
this.L = 207.5 / 100
|
||||||
this.W = 78 / 100;
|
this.W = 78 / 100
|
||||||
|
|
||||||
this.point_lf = [
|
this.mountOffsets = [
|
||||||
[this.cHp, 0, this.sHp, this.L / 2],
|
[this.L / 2, 0, this.W / 2],
|
||||||
[0, 1, 0, 0],
|
[this.L / 2, 0, -this.W / 2],
|
||||||
[-this.sHp, 0, this.cHp, this.W / 2],
|
[-this.L / 2, 0, this.W / 2],
|
||||||
[0, 0, 0, 1]
|
[-this.L / 2, 0, -this.W / 2]
|
||||||
];
|
]
|
||||||
|
|
||||||
this.point_rf = [
|
|
||||||
[this.cHp, 0, this.sHp, this.L / 2],
|
|
||||||
[0, 1, 0, 0],
|
|
||||||
[-this.sHp, 0, this.cHp, -this.W / 2],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
];
|
|
||||||
|
|
||||||
this.point_lb = [
|
|
||||||
[this.cHp, 0, this.sHp, -this.L / 2],
|
|
||||||
[0, 1, 0, 0],
|
|
||||||
[-this.sHp, 0, this.cHp, this.W / 2],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
];
|
|
||||||
|
|
||||||
this.point_rb = [
|
|
||||||
[this.cHp, 0, this.sHp, -this.L / 2],
|
|
||||||
[0, 1, 0, 0],
|
|
||||||
[-this.sHp, 0, this.cHp, -this.W / 2],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
];
|
|
||||||
this.Ix = [
|
|
||||||
[-1, 0, 0, 0],
|
|
||||||
[0, 1, 0, 0],
|
|
||||||
[0, 0, 1, 0],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public calcIK(body_state: body_state_t): number[] {
|
getDefaultFeetPos(): number[][] {
|
||||||
this.bodyIK(body_state);
|
return this.mountOffsets.map((offset, i) => {
|
||||||
|
return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.l1 : this.l1)]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
calcIK(p: body_state_t): number[] {
|
||||||
|
const roll = p.omega * this.DEG2RAD
|
||||||
|
const pitch = p.phi * this.DEG2RAD
|
||||||
|
const yaw = p.psi * this.DEG2RAD
|
||||||
|
const rot = this.euler2R(roll, pitch, yaw)
|
||||||
|
const inv_rot = [
|
||||||
|
[rot[0][0], rot[1][0], rot[2][0]],
|
||||||
|
[rot[0][1], rot[1][1], rot[2][1]],
|
||||||
|
[rot[0][2], rot[1][2], rot[2][2]]
|
||||||
|
]
|
||||||
|
const inv_trans = [
|
||||||
|
-inv_rot[0][0] * p.xm - inv_rot[0][1] * p.ym - inv_rot[0][2] * p.zm,
|
||||||
|
-inv_rot[1][0] * p.xm - inv_rot[1][1] * p.ym - inv_rot[1][2] * p.zm,
|
||||||
|
-inv_rot[2][0] * p.xm - inv_rot[2][1] * p.ym - inv_rot[2][2] * p.zm
|
||||||
|
]
|
||||||
|
return p.feet.flatMap((foot, i) => {
|
||||||
|
const [wx, wy, wz] = foot
|
||||||
|
const bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0]
|
||||||
|
const by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1]
|
||||||
|
const bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2]
|
||||||
|
|
||||||
|
const [mx, my, mz] = this.mountOffsets[i]
|
||||||
|
const px = bx - mx,
|
||||||
|
py = by - my,
|
||||||
|
pz = bz - mz
|
||||||
|
|
||||||
|
const lx =
|
||||||
|
this.invMountRot[0][0] * px + this.invMountRot[0][1] * py + this.invMountRot[0][2] * pz
|
||||||
|
const ly =
|
||||||
|
this.invMountRot[1][0] * px + this.invMountRot[1][1] * py + this.invMountRot[1][2] * pz
|
||||||
|
const lz =
|
||||||
|
this.invMountRot[2][0] * px + this.invMountRot[2][1] * py + this.invMountRot[2][2] * pz
|
||||||
|
|
||||||
|
const xLocal = i % 2 === 1 ? -lx : lx
|
||||||
|
return this.legIK(xLocal, ly, lz)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private legIK(x: number, y: number, z: number): [number, number, number] {
|
||||||
|
const F = sqrt(max(0, x * x + y * y - this.l1 * this.l1))
|
||||||
|
const G = F - this.l2
|
||||||
|
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 t3 = acos(max(-1, min(1, D)))
|
||||||
|
const t2 = atan2(z, G) - atan2(this.l4 * sin(t3), this.l3 + this.l4 * cos(t3))
|
||||||
|
return [t1, t2, t3]
|
||||||
|
}
|
||||||
|
|
||||||
|
private euler2R(roll: number, pitch: number, yaw: number): number[][] {
|
||||||
|
const cr = cos(roll),
|
||||||
|
sr = sin(roll)
|
||||||
|
const cp = cos(pitch),
|
||||||
|
sp = sin(pitch)
|
||||||
|
const cy = cos(yaw),
|
||||||
|
sy = sin(yaw)
|
||||||
return [
|
return [
|
||||||
...this.legIK(this.multiplyVector(this.inverse(this.Tlf), body_state.feet[0])),
|
[cp * cy, -cp * sy, sp],
|
||||||
...this.legIK(
|
[sr * sp * cy + sy * cr, -sr * sp * sy + cr * cy, -sr * cp],
|
||||||
this.multiplyVector(
|
[sr * sy - sp * cr * cy, sr * cy + sp * sy * cr, cr * cp]
|
||||||
this.Ix,
|
]
|
||||||
this.multiplyVector(this.inverse(this.Trf), body_state.feet[1])
|
|
||||||
)
|
|
||||||
),
|
|
||||||
...this.legIK(this.multiplyVector(this.inverse(this.Tlb), body_state.feet[2])),
|
|
||||||
...this.legIK(
|
|
||||||
this.multiplyVector(
|
|
||||||
this.Ix,
|
|
||||||
this.multiplyVector(this.inverse(this.Trb), body_state.feet[3])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyIK(p: body_state_t) {
|
|
||||||
const cos_omega = cos(p.omega * this.DEG2RAD);
|
|
||||||
const sin_omega = sin(p.omega * this.DEG2RAD);
|
|
||||||
const cos_phi = cos(p.phi * this.DEG2RAD);
|
|
||||||
const sin_phi = sin(p.phi * this.DEG2RAD);
|
|
||||||
const cos_psi = cos(p.psi * this.DEG2RAD);
|
|
||||||
const sin_psi = sin(p.psi * this.DEG2RAD);
|
|
||||||
|
|
||||||
const Tm: number[][] = [
|
|
||||||
[cos_phi * cos_psi, -sin_psi * cos_phi, sin_phi, p.xm],
|
|
||||||
[
|
|
||||||
sin_omega * sin_phi * cos_psi + sin_psi * cos_omega,
|
|
||||||
-sin_omega * sin_phi * sin_psi + cos_omega * cos_psi,
|
|
||||||
-sin_omega * cos_phi,
|
|
||||||
p.ym
|
|
||||||
],
|
|
||||||
[
|
|
||||||
sin_omega * sin_psi - sin_phi * cos_omega * cos_psi,
|
|
||||||
sin_omega * cos_psi + sin_phi * sin_psi * cos_omega,
|
|
||||||
cos_omega * cos_phi,
|
|
||||||
p.zm
|
|
||||||
],
|
|
||||||
[0, 0, 0, 1]
|
|
||||||
];
|
|
||||||
|
|
||||||
this.Tlf = this.matrixMultiply(Tm, this.point_lf);
|
|
||||||
this.Trf = this.matrixMultiply(Tm, this.point_rf);
|
|
||||||
this.Tlb = this.matrixMultiply(Tm, this.point_lb);
|
|
||||||
this.Trb = this.matrixMultiply(Tm, this.point_rb);
|
|
||||||
}
|
|
||||||
|
|
||||||
public legIK(point: number[]): number[] {
|
|
||||||
const [x, y, z] = point;
|
|
||||||
|
|
||||||
let F = sqrt(x ** 2 + y ** 2 - this.l1 ** 2);
|
|
||||||
if (isNaN(F)) F = this.l1;
|
|
||||||
|
|
||||||
const G = F - this.l2;
|
|
||||||
const H = sqrt(G ** 2 + z ** 2);
|
|
||||||
|
|
||||||
const theta1 = -atan2(y, x) - atan2(F, -this.l1);
|
|
||||||
const D = (H ** 2 - this.l3 ** 2 - this.l4 ** 2) / (2 * this.l3 * this.l4);
|
|
||||||
let theta3 = atan2(sqrt(1 - D ** 2), D);
|
|
||||||
if (isNaN(theta3)) theta3 = 0;
|
|
||||||
|
|
||||||
const theta2 = atan2(z, G) - atan2(this.l4 * sin(theta3), this.l3 + this.l4 * cos(theta3));
|
|
||||||
|
|
||||||
return [theta1, theta2, theta3];
|
|
||||||
}
|
|
||||||
|
|
||||||
matrixMultiply(a: number[][], b: number[][]): number[][] {
|
|
||||||
const result: number[][] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < a.length; i++) {
|
|
||||||
const row: number[] = [];
|
|
||||||
|
|
||||||
for (let j = 0; j < b[0].length; j++) {
|
|
||||||
let sum = 0;
|
|
||||||
|
|
||||||
for (let k = 0; k < a[i].length; k++) {
|
|
||||||
sum += a[i][k] * b[k][j];
|
|
||||||
}
|
|
||||||
|
|
||||||
row.push(sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
multiplyVector(matrix: number[][], vector: number[]): number[] {
|
|
||||||
const rows = matrix.length;
|
|
||||||
const cols = matrix[0].length;
|
|
||||||
const vectorLength = vector.length;
|
|
||||||
|
|
||||||
if (cols !== vectorLength) {
|
|
||||||
throw new Error('Matrix and vector dimensions do not match for multiplication.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < rows; i++) {
|
|
||||||
let sum = 0;
|
|
||||||
|
|
||||||
for (let j = 0; j < cols; j++) {
|
|
||||||
sum += matrix[i][j] * vector[j];
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private inverse(matrix: number[][]): number[][] {
|
|
||||||
const det = this.determinant(matrix);
|
|
||||||
const adjugate = this.adjugate(matrix);
|
|
||||||
const scalar = 1 / det;
|
|
||||||
const inverse: number[][] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < matrix.length; i++) {
|
|
||||||
const row: number[] = [];
|
|
||||||
|
|
||||||
for (let j = 0; j < matrix[i].length; j++) {
|
|
||||||
row.push(adjugate[i][j] * scalar);
|
|
||||||
}
|
|
||||||
|
|
||||||
inverse.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return inverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
private determinant(matrix: number[][]): number {
|
|
||||||
if (matrix.length !== matrix[0].length) {
|
|
||||||
throw new Error('The matrix is not square.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matrix.length === 2) {
|
|
||||||
return matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0];
|
|
||||||
}
|
|
||||||
|
|
||||||
let det = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < matrix.length; i++) {
|
|
||||||
const sign = i % 2 === 0 ? 1 : -1;
|
|
||||||
const subMatrix: number[][] = [];
|
|
||||||
|
|
||||||
for (let j = 1; j < matrix.length; j++) {
|
|
||||||
const row: number[] = [];
|
|
||||||
|
|
||||||
for (let k = 0; k < matrix.length; k++) {
|
|
||||||
if (k !== i) {
|
|
||||||
row.push(matrix[j][k]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subMatrix.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
det += sign * matrix[0][i] * this.determinant(subMatrix);
|
|
||||||
}
|
|
||||||
|
|
||||||
return det;
|
|
||||||
}
|
|
||||||
|
|
||||||
private adjugate(matrix: number[][]): number[][] {
|
|
||||||
if (matrix.length !== matrix[0].length) {
|
|
||||||
throw new Error('The matrix is not square.');
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjugate: number[][] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < matrix.length; i++) {
|
|
||||||
const row: number[] = [];
|
|
||||||
|
|
||||||
for (let j = 0; j < matrix[i].length; j++) {
|
|
||||||
const sign = (i + j) % 2 === 0 ? 1 : -1;
|
|
||||||
const subMatrix: number[][] = [];
|
|
||||||
|
|
||||||
for (let k = 0; k < matrix.length; k++) {
|
|
||||||
if (k !== i) {
|
|
||||||
const subRow: number[] = [];
|
|
||||||
|
|
||||||
for (let l = 0; l < matrix.length; l++) {
|
|
||||||
if (l !== j) {
|
|
||||||
subRow.push(matrix[k][l]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subMatrix.push(subRow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const cofactor = sign * this.determinant(subMatrix);
|
|
||||||
row.push(cofactor);
|
|
||||||
}
|
|
||||||
|
|
||||||
adjugate.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.transpose(adjugate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private transpose(matrix: number[][]): number[][] {
|
|
||||||
const transposed: number[][] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < matrix.length; i++) {
|
|
||||||
const row: number[] = [];
|
|
||||||
|
|
||||||
for (let j = 0; j < matrix[i].length; j++) {
|
|
||||||
row.push(matrix[j][i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
transposed.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return transposed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { readable, derived } from 'svelte/store'
|
||||||
|
|
||||||
|
export type GamepadState = {
|
||||||
|
available: boolean
|
||||||
|
gamepads: Gamepad[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
|
||||||
|
const update = () => {
|
||||||
|
const hasGamepadAPI = 'getGamepads' in navigator
|
||||||
|
if (!hasGamepadAPI) {
|
||||||
|
set({ available: false, gamepads: [] })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const gps = navigator.getGamepads?.() ?? []
|
||||||
|
const validGamepads = gps.filter(Boolean) as Gamepad[]
|
||||||
|
set({
|
||||||
|
available: true,
|
||||||
|
gamepads: validGamepads
|
||||||
|
})
|
||||||
|
raf = requestAnimationFrame(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('gamepadconnected', update)
|
||||||
|
window.addEventListener('gamepaddisconnected', update)
|
||||||
|
let raf = requestAnimationFrame(update)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(raf)
|
||||||
|
window.removeEventListener('gamepadconnected', update)
|
||||||
|
window.removeEventListener('gamepaddisconnected', update)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const gamepad = derived(gamepads, $gamepads =>
|
||||||
|
$gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null
|
||||||
|
)
|
||||||
|
|
||||||
|
export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0])
|
||||||
|
|
||||||
|
export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? [])
|
||||||
|
|
||||||
|
export const hasGamepad = derived(
|
||||||
|
gamepads,
|
||||||
|
$gamepads => $gamepads.available && $gamepads.gamepads.length > 0
|
||||||
|
)
|
||||||
@@ -1,122 +1,132 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
|
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
|
||||||
type SocketEvent = (typeof socketEvents)[number];
|
type SocketEvent = (typeof socketEvents)[number]
|
||||||
|
|
||||||
|
export enum Topics {
|
||||||
|
imu = 0,
|
||||||
|
mode = 1,
|
||||||
|
command = 2,
|
||||||
|
servo = 3,
|
||||||
|
input = 4,
|
||||||
|
angles = 5,
|
||||||
|
position = 6
|
||||||
|
}
|
||||||
|
|
||||||
function createWebSocket() {
|
function createWebSocket() {
|
||||||
let listeners = new Map<string, Set<(data?: unknown) => void>>();
|
let listeners = new Map<string | Topics, Set<(data?: unknown) => void>>()
|
||||||
const { subscribe, set } = writable(false);
|
const { subscribe, set } = writable(false)
|
||||||
const reconnectTimeoutTime = 5000;
|
const reconnectTimeoutTime = 5000
|
||||||
let unresponsiveTimeoutId: number;
|
let unresponsiveTimeoutId: number
|
||||||
let reconnectTimeoutId: number;
|
let reconnectTimeoutId: number
|
||||||
let ws: WebSocket;
|
let ws: WebSocket
|
||||||
let socketUrl: string | URL;
|
let socketUrl: string | URL
|
||||||
|
|
||||||
function init(url: string | URL) {
|
function init(url: string | URL) {
|
||||||
socketUrl = url;
|
socketUrl = url
|
||||||
connect();
|
connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
function disconnect(reason: SocketEvent, event?: Event) {
|
function disconnect(reason: SocketEvent, event?: Event) {
|
||||||
ws.close();
|
ws.close()
|
||||||
set(false);
|
set(false)
|
||||||
clearTimeout(unresponsiveTimeoutId);
|
clearTimeout(unresponsiveTimeoutId)
|
||||||
clearTimeout(reconnectTimeoutId);
|
clearTimeout(reconnectTimeoutId)
|
||||||
listeners.get(reason)?.forEach((listener) => listener(event));
|
listeners.get(reason)?.forEach(listener => listener(event))
|
||||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
|
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
ws = new WebSocket(socketUrl);
|
ws = new WebSocket(socketUrl)
|
||||||
ws.onopen = (ev) => {
|
ws.onopen = ev => {
|
||||||
set(true);
|
set(true)
|
||||||
clearTimeout(reconnectTimeoutId);
|
clearTimeout(reconnectTimeoutId)
|
||||||
listeners.get('open')?.forEach((listener) => listener(ev));
|
listeners.get('open')?.forEach(listener => listener(ev))
|
||||||
for (const event of listeners.keys()) {
|
for (const event of listeners.keys()) {
|
||||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
if (socketEvents.includes(event as SocketEvent)) continue
|
||||||
subscribeToEvent(event);
|
subscribeToEvent(event as unknown as Topics)
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ws.onmessage = (message) => {
|
ws.onmessage = message => {
|
||||||
resetUnresponsiveCheck();
|
resetUnresponsiveCheck()
|
||||||
let data = message.data;
|
let data = message.data
|
||||||
if (data instanceof ArrayBuffer) {
|
if (data instanceof ArrayBuffer) {
|
||||||
listeners.get('binary')?.forEach((listener) => listener(data));
|
listeners.get('binary')?.forEach(listener => listener(data))
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
data = data.substring(1);
|
data = data.substring(1)
|
||||||
|
|
||||||
if (!data) return;
|
if (!data) return
|
||||||
|
|
||||||
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
|
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['))
|
||||||
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
|
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
payload = JSON.parse(payload);
|
payload = JSON.parse(payload)
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
|
if (event) listeners.get(event)?.forEach(listener => listener(payload))
|
||||||
};
|
}
|
||||||
ws.onerror = (ev) => disconnect('error', ev);
|
ws.onerror = ev => disconnect('error', ev)
|
||||||
ws.onclose = (ev) => disconnect('close', ev);
|
ws.onclose = ev => disconnect('close', ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribe(event: string, listener?: (data: any) => void) {
|
function unsubscribe(event: Topics, listener?: (data: any) => void) {
|
||||||
let eventListeners = listeners.get(event);
|
let eventListeners = listeners.get(event)
|
||||||
if (!eventListeners) return;
|
if (!eventListeners) return
|
||||||
|
|
||||||
if (!eventListeners.size) {
|
if (!eventListeners.size) {
|
||||||
unsubscribeToEvent(event);
|
unsubscribeToEvent(event)
|
||||||
}
|
}
|
||||||
if (listener) {
|
if (listener) {
|
||||||
eventListeners?.delete(listener);
|
eventListeners?.delete(listener)
|
||||||
} else {
|
} else {
|
||||||
listeners.delete(event);
|
listeners.delete(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetUnresponsiveCheck() {
|
function resetUnresponsiveCheck() {
|
||||||
clearTimeout(unresponsiveTimeoutId);
|
clearTimeout(unresponsiveTimeoutId)
|
||||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendEvent(event: string, data: unknown) {
|
function sendEvent(event: Topics, data: unknown) {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||||
ws.send(`2/${event}[${JSON.stringify(data)}]`);
|
ws.send(JSON.stringify([2, event, data]))
|
||||||
}
|
}
|
||||||
|
|
||||||
function unsubscribeToEvent(event: string) {
|
function unsubscribeToEvent(event: Topics) {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||||
ws.send('1/' + event);
|
ws.send(`[1,${event}]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function subscribeToEvent(event: string) {
|
function subscribeToEvent(event: Topics) {
|
||||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||||
ws.send('0/' + event);
|
ws.send(`[0,${event}]`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
sendEvent,
|
sendEvent,
|
||||||
init,
|
init,
|
||||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
on: <T>(event: Topics | SocketEvent, listener: (data: T) => void): (() => void) => {
|
||||||
let eventListeners = listeners.get(event);
|
let eventListeners = listeners.get(event)
|
||||||
if (!eventListeners) {
|
if (!eventListeners) {
|
||||||
if (!socketEvents.includes(event as SocketEvent)) {
|
if (!socketEvents.includes(event)) {
|
||||||
subscribeToEvent(event);
|
subscribeToEvent(event)
|
||||||
}
|
}
|
||||||
eventListeners = new Set();
|
eventListeners = new Set()
|
||||||
listeners.set(event, eventListeners);
|
listeners.set(event, eventListeners)
|
||||||
}
|
}
|
||||||
eventListeners.add(listener as (data: any) => void);
|
eventListeners.add(listener as (data: any) => void)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unsubscribe(event, listener);
|
unsubscribe(event, listener)
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
off: (event: string, listener?: (data: any) => void) => {
|
off: (event: Topics, listener?: (data: any) => void) => {
|
||||||
unsubscribe(event, listener);
|
unsubscribe(event, listener)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const socket = createWebSocket();
|
export const socket = createWebSocket()
|
||||||
|
|||||||
+176
-133
@@ -1,178 +1,221 @@
|
|||||||
export type vector = { x: number; y: number };
|
export type vector = { x: number; y: number }
|
||||||
|
|
||||||
export interface ControllerInput {
|
export interface ControllerInput {
|
||||||
left: vector;
|
left: vector
|
||||||
right: vector;
|
right: vector
|
||||||
height: number;
|
height: number
|
||||||
speed: number;
|
speed: number
|
||||||
s1: number;
|
s1: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GithubRelease = {
|
export type GithubRelease = {
|
||||||
message: string;
|
message: string
|
||||||
tag_name: string;
|
tag_name: string
|
||||||
assets: Array<{
|
assets: Array<{
|
||||||
name: string;
|
name: string
|
||||||
browser_download_url: string;
|
browser_download_url: string
|
||||||
}>;
|
}>
|
||||||
};
|
}
|
||||||
|
|
||||||
export type angles = number[] | Int16Array;
|
export type angles = number[] | Int16Array
|
||||||
|
|
||||||
export type WifiStatus = {
|
export type WifiStatus = {
|
||||||
status: number;
|
status: number
|
||||||
local_ip: string;
|
local_ip: string
|
||||||
mac_address: string;
|
mac_address: string
|
||||||
rssi: number;
|
rssi: number
|
||||||
ssid: string;
|
ssid: string
|
||||||
bssid: string;
|
bssid: string
|
||||||
channel: number;
|
channel: number
|
||||||
subnet_mask: string;
|
subnet_mask: string
|
||||||
gateway_ip: string;
|
gateway_ip: string
|
||||||
dns_ip_1: string;
|
dns_ip_1: string
|
||||||
dns_ip_2?: string;
|
dns_ip_2?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type WifiSettings = {
|
export type WifiSettings = {
|
||||||
hostname: string;
|
hostname: string
|
||||||
priority_RSSI: boolean;
|
priority_RSSI: boolean
|
||||||
wifi_networks: KnownNetworkItem[];
|
wifi_networks: KnownNetworkItem[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type NetworkList = {
|
export type NetworkList = {
|
||||||
networks: NetworkItem[];
|
networks: NetworkItem[]
|
||||||
};
|
}
|
||||||
|
|
||||||
export type KnownNetworkItem = {
|
export type KnownNetworkItem = {
|
||||||
ssid: string;
|
ssid: string
|
||||||
password: string;
|
password: string
|
||||||
static_ip_config: boolean;
|
static_ip_config: boolean
|
||||||
local_ip?: string;
|
local_ip?: string
|
||||||
subnet_mask?: string;
|
subnet_mask?: string
|
||||||
gateway_ip?: string;
|
gateway_ip?: string
|
||||||
dns_ip_1?: string;
|
dns_ip_1?: string
|
||||||
dns_ip_2?: string;
|
dns_ip_2?: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type NetworkItem = {
|
export type NetworkItem = {
|
||||||
rssi: number;
|
rssi: number
|
||||||
ssid: string;
|
ssid: string
|
||||||
bssid: string;
|
bssid: string
|
||||||
channel: number;
|
channel: number
|
||||||
encryption_type: number;
|
encryption_type: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ApStatus = {
|
export type ApStatus = {
|
||||||
status: number;
|
status: number
|
||||||
ip_address: string;
|
ip_address: string
|
||||||
mac_address: string;
|
mac_address: string
|
||||||
station_num: number;
|
station_num: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ApSettings = {
|
export type ApSettings = {
|
||||||
provision_mode: number;
|
provision_mode: number
|
||||||
ssid: string;
|
ssid: string
|
||||||
password: string;
|
password: string
|
||||||
channel: number;
|
channel: number
|
||||||
ssid_hidden: boolean;
|
ssid_hidden: boolean
|
||||||
max_clients: number;
|
max_clients: number
|
||||||
local_ip: string;
|
local_ip: string
|
||||||
gateway_ip: string;
|
gateway_ip: string
|
||||||
subnet_mask: string;
|
subnet_mask: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type DownloadOTA = {
|
export type DownloadOTA = {
|
||||||
status: string;
|
status: string
|
||||||
progress: number;
|
progress: number
|
||||||
error: string;
|
error: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Analytics = {
|
export type Analytics = {
|
||||||
max_alloc_heap: number;
|
max_alloc_heap: number
|
||||||
psram_size: number;
|
psram_size: number
|
||||||
free_psram: number;
|
free_psram: number
|
||||||
free_heap: number;
|
free_heap: number
|
||||||
total_heap: number;
|
total_heap: number
|
||||||
min_free_heap: number;
|
min_free_heap: number
|
||||||
core_temp: number;
|
core_temp: number
|
||||||
fs_total: number;
|
fs_total: number
|
||||||
fs_used: number;
|
fs_used: number
|
||||||
uptime: number;
|
uptime: number
|
||||||
cpu0_usage: number;
|
cpu0_usage: number
|
||||||
cpu1_usage: number;
|
cpu1_usage: number
|
||||||
cpu_usage: number;
|
cpu_usage: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type Rssi = {
|
export type Rssi = {
|
||||||
rssi: number;
|
rssi: number
|
||||||
ssid: string;
|
ssid: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type StaticSystemInformation = {
|
export type StaticSystemInformation = {
|
||||||
esp_platform: string;
|
esp_platform: string
|
||||||
firmware_version: string;
|
firmware_version: string
|
||||||
cpu_freq_mhz: number;
|
cpu_freq_mhz: number
|
||||||
cpu_type: string;
|
cpu_type: string
|
||||||
cpu_rev: number;
|
cpu_rev: number
|
||||||
cpu_cores: number;
|
cpu_cores: number
|
||||||
sketch_size: number;
|
sketch_size: number
|
||||||
free_sketch_space: number;
|
free_sketch_space: number
|
||||||
sdk_version: string;
|
sdk_version: string
|
||||||
arduino_version: string;
|
arduino_version: string
|
||||||
flash_chip_size: number;
|
flash_chip_size: number
|
||||||
flash_chip_speed: number;
|
flash_chip_speed: number
|
||||||
cpu_reset_reason: string;
|
cpu_reset_reason: string
|
||||||
};
|
}
|
||||||
|
|
||||||
export type SystemInformation = Analytics & StaticSystemInformation;
|
export type SystemInformation = Analytics & StaticSystemInformation
|
||||||
|
|
||||||
export type IMU = {
|
export type IMU = {
|
||||||
x: number;
|
x: number
|
||||||
y: number;
|
y: number
|
||||||
z: number;
|
z: number
|
||||||
heading: number;
|
heading: number
|
||||||
altitude: number;
|
altitude: number
|
||||||
bmp_temp: number;
|
bmp_temp: number
|
||||||
pressure: number;
|
pressure: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export interface I2CDevice {
|
export interface I2CDevice {
|
||||||
address: number;
|
address: number
|
||||||
part_number: string;
|
part_number: string
|
||||||
name: string;
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PinConfig = {
|
||||||
|
pin: number
|
||||||
|
mode: string
|
||||||
|
type: string
|
||||||
|
role: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PeripheralsConfiguration = {
|
||||||
|
sda: number
|
||||||
|
scl: number
|
||||||
|
frequency: number
|
||||||
|
pins: PinConfig[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CameraSettings = {
|
export type CameraSettings = {
|
||||||
framesize: number;
|
framesize: number
|
||||||
quality: number;
|
quality: number
|
||||||
brightness: number;
|
brightness: number
|
||||||
contrast: number;
|
contrast: number
|
||||||
saturation: number;
|
saturation: number
|
||||||
sharpness: number;
|
sharpness: number
|
||||||
denoise: number;
|
denoise: number
|
||||||
special_effect: number;
|
special_effect: number
|
||||||
wb_mode: number;
|
wb_mode: number
|
||||||
vflip: boolean;
|
vflip: boolean
|
||||||
hmirror: boolean;
|
hmirror: boolean
|
||||||
};
|
}
|
||||||
|
|
||||||
export type File = number;
|
export type File = number
|
||||||
|
|
||||||
export interface Directory {
|
export interface Directory {
|
||||||
[key: string]: File | Directory;
|
[key: string]: File | Directory
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Servo = {
|
export type Servo = {
|
||||||
name: string;
|
name: string
|
||||||
channel: number;
|
channel: number
|
||||||
inverted: boolean;
|
inverted: boolean
|
||||||
angle: number;
|
angle: number
|
||||||
center_angle: number;
|
center_angle: number
|
||||||
};
|
}
|
||||||
|
|
||||||
export type ServoConfiguration = {
|
export type ServoConfiguration = {
|
||||||
is_active: boolean;
|
is_active: boolean
|
||||||
servo_pwm_frequency: number;
|
servo_pwm_frequency: number
|
||||||
servo_oscillator_frequency: number;
|
servo_oscillator_frequency: number
|
||||||
servos: Servo[];
|
servos: Servo[]
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface MDNSServiceQuery {
|
||||||
|
services: MDNSServiceItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MDNSServiceItem {
|
||||||
|
ip: string
|
||||||
|
port: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MDNSService {
|
||||||
|
service: string
|
||||||
|
protocol: string
|
||||||
|
port: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MDNSTxtRecord {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MDNSStatus {
|
||||||
|
started: boolean
|
||||||
|
hostname: string
|
||||||
|
instance: string
|
||||||
|
services: MDNSService[]
|
||||||
|
global_txt_records: MDNSTxtRecord[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,89 +1,92 @@
|
|||||||
import { Color, LoaderUtils, Vector3 } from 'three';
|
import { Color, LoaderUtils, Vector3 } from 'three'
|
||||||
import URDFLoader, { type URDFRobot } from 'urdf-loader';
|
import URDFLoader, { type URDFRobot } from 'urdf-loader'
|
||||||
import { XacroLoader } from 'xacro-parser';
|
import { XacroLoader } from 'xacro-parser'
|
||||||
import { Result } from '$lib/utilities';
|
import { Result } from '$lib/utilities'
|
||||||
import { jointNames, model } from '$lib/stores';
|
import { jointNames, model } from '$lib/stores'
|
||||||
import uzip from 'uzip';
|
import uzip from 'uzip'
|
||||||
import { fileService } from '$lib/services';
|
import { fileService } from '$lib/services'
|
||||||
|
|
||||||
let model_xml: XMLDocument;
|
let model_xml: XMLDocument
|
||||||
|
|
||||||
export const populateModelCache = async () => {
|
export const populateModelCache = async () => {
|
||||||
await cacheModelFiles();
|
await cacheModelFiles()
|
||||||
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
const modelRes = await loadModel('/yertle.URDF')
|
||||||
if (modelRes.isOk()) {
|
if (modelRes.isOk()) {
|
||||||
const [urdf, JOINT_NAME] = modelRes.inner;
|
const [urdf, JOINT_NAME] = modelRes.inner
|
||||||
jointNames.set(JOINT_NAME);
|
jointNames.set(JOINT_NAME)
|
||||||
model.set(urdf);
|
model.set(urdf)
|
||||||
} else {
|
} else {
|
||||||
console.error(modelRes.inner, { exception: modelRes.exception });
|
console.error(modelRes.inner, { exception: modelRes.exception })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const cacheModelFiles = async () => {
|
export const cacheModelFiles = async () => {
|
||||||
let data = await fetch('/stl.zip');
|
const data = await fetch('/URDF.zip')
|
||||||
|
|
||||||
var files = uzip.parse(await data.arrayBuffer());
|
const files = uzip.parse(await data.arrayBuffer())
|
||||||
|
|
||||||
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
|
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
|
||||||
const url = new URL(path, window.location.href);
|
const url = new URL(path, window.location.href)
|
||||||
fileService.saveFile(url.toString(), data);
|
fileService?.saveFile(url.toString(), data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
export const loadModelAsync = async (
|
export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
|
||||||
url: string
|
const urdfLoader = new URDFLoader()
|
||||||
): Promise<Result<[URDFRobot, string[]], string>> => {
|
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const xacroLoader = new XacroLoader();
|
|
||||||
const urdfLoader = new URDFLoader();
|
|
||||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url);
|
|
||||||
|
|
||||||
xacroLoader.load(
|
let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
|
||||||
url,
|
|
||||||
async (xml) => {
|
if (typeof xml === 'string') {
|
||||||
model_xml = xml;
|
xml = new window.DOMParser().parseFromString(xml, 'text/xml')
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise(resolve => {
|
||||||
|
model_xml = xml
|
||||||
try {
|
try {
|
||||||
const model = urdfLoader.parse(xml);
|
const model = urdfLoader.parse(xml)
|
||||||
model.rotation.x = -Math.PI / 2;
|
setupRobot(model)
|
||||||
model.rotation.z = Math.PI / 2;
|
|
||||||
model.traverse((c) => (c.castShadow = true));
|
|
||||||
model.updateMatrixWorld(true);
|
|
||||||
model.scale.setScalar(10);
|
|
||||||
const joints = Object.entries(model.joints)
|
const joints = Object.entries(model.joints)
|
||||||
.filter((joint) => joint[1].jointType !== 'fixed')
|
.filter(joint => joint[1].jointType !== 'fixed')
|
||||||
.map((joint) => joint[0]);
|
.map(joint => joint[0])
|
||||||
|
|
||||||
resolve(Result.ok([model, joints]));
|
resolve(Result.ok([model, joints]))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
resolve(Result.err('Failed to load model', error));
|
resolve(Result.err('Failed to load model', error))
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
(error) => resolve(Result.err('Failed to load model', error))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toeWorldPositions = (robot: URDFRobot) => {
|
|
||||||
const toe_positions: Vector3[] = [];
|
|
||||||
robot.traverse((child) => {
|
|
||||||
if (child.name.includes('toe') && !child.name.includes('_link')) {
|
|
||||||
const worldPosition = new Vector3();
|
|
||||||
child.getWorldPosition(worldPosition);
|
|
||||||
toe_positions.push(worldPosition);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return toe_positions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const footColor = () => {
|
const loadXacro = async (url: string): Promise<XMLDocument> =>
|
||||||
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element;
|
new Promise((resolve, reject) => {
|
||||||
const colorAttrStr = colorElem.getAttribute('rgba') as string;
|
new XacroLoader().load(url, resolve, reject)
|
||||||
|
})
|
||||||
|
|
||||||
|
function setupRobot(robot: URDFRobot) {
|
||||||
|
robot.rotation.x = -Math.PI / 2
|
||||||
|
robot.rotation.z = Math.PI / 2
|
||||||
|
robot.scale.setScalar(10)
|
||||||
|
robot.traverse(c => (c.castShadow = true))
|
||||||
|
robot.updateMatrixWorld(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToeWorldPositions(robot: URDFRobot): Vector3[] {
|
||||||
|
const toes: Vector3[] = []
|
||||||
|
robot.traverse(c => {
|
||||||
|
if (c.name.includes('toe') && !c.name.includes('_link'))
|
||||||
|
toes.push(c.getWorldPosition(new Vector3()))
|
||||||
|
})
|
||||||
|
return toes
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractFootColor = () => {
|
||||||
|
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element
|
||||||
|
const colorAttrStr = colorElem.getAttribute('rgba') as string
|
||||||
const colorStr = colorAttrStr
|
const colorStr = colorAttrStr
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
.map((val) => Math.floor(+val * 255))
|
.map(val => Math.floor(+val * 255))
|
||||||
.join(', ');
|
.join(', ')
|
||||||
|
|
||||||
return new Color(`rgb(${colorStr})`);
|
return new Color(`rgb(${colorStr})`)
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,36 +1,47 @@
|
|||||||
export const humanFileSize = (size: number): string => {
|
export const humanFileSize = (size: number): string => {
|
||||||
const units = ['B', 'kB', 'MB', 'GB', 'TB'];
|
const units = ['B', 'kB', 'MB', 'GB', 'TB']
|
||||||
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024))
|
||||||
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i];
|
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i]
|
||||||
};
|
}
|
||||||
|
|
||||||
export const capitalize = (str: string): string => {
|
export const capitalize = (str: string): string => {
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||||
};
|
}
|
||||||
|
|
||||||
export const convertSeconds = (seconds: number) => {
|
export const convertSeconds = (seconds: number) => {
|
||||||
// Calculate the number of seconds, minutes, hours, and days
|
// Calculate the number of seconds, minutes, hours, and days
|
||||||
let minutes = Math.floor(seconds / 60);
|
let minutes = Math.floor(seconds / 60)
|
||||||
let hours = Math.floor(minutes / 60);
|
let hours = Math.floor(minutes / 60)
|
||||||
let days = Math.floor(hours / 24);
|
const days = Math.floor(hours / 24)
|
||||||
|
|
||||||
// Calculate the remaining hours, minutes, and seconds
|
// Calculate the remaining hours, minutes, and seconds
|
||||||
hours = hours % 24;
|
hours = hours % 24
|
||||||
minutes = minutes % 60;
|
minutes = minutes % 60
|
||||||
seconds = seconds % 60;
|
seconds = seconds % 60
|
||||||
|
|
||||||
// Create the formatted string
|
// Create the formatted string
|
||||||
let result = '';
|
let result = ''
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
result += days + ' day' + (days > 1 ? 's' : '') + ' '
|
||||||
}
|
}
|
||||||
if (hours > 0) {
|
if (hours > 0) {
|
||||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' '
|
||||||
}
|
}
|
||||||
if (minutes > 0) {
|
if (minutes > 0) {
|
||||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' '
|
||||||
}
|
}
|
||||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
result += seconds + ' second' + (seconds > 1 ? 's' : '')
|
||||||
|
|
||||||
return result;
|
return result
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export const compareIp = (ip1: string, ip2: string) => {
|
||||||
|
const ip1Parts = ip1.split('.').map(Number)
|
||||||
|
const ip2Parts = ip2.split('.').map(Number)
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if (ip1Parts[i] !== ip2Parts[i]) {
|
||||||
|
return ip1Parts[i] > ip2Parts[i] ? 1 : -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|||||||
+12
-13
@@ -1,13 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData } from './$types';
|
import { goto } from '$app/navigation'
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import Visualization from '$lib/components/Visualization.svelte'
|
||||||
import Visualization from '$lib/components/Visualization.svelte';
|
import { socket } from '$lib/stores'
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
interface Props {
|
onMount(() => {
|
||||||
data: PageData;
|
socket.subscribe(isConnected => {
|
||||||
|
if (isConnected) {
|
||||||
|
goto('/controller')
|
||||||
}
|
}
|
||||||
|
})
|
||||||
let { data }: Props = $props();
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="hero bg-base-100 h-screen">
|
<div class="hero bg-base-100 h-screen">
|
||||||
@@ -16,13 +19,9 @@
|
|||||||
<Visualization sky={false} orbit panel={false} ground={false} />
|
<Visualization sky={false} orbit panel={false} ground={false} />
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body w-80">
|
<div class="card-body w-80">
|
||||||
<h2 class="card-title text-center text-2xl">Welcome to {data.app_name}</h2>
|
<h2 class="card-title text-center text-2xl">Begin you journey</h2>
|
||||||
<p class="py-6 text-center"></p>
|
<p class="py-6 text-center"></p>
|
||||||
<a
|
<a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}> Add Robot Dog </a>
|
||||||
class="btn btn-primary"
|
|
||||||
href="/controller"
|
|
||||||
onclick={() => notifications.success('You did it!', 1000)}>Begin</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
import { input, outControllerData, mode, modes, type Modes, ModesEnum } from '$lib/stores'
|
import { input, outControllerData, mode, modes, type Modes, ModesEnum } from '$lib/stores'
|
||||||
import type { vector } from '$lib/types/models'
|
import type { vector } from '$lib/types/models'
|
||||||
import { VerticalSlider } from '$lib/components/input'
|
import { VerticalSlider } from '$lib/components/input'
|
||||||
|
import { gamepadAxes, gamepadButtons, hasGamepad } from '$lib/stores/gamepad'
|
||||||
|
import { notifications } from '$lib/components/toasts/notifications'
|
||||||
|
|
||||||
let throttle = new throttler()
|
let throttle = new throttler()
|
||||||
let left: nipplejs.JoystickManager
|
let left: nipplejs.JoystickManager
|
||||||
@@ -13,6 +15,23 @@
|
|||||||
let throttle_timing = 40
|
let throttle_timing = 40
|
||||||
let data = new Array(8)
|
let data = new Array(8)
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if ($hasGamepad) {
|
||||||
|
notifications.success('🎮 Gamepad connected', 3000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
handleJoyMove('left', { x: $gamepadAxes[0], y: -$gamepadAxes[1] })
|
||||||
|
handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] })
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO React to button press
|
||||||
|
// $effect(() => {
|
||||||
|
// if ($gamepadButtons.length === 0) return
|
||||||
|
//
|
||||||
|
// })
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
left = nipplejs.create({
|
left = nipplejs.create({
|
||||||
zone: document.getElementById('left') as HTMLElement,
|
zone: document.getElementById('left') as HTMLElement,
|
||||||
@@ -70,7 +89,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleRange = (event: Event, key: 'speed' | 'height' | 's1') => {
|
const handleRange = (event: Event, key: 'speed' | 'height' | 's1') => {
|
||||||
const value: number = event.target?.value
|
const value: number = Number((event.target as HTMLInputElement).value)
|
||||||
|
|
||||||
input.update(inputData => {
|
input.update(inputData => {
|
||||||
inputData[key] = value
|
inputData[key] = value
|
||||||
@@ -127,7 +146,7 @@
|
|||||||
type="range"
|
type="range"
|
||||||
name="s1"
|
name="s1"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="25"
|
||||||
oninput={e => handleRange(e, 's1')}
|
oninput={e => handleRange(e, 's1')}
|
||||||
class="range range-sm range-primary" />
|
class="range range-sm range-primary" />
|
||||||
</div>
|
</div>
|
||||||
@@ -137,7 +156,7 @@
|
|||||||
type="range"
|
type="range"
|
||||||
name="speed"
|
name="speed"
|
||||||
min="0"
|
min="0"
|
||||||
max="100"
|
max="25"
|
||||||
oninput={e => handleRange(e, 'speed')}
|
oninput={e => handleRange(e, 'speed')}
|
||||||
class="range range-sm range-primary" />
|
class="range range-sm range-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import I2C from './i2c.svelte';
|
import I2C from './i2c.svelte'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types'
|
||||||
|
|
||||||
export const load = (async () => {
|
export const load = (async () => {
|
||||||
return {
|
return {
|
||||||
title: 'I2C'
|
title: 'I2C'
|
||||||
};
|
}
|
||||||
}) satisfies PageLoad;
|
}) satisfies PageLoad
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte'
|
||||||
import { socket } from '$lib/stores';
|
import { socket } from '$lib/stores'
|
||||||
import type { I2CDevice } from '$lib/types/models';
|
import type { I2CDevice } from '$lib/types/models'
|
||||||
import { Connection } from '$lib/components/icons';
|
import { Connection } from '$lib/components/icons'
|
||||||
|
import I2CSetting from './i2cSetting.svelte'
|
||||||
|
|
||||||
const i2cDevices = [
|
const i2cDevices = [
|
||||||
{ address: 30, part_number: 'HMC5883', name: '3-Axis Digital Compass/Magnetometer IC' },
|
{ address: 30, part_number: 'HMC5883', name: '3-Axis Digital Compass/Magnetometer IC' },
|
||||||
|
{ address: 41, part_number: 'BNO055', name: '9-Axis Absolute Orientation Sensor' },
|
||||||
{ address: 64, part_number: 'PCA9685', name: '16-channel PWM driver default address' },
|
{ address: 64, part_number: 'PCA9685', name: '16-channel PWM driver default address' },
|
||||||
{ address: 72, part_number: 'ADS1115', name: '4-channel 16-bit ADC' },
|
{ address: 72, part_number: 'ADS1115', name: '4-channel 16-bit ADC' },
|
||||||
{
|
{
|
||||||
@@ -15,15 +17,17 @@
|
|||||||
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
|
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
|
||||||
},
|
},
|
||||||
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
|
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
|
||||||
];
|
]
|
||||||
|
|
||||||
let active_devices: I2CDevice[] = $state([]);
|
let active_devices: I2CDevice[] = $state([])
|
||||||
|
|
||||||
|
let isLoading = $state(false)
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
socket.on('i2cScan', handleScan);
|
socket.on('i2cScan', handleScan)
|
||||||
socket.sendEvent('i2cScan', '');
|
triggerScan()
|
||||||
return () => socket.off('i2cScan', handleScan);
|
return () => socket.off('i2cScan', handleScan)
|
||||||
});
|
})
|
||||||
|
|
||||||
const handleScan = (data: any) => {
|
const handleScan = (data: any) => {
|
||||||
active_devices = data.addresses.map(
|
active_devices = data.addresses.map(
|
||||||
@@ -33,8 +37,14 @@
|
|||||||
part_number: 'Unknown',
|
part_number: 'Unknown',
|
||||||
name: 'Unknown'
|
name: 'Unknown'
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
};
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerScan = () => {
|
||||||
|
isLoading = true
|
||||||
|
socket.sendEvent('i2cScan', '')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
@@ -44,6 +54,17 @@
|
|||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span>I<sup>2</sup>C</span>
|
<span>I<sup>2</sup>C</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
{#snippet right()}
|
||||||
|
<button class="btn btn-primary" onclick={triggerScan} disabled={isLoading}>
|
||||||
|
{#if isLoading}
|
||||||
|
<span class="loading loading-ring loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
Scan
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
<I2CSetting />
|
||||||
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
{#if active_devices.length === 0}
|
{#if active_devices.length === 0}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<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 { onMount } from 'svelte'
|
||||||
|
import { modals } from 'svelte-modals'
|
||||||
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||||
|
|
||||||
|
let settings: PeripheralsConfiguration | null = $state(null)
|
||||||
|
let isEditing = $state(false)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
socket.on('peripheralSettings', handleSettings)
|
||||||
|
socket.sendEvent('peripheralSettings', '')
|
||||||
|
return () => socket.off('peripheralSettings', handleSettings)
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSettings = (data: any) => {
|
||||||
|
settings = data
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
modals.open(ConfirmDialog, {
|
||||||
|
title: 'Confirm configuration',
|
||||||
|
message:
|
||||||
|
'Are you sure you want to save this configuration? The operation cannot be undone. Please make sure you have the correct settings.',
|
||||||
|
labels: {
|
||||||
|
cancel: { label: 'Cancel', icon: Cancel },
|
||||||
|
confirm: { label: 'Confirm', icon: Power }
|
||||||
|
},
|
||||||
|
onConfirm: () => {
|
||||||
|
modals.close()
|
||||||
|
socket.sendEvent('peripheralSettings', settings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = $derived(isEditing ? EditOff : Edit)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if settings}
|
||||||
|
<div class="collapse bg-base-100 border-base-300 border">
|
||||||
|
<input type="checkbox" />
|
||||||
|
<div class="collapse-title font-semibold">Configuration</div>
|
||||||
|
<div class="collapse-content text-sm">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label for="sda" class="input validator">
|
||||||
|
SDA
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="sda"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
placeholder="Type a number between 1 to 48"
|
||||||
|
min="0"
|
||||||
|
max="48"
|
||||||
|
title="SDA pin number (0-48)"
|
||||||
|
disabled={!isEditing}
|
||||||
|
bind:value={settings.sda} />
|
||||||
|
</label>
|
||||||
|
<label for="scl" class="input validator">
|
||||||
|
SCL
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="scl"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
placeholder="Type a number between 1 to 48"
|
||||||
|
min="1"
|
||||||
|
max="48"
|
||||||
|
title="SCL pin number (0-48)"
|
||||||
|
disabled={!isEditing}
|
||||||
|
bind:value={settings.scl} />
|
||||||
|
</label>
|
||||||
|
<label class="input validator" for="frequency">
|
||||||
|
Frequency
|
||||||
|
<input
|
||||||
|
id="frequency"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
placeholder="Type a number between 100000 to 430000"
|
||||||
|
min="100000"
|
||||||
|
max="430000"
|
||||||
|
title="I2C frequency in Hz"
|
||||||
|
disabled={!isEditing}
|
||||||
|
bind:value={settings.frequency} />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-outline btn-primary" onclick={() => (isEditing = !isEditing)}>
|
||||||
|
<Icon class="h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
{#if isEditing}
|
||||||
|
<button class="btn btn-outline btn-primary" onclick={handleSave}>Save</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -1,61 +1,92 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||||
import { imu } from '$lib/stores/imu';
|
import { imu } from '$lib/stores/imu'
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js'
|
||||||
import { cubicOut } from "svelte/easing";
|
import { cubicOut } from 'svelte/easing'
|
||||||
import { slide } from "svelte/transition";
|
import { slide } from 'svelte/transition'
|
||||||
import { onDestroy, onMount } from "svelte";
|
import { onDestroy, onMount } from 'svelte'
|
||||||
import { daisyColor } from "$lib/utilities";
|
import { socket } from '$lib/stores'
|
||||||
import { socket } from "$lib/stores";
|
import type { IMU } from '$lib/types/models'
|
||||||
import type { IMU } from "$lib/types/models";
|
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||||
import { useFeatureFlags } from "$lib/stores/featureFlags";
|
import { Rotate3d } from '$lib/components/icons'
|
||||||
import { Rotate3d } from "$lib/components/icons";
|
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
Chart.register(...registerables)
|
||||||
|
|
||||||
Chart.register(...registerables);
|
const features = useFeatureFlags()
|
||||||
|
let intervalId: number
|
||||||
|
|
||||||
let angleChartElement: HTMLCanvasElement = $state();
|
let angleChartElement: HTMLCanvasElement = $state()
|
||||||
let angleChart: Chart;
|
let tempChartElement: HTMLCanvasElement = $state()
|
||||||
|
let altitudeChartElement: HTMLCanvasElement = $state()
|
||||||
|
|
||||||
let tempChartElement: HTMLCanvasElement = $state();
|
let angleChart: Chart
|
||||||
let tempChart: Chart;
|
let tempChart: Chart
|
||||||
|
let altitudeChart: Chart
|
||||||
|
|
||||||
let altitudeChartElement: HTMLCanvasElement = $state();
|
const getChartColors = () => {
|
||||||
let altitudeChart: Chart;
|
const style = getComputedStyle(document.body)
|
||||||
|
return {
|
||||||
const handleImu = (data: IMU) => {
|
primary: style.getPropertyValue('--color-primary'),
|
||||||
console.log(data);
|
secondary: style.getPropertyValue('--color-secondary'),
|
||||||
|
accent: style.getPropertyValue('--color-accent'),
|
||||||
imu.addData(data);
|
background: style.getPropertyValue('--color-background')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
const createBaseChartConfig = (bgColor: string) => ({
|
||||||
socket.on('imu', handleImu);
|
maintainAspectRatio: false,
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: true },
|
||||||
|
tooltip: { mode: 'index', intersect: false }
|
||||||
|
},
|
||||||
|
elements: { point: { radius: 1 } },
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: { color: bgColor },
|
||||||
|
ticks: { color: bgColor },
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
type: 'linear',
|
||||||
|
position: 'left',
|
||||||
|
min: 0,
|
||||||
|
max: 10,
|
||||||
|
grid: { color: bgColor },
|
||||||
|
ticks: { color: bgColor },
|
||||||
|
border: { color: bgColor }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const initializeCharts = () => {
|
||||||
|
const colors = getChartColors()
|
||||||
|
const baseConfig = createBaseChartConfig(colors.background)
|
||||||
|
|
||||||
angleChart = new Chart(angleChartElement, {
|
angleChart = new Chart(angleChartElement, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'x',
|
label: 'x',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: colors.primary,
|
||||||
backgroundColor: daisyColor('--p', 50),
|
backgroundColor: colors.primary,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
data: $imu.x,
|
data: $imu.x,
|
||||||
yAxisID: 'y'
|
yAxisID: 'y'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'y',
|
label: 'y',
|
||||||
borderColor: daisyColor('--s'),
|
borderColor: colors.secondary,
|
||||||
backgroundColor: daisyColor('--s', 50),
|
backgroundColor: colors.secondary,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
data: $imu.y,
|
data: $imu.y,
|
||||||
yAxisID: 'y'
|
yAxisID: 'y'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'z',
|
label: 'z',
|
||||||
borderColor: daisyColor('--a'),
|
borderColor: colors.accent,
|
||||||
backgroundColor: daisyColor('--a', 50),
|
backgroundColor: colors.accent,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
data: $imu.z,
|
data: $imu.z,
|
||||||
yAxisID: 'y'
|
yAxisID: 'y'
|
||||||
@@ -63,61 +94,30 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false,
|
...baseConfig,
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
radius: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
...baseConfig.scales,
|
||||||
grid: {
|
|
||||||
color: daisyColor('--bc', 10)
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: daisyColor('--bc')
|
|
||||||
},
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
...baseConfig.scales.y,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Angle [°]',
|
text: 'Angle [°]',
|
||||||
color: daisyColor('--bc'),
|
color: colors.background,
|
||||||
font: {
|
font: { size: 16, weight: 'bold' }
|
||||||
size: 16,
|
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
position: 'left',
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
grid: { color: daisyColor('--bc', 10) },
|
|
||||||
ticks: { color: daisyColor('--bc') },
|
|
||||||
border: { color: daisyColor('--bc', 10) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
|
||||||
tempChart = new Chart(tempChartElement, {
|
tempChart = new Chart(tempChartElement, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Barometer temperature',
|
label: 'Barometer temperature',
|
||||||
borderColor: daisyColor('--s'),
|
borderColor: colors.secondary,
|
||||||
backgroundColor: daisyColor('--s', 50),
|
backgroundColor: colors.secondary,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
data: $imu.bmp_temp,
|
data: $imu.bmp_temp,
|
||||||
yAxisID: 'y'
|
yAxisID: 'y'
|
||||||
@@ -125,61 +125,30 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false,
|
...baseConfig,
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
radius: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
...baseConfig.scales,
|
||||||
grid: {
|
|
||||||
color: daisyColor('--bc', 10)
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: daisyColor('--bc')
|
|
||||||
},
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
...baseConfig.scales.y,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Temperature [C°]',
|
text: 'Temperature [C°]',
|
||||||
color: daisyColor('--bc'),
|
color: colors.background,
|
||||||
font: {
|
font: { size: 16, weight: 'bold' }
|
||||||
size: 16,
|
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
position: 'left',
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
grid: { color: daisyColor('--bc', 10) },
|
|
||||||
ticks: { color: daisyColor('--bc') },
|
|
||||||
border: { color: daisyColor('--bc', 10) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
|
||||||
altitudeChart = new Chart(altitudeChartElement, {
|
altitudeChart = new Chart(altitudeChartElement, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Altitude',
|
label: 'Altitude',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: colors.primary,
|
||||||
backgroundColor: daisyColor('--p', 50),
|
backgroundColor: colors.primary,
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
data: $imu.altitude,
|
data: $imu.altitude,
|
||||||
yAxisID: 'y'
|
yAxisID: 'y'
|
||||||
@@ -187,88 +156,64 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
maintainAspectRatio: false,
|
...baseConfig,
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
display: true
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
mode: 'index',
|
|
||||||
intersect: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
elements: {
|
|
||||||
point: {
|
|
||||||
radius: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
...baseConfig.scales,
|
||||||
grid: {
|
|
||||||
color: daisyColor('--bc', 10)
|
|
||||||
},
|
|
||||||
ticks: {
|
|
||||||
color: daisyColor('--bc')
|
|
||||||
},
|
|
||||||
display: false
|
|
||||||
},
|
|
||||||
y: {
|
y: {
|
||||||
type: 'linear',
|
...baseConfig.scales.y,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Altitude [M]',
|
text: 'Altitude [M]',
|
||||||
color: daisyColor('--bc'),
|
color: colors.background,
|
||||||
font: {
|
font: { size: 16, weight: 'bold' }
|
||||||
size: 16,
|
}
|
||||||
weight: 'bold'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
position: 'left',
|
|
||||||
min: 0,
|
|
||||||
max: 10,
|
|
||||||
grid: { color: daisyColor('--bc', 10) },
|
|
||||||
ticks: { color: daisyColor('--bc') },
|
|
||||||
border: { color: daisyColor('--bc', 10) }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
setInterval(() => {
|
|
||||||
updateData(), 200;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onDestroy(() => {
|
const updateChartData = (chart: Chart, data: number[], label: string) => {
|
||||||
socket.off('imu', handleImu);
|
chart.data.labels = data
|
||||||
})
|
chart.data.datasets[0].data = data
|
||||||
|
chart.options.scales!.y!.min = Math.min(...data) - 1
|
||||||
|
chart.options.scales!.y!.max = Math.max(...data) + 1
|
||||||
|
chart.update('none')
|
||||||
|
}
|
||||||
|
|
||||||
const updateData = () => {
|
const updateData = () => {
|
||||||
if ($features.imu) {
|
if ($features.imu) {
|
||||||
angleChart.data.labels = $imu.x;
|
angleChart.data.labels = $imu.x
|
||||||
angleChart.data.datasets[0].data = $imu.x;
|
angleChart.data.datasets[0].data = $imu.x
|
||||||
angleChart.data.datasets[1].data = $imu.y;
|
angleChart.data.datasets[1].data = $imu.y
|
||||||
angleChart.data.datasets[2].data = $imu.z;
|
angleChart.data.datasets[2].data = $imu.z
|
||||||
angleChart.options.scales!.y!.min = Math.min(Math.min(...$imu.x), Math.min(...$imu.y), Math.min(...$imu.z)) - 1;
|
|
||||||
angleChart.options.scales!.y!.max = Math.max(Math.max(...$imu.x), Math.max(...$imu.y), Math.max(...$imu.z)) + 1;
|
const allValues = [...$imu.x, ...$imu.y, ...$imu.z]
|
||||||
angleChart.update('none');
|
angleChart.options.scales!.y!.min = Math.min(...allValues) - 1
|
||||||
|
angleChart.options.scales!.y!.max = Math.max(...allValues) + 1
|
||||||
|
angleChart.update('none')
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($features.bmp) {
|
if ($features.bmp) {
|
||||||
tempChart.data.labels = $imu.bmp_temp;
|
updateChartData(tempChart, $imu.bmp_temp, 'Temperature')
|
||||||
tempChart.data.datasets[0].data = $imu.bmp_temp;
|
updateChartData(altitudeChart, $imu.altitude, 'Altitude')
|
||||||
tempChart.options.scales!.y!.min = Math.min(...$imu.bmp_temp) - 1;
|
|
||||||
tempChart.options.scales!.y!.max = Math.max(...$imu.bmp_temp) + 1;
|
|
||||||
tempChart.update('none');
|
|
||||||
|
|
||||||
altitudeChart.data.labels = $imu.altitude;
|
|
||||||
altitudeChart.data.datasets[0].data = $imu.altitude;
|
|
||||||
altitudeChart.options.scales!.y!.min = Math.min(Math.min(...$imu.altitude)) - 1;
|
|
||||||
altitudeChart.options.scales!.y!.max = Math.max(Math.max(...$imu.altitude)) + 1;
|
|
||||||
altitudeChart.update('none');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
socket.on('imu', (data: IMU) => {
|
||||||
|
console.log(data)
|
||||||
|
imu.addData(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
initializeCharts()
|
||||||
|
intervalId = setInterval(updateData, 200)
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
socket.off('imu')
|
||||||
|
clearInterval(intervalId)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
@@ -278,33 +223,31 @@
|
|||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span>IMU</span>
|
<span>IMU</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#if $features.imu}
|
{#if $features.imu}
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1 h-60"
|
class="flex w-full flex-col space-y-1 h-60"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<canvas bind:this={angleChartElement}></canvas>
|
<canvas bind:this={angleChartElement}></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $features.bmp}
|
{#if $features.bmp}
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1 h-60"
|
class="flex w-full flex-col space-y-1 h-60"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<canvas bind:this={tempChartElement}></canvas>
|
<canvas bind:this={tempChartElement}></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1 h-60"
|
class="flex w-full flex-col space-y-1 h-60"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<canvas bind:this={altitudeChartElement}></canvas>
|
<canvas bind:this={altitudeChartElement}></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- <IMUSetting /> -->
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Servos from './servos.svelte';
|
import Servos from './servos.svelte'
|
||||||
import ServoTable from './ServoTable.svelte';
|
import ServoTable from './ServoTable.svelte'
|
||||||
|
|
||||||
|
let servoId = $state(0)
|
||||||
|
let pwm = $state(306)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||||
<Servos />
|
<Servos bind:servoId bind:pwm />
|
||||||
<ServoTable />
|
<ServoTable {servoId} {pwm} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,34 +1,57 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api'
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte'
|
||||||
|
import { RotateCw, RotateCcw } from '$lib/components/icons'
|
||||||
interface Props {
|
interface Props {
|
||||||
data?: any;
|
data?: any
|
||||||
|
servoId?: number
|
||||||
|
pwm?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
let { data = $bindable({
|
let {
|
||||||
|
data = $bindable({
|
||||||
servos: []
|
servos: []
|
||||||
}) }: Props = $props();
|
}),
|
||||||
|
pwm = $bindable(306),
|
||||||
|
servoId = $bindable(0)
|
||||||
|
}: Props = $props()
|
||||||
|
|
||||||
const updateValue = (event, index, key) => {
|
const updateValue = (event: Event, index: number, key: string) => {
|
||||||
data.servos[index][key] = event.target.innerText;
|
data.servos[index][key] = Number((event.target as HTMLInputElement).value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const syncConfig = async () => {
|
const syncConfig = async () => {
|
||||||
await api.post('/api/servo/config', data);
|
await api.post('/api/servo/config', data)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const toggleDirection = async (index: number) => {
|
||||||
|
data.servos[index].direction = data.servos[index].direction === 1 ? -1 : 1
|
||||||
|
await syncConfig()
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const result = await api.get('/api/servo/config');
|
const result = await api.get('/api/servo/config')
|
||||||
if (result.isOk()) {
|
if (result.isOk()) {
|
||||||
data = result.inner;
|
data = result.inner
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const setCenterPWM = async () => {
|
||||||
|
console.log('setCenterPWM', servoId, pwm)
|
||||||
|
data.servos[servoId]['center_pwm'] = pwm
|
||||||
|
await syncConfig()
|
||||||
}
|
}
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick={() => setCenterPWM()}>Set center pwm</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="table table-xs">
|
<table class="table table-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th>Servo</th>
|
||||||
<th>Center PWM</th>
|
<th>Center PWM</th>
|
||||||
<th>Center Angle</th>
|
<th>Center Angle</th>
|
||||||
<th>Direction</th>
|
<th>Direction</th>
|
||||||
@@ -37,34 +60,51 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{#each data.servos as servo, index}
|
{#each data.servos as servo, index}
|
||||||
<tr>
|
<tr class="hover:bg-base-200">
|
||||||
<td
|
<td class="font-medium">Servo {index}</td>
|
||||||
contenteditable="true"
|
<td>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input input-sm input-bordered w-20"
|
||||||
|
value={servo.center_pwm}
|
||||||
onblur={syncConfig}
|
onblur={syncConfig}
|
||||||
oninput={event => updateValue(event, index, 'center_pwm')}
|
oninput={event => updateValue(event, index, 'center_pwm')}
|
||||||
>
|
min="80"
|
||||||
{servo.center_pwm}
|
max="600" />
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td>
|
||||||
contenteditable="true"
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
class="input input-sm input-bordered w-20"
|
||||||
|
value={servo.center_angle}
|
||||||
onblur={syncConfig}
|
onblur={syncConfig}
|
||||||
oninput={event => updateValue(event, index, 'center_angle')}
|
oninput={event => updateValue(event, index, 'center_angle')}
|
||||||
>
|
min="-90"
|
||||||
{servo.center_angle}
|
max="90" />
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td>
|
||||||
contenteditable="true"
|
<button
|
||||||
onblur={syncConfig}
|
class="btn btn-sm btn-ghost"
|
||||||
oninput={event => updateValue(event, index, 'direction')}
|
title="Toggle direction {servo.direction}"
|
||||||
>
|
onclick={() => toggleDirection(index)}>
|
||||||
{servo.direction}
|
{#if servo.direction === 1}
|
||||||
|
<RotateCw class="w-4 h-4 text-green-500" />
|
||||||
|
{:else}
|
||||||
|
<RotateCcw class="w-4 h-4" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td>
|
||||||
contenteditable="true"
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
class="input input-sm input-bordered w-20"
|
||||||
|
value={servo.conversion}
|
||||||
onblur={syncConfig}
|
onblur={syncConfig}
|
||||||
oninput={event => updateValue(event, index, 'conversion')}
|
oninput={event => updateValue(event, index, 'conversion')}
|
||||||
>
|
min="0"
|
||||||
{servo.conversion}
|
max="10" />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,75 +1,63 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import { socket } from '$lib/stores'
|
||||||
import type { ServoConfiguration, Servo } from '$lib/types/models';
|
import { throttler as Throttler } from '$lib/utilities'
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
|
||||||
|
|
||||||
import { socket } from '$lib/stores';
|
let { servoId = $bindable(0), pwm = $bindable(306) } = $props()
|
||||||
import { onDestroy, onMount } from 'svelte';
|
|
||||||
import { throttler as Throttler } from '$lib/utilities';
|
|
||||||
import { MotorOutline } from '$lib/components/icons';
|
|
||||||
|
|
||||||
let isLoading = false;
|
let active = $state(false)
|
||||||
|
|
||||||
let active = $state(false);
|
let allServos = $state(false)
|
||||||
|
|
||||||
let servoId = $state(0);
|
const throttler = new Throttler()
|
||||||
|
|
||||||
const throttler = new Throttler();
|
const activateServo = () => {
|
||||||
|
socket.sendEvent('servoState', { active: 1 })
|
||||||
|
}
|
||||||
|
|
||||||
const sweep = (event: any) => {
|
const deactivateServo = () => {
|
||||||
let channel = event.detail.channel;
|
socket.sendEvent('servoState', { active: 0 })
|
||||||
socket.sendEvent('servoConfiguration', { servos: [{ channel, sweep: true }] });
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const activateServo = (event: any) => {
|
|
||||||
socket.sendEvent('servoState', { active: 1 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const deactivateServo = (event: any) => {
|
|
||||||
socket.sendEvent('servoState', { active: 0 });
|
|
||||||
};
|
|
||||||
|
|
||||||
let pwm = $state(306);
|
|
||||||
|
|
||||||
const updatePWM = () => {
|
const updatePWM = () => {
|
||||||
throttler.throttle(() => {
|
throttler.throttle(() => {
|
||||||
socket.sendEvent('servoPWM', { servo_id: servoId, pwm });
|
socket.sendEvent('servoPWM', { servo_id: servoId, pwm })
|
||||||
}, 10);
|
}, 10)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
const toggleMode = () => {
|
||||||
|
servoId = allServos ? -1 : 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<div class="flex flex-col">
|
||||||
{#snippet icon()}
|
<h2 class="text-lg">General servo configuration</h2>
|
||||||
<MotorOutline class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
|
||||||
{/snippet}
|
|
||||||
{#snippet title()}
|
|
||||||
<span>Servo</span>
|
<span>Servo</span>
|
||||||
{/snippet}
|
<span>{pwm}</span>
|
||||||
{pwm}
|
</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
min="80"
|
min="80"
|
||||||
max="600"
|
max="600"
|
||||||
bind:value={pwm}
|
bind:value={pwm}
|
||||||
oninput={updatePWM}
|
oninput={updatePWM}
|
||||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" />
|
||||||
/>
|
|
||||||
|
|
||||||
{#if isLoading}
|
|
||||||
<Spinner />
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h2 class="text-lg">General servo configuration</h2>
|
<h2 class="text-lg">General servo configuration</h2>
|
||||||
<span class="flex items-center gap-2">
|
<span>
|
||||||
<label for="servoId">Servo active {servoId}</label>
|
<label for="mode">All servoes</label>
|
||||||
<input type="range" min="0" max="11" step="1" bind:value={servoId} />
|
<input type="checkbox" class="toggle" bind:checked={allServos} onchange={toggleMode} />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<label for="active">Active</label>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="toggle"
|
class="toggle"
|
||||||
bind:checked={active}
|
bind:checked={active}
|
||||||
onchange={active ? activateServo : deactivateServo}
|
onchange={active ? activateServo : deactivateServo} />
|
||||||
/>
|
</span>
|
||||||
|
<span class="flex items-center gap-2">
|
||||||
|
<label for="servoId">Servo active {servoId}</label>
|
||||||
|
<input type="range" min="0" max="11" step="1" bind:value={servoId} />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
</SettingsCard>
|
|
||||||
|
|||||||
@@ -1,11 +1,24 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { FileIcon } from '$lib/components/icons'
|
import { FileIcon, TrashIcon } from '$lib/components/icons'
|
||||||
|
|
||||||
let { name, selected } = $props()
|
interface Props {
|
||||||
|
name: string
|
||||||
|
selected: (name: string) => void
|
||||||
|
onDelete: (name: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
let { name, selected, onDelete }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
<div class="flex items-center pl-4 group hover:bg-gray-700 rounded py-1">
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<button class="flex items-center gap-2 flex-grow" onclick={() => selected(name)}>
|
||||||
<span role="button" class="flex pl-4 gap-2 items-center" onclick={selected}>
|
<FileIcon class="w-4 h-4" />
|
||||||
<FileIcon />{name}
|
<span class="text-sm">{name}</span>
|
||||||
</span>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="opacity-0 group-hover:opacity-100 p-1 hover:text-red-500"
|
||||||
|
onclick={() => onDelete(name)}>
|
||||||
|
<TrashIcon class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -1,62 +1,172 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||||
import Spinner from "$lib/components/Spinner.svelte";
|
import Spinner from '$lib/components/Spinner.svelte'
|
||||||
import Folder from "./Folder.svelte";
|
import Folder from './Folder.svelte'
|
||||||
import { api } from "$lib/api";
|
import { api } from '$lib/api'
|
||||||
import type { Directory } from "$lib/types/models";
|
import type { Directory } from '$lib/types/models'
|
||||||
import { FolderIcon } from "$lib/components/icons";
|
import { FolderIcon, Add, FileIcon } from '$lib/components/icons'
|
||||||
|
import { modals } from 'svelte-modals'
|
||||||
|
import NewFolderDialog from './NewFolderDialog.svelte'
|
||||||
|
import NewFileDialog from './NewFileDialog.svelte'
|
||||||
|
|
||||||
let filename = $state('');
|
let filename = $state('')
|
||||||
|
let content = $state('')
|
||||||
|
let isEditing = $state(false)
|
||||||
|
|
||||||
const getFiles = async () => {
|
const getFiles = async () => {
|
||||||
const result = await api.get<Directory>('/api/files')
|
const result = await api.get<Directory>('/api/files')
|
||||||
if (result.isOk()) {
|
if (result.isOk()) {
|
||||||
return result.inner;
|
return result.inner
|
||||||
}
|
}
|
||||||
return { root: {} }
|
return { root: {} }
|
||||||
};
|
}
|
||||||
|
|
||||||
const getContent = async (name: string) => {
|
const getContent = async (name: string) => {
|
||||||
if (!name) return '';
|
if (!name) return ''
|
||||||
const result = await api.get(`/api/config/${name}`)
|
const result = await api.get(`/api/config/${name}`)
|
||||||
if (result.isOk()) {
|
if (result.isOk()) {
|
||||||
return JSON.stringify(result.inner, null, 4);
|
content = JSON.stringify(result.inner, null, 4)
|
||||||
|
return content
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const saveContent = async () => {
|
||||||
|
if (!filename) return
|
||||||
|
const result = await api.post('/api/files/edit', {
|
||||||
|
file: '/config/' + filename,
|
||||||
|
content
|
||||||
|
})
|
||||||
|
if (result.isOk()) {
|
||||||
|
isEditing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const deleteFile = async (name: string) => {
|
const deleteFile = async (name: string) => {
|
||||||
const result = await api.post(`/api/files/delete`, { file: "/config/"+ name })
|
if (!confirm(`Are you sure you want to delete ${name}?`)) return
|
||||||
|
const result = await api.post('/api/files/delete', { file: '/config/' + name })
|
||||||
if (result.isOk()) {
|
if (result.isOk()) {
|
||||||
return result.inner;
|
filename = ''
|
||||||
|
content = ''
|
||||||
}
|
}
|
||||||
return ''
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSelected = async (event:any) => {
|
const createFolder = async (folderName: string) => {
|
||||||
filename = event.detail.name;
|
if (!folderName) return
|
||||||
|
const result = await api.post('/api/files/mkdir', {
|
||||||
|
path: '/config/' + folderName
|
||||||
|
})
|
||||||
|
if (result.isOk()) {
|
||||||
|
// Refresh the file list
|
||||||
|
await getFiles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSelected = async (name: string) => {
|
||||||
|
filename = name
|
||||||
|
isEditing = false
|
||||||
|
await getContent(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const openNewFolderDialog = () => {
|
||||||
|
modals.open(NewFolderDialog, {
|
||||||
|
onConfirm: createFolder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const createFile = async (fileName: string) => {
|
||||||
|
if (!fileName) return
|
||||||
|
const result = await api.post('/api/files/edit', {
|
||||||
|
file: '/config/' + fileName,
|
||||||
|
content: '{}' // Default empty JSON object
|
||||||
|
})
|
||||||
|
if (result.isOk()) {
|
||||||
|
// Refresh the file list and select the new file
|
||||||
|
await getFiles()
|
||||||
|
await updateSelected(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openNewFileDialog = () => {
|
||||||
|
modals.open(NewFileDialog, {
|
||||||
|
onConfirm: createFile
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<SettingsCard collapsible={false}>
|
|
||||||
{#snippet icon()}
|
<!-- <SettingsCard collapsible={false}> -->
|
||||||
<FolderIcon class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
<!-- {#snippet icon()} -->
|
||||||
{/snippet}
|
<FolderIcon class="flex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
{#snippet title()}
|
<!-- {/snippet}
|
||||||
|
{#snippet title()} -->
|
||||||
|
<div class="flex justify-between items-center w-full gap-2">
|
||||||
<span>File System</span>
|
<span>File System</span>
|
||||||
{/snippet}
|
<div class="flex gap-2">
|
||||||
<div class="w-full overflow-x-auto">
|
<button class="btn btn-sm btn-primary flex items-center gap-2" onclick={openNewFileDialog}>
|
||||||
|
<FileIcon class="w-4 h-4" />
|
||||||
|
New File
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-primary flex items-center gap-2" onclick={openNewFolderDialog}>
|
||||||
|
<Add class="w-4 h-4" />
|
||||||
|
New Folder
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- {/snippet} -->
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row gap-4 w-full">
|
||||||
|
<!-- File Tree -->
|
||||||
|
<div
|
||||||
|
class="w-full md:w-[300px] md:min-w-[300px] md:max-w-[300px] border-b md:border-b-0 md:border-r pb-4 md:pb-0 md:pr-4">
|
||||||
{#await getFiles()}
|
{#await getFiles()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then files}
|
{:then files}
|
||||||
<Folder name="/" files={files.root} expanded on:selected={updateSelected}/>
|
<Folder
|
||||||
|
name="/"
|
||||||
|
files={files.root}
|
||||||
|
expanded
|
||||||
|
selected={updateSelected}
|
||||||
|
onDelete={deleteFile} />
|
||||||
{/await}
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Content -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
{#if filename}
|
||||||
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-2">
|
||||||
|
<h3 class="text-lg font-semibold truncate">{filename}</h3>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#if isEditing}
|
||||||
|
<button class="btn btn-sm btn-primary" onclick={saveContent}>Save</button>
|
||||||
|
<button class="btn btn-sm btn-secondary" onclick={() => (isEditing = false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
{:else}
|
||||||
|
<button class="btn btn-sm btn-primary" onclick={() => (isEditing = true)}>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-danger" onclick={() => deleteFile(filename)}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#await getContent(filename)}
|
{#await getContent(filename)}
|
||||||
<div>
|
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
{:then _}
|
||||||
{:then content}
|
{#if isEditing}
|
||||||
<pre>{content}</pre>
|
<textarea
|
||||||
|
class="w-full h-[300px] sm:h-[500px] font-mono p-2 bg-gray-800 text-white"
|
||||||
|
bind:value={content}></textarea>
|
||||||
|
{:else}
|
||||||
|
<pre
|
||||||
|
class="bg-gray-800 p-4 rounded overflow-auto max-h-[300px] sm:max-h-[500px]">{content}</pre>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<div class="text-center text-gray-500">Select a file to view its contents</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</div>
|
||||||
|
<!-- </SettingsCard> -->
|
||||||
|
|||||||
@@ -1,47 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Folder from './Folder.svelte';
|
import Folder from './Folder.svelte'
|
||||||
import File from './File.svelte';
|
import File from './File.svelte'
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { FolderIcon, FolderOpenOutline } from '$lib/components/icons'
|
||||||
import { FolderIcon, FolderOpenOutline } from '$lib/components/icons';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
expanded?: boolean;
|
expanded?: boolean
|
||||||
name: any;
|
name: string
|
||||||
files: any;
|
files: any
|
||||||
|
selected: (name: string) => void
|
||||||
|
onDelete: (name: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { expanded = $bindable(false), name, files }: Props = $props();
|
let { expanded = $bindable(false), name, files, selected, onDelete }: Props = $props()
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
expanded = !expanded;
|
expanded = !expanded
|
||||||
}
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
|
|
||||||
const updateSelected = async (event:any) => {
|
|
||||||
dispatch('selected', { name:event.detail.name });
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button class="flex pl-2" onclick={toggle}>
|
<div class="folder-item">
|
||||||
|
<button class="flex items-center pl-2 hover:bg-gray-700 w-full rounded py-1" onclick={toggle}>
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<FolderOpenOutline class="w-6 h-6" />
|
<FolderOpenOutline class="w-5 h-5 mr-1" />
|
||||||
{:else}
|
{:else}
|
||||||
<FolderIcon class="w-6 h-6" />
|
<FolderIcon class="w-5 h-5 mr-1" />
|
||||||
{/if}
|
{/if}
|
||||||
{name}
|
<span class="text-sm">{name}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if expanded}
|
{#if expanded}
|
||||||
<ul class="ml-5 border-l border-slate-600">
|
<ul class="ml-4 border-l border-gray-600 mt-1">
|
||||||
{#each Object.entries(files) as [name, content]}
|
{#each Object.entries(files) as [itemName, content]}
|
||||||
<li class="p-1">
|
<li class="py-1">
|
||||||
{#if typeof content == 'object'}
|
{#if typeof content === 'object'}
|
||||||
<Folder {name} files={content} on:selected={updateSelected} />
|
<Folder name={itemName} files={content} {selected} {onDelete} />
|
||||||
{:else}
|
{:else}
|
||||||
<File {name} on:selected={updateSelected}/>
|
<File name={itemName} {selected} {onDelete} />
|
||||||
{/if}
|
{/if}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { focusTrap } from 'svelte-focus-trap'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { exitBeforeEnter, modals, type ModalProps } from 'svelte-modals'
|
||||||
|
import { Cancel, Check } from '$lib/components/icons'
|
||||||
|
|
||||||
|
let { isOpen, onConfirm }: ModalProps = $props()
|
||||||
|
let fileName = $state('')
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (!fileName) return
|
||||||
|
onConfirm(fileName)
|
||||||
|
modals.close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
transition:fly={{ y: 50 }}
|
||||||
|
use:exitBeforeEnter
|
||||||
|
use:focusTrap>
|
||||||
|
<div
|
||||||
|
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||||
|
<h2 class="text-base-content text-start text-2xl font-bold">Create New File</h2>
|
||||||
|
<div class="divider my-2"></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="File name"
|
||||||
|
bind:value={fileName} />
|
||||||
|
<div class="divider my-2"></div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
|
||||||
|
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary inline-flex items-center" onclick={handleCreate}>
|
||||||
|
<Check class="mr-2 h-5 w-5" /><span>Create</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { focusTrap } from 'svelte-focus-trap'
|
||||||
|
import { fly } from 'svelte/transition'
|
||||||
|
import { exitBeforeEnter, modals, type ModalProps } from 'svelte-modals'
|
||||||
|
import { Cancel, Check } from '$lib/components/icons'
|
||||||
|
|
||||||
|
let { isOpen, onConfirm }: ModalProps = $props()
|
||||||
|
let folderName = $state('')
|
||||||
|
|
||||||
|
const handleCreate = () => {
|
||||||
|
if (!folderName) return
|
||||||
|
onConfirm(folderName)
|
||||||
|
modals.close()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<div
|
||||||
|
role="dialog"
|
||||||
|
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||||
|
transition:fly={{ y: 50 }}
|
||||||
|
use:exitBeforeEnter
|
||||||
|
use:focusTrap>
|
||||||
|
<div
|
||||||
|
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||||
|
<h2 class="text-base-content text-start text-2xl font-bold">Create New Folder</h2>
|
||||||
|
<div class="divider my-2"></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
placeholder="Folder name"
|
||||||
|
bind:value={folderName} />
|
||||||
|
<div class="divider my-2"></div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
|
||||||
|
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary inline-flex items-center" onclick={handleCreate}>
|
||||||
|
<Check class="mr-2 h-5 w-5" /><span>Create</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
const { icon, title, description } = $props()
|
|
||||||
|
|
||||||
const Icon = $derived(icon)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10 flex-none">
|
|
||||||
<Icon class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">{title}</div>
|
|
||||||
<div class="text-sm opacity-75">{description}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -6,12 +6,10 @@
|
|||||||
import Spinner from '$lib/components/Spinner.svelte'
|
import Spinner from '$lib/components/Spinner.svelte'
|
||||||
import { slide } from 'svelte/transition'
|
import { slide } from 'svelte/transition'
|
||||||
import { cubicOut } from 'svelte/easing'
|
import { cubicOut } from 'svelte/easing'
|
||||||
|
|
||||||
import type { SystemInformation, Analytics } from '$lib/types/models'
|
import type { SystemInformation, Analytics } from '$lib/types/models'
|
||||||
import { socket } from '$lib/stores/socket'
|
import { socket } from '$lib/stores/socket'
|
||||||
import { api } from '$lib/api'
|
import { api } from '$lib/api'
|
||||||
import { convertSeconds } from '$lib/utilities'
|
import { convertSeconds } from '$lib/utilities'
|
||||||
|
|
||||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||||
import {
|
import {
|
||||||
Cancel,
|
Cancel,
|
||||||
@@ -31,7 +29,7 @@
|
|||||||
Temperature,
|
Temperature,
|
||||||
Stopwatch
|
Stopwatch
|
||||||
} from '$lib/components/icons'
|
} from '$lib/components/icons'
|
||||||
import StatusItem from './StatusItem.svelte'
|
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||||
import ActionButton from './ActionButton.svelte'
|
import ActionButton from './ActionButton.svelte'
|
||||||
|
|
||||||
const features = useFeatureFlags()
|
const features = useFeatureFlags()
|
||||||
@@ -236,7 +234,7 @@
|
|||||||
{#each actionButtons as button}
|
{#each actionButtons as button}
|
||||||
{#if button.condition === undefined || button.condition()}
|
{#if button.condition === undefined || button.condition()}
|
||||||
<ActionButton
|
<ActionButton
|
||||||
on:click={button.onClick}
|
onclick={button.onClick}
|
||||||
icon={button.icon}
|
icon={button.icon}
|
||||||
label={button.label}
|
label={button.label}
|
||||||
type={button.type || 'primary'} />
|
type={button.type || 'primary'} />
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { preventDefault } from 'svelte/legacy';
|
import { preventDefault } from 'svelte/legacy'
|
||||||
|
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition'
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing'
|
||||||
import { PasswordInput } from '$lib/components/input';
|
import { PasswordInput } from '$lib/components/input'
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import { notifications } from '$lib/components/toasts/notifications'
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
import Spinner from '$lib/components/Spinner.svelte'
|
||||||
import type { ApSettings, ApStatus } from '$lib/types/models';
|
import type { ApSettings, ApStatus } from '$lib/types/models'
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api'
|
||||||
import { useFeatureFlags } from '$lib/stores';
|
import { useFeatureFlags } from '$lib/stores'
|
||||||
import { AP, Devices, Home, MAC } from '$lib/components/icons';
|
import { AP, Devices, Home, MAC } from '$lib/components/icons'
|
||||||
|
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags()
|
||||||
|
|
||||||
let apSettings: ApSettings = $state();
|
let apSettings: ApSettings = $state()
|
||||||
let apStatus: ApStatus = $state();
|
let apStatus: ApStatus = $state()
|
||||||
|
|
||||||
let formField: any = $state();
|
let formField: any = $state()
|
||||||
|
|
||||||
async function getAPStatus() {
|
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()) {
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
apStatus = result.inner;
|
apStatus = result.inner
|
||||||
return apStatus;
|
return apStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getAPSettings() {
|
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()) {
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
apSettings = result.inner;
|
apSettings = result.inner
|
||||||
return apSettings;
|
return apSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
getAPStatus();
|
getAPStatus()
|
||||||
}, 5000);
|
}, 5000)
|
||||||
|
|
||||||
onDestroy(() => clearInterval(interval));
|
onDestroy(() => clearInterval(interval))
|
||||||
|
|
||||||
onMount(getAPSettings);
|
onMount(getAPSettings)
|
||||||
|
|
||||||
let provisionMode = [
|
let provisionMode = [
|
||||||
{
|
{
|
||||||
@@ -61,13 +62,13 @@
|
|||||||
id: 2,
|
id: 2,
|
||||||
text: `Never`
|
text: `Never`
|
||||||
}
|
}
|
||||||
];
|
]
|
||||||
|
|
||||||
let apStatusDescription = [
|
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||||
{ bg_color: 'bg-success', text_color: 'text-success-content', description: 'Active' },
|
|
||||||
{ bg_color: 'bg-error', text_color: 'text-error-content', description: 'Inactive' },
|
let apStatusVariant: Variant[] = ['success', 'error', 'warning']
|
||||||
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
|
|
||||||
];
|
let apStatusDescription = ['Active', 'Inactive', 'Lingering']
|
||||||
|
|
||||||
let formErrors = $state({
|
let formErrors = $state({
|
||||||
ssid: false,
|
ssid: false,
|
||||||
@@ -76,79 +77,79 @@
|
|||||||
local_ip: false,
|
local_ip: false,
|
||||||
gateway_ip: false,
|
gateway_ip: false,
|
||||||
subnet_mask: false
|
subnet_mask: false
|
||||||
});
|
})
|
||||||
|
|
||||||
async function postAPSettings(data: ApSettings) {
|
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()) {
|
if (result.isErr()) {
|
||||||
notifications.error('User not authorized.', 3000);
|
notifications.error('User not authorized.', 3000)
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
notifications.success('Access Point settings updated.', 3000);
|
notifications.success('Access Point settings updated.', 3000)
|
||||||
apSettings = result.inner;
|
apSettings = result.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmitAP() {
|
function handleSubmitAP() {
|
||||||
let valid = true;
|
let valid = true
|
||||||
|
|
||||||
// Validate SSID
|
// Validate SSID
|
||||||
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.ssid = true;
|
formErrors.ssid = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.ssid = false;
|
formErrors.ssid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Channel
|
// Validate Channel
|
||||||
let channel = Number(apSettings.channel);
|
let channel = Number(apSettings.channel)
|
||||||
if (1 > channel || channel > 13) {
|
if (1 > channel || channel > 13) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.channel = true;
|
formErrors.channel = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.channel = false;
|
formErrors.channel = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate max_clients
|
// Validate max_clients
|
||||||
let maxClients = Number(apSettings.max_clients);
|
let maxClients = Number(apSettings.max_clients)
|
||||||
if (1 > maxClients || maxClients > 8) {
|
if (1 > maxClients || maxClients > 8) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.max_clients = true;
|
formErrors.max_clients = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.max_clients = false;
|
formErrors.max_clients = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegEx for IPv4
|
// RegEx for IPv4
|
||||||
const regexExp =
|
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
|
// Validate gateway IP
|
||||||
if (!regexExp.test(apSettings.gateway_ip)) {
|
if (!regexExp.test(apSettings.gateway_ip)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.gateway_ip = true;
|
formErrors.gateway_ip = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.gateway_ip = false;
|
formErrors.gateway_ip = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Subnet Mask
|
// Validate Subnet Mask
|
||||||
if (!regexExp.test(apSettings.subnet_mask)) {
|
if (!regexExp.test(apSettings.subnet_mask)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.subnet_mask = true;
|
formErrors.subnet_mask = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.subnet_mask = false;
|
formErrors.subnet_mask = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate local IP
|
// Validate local IP
|
||||||
if (!regexExp.test(apSettings.local_ip)) {
|
if (!regexExp.test(apSettings.local_ip)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.local_ip = true;
|
formErrors.local_ip = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.local_ip = false;
|
formErrors.local_ip = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit JSON to REST API
|
// Submit JSON to REST API
|
||||||
if (valid) {
|
if (valid) {
|
||||||
postAPSettings(apSettings);
|
postAPSettings(apSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -166,69 +167,25 @@
|
|||||||
{:then nothing}
|
{:then nothing}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1"
|
class="flex w-full flex-col space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
<StatusItem
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
icon={AP}
|
||||||
<div
|
title="Status"
|
||||||
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status]
|
variant={apStatusVariant[apStatus.status]}
|
||||||
.bg_color}"
|
description={apStatusDescription[apStatus.status]} />
|
||||||
>
|
|
||||||
<AP
|
|
||||||
class="h-auto w-full scale-75 {apStatusDescription[apStatus.status]
|
|
||||||
.text_color}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">Status</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{apStatusDescription[apStatus.status].description}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<Home class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">IP Address</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{apStatus.ip_address}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<MAC class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">MAC Address</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{apStatus.mac_address}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<Devices class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">AP Clients</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{apStatus.station_num}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium">
|
||||||
>
|
|
||||||
Change AP Settings
|
Change AP Settings
|
||||||
</div>
|
</div>
|
||||||
{#await getAPSettings()}
|
{#await getAPSettings()}
|
||||||
@@ -236,14 +193,12 @@
|
|||||||
{:then nothing}
|
{:then nothing}
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 p-0"
|
class="flex flex-col gap-2 p-0"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<form
|
<form
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||||
onsubmit={preventDefault(handleSubmitAP)}
|
onsubmit={preventDefault(handleSubmitAP)}
|
||||||
novalidate
|
novalidate
|
||||||
bind:this={formField}
|
bind:this={formField}>
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="apmode">
|
<label class="label" for="apmode">
|
||||||
<span class="label-text">Provide Access Point ...</span>
|
<span class="label-text">Provide Access Point ...</span>
|
||||||
@@ -251,8 +206,7 @@
|
|||||||
<select
|
<select
|
||||||
class="select select-bordered w-full"
|
class="select select-bordered w-full"
|
||||||
id="apmode"
|
id="apmode"
|
||||||
bind:value={apSettings.provision_mode}
|
bind:value={apSettings.provision_mode}>
|
||||||
>
|
|
||||||
{#each provisionMode as mode}
|
{#each provisionMode as mode}
|
||||||
<option value={mode.id}>
|
<option value={mode.id}>
|
||||||
{mode.text}
|
{mode.text}
|
||||||
@@ -275,13 +229,10 @@
|
|||||||
id="ssid"
|
id="ssid"
|
||||||
min="2"
|
min="2"
|
||||||
max="32"
|
max="32"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="ssid">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
>SSID must be between 2 and 32 characters long</span>
|
||||||
>SSID must be between 2 and 32 characters long</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -306,14 +257,10 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
bind:value={apSettings.channel}
|
bind:value={apSettings.channel}
|
||||||
id="channel"
|
id="channel"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="channel">
|
<label class="label" for="channel">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.channel ? '' : (
|
>Must be channel 1 to 13</span>
|
||||||
'hidden'
|
|
||||||
)}">Must be channel 1 to 13</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -332,14 +279,10 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
bind:value={apSettings.max_clients}
|
bind:value={apSettings.max_clients}
|
||||||
id="clients"
|
id="clients"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="clients">
|
<label class="label" for="clients">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.max_clients ? '' : (
|
>Maximum 8 clients allowed</span>
|
||||||
'hidden'
|
|
||||||
)}">Maximum 8 clients allowed</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -349,22 +292,18 @@
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.local_ip ?
|
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : (
|
||||||
'border-error border-2'
|
''
|
||||||
: ''}"
|
)}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.local_ip}
|
bind:value={apSettings.local_ip}
|
||||||
id="localIP"
|
id="localIP"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="localIP">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.local_ip ? '' : (
|
>Must be a valid IPv4 address</span>
|
||||||
'hidden'
|
|
||||||
)}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -374,22 +313,17 @@
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.gateway_ip}
|
bind:value={apSettings.gateway_ip}
|
||||||
id="gateway"
|
id="gateway"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="gateway">
|
<label class="label" for="gateway">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.gateway_ip ? '' : (
|
>Must be a valid IPv4 address</span>
|
||||||
'hidden'
|
|
||||||
)}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -398,22 +332,17 @@
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.subnet_mask}
|
bind:value={apSettings.subnet_mask}
|
||||||
id="subnet"
|
id="subnet"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="subnet">
|
<label class="label" for="subnet">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : (
|
>Must be a valid IPv4 address</span>
|
||||||
'hidden'
|
|
||||||
)}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -421,8 +350,7 @@
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={apSettings.ssid_hidden}
|
bind:checked={apSettings.ssid_hidden}
|
||||||
class="checkbox checkbox-primary"
|
class="checkbox checkbox-primary" />
|
||||||
/>
|
|
||||||
<span class="">Hide SSID</span>
|
<span class="">Hide SSID</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MDNS from './MDNS.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||||
|
<MDNS />
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte'
|
||||||
|
import { api } from '$lib/api'
|
||||||
|
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||||
|
import { AP, Home, MAC, Devices } from '$lib/components/icons'
|
||||||
|
import Spinner from '$lib/components/Spinner.svelte'
|
||||||
|
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||||
|
import { cubicOut } from 'svelte/easing'
|
||||||
|
import { slide } from 'svelte/transition'
|
||||||
|
import type { MDNSStatus, MDNSServiceItem, MDNSServiceQuery } from '$lib/types/models'
|
||||||
|
import { compareIp } from '$lib/utilities'
|
||||||
|
|
||||||
|
let mdnsStatus: MDNSStatus | undefined = $state()
|
||||||
|
let services: MDNSServiceItem[] = $state([])
|
||||||
|
let isLoading = $state(false)
|
||||||
|
|
||||||
|
const getMDNSStatus = async () => {
|
||||||
|
const result = await api.get<MDNSStatus>('/api/mdns/status')
|
||||||
|
if (result.isErr()) {
|
||||||
|
console.error('Error:', result.inner)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mdnsStatus = result.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryMDNSServices = async () => {
|
||||||
|
isLoading = true
|
||||||
|
const result = await api.post<MDNSServiceQuery>('/api/mdns/query', {
|
||||||
|
service: 'http',
|
||||||
|
protocol: 'tcp'
|
||||||
|
})
|
||||||
|
if (result.isErr()) {
|
||||||
|
console.error('Error:', result.inner)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
services = result.inner.services.sort((a, b) => compareIp(a.ip, b.ip))
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await getMDNSStatus()
|
||||||
|
await queryMDNSServices()
|
||||||
|
})
|
||||||
|
|
||||||
|
const triggerScan = async () => {
|
||||||
|
await queryMDNSServices()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SettingsCard collapsible={false}>
|
||||||
|
{#snippet icon()}
|
||||||
|
<AP class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||||
|
{/snippet}
|
||||||
|
{#snippet title()}
|
||||||
|
<span>MDNS</span>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet right()}
|
||||||
|
<button class="btn btn-primary" onclick={triggerScan} disabled={isLoading}>
|
||||||
|
{#if isLoading}
|
||||||
|
<span class="loading loading-ring loading-xs"></span>
|
||||||
|
{:else}
|
||||||
|
Scan
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/snippet}
|
||||||
|
<div class="w-full overflow-x-auto">
|
||||||
|
{#if mdnsStatus}
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-col space-y-1"
|
||||||
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
|
<StatusItem icon={Home} title="IP Address" description={mdnsStatus.hostname} />
|
||||||
|
|
||||||
|
<StatusItem icon={MAC} title="Instance" description={mdnsStatus.instance} />
|
||||||
|
|
||||||
|
<StatusItem icon={Devices} title="Services" description={mdnsStatus.services.length} />
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Ip address</th>
|
||||||
|
<th>Port</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each services as service}
|
||||||
|
<tr>
|
||||||
|
<td><Devices class="h-6 w-6" /></td>
|
||||||
|
<td>{service.name}</td>
|
||||||
|
<td>{service.ip}</td>
|
||||||
|
<td>{service.port}</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</SettingsCard>
|
||||||
+174
-334
@@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte'
|
||||||
import { modals } from 'svelte-modals';
|
import { modals } from 'svelte-modals'
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition'
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing'
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import { notifications } from '$lib/components/toasts/notifications'
|
||||||
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
|
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list'
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||||
import { PasswordInput } from '$lib/components/input';
|
import { PasswordInput } from '$lib/components/input'
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||||
import ScanNetworks from './Scan.svelte';
|
import ScanNetworks from './Scan.svelte'
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
import Spinner from '$lib/components/Spinner.svelte'
|
||||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
import InfoDialog from '$lib/components/InfoDialog.svelte'
|
||||||
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models';
|
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'
|
||||||
import { socket, useFeatureFlags } from '$lib/stores';
|
import { socket } from '$lib/stores'
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api'
|
||||||
import {
|
import {
|
||||||
Cancel,
|
Cancel,
|
||||||
Delete,
|
Delete,
|
||||||
@@ -32,9 +32,8 @@
|
|||||||
Add,
|
Add,
|
||||||
Scan,
|
Scan,
|
||||||
Edit
|
Edit
|
||||||
} from '$lib/components/icons';
|
} from '$lib/components/icons'
|
||||||
|
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||||
const features = useFeatureFlags();
|
|
||||||
|
|
||||||
let networkEditable: KnownNetworkItem = $state({
|
let networkEditable: KnownNetworkItem = $state({
|
||||||
ssid: '',
|
ssid: '',
|
||||||
@@ -45,21 +44,21 @@
|
|||||||
gateway_ip: undefined,
|
gateway_ip: undefined,
|
||||||
dns_ip_1: undefined,
|
dns_ip_1: undefined,
|
||||||
dns_ip_2: undefined
|
dns_ip_2: undefined
|
||||||
});
|
})
|
||||||
|
|
||||||
let static_ip_config = $state(false);
|
let static_ip_config = $state(false)
|
||||||
|
|
||||||
let newNetwork: boolean = $state(true);
|
let newNetwork: boolean = $state(true)
|
||||||
let showNetworkEditor: boolean = $state(false);
|
let showNetworkEditor: boolean = $state(false)
|
||||||
|
|
||||||
let wifiStatus: WifiStatus = $state();
|
let wifiStatus: WifiStatus = $state()
|
||||||
let wifiSettings: WifiSettings = $state();
|
let wifiSettings: WifiSettings = $state()
|
||||||
|
|
||||||
let dndNetworkList: KnownNetworkItem[] = $state([]);
|
let dndNetworkList: KnownNetworkItem[] = $state([])
|
||||||
|
|
||||||
let showWifiDetails = $state(false);
|
let showWifiDetails = $state(false)
|
||||||
|
|
||||||
let formField: any = $state();
|
let formField: any = $state()
|
||||||
|
|
||||||
let formErrors = $state({
|
let formErrors = $state({
|
||||||
ssid: false,
|
ssid: false,
|
||||||
@@ -68,155 +67,155 @@
|
|||||||
subnet_mask: false,
|
subnet_mask: false,
|
||||||
dns_1: false,
|
dns_1: false,
|
||||||
dns_2: false
|
dns_2: false
|
||||||
});
|
})
|
||||||
|
|
||||||
let formErrorhostname = $state(false);
|
let formErrorhostname = $state(false)
|
||||||
|
|
||||||
async function getWifiStatus() {
|
async function getWifiStatus() {
|
||||||
const result = await api.get<WifiStatus>('/api/wifi/sta/status');
|
const result = await api.get<WifiStatus>('/api/wifi/sta/status')
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error(`Error occurred while fetching: `, result.inner);
|
console.error(`Error occurred while fetching: `, result.inner)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
wifiStatus = result.inner;
|
wifiStatus = result.inner
|
||||||
return wifiStatus;
|
return wifiStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getWifiSettings() {
|
async function getWifiSettings() {
|
||||||
const result = await api.get<WifiSettings>('/api/wifi/sta/settings');
|
const result = await api.get<WifiSettings>('/api/wifi/sta/settings')
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error(`Error occurred while fetching: `, result.inner);
|
console.error(`Error occurred while fetching: `, result.inner)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
wifiSettings = result.inner;
|
wifiSettings = result.inner
|
||||||
dndNetworkList = wifiSettings.wifi_networks;
|
dndNetworkList = wifiSettings.wifi_networks
|
||||||
return wifiSettings;
|
return wifiSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => socket.off('WiFiSettings'));
|
onDestroy(() => socket.off('WiFiSettings'))
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
socket.on<WifiSettings>('WiFiSettings', data => {
|
socket.on<WifiSettings>('WiFiSettings', data => {
|
||||||
wifiSettings = data;
|
wifiSettings = data
|
||||||
dndNetworkList = wifiSettings.wifi_networks;
|
dndNetworkList = wifiSettings.wifi_networks
|
||||||
});
|
})
|
||||||
});
|
})
|
||||||
|
|
||||||
async function postWiFiSettings(data: WifiSettings) {
|
async function postWiFiSettings(data: WifiSettings) {
|
||||||
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data);
|
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data)
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error(`Error occurred while fetching: `, result.inner);
|
console.error(`Error occurred while fetching: `, result.inner)
|
||||||
notifications.error('User not authorized.', 3000);
|
notifications.error('User not authorized.', 3000)
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
wifiSettings = result.inner;
|
wifiSettings = result.inner
|
||||||
notifications.success('Wi-Fi settings updated.', 3000);
|
notifications.success('Wi-Fi settings updated.', 3000)
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHostName() {
|
function validateHostName() {
|
||||||
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
|
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
|
||||||
formErrorhostname = true;
|
formErrorhostname = true
|
||||||
} else {
|
} else {
|
||||||
formErrorhostname = false;
|
formErrorhostname = false
|
||||||
// Update global wifiSettings object
|
// Update global wifiSettings object
|
||||||
wifiSettings.wifi_networks = dndNetworkList;
|
wifiSettings.wifi_networks = dndNetworkList
|
||||||
// Post to REST API
|
// Post to REST API
|
||||||
postWiFiSettings(wifiSettings);
|
postWiFiSettings(wifiSettings)
|
||||||
console.log(wifiSettings);
|
console.log(wifiSettings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateWiFiForm(event: SubmitEvent) {
|
function validateWiFiForm(event: SubmitEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault()
|
||||||
let valid = true;
|
let valid = true
|
||||||
|
|
||||||
// Validate SSID
|
// Validate SSID
|
||||||
if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {
|
if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.ssid = true;
|
formErrors.ssid = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.ssid = false;
|
formErrors.ssid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
networkEditable.static_ip_config = static_ip_config;
|
networkEditable.static_ip_config = static_ip_config
|
||||||
|
|
||||||
if (networkEditable.static_ip_config) {
|
if (networkEditable.static_ip_config) {
|
||||||
// RegEx for IPv4
|
// RegEx for IPv4
|
||||||
const regexExp =
|
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
|
// Validate gateway IP
|
||||||
if (!regexExp.test(networkEditable.gateway_ip!)) {
|
if (!regexExp.test(networkEditable.gateway_ip!)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.gateway_ip = true;
|
formErrors.gateway_ip = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.gateway_ip = false;
|
formErrors.gateway_ip = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Subnet Mask
|
// Validate Subnet Mask
|
||||||
if (!regexExp.test(networkEditable.subnet_mask!)) {
|
if (!regexExp.test(networkEditable.subnet_mask!)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.subnet_mask = true;
|
formErrors.subnet_mask = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.subnet_mask = false;
|
formErrors.subnet_mask = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate local IP
|
// Validate local IP
|
||||||
if (!regexExp.test(networkEditable.local_ip!)) {
|
if (!regexExp.test(networkEditable.local_ip!)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.local_ip = true;
|
formErrors.local_ip = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.local_ip = false;
|
formErrors.local_ip = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate DNS 1
|
// Validate DNS 1
|
||||||
if (!regexExp.test(networkEditable.dns_ip_1!)) {
|
if (!regexExp.test(networkEditable.dns_ip_1!)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.dns_1 = true;
|
formErrors.dns_1 = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.dns_1 = false;
|
formErrors.dns_1 = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate DNS 2
|
// Validate DNS 2
|
||||||
if (!regexExp.test(networkEditable.dns_ip_2!)) {
|
if (!regexExp.test(networkEditable.dns_ip_2!)) {
|
||||||
valid = false;
|
valid = false
|
||||||
formErrors.dns_2 = true;
|
formErrors.dns_2 = true
|
||||||
} else {
|
} else {
|
||||||
formErrors.dns_2 = false;
|
formErrors.dns_2 = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
formErrors.local_ip = false;
|
formErrors.local_ip = false
|
||||||
formErrors.subnet_mask = false;
|
formErrors.subnet_mask = false
|
||||||
formErrors.gateway_ip = false;
|
formErrors.gateway_ip = false
|
||||||
formErrors.dns_1 = false;
|
formErrors.dns_1 = false
|
||||||
formErrors.dns_2 = false;
|
formErrors.dns_2 = false
|
||||||
}
|
}
|
||||||
// Submit JSON to REST API
|
// Submit JSON to REST API
|
||||||
if (valid) {
|
if (valid) {
|
||||||
if (newNetwork) {
|
if (newNetwork) {
|
||||||
dndNetworkList.push(networkEditable);
|
dndNetworkList.push(networkEditable)
|
||||||
} else {
|
} else {
|
||||||
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable);
|
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable)
|
||||||
}
|
}
|
||||||
addNetwork();
|
addNetwork()
|
||||||
dndNetworkList = [...dndNetworkList]; //Trigger reactivity
|
dndNetworkList = [...dndNetworkList] //Trigger reactivity
|
||||||
showNetworkEditor = false;
|
showNetworkEditor = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanForNetworks() {
|
function scanForNetworks() {
|
||||||
modals.open(ScanNetworks, {
|
modals.open(ScanNetworks, {
|
||||||
storeNetwork: (network: string) => {
|
storeNetwork: (network: string) => {
|
||||||
addNetwork();
|
addNetwork()
|
||||||
networkEditable.ssid = network;
|
networkEditable.ssid = network
|
||||||
showNetworkEditor = true;
|
showNetworkEditor = true
|
||||||
modals.close();
|
modals.close()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function addNetwork() {
|
function addNetwork() {
|
||||||
newNetwork = true;
|
newNetwork = true
|
||||||
networkEditable = {
|
networkEditable = {
|
||||||
ssid: '',
|
ssid: '',
|
||||||
password: '',
|
password: '',
|
||||||
@@ -226,13 +225,13 @@
|
|||||||
gateway_ip: undefined,
|
gateway_ip: undefined,
|
||||||
dns_ip_1: undefined,
|
dns_ip_1: undefined,
|
||||||
dns_ip_2: undefined
|
dns_ip_2: undefined
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEdit(index: number) {
|
function handleEdit(index: number) {
|
||||||
newNetwork = false;
|
newNetwork = false
|
||||||
showNetworkEditor = true;
|
showNetworkEditor = true
|
||||||
networkEditable = dndNetworkList[index];
|
networkEditable = dndNetworkList[index]
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(index: number) {
|
function confirmDelete(index: number) {
|
||||||
@@ -246,15 +245,15 @@
|
|||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
// Check if network is currently been edited and delete as well
|
// Check if network is currently been edited and delete as well
|
||||||
if (dndNetworkList[index].ssid === networkEditable.ssid) {
|
if (dndNetworkList[index].ssid === networkEditable.ssid) {
|
||||||
addNetwork();
|
addNetwork()
|
||||||
}
|
}
|
||||||
// Remove network from array
|
// Remove network from array
|
||||||
dndNetworkList.splice(index, 1);
|
dndNetworkList.splice(index, 1)
|
||||||
dndNetworkList = [...dndNetworkList]; //Trigger reactivity
|
dndNetworkList = [...dndNetworkList] //Trigger reactivity
|
||||||
showNetworkEditor = false;
|
showNetworkEditor = false
|
||||||
modals.close();
|
modals.close()
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkNetworkList() {
|
function checkNetworkList() {
|
||||||
@@ -265,20 +264,20 @@
|
|||||||
'You have reached the maximum number of networks. Please delete one to add another.',
|
'You have reached the maximum number of networks. Please delete one to add another.',
|
||||||
dismiss: { label: 'OK', icon: Check },
|
dismiss: { label: 'OK', icon: Check },
|
||||||
onDismiss: () => modals.close()
|
onDismiss: () => modals.close()
|
||||||
});
|
})
|
||||||
return false;
|
return false
|
||||||
} else {
|
} else {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) {
|
function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) {
|
||||||
if (!to || from === to) {
|
if (!to || from === to) {
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dndNetworkList = reorder(dndNetworkList, from.index, to.index);
|
dndNetworkList = reorder(dndNetworkList, from.index, to.index)
|
||||||
console.log(dndNetworkList);
|
console.log(dndNetworkList)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -295,78 +294,32 @@
|
|||||||
{:then nothing}
|
{:then nothing}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1"
|
class="flex w-full flex-col space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
<StatusItem
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
icon={AP}
|
||||||
<div
|
title="Status"
|
||||||
class="mask mask-hexagon h-auto w-10 {wifiStatus.status === 3 ?
|
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
||||||
'bg-success'
|
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
||||||
: 'bg-error'}"
|
|
||||||
>
|
|
||||||
<AP
|
|
||||||
class="h-auto w-full scale-75 {wifiStatus.status === 3 ?
|
|
||||||
'text-success-content'
|
|
||||||
: 'text-error-content'}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">Status</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.status === 3 ? 'Connected' : 'Inactive'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if wifiStatus.status === 3}
|
{#if wifiStatus.status === 3}
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<SSID class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">SSID</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.ssid}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<Home class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">IP Address</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.local_ip}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<WiFi class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">RSSI</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.rssi} dBm
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grow"></div>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-circle btn-ghost btn-sm modal-button"
|
class="btn btn-circle btn-ghost btn-sm modal-button"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
showWifiDetails = !showWifiDetails;
|
showWifiDetails = !showWifiDetails
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Down
|
<Down
|
||||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||||
showWifiDetails
|
showWifiDetails
|
||||||
) ?
|
) ?
|
||||||
'rotate-180'
|
'rotate-180'
|
||||||
: ''}"
|
: ''}" />
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</StatusItem>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -374,67 +327,16 @@
|
|||||||
{#if showWifiDetails}
|
{#if showWifiDetails}
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1 pt-1"
|
class="flex w-full flex-col space-y-1 pt-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<MAC class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">MAC Address</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.mac_address}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<Channel class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">Channel</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.channel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<Gateway class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">Gateway IP</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.gateway_ip}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<Subnet class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">Subnet Mask</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.subnet_mask}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
|
||||||
<DNS class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">DNS</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
{wifiStatus.dns_ip_1}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
@@ -442,8 +344,7 @@
|
|||||||
|
|
||||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||||
<div
|
<div
|
||||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium">
|
||||||
>
|
|
||||||
Saved Networks
|
Saved Networks
|
||||||
</div>
|
</div>
|
||||||
{#await getWifiSettings()}
|
{#await getWifiSettings()}
|
||||||
@@ -454,67 +355,48 @@
|
|||||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (checkNetworkList()) {
|
if (checkNetworkList()) {
|
||||||
addNetwork();
|
addNetwork()
|
||||||
showNetworkEditor = true;
|
showNetworkEditor = true
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
>
|
<Add class="h-6 w-6" /></button>
|
||||||
<Add class="h-6 w-6" /></button
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
if (checkNetworkList()) {
|
if (checkNetworkList()) {
|
||||||
scanForNetworks();
|
scanForNetworks()
|
||||||
showNetworkEditor = true;
|
showNetworkEditor = true
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
>
|
<Scan class="h-6 w-6" /></button>
|
||||||
<Scan class="h-6 w-6" /></button
|
|
||||||
>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="overflow-x-auto space-y-1"
|
class="overflow-x-auto space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<DragDropList
|
<DragDropList
|
||||||
id="networks"
|
id="networks"
|
||||||
type={VerticalDropZone}
|
type={VerticalDropZone}
|
||||||
itemSize={60}
|
itemSize={60}
|
||||||
itemCount={dndNetworkList.length}
|
itemCount={dndNetworkList.length}
|
||||||
on:drop={onDrop}
|
on:drop={onDrop}>
|
||||||
>
|
|
||||||
{#snippet children({ index })}
|
{#snippet children({ index })}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<StatusItem icon={Router} title={dndNetworkList[index].ssid}>
|
||||||
<div
|
|
||||||
class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"
|
|
||||||
>
|
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
|
||||||
<Router class="text-primary-content h-auto w-full scale-75" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">{dndNetworkList[index].ssid}</div>
|
|
||||||
</div>
|
|
||||||
<div class="grow"></div>
|
|
||||||
<div class="space-x-0 px-0 mx-0">
|
<div class="space-x-0 px-0 mx-0">
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm"
|
class="btn btn-ghost btn-sm"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
handleEdit(index);
|
handleEdit(index)
|
||||||
}}
|
}}>
|
||||||
>
|
<Edit class="h-6 w-6" /></button>
|
||||||
<Edit class="h-6 w-6" /></button
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn-ghost btn-sm"
|
class="btn btn-ghost btn-sm"
|
||||||
onclick={() => {
|
onclick={() => {
|
||||||
confirmDelete(index);
|
confirmDelete(index)
|
||||||
}}
|
}}>
|
||||||
>
|
|
||||||
<Delete class="text-error h-6 w-6" />
|
<Delete class="text-error h-6 w-6" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</StatusItem>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DragDropList>
|
</DragDropList>
|
||||||
</div>
|
</div>
|
||||||
@@ -523,8 +405,7 @@
|
|||||||
<div class="divider mb-0"></div>
|
<div class="divider mb-0"></div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col gap-2 p-0"
|
class="flex flex-col gap-2 p-0"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
<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 class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
@@ -542,24 +423,17 @@
|
|||||||
: ''}"
|
: ''}"
|
||||||
bind:value={wifiSettings.hostname}
|
bind:value={wifiSettings.hostname}
|
||||||
id="channel"
|
id="channel"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="channel">
|
<label class="label" for="channel">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrorhostname ? '' : (
|
>Host name must be between 2 and 32 characters long</span>
|
||||||
'hidden'
|
|
||||||
)}">Host name must be between 2 and 32 characters long</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||||
class="label inline-flex cursor-pointer content-end justify-start gap-4"
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={wifiSettings.priority_RSSI}
|
bind:checked={wifiSettings.priority_RSSI}
|
||||||
class="checkbox checkbox-primary sm:-mb-5"
|
class="checkbox checkbox-primary sm:-mb-5" />
|
||||||
/>
|
|
||||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -568,8 +442,7 @@
|
|||||||
<div class="divider my-0"></div>
|
<div class="divider my-0"></div>
|
||||||
<div
|
<div
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
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 }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="ssid">
|
||||||
<span class="label-text text-md">SSID</span>
|
<span class="label-text text-md">SSID</span>
|
||||||
@@ -585,14 +458,10 @@
|
|||||||
id="ssid"
|
id="ssid"
|
||||||
min="2"
|
min="2"
|
||||||
max="32"
|
max="32"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="ssid">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.ssid ? '' : (
|
>SSID must be between 3 and 32 characters long</span>
|
||||||
'hidden'
|
|
||||||
)}">SSID must be between 3 and 32 characters long</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -602,21 +471,18 @@
|
|||||||
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
||||||
</div>
|
</div>
|
||||||
<label
|
<label
|
||||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4"
|
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={static_ip_config}
|
bind:checked={static_ip_config}
|
||||||
class="checkbox checkbox-primary sm:-mb-5"
|
class="checkbox checkbox-primary sm:-mb-5" />
|
||||||
/>
|
|
||||||
<span class="sm:-mb-5">Static IP Config?</span>
|
<span class="sm:-mb-5">Static IP Config?</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{#if static_ip_config}
|
{#if static_ip_config}
|
||||||
<div
|
<div
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
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 }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="localIP">
|
||||||
<span class="label-text text-md">Local IP</span>
|
<span class="label-text text-md">Local IP</span>
|
||||||
@@ -631,14 +497,10 @@
|
|||||||
size="15"
|
size="15"
|
||||||
bind:value={networkEditable.local_ip}
|
bind:value={networkEditable.local_ip}
|
||||||
id="localIP"
|
id="localIP"
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="localIP">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {formErrors.local_ip ?
|
>Must be a valid IPv4 address</span>
|
||||||
''
|
|
||||||
: 'hidden'}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -655,16 +517,10 @@
|
|||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={networkEditable.gateway_ip}
|
bind:value={networkEditable.gateway_ip}
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="gateway">
|
<label class="label" for="gateway">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||||
class="label-text-alt text-error {(
|
>Must be a valid IPv4 address</span>
|
||||||
formErrors.gateway_ip
|
|
||||||
) ?
|
|
||||||
''
|
|
||||||
: 'hidden'}">Must be a valid IPv4 address</span
|
|
||||||
>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -680,16 +536,10 @@
|
|||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={networkEditable.subnet_mask}
|
bind:value={networkEditable.subnet_mask}
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="subnet">
|
<label class="label" for="subnet">
|
||||||
<span
|
<span
|
||||||
class="label-text-alt text-error {(
|
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
||||||
formErrors.subnet_mask
|
|
||||||
) ?
|
|
||||||
''
|
|
||||||
: 'hidden'}"
|
|
||||||
>
|
|
||||||
Must be a valid IPv4 address
|
Must be a valid IPv4 address
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -700,20 +550,15 @@
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.dns_1 ?
|
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={networkEditable.dns_ip_1}
|
bind:value={networkEditable.dns_ip_1}
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="gateway">
|
<label class="label" for="gateway">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
||||||
class="label-text-alt text-error {formErrors.dns_1 ? ''
|
|
||||||
: 'hidden'}"
|
|
||||||
>
|
|
||||||
Must be a valid IPv4 address
|
Must be a valid IPv4 address
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -724,20 +569,15 @@
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.dns_2 ?
|
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={networkEditable.dns_ip_2}
|
bind:value={networkEditable.dns_ip_2}
|
||||||
required
|
required />
|
||||||
/>
|
|
||||||
<label class="label" for="subnet">
|
<label class="label" for="subnet">
|
||||||
<span
|
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
||||||
class="label-text-alt text-error {formErrors.dns_2 ? ''
|
|
||||||
: 'hidden'}"
|
|
||||||
>
|
|
||||||
Must be a valid IPv4 address
|
Must be a valid IPv4 address
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
# Components
|
# Components
|
||||||
|
|
||||||
Spot is comprised of a 3D printed body, some hardware and list of electronic components.
|
Spot is comprised of a 3D-printed body, some hardware, and a list of electronic components.
|
||||||
|
|
||||||
## Hardware
|
## Hardware
|
||||||
|
|
||||||
Spot is 3D printed and is a combination of different Spot Micro designs, with some minor modification on top.
|
Spot is 3D-printed and is a combination of different Spot Micro designs, with some minor modifications.
|
||||||
The original design is developed by KDY0523.
|
The original design was developed by KDY0523.
|
||||||
|
|
||||||
- [robjk reinforced shoulder remix](https://www.thingiverse.com/thing:4937631)
|
- [robjk reinforced shoulder remix](https://www.thingiverse.com/thing:4937631)
|
||||||
- [Kooba SpotMicroESP32 remix](https://www.thingiverse.com/thing:4559827)
|
- [Kooba SpotMicroESP32 remix](https://www.thingiverse.com/thing:4559827)
|
||||||
- [KDY0532 original design](https://www.thingiverse.com/thing:3445283)
|
- [KDY0532 original design](https://www.thingiverse.com/thing:3445283)
|
||||||
|
|
||||||
The 3D prints is assembled with some additional component:
|
The 3D prints are assembled with some additional non-printable components:
|
||||||
|
|
||||||
- 84x M2x8 screws + M2 nuts
|
- 84x M2x8 screws + M2 nuts
|
||||||
- 92x M3x8 screws + M3 nuts
|
- 92x M3x8 screws + M3 nuts
|
||||||
@@ -20,7 +20,7 @@ The 3D prints is assembled with some additional component:
|
|||||||
|
|
||||||
## Electronics
|
## Electronics
|
||||||
|
|
||||||
These are the electronics i used for mine and can easily be switched up to suit your Spot's needs.
|
These are the electronics I used for mine, and they can easily be swapped to suit your Spot's needs.
|
||||||
|
|
||||||
| Component | Specification | Required | Recommendation |
|
| Component | Specification | Required | Recommendation |
|
||||||
| ------------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
|
| ------------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
|
||||||
@@ -39,6 +39,6 @@ These are the electronics i used for mine and can easily be switched up to suit
|
|||||||
| 7.6-8.4V Battery | Battery | No | Im using 4x 18650 in 2s2p configuration, but other people have 2s LiPos. |
|
| 7.6-8.4V Battery | Battery | No | Im using 4x 18650 in 2s2p configuration, but other people have 2s LiPos. |
|
||||||
| 4x Servo extension cables | Servo extension cables | Yes | You can either buy them or make them with a couple or headers and some cable. |
|
| 4x Servo extension cables | Servo extension cables | Yes | You can either buy them or make them with a couple or headers and some cable. |
|
||||||
|
|
||||||
I recommend getting a ESP32-S3 with a camera, allowing for more computation and imaging capabilities.
|
I recommend getting an ESP32-S3 with a camera, allowing for more computation and imaging capabilities.
|
||||||
|
|
||||||
It means a more responsive robot as its faster doing sensor fusion, calculating kinematic and gait planning, and networking.
|
It means a more responsive robot as it's faster at doing sensor fusion, calculating kinematics and gait planning, and networking.
|
||||||
|
|||||||
+20
-5
@@ -1,6 +1,6 @@
|
|||||||
# Assembly and calibration
|
# Assembly and calibration
|
||||||
|
|
||||||
There exist a number of great resources for the assembly of the spot micro. For this reason I refer to these, as the steps are the same for this version:
|
There are a number of great resources for the assembly of the Spot Micro. For this reason, I refer to these, as the steps are the same for this version:
|
||||||
|
|
||||||
- [Michael Kubina SpotMicroESP32 assembly](https://github.com/michaelkubina/SpotMicroESP32/tree/master/assembly)
|
- [Michael Kubina SpotMicroESP32 assembly](https://github.com/michaelkubina/SpotMicroESP32/tree/master/assembly)
|
||||||
- [Spot Micro AI assembly](https://spotmicroai.readthedocs.io/en/latest/assembly/)
|
- [Spot Micro AI assembly](https://spotmicroai.readthedocs.io/en/latest/assembly/)
|
||||||
@@ -9,7 +9,7 @@ There exist a number of great resources for the assembly of the spot micro. For
|
|||||||
|
|
||||||
Discussion about [Calibration](https://github.com/runeharlyk/SpotMicroESP32-Leika/discussions/118)
|
Discussion about [Calibration](https://github.com/runeharlyk/SpotMicroESP32-Leika/discussions/118)
|
||||||
|
|
||||||
Assuming the servos are connected to the PCA9685 and is powered on:
|
Assuming the servos are connected to the PCA9685 and are powered on:
|
||||||
|
|
||||||
### Calibrate in servo frame
|
### Calibrate in servo frame
|
||||||
|
|
||||||
@@ -38,13 +38,28 @@ You now have the values for the servos.
|
|||||||
|
|
||||||
### Calibration in body frame
|
### Calibration in body frame
|
||||||
|
|
||||||
They now has to calibrated to the body frame. It assumed they have the center pwm pointing straight down.
|
They now have to be calibrated to the body frame. It is assumed they have the center PWM pointing straight down.
|
||||||
|
|
||||||
1. Navigate to `/controller` and click on "Calibrate". This will set the servo to the center pwm value.
|
1. Navigate to `/controller` and click on "Calibrate". This will set the servo to the center pwm value.
|
||||||
2. Navigate to `peripherals/servo` - Here you can set the servo angle offset.
|
2. Navigate to `peripherals/servo` - Here you can set the servo angle offset.
|
||||||
|
|
||||||
All the legs should be pointing down. If they are not you have to options. 1; Physically move the servos to the correct position by un screwing the servo horns. 2; Update the servo offset in the servo table.
|
All the legs should be pointing down. If they are not, you have two options. 1. Physically move the servos to the correct position by unscrewing the servo horns. 2. Update the servo offset in the servo table.
|
||||||
|
|
||||||
## Circuit diagram
|
## Circuit diagram
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
PCA9685 Servo PWM numbers to joint:
|
||||||
|
| PWM_0 | Front Left Shoulder |
|
||||||
|
|--------|------------------------------|
|
||||||
|
| PWM_1 | Front Left Upper-Limb |
|
||||||
|
| PWM_2 | Front Left Leg (Lower-Limb) |
|
||||||
|
| PWM_3 | Front Right Shoulder |
|
||||||
|
| PWM_4 | Front Right Upper-Limb |
|
||||||
|
| PWM_5 | Front Right Leg (Lower-Limb) |
|
||||||
|
| PWM_6 | Rear Left Shoulder |
|
||||||
|
| PWM_7 | Rear Left Upper-Limb |
|
||||||
|
| PWM_8 | Rear Left Leg (Lower-Limb) |
|
||||||
|
| PWM_9 | Rear Right Shoulder |
|
||||||
|
| PWM_10 | Rear Right Upper-Limb |
|
||||||
|
| PWM_11 | Rear Right Leg (Lower-limb) |
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
# Software
|
# Software
|
||||||
|
|
||||||
The robots firmware is built using platform io using the arduino framework over ESP-IDF.
|
The robot's firmware is built using PlatformIO with the Arduino framework over ESP-IDF.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ To prepare the frontend code for the ESP32, a specific build chain is required.
|
|||||||
|
|
||||||
### Required Software
|
### Required Software
|
||||||
|
|
||||||
Install the following software to ensure all functionalities:
|
Install the following software to ensure all functionality:
|
||||||
|
|
||||||
- [VSCode](https://code.visualstudio.com/) - Preferred IDE for development
|
- [VSCode](https://code.visualstudio.com/) - Preferred IDE for development
|
||||||
- [Node.js](https://nodejs.org) - Needed for app building
|
- [Node.js](https://nodejs.org) - Needed for app building
|
||||||
@@ -45,9 +45,9 @@ For additional boards, refer to the [official board list](https://docs.platformi
|
|||||||
|
|
||||||
### Factory settings
|
### Factory settings
|
||||||
|
|
||||||
Update the `esp32/factory_setting.ini` with new wifi settings, app name and other device information.
|
Update the `esp32/factory_setting.ini` with new Wi-Fi settings, app name and other device information.
|
||||||
|
|
||||||
### Build & Upload Process
|
### Build & Upload Process
|
||||||
|
|
||||||
Update the `platformio.ini` file for your board, then navigate to the PlatformIO tab, select your environment, click `Upload Filesystem Image` and after uploading finish, click `Upload and Monitor`. The filesystem image only has to be uploaded the first time and will override config files on the microcontroller.
|
Update the `platformio.ini` file for your board, then navigate to the PlatformIO tab, select your environment, click `Upload Filesystem Image` and after uploading finishes, click `Upload and Monitor`. The filesystem image only needs to be uploaded the first time. It will override config files on the microcontroller.
|
||||||
When uploading new firmware the app is evaluated and if necessary will be rebuild.
|
When uploading new firmware, the app is evaluated, and if necessary, will be rebuilt.
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
## Connecting to the network
|
## Connecting to the network
|
||||||
|
|
||||||
If the wifi settings were configured using `esp32/factory_settings.ini` the robot will try to connect to the network.
|
If the Wi-Fi settings were configured using `esp32/factory_settings.ini`, the robot will try to connect to the network.
|
||||||
|
|
||||||
If it fails to connect, it will host a AP with a captive portal where it's possible to configure wifi settings.
|
If it fails to connect, it will host an AP with a captive portal where it's possible to configure Wi-Fi settings.
|
||||||
|
|
||||||
When the robot connect successfully the ip address will be printed to the serial monitor
|
When the robot connects successfully, the IP address will be printed to the serial monitor.
|
||||||
|
|
||||||
<!-- ## Calibrating servos -->
|
<!-- ## Calibrating servos -->
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
# Running the spot
|
# Running the spot
|
||||||
|
|
||||||
> *Prerequsition*: You have successfully build, flashed and configured your robot.
|
> *Prerequsition*: You have successfully built, flashed, and configured your robot.
|
||||||
|
|
||||||
Navigate to `/controller`
|
Navigate to `/controller`
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
<!-- When the robot is in a safe position, click on rest.
|
<!-- When the robot is in a safe position, click on Rest.
|
||||||
This will activate the servos and put the robot in the rest position. -->
|
This will activate the servos and put the robot in the rest position. -->
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
# Developing
|
# Developing
|
||||||
|
|
||||||
> _Prerequsition_: You have successfully build, flashed and configured your robot.
|
> _Prerequsition_: You have successfully built, flashed, and configured your robot.
|
||||||
|
|
||||||
## Setting up SvelteKit
|
## Setting up SvelteKit
|
||||||
|
|
||||||
### Proxy Configuration for Development
|
### Proxy Configuration for Development
|
||||||
|
|
||||||
Configure the proxy settings in the `vite.config.ts` file to direct API calls to your ESP32 device. By default it used the factory MDNS address, but can be changed to the ip if preferred.
|
Configure the proxy settings in the `vite.config.ts` file to direct API calls to your ESP32 device. By default, it uses the factory MDNS address, but it can be changed to the IP if preferred.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
server: {
|
server: {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
@@ -1,10 +1,10 @@
|
|||||||
# 🏁 Motion state controller
|
# 🏁 Motion state controller
|
||||||
|
|
||||||
The motion controller is a finite state machine with state allowing for static and dynamic posing, 8-phase crawl and bezier bases trot gait, and choreographed animation.
|
The motion controller is a finite state machine that allows for static and dynamic posing, 8-phase crawl, bezier-based trot gait, and choreographed animation.
|
||||||
|
|
||||||
## Controller Input Mapping
|
## Controller Input Mapping
|
||||||
|
|
||||||
The controller input is interpret different between the modes. For the walking it it looks like this:
|
The controller input is interpreted differently between the modes. For walking, it looks like this:
|
||||||
|
|
||||||
| Controller Input | Mapped to Gait Step | Range |
|
| Controller Input | Mapped to Gait Step | Range |
|
||||||
| ---------------- | ------------------- | ------- |
|
| ---------------- | ------------------- | ------- |
|
||||||
@@ -21,7 +21,7 @@ The controller input is interpret different between the modes. For the walking i
|
|||||||
|
|
||||||
## Walking gait
|
## Walking gait
|
||||||
|
|
||||||
General about walking gait
|
General description of walking gait.
|
||||||
|
|
||||||
Time step
|
Time step
|
||||||
|
|
||||||
@@ -31,9 +31,9 @@ Stance and swing controller
|
|||||||
|
|
||||||
## 8-phase crawl gait
|
## 8-phase crawl gait
|
||||||
|
|
||||||
The 8-phase crawl gait works by lifting one leg at a time while shifting its body weight away from the leg.
|
The 8-phase crawl gait works by lifting one leg at a time while shifting the body weight away from that leg.
|
||||||
|
|
||||||
As the name implies, the gait consist of 8 discrete phases, which represents which feet should be contact the ground or be in swing.
|
As the name implies, the gait consists of 8 discrete phases, which represent which feet should be in contact with the ground or in swing.
|
||||||
|
|
||||||
At each time step the phase time $t\in [0,1]$ is updated. When $t\geq 1$ the phase index is updated and phase time is reset.
|
At each time step the phase time $t\in [0,1]$ is updated. When $t\geq 1$ the phase index is updated and phase time is reset.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
data/
|
data/
|
||||||
www/
|
www/
|
||||||
build/
|
build/
|
||||||
lib/ESP32-sveltekit/WWWData.h
|
include/WWWData.h
|
||||||
**/.vscode/c_cpp_properties.json
|
**/.vscode/c_cpp_properties.json
|
||||||
**/.vscode/launch.json
|
**/.vscode/launch.json
|
||||||
@@ -6,3 +6,5 @@ build_flags =
|
|||||||
-D SERVE_CONFIG_FILES
|
-D SERVE_CONFIG_FILES
|
||||||
-D CORS_ORIGIN=\"*\"
|
-D CORS_ORIGIN=\"*\"
|
||||||
-D ENABLE_CORS
|
-D ENABLE_CORS
|
||||||
|
-D USE_MSGPACK=1 ; Use either msgpack or json
|
||||||
|
-D USE_JSON=0 ; Use either msgpack or json
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
[factory_settings]
|
[factory_settings]
|
||||||
build_flags =
|
build_flags =
|
||||||
-D APP_NAME=\"Spot-Micro\" ; [a-zA-Z0-9-_]
|
-D APP_NAME=\"Spot-Micro\" ; [a-zA-Z0-9-_]
|
||||||
-D APP_VERSION=\"0.0.1\"
|
-D APP_VERSION=\"0.1.0\"
|
||||||
|
|
||||||
; WiFi settings
|
; WiFi settings
|
||||||
-D FACTORY_WIFI_SSID=\"\"
|
-D FACTORY_WIFI_SSID=\"\"
|
||||||
|
|||||||
+13
-7
@@ -1,15 +1,21 @@
|
|||||||
[features]
|
[features]
|
||||||
build_flags =
|
build_flags =
|
||||||
-D USE_SLEEP=0
|
; Kinematics - Choose only one
|
||||||
-D USE_UPLOAD_FIRMWARE=1
|
-D SPOTMICRO_ESP32
|
||||||
|
;-D SPOTMICRO_YERTLE
|
||||||
|
|
||||||
|
; Firmware flags
|
||||||
|
-D USE_SLEEP=1
|
||||||
|
-D USE_UPLOAD_FIRMWARE=0
|
||||||
-D USE_DOWNLOAD_FIRMWARE=0
|
-D USE_DOWNLOAD_FIRMWARE=0
|
||||||
-D USE_MOTION=1
|
-D USE_MOTION=1
|
||||||
|
-D USE_MDNS=1
|
||||||
|
|
||||||
; Hardware specific
|
; Hardware specific
|
||||||
-D USE_IMU=0
|
-D USE_HMC5883=0
|
||||||
-D USE_MAG=0
|
-D USE_BMP180=0
|
||||||
-D USE_BMP=0
|
-D USE_MPU6050=0
|
||||||
-D USE_GPS=0
|
|
||||||
-D USE_WS2812=1
|
-D USE_WS2812=1
|
||||||
|
-D USE_BNO055=0
|
||||||
-D USE_USS=0
|
-D USE_USS=0
|
||||||
-D USE_SERVO=1
|
-D USE_PCA9685=1
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#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());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#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
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
#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; }
|
||||||
|
};
|
||||||
@@ -28,18 +28,23 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ESP32 IMU on by default
|
// ESP32 IMU on by default
|
||||||
#ifndef USE_IMU
|
#ifndef USE_MPU6050
|
||||||
#define USE_IMU 1
|
#define USE_MPU6050 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ESP32 IMU on by default
|
||||||
|
#ifndef USE_BNO055
|
||||||
|
#define USE_BNO055 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ESP32 magnetometer on by default
|
// ESP32 magnetometer on by default
|
||||||
#ifndef USE_MAG
|
#ifndef USE_HMC5883
|
||||||
#define USE_MAG 0
|
#define USE_HMC5883 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ESP32 barometer off by default
|
// ESP32 barometer off by default
|
||||||
#ifndef USE_BMP
|
#ifndef USE_BMP180
|
||||||
#define USE_BMP 0
|
#define USE_BMP180 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ESP32 SONAR off by default
|
// ESP32 SONAR off by default
|
||||||
@@ -47,13 +52,37 @@
|
|||||||
#define USE_USS 0
|
#define USE_USS 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// PCA9685 Servo controller on by default
|
||||||
|
#ifndef USE_PCA9685
|
||||||
|
#define USE_PCA9685 1
|
||||||
|
#endif
|
||||||
|
|
||||||
// ESP32 GPS off by default
|
// ESP32 GPS off by default
|
||||||
#ifndef USE_GPS
|
#ifndef USE_GPS
|
||||||
#define USE_GPS 0
|
#define USE_GPS 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ESP32 MDNS on by default
|
||||||
|
#ifndef USE_MDNS
|
||||||
|
#define USE_MDNS 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ESP32 MSGPACK on by default
|
||||||
|
#ifndef USE_MSGPACK
|
||||||
|
#define USE_MSGPACK 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ESP32 JSON off by default
|
||||||
|
#ifndef USE_JSON
|
||||||
|
#define USE_JSON 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static_assert(!(USE_JSON == 1 && USE_MSGPACK == 1), "Cannot set both USE_JSON and USE_MSGPACK to 1 simultaneously");
|
||||||
|
|
||||||
namespace feature_service {
|
namespace feature_service {
|
||||||
|
|
||||||
|
void printFeatureConfiguration();
|
||||||
|
|
||||||
void features(JsonObject &root);
|
void features(JsonObject &root);
|
||||||
|
|
||||||
esp_err_t getFeatures(PsychicRequest *request);
|
esp_err_t getFeatures(PsychicRequest *request);
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
#define DEVICE_CONFIG_FILE "/config/peripheral.json"
|
#define DEVICE_CONFIG_FILE "/config/peripheral.json"
|
||||||
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
|
#define WIFI_SETTINGS_FILE "/config/wifiSettings.json"
|
||||||
#define SERVO_SETTINGS_FILE "/config/servoSettings.json"
|
#define SERVO_SETTINGS_FILE "/config/servoSettings.json"
|
||||||
|
#define MDNS_SETTINGS_FILE "/config/mdnsSettings.json"
|
||||||
|
|
||||||
namespace FileSystem {
|
namespace FileSystem {
|
||||||
extern PsychicUploadHandler *uploadHandler;
|
extern PsychicUploadHandler *uploadHandler;
|
||||||
@@ -25,4 +26,6 @@ esp_err_t uploadFile(PsychicRequest *request, const String &filename, uint64_t i
|
|||||||
esp_err_t getFiles(PsychicRequest *request);
|
esp_err_t getFiles(PsychicRequest *request);
|
||||||
esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json);
|
esp_err_t handleDelete(PsychicRequest *request, JsonVariant &json);
|
||||||
esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json);
|
esp_err_t handleEdit(PsychicRequest *request, JsonVariant &json);
|
||||||
|
|
||||||
|
esp_err_t mkdir(PsychicRequest *request, JsonVariant &json);
|
||||||
} // namespace FileSystem
|
} // namespace FileSystem
|
||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
#include <WiFi.h>
|
#include <WiFi.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <event_socket.h>
|
#include <event_bus.hpp>
|
||||||
#include <PsychicHttp.h>
|
#include <PsychicHttp.h>
|
||||||
|
|
||||||
#include <HTTPClient.h>
|
#include <HTTPClient.h>
|
||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#include <PsychicHttp.h>
|
#include <PsychicHttp.h>
|
||||||
#include <system_service.h>
|
#include <system_service.h>
|
||||||
#include <event_socket.h>
|
#include <event_bus.hpp>
|
||||||
|
|
||||||
enum FileType { ft_none = 0, ft_firmware = 1, ft_md5 = 2 };
|
enum FileType { ft_none = 0, ft_firmware = 1, ft_md5 = 2 };
|
||||||
|
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#ifndef Kinematics_h
|
||||||
|
#define Kinematics_h
|
||||||
|
|
||||||
|
#include <utils/math_utils.h>
|
||||||
|
|
||||||
|
struct alignas(16) body_state_t {
|
||||||
|
float omega, phi, psi, xm, ym, zm;
|
||||||
|
float feet[4][4];
|
||||||
|
|
||||||
|
void updateFeet(const float newFeet[4][4]) { COPY_2D_ARRAY_4x4(feet, newFeet); }
|
||||||
|
|
||||||
|
bool operator==(const body_state_t &other) const {
|
||||||
|
if (!IS_ALMOST_EQUAL(omega, other.omega) || !IS_ALMOST_EQUAL(phi, other.phi) ||
|
||||||
|
!IS_ALMOST_EQUAL(psi, other.psi) || !IS_ALMOST_EQUAL(xm, other.xm) || !IS_ALMOST_EQUAL(ym, other.ym) ||
|
||||||
|
!IS_ALMOST_EQUAL(zm, other.zm)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return arrayEqual(feet, other.feet, 0.1f);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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 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 invMountRot[3][3] = {{0, 0, -1}, {0, 1, 0}, {1, 0, 0}};
|
||||||
|
|
||||||
|
alignas(16) float rot[3][3] = {0};
|
||||||
|
alignas(16) float inv_rot[3][3] = {0};
|
||||||
|
alignas(16) float inv_trans[3] = {0};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (currentState == body_state) return ESP_OK;
|
||||||
|
|
||||||
|
currentState.omega = body_state.omega;
|
||||||
|
currentState.phi = body_state.phi;
|
||||||
|
currentState.psi = body_state.psi;
|
||||||
|
currentState.xm = body_state.xm;
|
||||||
|
currentState.ym = body_state.ym;
|
||||||
|
currentState.zm = body_state.zm;
|
||||||
|
currentState.updateFeet(body_state.feet);
|
||||||
|
|
||||||
|
float roll = body_state.omega * DEG2RAD_F;
|
||||||
|
float pitch = body_state.phi * DEG2RAD_F;
|
||||||
|
float yaw = body_state.psi * DEG2RAD_F;
|
||||||
|
euler2R(roll, pitch, yaw, rot);
|
||||||
|
inverse(rot, inv_rot);
|
||||||
|
|
||||||
|
inv_trans[0] =
|
||||||
|
-inv_rot[0][0] * currentState.xm - inv_rot[0][1] * currentState.ym - inv_rot[0][2] * currentState.zm;
|
||||||
|
inv_trans[1] =
|
||||||
|
-inv_rot[1][0] * currentState.xm - inv_rot[1][1] * currentState.ym - inv_rot[1][2] * currentState.zm;
|
||||||
|
inv_trans[2] =
|
||||||
|
-inv_rot[2][0] * currentState.xm - inv_rot[2][1] * currentState.ym - inv_rot[2][2] * currentState.zm;
|
||||||
|
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
float wx = currentState.feet[i][0];
|
||||||
|
float wy = currentState.feet[i][1];
|
||||||
|
float wz = currentState.feet[i][2];
|
||||||
|
|
||||||
|
float bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0];
|
||||||
|
float by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1];
|
||||||
|
float bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2];
|
||||||
|
|
||||||
|
float mx = mountOffsets[i][0];
|
||||||
|
float my = mountOffsets[i][1];
|
||||||
|
float mz = mountOffsets[i][2];
|
||||||
|
|
||||||
|
float px = bx - mx;
|
||||||
|
float py = by - my;
|
||||||
|
float pz = bz - mz;
|
||||||
|
|
||||||
|
float lx = invMountRot[0][0] * px + invMountRot[0][1] * py + invMountRot[0][2] * pz;
|
||||||
|
float ly = invMountRot[1][0] * px + invMountRot[1][1] * py + invMountRot[1][2] * pz;
|
||||||
|
float lz = invMountRot[2][0] * px + invMountRot[2][1] * py + invMountRot[2][2] * pz;
|
||||||
|
|
||||||
|
float xLocal = (i % 2 == 1) ? -lx : lx;
|
||||||
|
legIK(xLocal, ly, lz, result + i * 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void euler2R(float roll, float pitch, float yaw, float rot[3][3]) {
|
||||||
|
float cos_roll = std::cos(roll);
|
||||||
|
float sin_roll = std::sin(roll);
|
||||||
|
float cos_pitch = std::cos(pitch);
|
||||||
|
float sin_pitch = std::sin(pitch);
|
||||||
|
float cos_yaw = std::cos(yaw);
|
||||||
|
float sin_yaw = std::sin(yaw);
|
||||||
|
|
||||||
|
rot[0][0] = cos_pitch * cos_yaw;
|
||||||
|
rot[0][1] = -sin_yaw * cos_pitch;
|
||||||
|
rot[0][2] = sin_pitch;
|
||||||
|
rot[1][0] = sin_roll * sin_pitch * cos_yaw + sin_yaw * cos_roll;
|
||||||
|
rot[1][1] = -sin_roll * sin_pitch * sin_yaw + cos_roll * cos_yaw;
|
||||||
|
rot[1][2] = -sin_roll * cos_pitch;
|
||||||
|
rot[2][0] = sin_roll * sin_yaw - sin_pitch * cos_roll * cos_yaw;
|
||||||
|
rot[2][1] = sin_roll * cos_yaw + sin_pitch * sin_yaw * cos_roll;
|
||||||
|
rot[2][2] = cos_roll * cos_pitch;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void inverse(float rot[3][3], float inv_rot[3][3]) {
|
||||||
|
inv_rot[0][0] = rot[0][0];
|
||||||
|
inv_rot[0][1] = rot[1][0];
|
||||||
|
inv_rot[0][2] = rot[2][0];
|
||||||
|
inv_rot[1][0] = rot[0][1];
|
||||||
|
inv_rot[1][1] = rot[1][1];
|
||||||
|
inv_rot[1][2] = rot[2][1];
|
||||||
|
inv_rot[2][0] = rot[0][2];
|
||||||
|
inv_rot[2][1] = rot[1][2];
|
||||||
|
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;
|
||||||
|
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);
|
||||||
|
#elif defined(SPOTMICRO_YERTLE)
|
||||||
|
result[2] = RAD_TO_DEG_F(theta3 + theta2);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <PsychicHttp.h>
|
||||||
|
#include <ESPmDNS.h>
|
||||||
|
#include <template/stateful_service.h>
|
||||||
|
#include <template/stateful_endpoint.h>
|
||||||
|
#include <template/stateful_persistence.h>
|
||||||
|
#include <settings/mdns_settings.h>
|
||||||
|
#include <utils/timing.h>
|
||||||
|
|
||||||
|
class MDNSService : public StatefulService<MDNSSettings> {
|
||||||
|
private:
|
||||||
|
FSPersistence<MDNSSettings> _persistence;
|
||||||
|
bool _started;
|
||||||
|
|
||||||
|
void reconfigureMDNS();
|
||||||
|
void startMDNS();
|
||||||
|
void stopMDNS();
|
||||||
|
void addServices();
|
||||||
|
|
||||||
|
public:
|
||||||
|
MDNSService();
|
||||||
|
~MDNSService();
|
||||||
|
|
||||||
|
void begin();
|
||||||
|
|
||||||
|
esp_err_t getStatus(PsychicRequest *request);
|
||||||
|
void getStatus(JsonObject &root);
|
||||||
|
|
||||||
|
static esp_err_t queryServices(PsychicRequest *request, JsonVariant &json);
|
||||||
|
|
||||||
|
StatefulHttpEndpoint<MDNSSettings> endpoint;
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
#ifndef MotionService_h
|
#ifndef MotionService_h
|
||||||
#define MotionService_h
|
#define MotionService_h
|
||||||
|
|
||||||
#include <event_socket.h>
|
#include <event_bus.hpp>
|
||||||
|
#include <topic.hpp>
|
||||||
#include <kinematics.h>
|
#include <kinematics.h>
|
||||||
#include <peripherals/servo_controller.h>
|
#include <peripherals/servo_controller.h>
|
||||||
#include <utils/timing.h>
|
#include <utils/timing.h>
|
||||||
@@ -24,48 +25,34 @@ class MotionService {
|
|||||||
MotionService(ServoController* servoController) : _servoController(servoController) {}
|
MotionService(ServoController* servoController) : _servoController(servoController) {}
|
||||||
|
|
||||||
void begin() {
|
void begin() {
|
||||||
socket.onEvent(INPUT_EVENT, [&](JsonObject &root, int originId) { handleInput(root, originId); });
|
setupEventBusSubscriptions();
|
||||||
|
body_state.updateFeet(kinematics.default_feet_positions);
|
||||||
socket.onEvent(MODE_EVENT, [&](JsonObject &root, int originId) { handleMode(root, originId); });
|
|
||||||
|
|
||||||
socket.onEvent(ANGLES_EVENT, [&](JsonObject &root, int originId) { anglesEvent(root, originId); });
|
|
||||||
|
|
||||||
socket.onEvent(POSITION_EVENT, [&](JsonObject &root, int originId) { positionEvent(root, originId); });
|
|
||||||
|
|
||||||
socket.onSubscribe(ANGLES_EVENT,
|
|
||||||
std::bind(&MotionService::syncAngles, this, std::placeholders::_1, std::placeholders::_2));
|
|
||||||
|
|
||||||
body_state.updateFeet(default_feet_positions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void anglesEvent(JsonObject &root, int originId) {
|
void anglesEvent(const MotionAnglesMsg& msg) {
|
||||||
JsonArray array = root["data"].as<JsonArray>();
|
|
||||||
for (int i = 0; i < 12; i++) {
|
for (int i = 0; i < 12; i++) {
|
||||||
angles[i] = array[i];
|
angles[i] = msg.angles[i];
|
||||||
}
|
}
|
||||||
syncAngles(String(originId));
|
syncAngles();
|
||||||
}
|
}
|
||||||
|
|
||||||
void positionEvent(JsonObject &root, int originId) {
|
void positionEvent(const MotionPositionMsg& msg) {
|
||||||
JsonArray array = root["data"].as<JsonArray>();
|
body_state.omega = msg.omega;
|
||||||
body_state.omega = array[0];
|
body_state.phi = msg.phi;
|
||||||
body_state.phi = array[1];
|
body_state.psi = msg.psi;
|
||||||
body_state.psi = array[2];
|
body_state.xm = msg.xm;
|
||||||
body_state.xm = array[3];
|
body_state.ym = msg.ym;
|
||||||
body_state.ym = array[4];
|
body_state.zm = msg.zm;
|
||||||
body_state.zm = array[5];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleInput(JsonObject &root, int originId) {
|
void handleInput(const MotionInputMsg& msg) {
|
||||||
JsonArray array = root["data"].as<JsonArray>();
|
command.lx = msg.lx;
|
||||||
command.lx = array[1];
|
command.ly = msg.ly;
|
||||||
command.lx = array[1];
|
command.rx = msg.rx;
|
||||||
command.ly = array[2];
|
command.ry = msg.ry;
|
||||||
command.rx = array[3];
|
command.h = msg.h;
|
||||||
command.ry = array[4];
|
command.s = msg.s;
|
||||||
command.h = array[5];
|
command.s1 = msg.s1;
|
||||||
command.s = array[6];
|
|
||||||
command.s1 = array[7];
|
|
||||||
|
|
||||||
body_state.ym = (command.h + 127.f) * 0.35f / 100;
|
body_state.ym = (command.h + 127.f) * 0.35f / 100;
|
||||||
|
|
||||||
@@ -75,31 +62,33 @@ class MotionService {
|
|||||||
body_state.psi = command.ry / 8;
|
body_state.psi = command.ry / 8;
|
||||||
body_state.xm = command.ly / 2 / 100;
|
body_state.xm = command.ly / 2 / 100;
|
||||||
body_state.zm = command.lx / 2 / 100;
|
body_state.zm = command.lx / 2 / 100;
|
||||||
body_state.updateFeet(default_feet_positions);
|
body_state.updateFeet(kinematics.default_feet_positions);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleMode(JsonObject &root, int originId) {
|
void handleMode(const MotionModeMsg& msg) {
|
||||||
motionState = (MOTION_STATE)root["data"].as<int>();
|
motionState = (MOTION_STATE)msg.mode;
|
||||||
ESP_LOGV("MotionService", "Mode %d", motionState);
|
ESP_LOGV("MotionService", "Mode %d", motionState);
|
||||||
char output[2];
|
|
||||||
itoa((int)motionState, output, 10);
|
|
||||||
motionState == MOTION_STATE::DEACTIVATED ? _servoController->deactivate() : _servoController->activate();
|
motionState == MOTION_STATE::DEACTIVATED ? _servoController->deactivate() : _servoController->activate();
|
||||||
socket.emit(MODE_EVENT, output, String(originId).c_str());
|
|
||||||
|
MotionModeMsg response;
|
||||||
|
response.mode = msg.mode;
|
||||||
|
EventBus<MotionModeMsg>::publishAsync(response, _modeHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void emitAngles(const String &originId = "", bool sync = false) {
|
void emitAngles() {
|
||||||
char output[100];
|
MotionAnglesMsg anglesMsg;
|
||||||
snprintf(output, sizeof(output), "[%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f]", angles[0],
|
for (int i = 0; i < 12; i++) {
|
||||||
angles[1], angles[2], angles[3], angles[4], angles[5], angles[6], angles[7], angles[8], angles[9],
|
anglesMsg.angles[i] = angles[i];
|
||||||
angles[10], angles[11]);
|
}
|
||||||
socket.emit(ANGLES_EVENT, output, originId.c_str());
|
EventBus<MotionAnglesMsg>::publishAsync(anglesMsg, _anglesHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void syncAngles(const String &originId = "", bool sync = false) {
|
void syncAngles() {
|
||||||
emitAngles(originId, sync);
|
emitAngles();
|
||||||
_servoController->setAngles(angles);
|
_servoController->setAngles(angles);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +126,29 @@ class MotionService {
|
|||||||
float* getAngles() { return angles; }
|
float* getAngles() { return angles; }
|
||||||
|
|
||||||
private:
|
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;
|
||||||
Kinematics kinematics;
|
Kinematics kinematics;
|
||||||
ControllerCommand command = {0, 0, 0, 0, 0, 0, 0, 0};
|
ControllerCommand command = {0, 0, 0, 0, 0, 0, 0, 0};
|
||||||
@@ -154,10 +166,13 @@ class MotionService {
|
|||||||
float 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};
|
float dir[12] = {1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1};
|
||||||
float default_feet_positions[4][4] = {{1, -1, 0.7, 1}, {1, -1, -0.7, 1}, {-1, -1, 0.7, 1}, {-1, -1, -0.7, 1}};
|
#if defined(SPOTMICRO_ESP32)
|
||||||
|
|
||||||
float rest_angles[12] = {0, 90, -145, 0, 90, -145, 0, 90, -145, 0, 90, -145};
|
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};
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct CommandMsg {
|
||||||
|
float x, y;
|
||||||
|
friend void toJson(JsonVariant v, CommandMsg const &c) {
|
||||||
|
JsonArray arr = v.to<JsonArray>();
|
||||||
|
arr.add(c.x);
|
||||||
|
arr.add(c.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
JsonArrayConst arr = o.as<JsonArrayConst>();
|
||||||
|
x = arr[0].as<float>();
|
||||||
|
y = arr[1].as<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct I2CScanMsg {
|
||||||
|
std::vector<uint8_t> addresses;
|
||||||
|
|
||||||
|
friend void toJson(JsonVariant v, I2CScanMsg const& c) {
|
||||||
|
JsonArray arr = v.to<JsonArray>();
|
||||||
|
for (uint8_t addr : c.addresses) arr.add(addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
addresses.clear();
|
||||||
|
for (JsonVariantConst val : o.as<JsonArrayConst>()) addresses.push_back(val.as<uint8_t>());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct ImuMsg {
|
||||||
|
float ypr[3];
|
||||||
|
friend void toJson(JsonVariant v, ImuMsg const &a) {
|
||||||
|
JsonArray arr = v.to<JsonArray>();
|
||||||
|
arr.add(a.ypr[0]);
|
||||||
|
arr.add(a.ypr[1]);
|
||||||
|
arr.add(a.ypr[2]);
|
||||||
|
}
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
JsonArrayConst arr = o.as<JsonArrayConst>();
|
||||||
|
ypr[0] = arr[0].as<float>();
|
||||||
|
ypr[1] = arr[1].as<float>();
|
||||||
|
ypr[2] = arr[2].as<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
enum class MotionState { DEACTIVATED, IDLE, CALIBRATION, REST, STAND, CRAWL, WALK };
|
||||||
|
|
||||||
|
struct ModeMsg {
|
||||||
|
MotionState mode;
|
||||||
|
friend void toJson(JsonVariant v, ModeMsg const &m) { v.set(static_cast<int>(m.mode)); }
|
||||||
|
void fromJson(JsonVariantConst o) { mode = (MotionState)o.as<int>(); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct MotionAnglesMsg {
|
||||||
|
float angles[12];
|
||||||
|
|
||||||
|
friend void toJson(JsonVariant v, MotionAnglesMsg const &m) {
|
||||||
|
JsonArray arr = v.to<JsonArray>();
|
||||||
|
for (int i = 0; i < 12; i++) {
|
||||||
|
arr.add(m.angles[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
JsonArrayConst arr = o.as<JsonArrayConst>();
|
||||||
|
for (int i = 0; i < 12 && i < arr.size(); i++) {
|
||||||
|
angles[i] = arr[i].as<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct MotionInputMsg {
|
||||||
|
float lx, ly, rx, ry, h, s, s1;
|
||||||
|
|
||||||
|
friend void toJson(JsonVariant v, MotionInputMsg const &m) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
JsonArrayConst arr = o.as<JsonArrayConst>();
|
||||||
|
lx = arr[0].as<float>();
|
||||||
|
ly = arr[1].as<float>();
|
||||||
|
rx = arr[2].as<float>();
|
||||||
|
ry = arr[3].as<float>();
|
||||||
|
h = arr[4].as<float>();
|
||||||
|
s = arr[5].as<float>();
|
||||||
|
s1 = arr[6].as<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct MotionModeMsg {
|
||||||
|
int mode;
|
||||||
|
|
||||||
|
friend void toJson(JsonVariant v, MotionModeMsg const &m) { v.set(m.mode); }
|
||||||
|
|
||||||
|
void fromJson(JsonVariantConst o) { mode = o.as<int>(); }
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
struct MotionPositionMsg {
|
||||||
|
float omega, phi, psi, xm, ym, zm;
|
||||||
|
|
||||||
|
friend void toJson(JsonVariant v, MotionPositionMsg const &m) {
|
||||||
|
JsonArray arr = v.to<JsonArray>();
|
||||||
|
arr.add(m.omega);
|
||||||
|
arr.add(m.phi);
|
||||||
|
arr.add(m.psi);
|
||||||
|
arr.add(m.xm);
|
||||||
|
arr.add(m.ym);
|
||||||
|
arr.add(m.zm);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
JsonArrayConst arr = o.as<JsonArrayConst>();
|
||||||
|
omega = arr[0].as<float>();
|
||||||
|
phi = arr[1].as<float>();
|
||||||
|
psi = arr[2].as<float>();
|
||||||
|
xm = arr[3].as<float>();
|
||||||
|
ym = arr[4].as<float>();
|
||||||
|
zm = arr[5].as<float>();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
|
#ifndef NUM_SERVOS
|
||||||
|
#define NUM_SERVOS 12
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct ServoMsg {
|
||||||
|
float angles[NUM_SERVOS];
|
||||||
|
friend void toJson(JsonVariant v, ServoMsg const &a) {
|
||||||
|
JsonArray arr = v.to<JsonArray>();
|
||||||
|
for (int i = 0; i < NUM_SERVOS; i++) {
|
||||||
|
arr.add(a.angles[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void fromJson(JsonVariantConst o) {
|
||||||
|
JsonArrayConst arr = o.as<JsonArrayConst>();
|
||||||
|
for (int i = 0; i < NUM_SERVOS; i++) {
|
||||||
|
angles[i] = arr[i].as<float>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#define TOPIC_LIST \
|
||||||
|
X(Imu, ImuMsg) \
|
||||||
|
X(Mode, ModeMsg) \
|
||||||
|
X(Command, CommandMsg) \
|
||||||
|
X(Servo, ServoMsg) \
|
||||||
|
X(MotionInput, MotionInputMsg) \
|
||||||
|
X(MotionAngles, MotionAnglesMsg) \
|
||||||
|
X(MotionPosition, MotionPositionMsg) \
|
||||||
|
X(MotionMode, MotionModeMsg)
|
||||||
@@ -6,12 +6,24 @@
|
|||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
#include <utils/math_utils.h>
|
#include <utils/math_utils.h>
|
||||||
|
|
||||||
|
#if FT_ENABLED(USE_MPU6050)
|
||||||
#include <MPU6050_6Axis_MotionApps612.h>
|
#include <MPU6050_6Axis_MotionApps612.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if FT_ENABLED(USE_BNO055)
|
||||||
|
#include <Adafruit_BNO055.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
class IMU {
|
class IMU {
|
||||||
public:
|
public:
|
||||||
IMU() {}
|
IMU()
|
||||||
|
#if FT_ENABLED(USE_BNO055)
|
||||||
|
: _imu(55, 0x29)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
}
|
||||||
bool initialize() {
|
bool initialize() {
|
||||||
|
#if FT_ENABLED(USE_MPU6050)
|
||||||
_imu.initialize();
|
_imu.initialize();
|
||||||
imu_success = _imu.testConnection();
|
imu_success = _imu.testConnection();
|
||||||
devStatus = _imu.dmpInitialize();
|
devStatus = _imu.dmpInitialize();
|
||||||
@@ -20,27 +32,46 @@ class IMU {
|
|||||||
_imu.setI2CMasterModeEnabled(false);
|
_imu.setI2CMasterModeEnabled(false);
|
||||||
_imu.setI2CBypassEnabled(true);
|
_imu.setI2CBypassEnabled(true);
|
||||||
_imu.setSleepEnabled(false);
|
_imu.setSleepEnabled(false);
|
||||||
|
#endif
|
||||||
|
#if FT_ENABLED(USE_BNO055)
|
||||||
|
imu_success = _imu.begin();
|
||||||
|
if (!imu_success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
_imu.setExtCrystalUse(true);
|
||||||
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool readIMU() {
|
bool readIMU() {
|
||||||
if (!imu_success) return false;
|
if (!imu_success) return false;
|
||||||
|
#if FT_ENABLED(USE_MPU6050)
|
||||||
bool updated = _imu.dmpGetCurrentFIFOPacket(fifoBuffer);
|
bool updated = _imu.dmpGetCurrentFIFOPacket(fifoBuffer);
|
||||||
_imu.dmpGetQuaternion(&q, fifoBuffer);
|
_imu.dmpGetQuaternion(&q, fifoBuffer);
|
||||||
_imu.dmpGetGravity(&gravity, &q);
|
_imu.dmpGetGravity(&gravity, &q);
|
||||||
_imu.dmpGetYawPitchRoll(ypr, &q, &gravity);
|
_imu.dmpGetYawPitchRoll(ypr, &q, &gravity);
|
||||||
|
ypr[0] *= 180 / M_PI;
|
||||||
|
ypr[1] *= 180 / M_PI;
|
||||||
|
ypr[2] *= 180 / M_PI;
|
||||||
return updated;
|
return updated;
|
||||||
|
#endif
|
||||||
|
#if FT_ENABLED(USE_BNO055)
|
||||||
|
sensors_event_t event;
|
||||||
|
_imu.getEvent(&event);
|
||||||
|
ypr[0] = (float)event.orientation.x;
|
||||||
|
ypr[1] = (float)event.orientation.y;
|
||||||
|
ypr[2] = (float)event.orientation.z;
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
float getTemperature() { return imu_success ? imu_temperature : -1; }
|
float getTemperature() { return imu_success ? imu_temperature : -1; }
|
||||||
|
|
||||||
float getAngleX() { return imu_success ? ypr[0] * 180 / M_PI : 0; }
|
float getAngleX() { return imu_success ? ypr[0] : 0; }
|
||||||
|
|
||||||
float getAngleY() { return imu_success ? ypr[1] * 180 / M_PI : 0; }
|
float getAngleY() { return imu_success ? ypr[1] : 0; }
|
||||||
|
|
||||||
float getAngleZ() { return imu_success ? ypr[2] * 180 / M_PI : 0; }
|
float getAngleZ() { return imu_success ? ypr[2] : 0; }
|
||||||
|
|
||||||
Quaternion* getQuaternion() { return &q; }
|
|
||||||
|
|
||||||
void readIMU(JsonObject& root) {
|
void readIMU(JsonObject& root) {
|
||||||
if (!imu_success) return;
|
if (!imu_success) return;
|
||||||
@@ -52,12 +83,17 @@ class IMU {
|
|||||||
bool active() { return imu_success; }
|
bool active() { return imu_success; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
#if FT_ENABLED(USE_MPU6050)
|
||||||
MPU6050 _imu;
|
MPU6050 _imu;
|
||||||
bool imu_success {false};
|
|
||||||
uint8_t devStatus {false};
|
uint8_t devStatus {false};
|
||||||
Quaternion q;
|
Quaternion q;
|
||||||
uint8_t fifoBuffer[64];
|
uint8_t fifoBuffer[64];
|
||||||
VectorFloat gravity;
|
VectorFloat gravity;
|
||||||
|
#endif
|
||||||
|
#if FT_ENABLED(USE_BNO055)
|
||||||
|
Adafruit_BNO055 _imu;
|
||||||
|
#endif
|
||||||
|
bool imu_success {false};
|
||||||
float ypr[3];
|
float ypr[3];
|
||||||
float imu_temperature {-1};
|
float imu_temperature {-1};
|
||||||
};
|
};
|
||||||
+23
-24
@@ -55,25 +55,25 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
_eventEndpoint.begin();
|
_eventEndpoint.begin();
|
||||||
_persistence.readFromFS();
|
_persistence.readFromFS();
|
||||||
|
|
||||||
socket.onEvent(EVENT_I2C_SCAN, [&](JsonObject &root, int originId) {
|
// socket.onEvent(EVENT_I2C_SCAN, [&](JsonObject &root, int originId) {
|
||||||
scanI2C();
|
// scanI2C();
|
||||||
emitI2C();
|
// emitI2C();
|
||||||
});
|
// });
|
||||||
|
|
||||||
socket.onSubscribe(EVENT_I2C_SCAN, [&](const String &originId, bool sync) {
|
// socket.onSubscribe(EVENT_I2C_SCAN, [&](const String &originId, bool sync) {
|
||||||
scanI2C();
|
// scanI2C();
|
||||||
emitI2C(originId, sync);
|
// emitI2C(originId, sync);
|
||||||
});
|
// });
|
||||||
|
|
||||||
updatePins();
|
updatePins();
|
||||||
|
|
||||||
#if FT_ENABLED(USE_IMU)
|
#if FT_ENABLED(USE_MPU6050 || USE_BNO055)
|
||||||
if (!_imu.initialize()) ESP_LOGE("IMUService", "IMU initialize failed");
|
if (!_imu.initialize()) ESP_LOGE("IMUService", "IMU initialize failed");
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_MAG)
|
#if FT_ENABLED(USE_HMC5883)
|
||||||
if (!_mag.initialize()) ESP_LOGE("IMUService", "MAG initialize failed");
|
if (!_mag.initialize()) ESP_LOGE("IMUService", "MAG initialize failed");
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_BMP)
|
#if FT_ENABLED(USE_BMP180)
|
||||||
if (!_bmp.initialize()) ESP_LOGE("IMUService", "BMP initialize failed");
|
if (!_bmp.initialize()) ESP_LOGE("IMUService", "BMP initialize failed");
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_USS)
|
#if FT_ENABLED(USE_USS)
|
||||||
@@ -115,7 +115,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
}
|
}
|
||||||
serializeJson(root, output);
|
serializeJson(root, output);
|
||||||
ESP_LOGI("Peripherals", "Emitting I2C scan results, %s %d", originId.c_str(), sync);
|
ESP_LOGI("Peripherals", "Emitting I2C scan results, %s %d", originId.c_str(), sync);
|
||||||
socket.emit(EVENT_I2C_SCAN, output, originId.c_str(), sync);
|
// socket.emit(EVENT_I2C_SCAN, output, originId.c_str(), sync);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scanI2C(uint8_t lower = 1, uint8_t higher = 127) {
|
void scanI2C(uint8_t lower = 1, uint8_t higher = 127) {
|
||||||
@@ -137,7 +137,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
/* IMU FUNCTIONS */
|
/* IMU FUNCTIONS */
|
||||||
bool readIMU() {
|
bool readIMU() {
|
||||||
bool updated = false;
|
bool updated = false;
|
||||||
#if FT_ENABLED(USE_IMU)
|
#if FT_ENABLED(USE_MPU6050 || USE_BNO055)
|
||||||
beginTransaction();
|
beginTransaction();
|
||||||
updated = _imu.readIMU();
|
updated = _imu.readIMU();
|
||||||
endTransaction();
|
endTransaction();
|
||||||
@@ -147,7 +147,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
|
|
||||||
bool readMag() {
|
bool readMag() {
|
||||||
bool updated = false;
|
bool updated = false;
|
||||||
#if FT_ENABLED(USE_MAG)
|
#if FT_ENABLED(USE_HMC5883)
|
||||||
beginTransaction();
|
beginTransaction();
|
||||||
updated = _mag.readMagnetometer();
|
updated = _mag.readMagnetometer();
|
||||||
endTransaction();
|
endTransaction();
|
||||||
@@ -157,7 +157,7 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
|
|
||||||
bool readBMP() {
|
bool readBMP() {
|
||||||
bool updated = false;
|
bool updated = false;
|
||||||
#if FT_ENABLED(USE_BMP)
|
#if FT_ENABLED(USE_BMP180)
|
||||||
beginTransaction();
|
beginTransaction();
|
||||||
updated = _bmp.readBarometer();
|
updated = _bmp.readBarometer();
|
||||||
endTransaction();
|
endTransaction();
|
||||||
@@ -181,25 +181,24 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
void emitIMU() {
|
void emitIMU() {
|
||||||
doc.clear();
|
doc.clear();
|
||||||
JsonObject root = doc.to<JsonObject>();
|
JsonObject root = doc.to<JsonObject>();
|
||||||
#if FT_ENABLED(USE_IMU)
|
#if FT_ENABLED(USE_MPU6050 || USE_BNO055)
|
||||||
_imu.readIMU(root);
|
_imu.readIMU(root);
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_MAG)
|
#if FT_ENABLED(USE_HMC5883)
|
||||||
_mag.readMagnetometer(root);
|
_mag.readMagnetometer(root);
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_BMP)
|
#if FT_ENABLED(USE_BMP180)
|
||||||
_bmp.readBarometer(root);
|
_bmp.readBarometer(root);
|
||||||
#endif
|
#endif
|
||||||
serializeJson(doc, message);
|
serializeJson(doc, message);
|
||||||
socket.emit(EVENT_IMU, message);
|
// socket.emit(EVENT_IMU, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void emitSonar() {
|
void emitSonar() {
|
||||||
#if FT_ENABLED(USE_USS)
|
#if FT_ENABLED(USE_USS)
|
||||||
|
|
||||||
char output[16];
|
char output[16];
|
||||||
snprintf(output, sizeof(output), "[%.1f,%.1f]", _left_distance, _right_distance);
|
snprintf(output, sizeof(output), "[%.1f,%.1f]", _left_distance, _right_distance);
|
||||||
socket.emit("sonar", output);
|
// socket.emit("sonar", output);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,13 +213,13 @@ class Peripherals : public StatefulService<PeripheralsConfiguration> {
|
|||||||
|
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
char message[MAX_ESP_IMU_SIZE];
|
char message[MAX_ESP_IMU_SIZE];
|
||||||
#if FT_ENABLED(USE_IMU)
|
#if FT_ENABLED(USE_MPU6050 || USE_BNO055)
|
||||||
IMU _imu;
|
IMU _imu;
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_MAG)
|
#if FT_ENABLED(USE_HMC5883)
|
||||||
Magnetometer _mag;
|
Magnetometer _mag;
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_BMP)
|
#if FT_ENABLED(USE_BMP180)
|
||||||
Barometer _bmp;
|
Barometer _bmp;
|
||||||
#endif
|
#endif
|
||||||
#if FT_ENABLED(USE_USS)
|
#if FT_ENABLED(USE_USS)
|
||||||
+22
-15
@@ -2,7 +2,7 @@
|
|||||||
#define ServoController_h
|
#define ServoController_h
|
||||||
|
|
||||||
#include <Adafruit_PWMServoDriver.h>
|
#include <Adafruit_PWMServoDriver.h>
|
||||||
#include <event_socket.h>
|
#include <event_bus.hpp>
|
||||||
#include <template/stateful_persistence.h>
|
#include <template/stateful_persistence.h>
|
||||||
#include <template/stateful_service.h>
|
#include <template/stateful_service.h>
|
||||||
#include <template/stateful_endpoint.h>
|
#include <template/stateful_endpoint.h>
|
||||||
@@ -32,16 +32,16 @@ class ServoController : public StatefulService<ServoSettings> {
|
|||||||
_persistence(ServoSettings::read, ServoSettings::update, this, SERVO_SETTINGS_FILE) {}
|
_persistence(ServoSettings::read, ServoSettings::update, this, SERVO_SETTINGS_FILE) {}
|
||||||
|
|
||||||
void begin() {
|
void begin() {
|
||||||
socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
|
// socket.onEvent(EVENT_SERVO_CONFIGURATION_SETTINGS,
|
||||||
[&](JsonObject &root, int originId) { servoEvent(root, originId); });
|
// [&](JsonObject &root, int originId) { servoEvent(root, originId); });
|
||||||
socket.onEvent(EVENT_SERVO_STATE, [&](JsonObject &root, int originId) { stateUpdate(root, originId); });
|
// socket.onEvent(EVENT_SERVO_STATE, [&](JsonObject &root, int originId) { stateUpdate(root, originId); });
|
||||||
_persistence.readFromFS();
|
_persistence.readFromFS();
|
||||||
|
|
||||||
initializePCA();
|
initializePCA();
|
||||||
socket.onEvent(EVENT_SERVO_STATE, [&](JsonObject &root, int originId) {
|
// socket.onEvent(EVENT_SERVO_STATE, [&](JsonObject &root, int originId) {
|
||||||
is_active = root["active"] | false;
|
// is_active = root["active"] | false;
|
||||||
is_active ? activate() : deactivate();
|
// is_active ? activate() : deactivate();
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
void pcaWrite(int index, int value) {
|
void pcaWrite(int index, int value) {
|
||||||
@@ -74,9 +74,15 @@ class ServoController : public StatefulService<ServoSettings> {
|
|||||||
|
|
||||||
void servoEvent(JsonObject &root, int originId) {
|
void servoEvent(JsonObject &root, int originId) {
|
||||||
control_state = SERVO_CONTROL_STATE::PWM;
|
control_state = SERVO_CONTROL_STATE::PWM;
|
||||||
uint8_t servo_id = root["servo_id"];
|
int8_t servo_id = root["servo_id"];
|
||||||
int pwm = root["pwm"].as<int>();
|
uint16_t pwm = root["pwm"].as<uint16_t>();
|
||||||
pcaWrite(servo_id, pwm);
|
if (servo_id < 0) {
|
||||||
|
uint16_t pwms[12];
|
||||||
|
std::fill_n(pwms, 12, pwm);
|
||||||
|
_pca.setMultiplePWM(pwms, 12);
|
||||||
|
} else {
|
||||||
|
_pca.setPWM(servo_id, 0, pwm);
|
||||||
|
}
|
||||||
ESP_LOGI("SERVO_CONTROLLER", "Setting servo %d to %d", servo_id, pwm);
|
ESP_LOGI("SERVO_CONTROLLER", "Setting servo %d to %d", servo_id, pwm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +91,7 @@ class ServoController : public StatefulService<ServoSettings> {
|
|||||||
snprintf(output, sizeof(output), "[%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f]", angles[0],
|
snprintf(output, sizeof(output), "[%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f,%.1f]", angles[0],
|
||||||
angles[1], angles[2], angles[3], angles[4], angles[5], angles[6], angles[7], angles[8], angles[9],
|
angles[1], angles[2], angles[3], angles[4], angles[5], angles[6], angles[7], angles[8], angles[9],
|
||||||
angles[10], angles[11]);
|
angles[10], angles[11]);
|
||||||
socket.emit("angles", output, String(originId).c_str());
|
// socket.emit("angles", output, String(originId).c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateActiveState() { is_active ? activate() : deactivate(); }
|
void updateActiveState() { is_active ? activate() : deactivate(); }
|
||||||
@@ -98,17 +104,18 @@ class ServoController : public StatefulService<ServoSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void calculatePWM() {
|
void calculatePWM() {
|
||||||
|
uint16_t pwms[12];
|
||||||
for (int i = 0; i < 12; i++) {
|
for (int i = 0; i < 12; i++) {
|
||||||
angles[i] = lerp(angles[i], target_angles[i], 0.2);
|
angles[i] = lerp(angles[i], target_angles[i], 0.05);
|
||||||
auto &servo = state().servos[i];
|
auto &servo = state().servos[i];
|
||||||
float angle = servo.direction * angles[i] + servo.centerAngle;
|
float angle = servo.direction * angles[i] + servo.centerAngle;
|
||||||
uint16_t pwm = angle * servo.conversion + servo.centerPwm;
|
uint16_t pwm = angle * servo.conversion + servo.centerPwm;
|
||||||
if (pwm < 125 || pwm > 600) {
|
if (pwm < 125 || pwm > 600) {
|
||||||
ESP_LOGE("ServoController", "Servo %d, Invalid PWM value %d", i, pwm);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
pcaWrite(i, pwm);
|
pwms[i] = pwm;
|
||||||
}
|
}
|
||||||
|
_pca.setMultiplePWM(pwms, 12);
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateServoState() {
|
void updateServoState() {
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <utils/json_utils.h>
|
||||||
|
#include <utils/string_utils.h>
|
||||||
|
#include <template/state_result.h>
|
||||||
|
#include <filesystem.h>
|
||||||
|
|
||||||
|
#ifndef FACTORY_MDNS_HOSTNAME
|
||||||
|
#define FACTORY_MDNS_HOSTNAME "esp32"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef FACTORY_MDNS_INSTANCE
|
||||||
|
#define FACTORY_MDNS_INSTANCE "ESP32 Device"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
String key;
|
||||||
|
String value;
|
||||||
|
|
||||||
|
void serialize(JsonObject &json) const {
|
||||||
|
json["key"] = key;
|
||||||
|
json["value"] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deserialize(const JsonObject &json) {
|
||||||
|
key = json["key"].as<String>();
|
||||||
|
value = json["value"].as<String>();
|
||||||
|
|
||||||
|
return key.length() > 0;
|
||||||
|
}
|
||||||
|
} mdns_txt_record_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
String service;
|
||||||
|
String protocol;
|
||||||
|
uint16_t port;
|
||||||
|
std::vector<mdns_txt_record_t> txtRecords;
|
||||||
|
|
||||||
|
void serialize(JsonObject &json) const {
|
||||||
|
json["service"] = service;
|
||||||
|
json["protocol"] = protocol;
|
||||||
|
json["port"] = port;
|
||||||
|
|
||||||
|
if (txtRecords.size() > 0) {
|
||||||
|
JsonArray txtArray = json["txt_records"].to<JsonArray>();
|
||||||
|
for (const auto &txt : txtRecords) {
|
||||||
|
JsonObject txtObj = txtArray.add<JsonObject>();
|
||||||
|
txt.serialize(txtObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deserialize(const JsonObject &json) {
|
||||||
|
service = json["service"].as<String>();
|
||||||
|
protocol = json["protocol"].as<String>();
|
||||||
|
port = json["port"] | 0;
|
||||||
|
|
||||||
|
txtRecords.clear();
|
||||||
|
if (json["txt_records"].is<JsonArray>()) {
|
||||||
|
JsonArray txtArray = json["txt_records"];
|
||||||
|
for (JsonObject txtObj : txtArray) {
|
||||||
|
mdns_txt_record_t txt;
|
||||||
|
if (txt.deserialize(txtObj)) {
|
||||||
|
txtRecords.push_back(txt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return service.length() > 0 && protocol.length() > 0 && port > 0;
|
||||||
|
}
|
||||||
|
} mdns_service_t;
|
||||||
|
|
||||||
|
class MDNSSettings {
|
||||||
|
public:
|
||||||
|
String hostname;
|
||||||
|
String instance;
|
||||||
|
std::vector<mdns_service_t> services;
|
||||||
|
std::vector<mdns_txt_record_t> globalTxtRecords;
|
||||||
|
|
||||||
|
static void read(MDNSSettings &settings, JsonObject &root) {
|
||||||
|
root["hostname"] = settings.hostname;
|
||||||
|
root["instance"] = settings.instance;
|
||||||
|
|
||||||
|
JsonArray servicesArray = root["services"].to<JsonArray>();
|
||||||
|
for (const auto &service : settings.services) {
|
||||||
|
JsonObject serviceObj = servicesArray.add<JsonObject>();
|
||||||
|
service.serialize(serviceObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray txtArray = root["global_txt_records"].to<JsonArray>();
|
||||||
|
for (const auto &txt : settings.globalTxtRecords) {
|
||||||
|
JsonObject txtObj = txtArray.add<JsonObject>();
|
||||||
|
txt.serialize(txtObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static StateUpdateResult update(JsonObject &root, MDNSSettings &settings) {
|
||||||
|
settings.hostname = root["hostname"] | FACTORY_MDNS_HOSTNAME;
|
||||||
|
settings.instance = root["instance"] | FACTORY_MDNS_INSTANCE;
|
||||||
|
|
||||||
|
settings.services.clear();
|
||||||
|
if (root["services"].is<JsonArray>()) {
|
||||||
|
JsonArray servicesArray = root["services"];
|
||||||
|
for (JsonObject serviceObj : servicesArray) {
|
||||||
|
mdns_service_t service;
|
||||||
|
if (service.deserialize(serviceObj)) {
|
||||||
|
settings.services.push_back(service);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.services.empty()) {
|
||||||
|
mdns_service_t httpService = {.service = "http", .protocol = "tcp", .port = 80};
|
||||||
|
settings.services.push_back(httpService);
|
||||||
|
|
||||||
|
mdns_service_t wsService = {.service = "ws", .protocol = "tcp", .port = 80};
|
||||||
|
settings.services.push_back(wsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.globalTxtRecords.clear();
|
||||||
|
if (root["global_txt_records"].is<JsonArray>()) {
|
||||||
|
JsonArray txtArray = root["global_txt_records"];
|
||||||
|
for (JsonObject txtObj : txtArray) {
|
||||||
|
mdns_txt_record_t txt;
|
||||||
|
if (txt.deserialize(txtObj)) {
|
||||||
|
settings.globalTxtRecords.push_back(txt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.globalTxtRecords.empty()) {
|
||||||
|
mdns_txt_record_t firmwareVersion = {.key = "Firmware Version", .value = APP_VERSION};
|
||||||
|
settings.globalTxtRecords.push_back(firmwareVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StateUpdateResult::CHANGED;
|
||||||
|
}
|
||||||
|
};
|
||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
#define SCL_PIN SCL
|
#define SCL_PIN SCL
|
||||||
#endif
|
#endif
|
||||||
#ifndef I2C_FREQUENCY
|
#ifndef I2C_FREQUENCY
|
||||||
#define I2C_FREQUENCY 100000UL
|
#define I2C_FREQUENCY 1000000UL
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
class PinConfig {
|
class PinConfig {
|
||||||
@@ -14,12 +14,14 @@
|
|||||||
#include <peripherals/servo_controller.h>
|
#include <peripherals/servo_controller.h>
|
||||||
#include <peripherals/led_service.h>
|
#include <peripherals/led_service.h>
|
||||||
#include <peripherals/camera_service.h>
|
#include <peripherals/camera_service.h>
|
||||||
#include <event_socket.h>
|
#include <event_bus.hpp>
|
||||||
|
#include <adapters/websocket.hpp>
|
||||||
#include <features.h>
|
#include <features.h>
|
||||||
#include <motion.h>
|
#include <motion.h>
|
||||||
#include <task_manager.h>
|
#include <task_manager.h>
|
||||||
#include <wifi_service.h>
|
#include <wifi_service.h>
|
||||||
#include <ap_service.h>
|
#include <ap_service.h>
|
||||||
|
#include <mdns_service.h>
|
||||||
|
|
||||||
#ifdef EMBED_WWW
|
#ifdef EMBED_WWW
|
||||||
#include <WWWData.h>
|
#include <WWWData.h>
|
||||||
@@ -60,6 +62,7 @@ class Spot {
|
|||||||
// act
|
// act
|
||||||
void updateActuators() {
|
void updateActuators() {
|
||||||
if (updatedMotion) _servoController.setAngles(_motionService.getAngles());
|
if (updatedMotion) _servoController.setAngles(_motionService.getAngles());
|
||||||
|
updatedMotion = false;
|
||||||
|
|
||||||
_servoController.updateServoState();
|
_servoController.updateServoState();
|
||||||
#if FT_ENABLED(USE_WS2812)
|
#if FT_ENABLED(USE_WS2812)
|
||||||
@@ -70,7 +73,7 @@ class Spot {
|
|||||||
// communicate
|
// communicate
|
||||||
void emitTelemetry() {
|
void emitTelemetry() {
|
||||||
if (updatedMotion) EXECUTE_EVERY_N_MS(100, { _motionService.emitAngles(); });
|
if (updatedMotion) EXECUTE_EVERY_N_MS(100, { _motionService.emitAngles(); });
|
||||||
EXECUTE_EVERY_N_MS(1000, { _peripherals.emitIMU(); });
|
EXECUTE_EVERY_N_MS(250, { _peripherals.emitIMU(); });
|
||||||
// _peripherals.emitSonar();
|
// _peripherals.emitSonar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +81,7 @@ class Spot {
|
|||||||
PsychicHttpServer _server;
|
PsychicHttpServer _server;
|
||||||
WiFiService _wifiService;
|
WiFiService _wifiService;
|
||||||
APService _apService;
|
APService _apService;
|
||||||
EventSocket _socket;
|
MDNSService _mdnsService;
|
||||||
#if FT_ENABLED(USE_UPLOAD_FIRMWARE)
|
#if FT_ENABLED(USE_UPLOAD_FIRMWARE)
|
||||||
FirmwareUploadService _uploadFirmwareService;
|
FirmwareUploadService _uploadFirmwareService;
|
||||||
#endif
|
#endif
|
||||||
@@ -100,7 +103,7 @@ class Spot {
|
|||||||
bool updatedMotion = false;
|
bool updatedMotion = false;
|
||||||
|
|
||||||
const char *_appName = APP_NAME;
|
const char *_appName = APP_NAME;
|
||||||
const u_int16_t _numberEndpoints = 115;
|
const u_int16_t _numberEndpoints = 130;
|
||||||
const u_int32_t _maxFileUpload = 2300000; // 2.3 MB
|
const u_int32_t _maxFileUpload = 2300000; // 2.3 MB
|
||||||
const uint16_t _port = 80;
|
const uint16_t _port = 80;
|
||||||
|
|
||||||
@@ -108,7 +111,6 @@ class Spot {
|
|||||||
void loop();
|
void loop();
|
||||||
static void _loopImpl(void *_this) { static_cast<Spot *>(_this)->loop(); }
|
static void _loopImpl(void *_this) { static_cast<Spot *>(_this)->loop(); }
|
||||||
void setupServer();
|
void setupServer();
|
||||||
void setupMDNS();
|
|
||||||
void startServices();
|
void startServices();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user