diff --git a/app/.eslintrc.cjs b/app/.eslintrc.cjs index 0b75758..6e9d20d 100644 --- a/app/.eslintrc.cjs +++ b/app/.eslintrc.cjs @@ -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' + } + } + ] +} diff --git a/app/.prettierrc b/app/.prettierrc index 647b80d..d8a02c3 100644 --- a/app/.prettierrc +++ b/app/.prettierrc @@ -1,7 +1,7 @@ { "useTabs": false, "singleQuote": true, - "tabWidth": 2, + "tabWidth": 4, "trailingComma": "none", "arrowParens": "avoid", "experimentalTernaries": true, diff --git a/app/.vscode/extensions.json b/app/.vscode/extensions.json index 11efe25..e228a52 100644 --- a/app/.vscode/extensions.json +++ b/app/.vscode/extensions.json @@ -1,3 +1,7 @@ { - "recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"] + "recommendations": [ + "svelte.svelte-vscode", + "bradlc.vscode-tailwindcss", + "esbenp.prettier-vscode" + ] } diff --git a/app/env.d.ts b/app/env.d.ts index fe77695..8c7f1c8 100644 --- a/app/env.d.ts +++ b/app/env.d.ts @@ -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 } diff --git a/app/package.json b/app/package.json index 5ae481a..496b907 100644 --- a/app/package.json +++ b/app/package.json @@ -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.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": { - "@msgpack/msgpack": "^3.1.2", - "@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", - "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" + "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": { + "@msgpack/msgpack": "^3.1.2", + "@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", + "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" } diff --git a/app/playwright.config.ts b/app/playwright.config.ts index e6e8a23..0f0b8a6 100644 --- a/app/playwright.config.ts +++ b/app/playwright.config.ts @@ -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 diff --git a/app/src/app.css b/app/src/app.css index 5e65e5c..2721cc4 100644 --- a/app/src/app.css +++ b/app/src/app.css @@ -23,6 +23,14 @@ --base-content: oklch(0.3 0.012 256); } +button { + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; +} + #nipple_0_0, #nipple_1_1 { z-index: 10 !important; diff --git a/app/src/app.d.ts b/app/src/app.d.ts index 743f07b..5b99784 100644 --- a/app/src/app.d.ts +++ b/app/src/app.d.ts @@ -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 {} diff --git a/app/src/app.html b/app/src/app.html index 9c7db81..0d164ff 100644 --- a/app/src/app.html +++ b/app/src/app.html @@ -1,14 +1,17 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ diff --git a/app/src/index.test.ts b/app/src/index.test.ts deleted file mode 100644 index e07cbbd..0000000 --- a/app/src/index.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -describe('sum test', () => { - it('adds 1 + 2 to equal 3', () => { - expect(1 + 2).toBe(3); - }); -}); diff --git a/app/src/lib/api.ts b/app/src/lib/api.ts index efcf40f..451f86a 100644 --- a/app/src/lib/api.ts +++ b/app/src/lib/api.ts @@ -1,22 +1,22 @@ -import { get } from 'svelte/store'; -import { Err, Ok, type Result } from './utilities'; -import { location } from './stores'; +import { get } from 'svelte/store' +import { Err, Ok, type Result } from './utilities' +import { location } from './stores' export namespace api { export function get(endpoint: string, params?: RequestInit) { - return sendRequest(endpoint, 'GET', null, params); + return sendRequest(endpoint, 'GET', null, params) } export function post(endpoint: string, data?: unknown) { - return sendRequest(endpoint, 'POST', data); + return sendRequest(endpoint, 'POST', data) } export function put(endpoint: string, data?: unknown) { - return sendRequest(endpoint, 'PUT', data); + return sendRequest(endpoint, 'PUT', data) } export function remove(endpoint: string) { - return sendRequest(endpoint, 'DELETE'); + return sendRequest(endpoint, 'DELETE') } } @@ -26,8 +26,8 @@ async function sendRequest( data?: unknown, params?: RequestInit ): Promise> { - 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,43 +38,42 @@ async function sendRequest( Authorization: 'Basic', 'Content-Type': 'application/json' } - }; + } - let response; + let response try { - response = await fetch(endpoint, request); + response = await fetch(endpoint, request) } catch (error) { - return Err.new(new Error(), 'An error has occurred'); + 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(location)) return url; - const protocol = window.location.protocol; - return `${protocol}//${get(location)}${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}`) } } diff --git a/app/src/lib/components/Collapsible.svelte b/app/src/lib/components/Collapsible.svelte index 9180622..14ce6c7 100644 --- a/app/src/lib/components/Collapsible.svelte +++ b/app/src/lib/components/Collapsible.svelte @@ -1,18 +1,18 @@
diff --git a/app/src/lib/components/ConfirmDialog.svelte b/app/src/lib/components/ConfirmDialog.svelte index c43ee4f..e1f5705 100644 --- a/app/src/lib/components/ConfirmDialog.svelte +++ b/app/src/lib/components/ConfirmDialog.svelte @@ -1,43 +1,48 @@ {#if isOpen} - {@const SvelteComponent = labels?.confirm.icon} - {/if} diff --git a/app/src/lib/components/GithubUpdateDialog.svelte b/app/src/lib/components/GithubUpdateDialog.svelte index 7c3e2ad..114e0c8 100644 --- a/app/src/lib/components/GithubUpdateDialog.svelte +++ b/app/src/lib/components/GithubUpdateDialog.svelte @@ -1,61 +1,61 @@ {#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() }} > Close - 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, type ModalProps } from 'svelte-modals' - let { - isOpen, - title, - message, - onDismiss, - labels = { - dismiss: { label: 'Dismiss', icon: Check }, - }, - }: ModalProps = $props(); + let { + isOpen, + title, + message, + onDismiss, + labels = { + dismiss: { label: 'Dismiss', icon: Check } + } + }: ModalProps = $props() {#if isOpen} - {/if} diff --git a/app/src/lib/components/OrientationIndicator.svelte b/app/src/lib/components/OrientationIndicator.svelte index ce1c4c5..04888ff 100644 --- a/app/src/lib/components/OrientationIndicator.svelte +++ b/app/src/lib/components/OrientationIndicator.svelte @@ -1,78 +1,78 @@
- +
diff --git a/app/src/lib/components/SettingsCard.svelte b/app/src/lib/components/SettingsCard.svelte index 364be63..1a82647 100644 --- a/app/src/lib/components/SettingsCard.svelte +++ b/app/src/lib/components/SettingsCard.svelte @@ -1,60 +1,76 @@ {#if collapsible} -
- - {@render icon?.()} - {@render title?.()} - - + class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg" + > +
+ + {@render icon?.()} + {@render title?.()} + + +
+ {#if open} +
+ {@render children?.()} +
+ {/if}
- {#if open} -
- {@render children?.()} -
- {/if} -
{:else} -
- - {@render icon?.()} - {@render title?.()} - - {@render right?.()} + class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg" + > +
+ + {@render icon?.()} + {@render title?.()} + + {@render right?.()} +
+
+ {@render children?.()} +
-
- {@render children?.()} -
-
{/if} diff --git a/app/src/lib/components/Spinner.svelte b/app/src/lib/components/Spinner.svelte index 36dee22..4a48424 100644 --- a/app/src/lib/components/Spinner.svelte +++ b/app/src/lib/components/Spinner.svelte @@ -1,9 +1,8 @@
- -

Loading...

+ +

Loading...

diff --git a/app/src/lib/components/StatusItem.svelte b/app/src/lib/components/StatusItem.svelte index e842d03..1174d43 100644 --- a/app/src/lib/components/StatusItem.svelte +++ b/app/src/lib/components/StatusItem.svelte @@ -1,45 +1,45 @@
- {#if icon} -
- + {#if icon} +
+ +
+ {/if} +
+
{title}
+
{description}
- {/if} -
-
{title}
-
{description}
-
- {@render children?.()} + {@render children?.()}
diff --git a/app/src/lib/components/Stream.svelte b/app/src/lib/components/Stream.svelte index 1c9589d..e4b51cd 100644 --- a/app/src/lib/components/Stream.svelte +++ b/app/src/lib/components/Stream.svelte @@ -1,10 +1,10 @@
diff --git a/app/src/lib/components/Toast.svelte b/app/src/lib/components/Toast.svelte index bc2eb22..86b924b 100644 --- a/app/src/lib/components/Toast.svelte +++ b/app/src/lib/components/Toast.svelte @@ -1,35 +1,37 @@
- {#each $notifications as notification (notification.id)} - {@const SvelteComponent = icon[notification.type]} -
- - {notification.message} -
- {/each} + {#each $notifications as notification (notification.id)} + {@const SvelteComponent = icon[notification.type]} +
+ + {notification.message} +
+ {/each}
diff --git a/app/src/lib/components/Visualization.svelte b/app/src/lib/components/Visualization.svelte index 18b1ee0..f153aac 100644 --- a/app/src/lib/components/Visualization.svelte +++ b/app/src/lib/components/Visualization.svelte @@ -1,332 +1,339 @@ diff --git a/app/src/lib/components/input/InputPassword.svelte b/app/src/lib/components/input/InputPassword.svelte index f0fe528..766fda0 100644 --- a/app/src/lib/components/input/InputPassword.svelte +++ b/app/src/lib/components/input/InputPassword.svelte @@ -1,19 +1,19 @@
- \ No newline at end of file + diff --git a/app/src/lib/components/input/VerticalSlider.svelte b/app/src/lib/components/input/VerticalSlider.svelte index a5248e9..2a528dd 100644 --- a/app/src/lib/components/input/VerticalSlider.svelte +++ b/app/src/lib/components/input/VerticalSlider.svelte @@ -1,34 +1,35 @@ + type="range" + style="writing-mode: vertical-lr; direction: rtl" + class="cursor-pointer" + {min} + {max} + {step} + bind:value + {...rest} +/> diff --git a/app/src/lib/components/input/index.ts b/app/src/lib/components/input/index.ts index eb527c5..edf3dce 100644 --- a/app/src/lib/components/input/index.ts +++ b/app/src/lib/components/input/index.ts @@ -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' diff --git a/app/src/lib/components/layout/Widget.svelte b/app/src/lib/components/layout/Widget.svelte index b6011d2..6ea7cfe 100644 --- a/app/src/lib/components/layout/Widget.svelte +++ b/app/src/lib/components/layout/Widget.svelte @@ -1,9 +1,9 @@
diff --git a/app/src/lib/components/layout/WidgetContainer.svelte b/app/src/lib/components/layout/WidgetContainer.svelte index 86463c5..a7b8005 100644 --- a/app/src/lib/components/layout/WidgetContainer.svelte +++ b/app/src/lib/components/layout/WidgetContainer.svelte @@ -1,40 +1,41 @@
-
- {#each container.widgets as widget, index (widget.id + '-' + index)} - - {#if isWidgetConfig(widget)} - {@const SvelteComponent = WidgetComponents[widget.component]} - - {:else if widget.widgets} - - {/if} - - {#if index !== container.widgets.length - 1} -
-
- {/if} - {/each} -
+
+ {#each container.widgets as widget, index (widget.id + '-' + index)} + + {#if isWidgetConfig(widget)} + {@const SvelteComponent = WidgetComponents[widget.component]} + + {:else if widget.widgets} + + {/if} + + {#if index !== container.widgets.length - 1} +
+ {/if} + {/each} +
diff --git a/app/src/lib/components/menu/GithubButton.svelte b/app/src/lib/components/menu/GithubButton.svelte index 163b165..28f005b 100644 --- a/app/src/lib/components/menu/GithubButton.svelte +++ b/app/src/lib/components/menu/GithubButton.svelte @@ -1,15 +1,15 @@ {#if github.active} - + {/if} diff --git a/app/src/lib/components/menu/LogoButton.svelte b/app/src/lib/components/menu/LogoButton.svelte index 7e77f98..c3d5b28 100644 --- a/app/src/lib/components/menu/LogoButton.svelte +++ b/app/src/lib/components/menu/LogoButton.svelte @@ -1,14 +1,11 @@ - - Logo -

{appName}

+
+ Logo +

{appName}

diff --git a/app/src/lib/components/menu/Menu.svelte b/app/src/lib/components/menu/Menu.svelte index ade0a9f..93b175d 100644 --- a/app/src/lib/components/menu/Menu.svelte +++ b/app/src/lib/components/menu/Menu.svelte @@ -1,194 +1,198 @@
- + - + -
+
-
- -
- {copyright} +
+ +
+ {copyright} +
-
diff --git a/app/src/lib/components/menu/MenuList.svelte b/app/src/lib/components/menu/MenuList.svelte index f945375..09debd9 100644 --- a/app/src/lib/components/menu/MenuList.svelte +++ b/app/src/lib/components/menu/MenuList.svelte @@ -1,48 +1,54 @@ diff --git a/app/src/lib/components/statusbar/FullscreenButton.svelte b/app/src/lib/components/statusbar/FullscreenButton.svelte index ea333a0..fbb1624 100644 --- a/app/src/lib/components/statusbar/FullscreenButton.svelte +++ b/app/src/lib/components/statusbar/FullscreenButton.svelte @@ -1,10 +1,10 @@ \ No newline at end of file + diff --git a/app/src/lib/components/statusbar/RSSIIndicator.svelte b/app/src/lib/components/statusbar/RSSIIndicator.svelte index 40f0a14..d9f6885 100644 --- a/app/src/lib/components/statusbar/RSSIIndicator.svelte +++ b/app/src/lib/components/statusbar/RSSIIndicator.svelte @@ -1,33 +1,33 @@
-
- {#if showDBm} - - {rssi} dBm - - {/if} -
- -
-
-
\ No newline at end of file +
+ {#if showDBm} + + {rssi} dBm + + {/if} +
+ +
+
+
diff --git a/app/src/lib/components/statusbar/SleepButton.svelte b/app/src/lib/components/statusbar/SleepButton.svelte index 1f75687..05e80a7 100644 --- a/app/src/lib/components/statusbar/SleepButton.svelte +++ b/app/src/lib/components/statusbar/SleepButton.svelte @@ -1,13 +1,13 @@ {#if $features.sleep} diff --git a/app/src/lib/components/statusbar/StopButton.svelte b/app/src/lib/components/statusbar/StopButton.svelte index d8f5c13..28ff091 100644 --- a/app/src/lib/components/statusbar/StopButton.svelte +++ b/app/src/lib/components/statusbar/StopButton.svelte @@ -1,10 +1,9 @@ - - \ No newline at end of file + diff --git a/app/src/lib/components/statusbar/ThemeButton.svelte b/app/src/lib/components/statusbar/ThemeButton.svelte index 809d759..b65ed15 100644 --- a/app/src/lib/components/statusbar/ThemeButton.svelte +++ b/app/src/lib/components/statusbar/ThemeButton.svelte @@ -1,9 +1,9 @@ \ No newline at end of file + diff --git a/app/src/lib/components/statusbar/TopBar.svelte b/app/src/lib/components/statusbar/TopBar.svelte index 85782c6..b08b9b7 100644 --- a/app/src/lib/components/statusbar/TopBar.svelte +++ b/app/src/lib/components/statusbar/TopBar.svelte @@ -1,17 +1,17 @@
- diff --git a/app/src/lib/components/statusbar/UpdateIndicator.svelte b/app/src/lib/components/statusbar/UpdateIndicator.svelte index 7dc0ffd..08e27d9 100644 --- a/app/src/lib/components/statusbar/UpdateIndicator.svelte +++ b/app/src/lib/components/statusbar/UpdateIndicator.svelte @@ -1,109 +1,111 @@ {#if update} -
- -
+
+ +
{/if} diff --git a/app/src/lib/components/statusbar/ViewSelector.svelte b/app/src/lib/components/statusbar/ViewSelector.svelte index 9961455..c145643 100644 --- a/app/src/lib/components/statusbar/ViewSelector.svelte +++ b/app/src/lib/components/statusbar/ViewSelector.svelte @@ -1,6 +1,6 @@ - v.name)} /> \ No newline at end of file + v.name)} /> diff --git a/app/src/lib/components/statusbar/statusbar.svelte b/app/src/lib/components/statusbar/statusbar.svelte index 61035cf..a724ed2 100644 --- a/app/src/lib/components/statusbar/statusbar.svelte +++ b/app/src/lib/components/statusbar/statusbar.svelte @@ -1,38 +1,38 @@ diff --git a/app/src/lib/components/toasts/Toast.svelte b/app/src/lib/components/toasts/Toast.svelte index 3c95016..497072b 100644 --- a/app/src/lib/components/toasts/Toast.svelte +++ b/app/src/lib/components/toasts/Toast.svelte @@ -1,35 +1,37 @@
- {#each $notifications as notification (notification.id)} - {@const SvelteComponent = icon[notification.type]} -
- - {notification.message} -
- {/each} + {#each $notifications as notification (notification.id)} + {@const SvelteComponent = icon[notification.type]} +
+ + {notification.message} +
+ {/each}
diff --git a/app/src/lib/components/toasts/notifications.ts b/app/src/lib/components/toasts/notifications.ts index 3131abb..57e28e5 100644 --- a/app/src/lib/components/toasts/notifications.ts +++ b/app/src/lib/components/toasts/notifications.ts @@ -3,40 +3,40 @@ import { writable, derived, type Writable } from 'svelte/store' 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 = 4000) => send(msg, 'error', timeout), + warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout), + info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout), + success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout) + } } function generateId() { - return '_' + Math.random().toString(36).substr(2, 9) + return '_' + Math.random().toString(36).substr(2, 9) } export const notifications = createNotificationStore() diff --git a/app/src/lib/components/widget/ChartWidget.svelte b/app/src/lib/components/widget/ChartWidget.svelte index 71d47a1..dd5fe92 100644 --- a/app/src/lib/components/widget/ChartWidget.svelte +++ b/app/src/lib/components/widget/ChartWidget.svelte @@ -1,101 +1,102 @@
-
- -
+
+ +
diff --git a/app/src/lib/components/widget/Selector.svelte b/app/src/lib/components/widget/Selector.svelte index b4666f3..7e3d606 100644 --- a/app/src/lib/components/widget/Selector.svelte +++ b/app/src/lib/components/widget/Selector.svelte @@ -1,19 +1,20 @@ diff --git a/app/src/lib/gait.ts b/app/src/lib/gait.ts index b6fb846..726e3db 100644 --- a/app/src/lib/gait.ts +++ b/app/src/lib/gait.ts @@ -3,423 +3,423 @@ import type { body_state_t } from './kinematic' import { currentKinematic } from './stores/featureFlags' export interface gait_state_t { - step_height: number - step_x: number - step_z: number - step_angle: number - step_velocity: number - step_depth: number + step_height: number + step_x: number + step_z: number + step_angle: number + step_velocity: number + step_depth: number } export interface ControllerCommand { - lx: number - ly: number - rx: number - ry: number - h: number - s: number - s1: number + lx: number + ly: number + rx: number + ry: number + h: number + s: number + s1: number } export abstract class GaitState { - protected abstract name: string + protected abstract name: string - protected dt = 0.02 - protected body_state!: body_state_t - protected gait_state: gait_state_t = { - step_height: 0.4, - step_x: 0, - step_z: 0, - step_angle: 0, - step_velocity: 1, - step_depth: 0.002 - } - - public get default_feet_pos() { - return get(currentKinematic).getDefaultFeetPos() - } - - protected get default_height() { - return 0.5 - } - - begin() { - console.log('Starting', this.name) - } - end() { - console.log('Ending', this.name) - } - step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { - this.map_command(command) - this.body_state = body_state - this.dt = dt / 1000 - return body_state - } - - map_command(command: ControllerCommand) { - const newCommand = { - step_height: 0.4 + (command.s1 + 1) / 2, - step_x: command.ly, - step_z: -command.lx, - step_velocity: command.s, - step_angle: command.rx, - step_depth: 0.002 + protected dt = 0.02 + protected body_state!: body_state_t + protected gait_state: gait_state_t = { + step_height: 0.4, + step_x: 0, + step_z: 0, + step_angle: 0, + step_velocity: 1, + step_depth: 0.002 } - this.gait_state = newCommand - } + public get default_feet_pos() { + return get(currentKinematic).getDefaultFeetPos() + } + + protected get default_height() { + return 0.5 + } + + begin() { + console.log('Starting', this.name) + } + end() { + console.log('Ending', this.name) + } + step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { + this.map_command(command) + this.body_state = body_state + this.dt = dt / 1000 + return body_state + } + + map_command(command: ControllerCommand) { + const newCommand = { + step_height: 0.4 + (command.s1 + 1) / 2, + step_x: command.ly, + step_z: -command.lx, + step_velocity: command.s, + step_angle: command.rx, + step_depth: 0.002 + } + + this.gait_state = newCommand + } } export class IdleState extends GaitState { - protected name = 'Idle' + protected name = 'Idle' } export class CalibrationState extends GaitState { - protected name = 'Calibration' + protected name = 'Calibration' - // eslint-disable-next-line @typescript-eslint/no-unused-vars - step(body_state: body_state_t, _command: ControllerCommand) { - body_state.omega = 0 - body_state.phi = 0 - body_state.psi = 0 - body_state.xm = 0 - body_state.ym = this.default_height * 10 - body_state.zm = 0 - body_state.feet = this.default_feet_pos - return body_state - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + step(body_state: body_state_t, _command: ControllerCommand) { + body_state.omega = 0 + body_state.phi = 0 + body_state.psi = 0 + body_state.xm = 0 + body_state.ym = this.default_height * 10 + body_state.zm = 0 + body_state.feet = this.default_feet_pos + return body_state + } } export class RestState extends GaitState { - protected name = 'Rest' + protected name = 'Rest' - // eslint-disable-next-line @typescript-eslint/no-unused-vars - step(body_state: body_state_t, _command: ControllerCommand) { - body_state.omega = 0 - body_state.phi = 0 - body_state.psi = 0 - body_state.xm = 0 - body_state.ym = this.default_height / 2 - body_state.zm = 0 - body_state.feet = this.default_feet_pos - return body_state - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + step(body_state: body_state_t, _command: ControllerCommand) { + body_state.omega = 0 + body_state.phi = 0 + body_state.psi = 0 + body_state.xm = 0 + body_state.ym = this.default_height / 2 + body_state.zm = 0 + body_state.feet = this.default_feet_pos + return body_state + } } export class StandState extends GaitState { - protected name = 'Stand' + protected name = 'Stand' - step(body_state: body_state_t, command: ControllerCommand) { - body_state.omega = 0 - body_state.phi = command.rx * 10 * (Math.PI / 2) - body_state.psi = command.ry * 10 * (Math.PI / 2) - body_state.xm = command.ly / 4 - body_state.zm = command.lx / 4 - body_state.feet = this.default_feet_pos - return body_state - } + step(body_state: body_state_t, command: ControllerCommand) { + body_state.omega = 0 + body_state.phi = command.rx * 10 * (Math.PI / 2) + body_state.psi = command.ry * 10 * (Math.PI / 2) + body_state.xm = command.ly / 4 + body_state.zm = command.lx / 4 + body_state.feet = this.default_feet_pos + return body_state + } } export class BezierState extends GaitState { - protected name = 'Bezier' - protected phase = 0 - protected phase_num = 0 - protected step_length = 0 - protected stand_offset = 0.85 - protected mode: 'crawl' | 'trot' = 'trot' - protected speed_factor = 1 - offset = [0, 0.5, 0.75, 0.25] + protected name = 'Bezier' + protected phase = 0 + protected phase_num = 0 + protected step_length = 0 + protected stand_offset = 0.85 + protected mode: 'crawl' | 'trot' = 'trot' + protected speed_factor = 1 + offset = [0, 0.5, 0.75, 0.25] - 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 shift_start_pos = { x: 0, z: 0 } + protected shift_target_pos = { x: 0, z: 0 } + protected shift_start_time = 0 + protected current_shift_leg = -1 - constructor() { - super() - this.set_mode(this.mode) - } - - begin() { - super.begin() - } - - set_mode(mode: 'crawl' | 'trot', duty?: number, order?: [number, number, number, number]) { - console.log('BezierState set_mode', mode) - - this.mode = mode - if (mode === 'crawl') { - this.speed_factor = 0.5 - this.stand_offset = duty ?? 0.85 - const o = order ?? [3, 0, 2, 1] - const base = [0, 0.25, 0.5, 0.75] - const offsets = new Array(4).fill(0) - for (let i = 0; i < 4; i++) offsets[o[i]] = base[i] - this.offset = offsets - } else { - this.speed_factor = 2 - this.stand_offset = duty ?? 0.6 - this.offset = order ? (order.map(v => v % 1) as number[]) : [0, 0.5, 0.5, 0] + constructor() { + super() + this.set_mode(this.mode) } - } - end() { - super.end() - } - - step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { - super.step(body_state, command, dt) - this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2) - if (this.gait_state.step_x < 0) this.step_length = -this.step_length - this.update_phase() - this.update_body_position() - this.update_feet_positions() - return this.body_state - } - - update_phase() { - const m = this.gait_state - if (m.step_x === 0 && m.step_z === 0 && m.step_angle === 0) { - this.phase = 0 - return + begin() { + super.begin() } - 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 - } - } - 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 + set_mode(mode: 'crawl' | 'trot', duty?: number, order?: [number, number, number, number]) { + console.log('BezierState set_mode', mode) - if (this.mode !== 'crawl') return - - const { stance, swing, next_swing, time_to_lift } = this.get_leg_states() - - if (stance.length >= 3 && swing.length === 0 && next_swing !== -1) { - if (this.current_shift_leg !== next_swing) { - this.current_shift_leg = next_swing - this.shift_start_pos.x = this.body_state.xm - this.shift_start_pos.z = this.body_state.zm - - const remaining_legs = stance.filter(leg => leg !== next_swing) - const target = this.stance_centroid(remaining_legs) - this.shift_target_pos.x = target[0] - this.shift_target_pos.z = target[2] - - this.shift_start_time = time_to_lift - } - - const total_time = this.shift_start_time - const progress = total_time > 0 ? 1 - time_to_lift / total_time : 1 - const smooth_progress = this.smoothstep01(Math.max(0, Math.min(1, progress))) - - this.body_state.xm = this.lerp( - this.shift_start_pos.x, - this.shift_target_pos.x, - smooth_progress - ) - this.body_state.zm = this.lerp( - this.shift_start_pos.z, - this.shift_target_pos.z, - smooth_progress - ) - } - } - - protected lerp(a: number, b: number, t: number): number { - return a + (b - a) * t - } - - protected stance_centroid(legs: number[]): number[] { - if (legs.length === 0) return [this.body_state.xm, 0, this.body_state.zm] - - let sx = 0, - sz = 0 - for (const i of legs) { - sx += this.body_state.feet[i][0] - sz += this.body_state.feet[i][2] - } - return [sx / legs.length, 0, sz / legs.length] - } - - protected get_leg_states(): { - stance: number[] - swing: number[] - next_swing: number - time_to_lift: number - } { - const stance: number[] = [] - const swing: number[] = [] - let next_swing = -1 - let min_time_to_swing = Infinity - - for (let i = 0; i < 4; i++) { - let phase = this.phase + this.offset[i] - if (phase >= 1) phase -= 1 - - if (phase <= this.stand_offset) { - stance.push(i) - const time_to_swing = this.stand_offset - phase - if (time_to_swing < min_time_to_swing) { - min_time_to_swing = time_to_swing - next_swing = i + this.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] } - } else { - swing.push(i) - } } - return { stance, swing, next_swing, time_to_lift: min_time_to_swing } - } + end() { + super.end() + } - protected smoothstep01(t: number): number { - const x = Math.max(0, Math.min(1, t)) - return x * x * (3 - 2 * x) - } + step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) { + super.step(body_state, command, dt) + this.step_length = Math.sqrt(this.gait_state.step_x ** 2 + this.gait_state.step_z ** 2) + if (this.gait_state.step_x < 0) this.step_length = -this.step_length + this.update_phase() + this.update_body_position() + this.update_feet_positions() + return this.body_state + } - update_feet_positions() { - for (let i = 0; i < 4; i++) this.body_state.feet[i] = this.update_foot_position(i) - } + 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 + } + } - 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)) - } + 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 - stand_controller(index: number, phase: number) { - const depth = this.gait_state.step_depth - return this.controller(index, phase, stance_curve, depth) - } + if (this.mode !== 'crawl') return - swing_controller(index: number, phase: number) { - const height = this.gait_state.step_height - return this.controller(index, phase, bezier_curve, height) - } + const { stance, swing, next_swing, time_to_lift } = this.get_leg_states() - controller( - index: number, - phase: number, - controller: (length: number, angle: number, ...args: number[]) => number[], - ...args: number[] - ) { - let length = this.step_length / 2 - let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2 - const delta_pos = controller(length, angle, ...args, phase) + 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 - length = this.gait_state.step_angle * 2 - angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index]) + 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] - const delta_rot = controller(length, angle, ...args, phase) + this.shift_start_time = time_to_lift + } - this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2 - this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2 - if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle) - this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2 + const 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))) - return this.body_state.feet[index] - } + this.body_state.xm = this.lerp( + this.shift_start_pos.x, + this.shift_target_pos.x, + smooth_progress + ) + this.body_state.zm = this.lerp( + this.shift_start_pos.z, + this.shift_target_pos.z, + smooth_progress + ) + } + } + + protected lerp(a: number, b: number, t: number): number { + return a + (b - a) * t + } + + protected stance_centroid(legs: number[]): number[] { + if (legs.length === 0) return [this.body_state.xm, 0, this.body_state.zm] + + let sx = 0, + sz = 0 + for (const i of legs) { + sx += this.body_state.feet[i][0] + sz += this.body_state.feet[i][2] + } + return [sx / legs.length, 0, sz / legs.length] + } + + protected get_leg_states(): { + stance: number[] + swing: number[] + next_swing: number + time_to_lift: number + } { + const stance: number[] = [] + const swing: number[] = [] + let next_swing = -1 + let min_time_to_swing = Infinity + + for (let i = 0; i < 4; i++) { + let phase = this.phase + this.offset[i] + if (phase >= 1) phase -= 1 + + if (phase <= this.stand_offset) { + stance.push(i) + const time_to_swing = this.stand_offset - phase + if (time_to_swing < min_time_to_swing) { + min_time_to_swing = time_to_swing + next_swing = i + } + } else { + swing.push(i) + } + } + + return { stance, swing, next_swing, time_to_lift: min_time_to_swing } + } + + protected smoothstep01(t: number): number { + const x = Math.max(0, Math.min(1, t)) + return x * x * (3 - 2 * x) + } + + update_feet_positions() { + for (let i = 0; i < 4; i++) this.body_state.feet[i] = this.update_foot_position(i) + } + + update_foot_position(index: number): number[] { + let phase = this.phase + this.offset[index] + if (phase >= 1) phase -= 1 + 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)) + } + + stand_controller(index: number, phase: number) { + const 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) + } + + controller( + index: number, + phase: number, + controller: (length: number, angle: number, ...args: number[]) => number[], + ...args: number[] + ) { + let length = this.step_length / 2 + let angle = Math.atan2(this.gait_state.step_z, this.step_length) * 2 + const delta_pos = controller(length, angle, ...args, phase) + + length = this.gait_state.step_angle * 2 + angle = yawArc(this.default_feet_pos[index], this.body_state.feet[index]) + + const delta_rot = controller(length, angle, ...args, phase) + + this.body_state.feet[index][0] += delta_pos[0] + delta_rot[0] * 0.2 + this.body_state.feet[index][2] += delta_pos[2] + delta_rot[2] * 0.2 + if (this.gait_state.step_x || this.gait_state.step_z || this.gait_state.step_angle) + this.body_state.feet[index][1] += delta_pos[1] + delta_rot[1] * 0.2 + + return this.body_state.feet[index] + } } const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => { - const X_POLAR = Math.cos(angle) - const Y_POLAR = Math.sin(angle) + const X_POLAR = Math.cos(angle) + const Y_POLAR = Math.sin(angle) - const step = length * (1 - 2 * phase) - const X = step * X_POLAR - const Z = step * Y_POLAR - let Y = 0 - if (length !== 0) Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length)) - return [X, Y, Z] + const step = length * (1 - 2 * phase) + const X = step * X_POLAR + const Z = step * Y_POLAR + let Y = 0 + if (length !== 0) Y = -depth * Math.cos((Math.PI * (X + Y)) / (2 * length)) + return [X, Y, Z] } const yawArc = (default_foot_pos: number[], current_foot_pos: number[]): number => { - const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2) - const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0]) - const offsets = [ - current_foot_pos[0] - default_foot_pos[0], - current_foot_pos[2] - default_foot_pos[2], - current_foot_pos[1] - default_foot_pos[1] - ] - const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2) - const offset_mod = Math.atan2(offset_mag, foot_mag) + const foot_mag = Math.sqrt(default_foot_pos[0] ** 2 + default_foot_pos[2] ** 2) + const foot_dir = Math.atan2(default_foot_pos[2], default_foot_pos[0]) + const offsets = [ + current_foot_pos[0] - default_foot_pos[0], + current_foot_pos[2] - default_foot_pos[2], + current_foot_pos[1] - default_foot_pos[1] + ] + const offset_mag = Math.sqrt(offsets[0] ** 2 + offsets[2] ** 2) + const offset_mod = Math.atan2(offset_mag, foot_mag) - return Math.PI / 2.0 + foot_dir + offset_mod + return Math.PI / 2.0 + foot_dir + offset_mod } const bezier_curve = (length: number, angle: number, height: number, phase: number): number[] => { - const control_points = get_control_points(length, angle, height) - const n = control_points.length - 1 + const control_points = get_control_points(length, angle, height) + const n = control_points.length - 1 - const point = [0, 0, 0] - for (let i = 0; i <= n; i++) { - const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i) - point[0] += bernstein_poly * control_points[i][0] - point[1] += bernstein_poly * control_points[i][1] - point[2] += bernstein_poly * control_points[i][2] - } - return point + const point = [0, 0, 0] + for (let i = 0; i <= n; i++) { + const bernstein_poly = comb(n, i) * Math.pow(phase, i) * Math.pow(1 - phase, n - i) + point[0] += bernstein_poly * control_points[i][0] + point[1] += bernstein_poly * control_points[i][1] + point[2] += bernstein_poly * control_points[i][2] + } + return point } const get_control_points = (length: number, angle: number, height: number): number[][] => { - const X_POLAR = Math.cos(angle) - const Z_POLAR = Math.sin(angle) + const X_POLAR = Math.cos(angle) + const Z_POLAR = Math.sin(angle) - const STEP = [ - -length, - -length * 1.4, - -length * 1.5, - -length * 1.5, - -length * 1.5, - 0.0, - 0.0, - 0.0, - length * 1.5, - length * 1.5, - length * 1.4, - length - ] + const STEP = [ + -length, + -length * 1.4, + -length * 1.5, + -length * 1.5, + -length * 1.5, + 0.0, + 0.0, + 0.0, + length * 1.5, + length * 1.5, + length * 1.4, + length + ] - const Y = [ - 0.0, - 0.0, - height * 0.9, - height * 0.9, - height * 0.9, - height * 0.9, - height * 0.9, - height * 1.1, - height * 1.1, - height * 1.1, - 0.0, - 0.0 - ] + const Y = [ + 0.0, + 0.0, + height * 0.9, + height * 0.9, + height * 0.9, + height * 0.9, + height * 0.9, + height * 1.1, + height * 1.1, + height * 1.1, + 0.0, + 0.0 + ] - const control_points: number[][] = [] + const control_points: number[][] = [] - for (let i = 0; i < STEP.length; i++) { - const X = STEP[i] * X_POLAR - const Z = STEP[i] * Z_POLAR - control_points.push([X, Y[i], Z]) - } + for (let i = 0; i < STEP.length; i++) { + const X = STEP[i] * X_POLAR + const Z = STEP[i] * Z_POLAR + control_points.push([X, Y[i], Z]) + } - return control_points + return control_points } const comb = (n: number, k: number): number => { - if (k < 0 || k > n) return 0 - if (k === 0 || k === n) return 1 - k = Math.min(k, n - k) - let c = 1 - for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1) - return c + if (k < 0 || k > n) return 0 + if (k === 0 || k === n) return 1 + k = Math.min(k, n - k) + let c = 1 + for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1) + return c } diff --git a/app/src/lib/kinematic.ts b/app/src/lib/kinematic.ts index ec26baa..9fa3418 100644 --- a/app/src/lib/kinematic.ts +++ b/app/src/lib/kinematic.ts @@ -1,32 +1,32 @@ export interface body_state_t { - omega: number - phi: number - psi: number - xm: number - ym: number - zm: number - feet: number[][] + omega: number + phi: number + psi: number + xm: number + ym: number + zm: number + feet: number[][] } export interface position { - x: number - y: number - z: number + x: number + y: number + z: number } export interface target_position { - x: number - z: number - yaw: number + x: number + z: number + yaw: number } export interface KinematicParams { - coxa: number - coxa_offset: number - femur: number - tibia: number - L: number - W: number + coxa: number + coxa_offset: number + femur: number + tibia: number + L: number + W: number } const { cos, sin, atan2, acos, sqrt, max, min } = Math @@ -34,107 +34,114 @@ const { cos, sin, atan2, acos, sqrt, max, min } = Math const DEG2RAD = 0.017453292519943 export default class Kinematic { - coxa: number - coxa_offset: number - femur: number - tibia: number + coxa: number + coxa_offset: number + femur: number + tibia: number - L: number - W: number + L: number + W: number - DEG2RAD = DEG2RAD + DEG2RAD = DEG2RAD - mountOffsets: number[][] + mountOffsets: number[][] - invMountRot = [ - [0, 0, -1], - [0, 1, 0], - [1, 0, 0] - ] - - constructor(params: KinematicParams) { - this.coxa = params.coxa - this.coxa_offset = params.coxa_offset - this.femur = params.femur - this.tibia = params.tibia - this.L = params.L - this.W = params.W - - this.mountOffsets = [ - [this.L / 2, 0, this.W / 2], - [this.L / 2, 0, -this.W / 2], - [-this.L / 2, 0, this.W / 2], - [-this.L / 2, 0, -this.W / 2] + invMountRot = [ + [0, 0, -1], + [0, 1, 0], + [1, 0, 0] ] - } - getDefaultFeetPos(): number[][] { - return this.mountOffsets.map((offset, i) => { - return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)] - }) - } + 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 - 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.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] + ] + } - const [mx, my, mz] = this.mountOffsets[i] - const px = bx - mx, - py = by - my, - pz = bz - mz + getDefaultFeetPos(): number[][] { + return this.mountOffsets.map((offset, i) => { + return [offset[0], -1, offset[2] + (i % 2 === 1 ? -this.coxa : this.coxa)] + }) + } - 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 + calcIK(p: body_state_t): number[] { + const roll = p.omega * this.DEG2RAD + const pitch = p.phi * this.DEG2RAD + const yaw = p.psi * this.DEG2RAD + const rot = this.euler2R(roll, pitch, yaw) + const inv_rot = [ + [rot[0][0], rot[1][0], rot[2][0]], + [rot[0][1], rot[1][1], rot[2][1]], + [rot[0][2], rot[1][2], rot[2][2]] + ] + const inv_trans = [ + -inv_rot[0][0] * p.xm - inv_rot[0][1] * p.ym - inv_rot[0][2] * p.zm, + -inv_rot[1][0] * p.xm - inv_rot[1][1] * p.ym - inv_rot[1][2] * p.zm, + -inv_rot[2][0] * p.xm - inv_rot[2][1] * p.ym - inv_rot[2][2] * p.zm + ] + return p.feet.flatMap((foot, i) => { + const [wx, wy, wz] = foot + const bx = inv_rot[0][0] * wx + inv_rot[0][1] * wy + inv_rot[0][2] * wz + inv_trans[0] + const by = inv_rot[1][0] * wx + inv_rot[1][1] * wy + inv_rot[1][2] * wz + inv_trans[1] + const bz = inv_rot[2][0] * wx + inv_rot[2][1] * wy + inv_rot[2][2] * wz + inv_trans[2] - const xLocal = i % 2 === 1 ? -lx : lx - return this.legIK(xLocal, ly, lz) - }) - } + const [mx, my, mz] = this.mountOffsets[i] + const px = bx - mx, + py = by - my, + pz = bz - mz - 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 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 - 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] - ] - } + const xLocal = i % 2 === 1 ? -lx : lx + return this.legIK(xLocal, ly, lz) + }) + } + + private legIK(x: number, y: number, z: number): [number, number, number] { + const F = sqrt(max(0, x * x + y * y - this.coxa * this.coxa)) + const G = F - this.coxa_offset + const H = sqrt(G * G + z * z) + const t1 = -atan2(y, x) - atan2(F, -this.coxa) + const D = + (H * H - this.femur * this.femur - this.tibia * this.tibia) / + (2 * this.femur * this.tibia) + const t3 = acos(max(-1, min(1, D))) + const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3)) + return [t1, t2, t3] + } + + private euler2R(roll: number, pitch: number, yaw: number): number[][] { + const cr = cos(roll), + sr = sin(roll) + const cp = cos(pitch), + sp = sin(pitch) + const cy = cos(yaw), + sy = sin(yaw) + return [ + [cp * cy, -cp * sy, sp], + [sr * sp * cy + sy * cr, -sr * sp * sy + cr * cy, -sr * cp], + [sr * sy - sp * cr * cy, sr * cy + sp * sy * cr, cr * cp] + ] + } } diff --git a/app/src/lib/sceneBuilder.ts b/app/src/lib/sceneBuilder.ts index 43648b2..2147a68 100644 --- a/app/src/lib/sceneBuilder.ts +++ b/app/src/lib/sceneBuilder.ts @@ -1,26 +1,26 @@ import { - Mesh, - PerspectiveCamera, - PlaneGeometry, - Scene, - WebGLRenderer, - AmbientLight, - DirectionalLight, - PCFSoftShadowMap, - type GridHelper, - ArrowHelper, - Vector3, - FogExp2, - CanvasTexture, - type ColorRepresentation, - type WebGLRendererParameters, - MeshPhongMaterial, - EquirectangularReflectionMapping, - ACESFilmicToneMapping, - MathUtils, - Group, - MeshBasicMaterial, - RepeatWrapping + 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' @@ -33,348 +33,348 @@ 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: Function | undefined - public gridHelper!: GridHelper - public model!: URDFRobot - public liveStreamTexture!: CanvasTexture - private fog!: FogExp2 - private isLoaded: boolean = false - public isDragging: boolean = false - highlightMaterial: any - sky!: Sky - transformControl: TransformControls - public modelGroup!: Group + public scene: Scene + public camera!: PerspectiveCamera + public ground!: Mesh + public renderer!: WebGLRenderer + public orbit: OrbitControls + public callback: Function | undefined + public gridHelper!: GridHelper + public model!: URDFRobot + public liveStreamTexture!: CanvasTexture + private fog!: FogExp2 + private isLoaded: boolean = false + public isDragging: boolean = false + highlightMaterial: any + sky!: Sky + transformControl: TransformControls + public modelGroup!: Group - constructor() { - this.scene = new Scene() - if (this.scene.environment?.mapping) { - this.scene.environment.mapping = EquirectangularReflectionMapping - } - return this - } - - public addRenderer = (parameters?: WebGLRendererParameters) => { - this.renderer = new WebGLRenderer(parameters) - this.renderer.outputColorSpace = 'srgb' - this.renderer.shadowMap.enabled = true - this.renderer.shadowMap.type = PCFSoftShadowMap - this.renderer.toneMapping = ACESFilmicToneMapping - this.renderer.toneMappingExposure = 0.85 - if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement) - return this - } - - public addSky = () => { - this.sky = new Sky() - this.sky.scale.setScalar(450000) - this.scene.add(this.sky) - const effectController = { - turbidity: 10, - rayleigh: 3, - mieCoefficient: 0.005, - mieDirectionalG: 0.7, - elevation: sunCalculator.calculateSunElevation(), - azimuth: 200, - exposure: this.renderer.toneMappingExposure - } - const uniforms = this.sky.material.uniforms - uniforms['turbidity'].value = effectController.turbidity - uniforms['rayleigh'].value = effectController.rayleigh - uniforms['mieCoefficient'].value = effectController.mieCoefficient - uniforms['mieDirectionalG'].value = effectController.mieDirectionalG - this.renderer.toneMappingExposure = 0.5 - const phi = MathUtils.degToRad(90 - effectController.elevation) - const theta = MathUtils.degToRad(effectController.azimuth) - const sun = new Vector3() - - sun.setFromSphericalCoords(1, phi, theta) - uniforms['sunPosition'].value.copy(sun) - return this - } - - public addPerspectiveCamera = (options: position) => { - this.camera = new PerspectiveCamera() - this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0) - this.scene.add(this.camera) - return this - } - - public addGroundPlane = (options?: position) => { - const checkerboardTexture = this.createCheckerboardTexture(1024, 2) - checkerboardTexture.wrapS = RepeatWrapping - checkerboardTexture.wrapT = RepeatWrapping - checkerboardTexture.repeat.set(100, 100) - const checkerboardMat = new MeshBasicMaterial({ - map: checkerboardTexture, - opacity: 0.1, - transparent: true - }) - - const plane = new PlaneGeometry(400, 400) - - this.ground = new Mesh(plane, checkerboardMat) - this.ground.rotation.x = -Math.PI / 2 - this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0) - this.ground.receiveShadow = true - this.scene.add(this.ground) - - const mirror = new Reflector(plane, { - clipBias: 0.003, - textureWidth: window.innerWidth * window.devicePixelRatio, - textureHeight: window.innerHeight * window.devicePixelRatio, - color: 0x00bfff - }) - mirror.rotateX(-Math.PI / 2) - this.scene.add(mirror) - - return this - } - - public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => { - this.orbit = new OrbitControls(this.camera, this.renderer.domElement) - this.orbit.minDistance = minDistance + (maxDistance - minDistance) / 2 - this.orbit.maxDistance = maxDistance - this.orbit.autoRotate = autoRotate - this.orbit.update() - this.orbit.minDistance = minDistance - return this - } - - public addAmbientLight = (options: light) => { - const ambientLight = new AmbientLight(options.color, options.intensity) - this.scene.add(ambientLight) - return this - } - - public addDirectionalLight = (options: directionalLight) => { - const directionalLight = new DirectionalLight(options.color, options.intensity) - directionalLight.castShadow = true - directionalLight.shadow.camera.top = 10 - directionalLight.shadow.camera.bottom = -10 - directionalLight.shadow.camera.right = 10 - directionalLight.shadow.camera.left = -10 - directionalLight.shadow.mapSize.set(4096, 4096) - - directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0) - this.scene.add(directionalLight) - return this - } - - private createCheckerboardTexture = (size: number, squares: number) => { - const canvas = document.createElement('canvas') - canvas.width = size - canvas.height = size - const context = canvas.getContext('2d') - - const squareSize = size / squares - - for (let y = 0; y < squares; y++) { - for (let x = 0; x < squares; x++) { - context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000' - context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize) - } - } - - 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 + constructor() { + this.scene = new Scene() + if (this.scene.environment?.mapping) { + this.scene.environment.mapping = EquirectangularReflectionMapping } - } + return this + } - if (c === m || !this.isJoint(c)) { - for (let i = 0; i < c.children.length; i++) { - const child = c.children[i] - if (!child.isURDFCollider) { - traverse(c.children[i]) - } + 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 } - traverse(m) - } - public addTransformControls = (model: any) => { - this.transformControl = new TransformControls(this.camera, this.renderer.domElement) - this.transformControl.addEventListener('dragging-changed', (event: any) => { - this.orbit.enabled = !event.value - this.isDragging = !event.value - }) - this.transformControl.attach(model) - this.scene.add(this.transformControl) - this.transformControl.setMode('rotate') - return this - } - - public addModel = (model: any) => { - this.modelGroup = new Group() - this.modelGroup.add(model) - this.model = model - this.scene.add(this.modelGroup) - return this - } - - public addDragControl = (updateAngle: any) => { - const highlightColor = '#FFFFFF' - const highlightMaterial = new MeshPhongMaterial({ - shininess: 10, - color: highlightColor, - emissive: highlightColor, - emissiveIntensity: 0.9 - }) - - const dragControls = new PointerURDFDragControls( - this.scene, - this.camera, - this.renderer.domElement - ) - dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { - this.setJointValue(joint.name, angle) - updateAngle(joint.name, angle) + 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 } - dragControls.onDragStart = () => { - this.orbit.enabled = false - this.isDragging = true + + 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 } - dragControls.onDragEnd = () => { - this.orbit.enabled = true - this.isDragging = false + + 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 } - 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 addAmbientLight = (options: light) => { + const ambientLight = new AmbientLight(options.color, options.intensity) + this.scene.add(ambientLight) + return this + } - public toggleFog = () => { - this.scene.fog = this.scene.fog ? null : this.fog - } + 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) - private handleRobotShadow = () => { - if (this.isLoaded) return - const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10) - setTimeout(() => clearInterval(intervalId), 1000) - this.isLoaded = true - } + 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 + } + } + + if (c === m || !this.isJoint(c)) { + for (let i = 0; i < c.children.length; i++) { + const child = c.children[i] + if (!child.isURDFCollider) { + traverse(c.children[i]) + } + } + } + } + traverse(m) + } + + public addTransformControls = (model: any) => { + this.transformControl = new TransformControls(this.camera, this.renderer.domElement) + this.transformControl.addEventListener('dragging-changed', (event: any) => { + this.orbit.enabled = !event.value + this.isDragging = !event.value + }) + this.transformControl.attach(model) + this.scene.add(this.transformControl) + this.transformControl.setMode('rotate') + return this + } + + public addModel = (model: any) => { + this.modelGroup = new Group() + this.modelGroup.add(model) + this.model = model + this.scene.add(this.modelGroup) + return this + } + + public addDragControl = (updateAngle: any) => { + const highlightColor = '#FFFFFF' + const highlightMaterial = new MeshPhongMaterial({ + shininess: 10, + color: highlightColor, + emissive: highlightColor, + emissiveIntensity: 0.9 + }) + + const dragControls = new PointerURDFDragControls( + this.scene, + this.camera, + this.renderer.domElement + ) + dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { + this.setJointValue(joint.name, angle) + updateAngle(joint.name, angle) + } + dragControls.onDragStart = () => { + this.orbit.enabled = false + this.isDragging = true + } + dragControls.onDragEnd = () => { + this.orbit.enabled = true + this.isDragging = false + } + dragControls.onHover = (joint: URDFMimicJoint) => + this.highlightLinkGeometry(joint, false, highlightMaterial) + dragControls.onUnhover = (joint: URDFMimicJoint) => + this.highlightLinkGeometry(joint, true, highlightMaterial) + + this.renderer.domElement.addEventListener( + 'touchstart', + data => dragControls._mouseDown(data.touches[0]), + { passive: true } + ) + this.renderer.domElement.addEventListener( + 'touchmove', + data => dragControls._mouseMove(data.touches[0]), + { passive: true } + ) + this.renderer.domElement.addEventListener( + 'touchend', + data => dragControls._mouseUp(data.touches[0]), + { passive: true } + ) + return this + } + + public toggleFog = () => { + this.scene.fog = this.scene.fog ? null : this.fog + } + + private handleRobotShadow = () => { + if (this.isLoaded) return + const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10) + setTimeout(() => clearInterval(intervalId), 1000) + this.isLoaded = true + } } diff --git a/app/src/lib/services/file-service.ts b/app/src/lib/services/file-service.ts index 70f27b5..ef9d668 100644 --- a/app/src/lib/services/file-service.ts +++ b/app/src/lib/services/file-service.ts @@ -1,54 +1,53 @@ -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> | null = browser - ? this.openDatabase() - : null; + private dbPromise: Promise> | null = + browser ? this.openDatabase() : null - private async openDatabase(): Promise> { - return new Promise((resolve) => { - const request = indexedDB.open('fileStorageDB', 1); + private async openDatabase(): Promise> { + 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> { - 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> { + 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> { - const storeResult = await this.getStore('readwrite'); - if (storeResult.isErr()) return Result.err('Failed to access store'); + public async saveFile(key: string, file: Uint8Array): Promise> { + 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> { - const storeResult = await this.getStore('readonly'); - if (storeResult.isErr()) return Result.err('Failed to access store'); + public async getFile(key: string): Promise> { + 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; \ No newline at end of file +export default browser ? new FileService() : null diff --git a/app/src/lib/services/index.ts b/app/src/lib/services/index.ts index 7a112d0..4a56b34 100644 --- a/app/src/lib/services/index.ts +++ b/app/src/lib/services/index.ts @@ -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' diff --git a/app/src/lib/services/result-service.ts b/app/src/lib/services/result-service.ts index ceb4900..c92b3bb 100644 --- a/app/src/lib/services/result-service.ts +++ b/app/src/lib/services/result-service.ts @@ -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, 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, 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() diff --git a/app/src/lib/stores/analytics.ts b/app/src/lib/stores/analytics.ts index 14c8299..01f44b6 100644 --- a/app/src/lib/stores/analytics.ts +++ b/app/src/lib/stores/analytics.ts @@ -1,55 +1,69 @@ -import { type Analytics } from '$lib/types/models'; -import { writable } from 'svelte/store'; +import { type Analytics } from '$lib/types/models' +import { writable } from 'svelte/store' let analytics_data = { - uptime: [], - free_heap: [], - total_heap: [], - used_heap: [], - min_free_heap: [], - max_alloc_heap: [], - fs_used: [], - fs_total: [], - core_temp: [], - cpu0_usage: [], - cpu1_usage: [], - cpu_usage: [] -}; - -const maxAnalyticsData = 100; - -function createAnalytics() { - 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) - })); - } - }; + uptime: [], + free_heap: [], + total_heap: [], + used_heap: [], + min_free_heap: [], + max_alloc_heap: [], + fs_used: [], + fs_total: [], + core_temp: [], + cpu0_usage: [], + cpu1_usage: [], + cpu_usage: [] } -export const analytics = createAnalytics(); +const maxAnalyticsData = 100 + +function createAnalytics() { + 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) + })) + } + } +} + +export const analytics = createAnalytics() diff --git a/app/src/lib/stores/application.ts b/app/src/lib/stores/application.ts index 4fa96c4..1975f79 100644 --- a/app/src/lib/stores/application.ts +++ b/app/src/lib/stores/application.ts @@ -1,67 +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 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; + id: string | number + component: keyof typeof WidgetComponents + props?: Record } export interface WidgetContainerConfig { - id: string | number; - layout?: 'row' | 'column' | 'wrap'; - header?: string; - widgets: Array; + id: string | number + layout?: 'row' | 'column' | 'wrap' + header?: string + widgets: Array } 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 -}; + Visualization, + Stream, + ChartWidget +} interface View { - name: string; - content: WidgetContainerConfig; + name: string + content: WidgetContainerConfig } const defaultViews: View[] = [ - { - name: 'Stream', - content: { - id: 'root', - layout: 'column', - widgets: [{ id: 2, component: 'Stream' }] - } - }, - { - name: '3D representation', - content: { - id: 'root', - layout: 'column', - widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }] - } - }, - { - name: 'Split screen', - content: { - id: 'root', - widgets: [ - { id: 2, component: 'Stream' }, - { id: 2, component: 'Visualization', props: { debug: true } } - ] - } - } -]; + { + 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 = persistentStore('views', defaultViews); +export const views: Writable = persistentStore('views', defaultViews) -export const selectedView = persistentStore('selected_view', get(views)[0].name); +export const selectedView = persistentStore('selected_view', get(views)[0].name) diff --git a/app/src/lib/stores/featureFlags.ts b/app/src/lib/stores/featureFlags.ts index f2177ed..6f978d6 100644 --- a/app/src/lib/stores/featureFlags.ts +++ b/app/src/lib/stores/featureFlags.ts @@ -8,55 +8,55 @@ import { base } from '$app/paths' let featureFlagsStore: Writable> export function useFeatureFlags() { - if (!featureFlagsStore) { - featureFlagsStore = persistentStore>('FeatureFlags', {}) + if (!featureFlagsStore) { + featureFlagsStore = persistentStore>('FeatureFlags', {}) - api.get>('/api/features').then(result => { - if (result.isOk()) featureFlagsStore.set(result.inner) - else { - notifications.error('Feature flag could not be fetched', 2500) - } - }) - } + api.get>('/api/features').then(result => { + if (result.isOk()) featureFlagsStore.set(result.inner) + else { + notifications.error('Feature flag could not be fetched', 2500) + } + }) + } - return featureFlagsStore + return featureFlagsStore } export const variants = { - SPOTMICRO_ESP32: { - model: `${base}/spot_micro.urdf.xacro`, - stl: `${base}/stl.zip`, - kinematics: { - coxa: 60.5 / 100, - coxa_offset: 10 / 100, - femur: 111.7 / 100, - tibia: 118.5 / 100, - L: 207.5 / 100, - W: 78 / 100 + SPOTMICRO_ESP32: { + model: `${base}/spot_micro.urdf.xacro`, + stl: `${base}/stl.zip`, + kinematics: { + coxa: 60.5 / 100, + coxa_offset: 10 / 100, + femur: 111.7 / 100, + tibia: 118.5 / 100, + L: 207.5 / 100, + W: 78 / 100 + } + }, + SPOTMICRO_YERTLE: { + model: `${base}/yertle.URDF`, + stl: `${base}/URDF.zip`, + kinematics: { + coxa: 35 / 100, + coxa_offset: 0 / 100, + femur: 130 / 100, + tibia: 130 / 100, + L: 240 / 100, + W: 78 / 100 + } } - }, - SPOTMICRO_YERTLE: { - model: `${base}/yertle.URDF`, - stl: `${base}/URDF.zip`, - kinematics: { - coxa: 35 / 100, - coxa_offset: 0 / 100, - femur: 130 / 100, - tibia: 130 / 100, - L: 240 / 100, - W: 78 / 100 - } - } } export const currentVariant = derived(useFeatureFlags(), $flagStore => { - const variantFlag = $flagStore['variant'] as string - return variantFlag && variants[variantFlag as keyof typeof variants] ? - variants[variantFlag as keyof typeof variants] - : variants.SPOTMICRO_ESP32 + 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) + currentVariant, + $variant => new Kinematic($variant.kinematics) ) diff --git a/app/src/lib/stores/fullscreen.ts b/app/src/lib/stores/fullscreen.ts index 4307250..1cc8098 100644 --- a/app/src/lib/stores/fullscreen.ts +++ b/app/src/lib/stores/fullscreen.ts @@ -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) + } } diff --git a/app/src/lib/stores/gamepad.ts b/app/src/lib/stores/gamepad.ts index bc311d2..5b82f7f 100644 --- a/app/src/lib/stores/gamepad.ts +++ b/app/src/lib/stores/gamepad.ts @@ -1,40 +1,40 @@ import { readable, derived } from 'svelte/store' export type GamepadState = { - available: boolean - gamepads: Gamepad[] + available: boolean + gamepads: Gamepad[] } export const gamepads = readable({ available: false, gamepads: [] }, set => { - const update = () => { - const hasGamepadAPI = 'getGamepads' in navigator - if (!hasGamepadAPI) { - set({ available: false, gamepads: [] }) - return + const update = () => { + const hasGamepadAPI = 'getGamepads' in navigator + if (!hasGamepadAPI) { + set({ available: false, gamepads: [] }) + return + } + + const gps = navigator.getGamepads?.() ?? [] + const validGamepads = gps.filter(Boolean) as Gamepad[] + set({ + available: true, + gamepads: validGamepads + }) + raf = requestAnimationFrame(update) } - const gps = navigator.getGamepads?.() ?? [] - const validGamepads = gps.filter(Boolean) as Gamepad[] - set({ - available: true, - gamepads: validGamepads - }) - raf = requestAnimationFrame(update) - } + window.addEventListener('gamepadconnected', update) + window.addEventListener('gamepaddisconnected', update) + let raf = requestAnimationFrame(update) - window.addEventListener('gamepadconnected', update) - window.addEventListener('gamepaddisconnected', update) - let raf = requestAnimationFrame(update) - - return () => { - cancelAnimationFrame(raf) - window.removeEventListener('gamepadconnected', update) - window.removeEventListener('gamepaddisconnected', update) - } + return () => { + cancelAnimationFrame(raf) + window.removeEventListener('gamepadconnected', update) + window.removeEventListener('gamepaddisconnected', update) + } }) export const gamepad = derived(gamepads, $gamepads => - $gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null + $gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null ) export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0]) @@ -42,6 +42,6 @@ export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? []) export const hasGamepad = derived( - gamepads, - $gamepads => $gamepads.available && $gamepads.gamepads.length > 0 + gamepads, + $gamepads => $gamepads.available && $gamepads.gamepads.length > 0 ) diff --git a/app/src/lib/stores/imu.ts b/app/src/lib/stores/imu.ts index 55fcc1c..12ee5e1 100644 --- a/app/src/lib/stores/imu.ts +++ b/app/src/lib/stores/imu.ts @@ -1,7 +1,7 @@ -import { writable } from 'svelte/store'; -import type { IMU } 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,16 +12,16 @@ export const imu = (() => { altitude: [] as number[], pressure: [] as number[], bmp_temp: [] as number[] - }); + }) const addData = (content: IMU) => { update(data => { - (Object.keys(content) as (keyof IMU)[]).forEach(key => { - data[key] = [...data[key], content[key]].slice(-maxIMUData); - }); - return data; - }); - }; + ;(Object.keys(content) as (keyof IMU)[]).forEach(key => { + data[key] = [...data[key], content[key]].slice(-maxIMUData) + }) + return data + }) + } - return { subscribe, addData }; -})(); + return { subscribe, addData } +})() diff --git a/app/src/lib/stores/index.ts b/app/src/lib/stores/index.ts index ba46074..8ab16e5 100644 --- a/app/src/lib/stores/index.ts +++ b/app/src/lib/stores/index.ts @@ -1,9 +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 './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' diff --git a/app/src/lib/stores/location-store.ts b/app/src/lib/stores/location-store.ts index ad1060e..d0d044f 100644 --- a/app/src/lib/stores/location-store.ts +++ b/app/src/lib/stores/location-store.ts @@ -1,5 +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 { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public' -export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', ''); +export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '') diff --git a/app/src/lib/stores/logging-store.ts b/app/src/lib/stores/logging-store.ts index ef01666..bc29c74 100644 --- a/app/src/lib/stores/logging-store.ts +++ b/app/src/lib/stores/logging-store.ts @@ -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 = writable(); +export const latestErrorLog: Writable = writable() -export const errorLogs: Writable = writable([]); +export const errorLogs: Writable = writable([]) diff --git a/app/src/lib/stores/model-store.ts b/app/src/lib/stores/model-store.ts index c1ac068..c8f3cd7 100644 --- a/app/src/lib/stores/model-store.ts +++ b/app/src/lib/stores/model-store.ts @@ -13,28 +13,28 @@ export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'wa export type Modes = (typeof modes)[number] export enum ModesEnum { - Deactivated = 0, - Idle = 1, - Calibration = 2, - Rest = 3, - Stand = 4, - Walk = 5 + Deactivated = 0, + Idle = 1, + Calibration = 2, + Rest = 3, + Stand = 4, + Walk = 5 } export enum WalkGaits { - Trot = 0, - Crawl = 1 + Trot = 0, + Crawl = 1 } export const walkGaits = ['trot', 'crawl'] as const export const walkGaitLabels: Record = { - [WalkGaits.Trot]: 'Trot', - [WalkGaits.Crawl]: 'Crawl' + [WalkGaits.Trot]: 'Trot', + [WalkGaits.Crawl]: 'Crawl' } export const walkGaitToMode = (gait: WalkGaits): 'trot' | 'crawl' => { - return gait === WalkGaits.Trot ? 'trot' : 'crawl' + return gait === WalkGaits.Trot ? 'trot' : 'crawl' } export const mode: Writable = writable(ModesEnum.Deactivated) @@ -46,9 +46,9 @@ export const outControllerData = writable([0, 0, 0, 0, 0, 1, 0]) export const kinematicData = writable([0, 0, 0, 0, 1, 0]) export const input: Writable = writable({ - left: { x: 0, y: 0 }, - right: { x: 0, y: 0 }, - height: 0.5, - speed: 0.5, - s1: 0.05 + left: { x: 0, y: 0 }, + right: { x: 0, y: 0 }, + height: 0.5, + speed: 0.5, + s1: 0.05 }) diff --git a/app/src/lib/stores/socket-store.ts b/app/src/lib/stores/socket-store.ts index f5faf54..c84a903 100644 --- a/app/src/lib/stores/socket-store.ts +++ b/app/src/lib/stores/socket-store.ts @@ -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 = 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 = 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; - logs: Writable; - mpu: Writable; - distances: Writable; + angles: Writable + logs: Writable + mpu: Writable + distances: Writable } export const socketData = { - angles: servoAngles, - logs, - mpu, - distances -}; + angles: servoAngles, + logs, + mpu, + distances +} diff --git a/app/src/lib/stores/socket.ts b/app/src/lib/stores/socket.ts index 716eea3..bf976d9 100644 --- a/app/src/lib/stores/socket.ts +++ b/app/src/lib/stores/socket.ts @@ -1,160 +1,160 @@ -import { writable } from 'svelte/store'; -import { encode, decode } from '@msgpack/msgpack'; +import { writable } from 'svelte/store' +import { encode, decode } from '@msgpack/msgpack' -const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const; -type SocketEvent = (typeof socketEvents)[number]; +const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const +type SocketEvent = (typeof socketEvents)[number] -type SocketMessage = [number, string?, unknown?]; +type SocketMessage = [number, string?, unknown?] -let useBinary = false; +let useBinary = false const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => { - useBinary = data instanceof ArrayBuffer; + useBinary = data instanceof ArrayBuffer - try { - if (useBinary) { - return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage; - } - return JSON.parse(data as string); - } catch (error) { - console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`); - } - return null; -}; - -const encodeMessage = (data: unknown) => { - try { - return useBinary ? encode(data) : JSON.stringify(data); - } catch (error) { - console.error(`Could not encode data: ${data} - ${error}`); - } -}; - -function createWebSocket() { - const listeners = new Map void>>(); - const { subscribe, set } = writable(false); - const reconnectTimeoutTime = 5000; - let unresponsiveTimeoutId: ReturnType; - let reconnectTimeoutId: ReturnType; - let ws: WebSocket; - let socketUrl: string | URL; - - function init(url: string | URL) { - socketUrl = url; - connect(); - } - - function disconnect(reason: SocketEvent, event?: Event) { - ws.close(); - set(false); - clearTimeout(unresponsiveTimeoutId); - clearTimeout(reconnectTimeoutId); - listeners.get(reason)?.forEach(listener => listener(event)); - reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime); - } - - function connect() { - ws = new WebSocket(socketUrl); - ws.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 unsubscribe(event: string, listener?: (data: unknown) => void) { - const eventListeners = listeners.get(event); - if (!eventListeners) return; - - if (!eventListeners.size) { - unsubscribeToEvent(event); - } - if (listener) { - eventListeners?.delete(listener); - } else { - listeners.delete(event); - } - } - - function resetUnresponsiveCheck() { - clearTimeout(unresponsiveTimeoutId); - unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime); - } - - function sendEvent(event: string, data: unknown) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - send([2, event, data]); - } - - function unsubscribeToEvent(event: string) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - send([1, event]); - } - - function subscribeToEvent(event: string) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - send([0, event]); - } - - function send(data: unknown) { - if (!ws || ws.readyState !== WebSocket.OPEN) return; - const serialized = encodeMessage(data); - if (!serialized) { - console.error('Could not serialize data:', data); - return; - } - ws.send(serialized); - } - - function ping() { - const serialized = encodeMessage([4]); - if (!serialized) { - console.error('Could not serialize message'); - return; - } - ws.send(serialized); - } - - return { - subscribe, - sendEvent, - init, - on: (event: string, listener: (data: T) => void): (() => void) => { - let eventListeners = listeners.get(event); - if (!eventListeners) { - if (!socketEvents.includes(event as SocketEvent)) { - subscribeToEvent(event); + try { + if (useBinary) { + return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage } - eventListeners = new Set(); - listeners.set(event, eventListeners); - } - eventListeners.add(listener as (data: unknown) => void); - - return () => { - unsubscribe(event, listener as (data: unknown) => void); - }; - }, - off: (event: string, listener?: (data: T) => void) => { - unsubscribe(event, listener as (data: unknown) => void); - }, - }; + return JSON.parse(data as string) + } catch (error) { + console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`) + } + return null } -export const socket = createWebSocket(); +const encodeMessage = (data: unknown) => { + try { + return useBinary ? encode(data) : JSON.stringify(data) + } catch (error) { + console.error(`Could not encode data: ${data} - ${error}`) + } +} + +function createWebSocket() { + const listeners = new Map void>>() + const { subscribe, set } = writable(false) + const reconnectTimeoutTime = 5000 + let unresponsiveTimeoutId: ReturnType + let reconnectTimeoutId: ReturnType + let ws: WebSocket + let socketUrl: string | URL + + function init(url: string | URL) { + socketUrl = url + connect() + } + + function disconnect(reason: SocketEvent, event?: Event) { + ws.close() + set(false) + clearTimeout(unresponsiveTimeoutId) + clearTimeout(reconnectTimeoutId) + listeners.get(reason)?.forEach(listener => listener(event)) + reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime) + } + + function connect() { + ws = new WebSocket(socketUrl) + ws.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 unsubscribe(event: string, listener?: (data: unknown) => void) { + const eventListeners = listeners.get(event) + if (!eventListeners) return + + if (!eventListeners.size) { + unsubscribeToEvent(event) + } + if (listener) { + eventListeners?.delete(listener) + } else { + listeners.delete(event) + } + } + + function resetUnresponsiveCheck() { + clearTimeout(unresponsiveTimeoutId) + unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime) + } + + function sendEvent(event: string, data: unknown) { + if (!ws || ws.readyState !== WebSocket.OPEN) return + send([2, event, data]) + } + + function unsubscribeToEvent(event: string) { + if (!ws || ws.readyState !== WebSocket.OPEN) return + send([1, event]) + } + + function subscribeToEvent(event: string) { + if (!ws || ws.readyState !== WebSocket.OPEN) return + send([0, event]) + } + + function send(data: unknown) { + if (!ws || ws.readyState !== WebSocket.OPEN) return + const serialized = encodeMessage(data) + if (!serialized) { + console.error('Could not serialize data:', data) + return + } + ws.send(serialized) + } + + function ping() { + const serialized = encodeMessage([4]) + if (!serialized) { + console.error('Could not serialize message') + return + } + ws.send(serialized) + } + + return { + subscribe, + sendEvent, + init, + on: (event: string, listener: (data: T) => void): (() => void) => { + let eventListeners = listeners.get(event) + if (!eventListeners) { + if (!socketEvents.includes(event as SocketEvent)) { + subscribeToEvent(event) + } + eventListeners = new Set() + listeners.set(event, eventListeners) + } + eventListeners.add(listener as (data: unknown) => void) + + return () => { + unsubscribe(event, listener as (data: unknown) => void) + } + }, + off: (event: string, listener?: (data: T) => void) => { + unsubscribe(event, listener as (data: unknown) => void) + } + } +} + +export const socket = createWebSocket() diff --git a/app/src/lib/stores/telemetry.ts b/app/src/lib/stores/telemetry.ts index 4dfeb1b..c197506 100644 --- a/app/src/lib/stores/telemetry.ts +++ b/app/src/lib/stores/telemetry.ts @@ -1,5 +1,5 @@ -import type { DownloadOTA } from '$lib/types/models'; -import { writable } from 'svelte/store'; +import type { DownloadOTA } from '$lib/types/models' +import { writable } from 'svelte/store' let telemetry_data = { rssi: { @@ -10,10 +10,10 @@ let telemetry_data = { progress: 0, error: '' } -}; +} function createTelemetry() { - const { subscribe, set, 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() diff --git a/app/src/lib/types/mathutils.d.ts b/app/src/lib/types/mathutils.d.ts index ac1fb63..01a3889 100644 --- a/app/src/lib/types/mathutils.d.ts +++ b/app/src/lib/types/mathutils.d.ts @@ -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 } diff --git a/app/src/lib/types/models.ts b/app/src/lib/types/models.ts index cfc42a1..616b222 100644 --- a/app/src/lib/types/models.ts +++ b/app/src/lib/types/models.ts @@ -1,239 +1,239 @@ export enum MessageTopic { - imu = 'imu', - mode = 'mode', - input = 'input', - analytics = 'analytics', - position = 'position', - angles = 'angles', - i2cScan = 'i2cScan', - peripheralSettings = 'peripheralSettings', - otastatus = 'otastatus', - gait = 'walk_gait', - servoState = 'servoState', - servoPWM = 'servoPWM', - WiFiSettings = 'WiFiSettings', - sonar = 'sonar', - rssi = 'rssi' + imu = 'imu', + mode = 'mode', + input = 'input', + analytics = 'analytics', + position = 'position', + angles = 'angles', + i2cScan = 'i2cScan', + peripheralSettings = 'peripheralSettings', + otastatus = 'otastatus', + gait = 'walk_gait', + servoState = 'servoState', + servoPWM = 'servoPWM', + WiFiSettings = 'WiFiSettings', + sonar = 'sonar', + rssi = 'rssi' } export type vector = { x: number; y: number } export interface ControllerInput { - left: vector - right: vector - height: number - speed: number - s1: number + left: vector + right: vector + height: number + speed: number + s1: number } export type GithubRelease = { - message: string - tag_name: string - assets: Array<{ - name: string - browser_download_url: string - }> + message: string + tag_name: string + assets: Array<{ + name: string + browser_download_url: string + }> } export type angles = number[] | Int16Array export type 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 IMU = { - x: number - y: number - z: number - heading: number - altitude: number - bmp_temp: number - pressure: number + x: number + y: number + z: number + heading: number + altitude: number + bmp_temp: number + pressure: number } export interface I2CDevice { - address: number - part_number: string - name: string + address: number + part_number: string + name: string } export type PinConfig = { - pin: number - mode: string - type: string - role: string + pin: number + mode: string + type: string + role: string } export type PeripheralsConfiguration = { - sda: number - scl: number - frequency: number - pins: PinConfig[] + sda: number + scl: number + frequency: number + pins: PinConfig[] } export type CameraSettings = { - framesize: number - quality: number - brightness: number - contrast: number - saturation: number - sharpness: number - denoise: number - special_effect: number - wb_mode: number - vflip: boolean - hmirror: boolean + framesize: number + quality: number + brightness: number + contrast: number + saturation: number + sharpness: number + denoise: number + special_effect: number + wb_mode: number + vflip: boolean + hmirror: boolean } export type File = number export interface Directory { - [key: string]: File | Directory + [key: string]: File | Directory } export type Servo = { - name: string - channel: number - inverted: boolean - angle: number - center_angle: number + name: string + channel: number + inverted: boolean + angle: number + center_angle: number } export type ServoConfiguration = { - is_active: boolean - servo_pwm_frequency: number - servo_oscillator_frequency: number - servos: Servo[] + is_active: boolean + servo_pwm_frequency: number + servo_oscillator_frequency: number + servos: Servo[] } export interface MDNSServiceQuery { - services: MDNSServiceItem[] + services: MDNSServiceItem[] } export interface MDNSServiceItem { - ip: string - port: number - name: string + ip: string + port: number + name: string } export interface MDNSService { - service: string - protocol: string - port: number + service: string + protocol: string + port: number } export interface MDNSTxtRecord { - key: string - value: string + key: string + value: string } export interface MDNSStatus { - started: boolean - hostname: string - instance: string - services: MDNSService[] - global_txt_records: MDNSTxtRecord[] + started: boolean + hostname: string + instance: string + services: MDNSService[] + global_txt_records: MDNSTxtRecord[] } diff --git a/app/src/lib/types/uzip.d.ts b/app/src/lib/types/uzip.d.ts index 05e744c..49b3d72 100644 --- a/app/src/lib/types/uzip.d.ts +++ b/app/src/lib/types/uzip.d.ts @@ -1,14 +1,14 @@ declare module 'uzip' { - interface UZIP { - parse(data: Uint8Array | ArrayBuffer): any; - compress(data: any): Uint8Array | ArrayBuffer; - compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer; - decompress(data: Uint8Array | ArrayBuffer): any; - decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer; - encode(data: any): Uint8Array | ArrayBuffer; - decode(data: Uint8Array | ArrayBuffer): any; - } + 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 } diff --git a/app/src/lib/utilities/buffer-utilities.ts b/app/src/lib/utilities/buffer-utilities.ts index a5f5182..5a62dbe 100644 --- a/app/src/lib/utilities/buffer-utilities.ts +++ b/app/src/lib/utilities/buffer-utilities.ts @@ -1,15 +1,15 @@ export class throttler { - private _throttlePause: boolean; - constructor() { - this._throttlePause = false; - } - throttle = (callback: Function, 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) + } } diff --git a/app/src/lib/utilities/color-utilities.ts b/app/src/lib/utilities/color-utilities.ts index 1500cb8..8acb7ef 100644 --- a/app/src/lib/utilities/color-utilities.ts +++ b/app/src/lib/utilities/color-utilities.ts @@ -1,6 +1,6 @@ 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).trim() + if (opacity >= 100) return color + const alpha = Math.min(Math.max(opacity, 0), 100) / 100 + return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})` +} diff --git a/app/src/lib/utilities/index.ts b/app/src/lib/utilities/index.ts index acbf139..c6724f7 100644 --- a/app/src/lib/utilities/index.ts +++ b/app/src/lib/utilities/index.ts @@ -1,9 +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 './position-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' diff --git a/app/src/lib/utilities/math-utilities.ts b/app/src/lib/utilities/math-utilities.ts index ec84267..2a48f6c 100644 --- a/app/src/lib/utilities/math-utilities.ts +++ b/app/src/lib/utilities/math-utilities.ts @@ -1,18 +1,18 @@ export const toUint8 = (number: number, min: number, max: number) => { - number = Math.max(min, Math.min(max, number)); - let 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)); - let 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; -}; \ No newline at end of file + int8 = Math.max(-128, Math.min(127, int8)) + const scaled = (int8 + 128) / 255 + const number = scaled * (max - min) + min + return number +} diff --git a/app/src/lib/utilities/model-utilities.ts b/app/src/lib/utilities/model-utilities.ts index c339865..771e927 100644 --- a/app/src/lib/utilities/model-utilities.ts +++ b/app/src/lib/utilities/model-utilities.ts @@ -10,84 +10,85 @@ import { get } from 'svelte/store' 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 loadModel(get(currentVariant).model) + if (modelRes.isOk()) { + const [urdf, JOINT_NAME] = modelRes.inner + jointNames.set(JOINT_NAME) + model.set(urdf) + } else { + console.error(modelRes.inner, { exception: modelRes.exception }) + } } export const cacheModelFiles = async () => { - const data = await fetch(get(currentVariant).stl) + const data = await fetch(get(currentVariant).stl) - const files = uzip.parse(await data.arrayBuffer()) + const files = uzip.parse(await data.arrayBuffer()) - for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) { - const url = new URL(path, window.location.href) - fileService?.saveFile(url.toString(), data) - } + for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) { + const url = new URL(path, window.location.href) + fileService?.saveFile(url.toString(), data) + } } export const loadModel = async (url: string): Promise> => { - const urdfLoader = new URDFLoader() - urdfLoader.workingPath = LoaderUtils.extractUrlBase(url) + const urdfLoader = new URDFLoader() + urdfLoader.workingPath = LoaderUtils.extractUrlBase(url) - let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text()) + let xml = + url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text()) - if (typeof xml === 'string') { - xml = new window.DOMParser().parseFromString(xml, 'text/xml') - } - - return new Promise(resolve => { - model_xml = xml - try { - const model = urdfLoader.parse(xml) - setupRobot(model) - const joints = Object.entries(model.joints) - .filter(joint => joint[1].jointType !== 'fixed') - .map(joint => joint[0]) - - resolve(Result.ok([model, joints])) - } catch (error) { - resolve(Result.err('Failed to load model', error)) + if (typeof xml === 'string') { + xml = new window.DOMParser().parseFromString(xml, 'text/xml') } - }) + + return new Promise(resolve => { + model_xml = xml + try { + const model = urdfLoader.parse(xml) + setupRobot(model) + const joints = Object.entries(model.joints) + .filter(joint => joint[1].jointType !== 'fixed') + .map(joint => joint[0]) + + resolve(Result.ok([model, joints])) + } catch (error) { + resolve(Result.err('Failed to load model', error)) + } + }) } const loadXacro = async (url: string): Promise => - new Promise((resolve, reject) => { - new XacroLoader().load(url, resolve, reject) - }) + 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) + 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 + 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(', ') + 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})`) } diff --git a/app/src/lib/utilities/position-utilities.ts b/app/src/lib/utilities/position-utilities.ts index 00db4a4..8af146d 100644 --- a/app/src/lib/utilities/position-utilities.ts +++ b/app/src/lib/utilities/position-utilities.ts @@ -1,84 +1,86 @@ 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); + 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)) - ); + 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); - } + 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 - ); - } + 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))); - } + 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; - } + 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) - ) - ); - } + 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); - } + degToRad(deg: number) { + return deg * (Math.PI / 180) + } - radToDeg(rad: number) { - return rad * (180 / Math.PI); - } + radToDeg(rad: number) { + return rad * (180 / Math.PI) + } } -export const sunCalculator = new SunCalculator(); +export const sunCalculator = new SunCalculator() diff --git a/app/src/lib/utilities/result/err.ts b/app/src/lib/utilities/result/err.ts index 8c879be..3e28c8d 100644 --- a/app/src/lib/utilities/result/err.ts +++ b/app/src/lib/utilities/result/err.ts @@ -1,42 +1,42 @@ export class Err { - #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 { - return true; - } + /** + * Type guard for `Err` + * @returns `true` if `Err`; `false` if `Ok` + */ + isErr(): this is Err { + return true + } - /** - * Create an `Err` - * @param inner - * @returns `Err(inner)` - */ - static new(inner: E, exception: F): Err { - return new Err(inner, exception); - } + /** + * Create an `Err` + * @param inner + * @returns `Err(inner)` + */ + static new(inner: E, exception: F): Err { + return new Err(inner, exception) + } } diff --git a/app/src/lib/utilities/result/index.ts b/app/src/lib/utilities/result/index.ts index 55e069b..5130b66 100644 --- a/app/src/lib/utilities/result/index.ts +++ b/app/src/lib/utilities/result/index.ts @@ -1,3 +1,3 @@ -export * from './err'; -export * from './ok'; -export * from './result'; +export * from './err' +export * from './ok' +export * from './result' diff --git a/app/src/lib/utilities/result/ok.ts b/app/src/lib/utilities/result/ok.ts index 868b3c8..d2f83c4 100644 --- a/app/src/lib/utilities/result/ok.ts +++ b/app/src/lib/utilities/result/ok.ts @@ -1,44 +1,44 @@ export class Ok { - #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 { - return true; - } + /** + * Type guard for `Ok` + * @returns `true` if `Ok`; `false` if `Err` + */ + isOk(): this is Ok { + 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(inner: T): Ok { - return new Ok(inner); - } + /** + * Create an `Ok` + * @param inner + * @returns `Ok(inner)` + */ + static new(inner: T): Ok { + return new Ok(inner) + } - /** - * Create an empty `Ok` - * @returns `Ok(void)` - */ - static void(): Ok { - return new Ok(undefined); - } + /** + * Create an empty `Ok` + * @returns `Ok(void)` + */ + static void(): Ok { + return new Ok(undefined) + } } diff --git a/app/src/lib/utilities/result/result.ts b/app/src/lib/utilities/result/result.ts index 4e86e00..796267c 100644 --- a/app/src/lib/utilities/result/result.ts +++ b/app/src/lib/utilities/result/result.ts @@ -1,20 +1,20 @@ -import { Err } from './err'; -import { Ok } from './ok'; +import { Err } from './err' +import { Ok } from './ok' -export type Result = Ok | Err; +export type Result = Ok | Err export namespace Result { - /** - * @returns `Ok` - */ - export function ok(value: T) { - return Ok.new(value); - } + /** + * @returns `Ok` + */ + export function ok(value: T) { + return Ok.new(value) + } - /** - * @returns `Err` - */ - export function err(error: E, exception?: F) { - return Err.new(error, exception); - } + /** + * @returns `Err` + */ + export function err(error: E, exception?: F) { + return Err.new(error, exception) + } } diff --git a/app/src/lib/utilities/string-utilities.ts b/app/src/lib/utilities/string-utilities.ts index e4b6c80..429caa2 100644 --- a/app/src/lib/utilities/string-utilities.ts +++ b/app/src/lib/utilities/string-utilities.ts @@ -1,47 +1,47 @@ 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'] + const i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024)) + return Number((size / Math.pow(1024, i)).toFixed(2)) * 1 + units[i] } export const capitalize = (str: string): string => { - return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() + return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase() } export const convertSeconds = (seconds: number) => { - // Calculate the number of seconds, minutes, hours, and days - let minutes = Math.floor(seconds / 60) - let hours = Math.floor(minutes / 60) - 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) + const days = Math.floor(hours / 24) - // Calculate the remaining hours, minutes, and seconds - hours = hours % 24 - minutes = minutes % 60 - seconds = seconds % 60 + // Calculate the remaining hours, minutes, and seconds + hours = hours % 24 + minutes = minutes % 60 + seconds = seconds % 60 - // Create the formatted string - let result = '' - if (days > 0) { - result += days + ' day' + (days > 1 ? 's' : '') + ' ' - } - if (hours > 0) { - result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ' - } - if (minutes > 0) { - result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ' - } - result += seconds + ' second' + (seconds > 1 ? 's' : '') + // Create the formatted string + let result = '' + if (days > 0) { + result += days + ' day' + (days > 1 ? 's' : '') + ' ' + } + if (hours > 0) { + result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ' + } + if (minutes > 0) { + result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ' + } + result += seconds + ' second' + (seconds > 1 ? 's' : '') - return result + return result } export const compareIp = (ip1: string, ip2: string) => { - const ip1Parts = ip1.split('.').map(Number) - const ip2Parts = ip2.split('.').map(Number) - for (let i = 0; i < 4; i++) { - if (ip1Parts[i] !== ip2Parts[i]) { - return ip1Parts[i] > ip2Parts[i] ? 1 : -1 + 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 0 } diff --git a/app/src/lib/utilities/svelte-utilities.ts b/app/src/lib/utilities/svelte-utilities.ts index ba78e77..57b6ba3 100644 --- a/app/src/lib/utilities/svelte-utilities.ts +++ b/app/src/lib/utilities/svelte-utilities.ts @@ -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 = (key: string, initialValue: T) => { - const savedValue = browser ? localStorage.getItem(key) : null; - const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue; - const store = writable(); + const savedValue = browser ? localStorage.getItem(key) : null + const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue + const store = writable() - 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 +} diff --git a/app/src/routes/+error.svelte b/app/src/routes/+error.svelte index c849218..062a73c 100644 --- a/app/src/routes/+error.svelte +++ b/app/src/routes/+error.svelte @@ -1,8 +1,8 @@
-

{page.status} {page.error?.message}

- Go to Home page +

{page.status} {page.error?.message}

+ Go to Home page
diff --git a/app/src/routes/+layout.svelte b/app/src/routes/+layout.svelte index d0f6a79..838efb5 100644 --- a/app/src/routes/+layout.svelte +++ b/app/src/routes/+layout.svelte @@ -1,129 +1,129 @@ - {page.data.title} + {page.data.title}
- -
- - + +
+ + - - {@render children?.()} -
- -
- - (menuOpen = false)} /> -
+ + {@render children?.()} +
+ +
+ + (menuOpen = false)} /> +
- - - {#snippet backdrop()} -
-
- {/snippet} + + + {#snippet backdrop()} +
+ {/snippet}
diff --git a/app/src/routes/+layout.ts b/app/src/routes/+layout.ts index 9f306b8..84f7629 100644 --- a/app/src/routes/+layout.ts +++ b/app/src/routes/+layout.ts @@ -2,21 +2,23 @@ export const prerender = true export const ssr = false const registerFetchIntercept = async () => { - const { fetch: originalFetch } = window - const fileService = (await import('$lib/services/file-service')).default - window.fetch = async (resource, config) => { - const url = resource instanceof Request ? resource.url : resource.toString() - const file = await fileService?.getFile(url) - return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config) - } + const { fetch: originalFetch } = window + const fileService = (await import('$lib/services/file-service')).default + window.fetch = async (resource, config) => { + const url = resource instanceof Request ? resource.url : resource.toString() + const file = await fileService?.getFile(url) + return file?.isOk() && file.inner ? + new Response(new Uint8Array(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: '2025 Rune Harlyk' + } } diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index f06e9c9..0533f9d 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -1,27 +1,29 @@
-
-
- +
+
+ +
+
+

Begin you journey

+

+ + Add Robot Dog + +
-
-

Begin you journey

-

- Add Robot Dog -
-
diff --git a/app/src/routes/connection/+page.svelte b/app/src/routes/connection/+page.svelte index 71cb700..1d04582 100644 --- a/app/src/routes/connection/+page.svelte +++ b/app/src/routes/connection/+page.svelte @@ -1,7 +1,7 @@
- +
diff --git a/app/src/routes/connection/+page.ts b/app/src/routes/connection/+page.ts index edd2443..4702682 100644 --- a/app/src/routes/connection/+page.ts +++ b/app/src/routes/connection/+page.ts @@ -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 diff --git a/app/src/routes/connection/Connection.svelte b/app/src/routes/connection/Connection.svelte index fb94f88..0c91f88 100644 --- a/app/src/routes/connection/Connection.svelte +++ b/app/src/routes/connection/Connection.svelte @@ -1,26 +1,26 @@ - {#snippet icon()} - - {/snippet} - {#snippet title()} - Connection - {/snippet} + {#snippet icon()} + + {/snippet} + {#snippet title()} + Connection + {/snippet} -
- - -
+
+ + +
- +
diff --git a/app/src/routes/controller/+page.svelte b/app/src/routes/controller/+page.svelte index 0c4bca9..cb5f73d 100644 --- a/app/src/routes/controller/+page.svelte +++ b/app/src/routes/controller/+page.svelte @@ -1,31 +1,31 @@
- -
- -
+ +
+ +
diff --git a/app/src/routes/controller/+page.ts b/app/src/routes/controller/+page.ts index f8b32d7..0dc72de 100644 --- a/app/src/routes/controller/+page.ts +++ b/app/src/routes/controller/+page.ts @@ -1,3 +1,3 @@ export const load = async () => { - return { title: 'Controller' }; -}; + return { title: 'Controller' } +} diff --git a/app/src/routes/controller/Controls.svelte b/app/src/routes/controller/Controls.svelte index 0ee77a6..d6c3958 100644 --- a/app/src/routes/controller/Controls.svelte +++ b/app/src/routes/controller/Controls.svelte @@ -1,202 +1,210 @@
-
-
-
- -
-