Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1aed91d9 | |||
| 923ea17702 | |||
| 3a401abfab | |||
| 26c36b8302 | |||
| bfc259e660 | |||
| 6368bf9213 | |||
| cd802f1c22 | |||
| 59bb1d9579 | |||
| ae98ba76f7 | |||
| bd8c8fd988 | |||
| 7de5a1aa7c | |||
| a3e4fdd8a5 | |||
| f82fa051f2 | |||
| b66ddc3e81 | |||
| c85ac41ebc | |||
| 78d01533f4 | |||
| 18d4d66758 | |||
| 1b9dc9bb9e | |||
| 767d1157df | |||
| 1799889712 | |||
| 0b5d7b1534 | |||
| 10b78e6919 | |||
| 3fd72d081e | |||
| 1f3a465d3e | |||
| cddb6023e7 | |||
| 2f46484e0a | |||
| 4fcaf5d77d | |||
| ea8ddb43ef | |||
| 774c546487 | |||
| 6f46c1f598 | |||
| bc810ee2dd | |||
| 54a0419770 | |||
| d7a6bffe0a | |||
| df087decdb | |||
| 527764b0b5 | |||
| 8c97c68d11 | |||
| e5bf10cdb0 | |||
| de3912ff10 | |||
| 251a791876 | |||
| e36365ead6 | |||
| cb5c095888 | |||
| 281fa32c89 | |||
| d899701195 | |||
| 7061166fcd | |||
| 36b39d41ba | |||
| 7d0a7861ea | |||
| bf8c9bce95 | |||
| 9c984d3215 | |||
| 43e76770a8 | |||
| 6e10eabd9f | |||
| 922a4e3665 | |||
| 5e162ffb71 | |||
| f21ce92d43 | |||
| 98f3fc674b | |||
| c5901c65b3 | |||
| 2eab893dd7 | |||
| a3be035f98 | |||
| 743aa073b7 | |||
| a3de13c619 | |||
| 90be771211 | |||
| 7d79ec39ab | |||
| 211ff7205b | |||
| d0aa3b7b42 | |||
| d529eaa201 | |||
| c8ee64d7f4 | |||
| ec4c3fd98e | |||
| 0cc372cd36 | |||
| 9be405a89d | |||
| e3cfe89e19 | |||
| 144b99c180 | |||
| c788e118e3 | |||
| aae16335b3 | |||
| a43c250ed1 | |||
| 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 |
@@ -0,0 +1,61 @@
|
||||
name: Deploy GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./app
|
||||
env:
|
||||
BASE_PATH: /SpotMicroESP32-Leika
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: "pnpm"
|
||||
cache-dependency-path: "./app/pnpm-lock.yaml"
|
||||
|
||||
- run: pnpm install
|
||||
- run: pnpm run build
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v4
|
||||
with:
|
||||
static_site_generator: "sveltekit"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: app/build/
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
|
||||
steps:
|
||||
- name: Deploy
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -2,20 +2,19 @@ name: PlatformIO CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'esp32/**'
|
||||
- "esp32/**"
|
||||
- "platformio.ini"
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [master]
|
||||
paths:
|
||||
- 'esp32/**'
|
||||
- "esp32/**"
|
||||
- "platformio.ini"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./esp32
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -28,8 +27,8 @@ jobs:
|
||||
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- run: pip install -r ./scripts/requirements.txt
|
||||
python-version: "3.x"
|
||||
- run: pip install -r esp32/scripts/requirements.txt
|
||||
- name: Install PlatformIO Core
|
||||
run: pip install --upgrade platformio
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*$py.class
|
||||
.pio
|
||||
|
||||
Vendored
+4
-4
@@ -2,10 +2,10 @@
|
||||
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||
// for the documentation about the extensions.json format
|
||||
"recommendations": [
|
||||
"platformio.platformio-ide",
|
||||
"svelte.svelte-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode",
|
||||
"platformio.platformio-ide",
|
||||
"svelte.svelte-vscode"
|
||||
],
|
||||
"unwantedRecommendations": [
|
||||
"ms-vscode.cpptools-extension-pack"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
PUBLIC_VITE_USE_HOST_NAME=true
|
||||
PUBLIC_USE_JSON=true
|
||||
PUBLIC_USE_MSGPACK=true
|
||||
@@ -3,7 +3,6 @@ node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
+8
-17
@@ -8,30 +8,21 @@ If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
npx sv create
|
||||
```
|
||||
|
||||
## 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
|
||||
npm run dev
|
||||
1: Delete package-lock.json
|
||||
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
|
||||
npm run dev -- --open
|
||||
```
|
||||
Running `git status` should show:
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
[](https://postimg.cc/7CFsp2bq)
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
|
||||
+1
-1
@@ -45,6 +45,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"@niku/vite-env-caster": "^1.0.2",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
@@ -52,7 +53,6 @@
|
||||
"compare-versions": "^6.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"daisyui": "^5.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nipplejs": "^0.10.1",
|
||||
"svelte-dnd-list": "^0.1.8",
|
||||
"svelte-modals": "^2.0.0",
|
||||
|
||||
Generated
+58
-41
@@ -8,15 +8,18 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@msgpack/msgpack':
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2
|
||||
'@niku/vite-env-caster':
|
||||
specifier: ^1.0.2
|
||||
version: 1.1.2
|
||||
'@sveltejs/adapter-auto':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
|
||||
'@tailwindcss/vite':
|
||||
specifier: ^4.0.12
|
||||
version: 4.0.12(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
version: 4.0.12(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
chart.js:
|
||||
specifier: ^4.4.2
|
||||
version: 4.4.2
|
||||
@@ -29,9 +32,6 @@ importers:
|
||||
daisyui:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
jwt-decode:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
nipplejs:
|
||||
specifier: ^0.10.1
|
||||
version: 0.10.1
|
||||
@@ -65,13 +65,13 @@ importers:
|
||||
version: 1.49.1
|
||||
'@sveltejs/adapter-static':
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))
|
||||
'@sveltejs/kit':
|
||||
specifier: ^2.5.27
|
||||
version: 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
'@sveltejs/vite-plugin-svelte':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
version: 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
'@types/eslint':
|
||||
specifier: ^8.56.0
|
||||
version: 8.56.0
|
||||
@@ -128,10 +128,10 @@ importers:
|
||||
version: 0.18.5
|
||||
vite:
|
||||
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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
vitest:
|
||||
specifier: ^1.2.0
|
||||
version: 1.2.0(jsdom@24.0.0)(lightningcss@1.29.2)
|
||||
version: 1.2.0(@types/node@24.0.12)(jsdom@24.0.0)(lightningcss@1.29.2)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -509,6 +509,10 @@ packages:
|
||||
'@kurkle/color@0.3.2':
|
||||
resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==}
|
||||
|
||||
'@msgpack/msgpack@3.1.2':
|
||||
resolution: {integrity: sha512-JEW4DEtBzfe8HvUYecLU9e6+XJnKDlUAIve8FvPzF3Kzs6Xo/KuZkZJsDH0wJXl/qEZbeeE7edxDNY3kMs39hQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
'@niku/vite-env-caster@1.1.2':
|
||||
resolution: {integrity: sha512-6I/8REFdmfeGnK92H3nYHGc6lExwjm72jLxAsDPlfji97Eej4rOMl6WuYGLgsQI0pl5RrMRMveeRdijdL6hW+Q==}
|
||||
|
||||
@@ -760,6 +764,9 @@ packages:
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
'@types/node@24.0.12':
|
||||
resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==}
|
||||
|
||||
'@types/semver@7.5.8':
|
||||
resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==}
|
||||
|
||||
@@ -1423,10 +1430,6 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
jwt-decode@4.0.0:
|
||||
resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -2019,6 +2022,9 @@ packages:
|
||||
ufo@1.5.3:
|
||||
resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==}
|
||||
|
||||
undici-types@7.8.0:
|
||||
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
|
||||
|
||||
universalify@0.2.0:
|
||||
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
@@ -2532,6 +2538,8 @@ snapshots:
|
||||
|
||||
'@kurkle/color@0.3.2': {}
|
||||
|
||||
'@msgpack/msgpack@3.1.2': {}
|
||||
|
||||
'@niku/vite-env-caster@1.1.2':
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
@@ -2613,18 +2621,18 @@ snapshots:
|
||||
|
||||
'@sinclair/typebox@0.27.8': {}
|
||||
|
||||
'@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
import-meta-resolve: 4.1.0
|
||||
|
||||
'@sveltejs/adapter-static@3.0.1(@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))':
|
||||
dependencies:
|
||||
'@sveltejs/kit': 2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
|
||||
'@sveltejs/kit@2.17.3(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
'@types/cookie': 0.6.0
|
||||
cookie: 0.6.0
|
||||
devalue: 5.1.1
|
||||
@@ -2637,27 +2645,27 @@ snapshots:
|
||||
set-cookie-parser: 2.6.0
|
||||
sirv: 3.0.1
|
||||
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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
|
||||
'@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
'@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
debug: 4.4.0
|
||||
svelte: 5.20.4
|
||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||
'@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||
dependencies:
|
||||
'@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.20.4)(vite@6.2.1(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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)))(svelte@5.20.4)(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
debug: 4.4.0
|
||||
deepmerge: 4.3.1
|
||||
kleur: 4.1.5
|
||||
magic-string: 0.30.17
|
||||
svelte: 5.20.4
|
||||
vite: 6.2.1(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))
|
||||
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
vitefu: 1.0.6(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2714,13 +2722,13 @@ snapshots:
|
||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.0.12
|
||||
'@tailwindcss/oxide-win32-x64-msvc': 4.0.12
|
||||
|
||||
'@tailwindcss/vite@4.0.12(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||
'@tailwindcss/vite@4.0.12(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2))':
|
||||
dependencies:
|
||||
'@tailwindcss/node': 4.0.12
|
||||
'@tailwindcss/oxide': 4.0.12
|
||||
lightningcss: 1.29.2
|
||||
tailwindcss: 4.0.12
|
||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
|
||||
'@tweenjs/tween.js@23.1.2': {}
|
||||
|
||||
@@ -2737,6 +2745,11 @@ snapshots:
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/node@24.0.12':
|
||||
dependencies:
|
||||
undici-types: 7.8.0
|
||||
optional: true
|
||||
|
||||
'@types/semver@7.5.8': {}
|
||||
|
||||
'@types/stats.js@0.17.3': {}
|
||||
@@ -3500,8 +3513,6 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
jwt-decode@4.0.0: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@@ -4010,6 +4021,9 @@ snapshots:
|
||||
|
||||
ufo@1.5.3: {}
|
||||
|
||||
undici-types@7.8.0:
|
||||
optional: true
|
||||
|
||||
universalify@0.2.0: {}
|
||||
|
||||
unplugin-icons@0.18.5:
|
||||
@@ -4054,13 +4068,13 @@ snapshots:
|
||||
|
||||
uzip@0.20201231.0: {}
|
||||
|
||||
vite-node@1.2.0(lightningcss@1.29.2):
|
||||
vite-node@1.2.0(@types/node@24.0.12)(lightningcss@1.29.2):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.0
|
||||
pathe: 1.1.2
|
||||
picocolors: 1.0.1
|
||||
vite: 5.4.14(lightningcss@1.29.2)
|
||||
vite: 5.4.14(@types/node@24.0.12)(lightningcss@1.29.2)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
@@ -4072,31 +4086,33 @@ snapshots:
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite@5.4.14(lightningcss@1.29.2):
|
||||
vite@5.4.14(@types/node@24.0.12)(lightningcss@1.29.2):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.3
|
||||
rollup: 4.34.8
|
||||
optionalDependencies:
|
||||
'@types/node': 24.0.12
|
||||
fsevents: 2.3.3
|
||||
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.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2):
|
||||
dependencies:
|
||||
esbuild: 0.25.0
|
||||
postcss: 8.5.3
|
||||
rollup: 4.34.8
|
||||
optionalDependencies:
|
||||
'@types/node': 24.0.12
|
||||
fsevents: 2.3.3
|
||||
jiti: 2.4.2
|
||||
lightningcss: 1.29.2
|
||||
yaml: 2.4.2
|
||||
|
||||
vitefu@1.0.6(vite@6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)):
|
||||
vitefu@1.0.6(vite@6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)):
|
||||
optionalDependencies:
|
||||
vite: 6.2.1(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
vite: 6.2.1(@types/node@24.0.12)(jiti@2.4.2)(lightningcss@1.29.2)(yaml@2.4.2)
|
||||
|
||||
vitest@1.2.0(jsdom@24.0.0)(lightningcss@1.29.2):
|
||||
vitest@1.2.0(@types/node@24.0.12)(jsdom@24.0.0)(lightningcss@1.29.2):
|
||||
dependencies:
|
||||
'@vitest/expect': 1.2.0
|
||||
'@vitest/runner': 1.2.0
|
||||
@@ -4116,10 +4132,11 @@ snapshots:
|
||||
strip-literal: 1.3.0
|
||||
tinybench: 2.8.0
|
||||
tinypool: 0.8.4
|
||||
vite: 5.4.14(lightningcss@1.29.2)
|
||||
vite-node: 1.2.0(lightningcss@1.29.2)
|
||||
vite: 5.4.14(@types/node@24.0.12)(lightningcss@1.29.2)
|
||||
vite-node: 1.2.0(@types/node@24.0.12)(lightningcss@1.29.2)
|
||||
why-is-node-running: 2.2.2
|
||||
optionalDependencies:
|
||||
'@types/node': 24.0.12
|
||||
jsdom: 24.0.0
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
|
||||
+19
-19
@@ -2,39 +2,39 @@
|
||||
@plugin "daisyui";
|
||||
|
||||
@plugin "daisyui" {
|
||||
themes:
|
||||
light --default,
|
||||
dark --prefersdark;
|
||||
themes:
|
||||
light --default,
|
||||
dark --prefersdark;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: 'light';
|
||||
default: true;
|
||||
--color-primary: #00bfff;
|
||||
--color-secondary: #3c00ff;
|
||||
--base-content: white;
|
||||
name: 'light';
|
||||
default: true;
|
||||
--color-primary: #00bfff;
|
||||
--color-secondary: #3c00ff;
|
||||
--base-content: white;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: 'dark';
|
||||
prefersdark: true;
|
||||
--color-primary: #00bfff;
|
||||
--color-secondary: #3c00ff;
|
||||
--base-content: oklch(0.3 0.012 256);
|
||||
name: 'dark';
|
||||
prefersdark: true;
|
||||
--color-primary: #00bfff;
|
||||
--color-secondary: #3c00ff;
|
||||
--base-content: oklch(0.3 0.012 256);
|
||||
}
|
||||
|
||||
#nipple_0_0,
|
||||
#nipple_1_1 {
|
||||
z-index: 10 !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
#three-gui-panel {
|
||||
top: 64px;
|
||||
right: 0px;
|
||||
top: 64px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
#three-gui-panel {
|
||||
top: 48px;
|
||||
}
|
||||
#three-gui-panel {
|
||||
top: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,61 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Cancel, Check } from '$lib/components/icons';
|
||||
import { modals, exitBeforeEnter } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { Cancel, Check } from '$lib/components/icons'
|
||||
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals'
|
||||
|
||||
// provided by <Modals />
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onConfirm: any;
|
||||
labels?: any;
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
labels = {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'OK', icon: Check }
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
labels = {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'OK', icon: Check }
|
||||
}
|
||||
}: Props = $props();
|
||||
}: ModalProps = $props()
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
{@const SvelteComponent = labels?.confirm.icon}
|
||||
{@const SvelteComponent = labels?.confirm.icon}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex items-center"
|
||||
onclick={() => modals.close()}
|
||||
>
|
||||
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onConfirm}
|
||||
>
|
||||
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
|
||||
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
|
||||
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,51 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Check } from './icons';
|
||||
import { exitBeforeEnter } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Check } from './icons';
|
||||
import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onDismiss: any;
|
||||
dismiss?: any;
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
dismiss = { label: 'Dismiss', icon: Check }
|
||||
}: Props = $props();
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
labels = {
|
||||
dismiss: { label: 'Dismiss', icon: Check },
|
||||
},
|
||||
}: ModalProps = $props();
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onDismiss}
|
||||
>
|
||||
<dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onDismiss}>
|
||||
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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;
|
||||
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,69 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Down } from './icons';
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
collapsible?: boolean;
|
||||
icon?: import('svelte').Snippet;
|
||||
title?: import('svelte').Snippet;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { Down } from './icons'
|
||||
interface Props {
|
||||
open?: boolean
|
||||
collapsible?: boolean
|
||||
icon?: import('svelte').Snippet
|
||||
title?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
right?: import('svelte').Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(true),
|
||||
collapsible = true,
|
||||
icon,
|
||||
title,
|
||||
children
|
||||
}: Props = $props();
|
||||
let { open = $bindable(true), collapsible = true, icon, title, children, right }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if collapsible}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
open = !open;
|
||||
}}
|
||||
>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
open = !open
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ?
|
||||
'rotate-180'
|
||||
: ''}" />
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div class="min-h-16 w-full p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
{@render right?.()}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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>
|
||||
@@ -6,7 +6,7 @@
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
type Object3D,
|
||||
SphereGeometry,
|
||||
Vector3,
|
||||
type NormalBufferAttributes,
|
||||
@@ -21,22 +21,23 @@
|
||||
servoAnglesOut,
|
||||
servoAngles,
|
||||
mpu,
|
||||
jointNames
|
||||
jointNames,
|
||||
currentKinematic,
|
||||
walkGait,
|
||||
walkGaits,
|
||||
walkGaitToMode
|
||||
} from '$lib/stores'
|
||||
import { footColor, populateModelCache, throttler, toeWorldPositions } from '$lib/utilities'
|
||||
import {
|
||||
extractFootColor,
|
||||
populateModelCache,
|
||||
throttler,
|
||||
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 {
|
||||
BezierState,
|
||||
CalibrationState,
|
||||
EightPhaseWalkState,
|
||||
FourPhaseWalkState,
|
||||
IdleState,
|
||||
RestState,
|
||||
StandState
|
||||
} from '$lib/gait'
|
||||
import { type body_state_t } from '$lib/kinematic'
|
||||
import { BezierState, CalibrationState, IdleState, RestState, StandState } from '$lib/gait'
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js'
|
||||
import type { URDFRobot } from 'urdf-loader'
|
||||
import { get } from 'svelte/store'
|
||||
@@ -47,20 +48,12 @@
|
||||
panel?: boolean
|
||||
debug?: boolean
|
||||
ground?: boolean
|
||||
zoom?: number
|
||||
}
|
||||
|
||||
let {
|
||||
sky = true,
|
||||
orbit = false,
|
||||
panel = true,
|
||||
debug = false,
|
||||
ground = true,
|
||||
zoom = 8
|
||||
}: Props = $props()
|
||||
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
|
||||
|
||||
let sceneManager = $state(new SceneBuilder())
|
||||
let canvas: HTMLCanvasElement = $state()
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
let currentModelAngles: number[] = new Array(12).fill(0)
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
@@ -73,7 +66,7 @@
|
||||
|
||||
let target_position = { x: 0, z: 0, yaw: 0 }
|
||||
|
||||
let kinematic = new Kinematic()
|
||||
let kinematic = get(currentKinematic)
|
||||
|
||||
let planners = {
|
||||
[ModesEnum.Deactivated]: new IdleState(),
|
||||
@@ -81,7 +74,6 @@
|
||||
[ModesEnum.Calibration]: new CalibrationState(),
|
||||
[ModesEnum.Rest]: new RestState(),
|
||||
[ModesEnum.Stand]: new StandState(),
|
||||
[ModesEnum.Crawl]: new EightPhaseWalkState(),
|
||||
[ModesEnum.Walk]: new BezierState()
|
||||
}
|
||||
let lastTick = performance.now()
|
||||
@@ -95,7 +87,7 @@
|
||||
xm: 0,
|
||||
ym: 0.5,
|
||||
zm: 0,
|
||||
feet: planners[ModesEnum.Idle].default_feet_pos
|
||||
feet: kinematic.getDefaultFeetPos()
|
||||
}
|
||||
|
||||
let settings = {
|
||||
@@ -120,6 +112,7 @@
|
||||
await populateModelCache()
|
||||
await createScene()
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
walkGait.subscribe(gait => planners[ModesEnum.Walk].set_mode(walkGaitToMode(gait)))
|
||||
if (panel) createPanel()
|
||||
})
|
||||
|
||||
@@ -180,7 +173,7 @@
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(Math.min(zoom, 8), 30, orbit)
|
||||
.addOrbitControls(2, 20, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
@@ -204,7 +197,7 @@
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry()
|
||||
const material = new LineBasicMaterial({ color: footColor() })
|
||||
const material = new LineBasicMaterial({ color: extractFootColor() })
|
||||
const line = new Line(geometry, material)
|
||||
trace_lines.push(geometry)
|
||||
sceneManager.scene.add(line)
|
||||
@@ -267,16 +260,15 @@
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const controlData = get(outControllerData)
|
||||
const data = {
|
||||
stop: controlData[0],
|
||||
lx: controlData[1],
|
||||
ly: controlData[2],
|
||||
rx: controlData[3],
|
||||
ry: controlData[4],
|
||||
h: controlData[5],
|
||||
s: controlData[6],
|
||||
s1: controlData[7]
|
||||
lx: controlData[0],
|
||||
ly: controlData[1],
|
||||
rx: controlData[2],
|
||||
ry: controlData[3],
|
||||
h: controlData[4],
|
||||
s: controlData[5],
|
||||
s1: controlData[6]
|
||||
}
|
||||
body_state.ym = ((data.h + 127) * 0.75) / 100
|
||||
body_state.ym = data.h
|
||||
|
||||
let planner = planners[get(mode)]
|
||||
const delta = performance.now() - lastTick
|
||||
@@ -311,7 +303,7 @@
|
||||
const robot = sceneManager.model
|
||||
if (!robot) return
|
||||
|
||||
const toes = toeWorldPositions(robot)
|
||||
const toes = getToeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
update_camera(robot)
|
||||
|
||||
@@ -35,6 +35,9 @@ export { default as Hamburger } from '~icons/mdi/hamburger-menu'
|
||||
export { default as FileIcon } from '~icons/mdi/file'
|
||||
export { default as FolderIcon } from '~icons/mdi/folder-outline'
|
||||
export { default as FolderOpenOutline } from '~icons/mdi/folder-open-outline'
|
||||
export { default as 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 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 Home } from '~icons/tabler/home'
|
||||
export { default as SSID } from '~icons/tabler/router'
|
||||
export { default as DNS } from '~icons/tabler/address-book'
|
||||
export { default as DNS } from '~icons/mdi/dns'
|
||||
export { default as Gateway } from '~icons/tabler/torii'
|
||||
export { default as Subnet } from '~icons/tabler/grid-dots'
|
||||
export { default as Channel } from '~icons/tabler/antenna'
|
||||
export { default as Scan } from '~icons/tabler/radar-2'
|
||||
export { default as Add } from '~icons/tabler/circle-plus'
|
||||
export { default as Edit } from '~icons/tabler/pencil'
|
||||
export { default as 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 Network } from '~icons/tabler/router'
|
||||
|
||||
@@ -1,37 +1,40 @@
|
||||
<script lang="ts">
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import {
|
||||
WidgetComponents,
|
||||
type WidgetContainerConfig,
|
||||
isWidgetConfig,
|
||||
} from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
|
||||
interface Props {
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
interface Props {
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
|
||||
let { container }: Props = $props();
|
||||
let { container }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}
|
||||
>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { base } from '$app/paths'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import GithubButton from '../menu/GithubButton.svelte'
|
||||
import LogoButton from '../menu/LogoButton.svelte'
|
||||
@@ -19,9 +20,10 @@
|
||||
Router,
|
||||
AP,
|
||||
Copyright,
|
||||
Metrics
|
||||
Metrics,
|
||||
DNS
|
||||
} from '$lib/components/icons'
|
||||
import appEnv from 'app-env'
|
||||
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
|
||||
|
||||
const features = useFeatureFlags()
|
||||
|
||||
@@ -40,6 +42,10 @@
|
||||
submenu?: menuItem[]
|
||||
}
|
||||
|
||||
function withBase(path: string) {
|
||||
return `${base}${path.startsWith('/') ? path : '/' + path}`
|
||||
}
|
||||
|
||||
let menuItems = $state<menuItem[]>([])
|
||||
|
||||
$effect(() => {
|
||||
@@ -47,13 +53,13 @@
|
||||
{
|
||||
title: 'Connection',
|
||||
icon: WiFi,
|
||||
href: '/connection',
|
||||
feature: !appEnv.VITE_USE_HOST_NAME
|
||||
href: withBase('/connection'),
|
||||
feature: !PUBLIC_VITE_USE_HOST_NAME
|
||||
},
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: '/controller',
|
||||
href: withBase('/controller'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
@@ -64,25 +70,25 @@
|
||||
{
|
||||
title: 'I2C',
|
||||
icon: Connection,
|
||||
href: '/peripherals/i2c',
|
||||
href: withBase('/peripherals/i2c'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Camera',
|
||||
icon: Camera,
|
||||
href: '/peripherals/camera',
|
||||
href: withBase('/peripherals/camera'),
|
||||
feature: $features.camera
|
||||
},
|
||||
{
|
||||
title: 'Servo',
|
||||
icon: MotorOutline,
|
||||
href: '/peripherals/servo',
|
||||
href: withBase('/peripherals/servo'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'IMU',
|
||||
icon: Rotate3d,
|
||||
href: '/peripherals/imu',
|
||||
href: withBase('/peripherals/imu'),
|
||||
feature: $features.imu || $features.mag || $features.bmp
|
||||
}
|
||||
]
|
||||
@@ -95,13 +101,19 @@
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: '/wifi/sta',
|
||||
href: withBase('/wifi/sta'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: '/wifi/ap',
|
||||
href: withBase('/wifi/ap'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'mDNS',
|
||||
icon: DNS,
|
||||
href: withBase('/wifi/mdns'),
|
||||
feature: true
|
||||
}
|
||||
]
|
||||
@@ -114,25 +126,25 @@
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: '/system/status',
|
||||
href: withBase('/system/status'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'File System',
|
||||
icon: Folder,
|
||||
href: '/system/filesystem',
|
||||
href: withBase('/system/filesystem'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: '/system/metrics',
|
||||
feature: $features.analytics
|
||||
href: withBase('/system/metrics'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: '/system/update',
|
||||
href: withBase('/system/update'),
|
||||
feature: $features.ota || $features.upload_firmware || $features.download_firmware
|
||||
}
|
||||
]
|
||||
@@ -165,7 +177,11 @@
|
||||
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
|
||||
<LogoButton {appName} />
|
||||
|
||||
<MenuList {menuItems} select={updateMenu} class="grow flex-nowrap overflow-y-auto" level="0" />
|
||||
<MenuList
|
||||
{menuItems}
|
||||
select={updateMenu}
|
||||
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
|
||||
level="0" />
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class={klass + ' menu'}>
|
||||
<ul class={klass + ' menu w-full'}>
|
||||
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
<li>
|
||||
{#if menuItem.submenu}
|
||||
<details open={menuItem.submenu.some(subItem => subItem.active)}>
|
||||
<summary class="text-lg font-bold">
|
||||
<summary class="font-bold">
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
|
||||
@@ -1,111 +1,109 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import type { GithubRelease } from '$lib/types/models';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
import { Cancel, CloudDown, Firmware } from '../icons';
|
||||
import { page } from '$app/state'
|
||||
import { modals } from 'svelte-modals'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'
|
||||
import { compareVersions } from 'compare-versions'
|
||||
import { onMount } from 'svelte'
|
||||
import { api } from '$lib/api'
|
||||
import type { GithubRelease } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Cancel, CloudDown, Firmware } from '../icons'
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags()
|
||||
|
||||
interface Props {
|
||||
update?: boolean;
|
||||
interface Props {
|
||||
update?: boolean
|
||||
}
|
||||
|
||||
let { update = $bindable(false) }: Props = $props()
|
||||
|
||||
let firmwareVersion: string = $state('')
|
||||
let firmwareDownloadLink: string = $state('')
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
)
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository')
|
||||
return
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
|
||||
let { update = $bindable(false) }: Props = $props();
|
||||
const results = result.inner
|
||||
update = false
|
||||
firmwareVersion = ''
|
||||
|
||||
let firmwareVersion: string = $state('');
|
||||
let firmwareDownloadLink: string = $state('');
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
};
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
);
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository');
|
||||
return;
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
|
||||
const results = result.inner;
|
||||
update = false;
|
||||
firmwareVersion = '';
|
||||
|
||||
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
update = true;
|
||||
firmwareVersion = results.tag_name;
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||
notifications.info('Firmware update available.', 5000);
|
||||
}
|
||||
}
|
||||
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target as string)
|
||||
) {
|
||||
update = true
|
||||
firmwareVersion = results.tag_name
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url
|
||||
notifications.info('Firmware update available.', 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url })
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI();
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
|
||||
}
|
||||
});
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
});
|
||||
}
|
||||
});
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI()
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000) // once per hour
|
||||
}
|
||||
})
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url)
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1">
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { writable, derived, type Writable } from 'svelte/store';
|
||||
import { writable, derived, type Writable } from 'svelte/store'
|
||||
|
||||
type StateType = 'info' | 'success' | 'warning' | 'error';
|
||||
type StateType = 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
type State = {
|
||||
id: string;
|
||||
type: StateType;
|
||||
message: string;
|
||||
};
|
||||
id: string
|
||||
type: StateType
|
||||
message: string
|
||||
}
|
||||
|
||||
function createNotificationStore() {
|
||||
const state: State[] = [];
|
||||
const notifications = writable(state);
|
||||
const { subscribe } = notifications;
|
||||
const state: State[] = []
|
||||
const notifications = writable(state)
|
||||
const { subscribe } = notifications
|
||||
|
||||
function send(message: string, type: StateType = 'info', timeout: number) {
|
||||
const id = generateId();
|
||||
setTimeout(() => {
|
||||
notifications.update((state) => {
|
||||
return state.filter((n) => n.id !== id);
|
||||
});
|
||||
}, timeout);
|
||||
notifications.update((state) => {
|
||||
return [...state, { id, type, message }];
|
||||
});
|
||||
}
|
||||
function send(message: string, type: StateType = 'info', timeout: number) {
|
||||
const id = generateId()
|
||||
setTimeout(() => {
|
||||
notifications.update(state => {
|
||||
return state.filter(n => n.id !== id)
|
||||
})
|
||||
}, timeout)
|
||||
notifications.update(state => {
|
||||
return [...state, { id, type, message }]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
|
||||
}
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9);
|
||||
return '_' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore();
|
||||
export const notifications = createNotificationStore()
|
||||
|
||||
@@ -1,103 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { daisyColor } from "$lib/utilities";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import { onMount } from "svelte";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let chartElement: HTMLCanvasElement = $state();
|
||||
let chart: Chart;
|
||||
let chartElement: HTMLCanvasElement;
|
||||
let chart: Chart;
|
||||
|
||||
interface Props {
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
interface Props {
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
|
||||
let { label, data, title }: Props = $props();
|
||||
let { label, data, title }: Props = $props();
|
||||
|
||||
Chart.register(...registerables);
|
||||
Chart.register(...registerables);
|
||||
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
}, 500);
|
||||
})
|
||||
setInterval(() => {
|
||||
chart.data.labels = data;
|
||||
chart.data.datasets[0].data = data;
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
<div class="w-full h-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface Props {
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change?: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:value={selectedOption}
|
||||
{...rest}
|
||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
bind:value={selectedOption}
|
||||
{...rest}
|
||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}">
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
+360
-387
@@ -1,452 +1,425 @@
|
||||
import type { body_state_t } from './kinematic';
|
||||
import { fromInt8 } from './utilities';
|
||||
|
||||
const { sin } = Math;
|
||||
import { get } from 'svelte/store'
|
||||
import type { body_state_t } from './kinematic'
|
||||
import { currentKinematic } from './stores/featureFlags'
|
||||
|
||||
export interface gait_state_t {
|
||||
step_height: number;
|
||||
step_x: number;
|
||||
step_z: number;
|
||||
step_angle: number;
|
||||
step_velocity: number;
|
||||
step_depth: number;
|
||||
step_height: number
|
||||
step_x: number
|
||||
step_z: number
|
||||
step_angle: number
|
||||
step_velocity: number
|
||||
step_depth: number
|
||||
}
|
||||
|
||||
export interface ControllerCommand {
|
||||
stop: number;
|
||||
lx: number;
|
||||
ly: number;
|
||||
rx: number;
|
||||
ry: number;
|
||||
h: number;
|
||||
s: number;
|
||||
s1: number;
|
||||
lx: number
|
||||
ly: number
|
||||
rx: number
|
||||
ry: number
|
||||
h: number
|
||||
s: number
|
||||
s1: number
|
||||
}
|
||||
|
||||
export abstract class GaitState {
|
||||
protected abstract name: string;
|
||||
protected abstract name: string
|
||||
|
||||
protected dt = 0.02;
|
||||
protected body_state!: body_state_t;
|
||||
protected gait_state: gait_state_t = {
|
||||
step_height: 0.4,
|
||||
step_x: 0,
|
||||
step_z: 0,
|
||||
step_angle: 0,
|
||||
step_velocity: 1,
|
||||
step_depth: 0.002
|
||||
};
|
||||
protected dt = 0.02
|
||||
protected body_state!: body_state_t
|
||||
protected gait_state: gait_state_t = {
|
||||
step_height: 0.4,
|
||||
step_x: 0,
|
||||
step_z: 0,
|
||||
step_angle: 0,
|
||||
step_velocity: 1,
|
||||
step_depth: 0.002
|
||||
}
|
||||
|
||||
public get default_feet_pos() {
|
||||
return [
|
||||
[1, -1, 1, 1],
|
||||
[1, -1, -1, 1],
|
||||
[-1, -1, 1, 1],
|
||||
[-1, -1, -1, 1]
|
||||
];
|
||||
public get default_feet_pos() {
|
||||
return get(currentKinematic).getDefaultFeetPos()
|
||||
}
|
||||
|
||||
protected get default_height() {
|
||||
return 0.5
|
||||
}
|
||||
|
||||
begin() {
|
||||
console.log('Starting', this.name)
|
||||
}
|
||||
end() {
|
||||
console.log('Ending', this.name)
|
||||
}
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
this.map_command(command)
|
||||
this.body_state = body_state
|
||||
this.dt = dt / 1000
|
||||
return body_state
|
||||
}
|
||||
|
||||
map_command(command: ControllerCommand) {
|
||||
const newCommand = {
|
||||
step_height: 0.4 + (command.s1 + 1) / 2,
|
||||
step_x: command.ly,
|
||||
step_z: -command.lx,
|
||||
step_velocity: command.s,
|
||||
step_angle: command.rx,
|
||||
step_depth: 0.002
|
||||
}
|
||||
|
||||
protected get default_height() {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
begin() {
|
||||
console.log('Starting', this.name);
|
||||
}
|
||||
end() {
|
||||
console.log('Ending', this.name);
|
||||
}
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
this.map_command(command);
|
||||
this.body_state = body_state;
|
||||
this.dt = dt / 1000;
|
||||
return body_state;
|
||||
}
|
||||
|
||||
map_command(command: ControllerCommand) {
|
||||
const newCommand = {
|
||||
step_height: 0.4 + (command.s1 / 128 + 1) / 2,
|
||||
step_x: Math.floor(fromInt8(command.ly, -1, 1) * 10) / 10,
|
||||
step_z: -(Math.floor(fromInt8(command.lx, -1, 1) * 10) / 10),
|
||||
step_velocity: command.s / 128 + 1,
|
||||
step_angle: command.rx / 128,
|
||||
step_depth: 0.002
|
||||
};
|
||||
|
||||
this.gait_state = newCommand;
|
||||
}
|
||||
this.gait_state = newCommand
|
||||
}
|
||||
}
|
||||
|
||||
export class IdleState extends GaitState {
|
||||
protected name = 'Idle';
|
||||
protected name = 'Idle'
|
||||
}
|
||||
|
||||
export class CalibrationState extends GaitState {
|
||||
protected name = 'Calibration';
|
||||
protected name = 'Calibration'
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
body_state.omega = 0;
|
||||
body_state.phi = 0;
|
||||
body_state.psi = 0;
|
||||
body_state.xm = 0;
|
||||
body_state.ym = this.default_height * 10;
|
||||
body_state.zm = 0;
|
||||
body_state.feet = this.default_feet_pos;
|
||||
return body_state;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
step(body_state: body_state_t, _command: ControllerCommand) {
|
||||
body_state.omega = 0
|
||||
body_state.phi = 0
|
||||
body_state.psi = 0
|
||||
body_state.xm = 0
|
||||
body_state.ym = this.default_height * 10
|
||||
body_state.zm = 0
|
||||
body_state.feet = this.default_feet_pos
|
||||
return body_state
|
||||
}
|
||||
}
|
||||
|
||||
export class RestState extends GaitState {
|
||||
protected name = 'Rest';
|
||||
protected name = 'Rest'
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
body_state.omega = 0;
|
||||
body_state.phi = 0;
|
||||
body_state.psi = 0;
|
||||
body_state.xm = 0;
|
||||
body_state.ym = this.default_height / 2;
|
||||
body_state.zm = 0;
|
||||
body_state.feet = this.default_feet_pos;
|
||||
return body_state;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
step(body_state: body_state_t, _command: ControllerCommand) {
|
||||
body_state.omega = 0
|
||||
body_state.phi = 0
|
||||
body_state.psi = 0
|
||||
body_state.xm = 0
|
||||
body_state.ym = this.default_height / 2
|
||||
body_state.zm = 0
|
||||
body_state.feet = this.default_feet_pos
|
||||
return body_state
|
||||
}
|
||||
}
|
||||
|
||||
export class StandState extends GaitState {
|
||||
protected name = 'Stand';
|
||||
protected name = 'Stand'
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
body_state.omega = 0;
|
||||
body_state.phi = command.rx / 8;
|
||||
body_state.psi = command.ry / 8;
|
||||
body_state.xm = command.ly / 2 / 100;
|
||||
body_state.zm = command.lx / 2 / 100;
|
||||
body_state.feet = this.default_feet_pos;
|
||||
return body_state;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class PhaseGaitState extends GaitState {
|
||||
protected tick = 0;
|
||||
protected phase = 0;
|
||||
protected phase_time = 0;
|
||||
protected abstract num_phases: number;
|
||||
protected abstract phase_speed_factor: number;
|
||||
protected abstract swing_stand_ratio: number;
|
||||
|
||||
protected contact_phases!: number[][];
|
||||
protected shifts!: number[][];
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
super.step(body_state, command, dt);
|
||||
this.update_phase();
|
||||
this.update_body_position();
|
||||
this.update_feet_positions();
|
||||
return this.body_state;
|
||||
}
|
||||
|
||||
update_phase() {
|
||||
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity;
|
||||
|
||||
if (this.phase_time >= 1) {
|
||||
this.phase += 1;
|
||||
if (this.phase == this.num_phases) this.phase = 0;
|
||||
this.phase_time = 0;
|
||||
}
|
||||
}
|
||||
|
||||
update_body_position() {
|
||||
if (this.num_phases === 4) return;
|
||||
|
||||
const shift = this.shifts[Math.floor(this.phase / 2)];
|
||||
|
||||
this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4;
|
||||
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4;
|
||||
}
|
||||
|
||||
update_feet_positions() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.body_state.feet[i] = this.update_foot_position(i);
|
||||
}
|
||||
}
|
||||
|
||||
update_foot_position(index: number): number[] {
|
||||
const contact = this.contact_phases[index][this.phase];
|
||||
return contact ? this.stand(index) : this.swing(index);
|
||||
}
|
||||
|
||||
stand(index: number): number[] {
|
||||
const delta_pos = [
|
||||
-this.gait_state.step_x * this.dt * this.swing_stand_ratio,
|
||||
0,
|
||||
-this.gait_state.step_z * this.dt * this.swing_stand_ratio
|
||||
];
|
||||
|
||||
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0];
|
||||
this.body_state.feet[index][1] = this.default_feet_pos[index][1];
|
||||
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
|
||||
return this.body_state.feet[index];
|
||||
}
|
||||
|
||||
swing(index: number): number[] {
|
||||
const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt];
|
||||
|
||||
if (this.gait_state.step_x == 0) {
|
||||
delta_pos[0] =
|
||||
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8;
|
||||
}
|
||||
|
||||
if (this.gait_state.step_z == 0) {
|
||||
delta_pos[2] =
|
||||
(this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8;
|
||||
}
|
||||
|
||||
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0];
|
||||
this.body_state.feet[index][1] =
|
||||
this.default_feet_pos[index][1] +
|
||||
sin(this.phase_time * Math.PI) * this.gait_state.step_height;
|
||||
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
|
||||
return this.body_state.feet[index];
|
||||
}
|
||||
}
|
||||
|
||||
export class FourPhaseWalkState extends PhaseGaitState {
|
||||
protected name = 'Four phase walk';
|
||||
protected num_phases = 4;
|
||||
protected phase_speed_factor = 6;
|
||||
protected contact_phases = [
|
||||
[1, 0, 1, 1],
|
||||
[1, 1, 1, 0],
|
||||
[1, 1, 1, 0],
|
||||
[1, 0, 1, 1]
|
||||
];
|
||||
protected swing_stand_ratio = 1 / (this.num_phases - 1);
|
||||
|
||||
begin() {
|
||||
super.begin();
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end();
|
||||
}
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
return super.step(body_state, command, dt);
|
||||
}
|
||||
}
|
||||
|
||||
export class EightPhaseWalkState extends PhaseGaitState {
|
||||
protected name = 'Eight phase walk';
|
||||
protected num_phases = 8;
|
||||
protected phase_speed_factor = 4;
|
||||
protected contact_phases = [
|
||||
[1, 0, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 0, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 0],
|
||||
[1, 1, 1, 0, 1, 1, 1, 1]
|
||||
];
|
||||
protected shifts = [
|
||||
[-0.05, 0, -0.2],
|
||||
[0.3, 0, 0.2],
|
||||
[-0.05, 0, 0.2],
|
||||
[0.3, 0, -0.2]
|
||||
];
|
||||
protected swing_stand_ratio = 1 / (this.num_phases - 1);
|
||||
|
||||
begin() {
|
||||
super.begin();
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end();
|
||||
}
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
return super.step(body_state, command, dt);
|
||||
}
|
||||
step(body_state: body_state_t, command: ControllerCommand) {
|
||||
body_state.omega = 0
|
||||
body_state.phi = command.rx * 10 * (Math.PI / 2)
|
||||
body_state.psi = command.ry * 10 * (Math.PI / 2)
|
||||
body_state.xm = command.ly / 4
|
||||
body_state.zm = command.lx / 4
|
||||
body_state.feet = this.default_feet_pos
|
||||
return body_state
|
||||
}
|
||||
}
|
||||
|
||||
export class BezierState extends GaitState {
|
||||
protected name = 'Bezier';
|
||||
protected phase = 0;
|
||||
protected phase_num = 0;
|
||||
protected step_length: number = 0;
|
||||
offset = [0, 0.5, 0.5, 0];
|
||||
protected name = 'Bezier'
|
||||
protected phase = 0
|
||||
protected phase_num = 0
|
||||
protected step_length = 0
|
||||
protected stand_offset = 0.85
|
||||
protected mode: 'crawl' | 'trot' = 'trot'
|
||||
protected speed_factor = 1
|
||||
offset = [0, 0.5, 0.75, 0.25]
|
||||
|
||||
begin() {
|
||||
super.begin();
|
||||
protected shift_start_pos = { x: 0, z: 0 }
|
||||
protected shift_target_pos = { x: 0, z: 0 }
|
||||
protected shift_start_time = 0
|
||||
protected current_shift_leg = -1
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.set_mode(this.mode)
|
||||
}
|
||||
|
||||
begin() {
|
||||
super.begin()
|
||||
}
|
||||
|
||||
set_mode(mode: 'crawl' | 'trot', duty?: number, order?: [number, number, number, number]) {
|
||||
console.log('BezierState set_mode', mode)
|
||||
|
||||
this.mode = mode
|
||||
if (mode === 'crawl') {
|
||||
this.speed_factor = 0.5
|
||||
this.stand_offset = duty ?? 0.85
|
||||
const o = order ?? [3, 0, 2, 1]
|
||||
const base = [0, 0.25, 0.5, 0.75]
|
||||
const offsets = new Array(4).fill(0)
|
||||
for (let i = 0; i < 4; i++) offsets[o[i]] = base[i]
|
||||
this.offset = offsets
|
||||
} else {
|
||||
this.speed_factor = 2
|
||||
this.stand_offset = duty ?? 0.6
|
||||
this.offset = order ? (order.map(v => v % 1) as number[]) : [0, 0.5, 0.5, 0]
|
||||
}
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end();
|
||||
end() {
|
||||
super.end()
|
||||
}
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
super.step(body_state, command, dt)
|
||||
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2)
|
||||
if (this.gait_state.step_x < 0) this.step_length = -this.step_length
|
||||
this.update_phase()
|
||||
this.update_body_position()
|
||||
this.update_feet_positions()
|
||||
return this.body_state
|
||||
}
|
||||
|
||||
update_phase() {
|
||||
const m = this.gait_state
|
||||
if (m.step_x === 0 && m.step_z === 0 && m.step_angle === 0) {
|
||||
this.phase = 0
|
||||
return
|
||||
}
|
||||
this.phase += this.dt * m.step_velocity * this.speed_factor
|
||||
if (this.phase >= 1) {
|
||||
this.phase_num = (this.phase_num + 1) % 2
|
||||
this.phase = 0
|
||||
}
|
||||
}
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
super.step(body_state, command, dt);
|
||||
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2);
|
||||
if (this.gait_state.step_x < 0) {
|
||||
this.step_length = -this.step_length;
|
||||
update_body_position() {
|
||||
const m = this.gait_state
|
||||
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
|
||||
if (!moving) return
|
||||
|
||||
if (this.mode !== 'crawl') return
|
||||
|
||||
const { stance, swing, next_swing, time_to_lift } = this.get_leg_states()
|
||||
|
||||
if (stance.length >= 3 && swing.length === 0 && next_swing !== -1) {
|
||||
if (this.current_shift_leg !== next_swing) {
|
||||
this.current_shift_leg = next_swing
|
||||
this.shift_start_pos.x = this.body_state.xm
|
||||
this.shift_start_pos.z = this.body_state.zm
|
||||
|
||||
const remaining_legs = stance.filter(leg => leg !== next_swing)
|
||||
const target = this.stance_centroid(remaining_legs)
|
||||
this.shift_target_pos.x = target[0]
|
||||
this.shift_target_pos.z = target[2]
|
||||
|
||||
this.shift_start_time = time_to_lift
|
||||
}
|
||||
|
||||
const total_time = this.shift_start_time
|
||||
const progress = total_time > 0 ? 1 - time_to_lift / total_time : 1
|
||||
const smooth_progress = this.smoothstep01(Math.max(0, Math.min(1, progress)))
|
||||
|
||||
this.body_state.xm = this.lerp(
|
||||
this.shift_start_pos.x,
|
||||
this.shift_target_pos.x,
|
||||
smooth_progress
|
||||
)
|
||||
this.body_state.zm = this.lerp(
|
||||
this.shift_start_pos.z,
|
||||
this.shift_target_pos.z,
|
||||
smooth_progress
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected lerp(a: number, b: number, t: number): number {
|
||||
return a + (b - a) * t
|
||||
}
|
||||
|
||||
protected stance_centroid(legs: number[]): number[] {
|
||||
if (legs.length === 0) return [this.body_state.xm, 0, this.body_state.zm]
|
||||
|
||||
let sx = 0,
|
||||
sz = 0
|
||||
for (const i of legs) {
|
||||
sx += this.body_state.feet[i][0]
|
||||
sz += this.body_state.feet[i][2]
|
||||
}
|
||||
return [sx / legs.length, 0, sz / legs.length]
|
||||
}
|
||||
|
||||
protected get_leg_states(): {
|
||||
stance: number[]
|
||||
swing: number[]
|
||||
next_swing: number
|
||||
time_to_lift: number
|
||||
} {
|
||||
const stance: number[] = []
|
||||
const swing: number[] = []
|
||||
let next_swing = -1
|
||||
let min_time_to_swing = Infinity
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
let phase = this.phase + this.offset[i]
|
||||
if (phase >= 1) phase -= 1
|
||||
|
||||
if (phase <= this.stand_offset) {
|
||||
stance.push(i)
|
||||
const time_to_swing = this.stand_offset - phase
|
||||
if (time_to_swing < min_time_to_swing) {
|
||||
min_time_to_swing = time_to_swing
|
||||
next_swing = i
|
||||
}
|
||||
this.update_phase();
|
||||
this.update_feet_positions();
|
||||
return this.body_state;
|
||||
} else {
|
||||
swing.push(i)
|
||||
}
|
||||
}
|
||||
|
||||
update_phase() {
|
||||
this.phase += this.dt * this.gait_state.step_velocity * 2;
|
||||
if (this.phase >= 1) {
|
||||
this.phase_num += 1;
|
||||
this.phase_num %= 2;
|
||||
this.phase = 0;
|
||||
}
|
||||
}
|
||||
return { stance, swing, next_swing, time_to_lift: min_time_to_swing }
|
||||
}
|
||||
|
||||
update_feet_positions() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.body_state.feet[i] = this.update_foot_position(i);
|
||||
}
|
||||
}
|
||||
protected smoothstep01(t: number): number {
|
||||
const x = Math.max(0, Math.min(1, t))
|
||||
return x * x * (3 - 2 * x)
|
||||
}
|
||||
|
||||
update_foot_position(index: number): number[] {
|
||||
let phase = this.phase + this.offset[index];
|
||||
if (phase >= 1) {
|
||||
phase -= 1;
|
||||
}
|
||||
this.body_state.feet[index][0] = this.default_feet_pos[index][0];
|
||||
this.body_state.feet[index][1] = this.default_feet_pos[index][1];
|
||||
this.body_state.feet[index][2] = this.default_feet_pos[index][2];
|
||||
return phase <= 0.75 ?
|
||||
this.stand_controller(index, phase / 0.75)
|
||||
: this.swing_controller(index, (phase - 0.75) / (1 - 0.75));
|
||||
}
|
||||
update_feet_positions() {
|
||||
for (let i = 0; i < 4; i++) this.body_state.feet[i] = this.update_foot_position(i)
|
||||
}
|
||||
|
||||
stand_controller(index: number, phase: number) {
|
||||
let depth = this.gait_state.step_depth;
|
||||
return this.controller(index, phase, stance_curve, depth);
|
||||
}
|
||||
update_foot_position(index: number): number[] {
|
||||
let phase = this.phase + this.offset[index]
|
||||
if (phase >= 1) phase -= 1
|
||||
this.body_state.feet[index][0] = this.default_feet_pos[index][0]
|
||||
this.body_state.feet[index][1] = this.default_feet_pos[index][1]
|
||||
this.body_state.feet[index][2] = this.default_feet_pos[index][2]
|
||||
return phase <= this.stand_offset ?
|
||||
this.stand_controller(index, phase / this.stand_offset)
|
||||
: this.swing_controller(index, (phase - this.stand_offset) / (1 - this.stand_offset))
|
||||
}
|
||||
|
||||
swing_controller(index: number, phase: number) {
|
||||
let height = this.gait_state.step_height;
|
||||
return this.controller(index, phase, bezier_curve, height);
|
||||
}
|
||||
stand_controller(index: number, phase: number) {
|
||||
const depth = this.gait_state.step_depth
|
||||
return this.controller(index, phase, stance_curve, depth)
|
||||
}
|
||||
|
||||
controller(
|
||||
index: number,
|
||||
phase: number,
|
||||
controller: (length: number, angle: number, ...args: number[]) => number[],
|
||||
...args: number[]
|
||||
) {
|
||||
let length = this.step_length / 2;
|
||||
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2;
|
||||
const delta_pos = controller(length, angle, ...args, phase);
|
||||
swing_controller(index: number, phase: number) {
|
||||
const height = this.gait_state.step_height
|
||||
return this.controller(index, phase, bezier_curve, height)
|
||||
}
|
||||
|
||||
length = this.gait_state.step_angle * 2;
|
||||
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index]);
|
||||
controller(
|
||||
index: number,
|
||||
phase: number,
|
||||
controller: (length: number, angle: number, ...args: number[]) => number[],
|
||||
...args: number[]
|
||||
) {
|
||||
let length = this.step_length / 2
|
||||
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2
|
||||
const delta_pos = controller(length, angle, ...args, phase)
|
||||
|
||||
const delta_rot = controller(length, angle, ...args, phase);
|
||||
length = this.gait_state.step_angle * 2
|
||||
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index])
|
||||
|
||||
this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2;
|
||||
this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2;
|
||||
if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle)
|
||||
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2;
|
||||
const delta_rot = controller(length, angle, ...args, phase)
|
||||
|
||||
return this.body_state.feet[index];
|
||||
}
|
||||
this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2
|
||||
this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2
|
||||
if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle)
|
||||
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2
|
||||
|
||||
return this.body_state.feet[index]
|
||||
}
|
||||
}
|
||||
|
||||
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
|
||||
const X_POLAR = Math.cos(angle);
|
||||
const Y_POLAR = Math.sin(angle);
|
||||
const X_POLAR = Math.cos(angle)
|
||||
const Y_POLAR = Math.sin(angle)
|
||||
|
||||
const step = length * (1 - 2 * phase);
|
||||
const X = step * X_POLAR;
|
||||
const Z = step * Y_POLAR;
|
||||
let Y = 0;
|
||||
|
||||
if (length !== 0) {
|
||||
Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length));
|
||||
}
|
||||
return [X, Y, Z];
|
||||
};
|
||||
const step = length * (1 - 2 * phase)
|
||||
const X = step * X_POLAR
|
||||
const Z = step * Y_POLAR
|
||||
let Y = 0
|
||||
if (length !== 0) Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length))
|
||||
return [X, Y, Z]
|
||||
}
|
||||
|
||||
const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => {
|
||||
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2);
|
||||
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0]);
|
||||
const offsets = [
|
||||
current_foot_pos[0] - default_foot_pos[0],
|
||||
current_foot_pos[2] - default_foot_pos[2],
|
||||
current_foot_pos[1] - default_foot_pos[1]
|
||||
];
|
||||
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2);
|
||||
const offset_mod = Math.atan2(offset_mag, foot_mag);
|
||||
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2)
|
||||
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0])
|
||||
const offsets = [
|
||||
current_foot_pos[0] - default_foot_pos[0],
|
||||
current_foot_pos[2] - default_foot_pos[2],
|
||||
current_foot_pos[1] - default_foot_pos[1]
|
||||
]
|
||||
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2)
|
||||
const offset_mod = Math.atan2(offset_mag, foot_mag)
|
||||
|
||||
return Math.PI / 2.0 + foot_dir + offset_mod;
|
||||
};
|
||||
return Math.PI / 2.0 + foot_dir + offset_mod
|
||||
}
|
||||
|
||||
const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => {
|
||||
const control_points = get_control_points(length, angle, height);
|
||||
const n = control_points.length - 1;
|
||||
const control_points = get_control_points(length, angle, height)
|
||||
const n = control_points.length - 1
|
||||
|
||||
const point = [0, 0, 0]
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i)
|
||||
point[0] += bernstein_poly * control_points[i][0]
|
||||
point[1] += bernstein_poly * control_points[i][1]
|
||||
point[2] += bernstein_poly * control_points[i][2]
|
||||
}
|
||||
return point
|
||||
}
|
||||
|
||||
const point = [0, 0, 0];
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i);
|
||||
point[0] += bernstein_poly * control_points[i][0];
|
||||
point[1] += bernstein_poly * control_points[i][1];
|
||||
point[2] += bernstein_poly * control_points[i][2];
|
||||
}
|
||||
return point;
|
||||
};
|
||||
const get_control_points = (length: number, angle: number, height: number): number[][] => {
|
||||
const X_POLAR = Math.cos(angle);
|
||||
const Z_POLAR = Math.sin(angle);
|
||||
const X_POLAR = Math.cos(angle)
|
||||
const Z_POLAR = Math.sin(angle)
|
||||
|
||||
const STEP = [
|
||||
-length,
|
||||
-length * 1.4,
|
||||
-length * 1.5,
|
||||
-length * 1.5,
|
||||
-length * 1.5,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
length * 1.5,
|
||||
length * 1.5,
|
||||
length * 1.4,
|
||||
length
|
||||
];
|
||||
const STEP = [
|
||||
-length,
|
||||
-length * 1.4,
|
||||
-length * 1.5,
|
||||
-length * 1.5,
|
||||
-length * 1.5,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
length * 1.5,
|
||||
length * 1.5,
|
||||
length * 1.4,
|
||||
length
|
||||
]
|
||||
|
||||
const Y = [
|
||||
0.0,
|
||||
0.0,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 1.1,
|
||||
height * 1.1,
|
||||
height * 1.1,
|
||||
0.0,
|
||||
0.0
|
||||
];
|
||||
const Y = [
|
||||
0.0,
|
||||
0.0,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 1.1,
|
||||
height * 1.1,
|
||||
height * 1.1,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
|
||||
const control_points: number[][] = [];
|
||||
const control_points: number[][] = []
|
||||
|
||||
for (let i = 0; i < STEP.length; i++) {
|
||||
const X = STEP[i] * X_POLAR;
|
||||
const Z = STEP[i] * Z_POLAR;
|
||||
control_points.push([X, Y[i], Z]);
|
||||
}
|
||||
for (let i = 0; i < STEP.length; i++) {
|
||||
const X = STEP[i] * X_POLAR
|
||||
const Z = STEP[i] * Z_POLAR
|
||||
control_points.push([X, Y[i], Z])
|
||||
}
|
||||
|
||||
return control_points;
|
||||
};
|
||||
return control_points
|
||||
}
|
||||
|
||||
const comb = (n: number, k: number): number => {
|
||||
if (k < 0 || k > n) return 0;
|
||||
if (k === 0 || k === n) return 1;
|
||||
k = Math.min(k, n - k);
|
||||
let c = 1;
|
||||
for (let i = 0; i < k; i++) {
|
||||
c = (c * (n - i)) / (i + 1);
|
||||
}
|
||||
return c;
|
||||
};
|
||||
if (k < 0 || k > n) return 0
|
||||
if (k === 0 || k === n) return 1
|
||||
k = Math.min(k, n - k)
|
||||
let c = 1
|
||||
for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1)
|
||||
return c
|
||||
}
|
||||
|
||||
+127
-307
@@ -1,320 +1,140 @@
|
||||
|
||||
export interface body_state_t {
|
||||
omega: number;
|
||||
phi: number;
|
||||
psi: number;
|
||||
xm: number;
|
||||
ym: number;
|
||||
zm: number;
|
||||
feet: number[][];
|
||||
omega: number
|
||||
phi: number
|
||||
psi: number
|
||||
xm: number
|
||||
ym: number
|
||||
zm: number
|
||||
feet: number[][]
|
||||
}
|
||||
|
||||
export interface position {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
}
|
||||
|
||||
export interface target_position {
|
||||
x: number;
|
||||
z: number;
|
||||
yaw: number;
|
||||
x: number
|
||||
z: number
|
||||
yaw: number
|
||||
}
|
||||
|
||||
const { cos, sin, atan2, sqrt } = Math;
|
||||
export interface KinematicParams {
|
||||
coxa: number
|
||||
coxa_offset: number
|
||||
femur: number
|
||||
tibia: number
|
||||
L: number
|
||||
W: number
|
||||
}
|
||||
|
||||
const DEG2RAD = 0.017453292519943;
|
||||
const { cos, sin, atan2, acos, sqrt, max, min } = Math
|
||||
|
||||
const DEG2RAD = 0.017453292519943
|
||||
|
||||
export default class Kinematic {
|
||||
l1: number;
|
||||
l2: number;
|
||||
l3: number;
|
||||
l4: number;
|
||||
|
||||
L: number;
|
||||
W: number;
|
||||
|
||||
DEG2RAD = DEG2RAD;
|
||||
|
||||
sHp = sin(Math.PI / 2);
|
||||
cHp = cos(Math.PI / 2);
|
||||
|
||||
Tlf: number[][] = [];
|
||||
Trf: number[][] = [];
|
||||
Tlb: number[][] = [];
|
||||
Trb: number[][] = [];
|
||||
|
||||
point_lf: number[][];
|
||||
point_rf: number[][];
|
||||
point_lb: number[][];
|
||||
point_rb: number[][];
|
||||
Ix: number[][];
|
||||
|
||||
constructor() {
|
||||
this.l1 = 60.5 / 100;
|
||||
this.l2 = 10 / 100;
|
||||
this.l3 = 100.7 / 100;
|
||||
this.l4 = 118.5 / 100;
|
||||
|
||||
this.L = 207.5 / 100;
|
||||
this.W = 78 / 100;
|
||||
|
||||
this.point_lf = [
|
||||
[this.cHp, 0, this.sHp, this.L / 2],
|
||||
[0, 1, 0, 0],
|
||||
[-this.sHp, 0, this.cHp, this.W / 2],
|
||||
[0, 0, 0, 1]
|
||||
];
|
||||
|
||||
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[] {
|
||||
this.bodyIK(body_state);
|
||||
|
||||
return [
|
||||
...this.legIK(this.multiplyVector(this.inverse(this.Tlf), body_state.feet[0])),
|
||||
...this.legIK(
|
||||
this.multiplyVector(
|
||||
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;
|
||||
}
|
||||
coxa: number
|
||||
coxa_offset: number
|
||||
femur: number
|
||||
tibia: number
|
||||
|
||||
L: number
|
||||
W: number
|
||||
|
||||
DEG2RAD = DEG2RAD
|
||||
|
||||
mountOffsets: number[][]
|
||||
|
||||
invMountRot = [
|
||||
[0, 0, -1],
|
||||
[0, 1, 0],
|
||||
[1, 0, 0]
|
||||
]
|
||||
|
||||
constructor(params: KinematicParams) {
|
||||
this.coxa = params.coxa
|
||||
this.coxa_offset = params.coxa_offset
|
||||
this.femur = params.femur
|
||||
this.tibia = params.tibia
|
||||
this.L = params.L
|
||||
this.W = params.W
|
||||
|
||||
this.mountOffsets = [
|
||||
[this.L / 2, 0, this.W / 2],
|
||||
[this.L / 2, 0, -this.W / 2],
|
||||
[-this.L / 2, 0, this.W / 2],
|
||||
[-this.L / 2, 0, -this.W / 2]
|
||||
]
|
||||
}
|
||||
|
||||
getDefaultFeetPos(): number[][] {
|
||||
return this.mountOffsets.map((offset, i) => {
|
||||
return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
|
||||
})
|
||||
}
|
||||
|
||||
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.coxa * this.coxa))
|
||||
const G = F - this.coxa_offset
|
||||
const H = sqrt(G * G + z * z)
|
||||
const t1 = -atan2(y, x) - atan2(F, -this.coxa)
|
||||
const D =
|
||||
(H * H - this.femur * this.femur - this.tibia * this.tibia) / (2 * this.femur * this.tibia)
|
||||
const t3 = acos(max(-1, min(1, D)))
|
||||
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * 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 [
|
||||
[cp * cy, -cp * sy, sp],
|
||||
[sr * sp * cy + sy * cr, -sr * sp * sy + cr * cy, -sr * cp],
|
||||
[sr * sy - sp * cr * cy, sr * cy + sp * sy * cr, cr * cp]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -157,10 +157,11 @@ export default class SceneBuilder {
|
||||
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
|
||||
this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
|
||||
this.orbit.minDistance = 5
|
||||
this.orbit.minDistance = minDistance + (maxDistance - minDistance) / 2
|
||||
this.orbit.maxDistance = maxDistance
|
||||
this.orbit.autoRotate = autoRotate
|
||||
this.orbit.update()
|
||||
this.orbit.minDistance = minDistance
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,62 @@
|
||||
import { api } from '$lib/api';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { api } from '$lib/api'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import Kinematic from '$lib/kinematic'
|
||||
import { persistentStore } from '$lib/utilities'
|
||||
import { derived, type Writable } from 'svelte/store'
|
||||
import { base } from '$app/paths'
|
||||
|
||||
let featureFlagsStore: Writable<Record<string, boolean>>;
|
||||
let featureFlagsStore: Writable<Record<string, boolean | string>>
|
||||
|
||||
export function useFeatureFlags() {
|
||||
if (!featureFlagsStore) {
|
||||
featureFlagsStore = writable<Record<string, boolean>>({});
|
||||
if (!featureFlagsStore) {
|
||||
featureFlagsStore = persistentStore<Record<string, boolean | string>>('FeatureFlags', {})
|
||||
|
||||
api.get<Record<string, boolean>>('/api/features').then((result) => {
|
||||
if (result.isOk()) featureFlagsStore.set(result.inner);
|
||||
else {
|
||||
notifications.error('Feature flag could not be fetched', 2500);
|
||||
}
|
||||
});
|
||||
}
|
||||
api.get<Record<string, boolean>>('/api/features').then(result => {
|
||||
if (result.isOk()) featureFlagsStore.set(result.inner)
|
||||
else {
|
||||
notifications.error('Feature flag could not be fetched', 2500)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return featureFlagsStore;
|
||||
}
|
||||
return featureFlagsStore
|
||||
}
|
||||
|
||||
export const variants = {
|
||||
SPOTMICRO_ESP32: {
|
||||
model: `${base}/spot_micro.urdf.xacro`,
|
||||
stl: `${base}/stl.zip`,
|
||||
kinematics: {
|
||||
coxa: 60.5 / 100,
|
||||
coxa_offset: 10 / 100,
|
||||
femur: 111.7 / 100,
|
||||
tibia: 118.5 / 100,
|
||||
L: 207.5 / 100,
|
||||
W: 78 / 100
|
||||
}
|
||||
},
|
||||
SPOTMICRO_YERTLE: {
|
||||
model: `${base}/yertle.URDF`,
|
||||
stl: `${base}/URDF.zip`,
|
||||
kinematics: {
|
||||
coxa: 35 / 100,
|
||||
coxa_offset: 0 / 100,
|
||||
femur: 130 / 100,
|
||||
tibia: 130 / 100,
|
||||
L: 240 / 100,
|
||||
W: 78 / 100
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const currentVariant = derived(useFeatureFlags(), $flagStore => {
|
||||
const variantFlag = $flagStore['variant'] as string
|
||||
return variantFlag && variants[variantFlag as keyof typeof variants] ?
|
||||
variants[variantFlag as keyof typeof variants]
|
||||
: variants.SPOTMICRO_ESP32
|
||||
})
|
||||
|
||||
export const currentKinematic = derived(
|
||||
currentVariant,
|
||||
$variant => new Kinematic($variant.kinematics)
|
||||
)
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
import { writable } from 'svelte/store';
|
||||
import appEnv from 'app-env';
|
||||
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public';
|
||||
|
||||
export const location = appEnv.VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
|
||||
export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
|
||||
|
||||
@@ -1,45 +1,54 @@
|
||||
import type { ControllerInput } from '$lib/types/models';
|
||||
import { persistentStore } from '$lib/utilities/svelte-utilities';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import type { ControllerInput } from '$lib/types/models'
|
||||
import { persistentStore } from '$lib/utilities/svelte-utilities'
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
|
||||
export const emulateModel = writable(true);
|
||||
export const emulateModel = writable(true)
|
||||
|
||||
export const jointNames = persistentStore('joint_names', <string[]>[]);
|
||||
export const jointNames = persistentStore('joint_names', <string[]>[])
|
||||
|
||||
export const model = writable();
|
||||
export const model = writable()
|
||||
|
||||
export const modes = [
|
||||
'deactivated',
|
||||
'idle',
|
||||
'calibration',
|
||||
'rest',
|
||||
'stand',
|
||||
'crawl',
|
||||
'walk'
|
||||
] as const;
|
||||
export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'walk'] as const
|
||||
|
||||
export type Modes = (typeof modes)[number];
|
||||
export type Modes = (typeof modes)[number]
|
||||
|
||||
export enum ModesEnum {
|
||||
Deactivated,
|
||||
Idle,
|
||||
Calibration,
|
||||
Rest,
|
||||
Stand,
|
||||
Crawl,
|
||||
Walk
|
||||
Deactivated = 0,
|
||||
Idle = 1,
|
||||
Calibration = 2,
|
||||
Rest = 3,
|
||||
Stand = 4,
|
||||
Walk = 5
|
||||
}
|
||||
|
||||
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated);
|
||||
export enum WalkGaits {
|
||||
Trot = 0,
|
||||
Crawl = 1
|
||||
}
|
||||
|
||||
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]);
|
||||
export const walkGaits = ['trot', 'crawl'] as const
|
||||
|
||||
export const kinematicData = writable([0, 0, 0, 0, 1, 0]);
|
||||
export const walkGaitLabels: Record<WalkGaits, string> = {
|
||||
[WalkGaits.Trot]: 'Trot',
|
||||
[WalkGaits.Crawl]: 'Crawl'
|
||||
}
|
||||
|
||||
export const walkGaitToMode = (gait: WalkGaits): 'trot' | 'crawl' => {
|
||||
return gait === WalkGaits.Trot ? 'trot' : 'crawl'
|
||||
}
|
||||
|
||||
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated)
|
||||
|
||||
export const walkGait: Writable<WalkGaits> = writable(WalkGaits.Trot)
|
||||
|
||||
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0])
|
||||
|
||||
export const kinematicData = writable([0, 0, 0, 0, 1, 0])
|
||||
|
||||
export const input: Writable<ControllerInput> = writable({
|
||||
left: { x: 0, y: 0 },
|
||||
right: { x: 0, y: 0 },
|
||||
height: 50,
|
||||
speed: 50,
|
||||
s1: 50
|
||||
});
|
||||
left: { x: 0, y: 0 },
|
||||
right: { x: 0, y: 0 },
|
||||
height: 0.5,
|
||||
speed: 0.5,
|
||||
s1: 0.05
|
||||
})
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { readable } from 'svelte/store';
|
||||
|
||||
export const heading = readable(0, (set) => {
|
||||
const updateHeading = (e: any) => {
|
||||
let alpha;
|
||||
if (e.webkitCompassHeading) alpha = e.webkitCompassHeading;
|
||||
else if (e.alpha) alpha = e.alpha;
|
||||
else {
|
||||
let q = e.target.quaternion;
|
||||
alpha =
|
||||
Math.atan2(2 * q[0] * q[1] + 2 * q[2] * q[3], 1 - 2 * q[1] * q[1] - 2 * q[2] * q[2]) *
|
||||
(180 / Math.PI);
|
||||
if (alpha < 0) alpha += 360;
|
||||
}
|
||||
set(alpha);
|
||||
};
|
||||
if ('AbsoluteOrientationSensor' in window) {
|
||||
var sensor = new window.AbsoluteOrientationSensor({ frequency: 60 }) as any;
|
||||
sensor.addEventListener('reading', updateHeading);
|
||||
sensor.start();
|
||||
} else if (window.DeviceMotionEvent) window.addEventListener('deviceorientation', updateHeading);
|
||||
|
||||
return () => {
|
||||
if ('AbsoluteOrientationSensor' in window) sensor.removeEventListener('reading', updateHeading);
|
||||
window.addEventListener('deviceorientation', updateHeading);
|
||||
};
|
||||
});
|
||||
+139
-101
@@ -1,122 +1,160 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
|
||||
type SocketEvent = (typeof socketEvents)[number];
|
||||
|
||||
type SocketMessage = [number, string?, unknown?];
|
||||
|
||||
let useBinary = false;
|
||||
|
||||
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
|
||||
useBinary = data instanceof ArrayBuffer;
|
||||
|
||||
try {
|
||||
if (useBinary) {
|
||||
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage;
|
||||
}
|
||||
return JSON.parse(data as string);
|
||||
} catch (error) {
|
||||
console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const encodeMessage = (data: unknown) => {
|
||||
try {
|
||||
return useBinary ? encode(data) : JSON.stringify(data);
|
||||
} catch (error) {
|
||||
console.error(`Could not encode data: ${data} - ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
function createWebSocket() {
|
||||
let listeners = new Map<string, Set<(data?: unknown) => void>>();
|
||||
const { subscribe, set } = writable(false);
|
||||
const reconnectTimeoutTime = 5000;
|
||||
let unresponsiveTimeoutId: number;
|
||||
let reconnectTimeoutId: number;
|
||||
let ws: WebSocket;
|
||||
let socketUrl: string | URL;
|
||||
const listeners = new Map<string, Set<(data?: unknown) => void>>();
|
||||
const { subscribe, set } = writable(false);
|
||||
const reconnectTimeoutTime = 5000;
|
||||
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>;
|
||||
let reconnectTimeoutId: ReturnType<typeof setTimeout>;
|
||||
let ws: WebSocket;
|
||||
let socketUrl: string | URL;
|
||||
|
||||
function init(url: string | URL) {
|
||||
socketUrl = url;
|
||||
connect();
|
||||
}
|
||||
function init(url: string | URL) {
|
||||
socketUrl = url;
|
||||
connect();
|
||||
}
|
||||
|
||||
function disconnect(reason: SocketEvent, event?: Event) {
|
||||
ws.close();
|
||||
set(false);
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get(reason)?.forEach((listener) => listener(event));
|
||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
|
||||
}
|
||||
function disconnect(reason: SocketEvent, event?: Event) {
|
||||
ws.close();
|
||||
set(false);
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get(reason)?.forEach(listener => listener(event));
|
||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(socketUrl);
|
||||
ws.onopen = (ev) => {
|
||||
set(true);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get('open')?.forEach((listener) => listener(ev));
|
||||
for (const event of listeners.keys()) {
|
||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
};
|
||||
ws.onmessage = (message) => {
|
||||
resetUnresponsiveCheck();
|
||||
let data = message.data;
|
||||
if (data instanceof ArrayBuffer) {
|
||||
listeners.get('binary')?.forEach((listener) => listener(data));
|
||||
return;
|
||||
}
|
||||
data = data.substring(1);
|
||||
function connect() {
|
||||
ws = new WebSocket(socketUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = ev => {
|
||||
ping();
|
||||
useBinary = true;
|
||||
ping();
|
||||
set(true);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get('open')?.forEach(listener => listener(ev));
|
||||
for (const event of listeners.keys()) {
|
||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
};
|
||||
ws.onmessage = frame => {
|
||||
resetUnresponsiveCheck();
|
||||
const message = decodeMessage(frame.data);
|
||||
if (!message) return;
|
||||
const [, event, payload = undefined] = message;
|
||||
if (event) listeners.get(event)?.forEach(listener => listener(payload));
|
||||
};
|
||||
ws.onerror = ev => disconnect('error', ev);
|
||||
ws.onclose = ev => disconnect('close', ev);
|
||||
}
|
||||
|
||||
if (!data) return;
|
||||
function unsubscribe(event: string, listener?: (data: unknown) => void) {
|
||||
const eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
|
||||
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
|
||||
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {}
|
||||
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
|
||||
};
|
||||
ws.onerror = (ev) => disconnect('error', ev);
|
||||
ws.onclose = (ev) => disconnect('close', ev);
|
||||
}
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function unsubscribe(event: string, listener?: (data: any) => void) {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([2, event, data]);
|
||||
}
|
||||
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
function unsubscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([1, event]);
|
||||
}
|
||||
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
||||
}
|
||||
function subscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([0, event]);
|
||||
}
|
||||
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(`2/${event}[${JSON.stringify(data)}]`);
|
||||
}
|
||||
function send(data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
const serialized = encodeMessage(data);
|
||||
if (!serialized) {
|
||||
console.error('Could not serialize data:', data);
|
||||
return;
|
||||
}
|
||||
ws.send(serialized);
|
||||
}
|
||||
|
||||
function unsubscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send('1/' + event);
|
||||
}
|
||||
function ping() {
|
||||
const serialized = encodeMessage([4]);
|
||||
if (!serialized) {
|
||||
console.error('Could not serialize message');
|
||||
return;
|
||||
}
|
||||
ws.send(serialized);
|
||||
}
|
||||
|
||||
function subscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send('0/' + event);
|
||||
}
|
||||
return {
|
||||
subscribe,
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event as SocketEvent)) {
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
eventListeners = new Set();
|
||||
listeners.set(event, eventListeners);
|
||||
}
|
||||
eventListeners.add(listener as (data: unknown) => void);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event as SocketEvent)) {
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
eventListeners = new Set();
|
||||
listeners.set(event, eventListeners);
|
||||
}
|
||||
eventListeners.add(listener as (data: any) => void);
|
||||
|
||||
return () => {
|
||||
unsubscribe(event, listener);
|
||||
};
|
||||
},
|
||||
off: (event: string, listener?: (data: any) => void) => {
|
||||
unsubscribe(event, listener);
|
||||
}
|
||||
};
|
||||
return () => {
|
||||
unsubscribe(event, listener as (data: unknown) => void);
|
||||
};
|
||||
},
|
||||
off: <T>(event: string, listener?: (data: T) => void) => {
|
||||
unsubscribe(event, listener as (data: unknown) => void);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const socket = createWebSocket();
|
||||
export const socket = createWebSocket();
|
||||
|
||||
+195
-134
@@ -1,178 +1,239 @@
|
||||
export type vector = { x: number; y: number };
|
||||
export enum MessageTopic {
|
||||
imu = 'imu',
|
||||
mode = 'mode',
|
||||
input = 'input',
|
||||
analytics = 'analytics',
|
||||
position = 'position',
|
||||
angles = 'angles',
|
||||
i2cScan = 'i2cScan',
|
||||
peripheralSettings = 'peripheralSettings',
|
||||
otastatus = 'otastatus',
|
||||
gait = 'walk_gait',
|
||||
servoState = 'servoState',
|
||||
servoPWM = 'servoPWM',
|
||||
WiFiSettings = 'WiFiSettings',
|
||||
sonar = 'sonar',
|
||||
rssi = 'rssi'
|
||||
}
|
||||
|
||||
export type vector = { x: number; y: number }
|
||||
|
||||
export interface ControllerInput {
|
||||
left: vector;
|
||||
right: vector;
|
||||
height: number;
|
||||
speed: number;
|
||||
s1: number;
|
||||
left: vector
|
||||
right: vector
|
||||
height: number
|
||||
speed: number
|
||||
s1: number
|
||||
}
|
||||
|
||||
export type GithubRelease = {
|
||||
message: string;
|
||||
tag_name: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}>;
|
||||
};
|
||||
message: string
|
||||
tag_name: string
|
||||
assets: Array<{
|
||||
name: string
|
||||
browser_download_url: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type angles = number[] | Int16Array;
|
||||
export type angles = number[] | Int16Array
|
||||
|
||||
export type WifiStatus = {
|
||||
status: number;
|
||||
local_ip: string;
|
||||
mac_address: string;
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
subnet_mask: string;
|
||||
gateway_ip: string;
|
||||
dns_ip_1: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
status: number
|
||||
local_ip: string
|
||||
mac_address: string
|
||||
rssi: number
|
||||
ssid: string
|
||||
bssid: string
|
||||
channel: number
|
||||
subnet_mask: string
|
||||
gateway_ip: string
|
||||
dns_ip_1: string
|
||||
dns_ip_2?: string
|
||||
}
|
||||
|
||||
export type WifiSettings = {
|
||||
hostname: string;
|
||||
priority_RSSI: boolean;
|
||||
wifi_networks: KnownNetworkItem[];
|
||||
};
|
||||
hostname: string
|
||||
priority_RSSI: boolean
|
||||
wifi_networks: KnownNetworkItem[]
|
||||
}
|
||||
|
||||
export type NetworkList = {
|
||||
networks: NetworkItem[];
|
||||
};
|
||||
networks: NetworkItem[]
|
||||
}
|
||||
|
||||
export type KnownNetworkItem = {
|
||||
ssid: string;
|
||||
password: string;
|
||||
static_ip_config: boolean;
|
||||
local_ip?: string;
|
||||
subnet_mask?: string;
|
||||
gateway_ip?: string;
|
||||
dns_ip_1?: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
ssid: string
|
||||
password: string
|
||||
static_ip_config: boolean
|
||||
local_ip?: string
|
||||
subnet_mask?: string
|
||||
gateway_ip?: string
|
||||
dns_ip_1?: string
|
||||
dns_ip_2?: string
|
||||
}
|
||||
|
||||
export type NetworkItem = {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
encryption_type: number;
|
||||
};
|
||||
rssi: number
|
||||
ssid: string
|
||||
bssid: string
|
||||
channel: number
|
||||
encryption_type: number
|
||||
}
|
||||
|
||||
export type ApStatus = {
|
||||
status: number;
|
||||
ip_address: string;
|
||||
mac_address: string;
|
||||
station_num: number;
|
||||
};
|
||||
status: number
|
||||
ip_address: string
|
||||
mac_address: string
|
||||
station_num: number
|
||||
}
|
||||
|
||||
export type ApSettings = {
|
||||
provision_mode: number;
|
||||
ssid: string;
|
||||
password: string;
|
||||
channel: number;
|
||||
ssid_hidden: boolean;
|
||||
max_clients: number;
|
||||
local_ip: string;
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
};
|
||||
provision_mode: number
|
||||
ssid: string
|
||||
password: string
|
||||
channel: number
|
||||
ssid_hidden: boolean
|
||||
max_clients: number
|
||||
local_ip: string
|
||||
gateway_ip: string
|
||||
subnet_mask: string
|
||||
}
|
||||
|
||||
export type DownloadOTA = {
|
||||
status: string;
|
||||
progress: number;
|
||||
error: string;
|
||||
};
|
||||
status: string
|
||||
progress: number
|
||||
error: string
|
||||
}
|
||||
|
||||
export type Analytics = {
|
||||
max_alloc_heap: number;
|
||||
psram_size: number;
|
||||
free_psram: number;
|
||||
free_heap: number;
|
||||
total_heap: number;
|
||||
min_free_heap: number;
|
||||
core_temp: number;
|
||||
fs_total: number;
|
||||
fs_used: number;
|
||||
uptime: number;
|
||||
cpu0_usage: number;
|
||||
cpu1_usage: number;
|
||||
cpu_usage: number;
|
||||
};
|
||||
max_alloc_heap: number
|
||||
psram_size: number
|
||||
free_psram: number
|
||||
free_heap: number
|
||||
total_heap: number
|
||||
min_free_heap: number
|
||||
core_temp: number
|
||||
fs_total: number
|
||||
fs_used: number
|
||||
uptime: number
|
||||
cpu0_usage: number
|
||||
cpu1_usage: number
|
||||
cpu_usage: number
|
||||
}
|
||||
|
||||
export type Rssi = {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
};
|
||||
rssi: number
|
||||
ssid: string
|
||||
}
|
||||
|
||||
export type StaticSystemInformation = {
|
||||
esp_platform: string;
|
||||
firmware_version: string;
|
||||
cpu_freq_mhz: number;
|
||||
cpu_type: string;
|
||||
cpu_rev: number;
|
||||
cpu_cores: number;
|
||||
sketch_size: number;
|
||||
free_sketch_space: number;
|
||||
sdk_version: string;
|
||||
arduino_version: string;
|
||||
flash_chip_size: number;
|
||||
flash_chip_speed: number;
|
||||
cpu_reset_reason: string;
|
||||
};
|
||||
esp_platform: string
|
||||
firmware_version: string
|
||||
cpu_freq_mhz: number
|
||||
cpu_type: string
|
||||
cpu_rev: number
|
||||
cpu_cores: number
|
||||
sketch_size: number
|
||||
free_sketch_space: number
|
||||
sdk_version: string
|
||||
arduino_version: string
|
||||
flash_chip_size: number
|
||||
flash_chip_speed: number
|
||||
cpu_reset_reason: string
|
||||
}
|
||||
|
||||
export type SystemInformation = Analytics & StaticSystemInformation;
|
||||
export type SystemInformation = Analytics & StaticSystemInformation
|
||||
|
||||
export type IMU = {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
heading: number;
|
||||
altitude: number;
|
||||
bmp_temp: number;
|
||||
pressure: number;
|
||||
};
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
heading: number
|
||||
altitude: number
|
||||
bmp_temp: number
|
||||
pressure: number
|
||||
}
|
||||
|
||||
export interface I2CDevice {
|
||||
address: number;
|
||||
part_number: string;
|
||||
name: string;
|
||||
address: number
|
||||
part_number: 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 = {
|
||||
framesize: number;
|
||||
quality: number;
|
||||
brightness: number;
|
||||
contrast: number;
|
||||
saturation: number;
|
||||
sharpness: number;
|
||||
denoise: number;
|
||||
special_effect: number;
|
||||
wb_mode: number;
|
||||
vflip: boolean;
|
||||
hmirror: boolean;
|
||||
};
|
||||
framesize: number
|
||||
quality: number
|
||||
brightness: number
|
||||
contrast: number
|
||||
saturation: number
|
||||
sharpness: number
|
||||
denoise: number
|
||||
special_effect: number
|
||||
wb_mode: number
|
||||
vflip: boolean
|
||||
hmirror: boolean
|
||||
}
|
||||
|
||||
export type File = number;
|
||||
export type File = number
|
||||
|
||||
export interface Directory {
|
||||
[key: string]: File | Directory;
|
||||
[key: string]: File | Directory
|
||||
}
|
||||
|
||||
export type Servo = {
|
||||
name: string;
|
||||
channel: number;
|
||||
inverted: boolean;
|
||||
angle: number;
|
||||
center_angle: number;
|
||||
};
|
||||
name: string
|
||||
channel: number
|
||||
inverted: boolean
|
||||
angle: number
|
||||
center_angle: number
|
||||
}
|
||||
|
||||
export type ServoConfiguration = {
|
||||
is_active: boolean;
|
||||
servo_pwm_frequency: number;
|
||||
servo_oscillator_frequency: number;
|
||||
servos: Servo[];
|
||||
};
|
||||
is_active: boolean
|
||||
servo_pwm_frequency: number
|
||||
servo_oscillator_frequency: number
|
||||
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,4 +1,6 @@
|
||||
export const daisyColor = (name: string, opacity: number = 100) => {
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return `oklch(${color} / ${opacity}%)`;
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
if (opacity >= 100) return color;
|
||||
const alpha = Math.min(Math.max(opacity, 0), 100) / 100;
|
||||
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`;
|
||||
};
|
||||
|
||||
@@ -1,89 +1,93 @@
|
||||
import { Color, LoaderUtils, Vector3 } from 'three';
|
||||
import URDFLoader, { type URDFRobot } from 'urdf-loader';
|
||||
import { XacroLoader } from 'xacro-parser';
|
||||
import { Result } from '$lib/utilities';
|
||||
import { jointNames, model } from '$lib/stores';
|
||||
import uzip from 'uzip';
|
||||
import { fileService } from '$lib/services';
|
||||
import { Color, LoaderUtils, Vector3 } from 'three'
|
||||
import URDFLoader, { type URDFRobot } from 'urdf-loader'
|
||||
import { XacroLoader } from 'xacro-parser'
|
||||
import { Result } from '$lib/utilities'
|
||||
import { currentVariant, jointNames, model } from '$lib/stores'
|
||||
import uzip from 'uzip'
|
||||
import { fileService } from '$lib/services'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
let model_xml: XMLDocument;
|
||||
let model_xml: XMLDocument
|
||||
|
||||
export const populateModelCache = async () => {
|
||||
await cacheModelFiles();
|
||||
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
||||
if (modelRes.isOk()) {
|
||||
const [urdf, JOINT_NAME] = modelRes.inner;
|
||||
jointNames.set(JOINT_NAME);
|
||||
model.set(urdf);
|
||||
} else {
|
||||
console.error(modelRes.inner, { exception: modelRes.exception });
|
||||
}
|
||||
};
|
||||
await cacheModelFiles()
|
||||
const modelRes = await loadModel(get(currentVariant).model)
|
||||
if (modelRes.isOk()) {
|
||||
const [urdf, JOINT_NAME] = modelRes.inner
|
||||
jointNames.set(JOINT_NAME)
|
||||
model.set(urdf)
|
||||
} else {
|
||||
console.error(modelRes.inner, { exception: modelRes.exception })
|
||||
}
|
||||
}
|
||||
|
||||
export const cacheModelFiles = async () => {
|
||||
let data = await fetch('/stl.zip');
|
||||
const data = await fetch(get(currentVariant).stl)
|
||||
|
||||
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][]) {
|
||||
const url = new URL(path, window.location.href);
|
||||
fileService.saveFile(url.toString(), data);
|
||||
}
|
||||
};
|
||||
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
|
||||
const url = new URL(path, window.location.href)
|
||||
fileService?.saveFile(url.toString(), data)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadModelAsync = async (
|
||||
url: string
|
||||
): Promise<Result<[URDFRobot, string[]], string>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xacroLoader = new XacroLoader();
|
||||
const urdfLoader = new URDFLoader();
|
||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url);
|
||||
export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
|
||||
const urdfLoader = new URDFLoader()
|
||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
|
||||
|
||||
xacroLoader.load(
|
||||
url,
|
||||
async (xml) => {
|
||||
model_xml = xml;
|
||||
try {
|
||||
const model = urdfLoader.parse(xml);
|
||||
model.rotation.x = -Math.PI / 2;
|
||||
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)
|
||||
.filter((joint) => joint[1].jointType !== 'fixed')
|
||||
.map((joint) => joint[0]);
|
||||
let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
|
||||
|
||||
resolve(Result.ok([model, joints]));
|
||||
} catch (error) {
|
||||
resolve(Result.err('Failed to load model', error));
|
||||
}
|
||||
},
|
||||
(error) => resolve(Result.err('Failed to load model', error))
|
||||
);
|
||||
});
|
||||
};
|
||||
if (typeof xml === 'string') {
|
||||
xml = new window.DOMParser().parseFromString(xml, 'text/xml')
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
model_xml = xml
|
||||
try {
|
||||
const model = urdfLoader.parse(xml)
|
||||
setupRobot(model)
|
||||
const joints = Object.entries(model.joints)
|
||||
.filter(joint => joint[1].jointType !== 'fixed')
|
||||
.map(joint => joint[0])
|
||||
|
||||
export const footColor = () => {
|
||||
const colorElem = model_xml.querySelector('material[name=foot_color] > color') as Element;
|
||||
const colorAttrStr = colorElem.getAttribute('rgba') as string;
|
||||
const colorStr = colorAttrStr
|
||||
.split(' ')
|
||||
.slice(0, 3)
|
||||
.map((val) => Math.floor(+val * 255))
|
||||
.join(', ');
|
||||
resolve(Result.ok([model, joints]))
|
||||
} catch (error) {
|
||||
resolve(Result.err('Failed to load model', error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return new Color(`rgb(${colorStr})`);
|
||||
};
|
||||
const loadXacro = async (url: string): Promise<XMLDocument> =>
|
||||
new Promise((resolve, reject) => {
|
||||
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
|
||||
.split(' ')
|
||||
.slice(0, 3)
|
||||
.map(val => Math.floor(+val * 255))
|
||||
.join(', ')
|
||||
|
||||
return new Color(`rgb(${colorStr})`)
|
||||
}
|
||||
|
||||
@@ -1,36 +1,47 @@
|
||||
export const humanFileSize = (size: number): string => {
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB'];
|
||||
var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
|
||||
return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i];
|
||||
};
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB']
|
||||
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]
|
||||
}
|
||||
|
||||
export const capitalize = (str: string): string => {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
|
||||
};
|
||||
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase()
|
||||
}
|
||||
|
||||
export const convertSeconds = (seconds: number) => {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60)
|
||||
let hours = Math.floor(minutes / 60)
|
||||
const days = Math.floor(hours / 24)
|
||||
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24
|
||||
minutes = minutes % 60
|
||||
seconds = seconds % 60
|
||||
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
// Create the formatted string
|
||||
let result = ''
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' '
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' '
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' '
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '')
|
||||
|
||||
return result;
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
goto('/');
|
||||
}, 3000);
|
||||
});
|
||||
import { page } from '$app/state'
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
<h1 class="text-4xl">404 - Page not found</h1>
|
||||
<p>You will be redirected to the home page in 3 seconds</p>
|
||||
</div>
|
||||
<h1>{page.status} {page.error?.message}</h1>
|
||||
<span>Go to <a class="btn btn-primary" href="/">Home page</a></span>
|
||||
</div>
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location,
|
||||
useFeatureFlags
|
||||
useFeatureFlags,
|
||||
walkGait
|
||||
} from '$lib/stores'
|
||||
import type { Analytics, DownloadOTA } from '$lib/types/models'
|
||||
import { type Analytics, type DownloadOTA } from '$lib/types/models'
|
||||
import { MessageTopic } from '$lib/types/models'
|
||||
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet
|
||||
}
|
||||
@@ -36,10 +39,11 @@
|
||||
|
||||
addEventListeners()
|
||||
|
||||
outControllerData.subscribe(data => socket.sendEvent('input', { data }))
|
||||
mode.subscribe(data => socket.sendEvent('mode', { data }))
|
||||
servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data }))
|
||||
kinematicData.subscribe(data => socket.sendEvent('position', { data }))
|
||||
outControllerData.subscribe(data => socket.sendEvent(MessageTopic.input, data))
|
||||
mode.subscribe(data => socket.sendEvent(MessageTopic.mode, data))
|
||||
walkGait.subscribe(data => socket.sendEvent(MessageTopic.gait, data))
|
||||
servoAnglesOut.subscribe(data => socket.sendEvent(MessageTopic.angles, data))
|
||||
kinematicData.subscribe(data => socket.sendEvent(MessageTopic.position, data))
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -50,24 +54,24 @@
|
||||
socket.on('open', handleOpen)
|
||||
socket.on('close', handleClose)
|
||||
socket.on('error', handleError)
|
||||
socket.on('rssi', handleNetworkStatus)
|
||||
socket.on('mode', (data: ModesEnum) => mode.set(data))
|
||||
socket.on('analytics', handleAnalytics)
|
||||
socket.on('angles', (angles: number[]) => {
|
||||
socket.on(MessageTopic.rssi, handleNetworkStatus)
|
||||
socket.on(MessageTopic.mode, (data: ModesEnum) => mode.set(data))
|
||||
socket.on(MessageTopic.analytics, handleAnalytics)
|
||||
socket.on(MessageTopic.angles, (angles: number[]) => {
|
||||
if (angles.length) servoAngles.set(angles)
|
||||
})
|
||||
features.subscribe(data => {
|
||||
if (data?.download_firmware) socket.on('otastatus', handleOAT)
|
||||
if (data?.sonar) socket.on('sonar', data => console.log(data))
|
||||
if (data?.download_firmware) socket.on(MessageTopic.otastatus, handleOAT)
|
||||
if (data?.sonar) socket.on(MessageTopic.sonar, data => console.log(data))
|
||||
})
|
||||
}
|
||||
|
||||
const removeEventListeners = () => {
|
||||
socket.off('analytics', handleAnalytics)
|
||||
socket.off(MessageTopic.analytics, handleAnalytics)
|
||||
socket.off('open', handleOpen)
|
||||
socket.off('close', handleClose)
|
||||
socket.off('rssi', handleNetworkStatus)
|
||||
socket.off('otastatus', handleOAT)
|
||||
socket.off(MessageTopic.rssi, handleNetworkStatus)
|
||||
socket.off(MessageTopic.otastatus, handleOAT)
|
||||
}
|
||||
|
||||
const handleOpen = () => {
|
||||
@@ -94,7 +98,7 @@
|
||||
<title>{page.data.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="drawer h-screen">
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
|
||||
+18
-18
@@ -1,22 +1,22 @@
|
||||
export const prerender = false;
|
||||
export const ssr = false;
|
||||
export const prerender = true
|
||||
export const ssr = false
|
||||
|
||||
const registerFetchIntercept = async () => {
|
||||
const { fetch: originalFetch } = window;
|
||||
const fileService = (await import('$lib/services/file-service')).default;
|
||||
window.fetch = async (resource, config) => {
|
||||
let url = resource instanceof Request ? resource.url : resource.toString();
|
||||
let file = await fileService.getFile(url);
|
||||
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
|
||||
};
|
||||
};
|
||||
const { fetch: originalFetch } = window
|
||||
const fileService = (await import('$lib/services/file-service')).default
|
||||
window.fetch = async (resource, config) => {
|
||||
const url = resource instanceof Request ? resource.url : resource.toString()
|
||||
const file = await fileService?.getFile(url)
|
||||
return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config)
|
||||
}
|
||||
}
|
||||
|
||||
export const load = async () => {
|
||||
await registerFetchIntercept();
|
||||
return {
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2024 Rune Harlyk'
|
||||
};
|
||||
};
|
||||
await registerFetchIntercept()
|
||||
return {
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
copyright: '2025 Rune Harlyk'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,15 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="grow-3 w-80 relative">
|
||||
<Visualization sky={false} orbit panel={false} ground={false} zoom={8} />
|
||||
<div class="absolute bottom-0 w-full h-40 bg-gradient-to-t from-base-100 to-transparent">
|
||||
</div>
|
||||
<div class="hero bg-base-100 h-screen">
|
||||
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
|
||||
<div class="w-64 h-64">
|
||||
<Visualization sky={false} orbit panel={false} ground={false} />
|
||||
</div>
|
||||
<div class="grow-3 flex justify-center">
|
||||
<a class="btn btn-primary rounded-full" href={$socket ? '/controller' : '/connection'}>
|
||||
Add Robot Dog
|
||||
</a>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-center text-2xl">Begin you journey</h2>
|
||||
<p class="py-6 text-center"></p>
|
||||
<a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}> Add Robot Dog </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,26 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket, useFeatureFlags } from '$lib/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket } from '$lib/stores';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Connection</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Connection</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<script lang="ts">
|
||||
import Controls from './Controls.svelte';
|
||||
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte';
|
||||
import { selectedView, views } from '$lib/stores/application';
|
||||
import { onMount } from 'svelte';
|
||||
import { mpu, socket } from '$lib/stores';
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import type { IMU } from '$lib/types/models';
|
||||
import Controls from './Controls.svelte'
|
||||
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte'
|
||||
import { selectedView, views } from '$lib/stores/application'
|
||||
import { onMount } from 'svelte'
|
||||
import { mpu, socket } from '$lib/stores'
|
||||
import { imu } from '$lib/stores/imu'
|
||||
import { MessageTopic, type IMU } from '$lib/types/models'
|
||||
|
||||
let layout = $derived($views.find(v => v.name === $selectedView)!);
|
||||
let layout = $derived($views.find(v => v.name === $selectedView)!)
|
||||
|
||||
onMount(() => {
|
||||
socket.on('imu', (data: IMU) => {
|
||||
imu.addData(data);
|
||||
if (data.heading)
|
||||
mpu.update(mpuData => {
|
||||
mpuData.heading = data.heading;
|
||||
console.log(data.heading);
|
||||
onMount(() => {
|
||||
socket.on(MessageTopic.imu, (data: IMU) => {
|
||||
imu.addData(data)
|
||||
if (data.heading)
|
||||
mpu.update(mpuData => {
|
||||
mpuData.heading = data.heading
|
||||
console.log(data.heading)
|
||||
|
||||
return mpuData;
|
||||
});
|
||||
});
|
||||
});
|
||||
return mpuData
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 select-none w-screen h-screen">
|
||||
<Controls />
|
||||
<div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
|
||||
<WidgetContainer container={layout.content} />
|
||||
</div>
|
||||
<Controls />
|
||||
<div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
|
||||
<WidgetContainer container={layout.content} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,47 @@
|
||||
<script lang="ts">
|
||||
import nipplejs from 'nipplejs'
|
||||
import { onMount } from 'svelte'
|
||||
import { capitalize, throttler, toInt8 } from '$lib/utilities'
|
||||
import { input, outControllerData, mode, modes, type Modes, ModesEnum } from '$lib/stores'
|
||||
import { capitalize, throttler } from '$lib/utilities'
|
||||
import {
|
||||
input,
|
||||
outControllerData,
|
||||
mode,
|
||||
modes,
|
||||
type Modes,
|
||||
ModesEnum,
|
||||
walkGaits,
|
||||
WalkGaits,
|
||||
walkGait,
|
||||
walkGaitLabels
|
||||
} from '$lib/stores'
|
||||
import type { vector } from '$lib/types/models'
|
||||
import { VerticalSlider } from '$lib/components/input'
|
||||
import { gamepadAxes, hasGamepad } from '$lib/stores/gamepad'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
|
||||
let throttle = new throttler()
|
||||
let left: nipplejs.JoystickManager
|
||||
let right: nipplejs.JoystickManager
|
||||
|
||||
let throttle_timing = 40
|
||||
let data = new Array(8)
|
||||
let data = new Array(7)
|
||||
|
||||
$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(() => {
|
||||
left = nipplejs.create({
|
||||
@@ -45,14 +75,13 @@
|
||||
}
|
||||
|
||||
const updateData = () => {
|
||||
data[0] = 0
|
||||
data[1] = toInt8($input.left.x, -1, 1)
|
||||
data[2] = toInt8($input.left.y, -1, 1)
|
||||
data[3] = toInt8($input.right.x, -1, 1)
|
||||
data[4] = toInt8($input.right.y, -1, 1)
|
||||
data[5] = toInt8($input.height, 0, 100)
|
||||
data[6] = toInt8($input.speed, 0, 100)
|
||||
data[7] = toInt8($input.s1, 0, 100)
|
||||
data[0] = $input.left.x
|
||||
data[1] = $input.left.y
|
||||
data[2] = $input.right.x
|
||||
data[3] = $input.right.y
|
||||
data[4] = $input.height
|
||||
data[5] = $input.speed
|
||||
data[6] = $input.s1
|
||||
|
||||
outControllerData.set(data)
|
||||
}
|
||||
@@ -70,7 +99,7 @@
|
||||
}
|
||||
|
||||
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 => {
|
||||
inputData[key] = value
|
||||
@@ -82,6 +111,10 @@
|
||||
const changeMode = (modeValue: Modes) => {
|
||||
mode.set(modes.indexOf(modeValue))
|
||||
}
|
||||
|
||||
const changeWalkGait = (walkGaitValue: WalkGaits) => {
|
||||
walkGait.set(walkGaitValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 w-screen h-screen">
|
||||
@@ -103,7 +136,11 @@
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-10 flex items-end">
|
||||
<div class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl">
|
||||
<VerticalSlider min={0} max={100} oninput={(e: Event) => handleRange(e, 'height')} />
|
||||
<VerticalSlider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
oninput={(e: Event) => handleRange(e, 'height')} />
|
||||
<label for="height">Ht</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -119,7 +156,20 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl}
|
||||
{#if $mode === ModesEnum.Walk}
|
||||
<div class="join">
|
||||
{#each Object.values(WalkGaits) as gaitValue}
|
||||
{#if typeof gaitValue === 'number'}
|
||||
<button
|
||||
class="btn join-item btn-sm"
|
||||
class:btn-secondary={$walkGait === gaitValue}
|
||||
onclick={() => changeWalkGait(gaitValue)}>
|
||||
{walkGaitLabels[gaitValue]}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<label for="s1">S1</label>
|
||||
@@ -127,7 +177,8 @@
|
||||
type="range"
|
||||
name="s1"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.01"
|
||||
max="1"
|
||||
oninput={e => handleRange(e, 's1')}
|
||||
class="range range-sm range-primary" />
|
||||
</div>
|
||||
@@ -137,7 +188,8 @@
|
||||
type="range"
|
||||
name="speed"
|
||||
min="0"
|
||||
max="100"
|
||||
step="0.01"
|
||||
max="1"
|
||||
oninput={e => handleRange(e, 'speed')}
|
||||
class="range range-sm range-primary" />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import I2C from './i2c.svelte';
|
||||
import I2C from './i2c.svelte'
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<I2C />
|
||||
<I2C />
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import type { PageLoad } from './$types'
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'I2C'
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
return {
|
||||
title: 'I2C'
|
||||
}
|
||||
}) satisfies PageLoad
|
||||
|
||||
@@ -1,57 +1,79 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { socket } from '$lib/stores';
|
||||
import type { I2CDevice } from '$lib/types/models';
|
||||
import { Connection } from '$lib/components/icons';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import { MessageTopic, type I2CDevice } from '$lib/types/models'
|
||||
import { Connection } from '$lib/components/icons'
|
||||
import I2CSetting from './i2cSetting.svelte'
|
||||
|
||||
const i2cDevices = [
|
||||
{ address: 30, part_number: 'HMC5883', name: '3-Axis Digital Compass/Magnetometer IC' },
|
||||
{ address: 64, part_number: 'PCA9685', name: '16-channel PWM driver default address' },
|
||||
{ address: 72, part_number: 'ADS1115', name: '4-channel 16-bit ADC' },
|
||||
{
|
||||
address: 104,
|
||||
part_number: 'MPU6050',
|
||||
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
|
||||
},
|
||||
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
|
||||
];
|
||||
const i2cDevices = [
|
||||
{ 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: 72, part_number: 'ADS1115', name: '4-channel 16-bit ADC' },
|
||||
{
|
||||
address: 104,
|
||||
part_number: 'MPU6050',
|
||||
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
|
||||
},
|
||||
{ address: 115, part_number: 'PAJ7620U2', name: 'Gesture sensor' },
|
||||
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
|
||||
]
|
||||
|
||||
let active_devices: I2CDevice[] = $state([]);
|
||||
let active_devices: I2CDevice[] = $state([])
|
||||
|
||||
onMount(() => {
|
||||
socket.on('i2cScan', handleScan);
|
||||
socket.sendEvent('i2cScan', '');
|
||||
return () => socket.off('i2cScan', handleScan);
|
||||
});
|
||||
let isLoading = $state(false)
|
||||
|
||||
const handleScan = (data: any) => {
|
||||
active_devices = data.addresses.map(
|
||||
(address: number) =>
|
||||
i2cDevices.find(device => device.address === address) || {
|
||||
address,
|
||||
part_number: 'Unknown',
|
||||
name: 'Unknown'
|
||||
}
|
||||
);
|
||||
};
|
||||
onMount(() => {
|
||||
socket.on(MessageTopic.i2cScan, handleScan)
|
||||
triggerScan()
|
||||
return () => socket.off(MessageTopic.i2cScan, handleScan)
|
||||
})
|
||||
|
||||
const handleScan = (data: any) => {
|
||||
active_devices = data.addresses.map(
|
||||
(address: number) =>
|
||||
i2cDevices.find(device => device.address === address) || {
|
||||
address,
|
||||
part_number: 'Unknown',
|
||||
name: 'Unknown'
|
||||
}
|
||||
)
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
const triggerScan = () => {
|
||||
isLoading = true
|
||||
socket.sendEvent(MessageTopic.i2cScan, '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Connection class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >I<sup>2</sup>C</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<Connection class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>I<sup>2</sup>C</span>
|
||||
{/snippet}
|
||||
{#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="grid">
|
||||
{#if active_devices.length === 0}
|
||||
<div>No I2C devices found</div>
|
||||
{:else}
|
||||
{#each active_devices as device}
|
||||
<div>[{device.address.toString(16)}] {device.part_number} - {device.name}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
<I2CSetting />
|
||||
|
||||
<div class="grid">
|
||||
{#if active_devices.length === 0}
|
||||
<div>No I2C devices found</div>
|
||||
{:else}
|
||||
{#each active_devices as device}
|
||||
<div>[{device.address.toString(16)}] {device.part_number} - {device.name}</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { Cancel, Edit, EditOff, Power } from '$lib/components/icons'
|
||||
import { socket } from '$lib/stores'
|
||||
import { MessageTopic, 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(MessageTopic.peripheralSettings, handleSettings)
|
||||
socket.sendEvent(MessageTopic.peripheralSettings, '')
|
||||
return () => socket.off(MessageTopic.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(MessageTopic.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,310 +1,253 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { daisyColor } from "$lib/utilities";
|
||||
import { socket } from "$lib/stores";
|
||||
import type { IMU } from "$lib/types/models";
|
||||
import { useFeatureFlags } from "$lib/stores/featureFlags";
|
||||
import { Rotate3d } from "$lib/components/icons";
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { imu } from '$lib/stores/imu'
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import { MessageTopic, type IMU } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Rotate3d } from '$lib/components/icons'
|
||||
|
||||
const features = useFeatureFlags();
|
||||
Chart.register(...registerables)
|
||||
|
||||
Chart.register(...registerables);
|
||||
const features = useFeatureFlags()
|
||||
let intervalId: ReturnType<typeof setInterval> | number
|
||||
|
||||
let angleChartElement: HTMLCanvasElement = $state();
|
||||
let angleChart: Chart;
|
||||
let angleChartElement: HTMLCanvasElement
|
||||
let tempChartElement: HTMLCanvasElement
|
||||
let altitudeChartElement: HTMLCanvasElement
|
||||
|
||||
let tempChartElement: HTMLCanvasElement = $state();
|
||||
let tempChart: Chart;
|
||||
let angleChart: Chart
|
||||
let tempChart: Chart
|
||||
let altitudeChart: Chart
|
||||
|
||||
let altitudeChartElement: HTMLCanvasElement = $state();
|
||||
let altitudeChart: Chart;
|
||||
|
||||
const handleImu = (data: IMU) => {
|
||||
console.log(data);
|
||||
|
||||
imu.addData(data);
|
||||
const getChartColors = () => {
|
||||
const style = getComputedStyle(document.body)
|
||||
return {
|
||||
primary: style.getPropertyValue('--color-primary'),
|
||||
secondary: style.getPropertyValue('--color-secondary'),
|
||||
accent: style.getPropertyValue('--color-accent'),
|
||||
background: style.getPropertyValue('--color-background')
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
socket.on('imu', handleImu);
|
||||
angleChart = new Chart(angleChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'x',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $imu.x,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'y',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $imu.y,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'z',
|
||||
borderColor: daisyColor('--a'),
|
||||
backgroundColor: daisyColor('--a', 50),
|
||||
borderWidth: 2,
|
||||
data: $imu.z,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Angle [°]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
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, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Barometer temperature',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $imu.bmp_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Temperature [C°]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
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, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Altitude',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $imu.altitude,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 1
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Altitude [M]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
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;
|
||||
});
|
||||
const createBaseChartConfig = (bgColor: string) => ({
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
tooltip: { mode: 'index' as const, intersect: false }
|
||||
},
|
||||
elements: { point: { radius: 1 } },
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: bgColor },
|
||||
ticks: { color: bgColor },
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear' as const,
|
||||
position: 'left' as const,
|
||||
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, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'x',
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primary,
|
||||
borderWidth: 2,
|
||||
data: $imu.x,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'y',
|
||||
borderColor: colors.secondary,
|
||||
backgroundColor: colors.secondary,
|
||||
borderWidth: 2,
|
||||
data: $imu.y,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'z',
|
||||
borderColor: colors.accent,
|
||||
backgroundColor: colors.accent,
|
||||
borderWidth: 2,
|
||||
data: $imu.z,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
scales: {
|
||||
...baseConfig.scales,
|
||||
y: {
|
||||
...baseConfig.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Angle [°]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off('imu', handleImu);
|
||||
tempChart = new Chart(tempChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Barometer temperature',
|
||||
borderColor: colors.secondary,
|
||||
backgroundColor: colors.secondary,
|
||||
borderWidth: 2,
|
||||
data: $imu.bmp_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
scales: {
|
||||
...baseConfig.scales,
|
||||
y: {
|
||||
...baseConfig.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Temperature [C°]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const updateData = () => {
|
||||
if ($features.imu) {
|
||||
angleChart.data.labels = $imu.x;
|
||||
angleChart.data.datasets[0].data = $imu.x;
|
||||
angleChart.data.datasets[1].data = $imu.y;
|
||||
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;
|
||||
angleChart.update('none');
|
||||
altitudeChart = new Chart(altitudeChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Altitude',
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primary,
|
||||
borderWidth: 2,
|
||||
data: $imu.altitude,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
scales: {
|
||||
...baseConfig.scales,
|
||||
y: {
|
||||
...baseConfig.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Altitude [M]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if ($features.bmp) {
|
||||
tempChart.data.labels = $imu.bmp_temp;
|
||||
tempChart.data.datasets[0].data = $imu.bmp_temp;
|
||||
tempChart.options.scales!.y!.min = Math.min(...$imu.bmp_temp) - 1;
|
||||
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');
|
||||
}
|
||||
const updateChartData = (chart: Chart, data: number[], label: string) => {
|
||||
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 = () => {
|
||||
if ($features.imu) {
|
||||
angleChart.data.labels = $imu.x
|
||||
angleChart.data.datasets[0].data = $imu.x
|
||||
angleChart.data.datasets[1].data = $imu.y
|
||||
angleChart.data.datasets[2].data = $imu.z
|
||||
|
||||
const allValues = [...$imu.x, ...$imu.y, ...$imu.z]
|
||||
angleChart.options.scales!.y!.min = Math.min(...allValues) - 1
|
||||
angleChart.options.scales!.y!.max = Math.max(...allValues) + 1
|
||||
angleChart.update('none')
|
||||
}
|
||||
|
||||
if ($features.bmp) {
|
||||
updateChartData(tempChart, $imu.bmp_temp, 'Temperature')
|
||||
updateChartData(altitudeChart, $imu.altitude, 'Altitude')
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
socket.on(MessageTopic.imu, (data: IMU) => {
|
||||
console.log(data)
|
||||
imu.addData(data)
|
||||
})
|
||||
|
||||
initializeCharts()
|
||||
intervalId = setInterval(updateData, 200)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off(MessageTopic.imu)
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Rotate3d class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >IMU</span>
|
||||
{/snippet}
|
||||
{#if $features.imu}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={angleChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $features.bmp}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={tempChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={altitudeChartElement}></canvas>
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<Rotate3d class="flex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>IMU</span>
|
||||
{/snippet}
|
||||
|
||||
{#if $features.imu}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={angleChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- <IMUSetting /> -->
|
||||
</SettingsCard>
|
||||
{/if}
|
||||
|
||||
{#if $features.bmp}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={tempChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={altitudeChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Servos from './servos.svelte';
|
||||
import ServoTable from './ServoTable.svelte';
|
||||
import Servos from './servos.svelte'
|
||||
import ServoTable from './ServoTable.svelte'
|
||||
|
||||
let servoId = $state(0)
|
||||
let pwm = $state(306)
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<Servos />
|
||||
<ServoTable />
|
||||
<Servos bind:servoId bind:pwm />
|
||||
<ServoTable {servoId} {pwm} />
|
||||
</div>
|
||||
|
||||
@@ -1,73 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api';
|
||||
import { onMount } from 'svelte';
|
||||
interface Props {
|
||||
data?: any;
|
||||
import { api } from '$lib/api'
|
||||
import { onMount } from 'svelte'
|
||||
import { RotateCw, RotateCcw } from '$lib/components/icons'
|
||||
interface Props {
|
||||
data?: any
|
||||
servoId?: number
|
||||
pwm?: number
|
||||
}
|
||||
|
||||
let {
|
||||
data = $bindable({
|
||||
servos: []
|
||||
}),
|
||||
pwm = $bindable(306),
|
||||
servoId = $bindable(0)
|
||||
}: Props = $props()
|
||||
|
||||
const updateValue = (event: Event, index: number, key: string) => {
|
||||
data.servos[index][key] = Number((event.target as HTMLInputElement).value)
|
||||
}
|
||||
|
||||
const syncConfig = async () => {
|
||||
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 () => {
|
||||
const result = await api.get('/api/servo/config')
|
||||
if (result.isOk()) {
|
||||
data = result.inner
|
||||
}
|
||||
})
|
||||
|
||||
let { data = $bindable({
|
||||
servos: []
|
||||
}) }: Props = $props();
|
||||
|
||||
const updateValue = (event, index, key) => {
|
||||
data.servos[index][key] = event.target.innerText;
|
||||
};
|
||||
|
||||
const syncConfig = async () => {
|
||||
await api.post('/api/servo/config', data);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
const result = await api.get('/api/servo/config');
|
||||
if (result.isOk()) {
|
||||
data = result.inner;
|
||||
}
|
||||
});
|
||||
const setCenterPWM = async () => {
|
||||
console.log('setCenterPWM', servoId, pwm)
|
||||
data.servos[servoId]['center_pwm'] = pwm
|
||||
await syncConfig()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Center PWM</th>
|
||||
<th>Center Angle</th>
|
||||
<th>Direction</th>
|
||||
<th>Conversion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.servos as servo, index}
|
||||
<tr>
|
||||
<td
|
||||
contenteditable="true"
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'center_pwm')}
|
||||
>
|
||||
{servo.center_pwm}
|
||||
</td>
|
||||
<td
|
||||
contenteditable="true"
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'center_angle')}
|
||||
>
|
||||
{servo.center_angle}
|
||||
</td>
|
||||
<td
|
||||
contenteditable="true"
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'direction')}
|
||||
>
|
||||
{servo.direction}
|
||||
</td>
|
||||
<td
|
||||
contenteditable="true"
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'conversion')}
|
||||
>
|
||||
{servo.conversion}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-primary" onclick={() => setCenterPWM()}>Set center pwm</button>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto">
|
||||
<table class="table table-xs">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Servo</th>
|
||||
<th>Center PWM</th>
|
||||
<th>Center Angle</th>
|
||||
<th>Direction</th>
|
||||
<th>Conversion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.servos as servo, index}
|
||||
<tr class="hover:bg-base-200">
|
||||
<td class="font-medium">Servo {index}</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
class="input input-sm input-bordered w-20"
|
||||
value={servo.center_pwm}
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'center_pwm')}
|
||||
min="80"
|
||||
max="600" />
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
step="0.1"
|
||||
class="input input-sm input-bordered w-20"
|
||||
value={servo.center_angle}
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'center_angle')}
|
||||
min="-90"
|
||||
max="90" />
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
title="Toggle direction {servo.direction}"
|
||||
onclick={() => toggleDirection(index)}>
|
||||
{#if servo.direction === 1}
|
||||
<RotateCw class="w-4 h-4 text-green-500" />
|
||||
{:else}
|
||||
<RotateCcw class="w-4 h-4" />
|
||||
{/if}
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="number"
|
||||
step="0.01"
|
||||
class="input input-sm input-bordered w-20"
|
||||
value={servo.conversion}
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'conversion')}
|
||||
min="0"
|
||||
max="10" />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -1,75 +1,64 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import type { ServoConfiguration, Servo } from '$lib/types/models';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { socket } from '$lib/stores'
|
||||
import { MessageTopic } from '$lib/types/models'
|
||||
import { throttler as Throttler } from '$lib/utilities'
|
||||
|
||||
import { socket } from '$lib/stores';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { throttler as Throttler } from '$lib/utilities';
|
||||
import { MotorOutline } from '$lib/components/icons';
|
||||
let { servoId = $bindable(0), pwm = $bindable(306) } = $props()
|
||||
|
||||
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(MessageTopic.servoState, { active: 1 })
|
||||
}
|
||||
|
||||
const sweep = (event: any) => {
|
||||
let channel = event.detail.channel;
|
||||
socket.sendEvent('servoConfiguration', { servos: [{ channel, sweep: true }] });
|
||||
};
|
||||
const deactivateServo = () => {
|
||||
socket.sendEvent(MessageTopic.servoState, { active: 0 })
|
||||
}
|
||||
|
||||
const activateServo = (event: any) => {
|
||||
socket.sendEvent('servoState', { active: 1 });
|
||||
};
|
||||
const updatePWM = () => {
|
||||
throttler.throttle(() => {
|
||||
socket.sendEvent(MessageTopic.servoPWM, { servo_id: servoId, pwm })
|
||||
}, 10)
|
||||
}
|
||||
|
||||
const deactivateServo = (event: any) => {
|
||||
socket.sendEvent('servoState', { active: 0 });
|
||||
};
|
||||
|
||||
let pwm = $state(306);
|
||||
|
||||
const updatePWM = () => {
|
||||
throttler.throttle(() => {
|
||||
socket.sendEvent('servoPWM', { servo_id: servoId, pwm });
|
||||
}, 10);
|
||||
};
|
||||
const toggleMode = () => {
|
||||
servoId = allServos ? -1 : 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<MotorOutline class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Servo</span>
|
||||
{/snippet}
|
||||
{pwm}
|
||||
<input
|
||||
type="range"
|
||||
min="80"
|
||||
max="600"
|
||||
bind:value={pwm}
|
||||
oninput={updatePWM}
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg">General servo configuration</h2>
|
||||
<span>Servo</span>
|
||||
<span>{pwm}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="80"
|
||||
max="600"
|
||||
bind:value={pwm}
|
||||
oninput={updatePWM}
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" />
|
||||
|
||||
{#if isLoading}
|
||||
<Spinner />
|
||||
{:else}
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg">General servo configuration</h2>
|
||||
<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} />
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
bind:checked={active}
|
||||
onchange={active ? activateServo : deactivateServo}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</SettingsCard>
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg">General servo configuration</h2>
|
||||
<span>
|
||||
<label for="mode">All servoes</label>
|
||||
<input type="checkbox" class="toggle" bind:checked={allServos} onchange={toggleMode} />
|
||||
</span>
|
||||
<span>
|
||||
<label for="active">Active</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
bind:checked={active}
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
<script>
|
||||
import { FileIcon } from '$lib/components/icons'
|
||||
<script lang="ts">
|
||||
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>
|
||||
|
||||
<!-- svelte-ignore a11y_interactive_supports_focus -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<span role="button" class="flex pl-4 gap-2 items-center" onclick={selected}>
|
||||
<FileIcon />{name}
|
||||
</span>
|
||||
<div class="flex items-center pl-4 group hover:bg-gray-700 rounded py-1">
|
||||
<button class="flex items-center gap-2 flex-grow" onclick={() => selected(name)}>
|
||||
<FileIcon class="w-4 h-4" />
|
||||
<span class="text-sm">{name}</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">
|
||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
||||
import Spinner from "$lib/components/Spinner.svelte";
|
||||
import Folder from "./Folder.svelte";
|
||||
import { api } from "$lib/api";
|
||||
import type { Directory } from "$lib/types/models";
|
||||
import { FolderIcon } from "$lib/components/icons";
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import Folder from './Folder.svelte'
|
||||
import { api } from '$lib/api'
|
||||
import type { Directory } from '$lib/types/models'
|
||||
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 result = await api.get<Directory>('/api/files')
|
||||
if (result.isOk()) {
|
||||
return result.inner;
|
||||
}
|
||||
return { root: {} }
|
||||
};
|
||||
|
||||
const getContent = async (name: string) => {
|
||||
if (!name) return '';
|
||||
const result = await api.get(`/api/config/${name}`)
|
||||
if (result.isOk()) {
|
||||
return JSON.stringify(result.inner, null, 4);
|
||||
}
|
||||
return ''
|
||||
const getFiles = async () => {
|
||||
const result = await api.get<Directory>('/api/files')
|
||||
if (result.isOk()) {
|
||||
return result.inner
|
||||
}
|
||||
return { root: {} }
|
||||
}
|
||||
|
||||
const deleteFile = async (name: string) => {
|
||||
const result = await api.post(`/api/files/delete`, { file: "/config/"+ name })
|
||||
if (result.isOk()) {
|
||||
return result.inner;
|
||||
}
|
||||
return ''
|
||||
const getContent = async (name: string) => {
|
||||
if (!name) return ''
|
||||
const result = await api.get(`/api/config/${name}`)
|
||||
if (result.isOk()) {
|
||||
content = JSON.stringify(result.inner, null, 4)
|
||||
return content
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const updateSelected = async (event:any) => {
|
||||
filename = event.detail.name;
|
||||
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) => {
|
||||
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()) {
|
||||
filename = ''
|
||||
content = ''
|
||||
}
|
||||
}
|
||||
|
||||
const createFolder = async (folderName: string) => {
|
||||
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>
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<FolderIcon class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >File System</span>
|
||||
{/snippet}
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getFiles()}
|
||||
<Spinner />
|
||||
{:then files}
|
||||
<Folder name="/" files={files.root} expanded on:selected={updateSelected}/>
|
||||
{/await}
|
||||
|
||||
{#await getContent(filename)}
|
||||
<div>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:then content}
|
||||
<pre>{content}</pre>
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
<!-- <SettingsCard collapsible={false}> -->
|
||||
<!-- {#snippet icon()} -->
|
||||
<FolderIcon class="flex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<!-- {/snippet}
|
||||
{#snippet title()} -->
|
||||
<div class="flex justify-between items-center w-full gap-2">
|
||||
<span>File System</span>
|
||||
<div class="flex gap-2">
|
||||
<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()}
|
||||
<Spinner />
|
||||
{:then files}
|
||||
<Folder
|
||||
name="/"
|
||||
files={files.root}
|
||||
expanded
|
||||
selected={updateSelected}
|
||||
onDelete={deleteFile} />
|
||||
{/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)}
|
||||
<Spinner />
|
||||
{:then _}
|
||||
{#if isEditing}
|
||||
<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}
|
||||
{:else}
|
||||
<div class="text-center text-gray-500">Select a file to view its contents</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- </SettingsCard> -->
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
<script lang="ts">
|
||||
import Folder from './Folder.svelte';
|
||||
import File from './File.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { FolderIcon, FolderOpenOutline } from '$lib/components/icons';
|
||||
import Folder from './Folder.svelte'
|
||||
import File from './File.svelte'
|
||||
import { FolderIcon, FolderOpenOutline } from '$lib/components/icons'
|
||||
|
||||
interface Props {
|
||||
expanded?: boolean;
|
||||
name: any;
|
||||
files: any;
|
||||
}
|
||||
interface Props {
|
||||
expanded?: boolean
|
||||
name: string
|
||||
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() {
|
||||
expanded = !expanded;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const updateSelected = async (event:any) => {
|
||||
dispatch('selected', { name:event.detail.name });
|
||||
}
|
||||
function toggle() {
|
||||
expanded = !expanded
|
||||
}
|
||||
</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}
|
||||
<FolderOpenOutline class="w-6 h-6" />
|
||||
<FolderOpenOutline class="w-5 h-5 mr-1" />
|
||||
{:else}
|
||||
<FolderIcon class="w-6 h-6" />
|
||||
<FolderIcon class="w-5 h-5 mr-1" />
|
||||
{/if}
|
||||
{name}
|
||||
</button>
|
||||
<span class="text-sm">{name}</span>
|
||||
</button>
|
||||
|
||||
{#if expanded}
|
||||
<ul class="ml-5 border-l border-slate-600">
|
||||
{#each Object.entries(files) as [name, content]}
|
||||
<li class="p-1">
|
||||
{#if typeof content == 'object'}
|
||||
<Folder {name} files={content} on:selected={updateSelected} />
|
||||
{:else}
|
||||
<File {name} on:selected={updateSelected}/>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if expanded}
|
||||
<ul class="ml-4 border-l border-gray-600 mt-1">
|
||||
{#each Object.entries(files) as [itemName, content]}
|
||||
<li class="py-1">
|
||||
{#if typeof content === 'object'}
|
||||
<Folder name={itemName} files={content} {selected} {onDelete} />
|
||||
{:else}
|
||||
<File name={itemName} {selected} {onDelete} />
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/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 +1,7 @@
|
||||
<script lang="ts">
|
||||
import SystemMetrics from './SystemMetrics.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
if (!$features.analytics) {
|
||||
goto('/');
|
||||
}
|
||||
import SystemMetrics from './SystemMetrics.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<SystemMetrics />
|
||||
<SystemMetrics />
|
||||
</div>
|
||||
|
||||
@@ -1,373 +1,369 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import { Metrics } from '$lib/components/icons';
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
|
||||
Chart.register(...registerables);
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import { Metrics } from '$lib/components/icons';
|
||||
|
||||
let cpuChartElement: HTMLCanvasElement = $state();
|
||||
let cpuChart: Chart;
|
||||
Chart.register(...registerables);
|
||||
|
||||
let heapChartElement: HTMLCanvasElement = $state();
|
||||
let heapChart: Chart;
|
||||
let cpuChartElement: HTMLCanvasElement;
|
||||
let cpuChart: Chart;
|
||||
|
||||
let filesystemChartElement: HTMLCanvasElement = $state();
|
||||
let filesystemChart: Chart;
|
||||
let heapChartElement: HTMLCanvasElement;
|
||||
let heapChart: Chart;
|
||||
|
||||
let temperatureChartElement: HTMLCanvasElement = $state();
|
||||
let temperatureChart: Chart;
|
||||
let filesystemChartElement: HTMLCanvasElement;
|
||||
let filesystemChart: Chart;
|
||||
|
||||
onMount(() => {
|
||||
cpuChart = new Chart(cpuChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.cpu_usage,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cpu usage core 0',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu0_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage core 1',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu1_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage total',
|
||||
borderColor: daisyColor('--s'),
|
||||
backgroundColor: daisyColor('--s', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu_usage,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cpu usage [%]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Used Heap',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.used_heap,
|
||||
fill:true,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
fill:true,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
setInterval(updateData, 500);
|
||||
});
|
||||
let temperatureChartElement: HTMLCanvasElement;
|
||||
let temperatureChart: Chart;
|
||||
|
||||
function updateData() {
|
||||
cpuChart.data.labels = $analytics.cpu_usage;
|
||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||
cpuChart.update('none');
|
||||
onMount(() => {
|
||||
cpuChart = new Chart(cpuChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.cpu_usage,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Cpu usage core 0',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu0_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage core 1',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu1_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
{
|
||||
label: 'Cpu usage total',
|
||||
borderColor: daisyColor('--color-secondary'),
|
||||
backgroundColor: daisyColor('--color-secondary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.cpu_usage,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Cpu usage [%]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
heapChart = new Chart(heapChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Used Heap',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.used_heap,
|
||||
fill: true,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heap [kb]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.total_heap[0]),
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
filesystemChart = new Chart(filesystemChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'File System Used',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.fs_used,
|
||||
fill: true,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'File System [kb]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: Math.round($analytics.fs_total[0]),
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
temperatureChart = new Chart(temperatureChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: $analytics.uptime,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Core Temperature',
|
||||
borderColor: daisyColor('--color-primary'),
|
||||
backgroundColor: daisyColor('--color-primary', 50),
|
||||
borderWidth: 2,
|
||||
data: $analytics.core_temp,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--color-base-content', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Core Temperature [°C]',
|
||||
color: daisyColor('--color-base-content'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
suggestedMin: 20,
|
||||
suggestedMax: 100,
|
||||
grid: { color: daisyColor('--color-base-content', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--color-base-content'),
|
||||
},
|
||||
border: { color: daisyColor('--color-base-content', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setInterval(updateData, 500);
|
||||
});
|
||||
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||
heapChart.update('none');
|
||||
function updateData() {
|
||||
cpuChart.data.labels = $analytics.cpu_usage;
|
||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||
cpuChart.update('none');
|
||||
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||
filesystemChart.update('none');
|
||||
heapChart.data.labels = $analytics.uptime;
|
||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||
heapChart.update('none');
|
||||
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
filesystemChart.data.labels = $analytics.uptime;
|
||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||
filesystemChart.update('none');
|
||||
|
||||
temperatureChart.data.labels = $analytics.uptime;
|
||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||
temperatureChart.update('none');
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >System Metrics</span>
|
||||
{/snippet}
|
||||
{#snippet icon()}
|
||||
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>System Metrics</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={cpuChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={cpuChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={heapChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={filesystemChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={temperatureChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={heapChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={filesystemChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-52"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={temperatureChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -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 { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
|
||||
import type { SystemInformation, Analytics } from '$lib/types/models'
|
||||
import { type SystemInformation, type Analytics, MessageTopic } from '$lib/types/models'
|
||||
import { socket } from '$lib/stores/socket'
|
||||
import { api } from '$lib/api'
|
||||
import { convertSeconds } from '$lib/utilities'
|
||||
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import {
|
||||
Cancel,
|
||||
@@ -31,12 +29,12 @@
|
||||
Temperature,
|
||||
Stopwatch
|
||||
} from '$lib/components/icons'
|
||||
import StatusItem from './StatusItem.svelte'
|
||||
import StatusItem from '$lib/components/StatusItem.svelte'
|
||||
import ActionButton from './ActionButton.svelte'
|
||||
|
||||
const features = useFeatureFlags()
|
||||
|
||||
let systemInformation: SystemInformation = $state()
|
||||
let systemInformation: SystemInformation | null = $state(null)
|
||||
|
||||
async function getSystemStatus() {
|
||||
const result = await api.get<SystemInformation>('/api/system/status')
|
||||
@@ -52,12 +50,17 @@
|
||||
|
||||
const postSleep = async () => await api.post('api/sleep')
|
||||
|
||||
onMount(() => socket.on('analytics', handleSystemData))
|
||||
onMount(() => socket.on(MessageTopic.analytics, handleSystemData))
|
||||
|
||||
onDestroy(() => socket.off('analytics', handleSystemData))
|
||||
|
||||
const handleSystemData = (data: Analytics) =>
|
||||
(systemInformation = { ...systemInformation, ...data })
|
||||
onDestroy(() => socket.off(MessageTopic.analytics, handleSystemData))
|
||||
const handleSystemData = (data: Analytics) => {
|
||||
if (systemInformation) {
|
||||
systemInformation = {
|
||||
...systemInformation,
|
||||
...(data as unknown as SystemInformation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const postRestart = async () => await api.post('/api/system/restart')
|
||||
|
||||
@@ -146,89 +149,92 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getSystemStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={CPU}
|
||||
title="Chip"
|
||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||
{:then}
|
||||
{#if systemInformation}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={CPU}
|
||||
title="Chip"
|
||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={SDK}
|
||||
title="SDK Version"
|
||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||
<StatusItem
|
||||
icon={SDK}
|
||||
title="SDK Version"
|
||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={CPP}
|
||||
title="Firmware Version"
|
||||
description={systemInformation.firmware_version} />
|
||||
<StatusItem
|
||||
icon={CPP}
|
||||
title="Firmware Version"
|
||||
description={systemInformation.firmware_version} />
|
||||
|
||||
<StatusItem
|
||||
icon={Speed}
|
||||
title="CPU Frequency"
|
||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||
}`} />
|
||||
<StatusItem
|
||||
icon={Speed}
|
||||
title="CPU Frequency"
|
||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||
}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Heap}
|
||||
title="Heap (Free / Max Alloc)"
|
||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||
<StatusItem
|
||||
icon={Heap}
|
||||
title="Heap (Free / Max Alloc)"
|
||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Pyramid}
|
||||
title="PSRAM (Size / Free)"
|
||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||
<StatusItem
|
||||
icon={Pyramid}
|
||||
title="PSRAM (Size / Free)"
|
||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Sketch}
|
||||
title="Sketch (Used / Free)"
|
||||
description={`${(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
<StatusItem
|
||||
icon={Sketch}
|
||||
title="Sketch (Used / Free)"
|
||||
description={`${(
|
||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||
100
|
||||
).toFixed(1)} % of
|
||||
${systemInformation.free_sketch_space / 1000000} MB used (${
|
||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000
|
||||
} MB free)`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Flash}
|
||||
title="Flash Chip (Size / Speed)"
|
||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
} MHz`} />
|
||||
<StatusItem
|
||||
icon={Flash}
|
||||
title="Flash Chip (Size / Speed)"
|
||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||
systemInformation.flash_chip_speed / 1000000
|
||||
} MHz`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Folder}
|
||||
title="File System (Used / Total)"
|
||||
description={`${((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(
|
||||
1
|
||||
)} % of ${systemInformation.fs_total / 1000000} MB used (${
|
||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||
}
|
||||
<StatusItem
|
||||
icon={Folder}
|
||||
title="File System (Used / Total)"
|
||||
description={`${(
|
||||
(systemInformation.fs_used / systemInformation.fs_total) *
|
||||
100
|
||||
).toFixed(1)} % of ${systemInformation.fs_total / 1000000} MB used (${
|
||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||
}
|
||||
MB free)`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Temperature}
|
||||
title="Core Temperature"
|
||||
description={`${
|
||||
systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||
}`} />
|
||||
<StatusItem
|
||||
icon={Temperature}
|
||||
title="Core Temperature"
|
||||
description={`${
|
||||
systemInformation.core_temp == 53.33 ?
|
||||
'NaN'
|
||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||
}`} />
|
||||
|
||||
<StatusItem
|
||||
icon={Stopwatch}
|
||||
title="Uptime"
|
||||
description={convertSeconds(systemInformation.uptime)} />
|
||||
<StatusItem
|
||||
icon={Stopwatch}
|
||||
title="Uptime"
|
||||
description={convertSeconds(systemInformation.uptime)} />
|
||||
|
||||
<StatusItem
|
||||
icon={Power}
|
||||
title="Reset Reason"
|
||||
description={systemInformation.cpu_reset_reason} />
|
||||
</div>
|
||||
<StatusItem
|
||||
icon={Power}
|
||||
title="Reset Reason"
|
||||
description={systemInformation.cpu_reset_reason} />
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
@@ -236,7 +242,7 @@
|
||||
{#each actionButtons as button}
|
||||
{#if button.condition === undefined || button.condition()}
|
||||
<ActionButton
|
||||
on:click={button.onClick}
|
||||
onclick={button.onClick}
|
||||
icon={button.icon}
|
||||
label={button.label}
|
||||
type={button.type || 'primary'} />
|
||||
|
||||
@@ -1,165 +1,154 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
};
|
||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||
headers
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28',
|
||||
};
|
||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||
headers,
|
||||
});
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
return result.inner as any;
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(assets: any) {
|
||||
let url = '';
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
assets[i].name.includes('.bin') &&
|
||||
assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
url = assets[i].browser_download_url;
|
||||
}
|
||||
}
|
||||
if (url === '') {
|
||||
// if no asset was found, use the first one
|
||||
modals.open(InfoDialog, {
|
||||
title: 'No matching firmware found',
|
||||
message:
|
||||
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
|
||||
dismiss: { label: 'OK', icon: Check },
|
||||
onDismiss: () => modals.close(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown },
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll(),
|
||||
});
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
return result.inner as any;
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(assets: any) {
|
||||
let url = '';
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
assets[i].name.includes('.bin') &&
|
||||
assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
url = assets[i].browser_download_url;
|
||||
}
|
||||
}
|
||||
if (url === '') {
|
||||
// if no asset was found, use the first one
|
||||
modals.open(InfoDialog, {
|
||||
title: 'No matching firmware found',
|
||||
message:
|
||||
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
|
||||
dismiss: { label: 'OK', icon: Check },
|
||||
onDismiss: () => modals.close()
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Github Firmware Manager</span>
|
||||
{/snippet}
|
||||
{#await getGithubAPI()}
|
||||
<Spinner />
|
||||
{:then githubReleases}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<div
|
||||
class="overflow-x-auto"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<table class="table w-full table-auto">
|
||||
<thead>
|
||||
<tr class="font-bold">
|
||||
<th align="left">Release</th>
|
||||
<th align="center" class="hidden sm:block">Release Date</th>
|
||||
<th align="center">Experimental</th>
|
||||
<th align="center">Install</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each githubReleases as release}
|
||||
<tr
|
||||
class={(
|
||||
compareVersions(
|
||||
$features.firmware_version,
|
||||
release.tag_name
|
||||
) === 0
|
||||
) ?
|
||||
'bg-primary text-primary-content'
|
||||
: 'bg-base-100 h-14'}
|
||||
>
|
||||
<td align="left" class="text-base font-semibold">
|
||||
<a
|
||||
href={release.html_url}
|
||||
class="link link-hover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{release.name}</a
|
||||
></td
|
||||
>
|
||||
<td align="center" class="hidden min-h-full align-middle sm:block">
|
||||
<div class="my-2">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium'
|
||||
}).format(new Date(release.published_at))}
|
||||
</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if release.prerelease}
|
||||
<Prerelease class="text-accent h-5 w-5" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if compareVersions($features.firmware_version, release.tag_name) != 0}
|
||||
<button
|
||||
class="btn btn-ghost btn-circle btn-sm"
|
||||
onclick={() => {
|
||||
confirmGithubUpdate(release.assets);
|
||||
}}
|
||||
>
|
||||
<CloudDown class="text-secondary h-6 w-6" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<Error class="h-6 w-6 shrink-0" />
|
||||
<span
|
||||
>Please connect to a network with internet access to perform a firmware update.</span
|
||||
>
|
||||
</div>
|
||||
{/await}
|
||||
{#snippet icon()}
|
||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Github Firmware Manager</span>
|
||||
{/snippet}
|
||||
{#await getGithubAPI()}
|
||||
<Spinner />
|
||||
{:then githubReleases}
|
||||
<div class="relative w-full overflow-visible">
|
||||
<div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<table class="table w-full table-auto">
|
||||
<thead>
|
||||
<tr class="font-bold">
|
||||
<th align="left">Release</th>
|
||||
<th align="center" class="hidden sm:block">Release Date</th>
|
||||
<th align="center">Experimental</th>
|
||||
<th align="center">Install</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each githubReleases as release}
|
||||
<tr
|
||||
class={(
|
||||
compareVersions($features.firmware_version as string, release.tag_name) === 0
|
||||
) ?
|
||||
'bg-primary text-primary-content'
|
||||
: 'bg-base-100 h-14'}>
|
||||
<td align="left" class="text-base font-semibold">
|
||||
<a
|
||||
href={release.html_url}
|
||||
class="link link-hover"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">{release.name}</a
|
||||
></td>
|
||||
<td align="center" class="hidden min-h-full align-middle sm:block">
|
||||
<div class="my-2">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'medium',
|
||||
}).format(new Date(release.published_at))}
|
||||
</div>
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if release.prerelease}
|
||||
<Prerelease class="text-accent h-5 w-5" />
|
||||
{/if}
|
||||
</td>
|
||||
<td align="center">
|
||||
{#if compareVersions($features.firmware_version as string, release.tag_name) != 0}
|
||||
<button
|
||||
class="btn btn-ghost btn-circle btn-sm"
|
||||
onclick={() => {
|
||||
confirmGithubUpdate(release.assets);
|
||||
}}>
|
||||
<CloudDown class="text-secondary h-6 w-6" />
|
||||
</button>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{:catch error}
|
||||
<div class="alert alert-error shadow-lg">
|
||||
<Error class="h-6 w-6 shrink-0" />
|
||||
<span>Please connect to a network with internet access to perform a firmware update.</span>
|
||||
</div>
|
||||
{/await}
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,57 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
||||
|
||||
let files: FileList = $state();
|
||||
let files: FileList | undefined = $state();
|
||||
|
||||
async function uploadBIN() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[0]);
|
||||
const result = await api.post('/api/firmware', formData);
|
||||
if (result.isErr()) console.error('Error:', result.inner);
|
||||
}
|
||||
async function uploadBIN() {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files![0]);
|
||||
const result = await api.post('/api/firmware', formData);
|
||||
if (result.isErr()) console.error('Error:', result.inner);
|
||||
}
|
||||
|
||||
function confirmBinUpload() {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Flashing the Device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Upload', icon: OTA }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
uploadBIN();
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmBinUpload() {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Flashing the Device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Upload', icon: OTA },
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
uploadBIN();
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Upload Firmware</span>
|
||||
{/snippet}
|
||||
<div class="alert alert-warning shadow-lg">
|
||||
<Warning class="h-6 w-6 shrink-0" />
|
||||
<span
|
||||
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload
|
||||
a (.md5) file first to verify the uploaded firmware.
|
||||
</span>
|
||||
</div>
|
||||
{#snippet icon()}
|
||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Upload Firmware</span>
|
||||
{/snippet}
|
||||
<div class="alert alert-warning shadow-lg">
|
||||
<Warning class="h-6 w-6 shrink-0" />
|
||||
<span
|
||||
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload a
|
||||
(.md5) file first to verify the uploaded firmware.
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
id="binFile"
|
||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||
bind:files
|
||||
accept=".bin,.md5"
|
||||
onchange={confirmBinUpload}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
id="binFile"
|
||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||
bind:files
|
||||
accept=".bin,.md5"
|
||||
onchange={confirmBinUpload} />
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,436 +1,365 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
import { preventDefault } from 'svelte/legacy';
|
||||
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { PasswordInput } from '$lib/components/input';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import type { ApSettings, ApStatus } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { AP, Devices, Home, MAC } from '$lib/components/icons';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { PasswordInput } from '$lib/components/input';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import type { ApSettings, ApStatus } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { AP, Devices, Home, MAC } from '$lib/components/icons';
|
||||
import StatusItem from '$lib/components/StatusItem.svelte';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
let apSettings: ApSettings | null = $state(null);
|
||||
let apStatus: ApStatus | null = $state(null);
|
||||
|
||||
let apSettings: ApSettings = $state();
|
||||
let apStatus: ApStatus = $state();
|
||||
let formField: any = $state();
|
||||
|
||||
let formField: any = $state();
|
||||
async function getAPStatus() {
|
||||
const result = await api.get<ApStatus>('/api/wifi/ap/status');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
apStatus = result.inner;
|
||||
return apStatus;
|
||||
}
|
||||
|
||||
async function getAPStatus() {
|
||||
const result = await api.get<ApStatus>('/api/wifi/ap/status');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
apStatus = result.inner;
|
||||
return apStatus;
|
||||
async function getAPSettings() {
|
||||
const result = await api.get<ApSettings>('/api/wifi/ap/settings');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
apSettings = result.inner;
|
||||
return apSettings;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getAPStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(getAPSettings);
|
||||
|
||||
let provisionMode = [
|
||||
{
|
||||
id: 0,
|
||||
text: `Always`,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
text: `When WiFi Disconnected`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: `Never`,
|
||||
},
|
||||
];
|
||||
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning';
|
||||
|
||||
let apStatusVariant: Variant[] = ['success', 'error', 'warning'];
|
||||
|
||||
let apStatusDescription = ['Active', 'Inactive', 'Lingering'];
|
||||
|
||||
let formErrors = $state({
|
||||
ssid: false,
|
||||
channel: false,
|
||||
max_clients: false,
|
||||
local_ip: false,
|
||||
gateway_ip: false,
|
||||
subnet_mask: false,
|
||||
});
|
||||
|
||||
async function postAPSettings(data: ApSettings) {
|
||||
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
|
||||
if (result.isErr()) {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
notifications.success('Access Point settings updated.', 3000);
|
||||
apSettings = result.inner;
|
||||
}
|
||||
|
||||
function handleSubmitAP() {
|
||||
if (!apSettings) return;
|
||||
let valid = true;
|
||||
|
||||
// Validate SSID
|
||||
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
||||
valid = false;
|
||||
formErrors.ssid = true;
|
||||
} else {
|
||||
formErrors.ssid = false;
|
||||
}
|
||||
|
||||
async function getAPSettings() {
|
||||
const result = await api.get<ApSettings>('/api/wifi/ap/settings');
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
apSettings = result.inner;
|
||||
return apSettings;
|
||||
// Validate Channel
|
||||
let channel = Number(apSettings.channel);
|
||||
if (1 > channel || channel > 13) {
|
||||
valid = false;
|
||||
formErrors.channel = true;
|
||||
} else {
|
||||
formErrors.channel = false;
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getAPStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(getAPSettings);
|
||||
|
||||
let provisionMode = [
|
||||
{
|
||||
id: 0,
|
||||
text: `Always`
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
text: `When WiFi Disconnected`
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
text: `Never`
|
||||
}
|
||||
];
|
||||
|
||||
let apStatusDescription = [
|
||||
{ bg_color: 'bg-success', text_color: 'text-success-content', description: 'Active' },
|
||||
{ bg_color: 'bg-error', text_color: 'text-error-content', description: 'Inactive' },
|
||||
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
|
||||
];
|
||||
|
||||
let formErrors = $state({
|
||||
ssid: false,
|
||||
channel: false,
|
||||
max_clients: false,
|
||||
local_ip: false,
|
||||
gateway_ip: false,
|
||||
subnet_mask: false
|
||||
});
|
||||
|
||||
async function postAPSettings(data: ApSettings) {
|
||||
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
|
||||
if (result.isErr()) {
|
||||
notifications.error('User not authorized.', 3000);
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
notifications.success('Access Point settings updated.', 3000);
|
||||
apSettings = result.inner;
|
||||
// Validate max_clients
|
||||
let maxClients = Number(apSettings.max_clients);
|
||||
if (1 > maxClients || maxClients > 8) {
|
||||
valid = false;
|
||||
formErrors.max_clients = true;
|
||||
} else {
|
||||
formErrors.max_clients = false;
|
||||
}
|
||||
|
||||
function handleSubmitAP() {
|
||||
let valid = true;
|
||||
// RegEx for IPv4
|
||||
const regexExp =
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||
|
||||
// Validate SSID
|
||||
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
|
||||
valid = false;
|
||||
formErrors.ssid = true;
|
||||
} else {
|
||||
formErrors.ssid = false;
|
||||
}
|
||||
|
||||
// Validate Channel
|
||||
let channel = Number(apSettings.channel);
|
||||
if (1 > channel || channel > 13) {
|
||||
valid = false;
|
||||
formErrors.channel = true;
|
||||
} else {
|
||||
formErrors.channel = false;
|
||||
}
|
||||
|
||||
// Validate max_clients
|
||||
let maxClients = Number(apSettings.max_clients);
|
||||
if (1 > maxClients || maxClients > 8) {
|
||||
valid = false;
|
||||
formErrors.max_clients = true;
|
||||
} else {
|
||||
formErrors.max_clients = false;
|
||||
}
|
||||
|
||||
// RegEx for IPv4
|
||||
const regexExp =
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||
|
||||
// Validate gateway IP
|
||||
if (!regexExp.test(apSettings.gateway_ip)) {
|
||||
valid = false;
|
||||
formErrors.gateway_ip = true;
|
||||
} else {
|
||||
formErrors.gateway_ip = false;
|
||||
}
|
||||
|
||||
// Validate Subnet Mask
|
||||
if (!regexExp.test(apSettings.subnet_mask)) {
|
||||
valid = false;
|
||||
formErrors.subnet_mask = true;
|
||||
} else {
|
||||
formErrors.subnet_mask = false;
|
||||
}
|
||||
|
||||
// Validate local IP
|
||||
if (!regexExp.test(apSettings.local_ip)) {
|
||||
valid = false;
|
||||
formErrors.local_ip = true;
|
||||
} else {
|
||||
formErrors.local_ip = false;
|
||||
}
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postAPSettings(apSettings);
|
||||
}
|
||||
// Validate gateway IP
|
||||
if (!regexExp.test(apSettings.gateway_ip)) {
|
||||
valid = false;
|
||||
formErrors.gateway_ip = true;
|
||||
} else {
|
||||
formErrors.gateway_ip = false;
|
||||
}
|
||||
|
||||
// Validate Subnet Mask
|
||||
if (!regexExp.test(apSettings.subnet_mask)) {
|
||||
valid = false;
|
||||
formErrors.subnet_mask = true;
|
||||
} else {
|
||||
formErrors.subnet_mask = false;
|
||||
}
|
||||
|
||||
// Validate local IP
|
||||
if (!regexExp.test(apSettings.local_ip)) {
|
||||
valid = false;
|
||||
formErrors.local_ip = true;
|
||||
} else {
|
||||
formErrors.local_ip = false;
|
||||
}
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postAPSettings(apSettings);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<AP class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Access Point</span>
|
||||
{/snippet}
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getAPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status]
|
||||
.bg_color}"
|
||||
>
|
||||
<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">
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
{#snippet icon()}
|
||||
<AP class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Access Point</span>
|
||||
{/snippet}
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getAPStatus()}
|
||||
<Spinner />
|
||||
{:then}
|
||||
{#if apStatus}
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
|
||||
>
|
||||
Change AP Settings
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<StatusItem
|
||||
icon={AP}
|
||||
title="Status"
|
||||
variant={apStatusVariant[apStatus.status]}
|
||||
description={apStatusDescription[apStatus.status]} />
|
||||
|
||||
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||
|
||||
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||
|
||||
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
|
||||
</div>
|
||||
{#await getAPSettings()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<form
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||
onsubmit={preventDefault(handleSubmitAP)}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
>
|
||||
<div>
|
||||
<label class="label" for="apmode">
|
||||
<span class="label-text">Provide Access Point ...</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
id="apmode"
|
||||
bind:value={apSettings.provision_mode}
|
||||
>
|
||||
{#each provisionMode as mode}
|
||||
<option value={mode.id}>
|
||||
{mode.text}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="ssid">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 2 and 32 characters long</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Preferred Channel</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="13"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.channel
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.channel}
|
||||
id="channel"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="channel">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.channel ? '' : (
|
||||
'hidden'
|
||||
)}">Must be channel 1 to 13</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text text-md">Max Clients</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="8"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.max_clients
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.max_clients}
|
||||
id="clients"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="clients">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.max_clients ? '' : (
|
||||
'hidden'
|
||||
)}">Maximum 8 clients allowed</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.local_ip}
|
||||
id="localIP"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="localIP">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.local_ip ? '' : (
|
||||
'hidden'
|
||||
)}">Must be a valid IPv4 address</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.gateway_ip}
|
||||
id="gateway"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="gateway">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.gateway_ip ? '' : (
|
||||
'hidden'
|
||||
)}">Must be a valid IPv4 address</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.subnet_mask}
|
||||
id="subnet"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="subnet">
|
||||
<span
|
||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : (
|
||||
'hidden'
|
||||
)}">Must be a valid IPv4 address</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={apSettings.ssid_hidden}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="">Hide SSID</span>
|
||||
</label>
|
||||
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium">
|
||||
Change AP Settings
|
||||
</div>
|
||||
{#await getAPSettings()}
|
||||
<Spinner />
|
||||
{:then}
|
||||
{#if apSettings}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<form
|
||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||
onsubmit={preventDefault(handleSubmitAP)}
|
||||
novalidate
|
||||
bind:this={formField}>
|
||||
<div>
|
||||
<label class="label" for="apmode">
|
||||
<span class="label-text">Provide Access Point ...</span>
|
||||
</label>
|
||||
<select
|
||||
class="select select-bordered w-full"
|
||||
id="apmode"
|
||||
bind:value={apSettings.provision_mode}>
|
||||
{#each provisionMode as mode}
|
||||
<option value={mode.id}>
|
||||
{mode.text}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text text-md">SSID</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.ssid
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.ssid}
|
||||
id="ssid"
|
||||
min="2"
|
||||
max="32"
|
||||
required />
|
||||
<label class="label" for="ssid">
|
||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||
>SSID must be between 2 and 32 characters long</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text text-md">Preferred Channel</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="13"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.channel
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.channel}
|
||||
id="channel"
|
||||
required />
|
||||
<label class="label" for="channel">
|
||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||
>Must be channel 1 to 13</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text text-md">Max Clients</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="8"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||
formErrors.max_clients
|
||||
) ?
|
||||
'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={apSettings.max_clients}
|
||||
id="clients"
|
||||
required />
|
||||
<label class="label" for="clients">
|
||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||
>Maximum 8 clients allowed</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text text-md">Local IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.local_ip}
|
||||
id="localIP"
|
||||
required />
|
||||
<label class="label" for="localIP">
|
||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text text-md">Gateway IP</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.gateway_ip}
|
||||
id="gateway"
|
||||
required />
|
||||
<label class="label" for="gateway">
|
||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text text-md">Subnet Mask</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||
: ''}"
|
||||
minlength="7"
|
||||
maxlength="15"
|
||||
size="15"
|
||||
bind:value={apSettings.subnet_mask}
|
||||
id="subnet"
|
||||
required />
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={apSettings.ssid_hidden}
|
||||
class="checkbox checkbox-primary" />
|
||||
<span class="">Hide SSID</span>
|
||||
</label>
|
||||
|
||||
<div class="place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
@@ -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>
|
||||
+115
-131
@@ -1,147 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||
import type { NetworkItem, NetworkList } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
|
||||
import { modals, exitBeforeEnter } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||
import type { NetworkItem, NetworkList } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { AP, Network, Reload, Cancel } from '$lib/components/icons';
|
||||
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
storeNetwork: any;
|
||||
let { isOpen, storeNetwork }: ModalProps = $props();
|
||||
|
||||
const encryptionType = [
|
||||
'Open',
|
||||
'WEP',
|
||||
'WPA PSK',
|
||||
'WPA2 PSK',
|
||||
'WPA WPA2 PSK',
|
||||
'WPA2 Enterprise',
|
||||
'WPA3 PSK',
|
||||
'WPA2 WPA3 PSK',
|
||||
'WAPI PSK'
|
||||
]
|
||||
|
||||
let listOfNetworks: NetworkItem[] = $state([])
|
||||
|
||||
let scanActive = $state(false)
|
||||
|
||||
let pollingId: ReturnType<typeof setTimeout> | number
|
||||
|
||||
async function scanNetworks() {
|
||||
scanActive = true
|
||||
await api.get('/api/wifi/scan')
|
||||
if ((await pollingResults()) == false) {
|
||||
pollingId = setInterval(() => pollingResults(), 1000)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let { isOpen, storeNetwork }: Props = $props();
|
||||
|
||||
const encryptionType = [
|
||||
'Open',
|
||||
'WEP',
|
||||
'WPA PSK',
|
||||
'WPA2 PSK',
|
||||
'WPA WPA2 PSK',
|
||||
'WPA2 Enterprise',
|
||||
'WPA3 PSK',
|
||||
'WPA2 WPA3 PSK',
|
||||
'WAPI PSK'
|
||||
];
|
||||
|
||||
let listOfNetworks: NetworkItem[] = $state([]);
|
||||
|
||||
let scanActive = $state(false);
|
||||
|
||||
let pollingId: number;
|
||||
|
||||
async function scanNetworks() {
|
||||
scanActive = true;
|
||||
await api.get('/api/wifi/scan');
|
||||
if ((await pollingResults()) == false) {
|
||||
pollingId = setInterval(() => pollingResults(), 1000);
|
||||
}
|
||||
return;
|
||||
async function pollingResults() {
|
||||
const result = await api.get<NetworkList>('/api/wifi/networks')
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner)
|
||||
return false
|
||||
}
|
||||
|
||||
async function pollingResults() {
|
||||
const result = await api.get<NetworkList>('/api/wifi/networks');
|
||||
if (result.isErr()) {
|
||||
console.error(`Error occurred while fetching: `, result.inner);
|
||||
return false;
|
||||
}
|
||||
let response = result.inner;
|
||||
listOfNetworks = response.networks;
|
||||
scanActive = false;
|
||||
if (listOfNetworks.length) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
return listOfNetworks.length;
|
||||
let response = result.inner
|
||||
listOfNetworks = response.networks
|
||||
scanActive = false
|
||||
if (listOfNetworks.length) {
|
||||
clearInterval(pollingId)
|
||||
pollingId = 0
|
||||
}
|
||||
return listOfNetworks.length
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
scanNetworks();
|
||||
});
|
||||
onMount(() => {
|
||||
scanNetworks()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollingId) {
|
||||
clearInterval(pollingId);
|
||||
pollingId = 0;
|
||||
}
|
||||
});
|
||||
onDestroy(() => {
|
||||
if (pollingId) {
|
||||
clearInterval(pollingId)
|
||||
pollingId = 0
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="overflow-y-auto">
|
||||
{#if scanActive}<div
|
||||
class="bg-base-100 flex flex-col items-center justify-center p-6"
|
||||
>
|
||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="overflow-y-auto">
|
||||
{#if scanActive}<div class="bg-base-100 flex flex-col items-center justify-center p-6">
|
||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="menu">
|
||||
{#each listOfNetworks as network, i}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
|
||||
onclick={() => {
|
||||
storeNetwork(network.ssid)
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
||||
<Network class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{network.ssid}</div>
|
||||
<div class="text-sm opacity-75">
|
||||
Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="menu">
|
||||
{#each listOfNetworks as network, i}
|
||||
<li>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div
|
||||
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
|
||||
onclick={() => {
|
||||
storeNetwork(network.ssid);
|
||||
}}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
||||
<Network
|
||||
class="text-primary-content h-auto w-full scale-75"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">{network.ssid}</div>
|
||||
<div class="text-sm opacity-75">
|
||||
Security: {encryptionType[network.encryption_type]},
|
||||
Channel: {network.channel}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex flex-none items-center"
|
||||
disabled={scanActive}
|
||||
onclick={scanNetworks}
|
||||
>
|
||||
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-primary inline-flex flex-none items-center"
|
||||
disabled={scanActive}
|
||||
onclick={scanNetworks}>
|
||||
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
||||
</button>
|
||||
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
onclick={() => modals.close()}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
onclick={() => modals.close()}>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+586
-735
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Plastic_-_Matte_(Black)
|
||||
Kd 0.098039 0.098039 0.098039
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Opaque(28,28,28)
|
||||
Kd 0.109804 0.109804 0.109804
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
newmtl Opaque(0,0,255)
|
||||
Kd 0.000000 0.000000 1.000000
|
||||
|
||||
newmtl Opaque(202,209,238)
|
||||
Kd 0.792157 0.819608 0.933333
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Opaque(28,28,28)
|
||||
Kd 0.109804 0.109804 0.109804
|
||||
|
||||
newmtl Opaque(0,0,255)
|
||||
Kd 0.000000 0.000000 1.000000
|
||||
|
||||
newmtl Opaque(202,209,238)
|
||||
Kd 0.792157 0.819608 0.933333
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Opaque(28,28,28)
|
||||
Kd 0.109804 0.109804 0.109804
|
||||
|
||||
newmtl Opaque(0,0,255)
|
||||
Kd 0.000000 0.000000 1.000000
|
||||
|
||||
newmtl Opaque(202,209,238)
|
||||
Kd 0.792157 0.819608 0.933333
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Opaque(28,28,28)
|
||||
Kd 0.109804 0.109804 0.109804
|
||||
|
||||
newmtl Opaque(0,0,255)
|
||||
Kd 0.000000 0.000000 1.000000
|
||||
|
||||
newmtl Opaque(202,209,238)
|
||||
Kd 0.792157 0.819608 0.933333
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,14 @@
|
||||
# WaveFront *.mtl file (generated by Autodesk ATF)
|
||||
|
||||
newmtl Stainless_Steel_-_Satin
|
||||
Kd 0.796078 0.796078 0.796078
|
||||
|
||||
newmtl Steel_-_Satin
|
||||
Kd 0.627451 0.627451 0.627451
|
||||
|
||||
newmtl Plastic_-_Matte_(Black)
|
||||
Kd 0.098039 0.098039 0.098039
|
||||
|
||||
newmtl Rubber_-_Soft
|
||||
Kd 0.152941 0.152941 0.152941
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1,402 @@
|
||||
<?xml version="1.0"?>
|
||||
<robot name="Dog">
|
||||
<material name="white">
|
||||
<color rgba="1 1 1 1" />
|
||||
</material>
|
||||
<material name="black">
|
||||
<color rgba="0 0 0 1" />
|
||||
</material>
|
||||
<material name="foot_color">
|
||||
<color rgba="0 0.75 1 1" />
|
||||
</material>
|
||||
<link name="base_link">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/frame.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 3.141" xyz="0 0 0.0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="1.5" />
|
||||
<inertia ixx="0" ixy="0" ixz="0" iyy="0" iyz="0" izz="0" />
|
||||
</inertial>
|
||||
</link>
|
||||
<!-- shell -->
|
||||
<link name="frame">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/shell.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 3.141" xyz="0 0 0" />
|
||||
<material name="white" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/shell.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 3.141" xyz="0 0 0.0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="base_to_frame" type="fixed">
|
||||
<parent link="base_link" />
|
||||
<child link="frame" />
|
||||
<origin xyz="0 0 0" />
|
||||
</joint>
|
||||
<!-- lf shoulder -->
|
||||
<link name="lf_shoulder">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/lf shoulder.stl" />
|
||||
</geometry>
|
||||
<origin rpy="3.141 3.141 0" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="lf_shoulder" type="continuous">
|
||||
<parent link="base_link" />
|
||||
<child link="lf_shoulder" />
|
||||
<origin xyz="0.090 0.037 0" />
|
||||
<axis xyz="1 0 0" />
|
||||
</joint>
|
||||
<link name="lf_thigh">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/femur.stl" />
|
||||
</geometry>
|
||||
<origin rpy="1.5708 0 1.5708" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="lf_thigh" type="continuous">
|
||||
<parent link="lf_shoulder" />
|
||||
<child link="lf_thigh" />
|
||||
<origin xyz="0.025 0.027 0 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="lf_shin">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="lf_shin" type="continuous">
|
||||
<parent link="lf_thigh" />
|
||||
<child link="lf_shin" />
|
||||
<origin xyz="0 0 -0.130 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="lf_toe">
|
||||
<visual>
|
||||
<geometry>
|
||||
<sphere radius="0.005" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
<material name="foot_color" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.05" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<sphere radius="0.020" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="lf_toe" type="fixed">
|
||||
<parent link="lf_shin" />
|
||||
<child link="lf_toe" />
|
||||
<origin xyz="0 0 -0.130" />
|
||||
</joint>
|
||||
<!-- rf shoulder -->
|
||||
<link name="rf_shoulder">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/rf shoulder.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 3.141" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="rf_shoulder" type="continuous">
|
||||
<parent link="base_link" />
|
||||
<child link="rf_shoulder" />
|
||||
<origin xyz="0.090 -0.040 0" />
|
||||
<axis xyz="1 0 0" />
|
||||
</joint>
|
||||
<link name="rf_thigh">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/femur.stl" />
|
||||
</geometry>
|
||||
<origin rpy="4.71239 3.141 1.5708" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="rf_thigh" type="continuous">
|
||||
<parent link="rf_shoulder" />
|
||||
<child link="rf_thigh" />
|
||||
<origin xyz="0.025 -0.027 0 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="rf_shin">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.0045 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.007 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="rf_shin" type="continuous">
|
||||
<parent link="rf_thigh" />
|
||||
<child link="rf_shin" />
|
||||
<origin xyz="0 0 -0.130 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="rf_toe">
|
||||
<visual>
|
||||
<geometry>
|
||||
<sphere radius="0.005" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
<material name="foot_color" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.05" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<sphere radius="0.020" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="rf_toe" type="fixed">
|
||||
<parent link="rf_shin" />
|
||||
<child link="rf_toe" />
|
||||
<origin xyz="0 0 -0.130" />
|
||||
</joint>
|
||||
<!-- lb shoulder -->
|
||||
<link name="lb_shoulder">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/lb shoulder.stl" />
|
||||
</geometry>
|
||||
<origin rpy="3.141 3.141 0" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="lb_shoulder" type="continuous">
|
||||
<parent link="base_link" />
|
||||
<child link="lb_shoulder" />
|
||||
<origin xyz="-0.081 0.038 0" />
|
||||
<axis xyz="1 0 0" />
|
||||
</joint>
|
||||
<link name="lb_thigh">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/femur.stl" />
|
||||
</geometry>
|
||||
<origin rpy="1.5708 0 1.5708" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="lb_thigh" type="continuous">
|
||||
<parent link="lb_shoulder" />
|
||||
<child link="lb_thigh" />
|
||||
<origin xyz="-0.043 0.027 0 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="lb_shin">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 0.008 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="lb_shin" type="continuous">
|
||||
<parent link="lb_thigh" />
|
||||
<child link="lb_shin" />
|
||||
<origin xyz="0 0 -0.130 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="lb_toe">
|
||||
<visual>
|
||||
<geometry>
|
||||
<sphere radius="0.0005" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
<material name="foot_color" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.05" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<sphere radius="0.020" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="lb_toe" type="fixed">
|
||||
<parent link="lb_shin" />
|
||||
<child link="lb_toe" />
|
||||
<origin xyz="0 0 -0.130" />
|
||||
</joint>
|
||||
<!-- rb arm -->
|
||||
<link name="rb_shoulder">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/rb shoulder.stl" />
|
||||
</geometry>
|
||||
<origin rpy="3.141 3.141 0" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="rb_shoulder" type="continuous">
|
||||
<parent link="base_link" />
|
||||
<child link="rb_shoulder" />
|
||||
<origin xyz="-0.081 -0.040 0" />
|
||||
<axis xyz="1 0 0" />
|
||||
</joint>
|
||||
<link name="rb_thigh">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/femur.stl" />
|
||||
</geometry>
|
||||
<origin rpy="4.71239 3.141 1.5708" xyz="0 0 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
</link>
|
||||
<joint name="rb_thigh" type="continuous">
|
||||
<parent link="rb_shoulder" />
|
||||
<child link="rb_thigh" />
|
||||
<origin xyz="-0.043 -0.027 0 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="rb_shin">
|
||||
<visual>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.0045 0" />
|
||||
<material name="black" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.025" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<mesh filename="package://URDF/tibia.stl" />
|
||||
</geometry>
|
||||
<origin rpy="0.610865 0 -1.5708" xyz="0 -0.007 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="rb_shin" type="continuous">
|
||||
<parent link="rb_thigh" />
|
||||
<child link="rb_shin" />
|
||||
<origin xyz="0 0 -0.130 " />
|
||||
<axis xyz="0 1 0" />
|
||||
</joint>
|
||||
<link name="rb_toe">
|
||||
<visual>
|
||||
<geometry>
|
||||
<sphere radius="0.0005" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
<material name="foot_color" />
|
||||
</visual>
|
||||
<inertial>
|
||||
<mass value="0.05" />
|
||||
<inertia ixx="0.4" ixy="0.4" ixz="0.4" iyy="0.4" iyz="0.4" izz="0.4" />
|
||||
</inertial>
|
||||
<collision>
|
||||
<geometry>
|
||||
<sphere radius="0.020" />
|
||||
</geometry>
|
||||
<origin rpy="0 0 0" xyz="0 0 0" />
|
||||
</collision>
|
||||
</link>
|
||||
<joint name="rb_toe" type="fixed">
|
||||
<parent link="rb_shin" />
|
||||
<child link="rb_toe" />
|
||||
<origin xyz="0 0 -0.130" />
|
||||
</joint>
|
||||
</robot>
|
||||
@@ -1,10 +1,10 @@
|
||||
import adapter from '@sveltejs/adapter-static'
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
const basePath = process.env.BASE_PATH ?? ''
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
// Consult https://kit.svelte.dev/docs/integrations#preprocessors
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
@@ -14,7 +14,10 @@ const config = {
|
||||
fallback: 'index.html',
|
||||
precompress: false,
|
||||
strict: true
|
||||
})
|
||||
}),
|
||||
paths: {
|
||||
base: basePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.route('**/api/features', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
);
|
||||
|
||||
await expect(page).toHaveTitle(/Spot micro controller/);
|
||||
});
|
||||
await page.goto('/')
|
||||
await expect(page).toHaveTitle(/Spot micro controller/)
|
||||
})
|
||||
|
||||
test('index page has expected h1', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.route('**/api/features', (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
body: JSON.stringify({})
|
||||
})
|
||||
);
|
||||
await expect(page.getByRole('heading', { name: 'Spot micro controller' }).first()).toBeVisible();
|
||||
});
|
||||
await page.goto('/')
|
||||
await expect(page.getByRole('heading', { name: 'Spot micro controller' }).first()).toBeVisible()
|
||||
})
|
||||
|
||||
+43
-25
@@ -1,31 +1,49 @@
|
||||
import type { UserConfig, Plugin } from 'vite';
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export default function viteLittleFS(): Plugin[] {
|
||||
return [
|
||||
{
|
||||
name: 'vite-plugin-littlefs',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
return [
|
||||
{
|
||||
name: 'vite-plugin-littlefs',
|
||||
enforce: 'post',
|
||||
apply: 'build',
|
||||
|
||||
async config(config, _configEnv) {
|
||||
const { assetFileNames, chunkFileNames, entryFileNames } =
|
||||
config.build?.rollupOptions?.output;
|
||||
async config(config) {
|
||||
const output = config.build?.rollupOptions?.output;
|
||||
|
||||
// Handle Server-build + Client Assets
|
||||
config.build.rollupOptions.output = {
|
||||
...config.build?.rollupOptions?.output,
|
||||
assetFileNames: assetFileNames.replace('.[hash]', '')
|
||||
};
|
||||
if (!output || !config.build?.rollupOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Client-build
|
||||
if (config.build?.rollupOptions?.output.chunkFileNames.includes('hash')) {
|
||||
config.build.rollupOptions.output = {
|
||||
...config.build?.rollupOptions?.output,
|
||||
chunkFileNames: chunkFileNames.replace('.[hash]', ''),
|
||||
entryFileNames: entryFileNames.replace('.[hash]', '')
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
const outputOptions = Array.isArray(output) ? output[0] : output;
|
||||
|
||||
if (!outputOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { assetFileNames, chunkFileNames, entryFileNames } = outputOptions;
|
||||
|
||||
if (assetFileNames && typeof assetFileNames === 'string') {
|
||||
config.build.rollupOptions.output = {
|
||||
...outputOptions,
|
||||
assetFileNames: assetFileNames.replace('.[hash]', ''),
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
chunkFileNames &&
|
||||
typeof chunkFileNames === 'string' &&
|
||||
chunkFileNames.includes('hash')
|
||||
) {
|
||||
config.build.rollupOptions.output = {
|
||||
...config.build.rollupOptions.output,
|
||||
chunkFileNames: chunkFileNames.replace('.[hash]', ''),
|
||||
...(entryFileNames &&
|
||||
typeof entryFileNames === 'string' && {
|
||||
entryFileNames: entryFileNames.replace('.[hash]', ''),
|
||||
}),
|
||||
};
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
+3
-3
@@ -5,7 +5,10 @@ import viteLittleFS from './vite-plugin-littlefs'
|
||||
import EnvCaster from '@niku/vite-env-caster'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
const basePath = process.env.BASE_PATH ?? ''
|
||||
|
||||
export default defineConfig({
|
||||
base: basePath,
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
sveltekit(),
|
||||
@@ -15,9 +18,6 @@ export default defineConfig({
|
||||
viteLittleFS(),
|
||||
EnvCaster()
|
||||
],
|
||||
test: {
|
||||
include: ['src/**/*.{test,spec}.{js,ts}']
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
# 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
|
||||
|
||||
Spot is 3D printed and is a combination of different Spot Micro designs, with some minor modification on top.
|
||||
The original design is developed by KDY0523.
|
||||
Spot is 3D-printed and is a combination of different Spot Micro designs, with some minor modifications.
|
||||
The original design was developed by KDY0523.
|
||||
|
||||
- [robjk reinforced shoulder remix](https://www.thingiverse.com/thing:4937631)
|
||||
- [Kooba SpotMicroESP32 remix](https://www.thingiverse.com/thing:4559827)
|
||||
- [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
|
||||
- 92x M3x8 screws + M3 nuts
|
||||
@@ -20,7 +20,7 @@ The 3D prints is assembled with some additional component:
|
||||
|
||||
## 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 |
|
||||
| ------------------------- | ----------------------------- | -------- | ------------------------------------------------------------------------------------------------------- |
|
||||
@@ -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. |
|
||||
| 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
|
||||
|
||||
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)
|
||||
- [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)
|
||||
|
||||
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
|
||||
|
||||
@@ -38,13 +38,28 @@ You now have the values for the servos.
|
||||
|
||||
### 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.
|
||||
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
|
||||
|
||||

|
||||

|
||||
|
||||
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) |
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user