Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c12ef332e | |||
| 481dfaf8e5 | |||
| 6769ffeb20 | |||
| 0586775849 | |||
| 4766f47e7e | |||
| d2d7d8e323 | |||
| c5155fe641 | |||
| f1312fb5c6 | |||
| a592848f34 | |||
| 06b05b2dc1 | |||
| 01d46f283b | |||
| 7c8c5b40a1 | |||
| 632f603fda | |||
| 4101ad033c | |||
| 3ee096bfab | |||
| 753e692fe2 | |||
| 40025a55c3 | |||
| 98262b2efc | |||
| 01e174f337 | |||
| a9fea7fd56 | |||
| e09ec81f1d | |||
| ee17f6862c | |||
| 8be7546eba | |||
| e156b732eb | |||
| 20c5a8ee92 | |||
| dac21a499f | |||
| 9a6c240140 | |||
| 8733ecd9b7 | |||
| fba531d3e8 | |||
| fc04d1b8d6 | |||
| 4c33a75164 | |||
| 6015e67d05 | |||
| f59f32ce26 | |||
| 3671610860 | |||
| c346f7f553 | |||
| f864616303 | |||
| ad2d28c9ba | |||
| 967923321f | |||
| 6b7e3281cf | |||
| fdf70f7eb8 | |||
| e4cb035ad9 | |||
| c02938b567 | |||
| c24740e8ec | |||
| e0d3912d83 | |||
| b113a30942 | |||
| 9534529e50 | |||
| 23a41d26b1 | |||
| 569c19ad1d | |||
| 17e30ebfe9 | |||
| 170e180c11 | |||
| 5a24038d68 | |||
| 99660b9a23 | |||
| 72f3bcfd78 | |||
| 297d61c188 | |||
| 382a58fc53 | |||
| 5daa299895 | |||
| af6609c97b | |||
| aca258268f | |||
| 9ff6dd7d4f | |||
| 40509cdd1f | |||
| 37f9238c55 | |||
| d14446d09e | |||
| 74cb4aaee4 | |||
| d90dbbcf21 | |||
| 31dd7f7ba4 | |||
| 113ac1bc2c | |||
| 0b01634a20 | |||
| 788f4ffea3 | |||
| d9285bbdc0 | |||
| a4eca1460e | |||
| 0c0061c9e0 | |||
| 88fec1e5d2 | |||
| d4b485160b | |||
| 97030e53a1 | |||
| 3da6e3c043 | |||
| 625b228103 | |||
| 6ae44241d5 | |||
| 1174fa4e12 | |||
| 01cbd7c117 | |||
| 1caa0ff96e | |||
| d83dd0badb | |||
| da0c1de47d | |||
| 15ec137edb | |||
| abdf763215 | |||
| a7c5a5f1cf | |||
| b37e8706a6 | |||
| e109f3584a | |||
| 8792c06e8a | |||
| 852ff91b7d | |||
| ba9d8e1bec | |||
| b7882ee6cf | |||
| d8ca913188 | |||
| ad86bc5fd4 | |||
| f04cdaa031 | |||
| 4b909adfb7 | |||
| f75b224a76 | |||
| 1a6e3626f6 | |||
| b5a8fe88ca | |||
| d5b003ab94 | |||
| 24d39e540e | |||
| b6fe8844f4 | |||
| 62f3ee1bcb | |||
| 07dc8b1d49 | |||
| 9c600f0773 | |||
| 66b1b8fa1b | |||
| eeff317abe | |||
| b346e104d4 | |||
| c9548e2da1 | |||
| 841ae91c33 | |||
| f2d86115fb | |||
| 0d596d9d3c | |||
| 0cce6075b9 | |||
| 0b1c27819e | |||
| 35c9f54f52 | |||
| f4bf5562f4 | |||
| 57c126a7bc | |||
| 35e1cc678a | |||
| f3d2fec0e9 | |||
| e919b2aa41 | |||
| 09f5460db7 | |||
| c92a931846 | |||
| 8f64edc3e4 | |||
| 7f03790cf7 | |||
| 622e15278f | |||
| 118caff7ba | |||
| dcfce13f4b | |||
| 8283e24407 | |||
| 2f8bcaa291 | |||
| 426b4a1332 | |||
| 316b1a52cb | |||
| 1b7ae688a6 | |||
| 9ca42dbc69 | |||
| d52a15eff7 | |||
| 9dc6742e82 | |||
| c1e12bffe8 | |||
| fc0914ded4 | |||
| f57d798971 | |||
| 2de1238405 | |||
| 0fd729be4a | |||
| ea42cc0aac | |||
| 7046957669 | |||
| 634b3292b4 | |||
| 4de6be7815 | |||
| da27ba37be | |||
| 87fe566d0d | |||
| ea5b16de0c | |||
| 386f1c627d | |||
| e77de7dbdb | |||
| a7eec4f7f2 | |||
| 4fff03ce54 | |||
| 9be13d1df5 | |||
| 698b7fbba9 | |||
| a3fc3eca2e | |||
| 6ce4747b4b | |||
| 89611b5e3e | |||
| fd652bd967 | |||
| 3a3de53752 | |||
| 10b0aa3c45 | |||
| d587b42987 | |||
| 9f3c4ffdf2 | |||
| 3054d5eb12 | |||
| 84633e5707 | |||
| 1c6b9f79c5 | |||
| 9923b66208 | |||
| 48d8b4f958 | |||
| 490207c9ff | |||
| 91156c42ae | |||
| 7dd5797481 | |||
| 7849f77712 | |||
| 1990501a66 | |||
| aad698f486 | |||
| 6ab093786a | |||
| b02d633f41 | |||
| d2094aa527 | |||
| 4d304cb567 | |||
| fb06437c43 | |||
| 5e0b31aaf2 | |||
| 756f1c0148 | |||
| 8ac2fad1b1 | |||
| e69e48533f | |||
| 9dbe31d207 | |||
| 2bffac6558 | |||
| db203e1503 | |||
| 97e0512dc3 | |||
| e8605336df | |||
| 818ed06a9a | |||
| 539600b0d3 | |||
| a4d8f0f613 | |||
| d903bd5a1c | |||
| fb8ee64ee4 | |||
| ac17be696b | |||
| 787d202a91 | |||
| ea7f7dc544 | |||
| bbd7d75b92 | |||
| 2d6466050b | |||
| 73c2038497 | |||
| 16e653afa8 | |||
| ce1558a6ab | |||
| 3ac81b376d | |||
| cd3ad93196 | |||
| a63b6b3633 | |||
| d77010ad41 | |||
| 92184e9456 | |||
| 092b19ae40 | |||
| 44fa0bd3e2 | |||
| 62fa5f79b6 | |||
| 42405ec93f | |||
| d07b0b5d7f | |||
| b9f24d9f4c | |||
| 2c5ac4dc5c | |||
| a19d6a2f4f | |||
| 951bfb4cd2 | |||
| 586dbc7a9a | |||
| cf55bf509e | |||
| 1ecc30fb21 | |||
| 47dd527c70 | |||
| d4c40a2a53 | |||
| a0c58841d7 | |||
| cfa729ff70 | |||
| 7ba5b5118a | |||
| 3da1717341 | |||
| 9978918bf9 | |||
| 904a1c5852 | |||
| 420428ec3e | |||
| ae7b1d8c99 | |||
| bdc535472d | |||
| ef4e476b89 | |||
| d33ffc7d95 | |||
| 1dd5cb631a | |||
| 4e530b4bff | |||
| c32e327320 | |||
| c9c6125462 | |||
| 2827b7c1b5 | |||
| 314a4939e2 | |||
| e805f017b9 | |||
| ed2a2b5c83 | |||
| 75fc3d9809 | |||
| a86b2fa50e | |||
| 296adfee51 | |||
| 00c56a2d68 | |||
| 3fd7f28d7e | |||
| d8659f8ed5 | |||
| 5e2f34f792 | |||
| 63459acc7f | |||
| db01879419 | |||
| ce8b48b101 | |||
| 42607df3d6 | |||
| af6015d6a0 | |||
| 1a3dabbc1e | |||
| 8afe3424d3 | |||
| 0e89643555 | |||
| 41c22399dc | |||
| 89ddd58935 | |||
| d6b3793275 | |||
| 6988c61a50 | |||
| 3f6348c49c |
@@ -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
|
||||
|
||||
@@ -37,7 +36,7 @@ jobs:
|
||||
run: pio run
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: esp32/build/firmware
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
.vscode/c_cpp_properties.json
|
||||
.vscode/launch.json
|
||||
.vscode/ipch
|
||||
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
.pio
|
||||
|
||||
+11
-6
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"experimentalTernaries": true,
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"svelteBracketNewLine": false,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
+8
-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`.
|
||||
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
declare module "app-env" {
|
||||
interface ENV {
|
||||
VITE_USE_HOST_NAME: boolean;
|
||||
}
|
||||
|
||||
const appEnv: ENV;
|
||||
export default appEnv;
|
||||
}
|
||||
+65
-59
@@ -1,61 +1,67 @@
|
||||
{
|
||||
"name": "spot_micro_controller",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@iconify-json/tabler": "^1.1.109",
|
||||
"@playwright/test": "^1.28.1",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/three": "^0.162.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"svelte-focus-trap": "^1.2.0",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.1.6",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite": "^5.0.3",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"chart.js": "^4.4.2",
|
||||
"compare-versions": "^6.1.0",
|
||||
"daisyui": "^4.10.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nipplejs": "^0.10.1",
|
||||
"svelte-dnd-list": "^0.1.8",
|
||||
"svelte-modals": "^1.3.0",
|
||||
"three": "^0.162.0",
|
||||
"urdf-loader": "^0.12.1",
|
||||
"uzip": "^0.20201231.0",
|
||||
"xacro-parser": "^0.3.9"
|
||||
}
|
||||
"name": "spot_micro_controller",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev --host",
|
||||
"build": "vite build",
|
||||
"build:embedded": "cross-env VITE_USE_HOST_NAME=true vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "pnpm run test:integration && pnpm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test:integration": "playwright test",
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@iconify-json/tabler": "^1.1.109",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/three": "^0.162.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-focus-trap": "^1.2.0",
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.5.0",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite": "^6.2.1",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@niku/vite-env-caster": "^1.0.2",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"chart.js": "^4.4.2",
|
||||
"compare-versions": "^6.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"daisyui": "^5.0.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"nipplejs": "^0.10.1",
|
||||
"svelte-dnd-list": "^0.1.8",
|
||||
"svelte-modals": "^2.0.0",
|
||||
"three": "^0.162.0",
|
||||
"urdf-loader": "^0.12.1",
|
||||
"uzip": "^0.20201231.0",
|
||||
"xacro-parser": "^0.3.9",
|
||||
"@types/msgpack-lite": "^0.1.11",
|
||||
"msgpack-lite": "^0.1.26"
|
||||
},
|
||||
"packageManager": "pnpm@9.3.0"
|
||||
}
|
||||
Generated
+1353
-1115
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
import tailwindcss from 'tailwindcss';
|
||||
import autoprefixer from 'autoprefixer';
|
||||
export default {
|
||||
plugins: [tailwindcss(), autoprefixer()]
|
||||
};
|
||||
+27
-5
@@ -1,9 +1,31 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import 'tailwindcss';
|
||||
@plugin "daisyui";
|
||||
|
||||
#nipple_0_0, #nipple_1_1 {
|
||||
z-index: 10!important;
|
||||
@plugin "daisyui" {
|
||||
themes:
|
||||
light --default,
|
||||
dark --prefersdark;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: 'light';
|
||||
default: true;
|
||||
--color-primary: #00bfff;
|
||||
--color-secondary: #3c00ff;
|
||||
--base-content: white;
|
||||
}
|
||||
|
||||
@plugin "daisyui/theme" {
|
||||
name: 'dark';
|
||||
prefersdark: true;
|
||||
--color-primary: #00bfff;
|
||||
--color-secondary: #3c00ff;
|
||||
--base-content: oklch(0.3 0.012 256);
|
||||
}
|
||||
|
||||
#nipple_0_0,
|
||||
#nipple_1_1 {
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
#three-gui-panel {
|
||||
|
||||
+3
-1
@@ -3,7 +3,9 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/logo512.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
+60
-53
@@ -1,73 +1,80 @@
|
||||
import { user } from '$lib/stores/user';
|
||||
import { get } from 'svelte/store';
|
||||
import { Err, Ok, type Result } from './utilities';
|
||||
import { location } from './stores';
|
||||
|
||||
export namespace api {
|
||||
export function get<TResponse>(endpoint: string, params?: RequestInit) {
|
||||
return sendRequest<TResponse>(endpoint, 'GET', null, params);
|
||||
}
|
||||
export function get<TResponse>(endpoint: string, params?: RequestInit) {
|
||||
return sendRequest<TResponse>(endpoint, 'GET', null, params);
|
||||
}
|
||||
|
||||
export function post<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'POST', data);
|
||||
}
|
||||
export function post<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'POST', data);
|
||||
}
|
||||
|
||||
export function put<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'PUT', data);
|
||||
}
|
||||
export function put<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'PUT', data);
|
||||
}
|
||||
|
||||
export function remove<TResponse>(endpoint: string) {
|
||||
return sendRequest<TResponse>(endpoint, 'DELETE');
|
||||
}
|
||||
export function remove<TResponse>(endpoint: string) {
|
||||
return sendRequest<TResponse>(endpoint, 'DELETE');
|
||||
}
|
||||
}
|
||||
|
||||
async function sendRequest<TResponse>(
|
||||
endpoint: string,
|
||||
method: string,
|
||||
data?: unknown,
|
||||
params?: RequestInit
|
||||
endpoint: string,
|
||||
method: string,
|
||||
data?: unknown,
|
||||
params?: RequestInit
|
||||
): Promise<Result<TResponse, Error>> {
|
||||
const user_token = get(user).bearer_token;
|
||||
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
|
||||
endpoint = resolveUrl(endpoint);
|
||||
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
|
||||
|
||||
const request = {
|
||||
...params,
|
||||
method,
|
||||
body,
|
||||
headers: {
|
||||
...params?.headers,
|
||||
Authorization: user_token ? 'Bearer ' + user_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
const request = {
|
||||
...params,
|
||||
method,
|
||||
body,
|
||||
headers: {
|
||||
...params?.headers,
|
||||
Authorization: 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
|
||||
let response;
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(endpoint, request);
|
||||
} catch (error) {
|
||||
return Err.new(new Error(), 'An error has occurred');
|
||||
}
|
||||
try {
|
||||
response = await fetch(endpoint, request);
|
||||
} catch (error) {
|
||||
return Err.new(new Error(), 'An error has occurred');
|
||||
}
|
||||
|
||||
const isResponseOk = response.status >= 200 && response.status < 400;
|
||||
if (!isResponseOk) {
|
||||
if (response.status === 401) {
|
||||
return Err.new(new ApiError(response), 'User was not authorized');
|
||||
}
|
||||
return Err.new(new ApiError(response), 'An error has occurred');
|
||||
}
|
||||
const isResponseOk = response.status >= 200 && response.status < 400;
|
||||
if (!isResponseOk) {
|
||||
if (response.status === 401) {
|
||||
return Err.new(new ApiError(response), 'User was not authorized');
|
||||
}
|
||||
return Err.new(new ApiError(response), 'An error has occurred');
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
return Ok.new(data as TResponse);
|
||||
} else {
|
||||
// Handle empty object as response
|
||||
return Ok.new(null as TResponse);
|
||||
}
|
||||
const contentType =
|
||||
response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
return Ok.new(data as TResponse);
|
||||
} else {
|
||||
// Handle empty object as response
|
||||
return Ok.new(null as TResponse);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveUrl(url: string): string {
|
||||
if (url.startsWith('http') || !get(location)) return url;
|
||||
const protocol = window.location.protocol;
|
||||
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public readonly response: Response) {
|
||||
super(`${response.status}`);
|
||||
}
|
||||
constructor(public readonly response: Response) {
|
||||
super(`${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Battery0 from '~icons/tabler/battery';
|
||||
import Battery25 from '~icons/tabler/battery-1';
|
||||
import Battery50 from '~icons/tabler/battery-2';
|
||||
import Battery75 from '~icons/tabler/battery-3';
|
||||
import Battery100 from '~icons/tabler/battery-4';
|
||||
import BatteryCharging from '~icons/tabler/battery-charging-2';
|
||||
|
||||
export let current = 0;
|
||||
export let voltage = 0;
|
||||
</script>
|
||||
|
||||
<div class="tooltip tooltip-left z-10" data-tip="{voltage}V {Math.floor(current*10)/10} mA">
|
||||
{#if voltage == 0}
|
||||
<BatteryCharging class="{$$props.class || ''} -rotate-90 animate-pulse" />
|
||||
{:else if voltage > 8.2}
|
||||
<Battery100 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if voltage > 8}
|
||||
<Battery75 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if voltage > 7.8}
|
||||
<Battery50 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if voltage > 7.6}
|
||||
<Battery25 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else}
|
||||
<Battery0 class="{$$props.class || ''} text-error -rotate-90 animate-pulse" />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,43 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Down from '~icons/tabler/chevron-down';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Down } from './icons';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function openCollapsible() {
|
||||
open = !open;
|
||||
if (open) {
|
||||
opened();
|
||||
} else {
|
||||
closed();
|
||||
}
|
||||
}
|
||||
|
||||
function openCollapsible() {
|
||||
open = !open;
|
||||
if (open) {
|
||||
dispatch('opened');
|
||||
} else {
|
||||
dispatch('closed');
|
||||
}
|
||||
}
|
||||
|
||||
export let open = false;
|
||||
let { icon, title, children, open, opened, closed, class: klass } = $props();
|
||||
</script>
|
||||
|
||||
<div class="{$$props.class || ''} relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => openCollapsible()}>
|
||||
<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 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
<button class="btn btn-circle btn-ghost btn-sm" onclick={() => openCollapsible()}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
open
|
||||
) ?
|
||||
'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -1,52 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import Check from '~icons/tabler/check';
|
||||
import { 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 />
|
||||
export let isOpen: boolean;
|
||||
|
||||
export let title: string;
|
||||
export let message: string;
|
||||
export let onConfirm: any;
|
||||
export let 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 }
|
||||
}
|
||||
}: 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 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
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" />
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={closeModal}
|
||||
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span
|
||||
>{labels?.cancel.label}</span
|
||||
></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
on:click={onConfirm}
|
||||
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span
|
||||
>{labels?.confirm.label}</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{@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
|
||||
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,92 +1,101 @@
|
||||
<script lang="ts">
|
||||
import { closeAllModals, onBeforeClose } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import { Cancel } from './icons';
|
||||
import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
// provided by <Modals />
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
let updating = true;
|
||||
let { isOpen }: Props = $props();
|
||||
|
||||
let progress = 0;
|
||||
$: if ($telemetry.download_ota.status == 'progress') {
|
||||
progress = $telemetry.download_ota.progress;
|
||||
}
|
||||
let updating = $state(true);
|
||||
|
||||
$: if ($telemetry.download_ota.status == 'error') {
|
||||
updating = false;
|
||||
}
|
||||
let progress = $state(0);
|
||||
$effect(() => {
|
||||
if ($telemetry.download_ota.status == 'progress') {
|
||||
progress = $telemetry.download_ota.progress;
|
||||
}
|
||||
});
|
||||
|
||||
let message = 'Preparing ...';
|
||||
let timerId: number;
|
||||
$effect(() => {
|
||||
if ($telemetry.download_ota.status == 'error') {
|
||||
updating = false;
|
||||
}
|
||||
});
|
||||
|
||||
$: if ($telemetry.download_ota.status == 'progress') {
|
||||
message = 'Downloading ...';
|
||||
} else if ($telemetry.download_ota.status == 'error') {
|
||||
message = $telemetry.download_ota.error;
|
||||
} else if ($telemetry.download_ota.status == 'finished') {
|
||||
message = 'Restarting ...';
|
||||
progress = 0;
|
||||
// Reload page after 5 sec
|
||||
timerId = setTimeout(() => {
|
||||
closeAllModals();
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
let message = $state('Preparing ...');
|
||||
|
||||
onBeforeClose(() => {
|
||||
if (updating) {
|
||||
// prevents modal from closing
|
||||
return false;
|
||||
} else {
|
||||
$telemetry.download_ota.status = 'idle';
|
||||
$telemetry.download_ota.error = '';
|
||||
$telemetry.download_ota.progress = 0;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
$effect(() => {
|
||||
if ($telemetry.download_ota.status == 'progress') {
|
||||
message = 'Downloading ...';
|
||||
} else if ($telemetry.download_ota.status == 'error') {
|
||||
message = $telemetry.download_ota.error;
|
||||
} else if ($telemetry.download_ota.status == 'finished') {
|
||||
message = 'Restarting ...';
|
||||
progress = 0;
|
||||
// Reload page after 5 sec
|
||||
setTimeout(() => {
|
||||
modals.closeAll();
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeClose(() => {
|
||||
if (updating) {
|
||||
// prevents modal from closing
|
||||
return false;
|
||||
} else {
|
||||
$telemetry.download_ota.status = 'idle';
|
||||
$telemetry.download_ota.error = '';
|
||||
$telemetry.download_ota.progress = 0;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
|
||||
<div class="divider my-2" />
|
||||
<div class="overflow-y-auto">
|
||||
<div class="bg-base-100 flex flex-col items-center justify-center p-6">
|
||||
{#if $telemetry.download_ota.status == 'progress'}
|
||||
<progress class="progress progress-primary w-56" value={progress} max="100" />
|
||||
{:else}
|
||||
<progress class="progress progress-primary w-56" />
|
||||
{/if}
|
||||
<p class="mt-8 text-2xl">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<div class="flex-grow" />
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
disabled={updating}
|
||||
on:click={() => {
|
||||
closeAllModals();
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">Updating Firmware</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="overflow-y-auto">
|
||||
<div class="bg-base-100 flex flex-col items-center justify-center p-6">
|
||||
{#if $telemetry.download_ota.status == 'progress'}
|
||||
<progress class="progress progress-primary w-56" value={progress} max="100"
|
||||
></progress>
|
||||
{:else}
|
||||
<progress class="progress progress-primary w-56"></progress>
|
||||
{/if}
|
||||
<p class="mt-8 text-2xl">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
disabled={updating}
|
||||
onclick={() => {
|
||||
modals.closeAll();
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,42 +1,51 @@
|
||||
<script lang="ts">
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Check from '~icons/tabler/check';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Check } from './icons';
|
||||
import { exitBeforeEnter } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
// provided by <Modals />
|
||||
|
||||
export let title: string;
|
||||
export let message: string;
|
||||
export let onDismiss: any;
|
||||
export let dismiss = { label: 'Dismiss', icon: Check };
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
message: string;
|
||||
onDismiss: any;
|
||||
dismiss?: any;
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
dismiss = { label: 'Dismiss', icon: Check }
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use: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" />
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
on:click={onDismiss}
|
||||
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<script lang="ts">
|
||||
let show = false;
|
||||
$: type = show ? 'text' : 'password';
|
||||
|
||||
export let value = '';
|
||||
export let id = '';
|
||||
function handleInput(e: any) {
|
||||
value = e.target.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<input {type} class="input input-bordered w-full" {value} on:input={handleInput} {id} />
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-1">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}"
|
||||
on:click={() => (show = false)}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path
|
||||
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
|
||||
/>
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}"
|
||||
on:click={() => (show = true)}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import * as THREE from 'three'
|
||||
import { imu } from '$lib/stores/imu'
|
||||
import SceneBuilder from '$lib/sceneBuilder'
|
||||
|
||||
let canvas: HTMLCanvasElement = $state()
|
||||
let sceneBuilder: SceneBuilder
|
||||
let cube: THREE.Mesh
|
||||
let targetRotation = new THREE.Euler()
|
||||
let lastUpdateTime = 0
|
||||
const LERP_SPEED = 5 // rotations per second
|
||||
|
||||
const initThreeJS = () => {
|
||||
sceneBuilder = new SceneBuilder()
|
||||
.addRenderer({ canvas: canvas, antialias: true, alpha: true })
|
||||
.addPerspectiveCamera({ x: 2, y: 0, z: 2 })
|
||||
.addOrbitControls(1, 10, false)
|
||||
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
|
||||
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
|
||||
.fillParent()
|
||||
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: 0x00ff00,
|
||||
transparent: true,
|
||||
opacity: 0.8
|
||||
})
|
||||
cube = new THREE.Mesh(geometry, material)
|
||||
sceneBuilder.scene.add(cube)
|
||||
|
||||
sceneBuilder.addRenderCb(() => {
|
||||
if (!cube) return
|
||||
const currentTime = performance.now()
|
||||
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
|
||||
lastUpdateTime = currentTime
|
||||
|
||||
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
|
||||
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
|
||||
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
|
||||
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
|
||||
})
|
||||
|
||||
sceneBuilder.startRenderLoop()
|
||||
}
|
||||
|
||||
const updateOrientation = () => {
|
||||
if (!cube) return
|
||||
|
||||
const y = -$imu.x[$imu.x.length - 1] || 0
|
||||
const x = $imu.y[$imu.y.length - 1] || 0
|
||||
const z = -$imu.z[$imu.z.length - 1] || 0
|
||||
|
||||
targetRotation.set(
|
||||
THREE.MathUtils.degToRad(x),
|
||||
THREE.MathUtils.degToRad(y),
|
||||
THREE.MathUtils.degToRad(z)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initThreeJS()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
sceneBuilder?.renderer?.dispose()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if ($imu) {
|
||||
updateOrientation()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
|
||||
<canvas class="w-full h-full" bind:this={canvas}></canvas>
|
||||
</div>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts">
|
||||
import WiFi from '~icons/tabler/wifi';
|
||||
import WiFi0 from '~icons/tabler/wifi-0';
|
||||
import WiFi1 from '~icons/tabler/wifi-1';
|
||||
import WiFi2 from '~icons/tabler/wifi-2';
|
||||
import WifiOff from '~icons/tabler/wifi-off';
|
||||
|
||||
export let showDBm = true;
|
||||
export let rssi_dbm = 0;
|
||||
</script>
|
||||
|
||||
<div class="indicator">
|
||||
<div class="tooltip tooltip-left" data-tip={rssi_dbm + " dBm"}>
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi_dbm} dBm
|
||||
</span>
|
||||
{/if}
|
||||
{#if rssi_dbm >= -55}
|
||||
<WiFi class={$$props.class || ''} />
|
||||
{:else if rssi_dbm >= -75}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi2 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{:else if rssi_dbm >= -85}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi1 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{:else if rssi_dbm === 0}
|
||||
<WifiOff class={$$props.class || ''} />
|
||||
{:else}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi0 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,56 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Down from '~icons/tabler/chevron-down';
|
||||
export let open = true;
|
||||
export let collapsible = true;
|
||||
import { 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, 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">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
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 }}
|
||||
>
|
||||
<slot />
|
||||
</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">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
<slot />
|
||||
</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}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Loader from '~icons/tabler/loader-2';
|
||||
import { Loader } from "./icons";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center justify-center p-6">
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<script lang="ts">
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||
|
||||
const {
|
||||
icon,
|
||||
title,
|
||||
description = '',
|
||||
variant = 'primary',
|
||||
class: klass = '',
|
||||
children = null
|
||||
} = $props<{
|
||||
icon?: any
|
||||
title: string
|
||||
description?: string | number
|
||||
variant?: Variant
|
||||
class?: string
|
||||
children?: () => any
|
||||
}>()
|
||||
|
||||
const Icon = $derived(icon)
|
||||
|
||||
const variants: Record<Variant, [string, string]> = {
|
||||
success: ['bg-success', 'text-success-content'],
|
||||
error: ['bg-error', 'text-error-content'],
|
||||
primary: ['bg-primary', 'text-primary-content'],
|
||||
info: ['bg-info', 'text-info-content'],
|
||||
warning: ['bg-warning', 'text-warning-content']
|
||||
}
|
||||
|
||||
const variantKey: Variant = (variant as Variant) in variants ? (variant as Variant) : 'primary'
|
||||
const [bgColor, textColor] = variants[variantKey]
|
||||
</script>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2 {klass}">
|
||||
{#if icon}
|
||||
<div class="mask mask-hexagon {bgColor} h-auto w-10 flex-none">
|
||||
<Icon class="{textColor} h-auto w-full scale-75" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grow">
|
||||
<div class="font-bold">{title}</div>
|
||||
<div class="text-sm opacity-75 grow">{description}</div>
|
||||
</div>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,21 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { user } from '$lib/stores/user';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { location } from '$lib/stores';
|
||||
|
||||
const ws_token = `?access_token=${$user.bearer_token}`
|
||||
let source = $state(`${$location}/api/camera/stream`);
|
||||
|
||||
let source = "/api/camera/stream"+ ws_token;
|
||||
|
||||
onDestroy(() => {
|
||||
source = '#';
|
||||
});
|
||||
onDestroy(() => (source = '#'));
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
<img
|
||||
src={source}
|
||||
class="absolute object-cover blur-3xl w-full h-full -z-10"
|
||||
alt="Live stream is down"
|
||||
/>
|
||||
<img src={source} class="object-contain w-full h-full" alt="Live stream is down" />
|
||||
<img
|
||||
src={source}
|
||||
class="absolute object-cover blur-3xl w-full h-full -z-10"
|
||||
alt="Live stream is down"
|
||||
/>
|
||||
<img src={source} class="object-contain w-full h-full" alt="Live stream is down" />
|
||||
</div>
|
||||
|
||||
@@ -2,35 +2,33 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import error from '~icons/tabler/circle-x';
|
||||
import success from '~icons/tabler/circle-check';
|
||||
import warning from '~icons/tabler/alert-triangle';
|
||||
import info from '~icons/tabler/info-circle';
|
||||
import { error, info, success, warning } from './icons';
|
||||
|
||||
export let theme = {
|
||||
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let { theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
|
||||
export let icon = {
|
||||
}, icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
};
|
||||
} } = $props();
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{@const SvelteComponent = icon[notification.type]}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
|
||||
<SvelteComponent class="h-6 w-6 shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { openModal, closeAllModals } from 'svelte-modals';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Firmware from '~icons/tabler/refresh-alert';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import CloudDown from '~icons/tabler/cloud-download';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import type { GithubRelease } from '$lib/models';
|
||||
|
||||
export let update = false;
|
||||
|
||||
let firmwareVersion: string;
|
||||
let firmwareDownloadLink: string;
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
const result = await api.get<GithubRelease>(`https://api.github.com/repos/${$page.data.github}/releases/latest`, {headers})
|
||||
if (result.inner.message === "404" || result.inner.message == "Not Found") {
|
||||
console.warn('Error: Could not find releases in the repository');
|
||||
return
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
}
|
||||
|
||||
const results = result.inner;
|
||||
update = false;
|
||||
firmwareVersion = '';
|
||||
|
||||
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($page.data.features.firmware_built_target)
|
||||
) {
|
||||
update = true;
|
||||
firmwareVersion = results.tag_name;
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||
notifications.info('Firmware update available.', 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
||||
if (result.isErr()){
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) {
|
||||
await getGithubAPI();
|
||||
const interval = setInterval(
|
||||
async () => {
|
||||
await getGithubAPI();
|
||||
},
|
||||
60 * 60 * 1000
|
||||
); // once per hour
|
||||
}
|
||||
});
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
openModal(GithubUpdateDialog, {
|
||||
onConfirm: () => closeAllModals()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>{firmwareVersion}</span
|
||||
>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
{/if}
|
||||
@@ -1,308 +1,339 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { BufferGeometry, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, Object3D, SphereGeometry, Vector3, type NormalBufferAttributes, type Object3DEventMap } from 'three';
|
||||
import uzip from 'uzip';
|
||||
import { ModesEnum, kinematicData, mode, model, outControllerData, servoAnglesOut, servoAngles, mpu, jointNames } from '$lib/stores';
|
||||
import { footColor, isEmbeddedApp, throttler, toeWorldPositions } from '$lib/utilities';
|
||||
import { fileService } from '$lib/services';
|
||||
import SceneBuilder from '$lib/sceneBuilder';
|
||||
import { lerp, degToRad } from 'three/src/math/MathUtils';
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||
import Kinematic, { type body_state_t } from '$lib/kinematic';
|
||||
import {EightPhaseWalkState, FourPhaseWalkState, IdleState, RestState, StandState} from '$lib/gait'
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js';
|
||||
import type { URDFRobot } from 'urdf-loader';
|
||||
import { get } from 'svelte/store';
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
BufferGeometry,
|
||||
Line,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
Object3D,
|
||||
SphereGeometry,
|
||||
Vector3,
|
||||
type NormalBufferAttributes,
|
||||
type Object3DEventMap
|
||||
} from 'three'
|
||||
import {
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
model,
|
||||
outControllerData,
|
||||
servoAnglesOut,
|
||||
servoAngles,
|
||||
mpu,
|
||||
jointNames
|
||||
} from '$lib/stores'
|
||||
import {
|
||||
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 { radToDeg } from 'three/src/math/MathUtils.js'
|
||||
import type { URDFRobot } from 'urdf-loader'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export let sky = true
|
||||
export let orbit = false
|
||||
export let panel = true
|
||||
export let debug = false
|
||||
export let ground = true
|
||||
interface Props {
|
||||
sky?: boolean
|
||||
orbit?: boolean
|
||||
panel?: boolean
|
||||
debug?: boolean
|
||||
ground?: boolean
|
||||
}
|
||||
|
||||
let sceneManager = new SceneBuilder();
|
||||
let canvas: HTMLCanvasElement
|
||||
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
|
||||
|
||||
let currentModelAngles: number[] = new Array(12).fill(0);
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
let gui_panel: GUI
|
||||
let Throttler = new throttler()
|
||||
let sceneManager = $state(new SceneBuilder())
|
||||
let canvas: HTMLCanvasElement = $state()
|
||||
|
||||
let feet_trace = new Array(4).fill([]);
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
let target: Object3D<Object3DEventMap>;
|
||||
let currentModelAngles: number[] = new Array(12).fill(0)
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
let gui_panel: GUI
|
||||
let Throttler = new throttler()
|
||||
|
||||
let target_position = {x: 0, z: 0, yaw: 0}
|
||||
let feet_trace = new Array(4).fill([])
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
let target: Object3D<Object3DEventMap>
|
||||
|
||||
let kinematic = new Kinematic()
|
||||
let target_position = { x: 0, z: 0, yaw: 0 }
|
||||
|
||||
let planners = {
|
||||
[ModesEnum.Idle]: new IdleState(),
|
||||
[ModesEnum.Rest]: new RestState(),
|
||||
[ModesEnum.Stand]: new StandState(),
|
||||
[ModesEnum.Crawl]: new EightPhaseWalkState(),
|
||||
[ModesEnum.Walk]: new FourPhaseWalkState()
|
||||
let kinematic = new Kinematic()
|
||||
|
||||
let planners = {
|
||||
[ModesEnum.Deactivated]: new IdleState(),
|
||||
[ModesEnum.Idle]: new IdleState(),
|
||||
[ModesEnum.Calibration]: new CalibrationState(),
|
||||
[ModesEnum.Rest]: new RestState(),
|
||||
[ModesEnum.Stand]: new StandState(),
|
||||
[ModesEnum.Crawl]: new EightPhaseWalkState(),
|
||||
[ModesEnum.Walk]: new BezierState()
|
||||
}
|
||||
let lastTick = performance.now()
|
||||
|
||||
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
|
||||
|
||||
let body_state = {
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.5,
|
||||
zm: 0,
|
||||
feet: kinematic.getDefaultFeetPos()
|
||||
}
|
||||
|
||||
let settings = {
|
||||
'Internal kinematic': true,
|
||||
'Robot transform controls': false,
|
||||
'Auto orient robot': true,
|
||||
'Trace feet': debug,
|
||||
'Target position': false,
|
||||
'Trace points': 30,
|
||||
'Fix camera on robot': true,
|
||||
'Smooth motion': true,
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.7,
|
||||
zm: 0,
|
||||
Background: 'black'
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await populateModelCache()
|
||||
await createScene()
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
if (panel) createPanel()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
canvas.remove()
|
||||
gui_panel?.destroy()
|
||||
})
|
||||
|
||||
const updateAnglesFromStore = (angles: number[]) => {
|
||||
if (sceneManager.isDragging) return
|
||||
if (settings['Internal kinematic']) return
|
||||
modelTargetAngles = angles
|
||||
}
|
||||
|
||||
const createPanel = () => {
|
||||
gui_panel = new GUI({ width: 310 })
|
||||
gui_panel.close()
|
||||
gui_panel.domElement.id = 'three-gui-panel'
|
||||
|
||||
const general = gui_panel.addFolder('General')
|
||||
general.add(settings, 'Internal kinematic')
|
||||
general.add(settings, 'Robot transform controls')
|
||||
general.add(settings, 'Auto orient robot')
|
||||
|
||||
const kinematic = gui_panel.addFolder('Kinematics')
|
||||
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
|
||||
|
||||
const visibility = gui_panel.addFolder('Visualization')
|
||||
visibility.add(settings, 'Trace feet')
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
visibility.add(settings, 'Target position')
|
||||
visibility.add(settings, 'Smooth motion')
|
||||
visibility.addColor(settings, 'Background')
|
||||
}
|
||||
|
||||
const updateKinematicPosition = () => {
|
||||
kinematicData.set([
|
||||
settings.omega,
|
||||
settings.phi,
|
||||
settings.psi,
|
||||
settings.xm,
|
||||
settings.ym,
|
||||
settings.zm
|
||||
])
|
||||
}
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
|
||||
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
|
||||
}
|
||||
|
||||
const createScene = async () => {
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(8, 30, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model)
|
||||
.addTransformControls(sceneManager.model)
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
.startRenderLoop()
|
||||
|
||||
if (ground) sceneManager.addGroundPlane()
|
||||
|
||||
const geometry = new SphereGeometry(0.1, 32, 16)
|
||||
const material = new MeshBasicMaterial({ color: 0xffff00 })
|
||||
target = new Mesh(geometry, material)
|
||||
sceneManager.scene.add(target)
|
||||
|
||||
if (debug) {
|
||||
sceneManager.addDragControl(updateAngles)
|
||||
}
|
||||
let lastTick = performance.now()
|
||||
if (sky) sceneManager.addSky()
|
||||
|
||||
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry()
|
||||
const material = new LineBasicMaterial({ color: extractFootColor() })
|
||||
const line = new Line(geometry, material)
|
||||
trace_lines.push(geometry)
|
||||
sceneManager.scene.add(line)
|
||||
}
|
||||
}
|
||||
|
||||
let body_state = {
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.5,
|
||||
zm: 0,
|
||||
feet: planners[ModesEnum.Idle].default_feet_pos
|
||||
const renderTraceLines = (foot_positions: Vector3[]) => {
|
||||
if (!settings['Trace feet']) {
|
||||
if (!feet_trace.length) return
|
||||
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
|
||||
feet_trace = new Array(4).fill([])
|
||||
return
|
||||
}
|
||||
|
||||
let settings = {
|
||||
'Internal kinematic':false,
|
||||
'Robot transform controls':false,
|
||||
'Auto orient robot':true,
|
||||
'Trace feet':debug,
|
||||
'Trace points': 30,
|
||||
'Fix camera on robot': true,
|
||||
'Smooth motion': true,
|
||||
'omega': 0,
|
||||
'phi': 0,
|
||||
'psi': 0,
|
||||
'xm': 0,
|
||||
'ym': 0.7,
|
||||
'zm': 0,
|
||||
'Background': "black"
|
||||
trace_lines.forEach((line, i) => {
|
||||
feet_trace[i].push(foot_positions[i])
|
||||
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
|
||||
line.setFromPoints(feet_trace[i])
|
||||
})
|
||||
}
|
||||
|
||||
const calculate_kinematics = () => {
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const position: body_state_t = {
|
||||
omega: settings.omega,
|
||||
phi: settings.phi,
|
||||
psi: settings.psi,
|
||||
xm: settings.xm,
|
||||
ym: settings.ym,
|
||||
zm: settings.zm,
|
||||
feet: body_state.feet
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await cacheModelFiles()
|
||||
await createScene();
|
||||
if (!isEmbeddedApp && panel) createPanel();
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
});
|
||||
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
|
||||
modelTargetAngles = new_angles
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
canvas.remove()
|
||||
gui_panel?.destroy()
|
||||
});
|
||||
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
|
||||
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
|
||||
|
||||
const updateAnglesFromStore = (angles: number[]) => {
|
||||
if (sceneManager.isDragging) return
|
||||
if (settings['Internal kinematic']) return
|
||||
modelTargetAngles = angles;
|
||||
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
|
||||
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
|
||||
|
||||
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
|
||||
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
|
||||
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
|
||||
}
|
||||
|
||||
const update_camera = (robot: URDFRobot) => {
|
||||
if (!settings['Fix camera on robot']) return
|
||||
sceneManager.orbit.target = robot.position.clone()
|
||||
}
|
||||
|
||||
const smooth = (start: number, end: number, amount: number) => {
|
||||
return settings['Smooth motion'] ? lerp(start, end, amount) : end
|
||||
}
|
||||
|
||||
const update_gait = () => {
|
||||
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]
|
||||
}
|
||||
body_state.ym = ((data.h + 127) * 0.35) / 100
|
||||
|
||||
let planner = planners[get(mode)]
|
||||
const delta = performance.now() - lastTick
|
||||
lastTick = performance.now()
|
||||
|
||||
body_state = planner.step(body_state, data, delta)
|
||||
|
||||
settings.omega = body_state.omega
|
||||
settings.phi = body_state.phi
|
||||
settings.psi = body_state.psi
|
||||
settings.xm = body_state.xm
|
||||
settings.ym = body_state.ym
|
||||
settings.zm = body_state.zm
|
||||
}
|
||||
|
||||
const update_robot_position = (robot: URDFRobot) => {
|
||||
if (!settings['Robot transform controls']) return
|
||||
settings.omega = radToDeg(robot.rotation.y)
|
||||
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
|
||||
settings.psi = radToDeg(robot.rotation.x) + 90
|
||||
settings.xm = robot.position.z * 100
|
||||
settings.zm = -robot.position.x * 100
|
||||
}
|
||||
|
||||
const updateTargetPosition = () => {
|
||||
target.visible = settings['Target position']
|
||||
target.position.x = smooth(target.position.x, target_position.x, 0.5)
|
||||
target.position.z = smooth(target.position.z, target_position.z, 0.5)
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
const robot = sceneManager.model
|
||||
if (!robot) return
|
||||
|
||||
const toes = getToeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
update_camera(robot)
|
||||
update_gait()
|
||||
calculate_kinematics()
|
||||
update_robot_position(robot)
|
||||
|
||||
sceneManager.transformControl.showX = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showY = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showZ = settings['Robot transform controls']
|
||||
|
||||
for (let i = 0; i < $jointNames.length; i++) {
|
||||
currentModelAngles[i] = smooth(
|
||||
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
||||
modelTargetAngles[i],
|
||||
0.1
|
||||
)
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
|
||||
}
|
||||
|
||||
const createPanel = () => {
|
||||
gui_panel = new GUI({width: 310});
|
||||
gui_panel.close();
|
||||
gui_panel.domElement.id = 'three-gui-panel';
|
||||
|
||||
const general = gui_panel.addFolder('General');
|
||||
general.add(settings, 'Internal kinematic')
|
||||
general.add(settings, 'Robot transform controls')
|
||||
general.add(settings, 'Auto orient robot')
|
||||
|
||||
const kinematic = gui_panel.addFolder('Kinematics');
|
||||
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen();
|
||||
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen();
|
||||
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen();
|
||||
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
|
||||
|
||||
const visibility = gui_panel.addFolder('Visualization');
|
||||
visibility.add(settings, 'Trace feet')
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
visibility.add(settings, 'Smooth motion')
|
||||
visibility.addColor(settings, 'Background')
|
||||
}
|
||||
|
||||
const updateKinematicPosition = () => {
|
||||
kinematicData.set([
|
||||
settings.omega,
|
||||
settings.phi,
|
||||
settings.psi,
|
||||
settings.xm,
|
||||
settings.ym,
|
||||
settings.zm
|
||||
])
|
||||
}
|
||||
|
||||
const cacheModelFiles = async () => {
|
||||
let data = await fetch('/stl.zip').then((data) => data.arrayBuffer());
|
||||
|
||||
var files = uzip.parse(data);
|
||||
|
||||
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
|
||||
const url = new URL(path, window.location.href);
|
||||
fileService.saveFile(url.toString(), data);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
|
||||
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
|
||||
};
|
||||
|
||||
const createScene = async () => {
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(8, 30, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 0.9 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.6 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model)
|
||||
.addTransformControls(sceneManager.model)
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
.startRenderLoop();
|
||||
|
||||
if (ground) sceneManager
|
||||
.addGroundPlane()
|
||||
.addGridHelper({ size: 30, divisions: 25 })
|
||||
|
||||
|
||||
const geometry = new SphereGeometry(0.1, 32, 16 );
|
||||
const material = new MeshBasicMaterial( { color: 0xffff00 } );
|
||||
target = new Mesh(geometry, material);
|
||||
|
||||
if (debug) {
|
||||
sceneManager.scene.add(target);
|
||||
sceneManager.addDragControl(updateAngles)
|
||||
}
|
||||
if (sky) sceneManager.addSky()
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry();
|
||||
const material = new LineBasicMaterial({ color: footColor() });
|
||||
const line = new Line(geometry, material);
|
||||
trace_lines.push(geometry);
|
||||
sceneManager.scene.add(line);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTraceLines = (foot_positions: Vector3[]) => {
|
||||
if (!settings['Trace feet']) {
|
||||
if (!feet_trace.length) return
|
||||
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
|
||||
feet_trace = new Array(4).fill([])
|
||||
return
|
||||
}
|
||||
|
||||
trace_lines.forEach((line, i) => {
|
||||
feet_trace[i].push(foot_positions[i])
|
||||
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
|
||||
line.setFromPoints(feet_trace[i]);
|
||||
})
|
||||
}
|
||||
|
||||
const calculate_kinematics = () => {
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const position:body_state_t = {
|
||||
omega: settings.omega,
|
||||
phi: settings.phi,
|
||||
psi: settings.psi,
|
||||
xm: settings.xm,
|
||||
ym: settings.ym,
|
||||
zm: settings.zm,
|
||||
feet: body_state.feet
|
||||
}
|
||||
|
||||
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]));
|
||||
modelTargetAngles = new_angles;
|
||||
}
|
||||
|
||||
const orient_robot = (robot: URDFRobot, toes:Vector3[]) => {
|
||||
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
|
||||
|
||||
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1);
|
||||
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1);
|
||||
|
||||
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1);
|
||||
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1);
|
||||
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1);
|
||||
}
|
||||
|
||||
const update_camera = (robot:URDFRobot) => {
|
||||
if (!settings['Fix camera on robot']) return
|
||||
sceneManager.orbit.target = robot.position.clone()
|
||||
}
|
||||
|
||||
const smooth = (start:number, end:number, amount:number) => {
|
||||
return settings['Smooth motion'] ? lerp(start, end, amount) : end
|
||||
}
|
||||
|
||||
const update_gait = () => {
|
||||
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],
|
||||
};
|
||||
body_state.ym = ((data.h + 127) * 0.75) / 100;
|
||||
|
||||
let planner = planners[get(mode)]
|
||||
const delta = performance.now() - lastTick
|
||||
lastTick = performance.now()
|
||||
|
||||
body_state = planner.step(body_state, data, delta);
|
||||
|
||||
settings.omega = body_state.omega
|
||||
settings.phi = body_state.phi
|
||||
settings.psi = body_state.psi
|
||||
settings.xm = body_state.xm
|
||||
settings.ym = body_state.ym
|
||||
settings.zm = body_state.zm
|
||||
}
|
||||
|
||||
const update_robot_position = (robot:URDFRobot) => {
|
||||
if (!settings['Robot transform controls']) return
|
||||
settings.omega = radToDeg(robot.rotation.y)
|
||||
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading -90
|
||||
settings.psi = radToDeg(robot.rotation.x) + 90
|
||||
settings.xm = robot.position.z * 100
|
||||
settings.zm = -robot.position.x * 100
|
||||
}
|
||||
|
||||
const updateTargetPosition = () => {
|
||||
target.position.x = smooth(target.position.x, target_position.x, 0.5)
|
||||
target.position.z = smooth(target.position.z, target_position.z, 0.5)
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
const robot = sceneManager.model;
|
||||
if (!robot) return;
|
||||
|
||||
const toes = toeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
update_camera(robot)
|
||||
update_gait()
|
||||
calculate_kinematics()
|
||||
update_robot_position(robot)
|
||||
|
||||
sceneManager.transformControl.showX = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showY = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showZ = settings['Robot transform controls']
|
||||
|
||||
for (let i = 0; i < $jointNames.length; i++) {
|
||||
currentModelAngles[i] = smooth((robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI), modelTargetAngles[i], 0.1);
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
|
||||
}
|
||||
|
||||
orient_robot(robot, toes)
|
||||
updateTargetPosition();
|
||||
};
|
||||
|
||||
orient_robot(robot, toes)
|
||||
updateTargetPosition()
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={sceneManager.fillParent} />
|
||||
<svelte:window onresize={sceneManager.fillParent} />
|
||||
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
export { default as Connection } from '~icons/mdi/connection'
|
||||
export { default as Users } from '~icons/mdi/users'
|
||||
export { default as Settings } from '~icons/mdi/settings'
|
||||
export { default as MdiController } from '~icons/mdi/controller'
|
||||
export { default as Devices } from '~icons/mdi/devices'
|
||||
export { default as Camera } from '~icons/mdi/camera-outline'
|
||||
export { default as Rotate3d } from '~icons/mdi/rotate-3d'
|
||||
export { default as MotorOutline } from '~icons/mdi/motor-outline'
|
||||
export { default as Health } from '~icons/mdi/stethoscope'
|
||||
export { default as Folder } from '~icons/mdi/folder-outline'
|
||||
export { default as Update } from '~icons/mdi/reload'
|
||||
export { default as Router } from '~icons/mdi/router'
|
||||
export { default as AP } from '~icons/mdi/access-point'
|
||||
export { default as Remote } from '~icons/mdi/network'
|
||||
export { default as Copyright } from '~icons/mdi/copyright'
|
||||
export { default as NTP } from '~icons/mdi/clock-check'
|
||||
export { default as Metrics } from '~icons/mdi/report-bar'
|
||||
export { default as MdiEyeOutline } from '~icons/mdi/eye-outline'
|
||||
export { default as MdiEyeOffOutline } from '~icons/mdi/eye-off-outline'
|
||||
export { default as Github } from '~icons/mdi/github'
|
||||
export { default as Avatar } from '~icons/mdi/user-circle'
|
||||
export { default as Logout } from '~icons/mdi/logout'
|
||||
export { default as Record } from '~icons/mdi/radio-button-unchecked'
|
||||
export { default as MdiFullscreen } from '~icons/mdi/fullscreen'
|
||||
export { default as MdiFullscreenExit } from '~icons/mdi/fullscreen-exit'
|
||||
export { default as WiFi } from '~icons/tabler/wifi'
|
||||
export { default as WiFi0 } from '~icons/tabler/wifi-0'
|
||||
export { default as WiFi1 } from '~icons/tabler/wifi-1'
|
||||
export { default as WiFi2 } from '~icons/tabler/wifi-2'
|
||||
export { default as WifiOff } from '~icons/tabler/wifi-off'
|
||||
export { default as MdiWeatherSunny } from '~icons/mdi/weather-sunny'
|
||||
export { default as MdiMoonAndStars } from '~icons/mdi/moon-and-stars'
|
||||
export { default as Hamburger } from '~icons/mdi/hamburger-menu'
|
||||
|
||||
export { default as FileIcon } from '~icons/mdi/file'
|
||||
export { default as FolderIcon } from '~icons/mdi/folder-outline'
|
||||
export { default as FolderOpenOutline } from '~icons/mdi/folder-open-outline'
|
||||
export { default as 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'
|
||||
export { default as Check } from '~icons/tabler/check'
|
||||
export { default as Login } from '~icons/tabler/login'
|
||||
export { default as Loader } from '~icons/tabler/loader-2'
|
||||
export { default as error } from '~icons/tabler/circle-x'
|
||||
export { default as success } from '~icons/tabler/circle-check'
|
||||
export { default as warning } from '~icons/tabler/alert-triangle'
|
||||
export { default as info } from '~icons/tabler/info-circle'
|
||||
export { default as Power } from '~icons/tabler/power'
|
||||
|
||||
export { default as MAC } from '~icons/tabler/dna-2'
|
||||
export { default as Home } from '~icons/tabler/home'
|
||||
export { default as SSID } from '~icons/tabler/router'
|
||||
export { default as DNS } from '~icons/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/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'
|
||||
export { default as Reload } from '~icons/tabler/reload'
|
||||
|
||||
export { default as Firmware } from '~icons/tabler/refresh-alert'
|
||||
export { default as CloudDown } from '~icons/tabler/cloud-download'
|
||||
export { default as Server } from '~icons/tabler/server'
|
||||
export { default as Clock } from '~icons/tabler/clock'
|
||||
export { default as UTC } from '~icons/tabler/clock-pin'
|
||||
export { default as Stopwatch } from '~icons/tabler/24-hours'
|
||||
|
||||
export { default as CPU } from '~icons/tabler/cpu'
|
||||
export { default as CPP } from '~icons/tabler/binary'
|
||||
export { default as Sleep } from '~icons/tabler/zzz'
|
||||
export { default as FactoryReset } from '~icons/tabler/refresh-dot'
|
||||
export { default as Speed } from '~icons/tabler/activity'
|
||||
export { default as Flash } from '~icons/tabler/device-sd-card'
|
||||
export { default as Pyramid } from '~icons/tabler/pyramid'
|
||||
export { default as Sketch } from '~icons/tabler/chart-pie'
|
||||
export { default as Heap } from '~icons/tabler/box-model'
|
||||
export { default as Temperature } from '~icons/tabler/temperature'
|
||||
export { default as SDK } from '~icons/tabler/sdk'
|
||||
|
||||
export { default as Prerelease } from '~icons/tabler/test-pipe'
|
||||
export { default as Error } from '~icons/tabler/circle-x'
|
||||
|
||||
export { default as OTA } from '~icons/tabler/file-upload'
|
||||
export { default as Warning } from '~icons/tabler/alert-triangle'
|
||||
|
||||
export { default as AddUser } from '~icons/tabler/user-plus'
|
||||
export { default as Admin } from '~icons/tabler/key'
|
||||
export { default as Save } from '~icons/tabler/device-floppy'
|
||||
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons";
|
||||
|
||||
interface Props {
|
||||
show?: boolean;
|
||||
value?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props();
|
||||
|
||||
let type = $derived(show ? 'text' : 'password');
|
||||
|
||||
const handleInput = (e: any) => value = e.target.value
|
||||
|
||||
const togglePassword = () => show = !show
|
||||
</script>
|
||||
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
<input {type} class="grow" {value} oninput={handleInput} {id} />
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<div onclick={togglePassword} role="button" tabindex="0">
|
||||
<MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" />
|
||||
<MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" />
|
||||
</div>
|
||||
</label>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
value?: any
|
||||
oninput?: any
|
||||
}
|
||||
|
||||
let {
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
value = $bindable((max - min) / 2),
|
||||
...rest
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
style="writing-mode: vertical-lr; direction: rtl"
|
||||
class="cursor-pointer"
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
bind:value
|
||||
{...rest} />
|
||||
|
||||
<style>
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
background: oklch(var(--p) / 1);
|
||||
border-radius: var(--rounded-box, 1rem);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as PasswordInput } from './InputPassword.svelte';
|
||||
export { default as VerticalSlider } from './VerticalSlider.svelte';
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="box-border overflow-hidden flex-1">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
|
||||
interface Props {
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
|
||||
let { container }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}
|
||||
>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Github } from "../icons";
|
||||
|
||||
interface Props {
|
||||
github: any;
|
||||
}
|
||||
|
||||
let { github }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if github.active}
|
||||
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer">
|
||||
<Github class="h-5 w-5" />
|
||||
</a>
|
||||
{/if}
|
||||
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
|
||||
/** @type {{appName: any}} */
|
||||
let { appName } = $props();
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||
</a>
|
||||
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import GithubButton from '../menu/GithubButton.svelte'
|
||||
import LogoButton from '../menu/LogoButton.svelte'
|
||||
import MenuList from '../menu/MenuList.svelte'
|
||||
import {
|
||||
Connection,
|
||||
Settings,
|
||||
MdiController,
|
||||
Devices,
|
||||
Camera,
|
||||
Rotate3d,
|
||||
MotorOutline,
|
||||
Health,
|
||||
Folder,
|
||||
Update,
|
||||
WiFi,
|
||||
Router,
|
||||
AP,
|
||||
Copyright,
|
||||
Metrics,
|
||||
DNS
|
||||
} from '$lib/components/icons'
|
||||
import appEnv from 'app-env'
|
||||
|
||||
const features = useFeatureFlags()
|
||||
|
||||
const appName = page.data.app_name
|
||||
|
||||
const copyright = page.data.copyright
|
||||
|
||||
const github = { href: 'https://github.com/' + page.data.github, active: true }
|
||||
|
||||
type menuItem = {
|
||||
title: string
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
submenu?: menuItem[]
|
||||
}
|
||||
|
||||
let menuItems = $state<menuItem[]>([])
|
||||
|
||||
$effect(() => {
|
||||
menuItems = [
|
||||
{
|
||||
title: 'Connection',
|
||||
icon: WiFi,
|
||||
href: '/connection',
|
||||
feature: !appEnv.VITE_USE_HOST_NAME
|
||||
},
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: '/controller',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Peripherals',
|
||||
icon: Devices,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'I2C',
|
||||
icon: Connection,
|
||||
href: '/peripherals/i2c',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Camera',
|
||||
icon: Camera,
|
||||
href: '/peripherals/camera',
|
||||
feature: $features.camera
|
||||
},
|
||||
{
|
||||
title: 'Servo',
|
||||
icon: MotorOutline,
|
||||
href: '/peripherals/servo',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'IMU',
|
||||
icon: Rotate3d,
|
||||
href: '/peripherals/imu',
|
||||
feature: $features.imu || $features.mag || $features.bmp
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
icon: WiFi,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: '/wifi/sta',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: '/wifi/ap',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'mDNS',
|
||||
icon: DNS,
|
||||
href: '/wifi/mdns',
|
||||
feature: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
icon: Settings,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: '/system/status',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'File System',
|
||||
icon: Folder,
|
||||
href: '/system/filesystem',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: '/system/metrics',
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: '/system/update',
|
||||
feature: $features.ota || $features.upload_firmware || $features.download_firmware
|
||||
}
|
||||
]
|
||||
}
|
||||
] as menuItem[]
|
||||
})
|
||||
|
||||
const { menuClicked } = $props()
|
||||
|
||||
function setActiveMenuItem(targetTitle: string) {
|
||||
menuItems.forEach(item => {
|
||||
item.active = item.title === targetTitle
|
||||
item.submenu?.forEach(subItem => {
|
||||
subItem.active = subItem.title === targetTitle
|
||||
})
|
||||
})
|
||||
menuItems = menuItems
|
||||
menuClicked()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setActiveMenuItem(page.data.title)
|
||||
})
|
||||
|
||||
const updateMenu = (event: any) => {
|
||||
setActiveMenuItem(event.details)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
|
||||
<LogoButton {appName} />
|
||||
|
||||
<MenuList
|
||||
{menuItems}
|
||||
select={updateMenu}
|
||||
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
|
||||
level="0" />
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<GithubButton {github} />
|
||||
<div class="flex items-center justify-end text-sm gap-2">
|
||||
<Copyright class="h-4 w-4" />{copyright}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import MenuList from './MenuList.svelte'
|
||||
type MenuItem = {
|
||||
title: string
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
submenu?: MenuItem[]
|
||||
}
|
||||
|
||||
let { level, menuItems, select, class: klass } = $props()
|
||||
|
||||
const selectMenuItem = (title: string) => {
|
||||
select(title)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class={klass + ' menu 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="font-bold">
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
<div class="pl-4">
|
||||
<MenuList menuItems={menuItem.submenu} level={level + 1} {select} class={klass} />
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a
|
||||
href={menuItem.href}
|
||||
class="font-bold"
|
||||
class:bg-base-100={menuItem.active}
|
||||
class:text-lg={level === 0}
|
||||
class:text-md={level === 1}
|
||||
onclick={() => selectMenuItem(menuItem.title)}>
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { isFullscreen, toggleFullscreen } from '$lib/stores';
|
||||
import { MdiFullscreenExit, MdiFullscreen } from '../icons';
|
||||
|
||||
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
|
||||
</script>
|
||||
|
||||
<button onclick={toggleFullscreen}>
|
||||
<SvelteComponent class="h-7 w-7" />
|
||||
</button>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
|
||||
|
||||
interface Props {
|
||||
showDBm?: boolean;
|
||||
rssi?: number;
|
||||
}
|
||||
|
||||
let { showDBm = false, rssi = 0 }: Props = $props();
|
||||
|
||||
const getWiFiIcon = () => {
|
||||
if (rssi === 0) return WifiOff;
|
||||
if (rssi >= -55) return WiFi;
|
||||
if (rssi >= -75) return WiFi2;
|
||||
if (rssi >= -85) return WiFi1;
|
||||
return WiFi0;
|
||||
};
|
||||
|
||||
const SvelteComponent = $derived(getWiFiIcon());
|
||||
</script>
|
||||
|
||||
<div class="indicator">
|
||||
<div class="tooltip tooltip-left" data-tip={rssi + " dBm"}>
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi} dBm
|
||||
</span>
|
||||
{/if}
|
||||
<div class="h-7 w-7">
|
||||
<SvelteComponent class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, Power } from '../icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const postSleep = async () => await api.post('/api/system/sleep');
|
||||
|
||||
const confirmSleep = () => {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm Power Down',
|
||||
message: 'Are you sure you want to switch off the device?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Switch Off', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
postSleep();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if $features.sleep}
|
||||
<div class="flex-none">
|
||||
<button class="btn btn-square btn-ghost h-9 w-10" onclick={confirmSleep}>
|
||||
<Power class="text-error h-9 w-9" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { mode, modes } from "$lib/stores";
|
||||
|
||||
const deactivate = async () => {
|
||||
mode.set(modes.indexOf('deactivated'));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { MdiWeatherSunny, MdiMoonAndStars } from "../icons";
|
||||
</script>
|
||||
|
||||
<label class="swap swap-rotate">
|
||||
<input type="checkbox" value="light" class="theme-controller" />
|
||||
<MdiWeatherSunny class="swap-off h-7 w-7" />
|
||||
<MdiMoonAndStars class="swap-on h-7 w-7" />
|
||||
</label>
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import MdiHamburgerMenu from '~icons/mdi/hamburger-menu';
|
||||
import {Hamburger} from '../icons'
|
||||
</script>
|
||||
|
||||
<div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800">
|
||||
<div class="flex gap-2 p-2">
|
||||
<a href="/">
|
||||
<svelte:component this={MdiHamburgerMenu} class="h-8 w-8"/>
|
||||
<Hamburger class="h-8 w-8"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { modals } from 'svelte-modals';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/api';
|
||||
import type { GithubRelease } from '$lib/types/models';
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||
import { Cancel, CloudDown, Firmware } from '../icons';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
interface Props {
|
||||
update?: boolean;
|
||||
}
|
||||
|
||||
let { update = $bindable(false) }: Props = $props();
|
||||
|
||||
let firmwareVersion: string = $state('');
|
||||
let firmwareDownloadLink: string = $state('');
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
};
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
);
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository');
|
||||
return;
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
|
||||
const results = result.inner;
|
||||
update = false;
|
||||
firmwareVersion = '';
|
||||
|
||||
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target)
|
||||
) {
|
||||
update = true;
|
||||
firmwareVersion = results.tag_name;
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||
notifications.info('Firmware update available.', 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI();
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
|
||||
}
|
||||
});
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { selectedView, views } from "$lib/stores/application";
|
||||
import Selector from "../widget/Selector.svelte";
|
||||
</script>
|
||||
|
||||
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} />
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { telemetry } from '$lib/stores/telemetry'
|
||||
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte'
|
||||
import UpdateIndicator from '$lib/components/statusbar/UpdateIndicator.svelte'
|
||||
import SleepButton from './SleepButton.svelte'
|
||||
import ThemeButton from './ThemeButton.svelte'
|
||||
import FullscreenButton from './FullscreenButton.svelte'
|
||||
import StopButton from './StopButton.svelte'
|
||||
import ViewSelector from './ViewSelector.svelte'
|
||||
import { Hamburger } from '../icons'
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-300 sticky top-0 z-10 h-12 min-h-fit drop-shadow-lg lg:h-16 gap-2 pr-0">
|
||||
<div class="flex flex-1 gap-2">
|
||||
<label for="main-menu" class="btn btn-ghost btn-circle btn-sm drawer-button">
|
||||
<Hamburger class="h-6 w-auto" />
|
||||
</label>
|
||||
{#if page.data.title === 'Controller'}
|
||||
<ViewSelector />
|
||||
{:else}
|
||||
<h1 class="px-2 text-xl font-bold lg:text-2xl">{page.data.title}</h1>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<UpdateIndicator />
|
||||
|
||||
<FullscreenButton />
|
||||
|
||||
<ThemeButton />
|
||||
|
||||
<RssiIndicator rssi={$telemetry.rssi.rssi} />
|
||||
|
||||
<SleepButton />
|
||||
|
||||
<StopButton />
|
||||
</div>
|
||||
@@ -2,35 +2,33 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import error from '~icons/tabler/circle-x';
|
||||
import success from '~icons/tabler/circle-check';
|
||||
import warning from '~icons/tabler/alert-triangle';
|
||||
import info from '~icons/tabler/info-circle';
|
||||
import { error, info, success, warning } from '../icons';
|
||||
|
||||
export let theme = {
|
||||
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let { theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
|
||||
export let icon = {
|
||||
}, icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
};
|
||||
} } = $props();
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4 z-20">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{@const SvelteComponent = icon[notification.type]}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
|
||||
<SvelteComponent class="h-6 w-6 shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { daisyColor } from "$lib/utilities";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
import { onMount } from "svelte";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
let chartElement: HTMLCanvasElement = $state();
|
||||
let chart: Chart;
|
||||
|
||||
interface Props {
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
|
||||
let { label, data, title }: Props = $props();
|
||||
|
||||
Chart.register(...registerables);
|
||||
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
}, 500);
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<div class="w-full h-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:value={selectedOption}
|
||||
{...rest}
|
||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
+391
-186
@@ -1,242 +1,447 @@
|
||||
import type { body_state_t } from './kinematic';
|
||||
import { fromInt8 } from './utilities';
|
||||
import type { body_state_t } from './kinematic'
|
||||
import Kinematic from './kinematic'
|
||||
import { fromInt8 } from './utilities'
|
||||
|
||||
const { sin } = Math;
|
||||
const { sin } = Math
|
||||
|
||||
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;
|
||||
stop: 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 static body_state: body_state_t;
|
||||
protected dt = 0.02
|
||||
protected body_state!: body_state_t
|
||||
protected gait_state: gait_state_t = {
|
||||
step_height: 0.4,
|
||||
step_x: 0,
|
||||
step_z: 0,
|
||||
step_angle: 0,
|
||||
step_velocity: 1,
|
||||
step_depth: 0.002
|
||||
}
|
||||
|
||||
public get default_feet_pos() {
|
||||
return [
|
||||
[1, -1, 1, 1],
|
||||
[1, -1, -1, 1],
|
||||
[-1, -1, 1, 1],
|
||||
[-1, -1, -1, 1]
|
||||
];
|
||||
}
|
||||
public get default_feet_pos() {
|
||||
return new Kinematic().getDefaultFeetPos()
|
||||
}
|
||||
|
||||
protected get default_height() {
|
||||
return 0.5;
|
||||
}
|
||||
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) {
|
||||
return body_state;
|
||||
}
|
||||
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): gait_state_t {
|
||||
return {
|
||||
step_height: 0.4 + Math.abs(command.ry / 128),
|
||||
step_x: (Math.floor(fromInt8(command.ly, -1, 1) * 10) / 10) * 3,
|
||||
step_z: -(Math.floor(fromInt8(command.lx, -1, 1) * 10) / 10) * 3,
|
||||
step_velocity: command.s / 128 + 1,
|
||||
step_angle: 0,
|
||||
step_depth: 0.2
|
||||
};
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
export class IdleState extends GaitState {
|
||||
protected name = 'Idle';
|
||||
protected name = 'Idle'
|
||||
}
|
||||
|
||||
export class CalibrationState extends GaitState {
|
||||
protected name = 'Calibration'
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
body_state.omega = 0
|
||||
body_state.phi = 0
|
||||
body_state.psi = 0
|
||||
body_state.xm = 0
|
||||
body_state.ym = this.default_height * 10
|
||||
body_state.zm = 0
|
||||
body_state.feet = this.default_feet_pos
|
||||
return body_state
|
||||
}
|
||||
}
|
||||
|
||||
export class RestState extends GaitState {
|
||||
protected name = 'Rest';
|
||||
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;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 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[][];
|
||||
protected contact_phases!: number[][]
|
||||
protected shifts!: number[][]
|
||||
|
||||
protected body_state!: body_state_t;
|
||||
protected gait_state!: gait_state_t;
|
||||
protected dt = 0.02;
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
super.step(body_state, command, dt)
|
||||
this.update_phase()
|
||||
this.update_body_position()
|
||||
this.update_feet_positions()
|
||||
return this.body_state
|
||||
}
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
this.body_state = body_state;
|
||||
this.gait_state = this.map_command(command);
|
||||
this.dt = dt / 1000;
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
update_body_position() {
|
||||
if (this.num_phases === 4) return;
|
||||
const shift = this.shifts[Math.floor(this.phase / 2)]
|
||||
|
||||
const shift = this.shifts[Math.floor(this.phase / 2)];
|
||||
this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4
|
||||
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4
|
||||
}
|
||||
|
||||
this.body_state.xm += (shift[0] - this.body_state.xm) * this.dt * 4;
|
||||
this.body_state.zm += (shift[2] - this.body_state.zm) * this.dt * 4;
|
||||
}
|
||||
update_feet_positions() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.body_state.feet[i] = this.update_foot_position(i)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
]
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
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_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
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
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 = 2.5;
|
||||
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);
|
||||
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();
|
||||
}
|
||||
begin() {
|
||||
super.begin()
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end();
|
||||
}
|
||||
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, 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 = 1.5;
|
||||
protected contact_phases = [
|
||||
[1, 0, 1, 1, 1, 1, 1, 1],
|
||||
[1, 1, 1, 0, 1, 1, 1, 1],
|
||||
[1, 1, 1, 1, 1, 0, 1, 1],
|
||||
[1, 1, 1, 1, 1, 1, 1, 0]
|
||||
];
|
||||
protected shifts = [
|
||||
[-0.3, 0, -0.2],
|
||||
[-0.3, 0, 0.2],
|
||||
[0.3, 0, -0.2],
|
||||
[0.3, 0, 0.2]
|
||||
];
|
||||
protected swing_stand_ratio = 1 / (this.num_phases - 1);
|
||||
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();
|
||||
}
|
||||
begin() {
|
||||
super.begin()
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end();
|
||||
}
|
||||
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, dt: number = 0.02) {
|
||||
return super.step(body_state, command, dt)
|
||||
}
|
||||
}
|
||||
|
||||
export class BezierState extends GaitState {
|
||||
protected name = 'Bezier'
|
||||
protected phase = 0
|
||||
protected phase_num = 0
|
||||
protected step_length: number = 0
|
||||
offset = [0, 0.5, 0.5, 0]
|
||||
|
||||
begin() {
|
||||
super.begin()
|
||||
}
|
||||
|
||||
end() {
|
||||
super.end()
|
||||
}
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
|
||||
super.step(body_state, command, dt)
|
||||
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2)
|
||||
if (this.gait_state.step_x < 0) {
|
||||
this.step_length = -this.step_length
|
||||
}
|
||||
this.update_phase()
|
||||
this.update_feet_positions()
|
||||
return this.body_state
|
||||
}
|
||||
|
||||
update_phase() {
|
||||
this.phase += this.dt * this.gait_state.step_velocity * 2
|
||||
if (this.phase >= 1) {
|
||||
this.phase_num += 1
|
||||
this.phase_num %= 2
|
||||
this.phase = 0
|
||||
}
|
||||
}
|
||||
|
||||
update_feet_positions() {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.body_state.feet[i] = this.update_foot_position(i)
|
||||
}
|
||||
}
|
||||
|
||||
update_foot_position(index: number): number[] {
|
||||
let phase = this.phase + this.offset[index]
|
||||
if (phase >= 1) {
|
||||
phase -= 1
|
||||
}
|
||||
this.body_state.feet[index][0] = this.default_feet_pos[index][0]
|
||||
this.body_state.feet[index][1] = this.default_feet_pos[index][1]
|
||||
this.body_state.feet[index][2] = this.default_feet_pos[index][2]
|
||||
return phase <= 0.75 ?
|
||||
this.stand_controller(index, phase / 0.75)
|
||||
: this.swing_controller(index, (phase - 0.75) / (1 - 0.75))
|
||||
}
|
||||
|
||||
stand_controller(index: number, phase: number) {
|
||||
let depth = this.gait_state.step_depth
|
||||
return this.controller(index, phase, stance_curve, depth)
|
||||
}
|
||||
|
||||
swing_controller(index: number, phase: number) {
|
||||
let height = this.gait_state.step_height
|
||||
return this.controller(index, phase, bezier_curve, height)
|
||||
}
|
||||
|
||||
controller(
|
||||
index: number,
|
||||
phase: number,
|
||||
controller: (length: number, angle: number, ...args: number[]) => number[],
|
||||
...args: number[]
|
||||
) {
|
||||
let length = this.step_length / 2
|
||||
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2
|
||||
const delta_pos = controller(length, angle, ...args, phase)
|
||||
|
||||
length = this.gait_state.step_angle * 2
|
||||
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index])
|
||||
|
||||
const delta_rot = controller(length, angle, ...args, phase)
|
||||
|
||||
this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2
|
||||
this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2
|
||||
if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle)
|
||||
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2
|
||||
|
||||
return this.body_state.feet[index]
|
||||
}
|
||||
}
|
||||
|
||||
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
|
||||
const X_POLAR = Math.cos(angle)
|
||||
const Y_POLAR = Math.sin(angle)
|
||||
|
||||
const step = length * (1 - 2 * phase)
|
||||
const X = step * X_POLAR
|
||||
const Z = step * Y_POLAR
|
||||
let Y = 0
|
||||
|
||||
if (length !== 0) {
|
||||
Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length))
|
||||
}
|
||||
return [X, Y, Z]
|
||||
}
|
||||
|
||||
const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => {
|
||||
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2)
|
||||
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0])
|
||||
const offsets = [
|
||||
current_foot_pos[0] - default_foot_pos[0],
|
||||
current_foot_pos[2] - default_foot_pos[2],
|
||||
current_foot_pos[1] - default_foot_pos[1]
|
||||
]
|
||||
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2)
|
||||
const offset_mod = Math.atan2(offset_mag, foot_mag)
|
||||
|
||||
return Math.PI / 2.0 + foot_dir + offset_mod
|
||||
}
|
||||
|
||||
const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => {
|
||||
const control_points = get_control_points(length, angle, height)
|
||||
const n = control_points.length - 1
|
||||
|
||||
const point = [0, 0, 0]
|
||||
for (let i = 0; i <= n; i++) {
|
||||
const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i)
|
||||
point[0] += bernstein_poly * control_points[i][0]
|
||||
point[1] += bernstein_poly * control_points[i][1]
|
||||
point[2] += bernstein_poly * control_points[i][2]
|
||||
}
|
||||
return point
|
||||
}
|
||||
const get_control_points = (length: number, angle: number, height: number): number[][] => {
|
||||
const X_POLAR = Math.cos(angle)
|
||||
const Z_POLAR = Math.sin(angle)
|
||||
|
||||
const STEP = [
|
||||
-length,
|
||||
-length * 1.4,
|
||||
-length * 1.5,
|
||||
-length * 1.5,
|
||||
-length * 1.5,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
length * 1.5,
|
||||
length * 1.5,
|
||||
length * 1.4,
|
||||
length
|
||||
]
|
||||
|
||||
const Y = [
|
||||
0.0,
|
||||
0.0,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 0.9,
|
||||
height * 1.1,
|
||||
height * 1.1,
|
||||
height * 1.1,
|
||||
0.0,
|
||||
0.0
|
||||
]
|
||||
|
||||
const control_points: number[][] = []
|
||||
|
||||
for (let i = 0; i < STEP.length; i++) {
|
||||
const X = STEP[i] * X_POLAR
|
||||
const Z = STEP[i] * Z_POLAR
|
||||
control_points.push([X, Y[i], Z])
|
||||
}
|
||||
|
||||
return control_points
|
||||
}
|
||||
|
||||
const comb = (n: number, k: number): number => {
|
||||
if (k < 0 || k > n) return 0
|
||||
if (k === 0 || k === n) return 1
|
||||
k = Math.min(k, n - k)
|
||||
let c = 1
|
||||
for (let i = 0; i < k; i++) {
|
||||
c = (c * (n - i)) / (i + 1)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
+118
-307
@@ -1,320 +1,131 @@
|
||||
|
||||
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;
|
||||
const { cos, sin, atan2, acos, sqrt, max, min } = Math
|
||||
|
||||
const DEG2RAD = 0.017453292519943;
|
||||
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;
|
||||
}
|
||||
l1: number
|
||||
l2: number
|
||||
l3: number
|
||||
l4: number
|
||||
|
||||
L: number
|
||||
W: number
|
||||
|
||||
DEG2RAD = DEG2RAD
|
||||
|
||||
mountOffsets: number[][]
|
||||
|
||||
invMountRot = [
|
||||
[0, 0, -1],
|
||||
[0, 1, 0],
|
||||
[1, 0, 0]
|
||||
]
|
||||
|
||||
constructor() {
|
||||
this.l1 = 60.5 / 100
|
||||
this.l2 = 10 / 100
|
||||
this.l3 = 111.7 / 100
|
||||
this.l4 = 118.5 / 100
|
||||
|
||||
this.L = 207.5 / 100
|
||||
this.W = 78 / 100
|
||||
|
||||
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.l1 : this.l1)]
|
||||
})
|
||||
}
|
||||
|
||||
calcIK(p: body_state_t): number[] {
|
||||
const roll = p.omega * this.DEG2RAD
|
||||
const pitch = p.phi * this.DEG2RAD
|
||||
const yaw = p.psi * this.DEG2RAD
|
||||
const rot = this.euler2R(roll, pitch, yaw)
|
||||
const inv_rot = [
|
||||
[rot[0][0], rot[1][0], rot[2][0]],
|
||||
[rot[0][1], rot[1][1], rot[2][1]],
|
||||
[rot[0][2], rot[1][2], rot[2][2]]
|
||||
]
|
||||
const inv_trans = [
|
||||
-inv_rot[0][0] * p.xm - inv_rot[0][1] * p.ym - inv_rot[0][2] * p.zm,
|
||||
-inv_rot[1][0] * p.xm - inv_rot[1][1] * p.ym - inv_rot[1][2] * p.zm,
|
||||
-inv_rot[2][0] * p.xm - inv_rot[2][1] * p.ym - inv_rot[2][2] * p.zm
|
||||
]
|
||||
return p.feet.flatMap((foot, i) => {
|
||||
const [wx, wy, wz] = foot
|
||||
const bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0]
|
||||
const by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1]
|
||||
const bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2]
|
||||
|
||||
const [mx, my, mz] = this.mountOffsets[i]
|
||||
const px = bx - mx,
|
||||
py = by - my,
|
||||
pz = bz - mz
|
||||
|
||||
const lx =
|
||||
this.invMountRot[0][0] * px + this.invMountRot[0][1] * py + this.invMountRot[0][2] * pz
|
||||
const ly =
|
||||
this.invMountRot[1][0] * px + this.invMountRot[1][1] * py + this.invMountRot[1][2] * pz
|
||||
const lz =
|
||||
this.invMountRot[2][0] * px + this.invMountRot[2][1] * py + this.invMountRot[2][2] * pz
|
||||
|
||||
const xLocal = i % 2 === 1 ? -lx : lx
|
||||
return this.legIK(xLocal, ly, lz)
|
||||
})
|
||||
}
|
||||
|
||||
private legIK(x: number, y: number, z: number): [number, number, number] {
|
||||
const F = sqrt(max(0, x * x + y * y - this.l1 * this.l1))
|
||||
const G = F - this.l2
|
||||
const H = sqrt(G * G + z * z)
|
||||
const t1 = -atan2(y, x) - atan2(F, -this.l1)
|
||||
const D = (H * H - this.l3 * this.l3 - this.l4 * this.l4) / (2 * this.l3 * this.l4)
|
||||
const t3 = acos(max(-1, min(1, D)))
|
||||
const t2 = atan2(z, G) - atan2(this.l4 * sin(t3), this.l3 + this.l4 * cos(t3))
|
||||
return [t1, t2, t3]
|
||||
}
|
||||
|
||||
private euler2R(roll: number, pitch: number, yaw: number): number[][] {
|
||||
const cr = cos(roll),
|
||||
sr = sin(roll)
|
||||
const cp = cos(pitch),
|
||||
sp = sin(pitch)
|
||||
const cy = cos(yaw),
|
||||
sy = sin(yaw)
|
||||
return [
|
||||
[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]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
export type vector = { x: number; y: number };
|
||||
|
||||
export interface ControllerInput {
|
||||
left: vector;
|
||||
right: vector;
|
||||
height: number;
|
||||
speed: number;
|
||||
s1: number;
|
||||
}
|
||||
|
||||
export type GithubRelease = {
|
||||
message: string;
|
||||
tag_name: string;
|
||||
assets: Array<{
|
||||
name: string;
|
||||
browser_download_url: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type JWT = { access_token: string };
|
||||
|
||||
export type angles = number[] | Int16Array;
|
||||
|
||||
export type WifiStatus = {
|
||||
status: number;
|
||||
local_ip: string;
|
||||
mac_address: string;
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
subnet_mask: string;
|
||||
gateway_ip: string;
|
||||
dns_ip_1: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
|
||||
export type WifiSettings = {
|
||||
hostname: string;
|
||||
priority_RSSI: boolean;
|
||||
wifi_networks: NetworkItem[];
|
||||
};
|
||||
|
||||
export type NetworkList = {
|
||||
networks: NetworkItem[];
|
||||
};
|
||||
|
||||
export type KnownNetworkItem = {
|
||||
ssid: string;
|
||||
password: string;
|
||||
static_ip_config: boolean;
|
||||
local_ip?: string;
|
||||
subnet_mask?: string;
|
||||
gateway_ip?: string;
|
||||
dns_ip_1?: string;
|
||||
dns_ip_2?: string;
|
||||
};
|
||||
|
||||
export type NetworkItem = {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
bssid: string;
|
||||
channel: number;
|
||||
encryption_type: number;
|
||||
};
|
||||
|
||||
export type ApStatus = {
|
||||
status: number;
|
||||
ip_address: string;
|
||||
mac_address: string;
|
||||
station_num: number;
|
||||
};
|
||||
|
||||
export type ApSettings = {
|
||||
provision_mode: number;
|
||||
ssid: string;
|
||||
password: string;
|
||||
channel: number;
|
||||
ssid_hidden: boolean;
|
||||
max_clients: number;
|
||||
local_ip: string;
|
||||
gateway_ip: string;
|
||||
subnet_mask: string;
|
||||
};
|
||||
|
||||
export type LightState = {
|
||||
led_on: boolean;
|
||||
};
|
||||
|
||||
export type NTPStatus = {
|
||||
status: number;
|
||||
utc_time: string;
|
||||
local_time: string;
|
||||
server: string;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type NTPSettings = {
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
tz_label: string;
|
||||
tz_format: string;
|
||||
};
|
||||
|
||||
export type Analytics = {
|
||||
max_alloc_heap: number;
|
||||
psram_size: number;
|
||||
free_psram: number;
|
||||
free_heap: number;
|
||||
total_heap: number;
|
||||
min_free_heap: number;
|
||||
core_temp: number;
|
||||
fs_total: number;
|
||||
fs_used: number;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type StaticSystemInformation = {
|
||||
esp_platform: string;
|
||||
firmware_version: string;
|
||||
cpu_freq_mhz: number;
|
||||
cpu_type: string;
|
||||
cpu_rev: number;
|
||||
cpu_cores: number;
|
||||
sketch_size: number;
|
||||
free_sketch_space: number;
|
||||
sdk_version: string;
|
||||
arduino_version: string;
|
||||
flash_chip_size: number;
|
||||
flash_chip_speed: number;
|
||||
cpu_reset_reason: string;
|
||||
};
|
||||
|
||||
export type SystemInformation = Analytics & StaticSystemInformation;
|
||||
|
||||
export type CameraSettings = {
|
||||
framesize: number;
|
||||
quality: number;
|
||||
brightness: number;
|
||||
contrast: number;
|
||||
saturation: number;
|
||||
sharpness: number;
|
||||
denoise: number;
|
||||
special_effect: number;
|
||||
wb_mode: number;
|
||||
vflip: boolean;
|
||||
hmirror: boolean;
|
||||
};
|
||||
|
||||
export type File = number;
|
||||
|
||||
export interface Directory {
|
||||
[key: string]: File | Directory;
|
||||
}
|
||||
|
||||
export type Servo = {
|
||||
name: string;
|
||||
channel: number;
|
||||
inverted: boolean;
|
||||
angle: number;
|
||||
center_angle: number;
|
||||
};
|
||||
|
||||
export type ServoConfiguration = {
|
||||
is_active: boolean;
|
||||
servo_pwm_frequency: number;
|
||||
servo_oscillator_frequency: number;
|
||||
servos: Servo[];
|
||||
};
|
||||
+327
-299
@@ -1,29 +1,31 @@
|
||||
import {
|
||||
Mesh,
|
||||
PerspectiveCamera,
|
||||
PlaneGeometry,
|
||||
Scene,
|
||||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
DirectionalLight,
|
||||
PCFSoftShadowMap,
|
||||
GridHelper,
|
||||
ArrowHelper,
|
||||
Vector3,
|
||||
FogExp2,
|
||||
CanvasTexture,
|
||||
type ColorRepresentation,
|
||||
type WebGLRendererParameters,
|
||||
MeshPhongMaterial,
|
||||
EquirectangularReflectionMapping,
|
||||
ACESFilmicToneMapping,
|
||||
MathUtils,
|
||||
MeshStandardMaterial,
|
||||
Group
|
||||
Mesh,
|
||||
PerspectiveCamera,
|
||||
PlaneGeometry,
|
||||
Scene,
|
||||
WebGLRenderer,
|
||||
AmbientLight,
|
||||
DirectionalLight,
|
||||
PCFSoftShadowMap,
|
||||
type GridHelper,
|
||||
ArrowHelper,
|
||||
Vector3,
|
||||
FogExp2,
|
||||
CanvasTexture,
|
||||
type ColorRepresentation,
|
||||
type WebGLRendererParameters,
|
||||
MeshPhongMaterial,
|
||||
EquirectangularReflectionMapping,
|
||||
ACESFilmicToneMapping,
|
||||
MathUtils,
|
||||
Group,
|
||||
MeshBasicMaterial,
|
||||
RepeatWrapping
|
||||
} from 'three';
|
||||
import { Sky } from 'three/addons/objects/Sky.js';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls';
|
||||
import { Reflector } from 'three/examples/jsm/objects/Reflector.js';
|
||||
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader';
|
||||
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls';
|
||||
import { sunCalculator } from './utilities/position-utilities';
|
||||
@@ -31,321 +33,347 @@ import { sunCalculator } from './utilities/position-utilities';
|
||||
export const addScene = () => new Scene();
|
||||
|
||||
interface position {
|
||||
x?: number;
|
||||
y?: number;
|
||||
z?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
z?: number;
|
||||
}
|
||||
|
||||
interface light {
|
||||
color?: ColorRepresentation;
|
||||
intensity?: number;
|
||||
}
|
||||
|
||||
interface gridOptions {
|
||||
divisions?: number;
|
||||
size?: number;
|
||||
color?: ColorRepresentation;
|
||||
intensity?: number;
|
||||
}
|
||||
|
||||
interface arrowOptions {
|
||||
origin: position;
|
||||
direction: position;
|
||||
length?: number;
|
||||
color?: ColorRepresentation;
|
||||
origin: position;
|
||||
direction: position;
|
||||
length?: number;
|
||||
color?: ColorRepresentation;
|
||||
}
|
||||
|
||||
type directionalLight = position & light;
|
||||
|
||||
type gridHelperOptions = gridOptions & position;
|
||||
|
||||
export default class SceneBuilder {
|
||||
public scene: Scene;
|
||||
public camera!: PerspectiveCamera;
|
||||
public ground!: Mesh;
|
||||
public renderer!: WebGLRenderer;
|
||||
public orbit: OrbitControls;
|
||||
public callback: Function | undefined;
|
||||
public gridHelper!: GridHelper;
|
||||
public model!: URDFRobot;
|
||||
public liveStreamTexture!: CanvasTexture;
|
||||
private fog!: FogExp2;
|
||||
private isLoaded: boolean = false;
|
||||
public isDragging: boolean = false;
|
||||
highlightMaterial: any;
|
||||
sky!: Sky;
|
||||
transformControl: TransformControls;
|
||||
public modelGroup!: Group;
|
||||
public scene: Scene;
|
||||
public camera!: PerspectiveCamera;
|
||||
public ground!: Mesh;
|
||||
public renderer!: WebGLRenderer;
|
||||
public orbit: OrbitControls;
|
||||
public callback: Function | undefined;
|
||||
public gridHelper!: GridHelper;
|
||||
public model!: URDFRobot;
|
||||
public liveStreamTexture!: CanvasTexture;
|
||||
private fog!: FogExp2;
|
||||
private isLoaded: boolean = false;
|
||||
public isDragging: boolean = false;
|
||||
highlightMaterial: any;
|
||||
sky!: Sky;
|
||||
transformControl: TransformControls;
|
||||
public modelGroup!: Group;
|
||||
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
if (this.scene.environment?.mapping) {
|
||||
this.scene.environment.mapping = EquirectangularReflectionMapping;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
constructor() {
|
||||
this.scene = new Scene();
|
||||
if (this.scene.environment?.mapping) {
|
||||
this.scene.environment.mapping = EquirectangularReflectionMapping;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public addRenderer = (parameters?: WebGLRendererParameters) => {
|
||||
this.renderer = new WebGLRenderer(parameters);
|
||||
this.renderer.outputColorSpace = 'srgb';
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = PCFSoftShadowMap;
|
||||
this.renderer.toneMapping = ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = 0.85;
|
||||
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
|
||||
return this;
|
||||
};
|
||||
public addRenderer = (parameters?: WebGLRendererParameters) => {
|
||||
this.renderer = new WebGLRenderer(parameters);
|
||||
this.renderer.outputColorSpace = 'srgb';
|
||||
this.renderer.shadowMap.enabled = true;
|
||||
this.renderer.shadowMap.type = PCFSoftShadowMap;
|
||||
this.renderer.toneMapping = ACESFilmicToneMapping;
|
||||
this.renderer.toneMappingExposure = 0.85;
|
||||
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addSky = () => {
|
||||
this.sky = new Sky();
|
||||
this.sky.scale.setScalar(450000);
|
||||
this.scene.add(this.sky);
|
||||
const effectController = {
|
||||
turbidity: 10,
|
||||
rayleigh: 3,
|
||||
mieCoefficient: 0.005,
|
||||
mieDirectionalG: 0.7,
|
||||
elevation: sunCalculator.calculateSunElevation(),
|
||||
azimuth: 180,
|
||||
exposure: this.renderer.toneMappingExposure
|
||||
};
|
||||
const uniforms = this.sky.material.uniforms;
|
||||
uniforms['turbidity'].value = effectController.turbidity;
|
||||
uniforms['rayleigh'].value = effectController.rayleigh;
|
||||
uniforms['mieCoefficient'].value = effectController.mieCoefficient;
|
||||
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
|
||||
this.renderer.toneMappingExposure = 0.5;
|
||||
const phi = MathUtils.degToRad(90 - effectController.elevation);
|
||||
const theta = MathUtils.degToRad(effectController.azimuth);
|
||||
const sun = new Vector3();
|
||||
public addSky = () => {
|
||||
this.sky = new Sky();
|
||||
this.sky.scale.setScalar(450000);
|
||||
this.scene.add(this.sky);
|
||||
const effectController = {
|
||||
turbidity: 10,
|
||||
rayleigh: 3,
|
||||
mieCoefficient: 0.005,
|
||||
mieDirectionalG: 0.7,
|
||||
elevation: sunCalculator.calculateSunElevation(),
|
||||
azimuth: 200,
|
||||
exposure: this.renderer.toneMappingExposure
|
||||
};
|
||||
const uniforms = this.sky.material.uniforms;
|
||||
uniforms['turbidity'].value = effectController.turbidity;
|
||||
uniforms['rayleigh'].value = effectController.rayleigh;
|
||||
uniforms['mieCoefficient'].value = effectController.mieCoefficient;
|
||||
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
|
||||
this.renderer.toneMappingExposure = 0.5;
|
||||
const phi = MathUtils.degToRad(90 - effectController.elevation);
|
||||
const theta = MathUtils.degToRad(effectController.azimuth);
|
||||
const sun = new Vector3();
|
||||
|
||||
sun.setFromSphericalCoords(1, phi, theta);
|
||||
uniforms['sunPosition'].value.copy(sun);
|
||||
return this;
|
||||
};
|
||||
sun.setFromSphericalCoords(1, phi, theta);
|
||||
uniforms['sunPosition'].value.copy(sun);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addPerspectiveCamera = (options: position) => {
|
||||
this.camera = new PerspectiveCamera();
|
||||
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
|
||||
this.scene.add(this.camera);
|
||||
return this;
|
||||
};
|
||||
public addPerspectiveCamera = (options: position) => {
|
||||
this.camera = new PerspectiveCamera();
|
||||
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
|
||||
this.scene.add(this.camera);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addGroundPlane = (options?: position) => {
|
||||
var planeMaterial = new MeshStandardMaterial({ color: 0x808080, side: 2, opacity: 0.5 });
|
||||
this.ground = new Mesh(new PlaneGeometry(), planeMaterial);
|
||||
this.ground.rotation.x = -Math.PI / 2;
|
||||
this.ground.scale.setScalar(30);
|
||||
this.ground.position.set(options?.x ?? 0, options?.y ?? 0, options?.z ?? 0);
|
||||
this.ground.receiveShadow = true;
|
||||
this.scene.add(this.ground);
|
||||
return this;
|
||||
};
|
||||
public addGroundPlane = (options?: position) => {
|
||||
const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
|
||||
checkerboardTexture.wrapS = RepeatWrapping;
|
||||
checkerboardTexture.wrapT = RepeatWrapping;
|
||||
checkerboardTexture.repeat.set(100, 100);
|
||||
const checkerboardMat = new MeshBasicMaterial({
|
||||
map: checkerboardTexture,
|
||||
opacity: 0.1,
|
||||
transparent: true
|
||||
});
|
||||
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
|
||||
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.orbit.minDistance = minDistance;
|
||||
this.orbit.maxDistance = maxDistance;
|
||||
this.orbit.autoRotate = autoRotate;
|
||||
this.orbit.update();
|
||||
return this;
|
||||
};
|
||||
const plane = new PlaneGeometry(400, 400);
|
||||
|
||||
public addAmbientLight = (options: light) => {
|
||||
const ambientLight = new AmbientLight(options.color, options.intensity);
|
||||
this.scene.add(ambientLight);
|
||||
return this;
|
||||
};
|
||||
this.ground = new Mesh(plane, checkerboardMat);
|
||||
this.ground.rotation.x = -Math.PI / 2;
|
||||
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0);
|
||||
this.ground.receiveShadow = true;
|
||||
this.scene.add(this.ground);
|
||||
|
||||
public addDirectionalLight = (options: directionalLight) => {
|
||||
const directionalLight = new DirectionalLight(options.color, options.intensity);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.camera.top = 10;
|
||||
directionalLight.shadow.camera.bottom = -10;
|
||||
directionalLight.shadow.camera.right = 10;
|
||||
directionalLight.shadow.camera.left = -10;
|
||||
directionalLight.shadow.mapSize.set(4096, 4096);
|
||||
const mirror = new Reflector(plane, {
|
||||
clipBias: 0.003,
|
||||
textureWidth: window.innerWidth * window.devicePixelRatio,
|
||||
textureHeight: window.innerHeight * window.devicePixelRatio,
|
||||
color: 0x00bfff
|
||||
});
|
||||
mirror.rotateX(-Math.PI / 2);
|
||||
this.scene.add(mirror);
|
||||
|
||||
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
||||
this.scene.add(directionalLight);
|
||||
return this;
|
||||
};
|
||||
return this;
|
||||
};
|
||||
|
||||
public addGridHelper = (options: gridHelperOptions) => {
|
||||
this.gridHelper = new GridHelper(options.size, options.divisions);
|
||||
this.gridHelper.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
||||
this.gridHelper.material.opacity = 0.2;
|
||||
this.gridHelper.material.depthWrite = false;
|
||||
this.gridHelper.material.transparent = true;
|
||||
this.scene.add(this.gridHelper);
|
||||
return this;
|
||||
};
|
||||
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
|
||||
this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.orbit.minDistance = minDistance;
|
||||
this.orbit.maxDistance = maxDistance;
|
||||
this.orbit.autoRotate = autoRotate;
|
||||
this.orbit.update();
|
||||
return this;
|
||||
};
|
||||
|
||||
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
|
||||
this.scene.fog = new FogExp2(color, density);
|
||||
return this;
|
||||
};
|
||||
public addAmbientLight = (options: light) => {
|
||||
const ambientLight = new AmbientLight(options.color, options.intensity);
|
||||
this.scene.add(ambientLight);
|
||||
return this;
|
||||
};
|
||||
|
||||
public fillParent = () => {
|
||||
const parentElement = this.renderer.domElement.parentElement;
|
||||
if (parentElement) {
|
||||
const width = parentElement.clientWidth;
|
||||
const height = parentElement.clientHeight;
|
||||
this.handleResize(width, height);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
public addDirectionalLight = (options: directionalLight) => {
|
||||
const directionalLight = new DirectionalLight(options.color, options.intensity);
|
||||
directionalLight.castShadow = true;
|
||||
directionalLight.shadow.camera.top = 10;
|
||||
directionalLight.shadow.camera.bottom = -10;
|
||||
directionalLight.shadow.camera.right = 10;
|
||||
directionalLight.shadow.camera.left = -10;
|
||||
directionalLight.shadow.mapSize.set(4096, 4096);
|
||||
|
||||
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
|
||||
this.renderer.setSize(width, height);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
return this;
|
||||
};
|
||||
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
|
||||
this.scene.add(directionalLight);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addRenderCb = (callback: Function) => {
|
||||
this.callback = callback;
|
||||
return this;
|
||||
};
|
||||
private createCheckerboardTexture = (size: number, squares: number) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
public startRenderLoop = () => {
|
||||
this.renderer.setAnimationLoop(() => {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
this.orbit.update();
|
||||
this.handleRobotShadow();
|
||||
if (this.callback) this.callback();
|
||||
if (!this.liveStreamTexture) return;
|
||||
});
|
||||
return this;
|
||||
};
|
||||
const squareSize = size / squares;
|
||||
|
||||
public addArrowHelper = (options?: arrowOptions) => {
|
||||
const dir = new Vector3(
|
||||
options?.direction.x ?? 0,
|
||||
options?.direction.y ?? 0,
|
||||
options?.direction.z ?? 0
|
||||
);
|
||||
const origin = new Vector3(
|
||||
options?.origin.x ?? 0,
|
||||
options?.origin.y ?? 0,
|
||||
options?.origin.z ?? 0
|
||||
);
|
||||
const arrowHelper = new ArrowHelper(
|
||||
dir,
|
||||
origin,
|
||||
options?.length ?? 1.5,
|
||||
options?.color ?? 0xff0000
|
||||
);
|
||||
this.scene.add(arrowHelper);
|
||||
return this;
|
||||
};
|
||||
for (let y = 0; y < squares; y++) {
|
||||
for (let x = 0; x < squares; x++) {
|
||||
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
|
||||
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
|
||||
}
|
||||
}
|
||||
|
||||
private setJointValue(jointName: string, angle: number) {
|
||||
if (!this.model) return;
|
||||
if (!this.model.joints[jointName]) return;
|
||||
this.model.joints[jointName].setJointValue(angle);
|
||||
}
|
||||
const texture = new CanvasTexture(canvas);
|
||||
texture.wrapS = texture.wrapT = RepeatWrapping;
|
||||
texture.anisotropy = 16;
|
||||
return texture;
|
||||
};
|
||||
|
||||
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
|
||||
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
|
||||
this.scene.fog = new FogExp2(color, density);
|
||||
return this;
|
||||
};
|
||||
|
||||
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
|
||||
const traverse = (c: any) => {
|
||||
if (c.type === 'Mesh') {
|
||||
if (revert) {
|
||||
c.material = c.__origMaterial;
|
||||
delete c.__origMaterial;
|
||||
} else {
|
||||
c.__origMaterial = c.material;
|
||||
c.material = material;
|
||||
}
|
||||
}
|
||||
public fillParent = () => {
|
||||
const parentElement = this.renderer.domElement.parentElement;
|
||||
if (parentElement) {
|
||||
const width = parentElement.clientWidth;
|
||||
const height = parentElement.clientHeight;
|
||||
this.handleResize(width, height);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
if (c === m || !this.isJoint(c)) {
|
||||
for (let i = 0; i < c.children.length; i++) {
|
||||
const child = c.children[i];
|
||||
if (!child.isURDFCollider) {
|
||||
traverse(c.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
traverse(m);
|
||||
};
|
||||
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
|
||||
this.renderer.setSize(width, height);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
return this;
|
||||
};
|
||||
|
||||
public addTransformControls = (model: any) => {
|
||||
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
|
||||
this.transformControl.addEventListener('dragging-changed', (event: any) => {
|
||||
this.orbit.enabled = !event.value;
|
||||
this.isDragging = !event.value;
|
||||
});
|
||||
this.transformControl.attach(model);
|
||||
this.scene.add(this.transformControl);
|
||||
this.transformControl.setMode('rotate');
|
||||
return this;
|
||||
};
|
||||
public addRenderCb = (callback: Function) => {
|
||||
this.callback = callback;
|
||||
return this;
|
||||
};
|
||||
|
||||
public addModel = (model: any) => {
|
||||
this.modelGroup = new Group();
|
||||
this.modelGroup.add(model);
|
||||
this.model = model;
|
||||
this.scene.add(this.modelGroup);
|
||||
return this;
|
||||
};
|
||||
public startRenderLoop = () => {
|
||||
this.renderer.setAnimationLoop(() => {
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
this.orbit.update();
|
||||
this.handleRobotShadow();
|
||||
if (this.callback) this.callback();
|
||||
if (!this.liveStreamTexture) return;
|
||||
});
|
||||
return this;
|
||||
};
|
||||
|
||||
public addDragControl = (updateAngle: any) => {
|
||||
const highlightColor = '#FFFFFF';
|
||||
const highlightMaterial = new MeshPhongMaterial({
|
||||
shininess: 10,
|
||||
color: highlightColor,
|
||||
emissive: highlightColor,
|
||||
emissiveIntensity: 0.25
|
||||
});
|
||||
public addArrowHelper = (options?: arrowOptions) => {
|
||||
const dir = new Vector3(
|
||||
options?.direction.x ?? 0,
|
||||
options?.direction.y ?? 0,
|
||||
options?.direction.z ?? 0
|
||||
);
|
||||
const origin = new Vector3(
|
||||
options?.origin.x ?? 0,
|
||||
options?.origin.y ?? 0,
|
||||
options?.origin.z ?? 0
|
||||
);
|
||||
const arrowHelper = new ArrowHelper(
|
||||
dir,
|
||||
origin,
|
||||
options?.length ?? 1.5,
|
||||
options?.color ?? 0xff0000
|
||||
);
|
||||
this.scene.add(arrowHelper);
|
||||
return this;
|
||||
};
|
||||
|
||||
const dragControls = new PointerURDFDragControls(
|
||||
this.scene,
|
||||
this.camera,
|
||||
this.renderer.domElement
|
||||
);
|
||||
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
|
||||
this.setJointValue(joint.name, angle);
|
||||
updateAngle(joint.name, angle);
|
||||
};
|
||||
dragControls.onDragStart = () => {
|
||||
this.orbit.enabled = false;
|
||||
this.isDragging = true;
|
||||
};
|
||||
dragControls.onDragEnd = () => {
|
||||
this.orbit.enabled = true;
|
||||
this.isDragging = false;
|
||||
};
|
||||
dragControls.onHover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, false, highlightMaterial);
|
||||
dragControls.onUnhover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, true, highlightMaterial);
|
||||
private setJointValue(jointName: string, angle: number) {
|
||||
if (!this.model) return;
|
||||
if (!this.model.joints[jointName]) return;
|
||||
this.model.joints[jointName].setJointValue(angle);
|
||||
}
|
||||
|
||||
this.renderer.domElement.addEventListener('touchstart', (data) =>
|
||||
dragControls._mouseDown(data.touches[0])
|
||||
);
|
||||
this.renderer.domElement.addEventListener('touchmove', (data) =>
|
||||
dragControls._mouseMove(data.touches[0])
|
||||
);
|
||||
this.renderer.domElement.addEventListener('touchend', (data) =>
|
||||
dragControls._mouseUp(data.touches[0])
|
||||
);
|
||||
return this;
|
||||
};
|
||||
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
|
||||
|
||||
public toggleFog = () => {
|
||||
this.scene.fog = this.scene.fog ? null : this.fog;
|
||||
};
|
||||
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
|
||||
const traverse = (c: any) => {
|
||||
if (c.type === 'Mesh') {
|
||||
if (revert) {
|
||||
c.material = c.__origMaterial;
|
||||
delete c.__origMaterial;
|
||||
} else {
|
||||
c.__origMaterial = c.material;
|
||||
c.material = material;
|
||||
}
|
||||
}
|
||||
|
||||
private handleRobotShadow = () => {
|
||||
if (this.isLoaded) return;
|
||||
const intervalId = setInterval(() => {
|
||||
this.model?.traverse((c) => (c.castShadow = true));
|
||||
}, 10);
|
||||
setTimeout(() => {
|
||||
clearInterval(intervalId);
|
||||
}, 1000);
|
||||
this.isLoaded = true;
|
||||
};
|
||||
if (c === m || !this.isJoint(c)) {
|
||||
for (let i = 0; i < c.children.length; i++) {
|
||||
const child = c.children[i];
|
||||
if (!child.isURDFCollider) {
|
||||
traverse(c.children[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
traverse(m);
|
||||
};
|
||||
|
||||
public addTransformControls = (model: any) => {
|
||||
this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
|
||||
this.transformControl.addEventListener('dragging-changed', (event: any) => {
|
||||
this.orbit.enabled = !event.value;
|
||||
this.isDragging = !event.value;
|
||||
});
|
||||
this.transformControl.attach(model);
|
||||
this.scene.add(this.transformControl);
|
||||
this.transformControl.setMode('rotate');
|
||||
return this;
|
||||
};
|
||||
|
||||
public addModel = (model: any) => {
|
||||
this.modelGroup = new Group();
|
||||
this.modelGroup.add(model);
|
||||
this.model = model;
|
||||
this.scene.add(this.modelGroup);
|
||||
return this;
|
||||
};
|
||||
|
||||
public addDragControl = (updateAngle: any) => {
|
||||
const highlightColor = '#FFFFFF';
|
||||
const highlightMaterial = new MeshPhongMaterial({
|
||||
shininess: 10,
|
||||
color: highlightColor,
|
||||
emissive: highlightColor,
|
||||
emissiveIntensity: 0.9
|
||||
});
|
||||
|
||||
const dragControls = new PointerURDFDragControls(
|
||||
this.scene,
|
||||
this.camera,
|
||||
this.renderer.domElement
|
||||
);
|
||||
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
|
||||
this.setJointValue(joint.name, angle);
|
||||
updateAngle(joint.name, angle);
|
||||
};
|
||||
dragControls.onDragStart = () => {
|
||||
this.orbit.enabled = false;
|
||||
this.isDragging = true;
|
||||
};
|
||||
dragControls.onDragEnd = () => {
|
||||
this.orbit.enabled = true;
|
||||
this.isDragging = false;
|
||||
};
|
||||
dragControls.onHover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, false, highlightMaterial);
|
||||
dragControls.onUnhover = (joint: URDFMimicJoint) =>
|
||||
this.highlightLinkGeometry(joint, true, highlightMaterial);
|
||||
|
||||
this.renderer.domElement.addEventListener(
|
||||
'touchstart',
|
||||
data => dragControls._mouseDown(data.touches[0]),
|
||||
{ passive: true }
|
||||
);
|
||||
this.renderer.domElement.addEventListener(
|
||||
'touchmove',
|
||||
data => dragControls._mouseMove(data.touches[0]),
|
||||
{ passive: true }
|
||||
);
|
||||
this.renderer.domElement.addEventListener(
|
||||
'touchend',
|
||||
data => dragControls._mouseUp(data.touches[0]),
|
||||
{ passive: true }
|
||||
);
|
||||
return this;
|
||||
};
|
||||
|
||||
public toggleFog = () => {
|
||||
this.scene.fog = this.scene.fog ? null : this.fog;
|
||||
};
|
||||
|
||||
private handleRobotShadow = () => {
|
||||
if (this.isLoaded) return;
|
||||
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10);
|
||||
setTimeout(() => clearInterval(intervalId), 1000);
|
||||
this.isLoaded = true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,51 +1,38 @@
|
||||
import { Result } from '$lib/utilities/result';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
class FileService {
|
||||
private dbName = 'fileStorageDB';
|
||||
private dbVersion = 1;
|
||||
private storeName = 'files';
|
||||
private dbPromise: Promise<Result<IDBDatabase, string>>;
|
||||
|
||||
constructor() {
|
||||
this.dbPromise = this.openDatabase();
|
||||
}
|
||||
private dbPromise: Promise<Result<IDBDatabase, string>> | null = browser
|
||||
? this.openDatabase()
|
||||
: null;
|
||||
|
||||
private async openDatabase(): Promise<Result<IDBDatabase, string>> {
|
||||
return new Promise((resolve) => {
|
||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||
const request = indexedDB.open('fileStorageDB', 1);
|
||||
|
||||
request.onerror = () => resolve(Result.err('Error opening database'));
|
||||
|
||||
request.onsuccess = () => resolve(Result.ok(request.result));
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = request.result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName);
|
||||
}
|
||||
request.onupgradeneeded = () => {
|
||||
request.result.createObjectStore('files');
|
||||
};
|
||||
request.onsuccess = () => resolve(Result.ok(request.result));
|
||||
request.onerror = () => resolve(Result.err('Error opening database'));
|
||||
});
|
||||
}
|
||||
|
||||
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
|
||||
if (!browser || !this.dbPromise)
|
||||
return Result.err('Not running in browser or DB not initialized');
|
||||
const dbResult = await this.dbPromise;
|
||||
if (dbResult.isErr()) {
|
||||
return Result.err('Database not initialized properly');
|
||||
}
|
||||
const db = dbResult.inner;
|
||||
const transaction = db.transaction(this.storeName, mode);
|
||||
return Result.ok(transaction.objectStore(this.storeName));
|
||||
if (dbResult.isErr()) return Result.err('Database not initialized');
|
||||
const store = dbResult.inner.transaction('files', mode).objectStore('files');
|
||||
return Result.ok(store);
|
||||
}
|
||||
|
||||
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
|
||||
const storeResult = await this.getStore('readwrite');
|
||||
if (storeResult.isErr()) {
|
||||
return Result.err('Failed to access object store for writing');
|
||||
}
|
||||
const store = storeResult.inner;
|
||||
if (storeResult.isErr()) return Result.err('Failed to access store');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const request = store.put(file, key);
|
||||
const request = storeResult.inner.put(file, key);
|
||||
request.onsuccess = () => resolve(Result.ok(request.result));
|
||||
request.onerror = () => resolve(Result.err('Failed to save file'));
|
||||
});
|
||||
@@ -53,19 +40,15 @@ class FileService {
|
||||
|
||||
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
|
||||
const storeResult = await this.getStore('readonly');
|
||||
if (storeResult.isErr()) {
|
||||
return Result.err('Failed to access object store for reading');
|
||||
}
|
||||
const store = storeResult.inner;
|
||||
if (storeResult.isErr()) return Result.err('Failed to access store');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const request = store.get(key);
|
||||
|
||||
const request = storeResult.inner.get(key);
|
||||
request.onsuccess = () =>
|
||||
resolve(request.result ? Result.ok(request.result) : Result.err('File content not found'));
|
||||
resolve(request.result ? Result.ok(request.result) : Result.err('File not found'));
|
||||
request.onerror = () => resolve(Result.err('Failed to retrieve file'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new FileService();
|
||||
export default browser ? new FileService() : null;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
|
||||
import Visualization from '$lib/components/Visualization.svelte';
|
||||
import Stream from '$lib/components/Stream.svelte';
|
||||
import ChartWidget from '$lib/components/widget/ChartWidget.svelte';
|
||||
|
||||
export interface WidgetConfig {
|
||||
id: string | number;
|
||||
component: keyof typeof WidgetComponents;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface WidgetContainerConfig {
|
||||
id: string | number;
|
||||
layout?: 'row' | 'column' | 'wrap';
|
||||
header?: string;
|
||||
widgets: Array<WidgetConfig | WidgetContainerConfig>;
|
||||
}
|
||||
|
||||
export const isWidgetConfig = (
|
||||
widget: WidgetConfig | WidgetContainerConfig
|
||||
): widget is WidgetConfig => 'component' in widget;
|
||||
|
||||
export const WidgetComponents = {
|
||||
Visualization,
|
||||
Stream,
|
||||
ChartWidget
|
||||
};
|
||||
|
||||
interface View {
|
||||
name: string;
|
||||
content: WidgetContainerConfig;
|
||||
}
|
||||
|
||||
const defaultViews: View[] = [
|
||||
{
|
||||
name: 'Stream',
|
||||
content: {
|
||||
id: 'root',
|
||||
layout: 'column',
|
||||
widgets: [{ id: 2, component: 'Stream' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '3D representation',
|
||||
content: {
|
||||
id: 'root',
|
||||
layout: 'column',
|
||||
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Split screen',
|
||||
content: {
|
||||
id: 'root',
|
||||
widgets: [
|
||||
{ id: 2, component: 'Stream' },
|
||||
{ id: 2, component: 'Visualization', props: { debug: true } }
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
export const views: Writable<View[]> = persistentStore('views', defaultViews);
|
||||
|
||||
export const selectedView = persistentStore('selected_view', get(views)[0].name);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { api } from '$lib/api';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
let featureFlagsStore: Writable<Record<string, boolean>>;
|
||||
|
||||
export function useFeatureFlags() {
|
||||
if (!featureFlagsStore) {
|
||||
featureFlagsStore = writable<Record<string, boolean>>({});
|
||||
|
||||
api.get<Record<string, boolean>>('/api/features').then((result) => {
|
||||
if (result.isOk()) featureFlagsStore.set(result.inner);
|
||||
else {
|
||||
notifications.error('Feature flag could not be fetched', 2500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return featureFlagsStore;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isFullscreen = writable(false);
|
||||
|
||||
export function toggleFullscreen() {
|
||||
isFullscreen.update((state) => {
|
||||
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen();
|
||||
return !state;
|
||||
});
|
||||
}
|
||||
|
||||
export function enterFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
isFullscreen.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function exitFullscreen() {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen();
|
||||
isFullscreen.set(false);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
+21
-30
@@ -1,36 +1,27 @@
|
||||
import { type IMU } from '$lib/types/models';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let imu_data = {
|
||||
x: <number[]>[],
|
||||
y: <number[]>[],
|
||||
z: <number[]>[],
|
||||
imu_temp: <number[]>[],
|
||||
altitude: <number[]>[],
|
||||
pressure: <number[]>[],
|
||||
bmp_temp: <number[]>[]
|
||||
};
|
||||
import type { IMU } from '$lib/types/models';
|
||||
|
||||
const maxIMUData = 100;
|
||||
|
||||
function createIMU() {
|
||||
const { subscribe, update } = writable(imu_data);
|
||||
export const imu = (() => {
|
||||
const { subscribe, update } = writable({
|
||||
x: [] as number[],
|
||||
y: [] as number[],
|
||||
z: [] as number[],
|
||||
heading: [] as number[],
|
||||
altitude: [] as number[],
|
||||
pressure: [] as number[],
|
||||
bmp_temp: [] as number[]
|
||||
});
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
addData: (content: IMU) => {
|
||||
update((imu_data) => ({
|
||||
...imu_data,
|
||||
x: [...imu_data.x, content.x].slice(-maxIMUData),
|
||||
y: [...imu_data.y, content.y].slice(-maxIMUData),
|
||||
z: [...imu_data.z, content.z].slice(-maxIMUData),
|
||||
imu_temp: [...imu_data.imu_temp, content.imu_temp].slice(-maxIMUData),
|
||||
altitude: [...imu_data.altitude, content.altitude].slice(-maxIMUData),
|
||||
pressure: [...imu_data.pressure, content.pressure].slice(-maxIMUData),
|
||||
bmp_temp: [...imu_data.bmp_temp, content.bmp_temp].slice(-maxIMUData)
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
const addData = (content: IMU) => {
|
||||
update(data => {
|
||||
(Object.keys(content) as (keyof IMU)[]).forEach(key => {
|
||||
data[key] = [...data[key], content[key]].slice(-maxIMUData);
|
||||
});
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
export const imu = createIMU();
|
||||
return { subscribe, addData };
|
||||
})();
|
||||
|
||||
@@ -2,3 +2,8 @@ export * from './socket-store';
|
||||
export * from './logging-store';
|
||||
export * from './model-store';
|
||||
export * from './socket';
|
||||
export * from './fullscreen';
|
||||
export * from './telemetry';
|
||||
export * from './analytics';
|
||||
export * from './featureFlags';
|
||||
export * from './location-store';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
import { writable } from 'svelte/store';
|
||||
import appEnv from 'app-env';
|
||||
|
||||
export const location = appEnv.VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
|
||||
@@ -1,14 +1,22 @@
|
||||
import type { ControllerInput } from '$lib/models';
|
||||
import type { ControllerInput } from '$lib/types/models';
|
||||
import { persistentStore } from '$lib/utilities/svelte-utilities';
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export const emulateModel = writable(true);
|
||||
|
||||
export const jointNames = persistentStore('joint_names', []);
|
||||
export const jointNames = persistentStore('joint_names', <string[]>[]);
|
||||
|
||||
export const model = writable();
|
||||
|
||||
export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'crawl', 'walk'] as const;
|
||||
export const modes = [
|
||||
'deactivated',
|
||||
'idle',
|
||||
'calibration',
|
||||
'rest',
|
||||
'stand',
|
||||
'crawl',
|
||||
'walk'
|
||||
] as const;
|
||||
|
||||
export type Modes = (typeof modes)[number];
|
||||
|
||||
@@ -22,7 +30,7 @@ export enum ModesEnum {
|
||||
Walk
|
||||
}
|
||||
|
||||
export const mode: Writable<ModesEnum> = writable(ModesEnum.Walk);
|
||||
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated);
|
||||
|
||||
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { type angles } from '$lib/models';
|
||||
import { type angles } from '$lib/types/models';
|
||||
|
||||
export const servoAnglesOut: Writable<number[]> = writable([
|
||||
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
|
||||
@@ -8,7 +8,6 @@ export const servoAngles: Writable<number[]> = writable([
|
||||
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
|
||||
]);
|
||||
export const logs = writable([] as string[]);
|
||||
export const battery = writable({});
|
||||
export const mpu = writable({ heading: 0 });
|
||||
export const sonar = writable([0, 0]);
|
||||
export const distances = writable({});
|
||||
@@ -16,7 +15,6 @@ export const distances = writable({});
|
||||
export interface socketDataCollection {
|
||||
angles: Writable<angles>;
|
||||
logs: Writable<string[]>;
|
||||
battery: Writable<unknown>;
|
||||
mpu: Writable<unknown>;
|
||||
distances: Writable<unknown>;
|
||||
}
|
||||
@@ -24,7 +22,6 @@ export interface socketDataCollection {
|
||||
export const socketData = {
|
||||
angles: servoAngles,
|
||||
logs,
|
||||
battery,
|
||||
mpu,
|
||||
distances
|
||||
};
|
||||
|
||||
+128
-118
@@ -1,122 +1,132 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
|
||||
type SocketEvent = (typeof socketEvents)[number];
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
|
||||
type SocketEvent = (typeof socketEvents)[number]
|
||||
|
||||
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;
|
||||
|
||||
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 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);
|
||||
|
||||
if (!data) return;
|
||||
|
||||
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
|
||||
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
|
||||
|
||||
try {
|
||||
payload = JSON.parse(payload);
|
||||
} catch (error) {}
|
||||
if (event) listeners.get(event)?.forEach((listener) => listener(payload));
|
||||
};
|
||||
ws.onerror = (ev) => disconnect('error', ev);
|
||||
ws.onclose = (ev) => disconnect('close', ev);
|
||||
}
|
||||
|
||||
function unsubscribe(event: string, listener?: (data: any) => void) {
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(`2/${event}[${JSON.stringify(data)}]`);
|
||||
}
|
||||
|
||||
function unsubscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send('1/' + event);
|
||||
}
|
||||
|
||||
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: any) => void);
|
||||
|
||||
return () => {
|
||||
unsubscribe(event, listener);
|
||||
};
|
||||
},
|
||||
off: (event: string, listener?: (data: any) => void) => {
|
||||
unsubscribe(event, listener);
|
||||
}
|
||||
};
|
||||
export enum Topics {
|
||||
imu = 0,
|
||||
mode = 1,
|
||||
command = 2,
|
||||
servo = 3,
|
||||
input = 4,
|
||||
angles = 5,
|
||||
position = 6
|
||||
}
|
||||
|
||||
export const socket = createWebSocket();
|
||||
function createWebSocket() {
|
||||
let listeners = new Map<string | Topics, Set<(data?: unknown) => void>>()
|
||||
const { subscribe, set } = writable(false)
|
||||
const reconnectTimeoutTime = 5000
|
||||
let unresponsiveTimeoutId: number
|
||||
let reconnectTimeoutId: number
|
||||
let ws: WebSocket
|
||||
let socketUrl: string | URL
|
||||
|
||||
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 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 as unknown as Topics)
|
||||
}
|
||||
}
|
||||
ws.onmessage = message => {
|
||||
resetUnresponsiveCheck()
|
||||
let data = message.data
|
||||
if (data instanceof ArrayBuffer) {
|
||||
listeners.get('binary')?.forEach(listener => listener(data))
|
||||
return
|
||||
}
|
||||
data = data.substring(1)
|
||||
|
||||
if (!data) return
|
||||
|
||||
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['))
|
||||
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'))
|
||||
|
||||
try {
|
||||
payload = JSON.parse(payload)
|
||||
} catch (error) {}
|
||||
if (event) listeners.get(event)?.forEach(listener => listener(payload))
|
||||
}
|
||||
ws.onerror = ev => disconnect('error', ev)
|
||||
ws.onclose = ev => disconnect('close', ev)
|
||||
}
|
||||
|
||||
function unsubscribe(event: Topics, listener?: (data: any) => void) {
|
||||
let eventListeners = listeners.get(event)
|
||||
if (!eventListeners) return
|
||||
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event)
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener)
|
||||
} else {
|
||||
listeners.delete(event)
|
||||
}
|
||||
}
|
||||
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId)
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
|
||||
}
|
||||
|
||||
function sendEvent(event: Topics, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
ws.send(JSON.stringify([2, event, data]))
|
||||
}
|
||||
|
||||
function unsubscribeToEvent(event: Topics) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
ws.send(`[1,${event}]`)
|
||||
}
|
||||
|
||||
function subscribeToEvent(event: Topics) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
ws.send(`[0,${event}]`)
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: Topics | SocketEvent, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event)
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event)) {
|
||||
subscribeToEvent(event)
|
||||
}
|
||||
eventListeners = new Set()
|
||||
listeners.set(event, eventListeners)
|
||||
}
|
||||
eventListeners.add(listener as (data: any) => void)
|
||||
|
||||
return () => {
|
||||
unsubscribe(event, listener)
|
||||
}
|
||||
},
|
||||
off: (event: Topics, listener?: (data: any) => void) => {
|
||||
unsubscribe(event, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const socket = createWebSocket()
|
||||
|
||||
@@ -1,45 +1,35 @@
|
||||
import type { Battery, DownloadOTA } from '$lib/types/models';
|
||||
import type { DownloadOTA } from '$lib/types/models';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
let telemetry_data = {
|
||||
rssi: {
|
||||
rssi: 0
|
||||
},
|
||||
battery: {
|
||||
voltage: 100,
|
||||
current: false
|
||||
},
|
||||
download_ota: {
|
||||
status: 'none',
|
||||
progress: 0,
|
||||
error: ''
|
||||
}
|
||||
rssi: {
|
||||
rssi: 0
|
||||
},
|
||||
download_ota: {
|
||||
status: 'none',
|
||||
progress: 0,
|
||||
error: ''
|
||||
}
|
||||
};
|
||||
|
||||
function createTelemetry() {
|
||||
const { subscribe, set, update } = writable(telemetry_data);
|
||||
const { subscribe, set, update } = writable(telemetry_data);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
setRSSI: (data: number) => {
|
||||
update((telemetry_data) => ({
|
||||
...telemetry_data,
|
||||
rssi: { rssi: data }
|
||||
}));
|
||||
},
|
||||
setBattery: (data: Battery) => {
|
||||
update((telemetry_data) => ({
|
||||
...telemetry_data,
|
||||
battery: { voltage: data.voltage, current: data.current }
|
||||
}));
|
||||
},
|
||||
setDownloadOTA: (data: DownloadOTA) => {
|
||||
update((telemetry_data) => ({
|
||||
...telemetry_data,
|
||||
download_ota: { status: data.status, progress: data.progress, error: data.error }
|
||||
}));
|
||||
}
|
||||
};
|
||||
return {
|
||||
subscribe,
|
||||
setRSSI: (data: number) => {
|
||||
update(telemetry_data => ({
|
||||
...telemetry_data,
|
||||
rssi: { rssi: data }
|
||||
}));
|
||||
},
|
||||
setDownloadOTA: (data: DownloadOTA) => {
|
||||
update(telemetry_data => ({
|
||||
...telemetry_data,
|
||||
download_ota: { status: data.status, progress: data.progress, error: data.error }
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const telemetry = createTelemetry();
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { jwtDecode } from 'jwt-decode';
|
||||
|
||||
export type userProfile = {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
bearer_token: string;
|
||||
};
|
||||
|
||||
type decodedJWT = {
|
||||
username: string;
|
||||
admin: boolean;
|
||||
};
|
||||
|
||||
let empty = {
|
||||
username: '',
|
||||
admin: false,
|
||||
bearer_token: ''
|
||||
};
|
||||
|
||||
function createStore() {
|
||||
const { subscribe, set } = writable(empty);
|
||||
|
||||
// retrieve store from sessionStorage / localStorage if available
|
||||
const userdata = localStorage.getItem('user');
|
||||
if (userdata) {
|
||||
set(JSON.parse(userdata));
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
init: (access_token: string) => {
|
||||
const decoded: decodedJWT = jwtDecode(access_token);
|
||||
const userdata = {
|
||||
bearer_token: access_token,
|
||||
username: decoded.username,
|
||||
admin: decoded.admin
|
||||
};
|
||||
set(userdata);
|
||||
// persist store in sessionStorage / localStorage
|
||||
localStorage.setItem('user', JSON.stringify(userdata));
|
||||
},
|
||||
invalidate: () => {
|
||||
console.log('Log out user');
|
||||
set(empty);
|
||||
// remove localStorage "user"
|
||||
localStorage.removeItem('user');
|
||||
// redirect to login page
|
||||
goto('/');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const user = createStore();
|
||||
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
declare module 'three/src/math/MathUtils' {
|
||||
export function generateUUID(): string;
|
||||
export function clamp(value: number, min: number, max: number): number;
|
||||
export function euclideanModulo(n: number, m: number): number;
|
||||
export function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number;
|
||||
export function lerp(x: number, y: number, t: number): number;
|
||||
export function smoothstep(x: number, min: number, max: number): number;
|
||||
export function smootherstep(x: number, min: number, max: number): number;
|
||||
export function randInt(low: number, high: number): number;
|
||||
export function randFloat(low: number, high: number): number;
|
||||
export function randFloatSpread(range: number): number;
|
||||
export function degToRad(degrees: number): number;
|
||||
export function radToDeg(radians: number): number;
|
||||
export function isPowerOfTwo(value: number): boolean;
|
||||
export function ceilPowerOfTwo(value: number): number;
|
||||
export function floorPowerOfTwo(value: number): number;
|
||||
}
|
||||
+197
-119
@@ -1,143 +1,221 @@
|
||||
export type vector = { x: number; y: number }
|
||||
|
||||
export interface ControllerInput {
|
||||
left: vector
|
||||
right: vector
|
||||
height: number
|
||||
speed: number
|
||||
s1: number
|
||||
}
|
||||
|
||||
export type GithubRelease = {
|
||||
message: string
|
||||
tag_name: string
|
||||
assets: Array<{
|
||||
name: string
|
||||
browser_download_url: string
|
||||
}>
|
||||
}
|
||||
|
||||
export type angles = number[] | Int16Array
|
||||
|
||||
export type WifiStatus = {
|
||||
status: number;
|
||||
local_ip: string;
|
||||
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[]
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export type NTPStatus = {
|
||||
status: number;
|
||||
utc_time: string;
|
||||
local_time: string;
|
||||
server: string;
|
||||
uptime: number;
|
||||
};
|
||||
|
||||
export type RSSI = {
|
||||
rssi: number;
|
||||
ssid: string;
|
||||
};
|
||||
|
||||
export type Battery = {
|
||||
voltage: number;
|
||||
current: boolean;
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
export type NTPSettings = {
|
||||
enabled: boolean;
|
||||
server: string;
|
||||
tz_label: string;
|
||||
tz_format: 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;
|
||||
imu_temp: 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
|
||||
}
|
||||
|
||||
export type File = number
|
||||
|
||||
export interface Directory {
|
||||
[key: string]: File | Directory
|
||||
}
|
||||
|
||||
export type Servo = {
|
||||
name: string
|
||||
channel: number
|
||||
inverted: boolean
|
||||
angle: number
|
||||
center_angle: number
|
||||
}
|
||||
|
||||
export type ServoConfiguration = {
|
||||
is_active: boolean
|
||||
servo_pwm_frequency: number
|
||||
servo_oscillator_frequency: number
|
||||
servos: Servo[]
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
declare module 'uzip' {
|
||||
interface UZIP {
|
||||
parse(data: Uint8Array | ArrayBuffer): any;
|
||||
compress(data: any): Uint8Array | ArrayBuffer;
|
||||
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer;
|
||||
decompress(data: Uint8Array | ArrayBuffer): any;
|
||||
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer;
|
||||
encode(data: any): Uint8Array | ArrayBuffer;
|
||||
decode(data: Uint8Array | ArrayBuffer): any;
|
||||
}
|
||||
|
||||
const uzip: UZIP;
|
||||
export default uzip;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export function daisyColor(name: string, opacity: number = 100) {
|
||||
export const daisyColor = (name: string, opacity: number = 100) => {
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
|
||||
return `oklch(${color} / ${opacity}%)`;
|
||||
}
|
||||
};
|
||||
@@ -4,4 +4,6 @@ export * from './svelte-utilities';
|
||||
export * from './math-utilities';
|
||||
export * from './buffer-utilities';
|
||||
export * from './model-utilities';
|
||||
export * from './location-utilities';
|
||||
export * from './position-utilities';
|
||||
export * from './string-utilities';
|
||||
export * from './color-utilities';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export const hostname = 'localhost'; //window.location.hostname;
|
||||
|
||||
export const isSecure = true; // window.location.protocol === 'https:';
|
||||
|
||||
export const location = 'localhost:5173'; //window.location; //import.meta.env.VITE_API_URL.replace('hostname', hostname);
|
||||
|
||||
const socketScheme = isSecure ? 'wss://' : 'ws://';
|
||||
|
||||
export const socketLocation = socketScheme + location; // import.meta.env.VITE_SOCKET_URL.replace('hostname', hostname);
|
||||
@@ -1,63 +1,92 @@
|
||||
import { Color, LoaderUtils, Vector3 } from 'three';
|
||||
import URDFLoader, { type URDFRobot } from 'urdf-loader';
|
||||
import { XacroLoader } from 'xacro-parser';
|
||||
import { Result } from '$lib/utilities';
|
||||
import { Color, LoaderUtils, Vector3 } from 'three'
|
||||
import URDFLoader, { type URDFRobot } from 'urdf-loader'
|
||||
import { XacroLoader } from 'xacro-parser'
|
||||
import { Result } from '$lib/utilities'
|
||||
import { jointNames, model } from '$lib/stores'
|
||||
import uzip from 'uzip'
|
||||
import { fileService } from '$lib/services'
|
||||
|
||||
let model_xml: XMLDocument;
|
||||
let model_xml: XMLDocument
|
||||
|
||||
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 populateModelCache = async () => {
|
||||
await cacheModelFiles()
|
||||
const modelRes = await loadModel('/yertle.URDF')
|
||||
if (modelRes.isOk()) {
|
||||
const [urdf, JOINT_NAME] = modelRes.inner
|
||||
jointNames.set(JOINT_NAME)
|
||||
model.set(urdf)
|
||||
} else {
|
||||
console.error(modelRes.inner, { exception: modelRes.exception })
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
export const cacheModelFiles = async () => {
|
||||
const data = await fetch('/URDF.zip')
|
||||
|
||||
resolve(Result.ok([model, joints]));
|
||||
} catch (error) {
|
||||
resolve(Result.err('Failed to load model', error));
|
||||
}
|
||||
},
|
||||
(error) => resolve(Result.err('Failed to load model', error))
|
||||
);
|
||||
});
|
||||
};
|
||||
const files = uzip.parse(await data.arrayBuffer())
|
||||
|
||||
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;
|
||||
};
|
||||
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 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(', ');
|
||||
export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
|
||||
const urdfLoader = new URDFLoader()
|
||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
|
||||
|
||||
return new Color(`rgb(${colorStr})`);
|
||||
};
|
||||
let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
|
||||
|
||||
if (typeof xml === 'string') {
|
||||
xml = new window.DOMParser().parseFromString(xml, 'text/xml')
|
||||
}
|
||||
|
||||
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])
|
||||
|
||||
resolve(Result.ok([model, joints]))
|
||||
} catch (error) {
|
||||
resolve(Result.err('Failed to load model', error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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,16 +1,16 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const isEmbeddedApp = import.meta.env.VITE_EMBEDDED_BUILD === 'true';
|
||||
export const persistentStore = <T>(key: string, initialValue: T) => {
|
||||
const savedValue = browser ? localStorage.getItem(key) : null;
|
||||
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue;
|
||||
const store = writable<T>();
|
||||
|
||||
export const persistentStore = (key: string, initialValue: any) => {
|
||||
const savedValue = browser ? JSON.parse(localStorage.getItem(key) as string) : null;
|
||||
const data = savedValue !== null ? savedValue : initialValue;
|
||||
const store = writable(data);
|
||||
|
||||
store.subscribe((value) => {
|
||||
browser && localStorage.setItem(key, JSON.stringify(value));
|
||||
store.subscribe(value => {
|
||||
if (browser) localStorage.setItem(key, JSON.stringify(value));
|
||||
});
|
||||
|
||||
store.set(data);
|
||||
|
||||
return store;
|
||||
};
|
||||
|
||||
@@ -1,129 +1,125 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import { analytics } from '$lib/stores/analytics';
|
||||
import type { userProfile } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { Modals, closeModal } from 'svelte-modals';
|
||||
import Toast from '$lib/components/toasts/Toast.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.css';
|
||||
import Menu from './menu.svelte';
|
||||
import Statusbar from './statusbar.svelte';
|
||||
import Login from './login.svelte';
|
||||
import { ModesEnum, kinematicData, mode, outControllerData, servoAngles, servoAnglesOut, socket } from '$lib/stores';
|
||||
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { Modals, modals } from 'svelte-modals';
|
||||
import Toast from '$lib/components/toasts/Toast.svelte';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade } from 'svelte/transition';
|
||||
import '../app.css';
|
||||
import Menu from '../lib/components/menu/Menu.svelte';
|
||||
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
|
||||
import {
|
||||
telemetry,
|
||||
analytics,
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
outControllerData,
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
location,
|
||||
useFeatureFlags
|
||||
} from '$lib/stores';
|
||||
import type { Analytics, DownloadOTA } from '$lib/types/models';
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.bearer_token !== '') {
|
||||
await validateUser($user);
|
||||
}
|
||||
const ws_token = $page.data.features.security ? '?access_token=' + $user.bearer_token : '';
|
||||
socket.init(`ws://${window.location.host}/ws/events${ws_token}`);
|
||||
let { children }: Props = $props();
|
||||
|
||||
addEventListeners();
|
||||
const features = useFeatureFlags();
|
||||
|
||||
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}));
|
||||
});
|
||||
onMount(async () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
|
||||
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 }));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
removeEventListeners();
|
||||
});
|
||||
|
||||
const addEventListeners = () => {
|
||||
socket.on('open', handleOpen);
|
||||
socket.on('close', handleClose);
|
||||
socket.on('error', handleError);
|
||||
socket.on('rssi', handleNetworkStatus);
|
||||
socket.on('mode', (data:ModesEnum) => mode.set(data));
|
||||
socket.on('angles', (angles:number[]) => { if (angles.length) servoAngles.set(angles)});
|
||||
if ($page.data.features.analytics) socket.on('analytics', handleAnalytics);
|
||||
if ($page.data.features.battery) socket.on('battery', handleBattery);
|
||||
if ($page.data.features.download_firmware) socket.on('otastatus', handleOAT);
|
||||
if ($page.data.features.sonar) socket.on('sonar', data => console.log(data))
|
||||
};
|
||||
socket.on('open', handleOpen);
|
||||
socket.on('close', handleClose);
|
||||
socket.on('error', handleError);
|
||||
socket.on('rssi', handleNetworkStatus);
|
||||
socket.on('mode', (data: ModesEnum) => mode.set(data));
|
||||
socket.on('analytics', handleAnalytics);
|
||||
socket.on('angles', (angles: number[]) => {
|
||||
if (angles.length) servoAngles.set(angles);
|
||||
});
|
||||
features.subscribe(data => {
|
||||
if (data?.download_firmware) socket.on('otastatus', handleOAT);
|
||||
if (data?.sonar) socket.on('sonar', data => console.log(data));
|
||||
});
|
||||
};
|
||||
|
||||
const removeEventListeners = () => {
|
||||
socket.off('analytics', handleAnalytics);
|
||||
socket.off('open', handleOpen);
|
||||
socket.off('close', handleClose);
|
||||
socket.off('rssi', handleNetworkStatus);
|
||||
socket.off('battery', handleBattery);
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
socket.off('analytics', handleAnalytics);
|
||||
socket.off('open', handleOpen);
|
||||
socket.off('close', handleClose);
|
||||
socket.off('rssi', handleNetworkStatus);
|
||||
socket.off('otastatus', handleOAT);
|
||||
};
|
||||
|
||||
async function validateUser(userdata: userProfile) {
|
||||
const result = await api.get('/api/verifyAuthorization')
|
||||
if (result.isErr()){
|
||||
user.invalidate();
|
||||
console.error('Error:', result.inner);
|
||||
}
|
||||
}
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
notifications.success('Connection to device established', 5000);
|
||||
};
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000);
|
||||
telemetry.setRSSI(0);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
notifications.error('Connection to device lost', 5000);
|
||||
telemetry.setRSSI(0);
|
||||
};
|
||||
const handleError = (data: any) => console.error(data);
|
||||
|
||||
const handleError = (data: any) => console.error(data);
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data);
|
||||
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data);
|
||||
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
|
||||
|
||||
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data);
|
||||
|
||||
const handleBattery = (data: Battery) => telemetry.setBattery(data);
|
||||
|
||||
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
|
||||
|
||||
let menuOpen = false;
|
||||
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
|
||||
|
||||
let menuOpen = $state(false);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$page.data.title}</title>
|
||||
<title>{page.data.title}</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if $page.data.features.security && $user.bearer_token === ''}
|
||||
<Login />
|
||||
{:else}
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
<div class="drawer">
|
||||
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
|
||||
<div class="drawer-content flex flex-col">
|
||||
<!-- Status bar content here -->
|
||||
<Statusbar />
|
||||
|
||||
<!-- Main page content here -->
|
||||
<slot />
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay" />
|
||||
<Menu
|
||||
on:menuClicked={() => menuOpen = false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- Main page content here -->
|
||||
{@render children?.()}
|
||||
</div>
|
||||
<!-- Side Navigation -->
|
||||
<div class="drawer-side z-30 shadow-lg">
|
||||
<label for="main-menu" class="drawer-overlay"></label>
|
||||
<Menu menuClicked={() => (menuOpen = false)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modals>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
slot="backdrop"
|
||||
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur"
|
||||
transition:fade
|
||||
on:click={closeModal}
|
||||
/>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
{#snippet backdrop()}
|
||||
<div
|
||||
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
|
||||
transition:fade
|
||||
onclick={modals.closeAll}
|
||||
></div>
|
||||
{/snippet}
|
||||
</Modals>
|
||||
|
||||
<Toast />
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { jointNames, model } from '$lib/stores';
|
||||
import { loadModelAsync } from '$lib/utilities/model-utilities';
|
||||
|
||||
export const prerender = true;
|
||||
export const prerender = false;
|
||||
export const ssr = false;
|
||||
|
||||
const registerFetchIntercept = async () => {
|
||||
@@ -14,24 +11,9 @@ const registerFetchIntercept = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const loadModelFiles = async () => {
|
||||
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
|
||||
if (modelRes.isOk()) {
|
||||
const [urdf, JOINT_NAME] = modelRes.inner;
|
||||
jointNames.set(JOINT_NAME);
|
||||
model.set(urdf);
|
||||
} else {
|
||||
console.error(modelRes.inner, { exception: modelRes.exception });
|
||||
}
|
||||
};
|
||||
|
||||
export const load = async ({ fetch }) => {
|
||||
export const load = async () => {
|
||||
await registerFetchIntercept();
|
||||
await loadModelFiles();
|
||||
const result = await fetch('/api/features');
|
||||
const features = await result.json();
|
||||
return {
|
||||
features,
|
||||
title: 'Spot micro controller',
|
||||
github: 'runeharlyk/SpotMicroESP32-Leika',
|
||||
app_name: 'Spot Micro Controller',
|
||||
|
||||
+21
-18
@@ -1,24 +1,27 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import Visualization from '$lib/components/Visualization.svelte';
|
||||
import { goto } from '$app/navigation'
|
||||
import Visualization from '$lib/components/Visualization.svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let data: PageData;
|
||||
onMount(() => {
|
||||
socket.subscribe(isConnected => {
|
||||
if (isConnected) {
|
||||
goto('/controller')
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="hero bg-base-100 h-screen">
|
||||
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
|
||||
<div class="w-64 h-64">
|
||||
<Visualization sky={false} orbit panel={false} ground={false}/>
|
||||
</div>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-center text-2xl">Welcome to {data.app_name}</h2>
|
||||
<p class="py-6 text-center"></p>
|
||||
<a
|
||||
class="btn btn-primary"
|
||||
href="/controller"
|
||||
on:click={() => notifications.success('You did it!', 1000)}>Begin</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<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="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>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Connection from './Connection.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
<Connection />
|
||||
</div>
|
||||
@@ -2,6 +2,6 @@ import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'NTP'
|
||||
title: 'Connection'
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket, useFeatureFlags } from '$lib/stores';
|
||||
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const update = () => {
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
{#snippet icon()}
|
||||
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Connection</span>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
</SettingsCard>
|
||||
@@ -1,7 +0,0 @@
|
||||
import type { PageLoad } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export const load = (async () => {
|
||||
goto('/');
|
||||
return;
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,263 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import Collapsible from '$lib/components/Collapsible.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { page } from '$app/stores';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { TIME_ZONES } from './timezones';
|
||||
import NTP from '~icons/tabler/clock-check';
|
||||
import Server from '~icons/tabler/server';
|
||||
import Clock from '~icons/tabler/clock';
|
||||
import UTC from '~icons/tabler/clock-pin';
|
||||
import Stopwatch from '~icons/tabler/24-hours';
|
||||
import type { NTPSettings, NTPStatus } from '$lib/types/models';
|
||||
import { api } from '$lib/api';
|
||||
|
||||
let ntpSettings: NTPSettings;
|
||||
let ntpStatus: NTPStatus;
|
||||
|
||||
async function getNTPStatus() {
|
||||
const result = await api.get<NTPStatus>('/api/ntpStatus');
|
||||
if (result.isErr()){
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
}
|
||||
ntpStatus = result.inner
|
||||
}
|
||||
|
||||
async function getNTPSettings() {
|
||||
const result = await api.get<NTPSettings>('/api/ntpSettings');
|
||||
if (result.isErr()){
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
}
|
||||
ntpSettings = result.inner
|
||||
}
|
||||
|
||||
const interval = setInterval(async () => {
|
||||
getNTPStatus();
|
||||
}, 5000);
|
||||
|
||||
onDestroy(() => clearInterval(interval));
|
||||
|
||||
onMount(() => {
|
||||
if (!$page.data.features.security || $user.admin) {
|
||||
getNTPSettings();
|
||||
}
|
||||
});
|
||||
|
||||
let formField: any;
|
||||
|
||||
let formErrors = {
|
||||
server: false
|
||||
};
|
||||
|
||||
async function postNTPSettings(data: NTPSettings) {
|
||||
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
|
||||
if (result.isErr()){
|
||||
notifications.error('User not authorized.', 3000);
|
||||
console.error('Error:', result.inner);
|
||||
return
|
||||
}
|
||||
ntpSettings = result.inner
|
||||
}
|
||||
|
||||
function handleSubmitNTP() {
|
||||
let valid = true;
|
||||
|
||||
// Validate Server
|
||||
// RegEx for IPv4
|
||||
const regexExpIPv4 =
|
||||
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
|
||||
const regexExpURL =
|
||||
/[-a-zA-Z0-9@:%_\+.~#?&//=]{2,256}\.[a-z]{2,4}\b(\/[-a-zA-Z0-9@:%_\+.~#?&//=]*)?/i;
|
||||
|
||||
if (!regexExpURL.test(ntpSettings.server) && !regexExpIPv4.test(ntpSettings.server)) {
|
||||
valid = false;
|
||||
formErrors.server = true;
|
||||
} else {
|
||||
formErrors.server = false;
|
||||
}
|
||||
|
||||
ntpSettings.tz_format = TIME_ZONES[ntpSettings.tz_label];
|
||||
|
||||
// Submit JSON to REST API
|
||||
if (valid) {
|
||||
postNTPSettings(ntpSettings);
|
||||
//alert('Form Valid');
|
||||
}
|
||||
}
|
||||
|
||||
function convertSeconds(seconds: number) {
|
||||
// Calculate the number of seconds, minutes, hours, and days
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
let hours = Math.floor(minutes / 60);
|
||||
let days = Math.floor(hours / 24);
|
||||
|
||||
// Calculate the remaining hours, minutes, and seconds
|
||||
hours = hours % 24;
|
||||
minutes = minutes % 60;
|
||||
seconds = seconds % 60;
|
||||
|
||||
// Create the formatted string
|
||||
let result = '';
|
||||
if (days > 0) {
|
||||
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (hours > 0) {
|
||||
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
|
||||
}
|
||||
result += seconds + ' second' + (seconds > 1 ? 's' : '');
|
||||
|
||||
return result;
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Clock slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Network Time</span>
|
||||
<div class="w-full overflow-x-auto">
|
||||
{#await getNTPStatus()}
|
||||
<Spinner />
|
||||
{:then nothing}
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div
|
||||
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
|
||||
? 'bg-success'
|
||||
: 'bg-error'}"
|
||||
>
|
||||
<NTP
|
||||
class="h-auto w-full scale-75 {ntpStatus.status === 1
|
||||
? 'text-success-content'
|
||||
: 'text-error-content'}"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Status</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.status === 1 ? 'Active' : 'Inactive'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Server class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">NTP Server</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{ntpStatus.server}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Clock class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Local Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long'
|
||||
}).format(new Date(ntpStatus.local_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<UTC class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">UTC Time</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{new Intl.DateTimeFormat('en-GB', {
|
||||
dateStyle: 'long',
|
||||
timeStyle: 'long',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(ntpStatus.utc_time))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
|
||||
<div class="mask mask-hexagon bg-primary h-auto w-10">
|
||||
<Stopwatch class="text-primary-content h-auto w-full scale-75" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold">Uptime</div>
|
||||
<div class="text-sm opacity-75">
|
||||
{convertSeconds(ntpStatus.uptime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
{#if !$page.data.features.security || $user.admin}
|
||||
<Collapsible open={false} on:closed={getNTPSettings}>
|
||||
<span slot="title">Change NTP Settings</span>
|
||||
<form
|
||||
class="form-control w-full"
|
||||
on:submit|preventDefault={handleSubmitNTP}
|
||||
novalidate
|
||||
bind:this={formField}
|
||||
>
|
||||
<label class="label cursor-pointer justify-start gap-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={ntpSettings.enabled}
|
||||
class="checkbox checkbox-primary"
|
||||
/>
|
||||
<span class="">Enable NTP</span>
|
||||
</label>
|
||||
<label class="label" for="server">
|
||||
<span class="label-text text-md">Server</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
min="3"
|
||||
max="64"
|
||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
|
||||
? 'border-error border-2'
|
||||
: ''}"
|
||||
bind:value={ntpSettings.server}
|
||||
id="server"
|
||||
required
|
||||
/>
|
||||
<label class="label" for="subnet">
|
||||
<span class="label-text-alt text-error {formErrors.server ? '' : 'hidden'}"
|
||||
>Must be a valid IPv4 address or URL</span
|
||||
>
|
||||
</label>
|
||||
<label class="label" for="tz">
|
||||
<span class="label-text text-md">Pick Time Zone</span>
|
||||
</label>
|
||||
<select class="select select-bordered" bind:value={ntpSettings.tz_label} id="tz">
|
||||
{#each Object.entries(TIME_ZONES) as [tz_label, tz_format]}
|
||||
<option value={tz_label}>{tz_label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<div class="mt-6 place-self-end">
|
||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
</SettingsCard>
|
||||
@@ -1,466 +0,0 @@
|
||||
export type TimeZones = {
|
||||
[name: string]: string
|
||||
};
|
||||
|
||||
export const TIME_ZONES: TimeZones = {
|
||||
"Africa/Abidjan": "GMT0",
|
||||
"Africa/Accra": "GMT0",
|
||||
"Africa/Addis_Ababa": "EAT-3",
|
||||
"Africa/Algiers": "CET-1",
|
||||
"Africa/Asmara": "EAT-3",
|
||||
"Africa/Bamako": "GMT0",
|
||||
"Africa/Bangui": "WAT-1",
|
||||
"Africa/Banjul": "GMT0",
|
||||
"Africa/Bissau": "GMT0",
|
||||
"Africa/Blantyre": "CAT-2",
|
||||
"Africa/Brazzaville": "WAT-1",
|
||||
"Africa/Bujumbura": "CAT-2",
|
||||
"Africa/Cairo": "EET-2",
|
||||
"Africa/Casablanca": "UNK-1",
|
||||
"Africa/Ceuta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Africa/Conakry": "GMT0",
|
||||
"Africa/Dakar": "GMT0",
|
||||
"Africa/Dar_es_Salaam": "EAT-3",
|
||||
"Africa/Djibouti": "EAT-3",
|
||||
"Africa/Douala": "WAT-1",
|
||||
"Africa/El_Aaiun": "UNK-1",
|
||||
"Africa/Freetown": "GMT0",
|
||||
"Africa/Gaborone": "CAT-2",
|
||||
"Africa/Harare": "CAT-2",
|
||||
"Africa/Johannesburg": "SAST-2",
|
||||
"Africa/Juba": "EAT-3",
|
||||
"Africa/Kampala": "EAT-3",
|
||||
"Africa/Khartoum": "CAT-2",
|
||||
"Africa/Kigali": "CAT-2",
|
||||
"Africa/Kinshasa": "WAT-1",
|
||||
"Africa/Lagos": "WAT-1",
|
||||
"Africa/Libreville": "WAT-1",
|
||||
"Africa/Lome": "GMT0",
|
||||
"Africa/Luanda": "WAT-1",
|
||||
"Africa/Lubumbashi": "CAT-2",
|
||||
"Africa/Lusaka": "CAT-2",
|
||||
"Africa/Malabo": "WAT-1",
|
||||
"Africa/Maputo": "CAT-2",
|
||||
"Africa/Maseru": "SAST-2",
|
||||
"Africa/Mbabane": "SAST-2",
|
||||
"Africa/Mogadishu": "EAT-3",
|
||||
"Africa/Monrovia": "GMT0",
|
||||
"Africa/Nairobi": "EAT-3",
|
||||
"Africa/Ndjamena": "WAT-1",
|
||||
"Africa/Niamey": "WAT-1",
|
||||
"Africa/Nouakchott": "GMT0",
|
||||
"Africa/Ouagadougou": "GMT0",
|
||||
"Africa/Porto-Novo": "WAT-1",
|
||||
"Africa/Sao_Tome": "GMT0",
|
||||
"Africa/Tripoli": "EET-2",
|
||||
"Africa/Tunis": "CET-1",
|
||||
"Africa/Windhoek": "CAT-2",
|
||||
"America/Adak": "HST10HDT,M3.2.0,M11.1.0",
|
||||
"America/Anchorage": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Anguilla": "AST4",
|
||||
"America/Antigua": "AST4",
|
||||
"America/Araguaina": "UNK3",
|
||||
"America/Argentina/Buenos_Aires": "UNK3",
|
||||
"America/Argentina/Catamarca": "UNK3",
|
||||
"America/Argentina/Cordoba": "UNK3",
|
||||
"America/Argentina/Jujuy": "UNK3",
|
||||
"America/Argentina/La_Rioja": "UNK3",
|
||||
"America/Argentina/Mendoza": "UNK3",
|
||||
"America/Argentina/Rio_Gallegos": "UNK3",
|
||||
"America/Argentina/Salta": "UNK3",
|
||||
"America/Argentina/San_Juan": "UNK3",
|
||||
"America/Argentina/San_Luis": "UNK3",
|
||||
"America/Argentina/Tucuman": "UNK3",
|
||||
"America/Argentina/Ushuaia": "UNK3",
|
||||
"America/Aruba": "AST4",
|
||||
"America/Asuncion": "UNK4UNK,M10.1.0/0,M3.4.0/0",
|
||||
"America/Atikokan": "EST5",
|
||||
"America/Bahia": "UNK3",
|
||||
"America/Bahia_Banderas": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Barbados": "AST4",
|
||||
"America/Belem": "UNK3",
|
||||
"America/Belize": "CST6",
|
||||
"America/Blanc-Sablon": "AST4",
|
||||
"America/Boa_Vista": "UNK4",
|
||||
"America/Bogota": "UNK5",
|
||||
"America/Boise": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Cambridge_Bay": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Campo_Grande": "UNK4",
|
||||
"America/Cancun": "EST5",
|
||||
"America/Caracas": "UNK4",
|
||||
"America/Cayenne": "UNK3",
|
||||
"America/Cayman": "EST5",
|
||||
"America/Chicago": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Chihuahua": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Costa_Rica": "CST6",
|
||||
"America/Creston": "MST7",
|
||||
"America/Cuiaba": "UNK4",
|
||||
"America/Curacao": "AST4",
|
||||
"America/Danmarkshavn": "GMT0",
|
||||
"America/Dawson": "MST7",
|
||||
"America/Dawson_Creek": "MST7",
|
||||
"America/Denver": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Detroit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Dominica": "AST4",
|
||||
"America/Edmonton": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Eirunepe": "UNK5",
|
||||
"America/El_Salvador": "CST6",
|
||||
"America/Fort_Nelson": "MST7",
|
||||
"America/Fortaleza": "UNK3",
|
||||
"America/Glace_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Godthab": "UNK3UNK,M3.5.0/-2,M10.5.0/-1",
|
||||
"America/Goose_Bay": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Grand_Turk": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Grenada": "AST4",
|
||||
"America/Guadeloupe": "AST4",
|
||||
"America/Guatemala": "CST6",
|
||||
"America/Guayaquil": "UNK5",
|
||||
"America/Guyana": "UNK4",
|
||||
"America/Halifax": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Havana": "CST5CDT,M3.2.0/0,M11.1.0/1",
|
||||
"America/Hermosillo": "MST7",
|
||||
"America/Indiana/Indianapolis": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Knox": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Marengo": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Petersburg": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Tell_City": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vevay": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Vincennes": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Indiana/Winamac": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Inuvik": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Iqaluit": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Jamaica": "EST5",
|
||||
"America/Juneau": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Louisville": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kentucky/Monticello": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Kralendijk": "AST4",
|
||||
"America/La_Paz": "UNK4",
|
||||
"America/Lima": "UNK5",
|
||||
"America/Los_Angeles": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Lower_Princes": "AST4",
|
||||
"America/Maceio": "UNK3",
|
||||
"America/Managua": "CST6",
|
||||
"America/Manaus": "UNK4",
|
||||
"America/Marigot": "AST4",
|
||||
"America/Martinique": "AST4",
|
||||
"America/Matamoros": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Mazatlan": "MST7MDT,M4.1.0,M10.5.0",
|
||||
"America/Menominee": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Merida": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Metlakatla": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Mexico_City": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Miquelon": "UNK3UNK,M3.2.0,M11.1.0",
|
||||
"America/Moncton": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Monterrey": "CST6CDT,M4.1.0,M10.5.0",
|
||||
"America/Montevideo": "UNK3",
|
||||
"America/Montreal": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Montserrat": "AST4",
|
||||
"America/Nassau": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/New_York": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nipigon": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Nome": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Noronha": "UNK2",
|
||||
"America/North_Dakota/Beulah": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/Center": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/North_Dakota/New_Salem": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Ojinaga": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"America/Panama": "EST5",
|
||||
"America/Pangnirtung": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Paramaribo": "UNK3",
|
||||
"America/Phoenix": "MST7",
|
||||
"America/Port-au-Prince": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Port_of_Spain": "AST4",
|
||||
"America/Porto_Velho": "UNK4",
|
||||
"America/Puerto_Rico": "AST4",
|
||||
"America/Punta_Arenas": "UNK3",
|
||||
"America/Rainy_River": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rankin_Inlet": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Recife": "UNK3",
|
||||
"America/Regina": "CST6",
|
||||
"America/Resolute": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Rio_Branco": "UNK5",
|
||||
"America/Santarem": "UNK3",
|
||||
"America/Santiago": "UNK4UNK,M9.1.6/24,M4.1.6/24",
|
||||
"America/Santo_Domingo": "AST4",
|
||||
"America/Sao_Paulo": "UNK3",
|
||||
"America/Scoresbysund": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"America/Sitka": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/St_Barthelemy": "AST4",
|
||||
"America/St_Johns": "NST3:30NDT,M3.2.0,M11.1.0",
|
||||
"America/St_Kitts": "AST4",
|
||||
"America/St_Lucia": "AST4",
|
||||
"America/St_Thomas": "AST4",
|
||||
"America/St_Vincent": "AST4",
|
||||
"America/Swift_Current": "CST6",
|
||||
"America/Tegucigalpa": "CST6",
|
||||
"America/Thule": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"America/Thunder_Bay": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tijuana": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Toronto": "EST5EDT,M3.2.0,M11.1.0",
|
||||
"America/Tortola": "AST4",
|
||||
"America/Vancouver": "PST8PDT,M3.2.0,M11.1.0",
|
||||
"America/Whitehorse": "MST7",
|
||||
"America/Winnipeg": "CST6CDT,M3.2.0,M11.1.0",
|
||||
"America/Yakutat": "AKST9AKDT,M3.2.0,M11.1.0",
|
||||
"America/Yellowknife": "MST7MDT,M3.2.0,M11.1.0",
|
||||
"Antarctica/Casey": "UNK-8",
|
||||
"Antarctica/Davis": "UNK-7",
|
||||
"Antarctica/DumontDUrville": "UNK-10",
|
||||
"Antarctica/Macquarie": "UNK-11",
|
||||
"Antarctica/Mawson": "UNK-5",
|
||||
"Antarctica/McMurdo": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Antarctica/Palmer": "UNK3",
|
||||
"Antarctica/Rothera": "UNK3",
|
||||
"Antarctica/Syowa": "UNK-3",
|
||||
"Antarctica/Troll": "UNK0UNK-2,M3.5.0/1,M10.5.0/3",
|
||||
"Antarctica/Vostok": "UNK-6",
|
||||
"Arctic/Longyearbyen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Asia/Aden": "UNK-3",
|
||||
"Asia/Almaty": "UNK-6",
|
||||
"Asia/Amman": "EET-2EEST,M3.5.4/24,M10.5.5/1",
|
||||
"Asia/Anadyr": "UNK-12",
|
||||
"Asia/Aqtau": "UNK-5",
|
||||
"Asia/Aqtobe": "UNK-5",
|
||||
"Asia/Ashgabat": "UNK-5",
|
||||
"Asia/Atyrau": "UNK-5",
|
||||
"Asia/Baghdad": "UNK-3",
|
||||
"Asia/Bahrain": "UNK-3",
|
||||
"Asia/Baku": "UNK-4",
|
||||
"Asia/Bangkok": "UNK-7",
|
||||
"Asia/Barnaul": "UNK-7",
|
||||
"Asia/Beirut": "EET-2EEST,M3.5.0/0,M10.5.0/0",
|
||||
"Asia/Bishkek": "UNK-6",
|
||||
"Asia/Brunei": "UNK-8",
|
||||
"Asia/Chita": "UNK-9",
|
||||
"Asia/Choibalsan": "UNK-8",
|
||||
"Asia/Colombo": "UNK-5:30",
|
||||
"Asia/Damascus": "EET-2EEST,M3.5.5/0,M10.5.5/0",
|
||||
"Asia/Dhaka": "UNK-6",
|
||||
"Asia/Dili": "UNK-9",
|
||||
"Asia/Dubai": "UNK-4",
|
||||
"Asia/Dushanbe": "UNK-5",
|
||||
"Asia/Famagusta": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Gaza": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Hebron": "EET-2EEST,M3.5.5/0,M10.5.6/1",
|
||||
"Asia/Ho_Chi_Minh": "UNK-7",
|
||||
"Asia/Hong_Kong": "HKT-8",
|
||||
"Asia/Hovd": "UNK-7",
|
||||
"Asia/Irkutsk": "UNK-8",
|
||||
"Asia/Jakarta": "WIB-7",
|
||||
"Asia/Jayapura": "WIT-9",
|
||||
"Asia/Jerusalem": "IST-2IDT,M3.4.4/26,M10.5.0",
|
||||
"Asia/Kabul": "UNK-4:30",
|
||||
"Asia/Kamchatka": "UNK-12",
|
||||
"Asia/Karachi": "PKT-5",
|
||||
"Asia/Kathmandu": "UNK-5:45",
|
||||
"Asia/Khandyga": "UNK-9",
|
||||
"Asia/Kolkata": "IST-5:30",
|
||||
"Asia/Krasnoyarsk": "UNK-7",
|
||||
"Asia/Kuala_Lumpur": "UNK-8",
|
||||
"Asia/Kuching": "UNK-8",
|
||||
"Asia/Kuwait": "UNK-3",
|
||||
"Asia/Macau": "CST-8",
|
||||
"Asia/Magadan": "UNK-11",
|
||||
"Asia/Makassar": "WITA-8",
|
||||
"Asia/Manila": "PST-8",
|
||||
"Asia/Muscat": "UNK-4",
|
||||
"Asia/Nicosia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Asia/Novokuznetsk": "UNK-7",
|
||||
"Asia/Novosibirsk": "UNK-7",
|
||||
"Asia/Omsk": "UNK-6",
|
||||
"Asia/Oral": "UNK-5",
|
||||
"Asia/Phnom_Penh": "UNK-7",
|
||||
"Asia/Pontianak": "WIB-7",
|
||||
"Asia/Pyongyang": "KST-9",
|
||||
"Asia/Qatar": "UNK-3",
|
||||
"Asia/Qyzylorda": "UNK-5",
|
||||
"Asia/Riyadh": "UNK-3",
|
||||
"Asia/Sakhalin": "UNK-11",
|
||||
"Asia/Samarkand": "UNK-5",
|
||||
"Asia/Seoul": "KST-9",
|
||||
"Asia/Shanghai": "CST-8",
|
||||
"Asia/Singapore": "UNK-8",
|
||||
"Asia/Srednekolymsk": "UNK-11",
|
||||
"Asia/Taipei": "CST-8",
|
||||
"Asia/Tashkent": "UNK-5",
|
||||
"Asia/Tbilisi": "UNK-4",
|
||||
"Asia/Tehran": "UNK-3:30UNK,J79/24,J263/24",
|
||||
"Asia/Thimphu": "UNK-6",
|
||||
"Asia/Tokyo": "JST-9",
|
||||
"Asia/Tomsk": "UNK-7",
|
||||
"Asia/Ulaanbaatar": "UNK-8",
|
||||
"Asia/Urumqi": "UNK-6",
|
||||
"Asia/Ust-Nera": "UNK-10",
|
||||
"Asia/Vientiane": "UNK-7",
|
||||
"Asia/Vladivostok": "UNK-10",
|
||||
"Asia/Yakutsk": "UNK-9",
|
||||
"Asia/Yangon": "UNK-6:30",
|
||||
"Asia/Yekaterinburg": "UNK-5",
|
||||
"Asia/Yerevan": "UNK-4",
|
||||
"Atlantic/Azores": "UNK1UNK,M3.5.0/0,M10.5.0/1",
|
||||
"Atlantic/Bermuda": "AST4ADT,M3.2.0,M11.1.0",
|
||||
"Atlantic/Canary": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Cape_Verde": "UNK1",
|
||||
"Atlantic/Faroe": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Madeira": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Atlantic/Reykjavik": "GMT0",
|
||||
"Atlantic/South_Georgia": "UNK2",
|
||||
"Atlantic/St_Helena": "GMT0",
|
||||
"Atlantic/Stanley": "UNK3",
|
||||
"Australia/Adelaide": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Brisbane": "AEST-10",
|
||||
"Australia/Broken_Hill": "ACST-9:30ACDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Currie": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Darwin": "ACST-9:30",
|
||||
"Australia/Eucla": "UNK-8:45",
|
||||
"Australia/Hobart": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Lindeman": "AEST-10",
|
||||
"Australia/Lord_Howe": "UNK-10:30UNK-11,M10.1.0,M4.1.0",
|
||||
"Australia/Melbourne": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Australia/Perth": "AWST-8",
|
||||
"Australia/Sydney": "AEST-10AEDT,M10.1.0,M4.1.0/3",
|
||||
"Etc/GMT": "GMT0",
|
||||
"Etc/GMT+0": "GMT0",
|
||||
"Etc/GMT+1": "UNK1",
|
||||
"Etc/GMT+10": "UNK10",
|
||||
"Etc/GMT+11": "UNK11",
|
||||
"Etc/GMT+12": "UNK12",
|
||||
"Etc/GMT+2": "UNK2",
|
||||
"Etc/GMT+3": "UNK3",
|
||||
"Etc/GMT+4": "UNK4",
|
||||
"Etc/GMT+5": "UNK5",
|
||||
"Etc/GMT+6": "UNK6",
|
||||
"Etc/GMT+7": "UNK7",
|
||||
"Etc/GMT+8": "UNK8",
|
||||
"Etc/GMT+9": "UNK9",
|
||||
"Etc/GMT-0": "GMT0",
|
||||
"Etc/GMT-1": "UNK-1",
|
||||
"Etc/GMT-10": "UNK-10",
|
||||
"Etc/GMT-11": "UNK-11",
|
||||
"Etc/GMT-12": "UNK-12",
|
||||
"Etc/GMT-13": "UNK-13",
|
||||
"Etc/GMT-14": "UNK-14",
|
||||
"Etc/GMT-2": "UNK-2",
|
||||
"Etc/GMT-3": "UNK-3",
|
||||
"Etc/GMT-4": "UNK-4",
|
||||
"Etc/GMT-5": "UNK-5",
|
||||
"Etc/GMT-6": "UNK-6",
|
||||
"Etc/GMT-7": "UNK-7",
|
||||
"Etc/GMT-8": "UNK-8",
|
||||
"Etc/GMT-9": "UNK-9",
|
||||
"Etc/GMT0": "GMT0",
|
||||
"Etc/Greenwich": "GMT0",
|
||||
"Etc/UCT": "UTC0",
|
||||
"Etc/UTC": "UTC0",
|
||||
"Etc/Universal": "UTC0",
|
||||
"Etc/Zulu": "UTC0",
|
||||
"Europe/Amsterdam": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Andorra": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Astrakhan": "UNK-4",
|
||||
"Europe/Athens": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Belgrade": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Berlin": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bratislava": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Brussels": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Bucharest": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Budapest": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Busingen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Chisinau": "EET-2EEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Copenhagen": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Dublin": "IST-1GMT0,M10.5.0,M3.5.0/1",
|
||||
"Europe/Gibraltar": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Guernsey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Helsinki": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Isle_of_Man": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Istanbul": "UNK-3",
|
||||
"Europe/Jersey": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Kaliningrad": "EET-2",
|
||||
"Europe/Kiev": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Kirov": "UNK-3",
|
||||
"Europe/Lisbon": "WET0WEST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Ljubljana": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/London": "GMT0BST,M3.5.0/1,M10.5.0",
|
||||
"Europe/Luxembourg": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Madrid": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Malta": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Mariehamn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Minsk": "UNK-3",
|
||||
"Europe/Monaco": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Moscow": "MSK-3",
|
||||
"Europe/Oslo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Paris": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Podgorica": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Prague": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Riga": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Rome": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Samara": "UNK-4",
|
||||
"Europe/San_Marino": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sarajevo": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Saratov": "UNK-4",
|
||||
"Europe/Simferopol": "MSK-3",
|
||||
"Europe/Skopje": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Sofia": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Stockholm": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Tallinn": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Tirane": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Ulyanovsk": "UNK-4",
|
||||
"Europe/Uzhgorod": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Vaduz": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vatican": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vienna": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Vilnius": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Volgograd": "UNK-4",
|
||||
"Europe/Warsaw": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zagreb": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Europe/Zaporozhye": "EET-2EEST,M3.5.0/3,M10.5.0/4",
|
||||
"Europe/Zurich": "CET-1CEST,M3.5.0,M10.5.0/3",
|
||||
"Indian/Antananarivo": "EAT-3",
|
||||
"Indian/Chagos": "UNK-6",
|
||||
"Indian/Christmas": "UNK-7",
|
||||
"Indian/Cocos": "UNK-6:30",
|
||||
"Indian/Comoro": "EAT-3",
|
||||
"Indian/Kerguelen": "UNK-5",
|
||||
"Indian/Mahe": "UNK-4",
|
||||
"Indian/Maldives": "UNK-5",
|
||||
"Indian/Mauritius": "UNK-4",
|
||||
"Indian/Mayotte": "EAT-3",
|
||||
"Indian/Reunion": "UNK-4",
|
||||
"Pacific/Apia": "UNK-13UNK,M9.5.0/3,M4.1.0/4",
|
||||
"Pacific/Auckland": "NZST-12NZDT,M9.5.0,M4.1.0/3",
|
||||
"Pacific/Bougainville": "UNK-11",
|
||||
"Pacific/Chatham": "UNK-12:45UNK,M9.5.0/2:45,M4.1.0/3:45",
|
||||
"Pacific/Chuuk": "UNK-10",
|
||||
"Pacific/Easter": "UNK6UNK,M9.1.6/22,M4.1.6/22",
|
||||
"Pacific/Efate": "UNK-11",
|
||||
"Pacific/Enderbury": "UNK-13",
|
||||
"Pacific/Fakaofo": "UNK-13",
|
||||
"Pacific/Fiji": "UNK-12UNK,M11.2.0,M1.2.3/99",
|
||||
"Pacific/Funafuti": "UNK-12",
|
||||
"Pacific/Galapagos": "UNK6",
|
||||
"Pacific/Gambier": "UNK9",
|
||||
"Pacific/Guadalcanal": "UNK-11",
|
||||
"Pacific/Guam": "ChST-10",
|
||||
"Pacific/Honolulu": "HST10",
|
||||
"Pacific/Kiritimati": "UNK-14",
|
||||
"Pacific/Kosrae": "UNK-11",
|
||||
"Pacific/Kwajalein": "UNK-12",
|
||||
"Pacific/Majuro": "UNK-12",
|
||||
"Pacific/Marquesas": "UNK9:30",
|
||||
"Pacific/Midway": "SST11",
|
||||
"Pacific/Nauru": "UNK-12",
|
||||
"Pacific/Niue": "UNK11",
|
||||
"Pacific/Norfolk": "UNK-11UNK,M10.1.0,M4.1.0/3",
|
||||
"Pacific/Noumea": "UNK-11",
|
||||
"Pacific/Pago_Pago": "SST11",
|
||||
"Pacific/Palau": "UNK-9",
|
||||
"Pacific/Pitcairn": "UNK8",
|
||||
"Pacific/Pohnpei": "UNK-11",
|
||||
"Pacific/Port_Moresby": "UNK-10",
|
||||
"Pacific/Rarotonga": "UNK10",
|
||||
"Pacific/Saipan": "ChST-10",
|
||||
"Pacific/Tahiti": "UNK10",
|
||||
"Pacific/Tarawa": "UNK-12",
|
||||
"Pacific/Tongatapu": "UNK-13",
|
||||
"Pacific/Wake": "UNK-12",
|
||||
"Pacific/Wallis": "UNK-12"
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Controls from './Controls.svelte';
|
||||
import { socket } from '$lib/stores';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
</script>
|
||||
<div class="select-none">
|
||||
{#if !$socket}
|
||||
<div class="absolute left-0 flex flex-col w-screen h-screen justify-center items-center backdrop-blur-sm z-10">
|
||||
<Spinner/>
|
||||
<h2>Waiting for connection</h2>
|
||||
</div>
|
||||
{/if}
|
||||
<Controls />
|
||||
<slot/>
|
||||
</div>
|
||||
@@ -1,9 +1,31 @@
|
||||
<script lang="ts">
|
||||
import Visualization from "$lib/components/Visualization.svelte";
|
||||
import Controls from './Controls.svelte';
|
||||
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte';
|
||||
import { selectedView, views } from '$lib/stores/application';
|
||||
import { onMount } from 'svelte';
|
||||
import { mpu, socket } from '$lib/stores';
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import type { IMU } from '$lib/types/models';
|
||||
|
||||
let layout = $derived($views.find(v => v.name === $selectedView)!);
|
||||
|
||||
onMount(() => {
|
||||
socket.on('imu', (data: IMU) => {
|
||||
imu.addData(data);
|
||||
if (data.heading)
|
||||
mpu.update(mpuData => {
|
||||
mpuData.heading = data.heading;
|
||||
console.log(data.heading);
|
||||
|
||||
return mpuData;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="grow flex">
|
||||
<div class="absolute h-screen w-full top-0">
|
||||
<Visualization debug />
|
||||
<div class="absolute top-0 select-none w-screen h-screen">
|
||||
<Controls />
|
||||
<div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
|
||||
<WidgetContainer container={layout.content} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,123 +1,169 @@
|
||||
<script lang="ts">
|
||||
import nipplejs from 'nipplejs';
|
||||
import { onMount } from 'svelte';
|
||||
import { capitalize, throttler, toInt8 } from '$lib/utilities';
|
||||
import { input, outControllerData, mode, modes, type Modes, ModesEnum, socket } from '$lib/stores';
|
||||
import type { vector } from '$lib/models';
|
||||
import nipplejs from 'nipplejs'
|
||||
import { onMount } from 'svelte'
|
||||
import { capitalize, throttler, toInt8 } from '$lib/utilities'
|
||||
import { input, outControllerData, mode, modes, type Modes, ModesEnum } from '$lib/stores'
|
||||
import type { vector } from '$lib/types/models'
|
||||
import { VerticalSlider } from '$lib/components/input'
|
||||
import { gamepadAxes, gamepadButtons, 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 = new throttler()
|
||||
let left: nipplejs.JoystickManager
|
||||
let right: nipplejs.JoystickManager
|
||||
|
||||
let throttle_timing = 40;
|
||||
let data = new Array(8);
|
||||
let throttle_timing = 40
|
||||
let data = new Array(8)
|
||||
|
||||
onMount(() => {
|
||||
left = nipplejs.create({
|
||||
zone: document.getElementById('left') as HTMLElement,
|
||||
color: 'grey',
|
||||
dynamicPage: true,
|
||||
mode: 'static',
|
||||
restOpacity: 0.3
|
||||
});
|
||||
|
||||
right = nipplejs.create({
|
||||
zone: document.getElementById('right') as HTMLElement,
|
||||
color: 'grey',
|
||||
dynamicPage: true,
|
||||
mode: 'static',
|
||||
restOpacity: 0.3
|
||||
});
|
||||
|
||||
left.on('move', (_, data) => handleJoyMove('left', data.vector));
|
||||
left.on('end', (_, __) => handleJoyMove('left', { x: 0, y: 0 }));
|
||||
right.on('move', (_, data) => handleJoyMove('right', data.vector));
|
||||
right.on('end', (_, __) => handleJoyMove('right', { x: 0, y: 0 }));
|
||||
});
|
||||
|
||||
const handleJoyMove = (key: 'left' | 'right', data: vector) => {
|
||||
input.update((inputData) => {
|
||||
inputData[key] = data;
|
||||
return inputData;
|
||||
});
|
||||
throttle.throttle(updateData, throttle_timing);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
outControllerData.set(data);
|
||||
};
|
||||
|
||||
const handleKeyup = (event: KeyboardEvent) => {
|
||||
const down = event.type === 'keydown';
|
||||
input.update((data) => {
|
||||
if (event.key === 'w') data.left.y = down ? -1 : 0;
|
||||
if (event.key === 'a') data.left.x = down ? -1 : 0;
|
||||
if (event.key === 's') data.left.y = down ? 1 : 0;
|
||||
if (event.key === 'd') data.left.x = down ? 1 : 0;
|
||||
return data;
|
||||
});
|
||||
throttle.throttle(updateData, throttle_timing);
|
||||
};
|
||||
|
||||
const handleRange = (event:Event, key: 'speed' | 'height' | 's1') => {
|
||||
const value:number = event.target?.value
|
||||
|
||||
input.update((inputData) => {
|
||||
inputData[key] = value;
|
||||
return inputData;
|
||||
});
|
||||
throttle.throttle(updateData, throttle_timing);
|
||||
$effect(() => {
|
||||
if ($hasGamepad) {
|
||||
notifications.success('🎮 Gamepad connected', 3000)
|
||||
}
|
||||
})
|
||||
|
||||
const changeMode = (modeValue: Modes) => {
|
||||
mode.set(modes.indexOf(modeValue));
|
||||
};
|
||||
$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({
|
||||
zone: document.getElementById('left') as HTMLElement,
|
||||
color: '#15191e80',
|
||||
dynamicPage: true,
|
||||
mode: 'static',
|
||||
restOpacity: 1
|
||||
})
|
||||
|
||||
right = nipplejs.create({
|
||||
zone: document.getElementById('right') as HTMLElement,
|
||||
color: '#15191e80',
|
||||
dynamicPage: true,
|
||||
mode: 'static',
|
||||
restOpacity: 1
|
||||
})
|
||||
|
||||
left.on('move', (_, data) => handleJoyMove('left', data.vector))
|
||||
left.on('end', (_, __) => handleJoyMove('left', { x: 0, y: 0 }))
|
||||
right.on('move', (_, data) => handleJoyMove('right', data.vector))
|
||||
right.on('end', (_, __) => handleJoyMove('right', { x: 0, y: 0 }))
|
||||
})
|
||||
|
||||
const handleJoyMove = (key: 'left' | 'right', data: vector) => {
|
||||
input.update(inputData => {
|
||||
inputData[key] = data
|
||||
return inputData
|
||||
})
|
||||
throttle.throttle(updateData, throttle_timing)
|
||||
}
|
||||
|
||||
const updateData = () => {
|
||||
data[0] = 0
|
||||
data[1] = toInt8($input.left.x, -1, 1)
|
||||
data[2] = toInt8($input.left.y, -1, 1)
|
||||
data[3] = toInt8($input.right.x, -1, 1)
|
||||
data[4] = toInt8($input.right.y, -1, 1)
|
||||
data[5] = toInt8($input.height, 0, 100)
|
||||
data[6] = toInt8($input.speed, 0, 100)
|
||||
data[7] = toInt8($input.s1, 0, 100)
|
||||
|
||||
outControllerData.set(data)
|
||||
}
|
||||
|
||||
const handleKeyup = (event: KeyboardEvent) => {
|
||||
const down = event.type === 'keydown'
|
||||
input.update(data => {
|
||||
if (event.key === 'w') data.left.y = down ? 1 : 0
|
||||
if (event.key === 'a') data.left.x = down ? 1 : 0
|
||||
if (event.key === 's') data.left.y = down ? -1 : 0
|
||||
if (event.key === 'd') data.left.x = down ? -1 : 0
|
||||
return data
|
||||
})
|
||||
throttle.throttle(updateData, throttle_timing)
|
||||
}
|
||||
|
||||
const handleRange = (event: Event, key: 'speed' | 'height' | 's1') => {
|
||||
const value: number = Number((event.target as HTMLInputElement).value)
|
||||
|
||||
input.update(inputData => {
|
||||
inputData[key] = value
|
||||
return inputData
|
||||
})
|
||||
throttle.throttle(updateData, throttle_timing)
|
||||
}
|
||||
|
||||
const changeMode = (modeValue: Modes) => {
|
||||
mode.set(modes.indexOf(modeValue))
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="absolute top-0 left-0 w-screen h-screen">
|
||||
<div class="absolute top-0 left-0 h-full w-full flex portrait:hidden">
|
||||
<div id="left" class="flex w-60 items-center justify-end" />
|
||||
<div class="flex-1" />
|
||||
<div id="right" class="flex w-60 items-center" />
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex">
|
||||
<div class="flex justify-center w-full">
|
||||
<kbd class="kbd">W</kbd>
|
||||
</div>
|
||||
<div class="flex justify-center gap-2 w-full">
|
||||
<kbd class="kbd">A</kbd>
|
||||
<kbd class="kbd">S</kbd>
|
||||
<kbd class="kbd">D</kbd>
|
||||
</div>
|
||||
<div class="flex justify-center w-full">
|
||||
</div>
|
||||
<div class="absolute top-0 left-0 h-full w-full flex portrait:hidden">
|
||||
<div id="left" class="flex w-60 items-center justify-end"></div>
|
||||
<div class="flex-1"></div>
|
||||
<div id="right" class="flex w-60 items-center"></div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex">
|
||||
<div class="flex justify-center w-full">
|
||||
<kbd class="kbd">W</kbd>
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-10 p-4 gap-4 flex items-end">
|
||||
{#each modes as modeValue}
|
||||
<button class="btn btn-outline" class:btn-active={$mode === modes.indexOf(modeValue)} on:click={() => changeMode(modeValue)}>
|
||||
{capitalize(modeValue)}
|
||||
</button>
|
||||
{/each}
|
||||
<div>
|
||||
{#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl}
|
||||
<label for="s1">S1</label>
|
||||
<input type="range" name="s1" min="0" max="100" on:input={(e) => handleRange(e, 's1')} class="range range-sm" />
|
||||
<label for="speed">Speed</label>
|
||||
<input type="range" name="speed" min="0" max="100" on:input={(e) => handleRange(e, 'speed')} class="range range-sm" />
|
||||
{/if}
|
||||
<label for="height">Height</label>
|
||||
<input type="range" name="height" min="0" max="100" on:input={(e) => handleRange(e, 'height')} class="range range-sm" />
|
||||
<div class="flex justify-center gap-2 w-full">
|
||||
<kbd class="kbd">A</kbd>
|
||||
<kbd class="kbd">S</kbd>
|
||||
<kbd class="kbd">D</kbd>
|
||||
</div>
|
||||
<div class="flex justify-center w-full"></div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-10 flex items-end">
|
||||
<div class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl">
|
||||
<VerticalSlider min={0} max={100} oninput={(e: Event) => handleRange(e, 'height')} />
|
||||
<label for="height">Ht</label>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden">
|
||||
<div class="join">
|
||||
{#each modes as modeValue}
|
||||
<button
|
||||
class="btn join-item"
|
||||
class:btn-primary={$mode === modes.indexOf(modeValue)}
|
||||
onclick={() => changeMode(modeValue)}>
|
||||
{capitalize(modeValue)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if $mode === ModesEnum.Walk || $mode === ModesEnum.Crawl}
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<label for="s1">S1</label>
|
||||
<input
|
||||
type="range"
|
||||
name="s1"
|
||||
min="0"
|
||||
max="25"
|
||||
oninput={e => handleRange(e, 's1')}
|
||||
class="range range-sm range-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="speed">Speed</label>
|
||||
<input
|
||||
type="range"
|
||||
name="speed"
|
||||
min="0"
|
||||
max="25"
|
||||
oninput={e => handleRange(e, 'speed')}
|
||||
class="range range-sm range-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<svelte:window on:keyup={handleKeyup} on:keydown={handleKeyup} />
|
||||
<svelte:window onkeyup={handleKeyup} onkeydown={handleKeyup} />
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { location } from '$lib/utilities';
|
||||
|
||||
let videoStream = `//${location}/api/stream`;
|
||||
|
||||
onDestroy(() => {
|
||||
videoStream = '#';
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
<img
|
||||
src={videoStream}
|
||||
class="absolute object-cover blur-3xl w-full h-full -z-10"
|
||||
alt="Live stream is down"
|
||||
/>
|
||||
<img src={videoStream} class="object-contain w-full h-full" alt="Live stream is down" />
|
||||
</div>
|
||||
@@ -1,111 +0,0 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import InputPassword from '$lib/components/InputPassword.svelte';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { fade, fly } from 'svelte/transition';
|
||||
import Login from '~icons/tabler/login';
|
||||
import { api } from '$lib/api';
|
||||
import type { JWT } from '$lib/models';
|
||||
|
||||
type SignInData = {
|
||||
password: string;
|
||||
username: string;
|
||||
};
|
||||
|
||||
let username = '';
|
||||
let password = '';
|
||||
|
||||
let loginFailed = false;
|
||||
|
||||
let token = { access_token: '' };
|
||||
|
||||
async function signInUser(data: SignInData) {
|
||||
const result = await api.post<JWT>('/api/signIn', data)
|
||||
if (result.isErr()){
|
||||
username = '';
|
||||
password = '';
|
||||
notifications.error('Wrong Username or Password!', 5000);
|
||||
loginFailed = true;
|
||||
setTimeout(() => {
|
||||
loginFailed = false;
|
||||
}, 1500);
|
||||
return
|
||||
}
|
||||
token = result.inner;
|
||||
user.init(token.access_token);
|
||||
username = $user.username;
|
||||
notifications.success('User ' + username + ' signed in', 5000);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="hero from-primary/30 to-secondary/30 min-h-screen bg-gradient-to-br">
|
||||
<div
|
||||
class="card lg:card-side bg-base-100 face shadow-2xl {loginFailed
|
||||
? 'failure border-error border-2'
|
||||
: ''}"
|
||||
in:fly={{ delay: 200, y: 100, duration: 500 }}
|
||||
out:fade={{ duration: 200 }}
|
||||
>
|
||||
<figure class="bg-base-200"><img src={logo} alt="Logo" class="h-auto w-48 lg:w-64" /></figure>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-2xl">Login</h2>
|
||||
<form class="form-control w-full max-w-xs">
|
||||
<label class="label" for="user">
|
||||
<span class="label-text text-md">Username</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full max-w-xs"
|
||||
id="user"
|
||||
bind:value={username}
|
||||
/>
|
||||
|
||||
<label class="label" for="pwd">
|
||||
<span class="label-text text-md">Password</span>
|
||||
</label>
|
||||
<InputPassword id="pwd" bind:value={password} />
|
||||
|
||||
<div class="card-actions mt-4 justify-end">
|
||||
<button
|
||||
class="btn btn-primary inline-flex items-center"
|
||||
on:click={() => {
|
||||
signInUser({ username, password });
|
||||
}}><Login class="mr-2 h-5 w-5" /><span>Login</span></button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.failure {
|
||||
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
|
||||
transform: translate3d(0, 0, 0);
|
||||
backface-visibility: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
@keyframes shake {
|
||||
10%,
|
||||
90% {
|
||||
transform: translatex(-1px);
|
||||
}
|
||||
|
||||
20%,
|
||||
80% {
|
||||
transform: translatex(2px);
|
||||
}
|
||||
|
||||
30%,
|
||||
50%,
|
||||
70% {
|
||||
transform: translatex(-4px);
|
||||
}
|
||||
|
||||
40%,
|
||||
60% {
|
||||
transform: translatex(4px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,276 +0,0 @@
|
||||
<script lang="ts">
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import MdiGithub from '~icons/mdi/github';
|
||||
import MdiConnection from '~icons/mdi/connection';
|
||||
import Users from '~icons/mdi/users';
|
||||
import Settings from '~icons/mdi/settings';
|
||||
import MdiController from '~icons/mdi/controller';
|
||||
import Devices from '~icons/mdi/devices'
|
||||
import Camera from '~icons/mdi/camera-outline';
|
||||
import Rotate3d from '~icons/mdi/rotate-3d';
|
||||
import MotorOutline from '~icons/mdi/motor-outline';
|
||||
import Health from '~icons/mdi/stethoscope';
|
||||
import Folder from '~icons/mdi/folder-outline';
|
||||
import Update from '~icons/mdi/reload';
|
||||
import WiFi from '~icons/mdi/wifi';
|
||||
import Router from '~icons/mdi/router';
|
||||
import AP from '~icons/mdi/access-point';
|
||||
import Remote from '~icons/mdi/network';
|
||||
import Avatar from '~icons/mdi/user-circle';
|
||||
import Logout from '~icons/mdi/logout';
|
||||
import Copyright from '~icons/mdi/copyright';
|
||||
import NTP from '~icons/mdi/clock-check';
|
||||
import Metrics from '~icons/mdi/report-bar';
|
||||
import { page } from '$app/stores';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const appName = $page.data.app_name;
|
||||
|
||||
const copyright = $page.data.copyright;
|
||||
|
||||
const github = { href: 'https://github.com/' + $page.data.github, active: true };
|
||||
|
||||
type menuItem = {
|
||||
title: string;
|
||||
icon: ConstructorOfATypedSvelteComponent;
|
||||
href?: string;
|
||||
feature: boolean;
|
||||
active?: boolean;
|
||||
submenu?: subMenuItem[];
|
||||
};
|
||||
|
||||
type subMenuItem = {
|
||||
title: string;
|
||||
icon: ConstructorOfATypedSvelteComponent;
|
||||
href: string;
|
||||
feature: boolean;
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
let menuItems = [
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: '/controller',
|
||||
feature: true,
|
||||
},
|
||||
{
|
||||
title: 'Peripherals',
|
||||
icon: Devices,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'I2C',
|
||||
icon: MdiConnection,
|
||||
href: '/peripherals/i2c',
|
||||
feature: true,
|
||||
},
|
||||
{
|
||||
title: 'Camera',
|
||||
icon: Camera,
|
||||
href: '/peripherals/camera',
|
||||
feature: $page.data.features.camera,
|
||||
},
|
||||
{
|
||||
title: 'Servo',
|
||||
icon: MotorOutline,
|
||||
href: '/peripherals/servo',
|
||||
feature: true,
|
||||
},
|
||||
{
|
||||
title: 'IMU',
|
||||
icon: Rotate3d,
|
||||
href: '/peripherals/imu',
|
||||
feature: $page.data.features.imu || $page.data.features.mag || $page.data.features.bmp,
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Connections',
|
||||
icon: Remote,
|
||||
feature: $page.data.features.ntp,
|
||||
submenu: [
|
||||
{
|
||||
title: 'NTP',
|
||||
icon: NTP,
|
||||
href: '/connections/ntp',
|
||||
feature: $page.data.features.ntp,
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
icon: WiFi,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: '/wifi/sta',
|
||||
feature: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: '/wifi/ap',
|
||||
feature: true,
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Users',
|
||||
icon: Users,
|
||||
href: '/user',
|
||||
feature: $page.data.features.security && $user.admin,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
icon: Settings,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: '/system/status',
|
||||
feature: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'File System',
|
||||
icon: Folder,
|
||||
href: '/system/filesystem',
|
||||
feature: true,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: '/system/metrics',
|
||||
feature: $page.data.features.analytics,
|
||||
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: '/system/update',
|
||||
feature:
|
||||
($page.data.features.ota ||
|
||||
$page.data.features.upload_firmware ||
|
||||
$page.data.features.download_firmware) &&
|
||||
(!$page.data.features.security || $user.admin),
|
||||
}
|
||||
]
|
||||
}
|
||||
] as menuItem[];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function setActiveMenuItem(targetTitle: string) {
|
||||
menuItems.forEach(item => {
|
||||
item.active = item.title === targetTitle;
|
||||
item.submenu?.forEach(subItem => {
|
||||
subItem.active = subItem.title === targetTitle;
|
||||
});
|
||||
});
|
||||
menuItems = menuItems
|
||||
dispatch('menuClicked');
|
||||
}
|
||||
|
||||
$: setActiveMenuItem($page.data.title);
|
||||
</script>
|
||||
|
||||
<div class="bg-base-200 text-base-content flex h-full w-80 flex-col p-4">
|
||||
<!-- Sidebar content here -->
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
|
||||
on:click={() => setActiveMenuItem('')}
|
||||
>
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||
</a>
|
||||
<ul class="menu rounded-box menu-vertical flex-nowrap overflow-y-auto">
|
||||
{#each menuItems as menuItem, i (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
<li>
|
||||
{#if menuItem.submenu}
|
||||
<details>
|
||||
<summary class="text-lg font-bold">
|
||||
<svelte:component this={menuItem.icon} class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
<ul>
|
||||
{#each menuItem.submenu as subMenuItem}
|
||||
{#if subMenuItem.feature}
|
||||
<li class="hover-bordered">
|
||||
<a
|
||||
href={subMenuItem.href}
|
||||
class:bg-base-100={subMenuItem.active}
|
||||
class="text-ml font-bold"
|
||||
on:click={() => {
|
||||
setActiveMenuItem(subMenuItem.title);
|
||||
menuItems = menuItems;
|
||||
}}
|
||||
><svelte:component
|
||||
this={subMenuItem.icon}
|
||||
class="h-5 w-5"
|
||||
/>{subMenuItem.title}</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</details>
|
||||
{:else}
|
||||
<a
|
||||
href={menuItem.href}
|
||||
class:bg-base-100={menuItem.active}
|
||||
class="text-lg font-bold"
|
||||
on:click={() => {
|
||||
setActiveMenuItem(menuItem.title);
|
||||
menuItems = menuItems;
|
||||
}}><svelte:component this={menuItem.icon} class="h-6 w-6" />{menuItem.title}</a
|
||||
>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<div class="flex-col" />
|
||||
<div class="flex-grow" />
|
||||
|
||||
{#if $page.data.features.security}
|
||||
<div class="flex items-center">
|
||||
<Avatar class="h-8 w-8" />
|
||||
<span class="flex-grow px-4 text-xl font-bold">{$user.username}</span>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="btn btn-ghost"
|
||||
on:click={() => {
|
||||
user.invalidate();
|
||||
}}
|
||||
>
|
||||
<Logout class="h-8 w-8 rotate-180" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="divider my-0" />
|
||||
<div class="flex items-center">
|
||||
{#if github.active}
|
||||
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer"
|
||||
><MdiGithub class="h-5 w-5" /></a
|
||||
>
|
||||
{/if}
|
||||
<div class="inline-flex flex-grow items-center justify-end text-sm">
|
||||
<Copyright class="h-4 w-4" /><span class="px-2">{copyright}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,17 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
||||
import Camera from '~icons/mdi/camera-outline'
|
||||
import Record from '~icons/mdi/radio-button-unchecked'
|
||||
import CameraSetting from './CameraSetting.svelte';
|
||||
import Stream from '$lib/components/Stream.svelte';
|
||||
import { Camera } from "$lib/components/icons";
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Camera slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">Camera</span>
|
||||
{#snippet icon()}
|
||||
<Camera class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span >Camera</span>
|
||||
{/snippet}
|
||||
<Stream />
|
||||
<CameraSetting />
|
||||
</SettingsCard>
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import type { CameraSettings } from '$lib/models';
|
||||
let settings:CameraSettings
|
||||
import type { CameraSettings } from '$lib/types/models';
|
||||
let settings:CameraSettings = $state()
|
||||
|
||||
const getCameraSettings = async () => {
|
||||
const result = await api.get<CameraSettings>('/api/camera/settings')
|
||||
@@ -27,7 +27,7 @@
|
||||
<Spinner />
|
||||
{:then _}
|
||||
<div class="flex flex-col gap-1">
|
||||
<button class="btn btn-primary" type="button" on:click={updateCameraSettings}>Update camera settings</button>
|
||||
<button class="btn btn-primary" type="button" onclick={updateCameraSettings}>Update camera settings</button>
|
||||
|
||||
<label for="brightness">
|
||||
Brightness {settings.brightness}
|
||||
|
||||
@@ -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,41 +1,78 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
||||
import MdiConnection from '~icons/mdi/connection';
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { socket } from "$lib/stores";
|
||||
import type { I2CDevice } from "$lib/types/models";
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import type { I2CDevice } from '$lib/types/models'
|
||||
import { Connection } from '$lib/components/icons'
|
||||
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: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
|
||||
]
|
||||
|
||||
let active_devices:I2CDevice[] = [];
|
||||
let active_devices: I2CDevice[] = $state([])
|
||||
|
||||
onMount(() => {
|
||||
socket.on('i2cScan', handleScan);
|
||||
socket.sendEvent('i2cScan', "");
|
||||
})
|
||||
let isLoading = $state(false)
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off('i2cScan', handleScan);
|
||||
})
|
||||
onMount(() => {
|
||||
socket.on('i2cScan', handleScan)
|
||||
triggerScan()
|
||||
return () => socket.off('i2cScan', handleScan)
|
||||
})
|
||||
|
||||
const handleScan = (data: any) => {
|
||||
active_devices = data.addresses.map((address:number) => i2cDevices.find(device => device.address === address))
|
||||
}
|
||||
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('i2cScan', '')
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<MdiConnection slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">I<sup>2</sup>C</span>
|
||||
{#snippet icon()}
|
||||
<Connection class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>I<sup>2</sup>C</span>
|
||||
{/snippet}
|
||||
{#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">
|
||||
{#each active_devices as device }
|
||||
<div>[{device.address.toString(16)}] {device.part_number} - {device.name}</div>
|
||||
{/each}
|
||||
</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 type { PeripheralsConfiguration } from '$lib/types/models'
|
||||
import { onMount } from 'svelte'
|
||||
import { modals } from 'svelte-modals'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
|
||||
let settings: PeripheralsConfiguration | null = $state(null)
|
||||
let isEditing = $state(false)
|
||||
|
||||
onMount(() => {
|
||||
socket.on('peripheralSettings', handleSettings)
|
||||
socket.sendEvent('peripheralSettings', '')
|
||||
return () => socket.off('peripheralSettings', handleSettings)
|
||||
})
|
||||
|
||||
const handleSettings = (data: any) => {
|
||||
settings = data
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm configuration',
|
||||
message:
|
||||
'Are you sure you want to save this configuration? The operation cannot be undone. Please make sure you have the correct settings.',
|
||||
labels: {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'Confirm', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close()
|
||||
socket.sendEvent('peripheralSettings', settings)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const Icon = $derived(isEditing ? EditOff : Edit)
|
||||
</script>
|
||||
|
||||
{#if settings}
|
||||
<div class="collapse bg-base-100 border-base-300 border">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title font-semibold">Configuration</div>
|
||||
<div class="collapse-content text-sm">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="sda" class="input validator">
|
||||
SDA
|
||||
|
||||
<input
|
||||
id="sda"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Type a number between 1 to 48"
|
||||
min="0"
|
||||
max="48"
|
||||
title="SDA pin number (0-48)"
|
||||
disabled={!isEditing}
|
||||
bind:value={settings.sda} />
|
||||
</label>
|
||||
<label for="scl" class="input validator">
|
||||
SCL
|
||||
|
||||
<input
|
||||
id="scl"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Type a number between 1 to 48"
|
||||
min="1"
|
||||
max="48"
|
||||
title="SCL pin number (0-48)"
|
||||
disabled={!isEditing}
|
||||
bind:value={settings.scl} />
|
||||
</label>
|
||||
<label class="input validator" for="frequency">
|
||||
Frequency
|
||||
<input
|
||||
id="frequency"
|
||||
type="number"
|
||||
required
|
||||
placeholder="Type a number between 100000 to 430000"
|
||||
min="100000"
|
||||
max="430000"
|
||||
title="I2C frequency in Hz"
|
||||
disabled={!isEditing}
|
||||
bind:value={settings.frequency} />
|
||||
</label>
|
||||
<div>
|
||||
<button class="btn btn-outline btn-primary" onclick={() => (isEditing = !isEditing)}>
|
||||
<Icon class="h-6 w-6" />
|
||||
</button>
|
||||
{#if isEditing}
|
||||
<button class="btn btn-outline btn-primary" onclick={handleSave}>Save</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,304 +1,253 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
||||
import Rotate3d from '~icons/mdi/rotate-3d';
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { slide } from "svelte/transition";
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import { daisyColor } from "$lib/DaisyUiHelper";
|
||||
import { socket } from "$lib/stores";
|
||||
import type { IMU } from "$lib/types/models";
|
||||
import { page } from "$app/stores";
|
||||
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 type { IMU } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Rotate3d } from '$lib/components/icons'
|
||||
|
||||
Chart.register(...registerables);
|
||||
Chart.register(...registerables)
|
||||
|
||||
let angleChartElement: HTMLCanvasElement;
|
||||
let angleChart: Chart;
|
||||
const features = useFeatureFlags()
|
||||
let intervalId: number
|
||||
|
||||
let tempChartElement: HTMLCanvasElement;
|
||||
let tempChart: Chart;
|
||||
let angleChartElement: HTMLCanvasElement = $state()
|
||||
let tempChartElement: HTMLCanvasElement = $state()
|
||||
let altitudeChartElement: HTMLCanvasElement = $state()
|
||||
|
||||
let altitudeChartElement: HTMLCanvasElement;
|
||||
let altitudeChart: Chart;
|
||||
let angleChart: Chart
|
||||
let tempChart: Chart
|
||||
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', intersect: false }
|
||||
},
|
||||
elements: { point: { radius: 1 } },
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: bgColor },
|
||||
ticks: { color: bgColor },
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 10,
|
||||
grid: { color: bgColor },
|
||||
ticks: { color: bgColor },
|
||||
border: { color: bgColor }
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const initializeCharts = () => {
|
||||
const colors = getChartColors()
|
||||
const baseConfig = createBaseChartConfig(colors.background)
|
||||
|
||||
angleChart = new Chart(angleChartElement, {
|
||||
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 ($page.data.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 ($page.data.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');
|
||||
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')
|
||||
}
|
||||
|
||||
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 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('imu', (data: IMU) => {
|
||||
console.log(data)
|
||||
imu.addData(data)
|
||||
})
|
||||
|
||||
initializeCharts()
|
||||
intervalId = setInterval(updateData, 200)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
socket.off('imu')
|
||||
clearInterval(intervalId)
|
||||
})
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
<Rotate3d slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
<span slot="title">IMU</span>
|
||||
{#if $page.data.features.imu}
|
||||
<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} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $page.data.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} />
|
||||
</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} />
|
||||
</div>
|
||||
{#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}
|
||||
<!-- <IMUSetting /> -->
|
||||
{/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,7 +1,12 @@
|
||||
<script lang="ts">
|
||||
import Servos from './servos.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 />
|
||||
<Servos bind:servoId bind:pwm />
|
||||
<ServoTable {servoId} {pwm} />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
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
|
||||
}
|
||||
})
|
||||
|
||||
const setCenterPWM = async () => {
|
||||
console.log('setCenterPWM', servoId, pwm)
|
||||
data.servos[servoId]['center_pwm'] = pwm
|
||||
await syncConfig()
|
||||
}
|
||||
</script>
|
||||
|
||||
<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,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Servo } from "$lib/models";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
export let servo: Servo;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const sweep = () => {
|
||||
dispatch('sweep', {channel: servo.channel});
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h2 class="text-lg">{ servo.name }</h2>
|
||||
<div class="flex gap-2 items-center">
|
||||
Is inverted <input type="checkbox" bind:checked={servo.inverted} class="toggle"/>
|
||||
</div>
|
||||
<div>
|
||||
Middle position <input type="number" bind:value={servo.center_angle} class="input input-bordered input-sm max-w-xs"/>
|
||||
</div>
|
||||
|
||||
<div class="relative mb-6">
|
||||
<label for="labels-range-input" class="sr-only">Labels range</label>
|
||||
<input id="labels-range-input" type="range" bind:value={servo.angle} min="0" max="180" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700">
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 absolute start-0 -bottom-6">0</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 absolute start-1/2 -translate-x-1/2 rtl:translate-x-1/2 -bottom-6">90</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-400 absolute end-0 -bottom-6">180</span>
|
||||
</div>
|
||||
<button class="btn btn-neutral btn-sm" on:click={sweep}>Sweep range</button>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user