1 Commits

Author SHA1 Message Date
Rune Harlyk d0b192a3e6 💅 Updates the frontpage 2025-03-08 13:06:56 +01:00
326 changed files with 12290 additions and 226944 deletions
-61
View File
@@ -1,61 +0,0 @@
name: Deploy GitHub Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./app
env:
BASE_PATH: /SpotMicroESP32-Leika
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
cache-dependency-path: "./app/pnpm-lock.yaml"
- run: pnpm install
- run: pnpm run build
- name: Setup Pages
uses: actions/configure-pages@v4
with:
static_site_generator: "sveltekit"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: app/build/
deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: github-pages
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
+9 -8
View File
@@ -2,19 +2,20 @@ name: PlatformIO CI
on:
push:
branches: [master]
branches: [ master ]
paths:
- "esp32/**"
- "platformio.ini"
- 'esp32/**'
pull_request:
branches: [master]
branches: [ master ]
paths:
- "esp32/**"
- "platformio.ini"
- 'esp32/**'
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./esp32
steps:
- uses: actions/checkout@v3
@@ -27,8 +28,8 @@ jobs:
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- run: pip install -r esp32/scripts/requirements.txt
python-version: '3.x'
- run: pip install -r ./scripts/requirements.txt
- name: Install PlatformIO Core
run: pip install --upgrade platformio
+2 -2
View File
@@ -2,7 +2,7 @@
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch
__pycache__/
*.py[cod]
*$py.class
.pio
*$py.class
+4 -4
View File
@@ -2,10 +2,10 @@
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode",
"platformio.platformio-ide",
"svelte.svelte-vscode"
"platformio.platformio-ide",
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
-3
View File
@@ -1,3 +0,0 @@
PUBLIC_VITE_USE_HOST_NAME=true
PUBLIC_USE_JSON=true
PUBLIC_USE_MSGPACK=true
+29 -29
View File
@@ -1,31 +1,31 @@
/** @type { import("eslint").Linter.Config } */
module.exports = {
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
}
root: true,
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:svelte/recommended',
'prettier'
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 2020,
extraFileExtensions: ['.svelte']
},
env: {
browser: true,
es2017: true,
node: true
},
overrides: [
{
files: ['*.svelte'],
parser: 'svelte-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser'
}
}
]
};
+1
View File
@@ -3,6 +3,7 @@ node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
+2 -1
View File
@@ -1,12 +1,13 @@
{
"useTabs": false,
"singleQuote": true,
"tabWidth": 4,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "avoid",
"experimentalTernaries": true,
"printWidth": 100,
"semi": false,
"svelteBracketNewLine": false,
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}
+1 -5
View File
@@ -1,7 +1,3 @@
{
"recommendations": [
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
]
"recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
}
+17 -8
View File
@@ -8,21 +8,30 @@ If you're seeing this, you've probably already done this step. Congrats!
```bash
# create a new project in the current directory
npx sv create
npm create svelte@latest
# create a new project in my-app
npm create svelte@latest my-app
```
## Developing
Once you've created your project, follow these steps:
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
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
```bash
npm run dev
Running `git status` should show:
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
[![example.png](https://i.postimg.cc/yddM3hH3/example.png)](https://postimg.cc/7CFsp2bq)
## Building
To create a production version of your app:
```bash
npm run build
```
You can preview the production build with `npm run preview`.
+6 -6
View File
@@ -1,8 +1,8 @@
declare module 'app-env' {
interface ENV {
VITE_USE_HOST_NAME: boolean
}
declare module "app-env" {
interface ENV {
VITE_USE_HOST_NAME: boolean;
}
const appEnv: ENV
export default appEnv
const appEnv: ENV;
export default appEnv;
}
+63 -63
View File
@@ -1,65 +1,65 @@
{
"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.2.3",
"@iconify-json/tabler": "^1.2.23",
"@playwright/test": "^1.56.0",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.46.4",
"@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^9.6.1",
"@types/three": "^0.180.0",
"@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^8.46.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^3.12.4",
"jsdom": "^27.0.0",
"prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.39.11",
"svelte-check": "^4.3.3",
"svelte-focus-trap": "^1.2.0",
"tailwindcss": "^4.1.14",
"tslib": "^2.8.1",
"typescript": "^5.9.3",
"unplugin-icons": "^22.4.2",
"vite": "^7.1.9",
"vitest": "^3.2.4"
},
"type": "module",
"dependencies": {
"@msgpack/msgpack": "^3.1.2",
"@niku/vite-env-caster": "^1.1.2",
"@sveltejs/adapter-auto": "^6.1.1",
"@tailwindcss/vite": "^4.1.14",
"chart.js": "^4.5.0",
"compare-versions": "^6.1.1",
"cross-env": "^10.1.0",
"daisyui": "^5.2.0",
"nipplejs": "^0.10.2",
"svelte-dnd-list": "^0.1.8",
"svelte-modals": "^2.0.1",
"three": "^0.180.0",
"urdf-loader": "^0.12.6",
"uzip": "^0.20201231.0",
"xacro-parser": "^0.3.10"
},
"packageManager": "pnpm@9.3.0"
"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"
},
"packageManager": "pnpm@9.3.0"
}
+9 -9
View File
@@ -1,12 +1,12 @@
import type { PlaywrightTestConfig } from '@playwright/test'
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'pnpm run build && pnpm run preview',
port: 4173
},
testDir: 'tests/integration',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
}
webServer: {
command: 'pnpm run build && pnpm run preview',
port: 4173
},
testDir: 'tests/integration',
testMatch: /(.+\.)?(test|spec)\.[jt]s/
};
export default config
export default config;
+2176 -1907
View File
File diff suppressed because it is too large Load Diff
+19 -27
View File
@@ -2,47 +2,39 @@
@plugin "daisyui";
@plugin "daisyui" {
themes:
light --default,
dark --prefersdark;
themes:
light --default,
dark --prefersdark;
}
@plugin "daisyui/theme" {
name: 'light';
default: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: white;
name: 'light';
default: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: white;
}
@plugin "daisyui/theme" {
name: 'dark';
prefersdark: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: oklch(0.3 0.012 256);
}
button {
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
name: 'dark';
prefersdark: true;
--color-primary: #00bfff;
--color-secondary: #3c00ff;
--base-content: oklch(0.3 0.012 256);
}
#nipple_0_0,
#nipple_1_1 {
z-index: 10 !important;
z-index: 10 !important;
}
#three-gui-panel {
top: 64px;
right: 0px;
top: 64px;
right: 0px;
}
@media (max-width: 1023px) {
#three-gui-panel {
top: 48px;
}
#three-gui-panel {
top: 48px;
}
}
+8 -8
View File
@@ -1,13 +1,13 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
}
}
export {}
export {};
+11 -14
View File
@@ -1,17 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo512.png" />
<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">
<div style="display: contents">%sveltekit.body%</div>
</body>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo512.png" />
<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">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+34 -33
View File
@@ -1,22 +1,22 @@
import { get } from 'svelte/store'
import { Err, Ok, type Result } from './utilities'
import { apiLocation } from './stores'
import { get } from 'svelte/store';
import { Err, Ok, type Result } from './utilities';
import { location } from './stores';
export const api = {
get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params)
},
export namespace api {
export function get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params);
}
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);
}
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);
}
remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE')
export function remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE');
}
}
@@ -26,8 +26,8 @@ async function sendRequest<TResponse>(
data?: unknown,
params?: RequestInit
): Promise<Result<TResponse, Error>> {
endpoint = resolveUrl(endpoint)
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,
@@ -38,42 +38,43 @@ async function sendRequest<TResponse>(
Authorization: 'Basic',
'Content-Type': 'application/json'
}
}
};
let response
let response;
try {
response = await fetch(endpoint, request)
} catch {
return Err.new(new Error(), 'An error has occurred')
response = await fetch(endpoint, request);
} catch (error) {
return Err.new(new Error(), 'An error has occurred');
}
const isResponseOk = response.status >= 200 && response.status < 400
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), 'User was not authorized');
}
return Err.new(new ApiError(response), 'An error has occurred')
return Err.new(new ApiError(response), 'An error has occurred');
}
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type')
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)
const data = await response.json();
return Ok.new(data as TResponse);
} else {
// Handle empty object as response
return Ok.new(null as TResponse)
return Ok.new(null as TResponse);
}
}
function resolveUrl(url: string): string {
if (url.startsWith('http') || !get(apiLocation)) return url
const protocol = window.location.protocol
return `${protocol}//${get(apiLocation)}${url.startsWith('/') ? '' : '/'}${url}`
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}`)
super(`${response.status}`);
}
}
+7 -7
View File
@@ -1,18 +1,18 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { Down } from './icons'
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { Down } from './icons';
function openCollapsible() {
open = !open
open = !open;
if (open) {
opened()
opened();
} else {
closed()
closed();
}
}
let { icon, title, children, open, opened, closed, class: klass } = $props()
let { icon, title, children, open, opened, closed, class: klass } = $props();
</script>
<div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
+20 -7
View File
@@ -1,8 +1,18 @@
<script lang="ts">
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'
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Cancel, Check } from '$lib/components/icons';
import { modals, exitBeforeEnter } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
title: string;
message: string;
onConfirm: any;
labels?: any;
}
let {
isOpen,
@@ -13,7 +23,7 @@
cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'OK', icon: Check }
}
}: ModalProps = $props()
}: Props = $props();
</script>
{#if isOpen}
@@ -34,12 +44,15 @@
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button
class="btn btn-error inline-flex items-center"
class="btn btn-primary inline-flex items-center"
onclick={() => modals.close()}
>
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
</button>
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
<button
class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onConfirm}
>
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
</button>
</div>
@@ -1,61 +1,61 @@
<script lang="ts">
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'
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 />
interface Props {
isOpen: boolean
isOpen: boolean;
}
let { isOpen }: Props = $props()
let { isOpen }: Props = $props();
let updating = $state(true)
let updating = $state(true);
let progress = $state(0)
let progress = $state(0);
$effect(() => {
if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress
progress = $telemetry.download_ota.progress;
}
})
});
$effect(() => {
if ($telemetry.download_ota.status == 'error') {
updating = false
updating = false;
}
})
});
let message = $state('Preparing ...')
let message = $state('Preparing ...');
$effect(() => {
if ($telemetry.download_ota.status == 'progress') {
message = 'Downloading ...'
message = 'Downloading ...';
} else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error
message = $telemetry.download_ota.error;
} else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...'
progress = 0
message = 'Restarting ...';
progress = 0;
// Reload page after 5 sec
setTimeout(() => {
modals.closeAll()
location.reload()
}, 5000)
modals.closeAll();
location.reload();
}, 5000);
}
})
});
onBeforeClose(() => {
if (updating) {
// prevents modal from closing
return false
return false;
} else {
$telemetry.download_ota.status = 'idle'
$telemetry.download_ota.error = ''
$telemetry.download_ota.progress = 0
return true
$telemetry.download_ota.status = 'idle';
$telemetry.download_ota.error = '';
$telemetry.download_ota.progress = 0;
return true;
}
})
});
</script>
{#if isOpen}
@@ -89,8 +89,8 @@
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating}
onclick={() => {
modals.closeAll()
location.reload()
modals.closeAll();
location.reload();
}}
>
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
+17 -9
View File
@@ -1,18 +1,26 @@
<script lang="ts">
import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'
import { Check } from './icons'
import { exitBeforeEnter, type ModalProps } from 'svelte-modals'
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Check } from './icons';
import { exitBeforeEnter } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
title: string;
message: string;
onDismiss: any;
dismiss?: any;
}
let {
isOpen,
title,
message,
onDismiss,
labels = {
dismiss: { label: 'Dismiss', icon: Check }
}
}: ModalProps = $props()
dismiss = { label: 'Dismiss', icon: Check }
}: Props = $props();
</script>
{#if isOpen}
@@ -35,7 +43,7 @@
class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}
>
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
<dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
</button>
</div>
</div>
@@ -1,78 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import * as THREE from 'three'
import { imu } from '$lib/stores/imu'
import SceneBuilder from '$lib/sceneBuilder'
let canvas: HTMLCanvasElement
let sceneBuilder: SceneBuilder
let cube: THREE.Mesh
let targetRotation = new THREE.Euler()
let lastUpdateTime = 0
const LERP_SPEED = 5 // rotations per second
const initThreeJS = () => {
sceneBuilder = new SceneBuilder()
.addRenderer({ canvas: canvas, antialias: true, alpha: true })
.addPerspectiveCamera({ x: 2, y: 0, z: 2 })
.addOrbitControls(1, 10, false)
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
.fillParent()
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.8
})
cube = new THREE.Mesh(geometry, material)
sceneBuilder.scene.add(cube)
sceneBuilder.addRenderCb(() => {
if (!cube) return
const currentTime = performance.now()
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
lastUpdateTime = currentTime
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
})
sceneBuilder.startRenderLoop()
}
const updateOrientation = () => {
if (!cube) return
const y = -$imu.x[$imu.x.length - 1] || 0
const x = $imu.y[$imu.y.length - 1] || 0
const z = -$imu.z[$imu.z.length - 1] || 0
targetRotation.set(
THREE.MathUtils.degToRad(x),
THREE.MathUtils.degToRad(y),
THREE.MathUtils.degToRad(z)
)
}
onMount(() => {
initThreeJS()
})
onDestroy(() => {
sceneBuilder?.renderer?.dispose()
})
$effect(() => {
if ($imu) {
updateOrientation()
}
})
</script>
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
<canvas class="w-full h-full" bind:this={canvas}></canvas>
</div>
+62 -69
View File
@@ -1,76 +1,69 @@
<script lang="ts">
import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'
import { Down } from './icons'
interface Props {
open?: boolean
collapsible?: boolean
icon?: import('svelte').Snippet
title?: import('svelte').Snippet
children?: import('svelte').Snippet
right?: import('svelte').Snippet
}
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { Down } from './icons';
interface Props {
open?: boolean;
collapsible?: boolean;
icon?: import('svelte').Snippet;
title?: import('svelte').Snippet;
children?: import('svelte').Snippet;
}
let {
open = $bindable(true),
collapsible = true,
icon,
title,
children,
right
}: Props = $props()
let {
open = $bindable(true),
collapsible = true,
icon,
title,
children
}: Props = $props();
</script>
{#if collapsible}
<div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline">
{@render icon?.()}
{@render title?.()}
</span>
<button
class="btn btn-circle btn-ghost btn-sm"
onclick={() => {
open = !open
}}
>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
open
) ?
'rotate-180'
: ''}"
/>
</button>
</div>
{#if open}
<div
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div>
<div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline">
{@render icon?.()}
{@render title?.()}
</span>
<button
class="btn btn-circle btn-ghost btn-sm"
onclick={() => {
open = !open;
}}
>
<Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
? 'rotate-180'
: ''}"
/>
</button>
</div>
{#if open}
<div
class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div>
{:else}
<div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div
class="min-h-16 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>
<div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div class="min-h-16 w-full p-4 text-xl font-medium">
<span class="inline-flex items-baseline">
{@render icon?.()}
{@render title?.()}
</span>
</div>
<div class="flex flex-col gap-2 p-4 pt-0">
{@render children?.()}
</div>
</div>
{/if}
-156
View File
@@ -1,156 +0,0 @@
<script lang="ts">
import { skill } from '$lib/stores'
import { onMount, onDestroy } from 'svelte'
let targetX = $state(0.5)
let targetZ = $state(0)
let targetYaw = $state(0)
let speed = $state(0.5)
const status = skill.status
const isActive = skill.isActive
const progress = skill.progress
const presets = [
{ name: 'Forward 0.5m', x: 0.5, z: 0, yaw: 0 },
{ name: 'Forward 1m', x: 1, z: 0, yaw: 0 },
{ name: 'Back 0.5m', x: -0.5, z: 0, yaw: 0 },
{ name: 'Left 0.5m', x: 0, z: 0.5, yaw: 0 },
{ name: 'Right 0.5m', x: 0, z: -0.5, yaw: 0 },
{ name: 'Turn Left 90°', x: 0, z: 0, yaw: 1.57 },
{ name: 'Turn Right 90°', x: 0, z: 0, yaw: -1.57 },
{ name: 'Turn 180°', x: 0, z: 0, yaw: 3.14 }
]
onMount(() => skill.init())
onDestroy(() => skill.destroy())
function executeSkill() {
skill.walk(targetX, targetZ, targetYaw, speed)
}
function runPreset(preset: (typeof presets)[0]) {
skill.walk(preset.x, preset.z, preset.yaw, speed)
}
function formatMeters(val: number): string {
return val.toFixed(3) + 'm'
}
function formatDegrees(rad: number): string {
return ((rad * 180) / Math.PI).toFixed(1) + '°'
}
</script>
<div class="card bg-base-200 shadow-xl">
<div class="card-body p-4">
<h2 class="card-title text-sm flex justify-between">
Skill Control
<span class="badge" class:badge-success={$isActive} class:badge-ghost={!$isActive}>
{$isActive ? 'Active' : 'Idle'}
</span>
</h2>
<div class="grid grid-cols-2 gap-2 text-xs mb-2">
<div class="stat bg-base-300 rounded-lg p-2">
<div class="stat-title text-xs">Position</div>
<div class="stat-value text-sm">
{formatMeters($status.x)}, {formatMeters($status.z)}
</div>
<div class="stat-desc">Yaw: {formatDegrees($status.yaw)}</div>
</div>
<div class="stat bg-base-300 rounded-lg p-2">
<div class="stat-title text-xs">Distance</div>
<div class="stat-value text-sm">{formatMeters($status.distance)}</div>
<div class="stat-desc">Total traveled</div>
</div>
</div>
{#if $isActive}
<div class="mb-2">
<div class="flex justify-between text-xs mb-1">
<span>Progress</span>
<span>{($progress * 100).toFixed(0)}%</span>
</div>
<progress class="progress progress-primary w-full" value={$progress} max="1"></progress>
<div class="text-xs text-base-content/60 mt-1">
Target: ({$status.skill_target_x.toFixed(2)}, {$status.skill_target_z.toFixed(2)}, {formatDegrees(
$status.skill_target_yaw
)})
</div>
</div>
{/if}
<div class="divider my-1 text-xs">Presets</div>
<div class="grid grid-cols-4 gap-1">
{#each presets as preset}
<button class="btn btn-xs btn-outline" onclick={() => runPreset(preset)}>
{preset.name}
</button>
{/each}
</div>
<div class="divider my-1 text-xs">Custom</div>
<div class="grid grid-cols-3 gap-2">
<div class="form-control">
<label class="label py-0" for="skill-x">
<span class="label-text text-xs">X (m)</span>
</label>
<input
id="skill-x"
type="number"
step="0.1"
bind:value={targetX}
class="input input-bordered input-xs w-full"
/>
</div>
<div class="form-control">
<label class="label py-0" for="skill-z">
<span class="label-text text-xs">Z (m)</span>
</label>
<input
id="skill-z"
type="number"
step="0.1"
bind:value={targetZ}
class="input input-bordered input-xs w-full"
/>
</div>
<div class="form-control">
<label class="label py-0" for="skill-yaw">
<span class="label-text text-xs">Yaw (rad)</span>
</label>
<input
id="skill-yaw"
type="number"
step="0.1"
bind:value={targetYaw}
class="input input-bordered input-xs w-full"
/>
</div>
</div>
<div class="form-control mt-2">
<label class="label py-0" for="skill-speed">
<span class="label-text text-xs">Speed: {speed.toFixed(2)}</span>
</label>
<input id="skill-speed" type="range" min="0.1" max="1" step="0.05" bind:value={speed} class="range range-xs range-primary" />
</div>
<div class="card-actions justify-between mt-2">
<div class="flex gap-1">
<button class="btn btn-xs btn-ghost" onclick={() => skill.resetPosition()}>Reset Pos</button>
</div>
<div class="flex gap-1">
<button class="btn btn-xs btn-error" onclick={() => skill.stop()} disabled={!$isActive}>
Stop
</button>
<button class="btn btn-xs btn-primary" onclick={executeSkill} disabled={$isActive}>
Execute
</button>
</div>
</div>
</div>
</div>
+4 -3
View File
@@ -1,8 +1,9 @@
<script lang="ts">
import { Loader } from './icons'
import { Loader } from "./icons";
</script>
<div class="flex h-full w-full flex-col items-center justify-center p-6">
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
<p class="text-xl">Loading...</p>
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
<p class="text-xl">Loading...</p>
</div>
-47
View File
@@ -1,47 +0,0 @@
<script lang="ts">
import type { ComponentType } from 'svelte'
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
const {
icon,
title,
description = '',
variant = 'primary',
class: klass = '',
children = null
} = $props<{
icon?: ComponentType
title: string
description?: string | number
variant?: Variant
class?: string
children?: () => ComponentType
}>()
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>
+4 -4
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import { onDestroy } from 'svelte'
import { apiLocation } from '$lib/stores'
import { onDestroy } from 'svelte';
import { location } from '$lib/stores';
let source = $state(`${$apiLocation}/api/camera/stream`)
let source = $state(`${$location}/api/camera/stream`);
onDestroy(() => (source = '#'))
onDestroy(() => (source = '#'));
</script>
<div class="w-full h-full">
+29 -31
View File
@@ -1,37 +1,35 @@
<script>
import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from './icons'
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { notifications } from '$lib/components/toasts/notifications';
import { error, info, success, warning } from './icons';
/** @type {{theme?: any, icon?: any}} */
let {
theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
},
icon = {
error: error,
success: success,
warning: warning,
info: info
}
} = $props()
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
}, 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 }}
>
<SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
{#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 }}
>
<SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
</div>
+324 -330
View File
@@ -1,346 +1,340 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import {
Mesh,
MeshBasicMaterial,
type Object3D,
SphereGeometry,
Vector3,
type Object3DEventMap,
Color
} from 'three'
import {
ModesEnum,
kinematicData,
mode,
model,
outControllerData,
servoAnglesOut,
servoAngles,
mpu,
jointNames,
currentKinematic,
walkGait,
walkGaitToMode
} from '$lib/stores'
import { 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 { type body_state_t } from '$lib/kinematic'
import { BezierState, CalibrationState, IdleState, RestState, StandState } from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store'
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 { footColor, populateModelCache, throttler, toeWorldPositions } 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'
interface Props {
defaultColor?: string | null
orbit?: boolean
panel?: boolean
debug?: boolean
ground?: boolean
interface Props {
sky?: boolean
orbit?: boolean
panel?: boolean
debug?: boolean
ground?: boolean
zoom?: number
}
let {
sky = true,
orbit = false,
panel = true,
debug = false,
ground = true,
zoom = 8
}: Props = $props()
let sceneManager = $state(new SceneBuilder())
let canvas: HTMLCanvasElement = $state()
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 feet_trace = new Array(4).fill([])
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
let target: Object3D<Object3DEventMap>
let target_position = { x: 0, z: 0, yaw: 0 }
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: planners[ModesEnum.Idle].default_feet_pos
}
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(Math.min(zoom, 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)
}
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
}
let {
defaultColor = '#0091ff',
orbit = false,
panel = true,
debug = false,
ground = true
}: Props = $props()
let sceneManager = $state(new SceneBuilder())
let canvas: HTMLCanvasElement
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: Object3D<Object3DEventMap>
let target_position = { x: 0, z: 0, yaw: 0 }
let kinematic = get(currentKinematic)
let planners = {
[ModesEnum.Deactivated]: new IdleState(),
[ModesEnum.Idle]: new IdleState(),
[ModesEnum.Calibration]: new CalibrationState(),
[ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(),
[ModesEnum.Walk]: new BezierState()
}
let lastTick = performance.now()
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
const THREEJS_SCALE = 10
let body_state = {
omega: 0,
phi: 0,
psi: 0,
xm: 0,
ym: 0.15,
zm: 0,
feet: kinematic.getDefaultFeetPos(),
cumulative_x: 0,
cumulative_y: 0,
cumulative_z: 0,
cumulative_roll: 0,
cumulative_pitch: 0,
cumulative_yaw: 0
}
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.15,
zm: 0,
Background: defaultColor
}
onMount(async () => {
await populateModelCache()
await createScene()
servoAngles.subscribe(updateAnglesFromStore)
walkGait.subscribe(gait => planners[ModesEnum.Walk].set_mode(walkGaitToMode(gait)))
if (panel) createPanel()
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])
})
}
onDestroy(() => {
canvas.remove()
gui_panel?.destroy()
})
const updateAnglesFromStore = (angles: number[]) => {
if (sceneManager.isDragging) return
if (settings['Internal kinematic']) return
modelTargetAngles = angles
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
}
const createPanel = () => {
gui_panel = new GUI({ width: 310 })
gui_panel.close()
gui_panel.domElement.id = 'three-gui-panel'
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
modelTargetAngles = new_angles
}
const general = gui_panel.addFolder('General')
general.add(settings, 'Internal kinematic')
general.add(settings, 'Robot transform controls')
general.add(settings, 'Auto orient robot')
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 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()
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
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').onChange(setSceneBackground).listen()
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.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.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 = 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]))
}
const updateKinematicPosition = () => {
kinematicData.set([
settings.omega,
settings.phi,
settings.psi,
settings.xm,
settings.ym,
settings.zm
])
}
const setSceneBackground = (c: string | null) => (sceneManager.scene.background = new Color(c!))
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(2, 20, orbit)
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
.addFogExp2(0xcccccc, 0.015)
.addModel($model as URDFRobot)
.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(angles => {
Object.entries(angles).forEach(([name, angle]) => {
updateAngles(name, angle)
})
})
}
if (defaultColor) setSceneBackground(settings['Background'] || defaultColor)
}
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,
cumulative_x: body_state.cumulative_x,
cumulative_y: body_state.cumulative_y,
cumulative_z: body_state.cumulative_z,
cumulative_roll: body_state.cumulative_roll,
cumulative_pitch: body_state.cumulative_pitch,
cumulative_yaw: body_state.cumulative_yaw
}
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))
const cumulativeYaw = body_state.cumulative_yaw
const cosYaw = Math.cos(cumulativeYaw)
const sinYaw = Math.sin(cumulativeYaw)
const rotatedXm = settings.xm * cosYaw - settings.zm * sinYaw
const rotatedZm = settings.xm * sinYaw + settings.zm * cosYaw
robot.position.x = smooth(
robot.position.x,
(-rotatedZm - body_state.cumulative_z) * THREEJS_SCALE,
0.1
)
robot.position.z = smooth(
robot.position.z,
(-rotatedXm - body_state.cumulative_x) * THREEJS_SCALE,
0.1
)
const pitch = degToRad(settings.psi - 90) + body_state.cumulative_pitch
const roll = degToRad(settings.omega) + body_state.cumulative_roll
robot.rotation.z = smooth(
robot.rotation.z,
degToRad(-settings.phi + $mpu.heading + 90) + cumulativeYaw,
0.1
)
robot.rotation.y = smooth(robot.rotation.y, roll, 0.1)
robot.rotation.x = smooth(robot.rotation.x, pitch, 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 = {
lx: controlData[0],
ly: controlData[1],
rx: controlData[2],
ry: controlData[3],
h: controlData[4],
s: controlData[5],
s1: controlData[6]
}
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 / THREEJS_SCALE
settings.zm = -robot.position.x / THREEJS_SCALE
}
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)
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 onresize={sceneManager.fillParent} />
+2 -6
View File
@@ -35,9 +35,6 @@ 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'
@@ -53,14 +50,13 @@ 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 DNS } from '~icons/tabler/address-book'
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 Edit } from '~icons/tabler/pencil'
export { default as Delete } from '~icons/tabler/trash'
export { default as Network } from '~icons/tabler/router'
@@ -1,19 +1,19 @@
<script lang="ts">
import { MdiEyeOffOutline, MdiEyeOutline } from '../icons'
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons";
interface Props {
show?: boolean
value?: string
id?: string
show?: boolean;
value?: string;
id?: string;
}
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props()
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props();
let type = $derived(show ? 'text' : 'password');
let type = $derived(show ? 'text' : 'password')
const handleInput = (e: any) => value = e.target.value
const handleInput = (e: Event) => (value = (e.target as HTMLInputElement).value)
const togglePassword = () => (show = !show)
const togglePassword = () => show = !show
</script>
<label class="input input-bordered flex items-center gap-2">
@@ -23,4 +23,4 @@
<MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" />
<MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" />
</div>
</label>
</label>
@@ -1,35 +1,34 @@
<script lang="ts">
interface Props {
min?: number
max?: number
step?: number
value?: number
oninput?: (value: number) => void
}
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()
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}
/>
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);
}
input[type='range']::-webkit-slider-runnable-track {
background: oklch(var(--p) / 1);
border-radius: var(--rounded-box, 1rem);
}
</style>
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as PasswordInput } from './InputPassword.svelte'
export { default as VerticalSlider } from './VerticalSlider.svelte'
export { default as PasswordInput } from './InputPassword.svelte';
export { default as VerticalSlider } from './VerticalSlider.svelte';
+2 -2
View File
@@ -1,9 +1,9 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet
children?: import('svelte').Snippet;
}
let { children }: Props = $props()
let { children }: Props = $props();
</script>
<div class="box-border overflow-hidden flex-1">
@@ -1,41 +1,37 @@
<script lang="ts">
import WidgetContainer from './WidgetContainer.svelte'
import {
WidgetComponents,
type WidgetContainerConfig,
isWidgetConfig
} from '$lib/stores/application'
import Widget from './Widget.svelte'
import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
import Widget from './Widget.svelte';
interface Props {
container: WidgetContainerConfig
}
interface Props {
container: WidgetContainerConfig;
}
let { container }: Props = $props()
let { container }: Props = $props();
</script>
<div class="w-full h-full flex flex-col overflow-hidden">
<div
class="flex w-full h-full"
class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}
>
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
{@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets}
<WidgetContainer container={widget} />
{/if}
</Widget>
{#if index !== container.widgets.length - 1}
<div
class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}
></div>
{/if}
{/each}
</div>
<div
class="flex w-full h-full"
class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}
>
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
{@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets}
<WidgetContainer container={widget} />
{/if}
</Widget>
{#if index !== container.widgets.length - 1}
<div
class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}
></div>
{/if}
{/each}
</div>
</div>
@@ -1,15 +1,15 @@
<script lang="ts">
import { Github } from '../icons'
import { Github } from "../icons";
interface Props {
github: { url: string; version: string; active?: boolean; href?: string }
}
interface Props {
github: any;
}
let { github }: Props = $props()
let { github }: Props = $props();
</script>
{#if github.active}
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer">
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer">
<Github class="h-5 w-5" />
</a>
{/if}
@@ -1,15 +1,14 @@
<script>
import logo from '$lib/assets/logo512.png'
import { resolve } from '$app/paths'
import logo from '$lib/assets/logo512.png';
/** @type {{appName: any}} */
let { appName } = $props()
/** @type {{appName: any}} */
let { appName } = $props();
</script>
<a
href={resolve('/')}
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
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>
<img src={logo} alt="Logo" class="h-12 w-12" />
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
</a>
+157 -179
View File
@@ -1,200 +1,178 @@
<script lang="ts">
import { page } from '$app/state'
import { base } from '$app/paths'
import { useFeatureFlags } from '$lib/stores/featureFlags'
import GithubButton from '../menu/GithubButton.svelte'
import LogoButton from '../menu/LogoButton.svelte'
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 { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
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
} from '$lib/components/icons'
import appEnv from 'app-env'
const features = useFeatureFlags()
const features = useFeatureFlags()
const appName = page.data.app_name
const appName = page.data.app_name
const copyright = page.data.copyright
const copyright = page.data.copyright
const github = { href: 'https://github.com/' + page.data.github, active: true }
const github = { href: 'https://github.com/' + page.data.github, active: true }
import type { ComponentType } from 'svelte'
type menuItem = {
title: string
icon: ConstructorOfATypedSvelteComponent
href?: string
feature: boolean
active?: boolean
submenu?: menuItem[]
}
type menuItem = {
title: string
icon: ComponentType
href?: string
feature: boolean
active?: boolean
submenu?: menuItem[]
}
let menuItems = $state<menuItem[]>([])
function withBase(path: string) {
return `${base}${path.startsWith('/') ? path : '/' + path}`
}
$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: '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: $features.analytics
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
feature: $features.ota || $features.upload_firmware || $features.download_firmware
}
]
}
] as menuItem[]
})
let menuItems = $state<menuItem[]>([])
const { menuClicked } = $props()
$effect(() => {
menuItems = [
{
title: 'Connection',
icon: WiFi,
href: withBase('/connection'),
feature: !PUBLIC_VITE_USE_HOST_NAME
},
{
title: 'Controller',
icon: MdiController,
href: withBase('/controller'),
feature: true
},
{
title: 'Peripherals',
icon: Devices,
feature: true,
submenu: [
{
title: 'I2C',
icon: Connection,
href: withBase('/peripherals/i2c'),
feature: true
},
{
title: 'Camera',
icon: Camera,
href: withBase('/peripherals/camera'),
feature: $features.camera
},
{
title: 'Servo',
icon: MotorOutline,
href: withBase('/peripherals/servo'),
feature: true
},
{
title: 'IMU',
icon: Rotate3d,
href: withBase('/peripherals/imu'),
feature: $features.imu || $features.mag || $features.bmp
}
]
},
{
title: 'WiFi',
icon: WiFi,
feature: true,
submenu: [
{
title: 'WiFi Station',
icon: Router,
href: withBase('/wifi/sta'),
feature: true
},
{
title: 'Access Point',
icon: AP,
href: withBase('/wifi/ap'),
feature: true
},
{
title: 'mDNS',
icon: DNS,
href: withBase('/wifi/mdns'),
feature: true
}
]
},
{
title: 'System',
icon: Settings,
feature: true,
submenu: [
{
title: 'System Status',
icon: Health,
href: withBase('/system/status'),
feature: true
},
{
title: 'File System',
icon: Folder,
href: withBase('/system/filesystem'),
feature: true
},
{
title: 'System Metrics',
icon: Metrics,
href: withBase('/system/metrics'),
feature: true
},
{
title: 'Firmware Update',
icon: Update,
href: withBase('/system/update'),
feature:
$features.ota ||
$features.upload_firmware ||
$features.download_firmware
}
]
}
] as menuItem[]
function setActiveMenuItem(targetTitle: string) {
menuItems.forEach(item => {
item.active = item.title === targetTitle
item.submenu?.forEach(subItem => {
subItem.active = subItem.title === targetTitle
})
})
menuItems = menuItems
menuClicked()
}
const { menuClicked } = $props()
$effect(() => {
setActiveMenuItem(page.data.title)
})
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: CustomEvent) => {
setActiveMenuItem(event.details)
}
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} />
<LogoButton {appName} />
<MenuList
{menuItems}
select={updateMenu}
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
level="0"
/>
<MenuList {menuItems} select={updateMenu} class="grow flex-nowrap overflow-y-auto" level="0" />
<div class="divider my-0"></div>
<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 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>
+41 -49
View File
@@ -1,56 +1,48 @@
<script lang="ts">
import MenuList from './MenuList.svelte'
import type { ComponentType } from 'svelte'
import MenuList from './MenuList.svelte'
type MenuItem = {
title: string
icon: ConstructorOfATypedSvelteComponent
href?: string
feature: boolean
active?: boolean
submenu?: MenuItem[]
}
type MenuItem = {
title: string
icon: ComponentType
href?: string
feature: boolean
active?: boolean
submenu?: MenuItem[]
}
let { level, menuItems, select, class: klass } = $props()
let { level, menuItems, select, class: klass } = $props()
const selectMenuItem = (title: string) => {
select(title)
}
const selectMenuItem = (title: string) => {
select(title)
}
</script>
<ul class={klass + ' menu w-full'}>
{#each menuItems as MenuItem[] as menuItem (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>
<ul class={klass + ' menu'}>
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
{#if menuItem.feature}
<li>
{#if menuItem.submenu}
<details open={menuItem.submenu.some(subItem => subItem.active)}>
<summary class="text-lg font-bold">
<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}
{/each}
</li>
{/if}
{/each}
</ul>
@@ -1,10 +1,10 @@
<script lang="ts">
import { isFullscreen, toggleFullscreen } from '$lib/stores'
import { MdiFullscreenExit, MdiFullscreen } from '../icons'
import { isFullscreen, toggleFullscreen } from '$lib/stores';
import { MdiFullscreenExit, MdiFullscreen } from '../icons';
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen)
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
</script>
<button onclick={toggleFullscreen}>
<SvelteComponent class="h-7 w-7" />
</button>
</button>
@@ -1,33 +1,33 @@
<script lang="ts">
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from '../icons'
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
interface Props {
showDBm?: boolean
rssi?: number
}
interface Props {
showDBm?: boolean;
rssi?: number;
}
let { showDBm = false, rssi = 0 }: Props = $props()
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 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())
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>
<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>
@@ -1,13 +1,13 @@
<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'
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 features = useFeatureFlags();
const postSleep = async () => await api.post('/api/system/sleep')
const postSleep = async () => await api.post('/api/system/sleep');
const confirmSleep = () => {
modals.open(ConfirmDialog, {
@@ -18,11 +18,11 @@
confirm: { label: 'Switch Off', icon: Power }
},
onConfirm: () => {
modals.close()
postSleep()
modals.close();
postSleep();
}
})
}
});
};
</script>
{#if $features.sleep}
@@ -1,9 +1,10 @@
<script lang="ts">
import { mode, modes } from '$lib/stores'
import { mode, modes } from "$lib/stores";
const deactivate = async () => {
mode.set(modes.indexOf('deactivated'))
}
mode.set(modes.indexOf('deactivated'));
};
</script>
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
@@ -1,9 +1,9 @@
<script lang="ts">
import { MdiWeatherSunny, MdiMoonAndStars } from '../icons'
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>
</label>
@@ -1,18 +1,17 @@
<script lang="ts">
import { Hamburger } from '../icons'
import { resolve } from '$app/paths'
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={resolve('/')}>
<Hamburger class="h-8 w-8" />
<div class="flex gap-2 p-2">
<a href="/">
<Hamburger class="h-8 w-8"/>
</a>
</div>
</div>
<style>
.topbar {
height: 50px;
}
.topbar {
height: 50px;
}
</style>
@@ -1,80 +1,80 @@
<script lang="ts">
import { page } from '$app/state'
import { modals } from 'svelte-modals'
import { notifications } from '$lib/components/toasts/notifications'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'
import { compareVersions } from 'compare-versions'
import { onMount } from 'svelte'
import { api } from '$lib/api'
import type { GithubRelease } from '$lib/types/models'
import { useFeatureFlags } from '$lib/stores/featureFlags'
import { Cancel, CloudDown, Firmware } from '../icons'
import { page } from '$app/state';
import { modals } from 'svelte-modals';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/types/models';
import { useFeatureFlags } from '$lib/stores/featureFlags';
import { Cancel, CloudDown, Firmware } from '../icons';
const features = useFeatureFlags()
const features = useFeatureFlags();
interface Props {
update?: boolean
update?: boolean;
}
let { update = $bindable(false) }: Props = $props()
let { update = $bindable(false) }: Props = $props();
let firmwareVersion: string = $state('')
let firmwareDownloadLink: string = $state('')
let firmwareVersion: string = $state('');
let firmwareDownloadLink: string = $state('');
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
}
};
const result = await api.get<GithubRelease>(
`https://api.github.com/repos/${page.data.github}/releases/latest`,
{ headers }
)
);
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
console.warn('Error: Could not find releases in the repository')
return
console.warn('Error: Could not find releases in the repository');
return;
}
if (result.isErr()) {
console.error('Error:', result.inner)
return
console.error('Error:', result.inner);
return;
}
const results = result.inner
update = false
firmwareVersion = ''
const results = result.inner;
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
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 as string)
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)
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
}
}
async function postGithubDownload(url: string) {
const result = await api.post('/api/downloadUpdate', { download_url: url })
const result = await api.post('/api/downloadUpdate', { download_url: url });
if (result.isErr()) {
console.error('Error:', result.inner)
return
console.error('Error:', result.inner);
return;
}
}
onMount(async () => {
if ($features.download_firmware) {
await getGithubAPI()
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000) // once per hour
await getGithubAPI();
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
}
})
});
function confirmGithubUpdate(url: string) {
modals.open(ConfirmDialog, {
@@ -85,12 +85,12 @@
confirm: { label: 'Update', icon: CloudDown }
},
onConfirm: () => {
postGithubDownload(url)
postGithubDownload(url);
modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll()
})
});
}
})
});
}
</script>
@@ -1,6 +1,6 @@
<script lang="ts">
import { selectedView, views } from '$lib/stores/application'
import Selector from '../widget/Selector.svelte'
import { selectedView, views } from "$lib/stores/application";
import Selector from "../widget/Selector.svelte";
</script>
<Selector bind:selectedOption={$selectedView} options={$views.map(v => v.name)} />
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} />
@@ -1,38 +1,38 @@
<script lang="ts">
import { page } from '$app/state'
import { telemetry } from '$lib/stores/telemetry'
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'
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>
<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 />
<UpdateIndicator />
<FullscreenButton />
<FullscreenButton />
<ThemeButton />
<ThemeButton />
<RssiIndicator rssi={$telemetry.rssi.rssi} />
<RssiIndicator rssi={$telemetry.rssi.rssi} />
<SleepButton />
<SleepButton />
<StopButton />
<StopButton />
</div>
+29 -31
View File
@@ -1,37 +1,35 @@
<script>
import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from '../icons'
import { flip } from 'svelte/animate';
import { fly } from 'svelte/transition';
import { notifications } from '$lib/components/toasts/notifications';
import { error, info, success, warning } from '../icons';
/** @type {{theme?: any, icon?: any}} */
let {
theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
},
icon = {
error: error,
success: success,
warning: warning,
info: info
}
} = $props()
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error',
success: 'alert-success',
warning: 'alert-warning',
info: 'alert-info'
}, 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 }}
>
<SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
{#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 }}
>
<SvelteComponent class="h-6 w-6 shrink-0" />
<span>{notification.message}</span>
</div>
{/each}
</div>
+30 -30
View File
@@ -1,42 +1,42 @@
import { writable } from 'svelte/store'
import { writable, derived, type Writable } from 'svelte/store';
type StateType = 'info' | 'success' | 'warning' | 'error'
type StateType = 'info' | 'success' | 'warning' | 'error';
type State = {
id: string
type: StateType
message: string
}
id: string;
type: StateType;
message: string;
};
function createNotificationStore() {
const state: State[] = []
const notifications = writable(state)
const { subscribe } = notifications
const state: State[] = [];
const notifications = writable(state);
const { subscribe } = notifications;
function send(message: string, type: StateType = 'info', timeout: number) {
const id = generateId()
setTimeout(() => {
notifications.update(state => {
return state.filter(n => n.id !== id)
})
}, timeout)
notifications.update(state => {
return [...state, { id, type, message }]
})
}
function send(message: string, type: StateType = 'info', timeout: number) {
const id = generateId();
setTimeout(() => {
notifications.update((state) => {
return state.filter((n) => n.id !== id);
});
}, timeout);
notifications.update((state) => {
return [...state, { id, type, message }];
});
}
return {
subscribe,
send,
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
}
return {
subscribe,
send,
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
};
}
function generateId() {
return '_' + Math.random().toString(36).substr(2, 9)
return '_' + Math.random().toString(36).substr(2, 9);
}
export const notifications = createNotificationStore()
export const notifications = createNotificationStore();
@@ -1,97 +1,98 @@
<script lang="ts">
import { daisyColor } from '$lib/utilities'
import { Chart, registerables } from 'chart.js'
import { onMount } from 'svelte'
import { cubicOut } from 'svelte/easing'
import { slide } from 'svelte/transition'
import { daisyColor } from "$lib/utilities";
import { Chart, registerables } from "chart.js";
import { onMount } from "svelte";
import { cubicOut } from "svelte/easing";
import { slide } from "svelte/transition";
let chartElement: HTMLCanvasElement
let chart: Chart<'line', number[], number>
let chartElement: HTMLCanvasElement = $state();
let chart: Chart;
interface Props {
label: string
data: number[]
title: string
}
interface Props {
label: any;
data: number[];
title: any;
}
let { label, data, title }: Props = $props()
let { label, data, title }: Props = $props();
Chart.register(...registerables)
Chart.register(...registerables);
onMount(() => {
chart = new Chart(chartElement, {
type: 'line',
data: {
labels: data,
datasets: [
{
label,
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
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) }
}
}
}
})
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)
}, 500);
})
</script>
<div class="w-full h-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
@@ -99,4 +100,4 @@
>
<canvas bind:this={chartElement}></canvas>
</div>
</div>
</div>
@@ -1,12 +1,12 @@
<script lang="ts">
interface Props {
options?: string[]
selectedOption?: string
change?: () => void
[key: string]: unknown
options?: string[];
selectedOption?: string;
change: () => void;
[key: string]: any;
}
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props()
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
</script>
<select
+312 -368
View File
@@ -1,346 +1,332 @@
import { get } from 'svelte/store'
import type { body_state_t } from './kinematic'
import { currentKinematic } from './stores/featureFlags'
import type { body_state_t } from './kinematic';
import { fromInt8 } from './utilities';
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 {
lx: number
ly: number
rx: number
ry: number
h: number
s: number
s1: 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 dt = 0.02
protected body_state!: body_state_t
protected get kinematic() {
return get(currentKinematic)
}
protected abstract name: string;
protected dt = 0.02;
protected body_state!: body_state_t;
protected gait_state: gait_state_t = {
step_height: 0,
step_height: 0.4,
step_x: 0,
step_z: 0,
step_angle: 0,
step_velocity: 1,
step_depth: 0
}
step_depth: 0.002
};
public get default_feet_pos() {
return this.kinematic.getDefaultFeetPos()
return [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
];
}
protected get default_height() {
return this.kinematic.default_body_height
}
protected get default_step_depth() {
return this.kinematic.default_step_depth
}
protected get default_step_height() {
return this.kinematic.default_step_height
return 0.5;
}
begin() {
console.log('Starting', this.name)
console.log('Starting', this.name);
}
end() {
console.log('Ending', this.name)
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
if (body_state.cumulative_x === undefined) {
body_state.cumulative_x = 0
body_state.cumulative_y = 0
body_state.cumulative_z = 0
body_state.cumulative_roll = 0
body_state.cumulative_pitch = 0
body_state.cumulative_yaw = 0
}
return body_state
this.map_command(command);
this.body_state = body_state;
this.dt = dt / 1000;
return body_state;
}
map_command(command: ControllerCommand) {
const kin = this.kinematic
this.gait_state = {
step_height: command.s1 * kin.max_step_height,
step_x: command.ly * kin.max_step_length,
step_z: -command.lx * kin.max_step_length,
step_velocity: command.s,
step_angle: command.rx,
step_depth: kin.default_step_depth
}
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'
step(body_state: body_state_t, command: ControllerCommand) {
super.step(body_state, command)
return body_state
}
protected name = 'Idle';
}
export class CalibrationState extends GaitState {
protected name = 'Calibration'
protected name = 'Calibration';
step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command)
body_state.omega = 0
body_state.phi = 0
body_state.psi = 0
body_state.xm = 0
body_state.ym = this.kinematic.max_body_height
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 * 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) {
super.step(body_state, _command)
body_state.omega = 0
body_state.phi = 0
body_state.psi = 0
body_state.xm = 0
body_state.ym = this.kinematic.min_body_height
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) {
super.step(body_state, command)
const kin = this.kinematic
body_state.omega = 0
body_state.ym = kin.min_body_height + command.h * kin.body_height_range
body_state.psi = command.ry * kin.max_pitch
body_state.phi = command.rx * kin.max_roll
body_state.xm = command.ly * kin.max_body_shift_x
body_state.zm = command.lx * kin.max_body_shift_z
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;
}
}
export class BezierState extends GaitState {
protected name = 'Bezier'
protected phase = 0
protected phase_num = 0
protected step_length = 0
protected stand_offset = 0.75
protected mode: 'crawl' | 'trot' = 'trot'
protected speed_factor = 1
offset = [0, 0.5, 0.75, 0.25]
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 shift_start_pos = { x: 0, z: 0 }
protected shift_target_pos = { x: 0, z: 0 }
protected shift_start_time = 0
protected current_shift_leg = -1
protected last_body_state: body_state_t | null = null
protected cumulative_position = { x: 0, y: 0, z: 0 }
protected cumulative_orientation = { roll: 0, pitch: 0, yaw: 0 }
constructor() {
super()
this.set_mode(this.mode)
}
begin() {
super.begin()
}
set_mode(mode: 'crawl' | 'trot', duty?: number, order?: [number, number, number, number]) {
console.log('BezierState set_mode', mode)
this.mode = mode
if (mode === 'crawl') {
this.speed_factor = 0.5
this.stand_offset = duty ?? 0.85
const o = order ?? [3, 0, 2, 1]
const base = [0, 0.25, 0.5, 0.75]
const offsets = new Array(4).fill(0)
for (let i = 0; i < 4; i++) offsets[o[i]] = base[i]
this.offset = offsets
} else {
this.speed_factor = 2
this.stand_offset = duty ?? 0.6
this.offset = order ? (order.map(v => v % 1) as number[]) : [0, 0.5, 0.5, 0]
}
}
end() {
super.end()
}
protected contact_phases!: number[][];
protected shifts!: number[][];
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
super.step(body_state, command, dt)
const kin = this.kinematic
this.body_state.ym = kin.min_body_height + command.h * kin.body_height_range
this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2)
if (this.gait_state.step_x < 0) this.step_length = -this.step_length
this.update_phase()
this.update_body_position()
this.update_feet_positions()
this.update_cumulative_position()
return this.body_state
super.step(body_state, command, dt);
this.update_phase();
this.update_body_position();
this.update_feet_positions();
return this.body_state;
}
update_phase() {
const m = this.gait_state
if (m.step_x === 0 && m.step_z === 0 && m.step_angle === 0) {
this.phase = 0
return
}
this.phase += this.dt * m.step_velocity * this.speed_factor
if (this.phase >= 1) {
this.phase_num = (this.phase_num + 1) % 2
this.phase = 0
this.phase_time += this.dt * this.phase_speed_factor * this.gait_state.step_velocity;
if (this.phase_time >= 1) {
this.phase += 1;
if (this.phase == this.num_phases) this.phase = 0;
this.phase_time = 0;
}
}
update_body_position() {
const m = this.gait_state
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
if (!moving) return
if (this.num_phases === 4) return;
if (this.mode !== 'crawl') return
const shift = this.shifts[Math.floor(this.phase / 2)];
const { stance, swing, next_swing, time_to_lift } = this.get_leg_states()
if (stance.length >= 3 && swing.length === 0 && next_swing !== -1) {
if (this.current_shift_leg !== next_swing) {
this.current_shift_leg = next_swing
this.shift_start_pos.x = this.body_state.xm
this.shift_start_pos.z = this.body_state.zm
const remaining_legs = stance.filter(leg => leg !== next_swing)
const target = this.stance_centroid(remaining_legs)
this.shift_target_pos.x = target[0]
this.shift_target_pos.z = target[2]
this.shift_start_time = time_to_lift
}
const total_time = this.shift_start_time
const progress = total_time > 0 ? 1 - time_to_lift / total_time : 1
const smooth_progress = this.smoothstep01(Math.max(0, Math.min(1, progress)))
this.body_state.xm = this.lerp(
this.shift_start_pos.x,
this.shift_target_pos.x,
smooth_progress
)
this.body_state.zm = this.lerp(
this.shift_start_pos.z,
this.shift_target_pos.z,
smooth_progress
)
}
}
protected lerp(a: number, b: number, t: number): number {
return a + (b - a) * t
}
protected stance_centroid(legs: number[]): number[] {
if (legs.length === 0) return [this.body_state.xm, 0, this.body_state.zm]
let sx = 0,
sz = 0
for (const i of legs) {
sx += this.body_state.feet[i][0]
sz += this.body_state.feet[i][2]
}
return [sx / legs.length, 0, sz / legs.length]
}
protected get_leg_states(): {
stance: number[]
swing: number[]
next_swing: number
time_to_lift: number
} {
const stance: number[] = []
const swing: number[] = []
let next_swing = -1
let min_time_to_swing = Infinity
for (let i = 0; i < 4; i++) {
let phase = this.phase + this.offset[i]
if (phase >= 1) phase -= 1
if (phase <= this.stand_offset) {
stance.push(i)
const time_to_swing = this.stand_offset - phase
if (time_to_swing < min_time_to_swing) {
min_time_to_swing = time_to_swing
next_swing = i
}
} else {
swing.push(i)
}
}
return { stance, swing, next_swing, time_to_lift: min_time_to_swing }
}
protected smoothstep01(t: number): number {
const x = Math.max(0, Math.min(1, t))
return x * x * (3 - 2 * x)
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)
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 <= this.stand_offset ?
this.stand_controller(index, phase / this.stand_offset)
: this.swing_controller(index, (phase - this.stand_offset) / (1 - this.stand_offset))
const contact = this.contact_phases[index][this.phase];
return contact ? this.stand(index) : this.swing(index);
}
stand(index: number): number[] {
const delta_pos = [
-this.gait_state.step_x * this.dt * this.swing_stand_ratio,
0,
-this.gait_state.step_z * this.dt * this.swing_stand_ratio
];
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0];
this.body_state.feet[index][1] = this.default_feet_pos[index][1];
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
return this.body_state.feet[index];
}
swing(index: number): number[] {
const delta_pos = [this.gait_state.step_x * this.dt, 0, this.gait_state.step_z * this.dt];
if (this.gait_state.step_x == 0) {
delta_pos[0] =
(this.default_feet_pos[index][0] - this.body_state.feet[index][0]) * this.dt * 8;
}
if (this.gait_state.step_z == 0) {
delta_pos[2] =
(this.default_feet_pos[index][2] - this.body_state.feet[index][2]) * this.dt * 8;
}
this.body_state.feet[index][0] = this.body_state.feet[index][0] + delta_pos[0];
this.body_state.feet[index][1] =
this.default_feet_pos[index][1] +
sin(this.phase_time * Math.PI) * this.gait_state.step_height;
this.body_state.feet[index][2] = this.body_state.feet[index][2] + delta_pos[2];
return this.body_state.feet[index];
}
}
export class FourPhaseWalkState extends PhaseGaitState {
protected name = 'Four phase walk';
protected num_phases = 4;
protected phase_speed_factor = 6;
protected contact_phases = [
[1, 0, 1, 1],
[1, 1, 1, 0],
[1, 1, 1, 0],
[1, 0, 1, 1]
];
protected swing_stand_ratio = 1 / (this.num_phases - 1);
begin() {
super.begin();
}
end() {
super.end();
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt);
}
}
export class EightPhaseWalkState extends PhaseGaitState {
protected name = 'Eight phase walk';
protected num_phases = 8;
protected phase_speed_factor = 4;
protected contact_phases = [
[1, 0, 1, 1, 1, 1, 1, 1],
[1, 1, 1, 1, 1, 0, 1, 1],
[1, 1, 1, 1, 1, 1, 1, 0],
[1, 1, 1, 0, 1, 1, 1, 1]
];
protected shifts = [
[-0.05, 0, -0.2],
[0.3, 0, 0.2],
[-0.05, 0, 0.2],
[0.3, 0, -0.2]
];
protected swing_stand_ratio = 1 / (this.num_phases - 1);
begin() {
super.begin();
}
end() {
super.end();
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return super.step(body_state, command, dt);
}
}
export class BezierState extends GaitState {
protected name = 'Bezier';
protected phase = 0;
protected phase_num = 0;
protected step_length: number = 0;
offset = [0, 0.5, 0.5, 0];
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) {
const depth = this.gait_state.step_depth
return this.controller(index, phase, stance_curve, depth)
let depth = this.gait_state.step_depth;
return this.controller(index, phase, stance_curve, depth);
}
swing_controller(index: number, phase: number) {
const height = this.gait_state.step_height
return this.controller(index, phase, bezier_curve, height)
let height = this.gait_state.step_height;
return this.controller(index, phase, bezier_curve, height);
}
controller(
@@ -349,113 +335,69 @@ export class BezierState extends GaitState {
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)
let length = this.step_length / 2;
let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2;
const delta_pos = controller(length, angle, ...args, phase);
const kin = this.kinematic
length = this.gait_state.step_angle * kin.max_step_length
angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index])
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)
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
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
this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2;
return this.body_state.feet[index]
}
update_cumulative_position() {
if (this.last_body_state === null) {
this.last_body_state = { ...this.body_state }
this.body_state.cumulative_x = 0
this.body_state.cumulative_y = 0
this.body_state.cumulative_z = 0
this.body_state.cumulative_roll = 0
this.body_state.cumulative_pitch = 0
this.body_state.cumulative_yaw = 0
return
}
const m = this.gait_state
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
if (moving) {
const step_displacement_x_local =
m.step_x * m.step_velocity * this.dt * this.speed_factor
const step_displacement_z_local =
m.step_z * m.step_velocity * this.dt * this.speed_factor
const step_displacement_yaw =
m.step_angle * m.step_velocity * this.dt * this.speed_factor
const cos_yaw = Math.cos(this.cumulative_orientation.yaw)
const sin_yaw = Math.sin(this.cumulative_orientation.yaw)
const step_displacement_x =
step_displacement_x_local * cos_yaw - step_displacement_z_local * sin_yaw
const step_displacement_z =
step_displacement_x_local * sin_yaw + step_displacement_z_local * cos_yaw
this.cumulative_position.x += step_displacement_x
this.cumulative_position.z += step_displacement_z
this.cumulative_orientation.yaw += step_displacement_yaw
}
this.body_state.cumulative_x = this.cumulative_position.x
this.body_state.cumulative_y = this.cumulative_position.y
this.body_state.cumulative_z = this.cumulative_position.z
this.body_state.cumulative_roll = this.cumulative_orientation.roll
this.body_state.cumulative_pitch = this.cumulative_orientation.pitch
this.body_state.cumulative_yaw = this.cumulative_orientation.yaw
this.last_body_state = { ...this.body_state }
return this.body_state.feet[index];
}
}
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
const X_POLAR = Math.cos(angle)
const Y_POLAR = Math.sin(angle)
const X_POLAR = Math.cos(angle);
const Y_POLAR = Math.sin(angle);
const step = length * (1 - 2 * phase)
const X = step * X_POLAR
const Z = step * Y_POLAR
let Y = 0
if (length !== 0) Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length))
return [X, Y, Z]
}
const step = length * (1 - 2 * phase);
const X = step * X_POLAR;
const Z = step * Y_POLAR;
let Y = 0;
if (length !== 0) {
Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length));
}
return [X, Y, Z];
};
const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => {
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2)
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0])
const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2);
const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0]);
const offsets = [
current_foot_pos[0] - default_foot_pos[0],
current_foot_pos[2] - default_foot_pos[2],
current_foot_pos[1] - default_foot_pos[1]
]
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2)
const offset_mod = Math.atan2(offset_mag, foot_mag)
];
const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2);
const offset_mod = Math.atan2(offset_mag, foot_mag);
return Math.PI / 2.0 + foot_dir + offset_mod
}
return Math.PI / 2.0 + foot_dir + offset_mod;
};
const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => {
const control_points = get_control_points(length, angle, height)
const n = control_points.length - 1
const control_points = get_control_points(length, angle, height);
const n = control_points.length - 1;
const point = [0, 0, 0]
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]
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
}
return point;
};
const get_control_points = (length: number, angle: number, height: number): number[][] => {
const X_POLAR = Math.cos(angle)
const Z_POLAR = Math.sin(angle)
const X_POLAR = Math.cos(angle);
const Z_POLAR = Math.sin(angle);
const STEP = [
-length,
@@ -470,7 +412,7 @@ const get_control_points = (length: number, angle: number, height: number): numb
length * 1.5,
length * 1.4,
length
]
];
const Y = [
0.0,
@@ -485,24 +427,26 @@ const get_control_points = (length: number, angle: number, height: number): numb
height * 1.1,
0.0,
0.0
]
];
const control_points: number[][] = []
const control_points: number[][] = [];
for (let i = 0; i < STEP.length; i++) {
const X = STEP[i] * X_POLAR
const Z = STEP[i] * Z_POLAR
control_points.push([X, Y[i], Z])
const X = STEP[i] * X_POLAR;
const Z = STEP[i] * Z_POLAR;
control_points.push([X, Y[i], Z]);
}
return control_points
}
return control_points;
};
const comb = (n: number, k: number): number => {
if (k < 0 || k > n) return 0
if (k === 0 || k === n) return 1
k = Math.min(k, n - k)
let c = 1
for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1)
return c
}
if (k < 0 || k > n) return 0;
if (k === 0 || k === n) return 1;
k = Math.min(k, n - k);
let c = 1;
for (let i = 0; i < k; i++) {
c = (c * (n - i)) / (i + 1);
}
return c;
};
+291 -155
View File
@@ -1,184 +1,320 @@
export interface body_state_t {
omega: number
phi: number
psi: number
xm: number
ym: number
zm: number
feet: number[][]
cumulative_x: number
cumulative_y: number
cumulative_z: number
cumulative_roll: number
cumulative_pitch: number
cumulative_yaw: 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;
}
export interface KinematicParams {
coxa: number
coxa_offset: number
femur: number
tibia: number
L: number
W: 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 {
coxa: number
coxa_offset: number
femur: number
tibia: number
l1: number;
l2: number;
l3: number;
l4: number;
L: number
W: number
L: number;
W: number;
DEG2RAD = DEG2RAD
DEG2RAD = DEG2RAD;
max_roll: number
max_pitch: number
max_body_shift_x: number
max_body_shift_z: number
max_leg_reach: number
min_body_height: number
max_body_height: number
body_height_range: number
max_step_length: number
max_step_height: number
default_step_depth: number
default_body_height: number
default_step_height: number
sHp = sin(Math.PI / 2);
cHp = cos(Math.PI / 2);
mountOffsets: number[][]
default_feet_positions: number[][]
Tlf: number[][] = [];
Trf: number[][] = [];
Tlb: number[][] = [];
Trb: number[][] = [];
invMountRot = [
[0, 0, -1],
[0, 1, 0],
[1, 0, 0]
]
point_lf: number[][];
point_rf: number[][];
point_lb: number[][];
point_rb: number[][];
Ix: number[][];
constructor(params: KinematicParams) {
this.coxa = params.coxa
this.coxa_offset = params.coxa_offset
this.femur = params.femur
this.tibia = params.tibia
this.L = params.L
this.W = params.W
constructor() {
this.l1 = 60.5 / 100;
this.l2 = 10 / 100;
this.l3 = 100.7 / 100;
this.l4 = 118.5 / 100;
this.max_roll = 15 * (Math.PI / 2)
this.max_pitch = 15 * (Math.PI / 2)
this.max_body_shift_x = this.W / 3
this.max_body_shift_z = this.W / 3
this.max_leg_reach = this.femur + this.tibia - this.coxa_offset
this.min_body_height = this.max_leg_reach * 0.45
this.max_body_height = this.max_leg_reach * 1
this.body_height_range = this.max_body_height - this.min_body_height
this.max_step_length = this.max_leg_reach * 0.8
this.max_step_height = this.max_leg_reach / 2
this.default_step_depth = 0.002
this.default_body_height = this.min_body_height + this.body_height_range / 2
this.default_step_height = this.default_body_height / 2
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]
]
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.default_feet_positions = this.mountOffsets.map((offset, i) => {
return [offset[0], 0, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)]
})
}
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]
];
getDefaultFeetPos(): number[][] {
return this.default_feet_positions.map(pos => [...pos])
}
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]
];
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]
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]
];
}
const [mx, my, mz] = this.mountOffsets[i]
const px = bx - mx,
py = by - my,
pz = bz - mz
public calcIK(body_state: body_state_t): number[] {
this.bodyIK(body_state);
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
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])
)
)
];
}
const xLocal = i % 2 === 1 ? -lx : lx
return this.legIK(xLocal, ly, lz)
})
}
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);
private legIK(x: number, y: number, z: number): [number, number, number] {
const F = sqrt(max(0, x * x + y * y - this.coxa * this.coxa))
const G = F - this.coxa_offset
const H = sqrt(G * G + z * z)
const t1 = -atan2(y, x) - atan2(F, -this.coxa)
const D =
(H * H - this.femur * this.femur - this.tibia * this.tibia) /
(2 * this.femur * this.tibia)
const t3 = acos(max(-1, min(1, D)))
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
return [t1, t2, t3]
}
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]
];
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]
]
}
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;
}
}
+344 -313
View File
@@ -1,348 +1,379 @@
import {
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
type GridHelper,
ArrowHelper,
Vector3,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
MeshPhongMaterial,
EquirectangularReflectionMapping,
ACESFilmicToneMapping,
Group,
MeshBasicMaterial,
RepeatWrapping,
type Object3D
Mesh,
PerspectiveCamera,
PlaneGeometry,
Scene,
WebGLRenderer,
AmbientLight,
DirectionalLight,
PCFSoftShadowMap,
type GridHelper,
ArrowHelper,
Vector3,
FogExp2,
CanvasTexture,
type ColorRepresentation,
type WebGLRendererParameters,
MeshPhongMaterial,
EquirectangularReflectionMapping,
ACESFilmicToneMapping,
MathUtils,
Group,
MeshBasicMaterial,
RepeatWrapping
} from 'three'
import { Sky } from 'three/addons/objects/Sky.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
import { sunCalculator } from './utilities/position-utilities'
export const addScene = () => new Scene()
interface position {
x?: number
y?: number
z?: number
x?: number
y?: number
z?: number
}
interface light {
color?: ColorRepresentation
intensity?: number
color?: ColorRepresentation
intensity?: number
}
interface arrowOptions {
origin: position
direction: position
length?: number
color?: ColorRepresentation
origin: position
direction: position
length?: number
color?: ColorRepresentation
}
type directionalLight = position & light
export default class SceneBuilder {
public scene: Scene
public camera!: PerspectiveCamera
public ground!: Mesh
public renderer!: WebGLRenderer
public orbit: OrbitControls
public callback: (() => void) | undefined
public gridHelper!: GridHelper
public model!: URDFRobot
public liveStreamTexture!: CanvasTexture
private fog!: FogExp2
private isLoaded: boolean = false
public isDragging: boolean = false
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
constructor() {
this.scene = new Scene()
if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping
}
return this
}
public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters)
this.renderer.outputColorSpace = 'srgb'
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = PCFSoftShadowMap
this.renderer.toneMapping = ACESFilmicToneMapping
this.renderer.toneMappingExposure = 0.85
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement)
return this
}
public addSky = () => {
this.sky = new Sky()
this.sky.scale.setScalar(450000)
this.scene.add(this.sky)
const effectController = {
turbidity: 10,
rayleigh: 3,
mieCoefficient: 0.005,
mieDirectionalG: 0.7,
elevation: sunCalculator.calculateSunElevation(),
azimuth: 200,
exposure: this.renderer.toneMappingExposure
}
const uniforms = this.sky.material.uniforms
uniforms['turbidity'].value = effectController.turbidity
uniforms['rayleigh'].value = effectController.rayleigh
uniforms['mieCoefficient'].value = effectController.mieCoefficient
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG
this.renderer.toneMappingExposure = 0.5
const phi = MathUtils.degToRad(90 - effectController.elevation)
const theta = MathUtils.degToRad(effectController.azimuth)
const sun = new Vector3()
sun.setFromSphericalCoords(1, phi, theta)
uniforms['sunPosition'].value.copy(sun)
return this
}
public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera()
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0)
this.scene.add(this.camera)
return this
}
public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2)
checkerboardTexture.wrapS = RepeatWrapping
checkerboardTexture.wrapT = RepeatWrapping
checkerboardTexture.repeat.set(100, 100)
const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture,
opacity: 0.1,
transparent: true
})
const plane = new PlaneGeometry(400, 400)
this.ground = new Mesh(plane, checkerboardMat)
this.ground.rotation.x = -Math.PI / 2
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0)
this.ground.receiveShadow = true
this.scene.add(this.ground)
const mirror = new Reflector(plane, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff
})
mirror.rotateX(-Math.PI / 2)
this.scene.add(mirror)
return this
}
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
this.orbit.minDistance = 5
this.orbit.maxDistance = maxDistance
this.orbit.autoRotate = autoRotate
this.orbit.update()
return this
}
public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity)
this.scene.add(ambientLight)
return this
}
public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity)
directionalLight.castShadow = true
directionalLight.shadow.camera.top = 10
directionalLight.shadow.camera.bottom = -10
directionalLight.shadow.camera.right = 10
directionalLight.shadow.camera.left = -10
directionalLight.shadow.mapSize.set(4096, 4096)
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0)
this.scene.add(directionalLight)
return this
}
private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const context = canvas.getContext('2d')
const squareSize = size / squares
for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize)
}
}
const texture = new CanvasTexture(canvas)
texture.wrapS = texture.wrapT = RepeatWrapping
texture.anisotropy = 16
return texture
}
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density)
return this
}
public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement
if (parentElement) {
const width = parentElement.clientWidth
const height = parentElement.clientHeight
this.handleResize(width, height)
}
return this
}
public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height)
this.renderer.setPixelRatio(window.devicePixelRatio)
this.camera.aspect = width / height
this.camera.updateProjectionMatrix()
return this
}
public addRenderCb = (callback: Function) => {
this.callback = callback
return this
}
public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera)
this.orbit.update()
this.handleRobotShadow()
if (this.callback) this.callback()
if (!this.liveStreamTexture) return
})
return this
}
public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(
options?.direction.x ?? 0,
options?.direction.y ?? 0,
options?.direction.z ?? 0
)
const origin = new Vector3(
options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
)
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
)
this.scene.add(arrowHelper)
return this
}
private setJointValue(jointName: string, angle: number) {
if (!this.model) return
if (!this.model.joints[jointName]) return
this.model.joints[jointName].setJointValue(angle)
}
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: any) => {
if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial
delete c.__origMaterial
} else {
c.__origMaterial = c.material
c.material = material
}
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 addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera()
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0)
this.scene.add(this.camera)
return this
}
public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2)
checkerboardTexture.wrapS = RepeatWrapping
checkerboardTexture.wrapT = RepeatWrapping
checkerboardTexture.repeat.set(100, 100)
const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture,
opacity: 0.1,
transparent: true
})
const plane = new PlaneGeometry(400, 400)
this.ground = new Mesh(plane, checkerboardMat)
this.ground.rotation.x = -Math.PI / 2
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0)
this.ground.receiveShadow = true
this.scene.add(this.ground)
const mirror = new Reflector(plane, {
clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff
})
mirror.rotateX(-Math.PI / 2)
this.scene.add(mirror)
return this
}
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement)
this.orbit.minDistance = minDistance + (maxDistance - minDistance) / 2
this.orbit.maxDistance = maxDistance
this.orbit.autoRotate = autoRotate
this.orbit.update()
this.orbit.minDistance = minDistance
return this
}
public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity)
this.scene.add(ambientLight)
return this
}
public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity)
directionalLight.castShadow = true
directionalLight.shadow.camera.top = 10
directionalLight.shadow.camera.bottom = -10
directionalLight.shadow.camera.right = 10
directionalLight.shadow.camera.left = -10
directionalLight.shadow.mapSize.set(4096, 4096)
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0)
this.scene.add(directionalLight)
return this
}
private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas')
canvas.width = size
canvas.height = size
const context = canvas.getContext('2d')
const squareSize = size / squares
for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize)
}
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])
}
}
const texture = new CanvasTexture(canvas)
texture.wrapS = texture.wrapT = RepeatWrapping
texture.anisotropy = 16
return texture
}
}
traverse(m)
}
public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density)
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 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)
}
public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement
if (parentElement) {
const width = parentElement.clientWidth
const height = parentElement.clientHeight
this.handleResize(width, height)
}
return this
dragControls.onDragStart = () => {
this.orbit.enabled = false
this.isDragging = true
}
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
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)
public addRenderCb = (callback: () => void) => {
this.callback = callback
return this
}
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 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 toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog
}
public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3(
options?.direction.x ?? 0,
options?.direction.y ?? 0,
options?.direction.z ?? 0
)
const origin = new Vector3(
options?.origin.x ?? 0,
options?.origin.y ?? 0,
options?.origin.z ?? 0
)
const arrowHelper = new ArrowHelper(
dir,
origin,
options?.length ?? 1.5,
options?.color ?? 0xff0000
)
this.scene.add(arrowHelper)
return this
}
private setJointValue(jointName: string, angle: number) {
if (!this.model) return
if (!this.model.joints[jointName]) return
this.model.joints[jointName].setJointValue(angle)
}
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: Object3D) => {
if (c.type === 'Mesh') {
if (revert) {
c.material = c.__origMaterial
delete c.__origMaterial
} else {
c.__origMaterial = c.material
c.material = material
}
}
if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) {
const child = c.children[i]
if (!child.isURDFCollider) {
traverse(c.children[i])
}
}
}
}
traverse(m)
}
public addTransformControls = (model: Object3D) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement)
this.transformControl.addEventListener('dragging-changed', (event: { value: boolean }) => {
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: URDFRobot) => {
this.modelGroup = new Group()
this.modelGroup.add(model)
this.model = model
this.scene.add(this.modelGroup)
return this
}
public addDragControl = (updateAngle: (angles: Record<string, number>) => void) => {
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
}
private handleRobotShadow = () => {
if (this.isLoaded) return
const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10)
setTimeout(() => clearInterval(intervalId), 1000)
this.isLoaded = true
}
}
+43 -42
View File
@@ -1,53 +1,54 @@
import { Result } from '$lib/utilities/result'
import { browser } from '$app/environment'
import { Result } from '$lib/utilities/result';
import { browser } from '$app/environment';
class FileService {
private dbPromise: Promise<Result<IDBDatabase, string>> | null =
browser ? this.openDatabase() : null
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('fileStorageDB', 1)
private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => {
const request = indexedDB.open('fileStorageDB', 1);
request.onupgradeneeded = () => {
request.result.createObjectStore('files')
}
request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Error opening database'))
})
}
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')
const store = dbResult.inner.transaction('files', mode).objectStore('files')
return Result.ok(store)
}
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');
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 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 store');
return new Promise(resolve => {
const request = storeResult.inner.put(file, key)
request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Failed to save file'))
})
}
return new Promise((resolve) => {
const request = storeResult.inner.put(file, key);
request.onsuccess = () => resolve(Result.ok(request.result));
request.onerror = () => resolve(Result.err('Failed to save file'));
});
}
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 store')
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 store');
return new Promise(resolve => {
const request = storeResult.inner.get(key)
request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File not found'))
request.onerror = () => resolve(Result.err('Failed to retrieve file'))
})
}
return new Promise((resolve) => {
const request = storeResult.inner.get(key);
request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File not found'));
request.onerror = () => resolve(Result.err('Failed to retrieve file'));
});
}
}
export default browser ? new FileService() : null
export default browser ? new FileService() : null;
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as fileService } from './file-service'
export { default as resultService } from './result-service'
export { default as fileService } from './file-service';
export { default as resultService } from './result-service';
+14 -14
View File
@@ -1,19 +1,19 @@
import { errorLogs, latestErrorLog } from '$lib/stores'
import type { Result } from '$lib/utilities'
import { errorLogs, latestErrorLog } from '$lib/stores';
import type { Result } from '$lib/utilities';
class ResultService {
public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception }
latestErrorLog.set(errorLogEntry)
errorLogs.update(entries => {
entries.push(errorLogEntry)
return entries
})
}
public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception };
latestErrorLog.set(errorLogEntry);
errorLogs.update((entries) => {
entries.push(errorLogEntry);
return entries;
});
}
return result
}
return result;
}
}
export default new ResultService()
export default new ResultService();
+48 -62
View File
@@ -1,69 +1,55 @@
import { type Analytics } from '$lib/types/models'
import { writable } from 'svelte/store'
import { type Analytics } from '$lib/types/models';
import { writable } from 'svelte/store';
const analytics_data = {
uptime: <number[]>[],
free_heap: <number[]>[],
total_heap: <number[]>[],
used_heap: <number[]>[],
min_free_heap: <number[]>[],
max_alloc_heap: <number[]>[],
fs_used: <number[]>[],
fs_total: <number[]>[],
core_temp: <number[]>[],
cpu0_usage: <number[]>[],
cpu1_usage: <number[]>[],
cpu_usage: <number[]>[]
}
let analytics_data = {
uptime: <number[]>[],
free_heap: <number[]>[],
total_heap: <number[]>[],
used_heap: <number[]>[],
min_free_heap: <number[]>[],
max_alloc_heap: <number[]>[],
fs_used: <number[]>[],
fs_total: <number[]>[],
core_temp: <number[]>[],
cpu0_usage: <number[]>[],
cpu1_usage: <number[]>[],
cpu_usage: <number[]>[]
};
const maxAnalyticsData = 100
const maxAnalyticsData = 100;
function createAnalytics() {
const { subscribe, update } = writable(analytics_data)
const { subscribe, update } = writable(analytics_data);
return {
subscribe,
addData: (content: Analytics) => {
update(analytics_data => ({
...analytics_data,
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(
-maxAnalyticsData
),
total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
-maxAnalyticsData
),
used_heap: [
...analytics_data.used_heap,
(content.total_heap - content.free_heap) / 1000
].slice(-maxAnalyticsData),
min_free_heap: [
...analytics_data.min_free_heap,
content.min_free_heap / 1000
].slice(-maxAnalyticsData),
max_alloc_heap: [
...analytics_data.max_alloc_heap,
content.max_alloc_heap / 1000
].slice(-maxAnalyticsData),
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(
-maxAnalyticsData
),
fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(
-maxAnalyticsData
),
core_temp: [...analytics_data.core_temp, content.core_temp].slice(
-maxAnalyticsData
),
cpu0_usage: [...analytics_data.cpu0_usage, content.cpu0_usage].slice(
-maxAnalyticsData
),
cpu1_usage: [...analytics_data.cpu1_usage, content.cpu1_usage].slice(
-maxAnalyticsData
),
cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData)
}))
}
}
return {
subscribe,
addData: (content: Analytics) => {
update((analytics_data) => ({
...analytics_data,
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(-maxAnalyticsData),
total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
-maxAnalyticsData
),
used_heap: [
...analytics_data.used_heap,
(content.total_heap - content.free_heap) / 1000
].slice(-maxAnalyticsData),
min_free_heap: [...analytics_data.min_free_heap, content.min_free_heap / 1000].slice(
-maxAnalyticsData
),
max_alloc_heap: [...analytics_data.max_alloc_heap, content.max_alloc_heap / 1000].slice(
-maxAnalyticsData
),
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(-maxAnalyticsData),
fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(-maxAnalyticsData),
core_temp: [...analytics_data.core_temp, content.core_temp].slice(-maxAnalyticsData),
cpu0_usage: [...analytics_data.cpu0_usage, content.cpu0_usage].slice(-maxAnalyticsData),
cpu1_usage: [...analytics_data.cpu1_usage, content.cpu1_usage].slice(-maxAnalyticsData),
cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData)
}));
}
};
}
export const analytics = createAnalytics()
export const analytics = createAnalytics();
+49 -61
View File
@@ -1,79 +1,67 @@
import { persistentStore } from '$lib/utilities'
import { get, type Writable } from 'svelte/store'
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'
import SkillPanel from '$lib/components/SkillPanel.svelte'
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, unknown>
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>
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
widget: WidgetConfig | WidgetContainerConfig
): widget is WidgetConfig => 'component' in widget;
export const WidgetComponents = {
Visualization,
Stream,
ChartWidget,
SkillPanel
}
Visualization,
Stream,
ChartWidget
};
interface View {
name: string
content: WidgetContainerConfig
name: string;
content: WidgetContainerConfig;
}
const defaultViews: View[] = [
{
name: '3D representation',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
}
},
{
name: 'Stream',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Stream' }]
}
},
{
name: 'Split screen',
content: {
id: 'root',
widgets: [
{ id: 2, component: 'Stream' },
{ id: 2, component: 'Visualization', props: { debug: true } }
]
}
},
{
name: 'Skills',
content: {
id: 'root',
widgets: [
{ id: 1, component: 'Visualization', props: { debug: true } },
{ id: 2, component: 'SkillPanel' }
]
}
}
]
{
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 views: Writable<View[]> = persistentStore('views', defaultViews);
export const selectedView = persistentStore('selected_view', get(views)[0].name)
export const selectedView = persistentStore('selected_view', get(views)[0].name);
+15 -59
View File
@@ -1,64 +1,20 @@
import { api } from '$lib/api'
import { notifications } from '$lib/components/toasts/notifications'
import Kinematic from '$lib/kinematic'
import { persistentStore } from '$lib/utilities'
import { derived, type Writable } from 'svelte/store'
import { resolve } from '$app/paths'
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 | string>>
let featureFlagsStore: Writable<Record<string, boolean>>;
export function useFeatureFlags() {
if (!featureFlagsStore) {
featureFlagsStore = persistentStore<Record<string, boolean | string>>('FeatureFlags', {})
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)
}
})
}
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
}
const base = resolve('/')
export const variants = {
SPOTMICRO_ESP32: {
model: `${base}spot_micro.urdf.xacro`,
stl: `${base}stl.zip`,
kinematics: {
coxa: 0.0605,
coxa_offset: 0.01,
femur: 0.1112,
tibia: 0.1185,
L: 0.2075,
W: 0.078
}
},
SPOTMICRO_YERTLE: {
model: `${base}yertle.URDF`,
stl: `${base}URDF.zip`,
kinematics: {
coxa: 0.035,
coxa_offset: 0.0,
femur: 0.13,
tibia: 0.13,
L: 0.24,
W: 0.078
}
}
}
export const currentVariant = derived(useFeatureFlags(), $flagStore => {
const variantFlag = $flagStore['variant'] as string
return variantFlag && variants[variantFlag as keyof typeof variants] ?
variants[variantFlag as keyof typeof variants]
: variants.SPOTMICRO_ESP32
})
export const currentKinematic = derived(
currentVariant,
$variant => new Kinematic($variant.kinematics)
)
return featureFlagsStore;
}
+14 -14
View File
@@ -1,24 +1,24 @@
import { writable } from 'svelte/store'
import { writable } from 'svelte/store';
export const isFullscreen = writable(false)
export const isFullscreen = writable(false);
export function toggleFullscreen() {
isFullscreen.update(state => {
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen()
return !state
})
isFullscreen.update((state) => {
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen();
return !state;
});
}
export function enterFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen()
isFullscreen.set(true)
}
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen();
isFullscreen.set(true);
}
}
export function exitFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen()
isFullscreen.set(false)
}
if (document.fullscreenElement) {
document.exitFullscreen();
isFullscreen.set(false);
}
}
-87
View File
@@ -1,87 +0,0 @@
import { readable, derived } from 'svelte/store'
export type GamepadState = {
available: boolean
gamepads: Gamepad[]
}
const DEADZONE = 0.15
const dz = (x: number) => {
const a = Math.abs(x)
if (a < DEADZONE) return 0
return ((a - DEADZONE) / (1 - DEADZONE)) * Math.sign(x)
}
let raf = 0
let running = false
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
const update = () => {
const pads = navigator.getGamepads?.() ?? []
const list = Array.from(pads)
.map(p => p || null)
.filter(Boolean) as Gamepad[]
set({ available: 'getGamepads' in navigator, gamepads: list })
raf = requestAnimationFrame(update)
}
const onConnect = () => update()
const onDisconnect = () => update()
const onVis = () => {
if (document.hidden) {
running = false
cancelAnimationFrame(raf)
} else if (!running) {
running = true
raf = requestAnimationFrame(update)
}
}
window.addEventListener('gamepadconnected', onConnect)
window.addEventListener('gamepaddisconnected', onDisconnect)
document.addEventListener('visibilitychange', onVis)
running = true
raf = requestAnimationFrame(update)
return () => {
running = false
cancelAnimationFrame(raf)
window.removeEventListener('gamepadconnected', onConnect)
window.removeEventListener('gamepaddisconnected', onDisconnect)
document.removeEventListener('visibilitychange', onVis)
}
})
export const gamepad = derived(gamepads, s =>
s.available && s.gamepads.length ? s.gamepads[0] : null
)
export const hasGamepad = derived(gamepads, s => s.available && s.gamepads.length > 0)
export const gamepadAxes = derived(gamepad, g => (g ? g.axes.map(dz) : [0, 0, 0, 0]))
type ButtonEdge = { pressed: boolean; value: number; justPressed: boolean; justReleased: boolean }
const prev = new Map<number, { pressed: boolean; value: number }[]>()
export const gamepadButtons = derived(gamepad, g => g?.buttons ?? [])
export const gamepadButtonsEdges = derived(gamepad, g => {
if (!g) return [] as ButtonEdge[]
const p = prev.get(g.index) || []
const out = g.buttons.map((b, i): ButtonEdge => {
const pr = p[i] || { pressed: false, value: 0 }
const pressed = !!b.pressed || b.value > 0.5
return {
pressed,
value: b.value,
justPressed: pressed && !pr.pressed,
justReleased: !pressed && pr.pressed
}
})
prev.set(
g.index,
out.map(x => ({ pressed: x.pressed, value: x.value }))
)
return out
})
+13 -26
View File
@@ -1,7 +1,7 @@
import { writable } from 'svelte/store'
import type { IMUMsg } from '$lib/types/models'
import { writable } from 'svelte/store';
import type { IMU } from '$lib/types/models';
const maxIMUData = 100
const maxIMUData = 100;
export const imu = (() => {
const { subscribe, update } = writable({
@@ -12,29 +12,16 @@ export const imu = (() => {
altitude: [] as number[],
pressure: [] as number[],
bmp_temp: [] as number[]
})
});
const addData = (content: IMUMsg) => {
const addData = (content: IMU) => {
update(data => {
if (content.imu && content.imu[4]) {
data.x = [...data.x, content.imu[0]].slice(-maxIMUData)
data.y = [...data.y, content.imu[1]].slice(-maxIMUData)
data.z = [...data.z, content.imu[2]].slice(-maxIMUData)
}
(Object.keys(content) as (keyof IMU)[]).forEach(key => {
data[key] = [...data[key], content[key]].slice(-maxIMUData);
});
return data;
});
};
if (content.mag && content.mag[4]) {
data.heading = [...data.heading, content.mag[3]].slice(-maxIMUData)
}
if (content.bmp && content.bmp[3]) {
data.pressure = [...data.pressure, content.bmp[0]].slice(-maxIMUData)
data.altitude = [...data.altitude, content.bmp[1]].slice(-maxIMUData)
data.bmp_temp = [...data.bmp_temp, content.bmp[2]].slice(-maxIMUData)
}
return data
})
}
return { subscribe, addData }
})()
return { subscribe, addData };
})();
+9 -10
View File
@@ -1,10 +1,9 @@
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'
export * from './skill'
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';
+4 -5
View File
@@ -1,6 +1,5 @@
import { persistentStore } from '$lib/utilities'
import { writable } from 'svelte/store'
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
import { persistentStore } from '$lib/utilities';
import { writable } from 'svelte/store';
import appEnv from 'app-env';
export const apiLocation =
PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '')
export const location = appEnv.VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
+6 -6
View File
@@ -1,11 +1,11 @@
import { writable, type Writable } from 'svelte/store'
import { writable, type Writable } from 'svelte/store';
export interface errorLog {
message: unknown
tag?: string
exception?: unknown
message: unknown;
tag?: string;
exception?: unknown;
}
export const latestErrorLog: Writable<errorLog> = writable()
export const latestErrorLog: Writable<errorLog> = writable();
export const errorLogs: Writable<errorLog[]> = writable([])
export const errorLogs: Writable<errorLog[]> = writable([]);
+32 -41
View File
@@ -1,54 +1,45 @@
import type { ControllerInput } from '$lib/types/models'
import { persistentStore } from '$lib/utilities/svelte-utilities'
import { writable, type Writable } from 'svelte/store'
import type { ControllerInput } from '$lib/types/models';
import { persistentStore } from '$lib/utilities/svelte-utilities';
import { writable, type Writable } from 'svelte/store';
export const emulateModel = writable(true)
export const emulateModel = writable(true);
export const jointNames = persistentStore('joint_names', <string[]>[])
export const jointNames = persistentStore('joint_names', <string[]>[]);
export const model = writable()
export const model = writable();
export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'walk'] as const
export const modes = [
'deactivated',
'idle',
'calibration',
'rest',
'stand',
'crawl',
'walk'
] as const;
export type Modes = (typeof modes)[number]
export type Modes = (typeof modes)[number];
export enum ModesEnum {
Deactivated = 0,
Idle = 1,
Calibration = 2,
Rest = 3,
Stand = 4,
Walk = 5
Deactivated,
Idle,
Calibration,
Rest,
Stand,
Crawl,
Walk
}
export enum WalkGaits {
Trot = 0,
Crawl = 1
}
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated);
export const walkGaits = ['trot', 'crawl'] as const
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]);
export const walkGaitLabels: Record<WalkGaits, string> = {
[WalkGaits.Trot]: 'Trot',
[WalkGaits.Crawl]: 'Crawl'
}
export const walkGaitToMode = (gait: WalkGaits): 'trot' | 'crawl' => {
return gait === WalkGaits.Trot ? 'trot' : 'crawl'
}
export const mode: Writable<ModesEnum> = writable(ModesEnum.Deactivated)
export const walkGait: Writable<WalkGaits> = writable(WalkGaits.Trot)
export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0])
export const kinematicData = writable([0, 0, 0, 0, 1, 0])
export const kinematicData = writable([0, 0, 0, 0, 1, 0]);
export const input: Writable<ControllerInput> = writable({
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 0.5,
speed: 0.5,
s1: 0.5
})
left: { x: 0, y: 0 },
right: { x: 0, y: 0 },
height: 50,
speed: 50,
s1: 50
});
+27
View File
@@ -0,0 +1,27 @@
import { readable } from 'svelte/store';
export const heading = readable(0, (set) => {
const updateHeading = (e: any) => {
let alpha;
if (e.webkitCompassHeading) alpha = e.webkitCompassHeading;
else if (e.alpha) alpha = e.alpha;
else {
let q = e.target.quaternion;
alpha =
Math.atan2(2 * q[0] * q[1] + 2 * q[2] * q[3], 1 - 2 * q[1] * q[1] - 2 * q[2] * q[2]) *
(180 / Math.PI);
if (alpha < 0) alpha += 360;
}
set(alpha);
};
if ('AbsoluteOrientationSensor' in window) {
var sensor = new window.AbsoluteOrientationSensor({ frequency: 60 }) as any;
sensor.addEventListener('reading', updateHeading);
sensor.start();
} else if (window.DeviceMotionEvent) window.addEventListener('deviceorientation', updateHeading);
return () => {
if ('AbsoluteOrientationSensor' in window) sensor.removeEventListener('reading', updateHeading);
window.addEventListener('deviceorientation', updateHeading);
};
});
-85
View File
@@ -1,85 +0,0 @@
import { writable, derived } from 'svelte/store'
import { socket } from './socket'
import { MessageTopic, type SkillStatus, type SkillCommand } from '$lib/types/models'
const defaultStatus: SkillStatus = {
x: 0,
y: 0,
z: 0,
yaw: 0,
distance: 0,
skill_active: false,
skill_target_x: 0,
skill_target_z: 0,
skill_target_yaw: 0,
skill_traveled_x: 0,
skill_traveled_z: 0,
skill_rotated: 0,
skill_progress: 0,
skill_complete: false
}
function createSkillStore() {
const status = writable<SkillStatus>(defaultStatus)
const history = writable<SkillCommand[]>([])
let unsubscribe: (() => void) | null = null
function init() {
if (unsubscribe) return
unsubscribe = socket.on<SkillStatus>(MessageTopic.skillStatus, data => {
status.set(data)
if (data.event === 'complete') {
history.update(h => [...h.slice(-9), getCurrentTarget(data)])
}
})
}
function getCurrentTarget(s: SkillStatus): SkillCommand {
return { x: s.skill_target_x, z: s.skill_target_z, yaw: s.skill_target_yaw }
}
function execute(cmd: SkillCommand) {
socket.sendEvent(MessageTopic.skill, cmd)
}
function walk(x: number, z: number = 0, yaw: number = 0, speed: number = 0.5) {
execute({ x, z, yaw, speed })
}
function turn(yaw: number, speed: number = 0.5) {
execute({ x: 0, z: 0, yaw, speed })
}
function stop() {
socket.sendEvent(MessageTopic.displacement, { action: 'clear' })
}
function resetPosition() {
socket.sendEvent(MessageTopic.displacement, { action: 'reset' })
}
function destroy() {
if (unsubscribe) {
unsubscribe()
unsubscribe = null
}
}
return {
status,
history,
init,
destroy,
execute,
walk,
turn,
stop,
resetPosition,
isActive: derived(status, $s => $s.skill_active),
progress: derived(status, $s => $s.skill_progress),
position: derived(status, $s => ({ x: $s.x, y: $s.y, z: $s.z, yaw: $s.yaw }))
}
}
export const skill = createSkillStore()
+19 -19
View File
@@ -1,27 +1,27 @@
import { writable, type Writable } from 'svelte/store'
import { type angles } from '$lib/types/models'
import { writable, type Writable } from 'svelte/store';
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
])
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]);
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 mpu = writable({ heading: 0 })
export const sonar = writable([0, 0])
export const distances = writable({})
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]);
export const logs = writable([] as string[]);
export const mpu = writable({ heading: 0 });
export const sonar = writable([0, 0]);
export const distances = writable({});
export interface socketDataCollection {
angles: Writable<angles>
logs: Writable<string[]>
mpu: Writable<unknown>
distances: Writable<unknown>
angles: Writable<angles>;
logs: Writable<string[]>;
mpu: Writable<unknown>;
distances: Writable<unknown>;
}
export const socketData = {
angles: servoAngles,
logs,
mpu,
distances
}
angles: servoAngles,
logs,
mpu,
distances
};
+104 -142
View File
@@ -1,160 +1,122 @@
import { writable } from 'svelte/store'
import { encode, decode } from '@msgpack/msgpack'
import { writable } from 'svelte/store';
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number]
type SocketMessage = [number, string?, unknown?]
let useBinary = false
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer
try {
if (useBinary) {
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage
}
return JSON.parse(data as string)
} catch (error) {
console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`)
}
return null
}
const encodeMessage = (data: unknown) => {
try {
return useBinary ? encode(data) : JSON.stringify(data)
} catch (error) {
console.error(`Could not encode data: ${data} - ${error}`)
}
}
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
type SocketEvent = (typeof socketEvents)[number];
function createWebSocket() {
const listeners = new Map<string, Set<(data?: unknown) => void>>()
const { subscribe, set } = writable(false)
const reconnectTimeoutTime = 5000
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
let reconnectTimeoutId: ReturnType<typeof setTimeout>
let ws: WebSocket
let socketUrl: string | URL
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 init(url: string | URL) {
socketUrl = url;
connect();
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close()
set(false)
clearTimeout(unresponsiveTimeoutId)
clearTimeout(reconnectTimeoutId)
listeners.get(reason)?.forEach(listener => listener(event))
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
}
function disconnect(reason: SocketEvent, event?: Event) {
ws.close();
set(false);
clearTimeout(unresponsiveTimeoutId);
clearTimeout(reconnectTimeoutId);
listeners.get(reason)?.forEach((listener) => listener(event));
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
}
function connect() {
ws = new WebSocket(socketUrl)
ws.binaryType = 'arraybuffer'
ws.onopen = ev => {
ping()
useBinary = true
ping()
set(true)
clearTimeout(reconnectTimeoutId)
listeners.get('open')?.forEach(listener => listener(ev))
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue
subscribeToEvent(event)
}
}
ws.onmessage = frame => {
resetUnresponsiveCheck()
const message = decodeMessage(frame.data)
if (!message) return
const [, event, payload = undefined] = message
if (event) listeners.get(event)?.forEach(listener => listener(payload))
}
ws.onerror = ev => disconnect('error', ev)
ws.onclose = ev => disconnect('close', ev)
}
function connect() {
ws = new WebSocket(socketUrl);
ws.onopen = (ev) => {
set(true);
clearTimeout(reconnectTimeoutId);
listeners.get('open')?.forEach((listener) => listener(ev));
for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue;
subscribeToEvent(event);
}
};
ws.onmessage = (message) => {
resetUnresponsiveCheck();
let data = message.data;
if (data instanceof ArrayBuffer) {
listeners.get('binary')?.forEach((listener) => listener(data));
return;
}
data = data.substring(1);
function unsubscribe(event: string, listener?: (data: unknown) => void) {
const eventListeners = listeners.get(event)
if (!eventListeners) return
if (!data) return;
if (!eventListeners.size) {
unsubscribeToEvent(event)
}
if (listener) {
eventListeners?.delete(listener)
} else {
listeners.delete(event)
}
}
let event = data.substring(data.indexOf('/') + 1, data.indexOf('['));
let payload = data.substring(data.indexOf('[') + 1, data.lastIndexOf(']'));
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId)
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
}
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 sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
send([2, event, data])
}
function unsubscribe(event: string, listener?: (data: any) => void) {
let eventListeners = listeners.get(event);
if (!eventListeners) return;
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
send([1, event])
}
if (!eventListeners.size) {
unsubscribeToEvent(event);
}
if (listener) {
eventListeners?.delete(listener);
} else {
listeners.delete(event);
}
}
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
send([0, event])
}
function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId);
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
}
function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return
const serialized = encodeMessage(data)
if (!serialized) {
console.error('Could not serialize data:', data)
return
}
ws.send(serialized)
}
function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send(`2/${event}[${JSON.stringify(data)}]`);
}
function ping() {
const serialized = encodeMessage([4])
if (!serialized) {
console.error('Could not serialize message')
return
}
ws.send(serialized)
}
function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('1/' + event);
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event)
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event)
}
eventListeners = new Set()
listeners.set(event, eventListeners)
}
eventListeners.add(listener as (data: unknown) => void)
function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return;
ws.send('0/' + event);
}
return () => {
unsubscribe(event, listener as (data: unknown) => void)
}
},
off: <T>(event: string, listener?: (data: T) => void) => {
unsubscribe(event, listener as (data: unknown) => void)
}
}
return {
subscribe,
sendEvent,
init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event);
if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event);
}
eventListeners = new Set();
listeners.set(event, eventListeners);
}
eventListeners.add(listener as (data: any) => void);
return () => {
unsubscribe(event, listener);
};
},
off: (event: string, listener?: (data: any) => void) => {
unsubscribe(event, listener);
}
};
}
export const socket = createWebSocket()
export const socket = createWebSocket();
+9 -9
View File
@@ -1,7 +1,7 @@
import type { DownloadOTA } from '$lib/types/models'
import { writable } from 'svelte/store'
import type { DownloadOTA } from '$lib/types/models';
import { writable } from 'svelte/store';
const telemetry_data = {
let telemetry_data = {
rssi: {
rssi: 0
},
@@ -10,10 +10,10 @@ const telemetry_data = {
progress: 0,
error: ''
}
}
};
function createTelemetry() {
const { subscribe, update } = writable(telemetry_data)
const { subscribe, set, update } = writable(telemetry_data);
return {
subscribe,
@@ -21,15 +21,15 @@ function createTelemetry() {
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()
export const telemetry = createTelemetry();
+15 -15
View File
@@ -1,17 +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
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;
}
+133 -233
View File
@@ -1,278 +1,178 @@
export enum MessageTopic {
imu = 'imu',
imuCalibrate = 'imuCalibrate',
mode = 'mode',
input = 'input',
analytics = 'analytics',
position = 'position',
angles = 'angles',
i2cScan = 'i2cScan',
peripheralSettings = 'peripheralSettings',
otastatus = 'otastatus',
gait = 'walk_gait',
servoState = 'servoState',
servoPWM = 'servoPWM',
WiFiSettings = 'WiFiSettings',
sonar = 'sonar',
rssi = 'rssi',
skill = 'skill',
skillStatus = 'skill_status',
displacement = 'displacement'
}
export type vector = { x: number; y: number }
export type vector = { x: number; y: number };
export interface ControllerInput {
left: vector
right: vector
height: number
speed: number
s1: number
left: vector;
right: vector;
height: number;
speed: number;
s1: number;
}
export type GithubRelease = {
message: string
tag_name: string
message: string;
tag_name: string;
assets: Array<{
name: string
browser_download_url: string
}>
}
name: string;
browser_download_url: string;
}>;
};
export type angles = number[] | Int16Array
export type angles = number[] | Int16Array;
export type WifiStatus = {
status: number
local_ip: string
mac_address: string
rssi: number
ssid: string
bssid: string
channel: number
subnet_mask: string
gateway_ip: string
dns_ip_1: string
dns_ip_2?: string
}
status: number;
local_ip: string;
mac_address: string;
rssi: number;
ssid: string;
bssid: string;
channel: number;
subnet_mask: string;
gateway_ip: string;
dns_ip_1: string;
dns_ip_2?: string;
};
export type WifiSettings = {
hostname: string
priority_RSSI: boolean
wifi_networks: KnownNetworkItem[]
}
hostname: string;
priority_RSSI: boolean;
wifi_networks: KnownNetworkItem[];
};
export type NetworkList = {
networks: NetworkItem[]
}
networks: NetworkItem[];
};
export type KnownNetworkItem = {
ssid: string
password: string
static_ip_config: boolean
local_ip?: string
subnet_mask?: string
gateway_ip?: string
dns_ip_1?: string
dns_ip_2?: string
}
ssid: string;
password: string;
static_ip_config: boolean;
local_ip?: string;
subnet_mask?: string;
gateway_ip?: string;
dns_ip_1?: string;
dns_ip_2?: string;
};
export type NetworkItem = {
rssi: number
ssid: string
bssid: string
channel: number
encryption_type: number
}
rssi: number;
ssid: string;
bssid: string;
channel: number;
encryption_type: number;
};
export type ApStatus = {
status: number
ip_address: string
mac_address: string
station_num: number
}
status: number;
ip_address: string;
mac_address: string;
station_num: number;
};
export type ApSettings = {
provision_mode: number
ssid: string
password: string
channel: number
ssid_hidden: boolean
max_clients: number
local_ip: string
gateway_ip: string
subnet_mask: string
}
provision_mode: number;
ssid: string;
password: string;
channel: number;
ssid_hidden: boolean;
max_clients: number;
local_ip: string;
gateway_ip: string;
subnet_mask: string;
};
export type DownloadOTA = {
status: string
progress: number
error: string
}
status: string;
progress: number;
error: string;
};
export type Analytics = {
max_alloc_heap: number
psram_size: number
free_psram: number
free_heap: number
total_heap: number
min_free_heap: number
core_temp: number
fs_total: number
fs_used: number
uptime: number
cpu0_usage: number
cpu1_usage: number
cpu_usage: number
}
max_alloc_heap: number;
psram_size: number;
free_psram: number;
free_heap: number;
total_heap: number;
min_free_heap: number;
core_temp: number;
fs_total: number;
fs_used: number;
uptime: number;
cpu0_usage: number;
cpu1_usage: number;
cpu_usage: number;
};
export type Rssi = {
rssi: number
ssid: string
}
rssi: number;
ssid: string;
};
export type StaticSystemInformation = {
esp_platform: string
firmware_version: string
cpu_freq_mhz: number
cpu_type: string
cpu_rev: number
cpu_cores: number
sketch_size: number
free_sketch_space: number
sdk_version: string
arduino_version: string
flash_chip_size: number
flash_chip_speed: number
cpu_reset_reason: string
}
esp_platform: string;
firmware_version: string;
cpu_freq_mhz: number;
cpu_type: string;
cpu_rev: number;
cpu_cores: number;
sketch_size: number;
free_sketch_space: number;
sdk_version: string;
arduino_version: string;
flash_chip_size: number;
flash_chip_speed: number;
cpu_reset_reason: string;
};
export type SystemInformation = Analytics & StaticSystemInformation
export type SystemInformation = Analytics & StaticSystemInformation;
export type IMU = {
x: number
y: number
z: number
heading: number
altitude: number
bmp_temp: number
pressure: number
}
export type IMUMsg = {
imu: [number, number, number, number, boolean]
mag: [number, number, number, number, boolean]
bmp: [number, number, number, boolean]
}
export type IMUCalibrationResult = {
success: boolean
}
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
}
export type PinConfig = {
pin: number
mode: string
type: string
role: string
}
export type PeripheralsConfiguration = {
sda: number
scl: number
frequency: number
pins: PinConfig[]
address: number;
part_number: string;
name: string;
}
export type CameraSettings = {
framesize: number
quality: number
brightness: number
contrast: number
saturation: number
sharpness: number
denoise: number
special_effect: number
wb_mode: number
vflip: boolean
hmirror: boolean
}
framesize: number;
quality: number;
brightness: number;
contrast: number;
saturation: number;
sharpness: number;
denoise: number;
special_effect: number;
wb_mode: number;
vflip: boolean;
hmirror: boolean;
};
export type File = number
export type File = number;
export interface Directory {
[key: string]: File | Directory
[key: string]: File | Directory;
}
export type Servo = {
name: string
channel: number
inverted: boolean
angle: number
center_angle: number
}
name: string;
channel: number;
inverted: boolean;
angle: number;
center_angle: number;
};
export type ServoConfiguration = {
is_active: boolean
servo_pwm_frequency: number
servo_oscillator_frequency: number
servos: Servo[]
}
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[]
}
export interface SkillCommand {
x: number
z: number
yaw: number
speed?: number
}
export interface SkillStatus {
x: number
y: number
z: number
yaw: number
distance: number
skill_active: boolean
skill_target_x: number
skill_target_z: number
skill_target_yaw: number
skill_traveled_x: number
skill_traveled_z: number
skill_rotated: number
skill_progress: number
skill_complete: boolean
event?: string
}
is_active: boolean;
servo_pwm_frequency: number;
servo_oscillator_frequency: number;
servos: Servo[];
};
+11 -11
View File
@@ -1,14 +1,14 @@
declare module 'uzip' {
interface UZIP {
parse(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
compress(data: Record<string, Uint8Array>): Uint8Array | ArrayBuffer
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
decompress(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
encode(data: Record<string, Uint8Array>): Uint8Array | ArrayBuffer
decode(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
}
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
const uzip: UZIP;
export default uzip;
}
+12 -12
View File
@@ -1,15 +1,15 @@
export class throttler {
private _throttlePause: boolean
constructor() {
this._throttlePause = false
}
throttle = (callback: () => void, time: number) => {
if (this._throttlePause) return
private _throttlePause: boolean;
constructor() {
this._throttlePause = false;
}
throttle = (callback: Function, time: number) => {
if (this._throttlePause) return;
this._throttlePause = true
setTimeout(() => {
callback()
this._throttlePause = false
}, time)
}
this._throttlePause = true;
setTimeout(() => {
callback();
this._throttlePause = false;
}, time);
};
}
+3 -5
View File
@@ -1,6 +1,4 @@
export const daisyColor = (name: string, opacity: number = 100) => {
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
if (opacity >= 100) return color
const alpha = Math.min(Math.max(opacity, 0), 100) / 100
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`
}
const color = getComputedStyle(document.documentElement).getPropertyValue(name);
return `oklch(${color} / ${opacity}%)`;
};
+9 -8
View File
@@ -1,8 +1,9 @@
export * from './result'
export * from './string-utilities'
export * from './svelte-utilities'
export * from './math-utilities'
export * from './buffer-utilities'
export * from './model-utilities'
export * from './string-utilities'
export * from './color-utilities'
export * from './result';
export * from './string-utilities';
export * from './svelte-utilities';
export * from './math-utilities';
export * from './buffer-utilities';
export * from './model-utilities';
export * from './position-utilities';
export * from './string-utilities';
export * from './color-utilities';
+13 -13
View File
@@ -1,18 +1,18 @@
export const toUint8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number))
const scaled = ((number - min) / (max - min)) * 255
return Math.round(scaled) & 0xff
}
number = Math.max(min, Math.min(max, number));
let scaled = ((number - min) / (max - min)) * 255;
return Math.round(scaled) & 0xff;
};
export const toInt8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number))
const scaled = ((number - min) / (max - min)) * 255 - 128
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0
}
number = Math.max(min, Math.min(max, number));
let scaled = ((number - min) / (max - min)) * 255 - 128;
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0;
};
export const fromInt8 = (int8: number, min: number, max: number) => {
int8 = Math.max(-128, Math.min(127, int8))
const scaled = (int8 + 128) / 255
const number = scaled * (max - min) + min
return number
}
int8 = Math.max(-128, Math.min(127, int8));
const scaled = (int8 + 128) / 255;
const number = scaled * (max - min) + min;
return number;
};
+76 -83
View File
@@ -1,96 +1,89 @@
import { Color, Vector3 } from 'three'
import URDFLoader, { type URDFRobot } from 'urdf-loader'
import { XacroLoader } from 'xacro-parser'
import { Result } from '$lib/utilities'
import { currentVariant, jointNames, model } from '$lib/stores'
import uzip from 'uzip'
import { fileService } from '$lib/services'
import { get } from 'svelte/store'
import { resolve } from '$app/paths'
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 populateModelCache = async () => {
await cacheModelFiles()
const modelRes = await loadModel(get(currentVariant).model)
if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner
jointNames.set(JOINT_NAME)
model.set(urdf)
} else {
console.error(modelRes.inner, { exception: modelRes.exception })
}
}
await cacheModelFiles();
const modelRes = await loadModelAsync('/spot_micro.urdf.xacro');
if (modelRes.isOk()) {
const [urdf, JOINT_NAME] = modelRes.inner;
jointNames.set(JOINT_NAME);
model.set(urdf);
} else {
console.error(modelRes.inner, { exception: modelRes.exception });
}
};
export const cacheModelFiles = async () => {
const data = await fetch(get(currentVariant).stl)
let data = await fetch('/stl.zip');
const files = uzip.parse(await data.arrayBuffer())
var files = uzip.parse(await data.arrayBuffer());
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const normalizedPath = path.startsWith('/') ? path : '/' + path
const resolvedUrl = resolve(normalizedPath as any)
fileService?.saveFile(resolvedUrl, data)
fileService?.saveFile(normalizedPath, data)
}
}
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const url = new URL(path, window.location.href);
fileService.saveFile(url.toString(), data);
}
};
export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
const urdfLoader = new URDFLoader()
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);
let xml =
url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
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]);
if (typeof xml === 'string') {
xml = new window.DOMParser().parseFromString(xml, 'text/xml')
}
resolve(Result.ok([model, joints]));
} catch (error) {
resolve(Result.err('Failed to load model', error));
}
},
(error) => resolve(Result.err('Failed to load model', error))
);
});
};
return new Promise(resolve => {
model_xml = xml
try {
const model = urdfLoader.parse(xml)
setupRobot(model)
const joints = Object.entries(model.joints)
.filter(joint => joint[1].jointType !== 'fixed')
.map(joint => joint[0])
export const 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;
};
resolve(Result.ok([model, joints]))
} catch (error) {
resolve(Result.err('Failed to load model', error))
}
})
}
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(', ');
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})`)
}
return new Color(`rgb(${colorStr})`);
};
@@ -0,0 +1,84 @@
class SunCalculator {
calculateSunElevation(lat: number = 55, lon: number = 12) {
const now = new Date();
const JD = this.getJulianDate(now);
const solarDec = this.getSolarDeclination(JD);
const solarTime = this.getSolarTime(now, lon);
const hourAngle = (solarTime - 12) * 15;
const elevation = Math.asin(
Math.sin(this.degToRad(lat)) * Math.sin(solarDec) +
Math.cos(this.degToRad(lat)) * Math.cos(solarDec) * Math.cos(this.degToRad(hourAngle))
);
return this.radToDeg(elevation);
}
getJulianDate(date: Date) {
const Y = date.getUTCFullYear();
const M = date.getUTCMonth() + 1;
const D =
date.getUTCDate() +
date.getUTCHours() / 24 +
date.getUTCMinutes() / 1440 +
date.getUTCSeconds() / 86400;
const A = Math.floor((14 - M) / 12);
const Y1 = Y + 4800 - A;
const M1 = M + 12 * A - 3;
return (
D +
Math.floor((153 * M1 + 2) / 5) +
365 * Y1 +
Math.floor(Y1 / 4) -
Math.floor(Y1 / 100) +
Math.floor(Y1 / 400) -
32045
);
}
getSolarDeclination(JulianDate: number) {
const n = JulianDate - 2451545;
const L = (280.46 + 0.9856474 * n) % 360;
const g = this.degToRad((357.528 + 0.9856003 * n) % 360);
const lambda = this.degToRad(L + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g));
return Math.asin(Math.sin(lambda) * Math.sin(this.degToRad(23.44)));
}
getSolarTime(date: Date, lon: number) {
const EoT = this.getEquationOfTime(date);
const offset = date.getTimezoneOffset() / 60;
const standardMeridian = Math.round(lon / 15) * 15;
const solarTime =
date.getUTCHours() +
(date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 -
offset;
return (solarTime + 24) % 24;
}
getEquationOfTime(date: Date) {
const JD = this.getJulianDate(date);
const n = JD - 2451545;
const g = this.degToRad((357.528 + 0.9856003 * n) % 360);
const q = this.degToRad((280.46 + 0.9856474 * n) % 360);
return (
4 *
this.radToDeg(
0.000075 +
0.001868 * Math.cos(q) -
0.032077 * Math.sin(g) -
0.014615 * Math.cos(2 * q) -
0.040849 * Math.sin(2 * g)
)
);
}
degToRad(deg: number) {
return deg * (Math.PI / 180);
}
radToDeg(rad: number) {
return rad * (180 / Math.PI);
}
}
export const sunCalculator = new SunCalculator();
+34 -34
View File
@@ -1,42 +1,42 @@
export class Err<T, U> {
#inner: T
#exception?: U
#inner: T;
#exception?: U;
constructor(inner: T, exception?: U) {
this.#inner = inner
this.#exception = exception
}
constructor(inner: T, exception?: U) {
this.#inner = inner;
this.#exception = exception;
}
get inner(): T {
return this.#inner
}
get inner(): T {
return this.#inner;
}
get exception(): U | undefined {
return this.#exception
}
get exception(): U | undefined {
return this.#exception;
}
/**
* Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err`
*/
isOk(): false {
return false
}
/**
* Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err`
*/
isOk(): false {
return false;
}
/**
* Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok`
*/
isErr(): this is Err<T, U> {
return true
}
/**
* Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok`
*/
isErr(): this is Err<T, U> {
return true;
}
/**
* Create an `Err`
* @param inner
* @returns `Err(inner)`
*/
static new<E, F>(inner: E, exception: F): Err<E, F> {
return new Err<E, F>(inner, exception)
}
/**
* Create an `Err`
* @param inner
* @returns `Err(inner)`
*/
static new<E, F>(inner: E, exception: F): Err<E, F> {
return new Err<E, F>(inner, exception);
}
}
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './err'
export * from './ok'
export * from './result'
export * from './err';
export * from './ok';
export * from './result';
+36 -36
View File
@@ -1,44 +1,44 @@
export class Ok<T> {
#inner: T
#inner: T;
constructor(inner: T) {
this.#inner = inner
}
constructor(inner: T) {
this.#inner = inner;
}
get inner(): T {
return this.#inner
}
get inner(): T {
return this.#inner;
}
/**
* Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err`
*/
isOk(): this is Ok<T> {
return true
}
/**
* Type guard for `Ok`
* @returns `true` if `Ok`; `false` if `Err`
*/
isOk(): this is Ok<T> {
return true;
}
/**
* Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok`
*/
isErr(): false {
return false
}
/**
* Type guard for `Err`
* @returns `true` if `Err`; `false` if `Ok`
*/
isErr(): false {
return false;
}
/**
* Create an `Ok`
* @param inner
* @returns `Ok(inner)`
*/
static new<T>(inner: T): Ok<T> {
return new Ok<T>(inner)
}
/**
* Create an `Ok`
* @param inner
* @returns `Ok(inner)`
*/
static new<T>(inner: T): Ok<T> {
return new Ok<T>(inner);
}
/**
* Create an empty `Ok`
* @returns `Ok(void)`
*/
static void(): Ok<void> {
return new Ok(undefined)
}
/**
* Create an empty `Ok`
* @returns `Ok(void)`
*/
static void(): Ok<void> {
return new Ok(undefined);
}
}
+16 -16
View File
@@ -1,20 +1,20 @@
import { Err } from './err'
import { Ok } from './ok'
import { Err } from './err';
import { Ok } from './ok';
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>;
export const Result = {
/**
* @returns `Ok<T>`
*/
ok<T = unknown>(value: T) {
return Ok.new(value)
},
export namespace Result {
/**
* @returns `Ok<T>`
*/
export function ok<T = unknown>(value: T) {
return Ok.new(value);
}
/**
* @returns `Err<E, F>`
*/
err<E = unknown, F = unknown>(error: E, exception?: F) {
return Err.new(error, exception)
}
/**
* @returns `Err<E, F>`
*/
export function err<E = unknown, F = unknown>(error: E, exception?: F) {
return Err.new(error, exception);
}
}
+28 -39
View File
@@ -1,47 +1,36 @@
export const humanFileSize = (size: number): string => {
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]
}
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];
};
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)
const 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);
let 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
}
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
}
return result;
};
+11 -11
View File
@@ -1,16 +1,16 @@
import { writable } from 'svelte/store'
import { browser } from '$app/environment'
import { writable } from 'svelte/store';
import { browser } from '$app/environment';
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>()
const savedValue = browser ? localStorage.getItem(key) : null;
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue;
const store = writable<T>();
store.subscribe(value => {
if (browser) localStorage.setItem(key, JSON.stringify(value))
})
store.subscribe(value => {
if (browser) localStorage.setItem(key, JSON.stringify(value));
});
store.set(data)
store.set(data);
return store
}
return store;
};
+11 -5
View File
@@ -1,9 +1,15 @@
<script lang="ts">
import { page } from '$app/state'
import { resolve } from '$app/paths'
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
setTimeout(() => {
goto('/');
}, 3000);
});
</script>
<div class="flex justify-center items-center w-full h-full">
<h1>{page.status} {page.error?.message}</h1>
<span>Go to <a class="btn btn-primary" href={resolve('/')}>Home page</a></span>
</div>
<h1 class="text-4xl">404 - Page not found</h1>
<p>You will be redirected to the home page in 3 seconds</p>
</div>
+96 -101
View File
@@ -1,130 +1,125 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import { page } from '$app/state'
import { Modals, modals } from 'svelte-modals'
import Toast from '$lib/components/toasts/Toast.svelte'
import { notifications } from '$lib/components/toasts/notifications'
import { fade } from 'svelte/transition'
import '../app.css'
import Menu from '../lib/components/menu/Menu.svelte'
import Statusbar from '../lib/components/statusbar/statusbar.svelte'
import {
telemetry,
analytics,
ModesEnum,
kinematicData,
mode,
outControllerData,
servoAngles,
servoAnglesOut,
socket,
apiLocation,
useFeatureFlags,
walkGait
} from '$lib/stores'
import { type Analytics, type DownloadOTA } from '$lib/types/models'
import { MessageTopic } from '$lib/types/models'
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
}
interface Props {
children?: import('svelte').Snippet
}
let { children }: Props = $props()
let { children }: Props = $props()
const features = useFeatureFlags()
const features = useFeatureFlags()
onMount(async () => {
const ws = $location ? $location : window.location.host
socket.init(`ws://${ws}/api/ws/events`)
onMount(async () => {
const ws = $apiLocation ? $apiLocation : window.location.host
socket.init(`ws://${ws}/api/ws`)
addEventListeners()
addEventListeners()
outControllerData.subscribe(data => socket.sendEvent('input', { data }))
mode.subscribe(data => socket.sendEvent('mode', { data }))
servoAnglesOut.subscribe(data => socket.sendEvent('angles', { data }))
kinematicData.subscribe(data => socket.sendEvent('position', { data }))
})
outControllerData.subscribe(data => socket.sendEvent(MessageTopic.input, data))
mode.subscribe(data => socket.sendEvent(MessageTopic.mode, data))
walkGait.subscribe(data => socket.sendEvent(MessageTopic.gait, data))
servoAnglesOut.subscribe(data => socket.sendEvent(MessageTopic.angles, data))
kinematicData.subscribe(data => socket.sendEvent(MessageTopic.position, data))
onDestroy(() => {
removeEventListeners()
})
const addEventListeners = () => {
socket.on('open', handleOpen)
socket.on('close', handleClose)
socket.on('error', handleError)
socket.on('rssi', handleNetworkStatus)
socket.on('mode', (data: ModesEnum) => mode.set(data))
socket.on('analytics', handleAnalytics)
socket.on('angles', (angles: number[]) => {
if (angles.length) servoAngles.set(angles)
})
onDestroy(() => {
removeEventListeners()
features.subscribe(data => {
if (data?.download_firmware) socket.on('otastatus', handleOAT)
if (data?.sonar) socket.on('sonar', data => console.log(data))
})
}
const addEventListeners = () => {
socket.on('open', handleOpen)
socket.on('close', handleClose)
socket.on('error', handleError)
socket.on(MessageTopic.rssi, handleNetworkStatus)
socket.on(MessageTopic.mode, (data: ModesEnum) => mode.set(data))
socket.on(MessageTopic.analytics, handleAnalytics)
socket.on(MessageTopic.angles, (angles: number[]) => {
if (angles.length) servoAngles.set(angles)
})
features.subscribe(data => {
if (data?.download_firmware) socket.on(MessageTopic.otastatus, handleOAT)
if (data?.sonar) socket.on(MessageTopic.sonar, data => console.log(data))
})
}
const removeEventListeners = () => {
socket.off('analytics', handleAnalytics)
socket.off('open', handleOpen)
socket.off('close', handleClose)
socket.off('rssi', handleNetworkStatus)
socket.off('otastatus', handleOAT)
}
const removeEventListeners = () => {
socket.off(MessageTopic.analytics, handleAnalytics)
socket.off('open', handleOpen)
socket.off('close', handleClose)
socket.off(MessageTopic.rssi, handleNetworkStatus)
socket.off(MessageTopic.otastatus, handleOAT)
}
const 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: unknown) => console.error(data)
const handleAnalytics = (data: Analytics) => analytics.addData(data)
const handleAnalytics = (data: Analytics) => analytics.addData(data)
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data)
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data)
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data)
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data)
let menuOpen = $state(false)
let menuOpen = $state(false)
</script>
<svelte:head>
<title>{page.data.title}</title>
<title>{page.data.title}</title>
</svelte:head>
<div class="drawer">
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
<div class="drawer-content flex flex-col">
<!-- Status bar content here -->
<Statusbar />
<div class="drawer h-screen">
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
<div class="drawer-content flex flex-col">
<!-- Status bar content here -->
<Statusbar />
<!-- Main page content here -->
{@render children?.()}
</div>
<!-- Side Navigation -->
<div class="drawer-side z-30 shadow-lg">
<label for="main-menu" class="drawer-overlay"></label>
<Menu menuClicked={() => (menuOpen = false)} />
</div>
<!-- Main page content here -->
{@render children?.()}
</div>
<!-- Side Navigation -->
<div class="drawer-side z-30 shadow-lg">
<label for="main-menu" class="drawer-overlay"></label>
<Menu menuClicked={() => (menuOpen = false)} />
</div>
</div>
<Modals>
{#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}
onkeydown={e => e.key === 'Escape' && modals.closeAll()}
role="button"
tabindex="0"
></div>
{/snippet}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
{#snippet backdrop()}
<div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade
onclick={modals.closeAll}>
</div>
{/snippet}
</Modals>
<Toast />
+18 -30
View File
@@ -1,34 +1,22 @@
export const prerender = true
export const ssr = false
export const prerender = false;
export const ssr = false;
const registerFetchIntercept = async () => {
const { fetch: originalFetch } = window
const fileService = (await import('$lib/services/file-service')).default
window.fetch = async (resource, config) => {
const url = resource instanceof Request ? resource.url : resource.toString()
let file = await fileService?.getFile(url)
if (file?.isOk() && file.inner) return new Response(new Uint8Array(file.inner))
if (url.startsWith('http')) {
try {
const urlObj = new URL(url)
const pathOnly = urlObj.pathname
file = await fileService?.getFile(pathOnly)
if (file?.isOk() && file.inner) return new Response(new Uint8Array(file.inner))
} catch {}
}
return originalFetch(resource, config)
}
}
const { fetch: originalFetch } = window;
const fileService = (await import('$lib/services/file-service')).default;
window.fetch = async (resource, config) => {
let url = resource instanceof Request ? resource.url : resource.toString();
let file = await fileService.getFile(url);
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config);
};
};
export const load = async () => {
await registerFetchIntercept()
return {
title: 'Spot micro controller',
github: 'runeharlyk/SpotMicroESP32-Leika',
app_name: 'Spot Micro Controller',
copyright: '2025 Rune Harlyk'
}
}
await registerFetchIntercept();
return {
title: 'Spot micro controller',
github: 'runeharlyk/SpotMicroESP32-Leika',
app_name: 'Spot Micro Controller',
copyright: '2024 Rune Harlyk'
};
};
+22 -23
View File
@@ -1,30 +1,29 @@
<script lang="ts">
import { goto } from '$app/navigation'
import Visualization from '$lib/components/Visualization.svelte'
import { socket } from '$lib/stores'
import { onMount } from 'svelte'
import { resolve } from '$app/paths'
import { goto } from '$app/navigation'
import Visualization from '$lib/components/Visualization.svelte'
import { socket } from '$lib/stores'
import { onMount } from 'svelte'
onMount(() => {
socket.subscribe(isConnected => {
if (isConnected) {
goto(resolve('/controller'))
}
})
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 defaultColor={null} 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={resolve($socket ? '/controller' : '/connection')}>
Add Robot Dog
</a>
</div>
<div class="w-full h-full flex justify-center items-center">
<div class="h-full flex flex-col">
<div class="grow-3 w-80 relative">
<Visualization sky={false} orbit panel={false} ground={false} zoom={8} />
<div class="absolute bottom-0 w-full h-40 bg-gradient-to-t from-base-100 to-transparent">
</div>
</div>
<div class="grow-3 flex justify-center">
<a class="btn btn-primary rounded-full" href={$socket ? '/controller' : '/connection'}>
Add Robot Dog
</a>
</div>
</div>
</div>
+2 -2
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import Connection from './Connection.svelte'
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 />
<Connection />
</div>
+5 -5
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'
import type { PageLoad } from './$types';
export const load = (async () => {
return {
title: 'Connection'
}
}) satisfies PageLoad
return {
title: 'Connection'
};
}) satisfies PageLoad;
+11 -9
View File
@@ -1,25 +1,27 @@
<script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte'
import { WiFi } from '$lib/components/icons'
import { apiLocation, socket } from '$lib/stores'
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 = $apiLocation ? $apiLocation : window.location.host
socket.init(`ws://${ws}/api/ws`)
}
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" />
<WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span>Connection</span>
<span >Connection</span>
{/snippet}
<div class="flex">
<label class="label w-32" for="server">Address:</label>
<input class="input" bind:value={$apiLocation} />
<input class="input" bind:value={$location} />
</div>
<button class="btn btn-primary" onclick={update}>Update</button>
+16 -16
View File
@@ -1,26 +1,26 @@
<script lang="ts">
import Controls from './Controls.svelte'
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte'
import { selectedView, views } from '$lib/stores/application'
import { onMount } from 'svelte'
import { mpu, socket } from '$lib/stores'
import { imu } from '$lib/stores/imu'
import { MessageTopic, type IMU } from '$lib/types/models'
import Controls from './Controls.svelte';
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte';
import { selectedView, views } from '$lib/stores/application';
import { onMount } from 'svelte';
import { mpu, socket } from '$lib/stores';
import { imu } from '$lib/stores/imu';
import type { IMU } from '$lib/types/models';
let layout = $derived($views.find(v => v.name === $selectedView)!)
let layout = $derived($views.find(v => v.name === $selectedView)!);
onMount(() => {
socket.on(MessageTopic.imu, (data: IMU) => {
imu.addData(data)
socket.on('imu', (data: IMU) => {
imu.addData(data);
if (data.heading)
mpu.update(mpuData => {
mpuData.heading = data.heading
console.log(data.heading)
mpuData.heading = data.heading;
console.log(data.heading);
return mpuData
})
})
})
return mpuData;
});
});
});
</script>
<div class="absolute top-0 select-none w-screen h-screen">
+2 -2
View File
@@ -1,3 +1,3 @@
export const load = async () => {
return { title: 'Controller' }
}
return { title: 'Controller' };
};
+123 -198
View File
@@ -1,225 +1,150 @@
<script lang="ts">
import nipplejs from 'nipplejs'
import { onMount } from 'svelte'
import { capitalize, throttler } from '$lib/utilities'
import {
input,
outControllerData,
mode,
modes,
type Modes,
ModesEnum,
WalkGaits,
walkGait,
walkGaitLabels
} from '$lib/stores'
import type { vector } from '$lib/types/models'
import { VerticalSlider } from '$lib/components/input'
import { gamepadAxes, gamepadButtonsEdges, hasGamepad } from '$lib/stores/gamepad'
import { notifications } from '$lib/components/toasts/notifications'
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'
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(7)
let throttle_timing = 40
let data = new Array(8)
$effect(() => {
if ($hasGamepad) {
notifications.success('🎮 Gamepad connected', 3000)
}
onMount(() => {
left = nipplejs.create({
zone: document.getElementById('left') as HTMLElement,
color: '#15191e80',
dynamicPage: true,
mode: 'static',
restOpacity: 1
})
$effect(() => {
handleJoyMove('left', { x: $gamepadAxes[0], y: -$gamepadAxes[1] })
handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] })
right = nipplejs.create({
zone: document.getElementById('right') as HTMLElement,
color: '#15191e80',
dynamicPage: true,
mode: 'static',
restOpacity: 1
})
$effect(() => {
if (!$hasGamepad) return
const b = $gamepadButtonsEdges
if (!b.length) return
if (b[0]?.justPressed) mode.set(5)
if (b[1]?.justPressed) mode.set(4)
if (b[2]?.justPressed) mode.set(3)
if (b[3]?.justPressed) mode.set(0)
if (b[12]?.justPressed)
input.update(inputData => {
inputData['height'] = Math.min(inputData.height + 0.1, 1)
return inputData
})
if (b[13]?.justPressed)
input.update(inputData => {
inputData['height'] = Math.min(inputData.height - 0.1, 1)
return inputData
})
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)
}
onMount(() => {
left = nipplejs.create({
zone: document.getElementById('left') as HTMLElement,
color: '#15191e80',
dynamicPage: true,
mode: 'static',
restOpacity: 1
})
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)
right = nipplejs.create({
zone: document.getElementById('right') as HTMLElement,
color: '#15191e80',
dynamicPage: true,
mode: 'static',
restOpacity: 1
})
outControllerData.set(data)
}
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 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 handleJoyMove = (key: 'left' | 'right', data: vector) => {
input.update(inputData => {
inputData[key] = data
return inputData
})
throttle.throttle(updateData, throttle_timing)
}
const handleRange = (event: Event, key: 'speed' | 'height' | 's1') => {
const value: number = event.target?.value
const updateData = () => {
data[0] = $input.left.x
data[1] = $input.left.y
data[2] = $input.right.x
data[3] = $input.right.y
data[4] = $input.height
data[5] = $input.speed
data[6] = $input.s1
input.update(inputData => {
inputData[key] = value
return inputData
})
throttle.throttle(updateData, throttle_timing)
}
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
if (event.key === 'ArrowLeft') data.right.x = down ? 1 : 0
if (event.key === 'ArrowRight') data.right.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))
}
const changeWalkGait = (walkGaitValue: WalkGaits) => {
walkGait.set(walkGaitValue)
}
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>
<div class="flex-1"></div>
<div id="right" class="flex w-60 items-center"></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 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="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="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={1}
step={0.01}
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>
<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}
<div class="join">
{#each Object.values(WalkGaits) as gaitValue}
{#if typeof gaitValue === 'number'}
<button
class="btn join-item btn-sm"
class:btn-secondary={$walkGait === gaitValue}
onclick={() => changeWalkGait(gaitValue)}
>
{walkGaitLabels[gaitValue]}
</button>
{/if}
{/each}
</div>
<div class="flex gap-4">
<div>
<label for="s1">S1</label>
<input
type="range"
name="s1"
min="0"
step="0.01"
max="1"
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"
step="0.01"
max="1"
oninput={e => handleRange(e, 'speed')}
class="range range-sm range-primary"
/>
</div>
</div>
{/if}
{#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="100"
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="100"
oninput={e => handleRange(e, 'speed')}
class="range range-sm range-primary" />
</div>
</div>
{/if}
</div>
</div>
</div>
<svelte:window onkeyup={handleKeyup} onkeydown={handleKeyup} />

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