70 Commits

Author SHA1 Message Date
Rune Harlyk 07fc90eb71 🎨 Expand animation with looping 2025-10-20 20:44:56 +02:00
Rune Harlyk 7bc11bf94b Adds animation state 2025-10-20 20:44:55 +02:00
Rune Harlyk 59f6089335 🫏 Adds animation experiment 2025-10-20 20:40:09 +02:00
Rune Harlyk 64ef3d31eb 🐛 Fix relative path in app 2025-10-20 20:34:33 +02:00
Rune Harlyk b14f005b22 🐛 Fix model loading on github pages 2025-10-20 20:17:57 +02:00
Rune Harlyk 72a288145d 🎨 Set 3D representation as default view 2025-10-20 19:22:23 +02:00
Rune Harlyk af0815b01f 🎨 Reduce stand offset 2025-10-20 19:08:09 +02:00
Rune Harlyk df3e813470 🎨 Improve rotation handling 2025-10-20 19:08:09 +02:00
Rune Harlyk 1b28b8b7fd 🐛 Fix stl relative model path 2025-10-20 19:08:09 +02:00
Rune Harlyk c449cb3390 🎨 Adds rotation keyboard controls 2025-10-20 19:08:09 +02:00
Rune Harlyk 05a420f345 Adds cumulative displacement of the robot 2025-10-20 19:08:09 +02:00
Rune Harlyk df395657e3 🎨 Removes deprecated base 2025-10-20 17:35:39 +02:00
Rune Harlyk 8970457353 🎨 Fix different typing problems 2025-10-14 20:07:12 +02:00
Rune Harlyk 0aab42f0e9 🎮 Maps controller buttons to modes 2025-10-14 19:41:40 +02:00
Rune Harlyk 76d965ff43 🎨 Updates defaults motion smoothing 2025-10-11 20:51:02 +02:00
Rune Harlyk 0b9921e592 🎨 Updates duty and fixes direction angle 2025-10-11 19:16:30 +02:00
Rune Harlyk aee29c47e4 🎨 Improves mode handling 2025-10-11 15:29:22 +02:00
Rune Harlyk f2ee454b89 ⬆️ Upgrade frontend dependencies 2025-10-11 11:02:17 +02:00
Rune Harlyk a77eb0b1e0 🎨 Lint project 2025-10-11 10:54:07 +02:00
Rune Harlyk 91a7b170fe 🎨 format 2025-10-11 10:42:32 +02:00
Rune Harlyk 4d51b9f556 🎨 Adds kinematics config to readme 2025-10-10 22:23:51 +02:00
Rune Harlyk 92a98064c3 🎨 Updates readme 2025-10-10 22:05:27 +02:00
Rune Harlyk 1fbddd483c Adds option to control sim using web app 2025-10-10 22:05:27 +02:00
Rune Harlyk d47ce02cc6 ️ Makes training parallelized 2025-10-10 22:05:27 +02:00
Rune Harlyk 01c4a80c8f 🔥 Clean up gitignore 2025-10-10 22:05:27 +02:00
Rune Harlyk 174d77a9fd Updates training script with stablebaseline 2025-10-10 22:05:27 +02:00
Rune Harlyk a078f28a82 🎨 Use real variables 2025-10-10 22:05:27 +02:00
Rune Harlyk f3f3864b83 🔥 Remove simple play kinematics 2025-10-10 22:05:27 +02:00
Rune Harlyk 46bb5f74b1 🎨 Fixes gait in sim 2025-10-10 22:05:27 +02:00
Rune Harlyk 89a0316fb4 Adds script to play with kinematics 2025-10-10 22:05:27 +02:00
Rune Harlyk 51ee910fb6 🐛 Fixes many smaller simulation pains 2025-10-10 22:05:27 +02:00
Rune Harlyk a198de05c2 Fixes body kin rot 2025-10-10 22:05:27 +02:00
Rune Harlyk d3db2b3650 ♻️ Update sim structure 2025-10-10 22:05:27 +02:00
Rune Harlyk 5a6f195f56 🫐 Updates foot color for urdf 2025-10-10 22:05:27 +02:00
Rune Harlyk 0cae981779 🧁 Simplifies backpart stl 2025-10-10 22:05:27 +02:00
Rune Harlyk c541b3f474 🧼 Removes print 2025-10-10 22:05:27 +02:00
Rune Harlyk ceccb2c901 🪇 Adds git input function to GUI 2025-10-10 22:05:27 +02:00
Rune Harlyk 8c21f3e2e4 🎯 Updates number of solve iterations 2025-10-10 22:05:27 +02:00
Rune Harlyk 55eecdc8d7 🛹 Adds static gui to env 2025-10-10 22:05:27 +02:00
Rune Harlyk b98c0e866b 🍒 Saves the initial state for faster reload 2025-10-10 22:05:27 +02:00
Rune Harlyk 3d294f38c2 🪴 Adds gitignore for python 2025-10-10 22:05:27 +02:00
Rune Harlyk a237dc3995 📏 Tries to rebuild kinematics in python 2025-10-10 22:05:27 +02:00
Rune Harlyk 80c74dc745 🧹 Formats urdf 2025-10-10 22:05:27 +02:00
Rune Harlyk fb9313913d 🤖 Adds plane 2025-10-10 22:05:27 +02:00
Rune Harlyk 33e7fac74c 🤖 Adds initial sim structure 2025-10-10 22:05:27 +02:00
Rune Harlyk 2face72aee 🎨 Clamp servo pwm 2025-10-09 18:33:17 +02:00
Rune Harlyk 1f8e7efdb2 Adds option to rotate gesture sensor 2025-10-09 18:33:04 +02:00
Rune Harlyk b184449e7b 🔥 Clean up arduino libs 2025-10-09 18:31:40 +02:00
Rune Harlyk bc31b1b2dd Replace millis with esp timer 2025-10-09 17:49:36 +02:00
Rune Harlyk 12e1f80830 🐛 Adds missing function definitions in socket adapter 2025-09-18 18:50:04 +02:00
Rune Harlyk 1cadcf8bdb 🎨 Pull subscribe logic out from websocket 2025-09-18 18:50:04 +02:00
Rune Harlyk 06d27e0644 🎨 Renames event socket to websocket adapter 2025-09-18 18:50:04 +02:00
Rune Harlyk 98b519dee8 🐛 Adds servo config over http 2025-09-18 18:50:04 +02:00
Rune Harlyk 4da2d7fa20 🔥 Cleans up build flags 2025-09-18 18:50:04 +02:00
Rune Harlyk 0f992b26e9 🔥 Removes unused feature flags 2025-09-18 18:50:04 +02:00
Rune Harlyk 2a57d1ecc3 🔥 Removes firmware rename script 2025-09-18 18:50:04 +02:00
Rune Harlyk fd3180d08b 🔥 Removes unused libs 2025-09-18 18:50:04 +02:00
Rune Harlyk 43b5216d9f ️ Removes task manager dependency 2025-09-18 18:50:04 +02:00
Rune Harlyk e1e11346b4 🔥 Removes unused functions and constants 2025-09-18 18:50:04 +02:00
Rune Harlyk 3ce8c88a84 🎨 Replace Arduino String with std::string 2025-09-18 18:50:04 +02:00
Rune Harlyk 0285b522f1 🎨 Replaces delay with vTaskDelay 2025-09-18 18:50:04 +02:00
Rune Harlyk 4ea287b162 🐛 Fixes table linking 2025-09-14 19:43:34 +02:00
Rune Harlyk c2d52449b4 🎨 Makes file system service use define var 2025-09-14 19:43:34 +02:00
Rune Harlyk f9a0880cd9 Moves servo event to main 2025-09-14 19:43:34 +02:00
Rune Harlyk 1bb098e952 ⬇️ Downgrades fastled version 2025-09-14 19:43:34 +02:00
Rune Harlyk 9c74c8e87b 🚨 Fixes build error for esp-idf 2025-09-14 19:43:34 +02:00
Rune Harlyk 3f4d956903 Adds partion tables 2025-09-14 19:43:34 +02:00
Rune Harlyk a5371c36b9 ♻️ Moves peripherals to source file, add sensor base 2025-09-14 19:43:34 +02:00
Rune Harlyk 41b863a0eb ♻️ Moves motion implementation to source file 2025-09-14 19:43:34 +02:00
Rune Harlyk 7fd35f3f48 ♻️ Major clean up of project structure 2025-09-14 19:43:34 +02:00
243 changed files with 13386 additions and 10798 deletions
+1 -1
View File
@@ -28,4 +28,4 @@ module.exports = {
} }
} }
] ]
}; }
+1 -2
View File
@@ -1,13 +1,12 @@
{ {
"useTabs": false, "useTabs": false,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "none",
"arrowParens": "avoid", "arrowParens": "avoid",
"experimentalTernaries": true, "experimentalTernaries": true,
"printWidth": 100, "printWidth": 100,
"semi": false, "semi": false,
"svelteBracketNewLine": false,
"plugins": ["prettier-plugin-svelte"], "plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }
+5 -1
View File
@@ -1,3 +1,7 @@
{ {
"recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"] "recommendations": [
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
]
} }
+4 -4
View File
@@ -1,8 +1,8 @@
declare module "app-env" { declare module 'app-env' {
interface ENV { interface ENV {
VITE_USE_HOST_NAME: boolean; VITE_USE_HOST_NAME: boolean
} }
const appEnv: ENV; const appEnv: ENV
export default appEnv; export default appEnv
} }
+37 -37
View File
@@ -16,50 +16,50 @@
"test:unit": "vitest" "test:unit": "vitest"
}, },
"devDependencies": { "devDependencies": {
"@iconify-json/mdi": "^1.1.64", "@iconify-json/mdi": "^1.2.3",
"@iconify-json/tabler": "^1.1.109", "@iconify-json/tabler": "^1.2.23",
"@playwright/test": "^1.49.1", "@playwright/test": "^1.56.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.5.27", "@sveltejs/kit": "^2.46.4",
"@sveltejs/vite-plugin-svelte": "^5.0.3", "@sveltejs/vite-plugin-svelte": "^6.2.1",
"@types/eslint": "^8.56.0", "@types/eslint": "^9.6.1",
"@types/three": "^0.162.0", "@types/three": "^0.180.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^8.46.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^8.46.0",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.21",
"eslint": "^8.56.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-svelte": "^2.45.1", "eslint-plugin-svelte": "^3.12.4",
"jsdom": "^24.0.0", "jsdom": "^27.0.0",
"prettier": "^3.1.1", "prettier": "^3.6.2",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.0.0", "svelte": "^5.39.11",
"svelte-check": "^4.0.0", "svelte-check": "^4.3.3",
"svelte-focus-trap": "^1.2.0", "svelte-focus-trap": "^1.2.0",
"tailwindcss": "^4.0.12", "tailwindcss": "^4.1.14",
"tslib": "^2.6.1", "tslib": "^2.8.1",
"typescript": "^5.5.0", "typescript": "^5.9.3",
"unplugin-icons": "^0.18.5", "unplugin-icons": "^22.4.2",
"vite": "^6.2.1", "vite": "^7.1.9",
"vitest": "^1.2.0" "vitest": "^3.2.4"
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^3.1.2", "@msgpack/msgpack": "^3.1.2",
"@niku/vite-env-caster": "^1.0.2", "@niku/vite-env-caster": "^1.1.2",
"@sveltejs/adapter-auto": "^4.0.0", "@sveltejs/adapter-auto": "^6.1.1",
"@tailwindcss/vite": "^4.0.12", "@tailwindcss/vite": "^4.1.14",
"chart.js": "^4.4.2", "chart.js": "^4.5.0",
"compare-versions": "^6.1.0", "compare-versions": "^6.1.1",
"cross-env": "^7.0.3", "cross-env": "^10.1.0",
"daisyui": "^5.0.0", "daisyui": "^5.2.0",
"nipplejs": "^0.10.1", "nipplejs": "^0.10.2",
"svelte-dnd-list": "^0.1.8", "svelte-dnd-list": "^0.1.8",
"svelte-modals": "^2.0.0", "svelte-modals": "^2.0.1",
"three": "^0.162.0", "three": "^0.180.0",
"urdf-loader": "^0.12.1", "urdf-loader": "^0.12.6",
"uzip": "^0.20201231.0", "uzip": "^0.20201231.0",
"xacro-parser": "^0.3.9" "xacro-parser": "^0.3.10"
}, },
"packageManager": "pnpm@9.3.0" "packageManager": "pnpm@9.3.0"
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import type { PlaywrightTestConfig } from '@playwright/test'; import type { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
webServer: { webServer: {
@@ -7,6 +7,6 @@ const config: PlaywrightTestConfig = {
}, },
testDir: 'tests/integration', testDir: 'tests/integration',
testMatch: /(.+\.)?(test|spec)\.[jt]s/ testMatch: /(.+\.)?(test|spec)\.[jt]s/
}; }
export default config; export default config
+1878 -2164
View File
File diff suppressed because it is too large Load Diff
+8
View File
@@ -23,6 +23,14 @@
--base-content: oklch(0.3 0.012 256); --base-content: oklch(0.3 0.012 256);
} }
button {
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
}
#nipple_0_0, #nipple_0_0,
#nipple_1_1 { #nipple_1_1 {
z-index: 10 !important; z-index: 10 !important;
+1 -1
View File
@@ -10,4 +10,4 @@ declare global {
} }
} }
export {}; export {}
+4 -1
View File
@@ -3,7 +3,10 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo512.png" /> <link rel="icon" href="%sveltekit.assets%/logo512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" /> <meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
%sveltekit.head% %sveltekit.head%
-7
View File
@@ -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);
});
});
+33 -34
View File
@@ -1,22 +1,22 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store'
import { Err, Ok, type Result } from './utilities'; import { Err, Ok, type Result } from './utilities'
import { location } from './stores'; import { apiLocation } from './stores'
export namespace api { export const api = {
export function get<TResponse>(endpoint: string, params?: RequestInit) { get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params); return sendRequest<TResponse>(endpoint, 'GET', null, params)
} },
export function post<TResponse>(endpoint: string, data?: unknown) { post<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'POST', data); return sendRequest<TResponse>(endpoint, 'POST', data)
} },
export function put<TResponse>(endpoint: string, data?: unknown) { put<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'PUT', data); return sendRequest<TResponse>(endpoint, 'PUT', data)
} },
export function remove<TResponse>(endpoint: string) { remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE'); return sendRequest<TResponse>(endpoint, 'DELETE')
} }
} }
@@ -26,8 +26,8 @@ async function sendRequest<TResponse>(
data?: unknown, data?: unknown,
params?: RequestInit params?: RequestInit
): Promise<Result<TResponse, Error>> { ): Promise<Result<TResponse, Error>> {
endpoint = resolveUrl(endpoint); endpoint = resolveUrl(endpoint)
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined; const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined
const request = { const request = {
...params, ...params,
@@ -38,43 +38,42 @@ async function sendRequest<TResponse>(
Authorization: 'Basic', Authorization: 'Basic',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}; }
let response; let response
try { try {
response = await fetch(endpoint, request); response = await fetch(endpoint, request)
} catch (error) { } catch {
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 (!isResponseOk) {
if (response.status === 401) { 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 = const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type')
response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) { if (contentType && contentType.includes('application/json')) {
const data = await response.json(); const data = await response.json()
return Ok.new(data as TResponse); return Ok.new(data as TResponse)
} else { } else {
// Handle empty object as response // Handle empty object as response
return Ok.new(null as TResponse); return Ok.new(null as TResponse)
} }
} }
function resolveUrl(url: string): string { function resolveUrl(url: string): string {
if (url.startsWith('http') || !get(location)) return url; if (url.startsWith('http') || !get(apiLocation)) return url
const protocol = window.location.protocol; const protocol = window.location.protocol
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`; return `${protocol}//${get(apiLocation)}${url.startsWith('/') ? '' : '/'}${url}`
} }
export class ApiError extends Error { export class ApiError extends Error {
constructor(public readonly response: Response) { constructor(public readonly response: Response) {
super(`${response.status}`); super(`${response.status}`)
} }
} }
+7 -7
View File
@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { Down } from './icons'; import { Down } from './icons'
function openCollapsible() { function openCollapsible() {
open = !open; open = !open
if (open) { if (open) {
opened(); opened()
} else { } else {
closed(); closed()
} }
} }
let { icon, title, children, open, opened, closed, class: klass } = $props(); let { icon, title, children, open, opened, closed, class: klass } = $props()
</script> </script>
<div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden"> <div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
+8 -3
View File
@@ -23,15 +23,20 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
use:exitBeforeEnter use:exitBeforeEnter
use:focusTrap> use:focusTrap
>
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"> class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}> <button
class="btn btn-error inline-flex items-center"
onclick={() => modals.close()}
>
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span> <labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
</button> </button>
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}> <button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
@@ -1,61 +1,61 @@
<script lang="ts"> <script lang="ts">
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { telemetry } from '$lib/stores/telemetry'; import { telemetry } from '$lib/stores/telemetry'
import { Cancel } from './icons'; import { Cancel } from './icons'
import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals'; import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals'
// provided by <Modals /> // provided by <Modals />
interface Props { interface Props {
isOpen: boolean; isOpen: boolean
} }
let { isOpen }: Props = $props(); let { isOpen }: Props = $props()
let updating = $state(true); let updating = $state(true)
let progress = $state(0); let progress = $state(0)
$effect(() => { $effect(() => {
if ($telemetry.download_ota.status == 'progress') { if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress; progress = $telemetry.download_ota.progress
} }
}); })
$effect(() => { $effect(() => {
if ($telemetry.download_ota.status == 'error') { if ($telemetry.download_ota.status == 'error') {
updating = false; updating = false
} }
}); })
let message = $state('Preparing ...'); let message = $state('Preparing ...')
$effect(() => { $effect(() => {
if ($telemetry.download_ota.status == 'progress') { if ($telemetry.download_ota.status == 'progress') {
message = 'Downloading ...'; message = 'Downloading ...'
} else if ($telemetry.download_ota.status == 'error') { } else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error; message = $telemetry.download_ota.error
} else if ($telemetry.download_ota.status == 'finished') { } else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...'; message = 'Restarting ...'
progress = 0; progress = 0
// Reload page after 5 sec // Reload page after 5 sec
setTimeout(() => { setTimeout(() => {
modals.closeAll(); modals.closeAll()
location.reload(); location.reload()
}, 5000); }, 5000)
} }
}); })
onBeforeClose(() => { onBeforeClose(() => {
if (updating) { if (updating) {
// prevents modal from closing // prevents modal from closing
return false; return false
} else { } else {
$telemetry.download_ota.status = 'idle'; $telemetry.download_ota.status = 'idle'
$telemetry.download_ota.error = ''; $telemetry.download_ota.error = ''
$telemetry.download_ota.progress = 0; $telemetry.download_ota.progress = 0
return true; return true
} }
}); })
</script> </script>
{#if isOpen} {#if isOpen}
@@ -89,8 +89,8 @@
class="btn btn-warning text-warning-content inline-flex flex-none items-center" class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating} disabled={updating}
onclick={() => { onclick={() => {
modals.closeAll(); modals.closeAll()
location.reload(); location.reload()
}} }}
> >
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button <Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
+13 -10
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { Check } from './icons'; import { Check } from './icons'
import { exitBeforeEnter, type ModalProps } from 'svelte-modals'; import { exitBeforeEnter, type ModalProps } from 'svelte-modals'
let { let {
isOpen, isOpen,
@@ -10,9 +10,9 @@
message, message,
onDismiss, onDismiss,
labels = { labels = {
dismiss: { label: 'Dismiss', icon: Check }, dismiss: { label: 'Dismiss', icon: Check }
}, }
}: ModalProps = $props(); }: ModalProps = $props()
</script> </script>
{#if isOpen} {#if isOpen}
@@ -21,9 +21,11 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
use:exitBeforeEnter use:exitBeforeEnter
use:focusTrap> use:focusTrap
>
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"> class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <p class="text-base-content mb-1 text-start">{message}</p>
@@ -31,7 +33,8 @@
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <button
class="btn btn-warning text-warning-content inline-flex items-center" class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}> onclick={onDismiss}
>
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span> <labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
</button> </button>
</div> </div>
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte'
import * as THREE from 'three'; import * as THREE from 'three'
import { imu } from '$lib/stores/imu'; import { imu } from '$lib/stores/imu'
import SceneBuilder from '$lib/sceneBuilder'; import SceneBuilder from '$lib/sceneBuilder'
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement
let sceneBuilder: SceneBuilder; let sceneBuilder: SceneBuilder
let cube: THREE.Mesh; let cube: THREE.Mesh
let targetRotation = new THREE.Euler(); let targetRotation = new THREE.Euler()
let lastUpdateTime = 0; let lastUpdateTime = 0
const LERP_SPEED = 5; // rotations per second const LERP_SPEED = 5 // rotations per second
const initThreeJS = () => { const initThreeJS = () => {
sceneBuilder = new SceneBuilder() sceneBuilder = new SceneBuilder()
@@ -18,59 +18,59 @@
.addOrbitControls(1, 10, false) .addOrbitControls(1, 10, false)
.addAmbientLight({ color: 0x404040, intensity: 0.5 }) .addAmbientLight({ color: 0x404040, intensity: 0.5 })
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 }) .addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
.fillParent(); .fillParent()
const geometry = new THREE.BoxGeometry(1, 1, 1); const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshPhongMaterial({ const material = new THREE.MeshPhongMaterial({
color: 0x00ff00, color: 0x00ff00,
transparent: true, transparent: true,
opacity: 0.8, opacity: 0.8
}); })
cube = new THREE.Mesh(geometry, material); cube = new THREE.Mesh(geometry, material)
sceneBuilder.scene.add(cube); sceneBuilder.scene.add(cube)
sceneBuilder.addRenderCb(() => { sceneBuilder.addRenderCb(() => {
if (!cube) return; if (!cube) return
const currentTime = performance.now(); const currentTime = performance.now()
const deltaTime = (currentTime - lastUpdateTime) / 1000; // convert to seconds const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
lastUpdateTime = currentTime; lastUpdateTime = currentTime
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime); const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor); cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor); cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor); cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
}); })
sceneBuilder.startRenderLoop(); sceneBuilder.startRenderLoop()
}; }
const updateOrientation = () => { const updateOrientation = () => {
if (!cube) return; if (!cube) return
const y = -$imu.x[$imu.x.length - 1] || 0; const y = -$imu.x[$imu.x.length - 1] || 0
const x = $imu.y[$imu.y.length - 1] || 0; const x = $imu.y[$imu.y.length - 1] || 0
const z = -$imu.z[$imu.z.length - 1] || 0; const z = -$imu.z[$imu.z.length - 1] || 0
targetRotation.set( targetRotation.set(
THREE.MathUtils.degToRad(x), THREE.MathUtils.degToRad(x),
THREE.MathUtils.degToRad(y), THREE.MathUtils.degToRad(y),
THREE.MathUtils.degToRad(z) THREE.MathUtils.degToRad(z)
); )
}; }
onMount(() => { onMount(() => {
initThreeJS(); initThreeJS()
}); })
onDestroy(() => { onDestroy(() => {
sceneBuilder?.renderer?.dispose(); sceneBuilder?.renderer?.dispose()
}); })
$effect(() => { $effect(() => {
if ($imu) { if ($imu) {
updateOrientation(); updateOrientation()
} }
}); })
</script> </script>
<div class="h-60 w-60 border-2 border-base-300 rounded-md"> <div class="h-60 w-60 border-2 border-base-300 rounded-md">
+25 -9
View File
@@ -11,14 +11,23 @@
right?: import('svelte').Snippet right?: import('svelte').Snippet
} }
let { open = $bindable(true), collapsible = true, icon, title, children, right }: Props = $props() let {
open = $bindable(true),
collapsible = true,
icon,
title,
children,
right
}: Props = $props()
</script> </script>
{#if collapsible} {#if collapsible}
<div <div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"> class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div <div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
{@render icon?.()} {@render icon?.()}
{@render title?.()} {@render title?.()}
@@ -27,26 +36,33 @@
class="btn btn-circle btn-ghost btn-sm" class="btn btn-circle btn-ghost btn-sm"
onclick={() => { onclick={() => {
open = !open open = !open
}}> }}
>
<Down <Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ? class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
open
) ?
'rotate-180' 'rotate-180'
: ''}" /> : ''}"
/>
</button> </button>
</div> </div>
{#if open} {#if open}
<div <div
class="flex flex-col gap-2 p-4 pt-0" class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()} {@render children?.()}
</div> </div>
{/if} {/if}
</div> </div>
{:else} {:else}
<div <div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"> class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div <div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
{@render icon?.()} {@render icon?.()}
{@render title?.()} {@render title?.()}
+1 -2
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Loader } from "./icons"; import { Loader } from './icons'
</script> </script>
<div class="flex h-full w-full flex-col items-center justify-center p-6"> <div class="flex h-full w-full flex-col items-center justify-center p-6">
+4 -2
View File
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { ComponentType } from 'svelte'
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning' type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
const { const {
@@ -9,12 +11,12 @@
class: klass = '', class: klass = '',
children = null children = null
} = $props<{ } = $props<{
icon?: any icon?: ComponentType
title: string title: string
description?: string | number description?: string | number
variant?: Variant variant?: Variant
class?: string class?: string
children?: () => any children?: () => ComponentType
}>() }>()
const Icon = $derived(icon) const Icon = $derived(icon)
+4 -4
View File
@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte'
import { location } from '$lib/stores'; import { apiLocation } from '$lib/stores'
let source = $state(`${$location}/api/camera/stream`); let source = $state(`${$apiLocation}/api/camera/stream`)
onDestroy(() => (source = '#')); onDestroy(() => (source = '#'))
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
+10 -8
View File
@@ -1,22 +1,24 @@
<script> <script>
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from './icons'; import { error, info, success, warning } from './icons'
/** @type {{theme?: any, icon?: any}} */ /** @type {{theme?: any, icon?: any}} */
let { theme = { let {
theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}, icon = { },
icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
} } = $props(); }
} = $props()
</script> </script>
<div class="toast toast-end mr-4"> <div class="toast toast-end mr-4">
+53 -39
View File
@@ -24,7 +24,6 @@
jointNames, jointNames,
currentKinematic, currentKinematic,
walkGait, walkGait,
walkGaits,
walkGaitToMode walkGaitToMode
} from '$lib/stores' } from '$lib/stores'
import { import {
@@ -37,7 +36,14 @@
import { lerp, degToRad } from 'three/src/math/MathUtils' import { lerp, degToRad } from 'three/src/math/MathUtils'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import { type body_state_t } from '$lib/kinematic' import { type body_state_t } from '$lib/kinematic'
import { BezierState, CalibrationState, IdleState, RestState, StandState } from '$lib/gait' import {
Animater,
BezierState,
CalibrationState,
IdleState,
RestState,
StandState
} from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js' import { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader' import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store' import { get } from 'svelte/store'
@@ -60,8 +66,6 @@
let gui_panel: GUI let gui_panel: GUI
let Throttler = new throttler() let Throttler = new throttler()
let feet_trace = new Array(4).fill([])
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
let target: Object3D<Object3DEventMap> let target: Object3D<Object3DEventMap>
let target_position = { x: 0, z: 0, yaw: 0 } let target_position = { x: 0, z: 0, yaw: 0 }
@@ -74,7 +78,8 @@
[ModesEnum.Calibration]: new CalibrationState(), [ModesEnum.Calibration]: new CalibrationState(),
[ModesEnum.Rest]: new RestState(), [ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(), [ModesEnum.Stand]: new StandState(),
[ModesEnum.Walk]: new BezierState() [ModesEnum.Walk]: new BezierState(),
[ModesEnum.Animate]: new Animater()
} }
let lastTick = performance.now() let lastTick = performance.now()
@@ -87,7 +92,13 @@
xm: 0, xm: 0,
ym: 0.5, ym: 0.5,
zm: 0, zm: 0,
feet: kinematic.getDefaultFeetPos() feet: kinematic.getDefaultFeetPos(),
cumulative_x: 0,
cumulative_y: 0,
cumulative_z: 0,
cumulative_roll: 0,
cumulative_pitch: 0,
cumulative_yaw: 0
} }
let settings = { let settings = {
@@ -166,7 +177,10 @@
const updateAngles = (name: string, angle: number) => { const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI) modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100) Throttler.throttle(
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))),
100
)
} }
const createScene = async () => { const createScene = async () => {
@@ -177,7 +191,7 @@
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 }) .addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
.addAmbientLight({ color: 0xffffff, intensity: 0.5 }) .addAmbientLight({ color: 0xffffff, intensity: 0.5 })
.addFogExp2(0xcccccc, 0.015) .addFogExp2(0xcccccc, 0.015)
.addModel($model) .addModel($model as URDFRobot)
.addTransformControls(sceneManager.model) .addTransformControls(sceneManager.model)
.fillParent() .fillParent()
.addRenderCb(render) .addRenderCb(render)
@@ -191,32 +205,13 @@
sceneManager.scene.add(target) sceneManager.scene.add(target)
if (debug) { if (debug) {
sceneManager.addDragControl(updateAngles) sceneManager.addDragControl((angles: Record<string, number>) => {
Object.entries(angles).forEach(([name, angle]) => {
updateAngles(name, angle)
})
})
} }
if (sky) sceneManager.addSky() if (sky) sceneManager.addSky()
for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry()
const material = new LineBasicMaterial({ color: extractFootColor() })
const line = new Line(geometry, material)
trace_lines.push(geometry)
sceneManager.scene.add(line)
}
}
const renderTraceLines = (foot_positions: Vector3[]) => {
if (!settings['Trace feet']) {
if (!feet_trace.length) return
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
feet_trace = new Array(4).fill([])
return
}
trace_lines.forEach((line, i) => {
feet_trace[i].push(foot_positions[i])
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
line.setFromPoints(feet_trace[i])
})
} }
const calculate_kinematics = () => { const calculate_kinematics = () => {
@@ -228,7 +223,13 @@
xm: settings.xm, xm: settings.xm,
ym: settings.ym, ym: settings.ym,
zm: settings.zm, zm: settings.zm,
feet: body_state.feet feet: body_state.feet,
cumulative_x: body_state.cumulative_x,
cumulative_y: body_state.cumulative_y,
cumulative_z: body_state.cumulative_z,
cumulative_roll: body_state.cumulative_roll,
cumulative_pitch: body_state.cumulative_pitch,
cumulative_yaw: body_state.cumulative_yaw
} }
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i])) let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
@@ -239,12 +240,26 @@
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y)) robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1) const cumulativeYaw = body_state.cumulative_yaw
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1) const cosYaw = Math.cos(cumulativeYaw)
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1) const sinYaw = Math.sin(cumulativeYaw)
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1) const rotatedXm = settings.xm * cosYaw - settings.zm * sinYaw
const rotatedZm = settings.xm * sinYaw + settings.zm * cosYaw
robot.position.x = smooth(robot.position.x, -rotatedZm - body_state.cumulative_z * 1.2, 0.1)
robot.position.z = smooth(robot.position.z, -rotatedXm - body_state.cumulative_x * 1.2, 0.1)
const pitch = degToRad(settings.psi - 90) + body_state.cumulative_pitch
const roll = degToRad(settings.omega) + body_state.cumulative_roll
robot.rotation.z = smooth(
robot.rotation.z,
degToRad(-settings.phi + $mpu.heading + 90) + cumulativeYaw,
0.1
)
robot.rotation.y = smooth(robot.rotation.y, roll, 0.1)
robot.rotation.x = smooth(robot.rotation.x, pitch, 0.1)
} }
const update_camera = (robot: URDFRobot) => { const update_camera = (robot: URDFRobot) => {
@@ -305,7 +320,6 @@
const toes = getToeWorldPositions(robot) const toes = getToeWorldPositions(robot)
renderTraceLines(toes)
update_camera(robot) update_camera(robot)
update_gait() update_gait()
calculate_kinematics() calculate_kinematics()
@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons"; import { MdiEyeOffOutline, MdiEyeOutline } from '../icons'
interface Props { interface Props {
show?: boolean; show?: boolean
value?: string; value?: string
id?: string; id?: string
} }
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props(); let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props()
let type = $derived(show ? 'text' : 'password'); let type = $derived(show ? 'text' : 'password')
const handleInput = (e: any) => value = e.target.value const handleInput = (e: Event) => (value = (e.target as HTMLInputElement).value)
const togglePassword = () => show = !show const togglePassword = () => (show = !show)
</script> </script>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex items-center gap-2">
@@ -3,8 +3,8 @@
min?: number min?: number
max?: number max?: number
step?: number step?: number
value?: any value?: number
oninput?: any oninput?: (value: number) => void
} }
let { let {
@@ -24,7 +24,8 @@
{max} {max}
{step} {step}
bind:value bind:value
{...rest} /> {...rest}
/>
<style> <style>
input[type='range']::-webkit-slider-runnable-track { input[type='range']::-webkit-slider-runnable-track {
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as PasswordInput } from './InputPassword.svelte'; export { default as PasswordInput } from './InputPassword.svelte'
export { default as VerticalSlider } from './VerticalSlider.svelte'; export { default as VerticalSlider } from './VerticalSlider.svelte'
+2 -2
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
children?: import('svelte').Snippet; children?: import('svelte').Snippet
} }
let { children }: Props = $props(); let { children }: Props = $props()
</script> </script>
<div class="box-border overflow-hidden flex-1"> <div class="box-border overflow-hidden flex-1">
@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import WidgetContainer from './WidgetContainer.svelte'; import WidgetContainer from './WidgetContainer.svelte'
import { import {
WidgetComponents, WidgetComponents,
type WidgetContainerConfig, type WidgetContainerConfig,
isWidgetConfig, isWidgetConfig
} from '$lib/stores/application'; } from '$lib/stores/application'
import Widget from './Widget.svelte'; import Widget from './Widget.svelte'
interface Props { interface Props {
container: WidgetContainerConfig; container: WidgetContainerConfig
} }
let { container }: Props = $props(); let { container }: Props = $props()
</script> </script>
<div class="w-full h-full flex flex-col overflow-hidden"> <div class="w-full h-full flex flex-col overflow-hidden">
@@ -19,7 +19,8 @@
class="flex w-full h-full" class="flex w-full h-full"
class:flex-row={container.layout === 'column'} class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'} class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}> class:flex-wrap={container.layout === 'wrap'}
>
{#each container.widgets as widget, index (widget.id + '-' + index)} {#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget> <Widget>
{#if isWidgetConfig(widget)} {#if isWidgetConfig(widget)}
@@ -32,8 +33,8 @@
{#if index !== container.widgets.length - 1} {#if index !== container.widgets.length - 1}
<div <div
class="divider bg-base-300 m-0" class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}> class:divider-horizontal={container.layout === 'column'}
</div> ></div>
{/if} {/if}
{/each} {/each}
</div> </div>
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Github } from "../icons"; import { Github } from '../icons'
interface Props { interface Props {
github: any; github: { url: string; version: string; active?: boolean; href?: string }
} }
let { github }: Props = $props(); let { github }: Props = $props()
</script> </script>
{#if github.active} {#if github.active}
@@ -1,12 +1,13 @@
<script> <script>
import logo from '$lib/assets/logo512.png'; import logo from '$lib/assets/logo512.png'
import { resolve } from '$app/paths'
/** @type {{appName: any}} */ /** @type {{appName: any}} */
let { appName } = $props(); let { appName } = $props()
</script> </script>
<a <a
href="/" href={resolve('/')}
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]" class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
> >
<img src={logo} alt="Logo" class="h-12 w-12" /> <img src={logo} alt="Logo" class="h-12 w-12" />
+10 -4
View File
@@ -33,9 +33,11 @@
const github = { href: 'https://github.com/' + page.data.github, active: true } const github = { href: 'https://github.com/' + page.data.github, active: true }
import type { ComponentType } from 'svelte'
type menuItem = { type menuItem = {
title: string title: string
icon: ConstructorOfATypedSvelteComponent icon: ComponentType
href?: string href?: string
feature: boolean feature: boolean
active?: boolean active?: boolean
@@ -145,7 +147,10 @@
title: 'Firmware Update', title: 'Firmware Update',
icon: Update, icon: Update,
href: withBase('/system/update'), href: withBase('/system/update'),
feature: $features.ota || $features.upload_firmware || $features.download_firmware feature:
$features.ota ||
$features.upload_firmware ||
$features.download_firmware
} }
] ]
} }
@@ -169,7 +174,7 @@
setActiveMenuItem(page.data.title) setActiveMenuItem(page.data.title)
}) })
const updateMenu = (event: any) => { const updateMenu = (event: CustomEvent) => {
setActiveMenuItem(event.details) setActiveMenuItem(event.details)
} }
</script> </script>
@@ -181,7 +186,8 @@
{menuItems} {menuItems}
select={updateMenu} select={updateMenu}
class="grow flex-nowrap overflow-y-auto overflow-x-hidden" class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
level="0" /> level="0"
/>
<div class="divider my-0"></div> <div class="divider my-0"></div>
+12 -4
View File
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import MenuList from './MenuList.svelte' import MenuList from './MenuList.svelte'
import type { ComponentType } from 'svelte'
type MenuItem = { type MenuItem = {
title: string title: string
icon: ConstructorOfATypedSvelteComponent icon: ComponentType
href?: string href?: string
feature: boolean feature: boolean
active?: boolean active?: boolean
@@ -17,7 +19,7 @@
</script> </script>
<ul class={klass + ' menu w-full'}> <ul class={klass + ' menu w-full'}>
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)} {#each menuItems as MenuItem[] as menuItem (menuItem.title)}
{#if menuItem.feature} {#if menuItem.feature}
<li> <li>
{#if menuItem.submenu} {#if menuItem.submenu}
@@ -27,7 +29,12 @@
{menuItem.title} {menuItem.title}
</summary> </summary>
<div class="pl-4"> <div class="pl-4">
<MenuList menuItems={menuItem.submenu} level={level + 1} {select} class={klass} /> <MenuList
menuItems={menuItem.submenu}
level={level + 1}
{select}
class={klass}
/>
</div> </div>
</details> </details>
{:else} {:else}
@@ -37,7 +44,8 @@
class:bg-base-100={menuItem.active} class:bg-base-100={menuItem.active}
class:text-lg={level === 0} class:text-lg={level === 0}
class:text-md={level === 1} class:text-md={level === 1}
onclick={() => selectMenuItem(menuItem.title)}> onclick={() => selectMenuItem(menuItem.title)}
>
<menuItem.icon class="h-6 w-6" /> <menuItem.icon class="h-6 w-6" />
{menuItem.title} {menuItem.title}
</a> </a>
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { isFullscreen, toggleFullscreen } from '$lib/stores'; import { isFullscreen, toggleFullscreen } from '$lib/stores'
import { MdiFullscreenExit, MdiFullscreen } from '../icons'; import { MdiFullscreenExit, MdiFullscreen } from '../icons'
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen); const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen)
</script> </script>
<button onclick={toggleFullscreen}> <button onclick={toggleFullscreen}>
@@ -1,26 +1,26 @@
<script lang="ts"> <script lang="ts">
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons"; import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from '../icons'
interface Props { interface Props {
showDBm?: boolean; showDBm?: boolean
rssi?: number; rssi?: number
} }
let { showDBm = false, rssi = 0 }: Props = $props(); let { showDBm = false, rssi = 0 }: Props = $props()
const getWiFiIcon = () => { const getWiFiIcon = () => {
if (rssi === 0) return WifiOff; if (rssi === 0) return WifiOff
if (rssi >= -55) return WiFi; if (rssi >= -55) return WiFi
if (rssi >= -75) return WiFi2; if (rssi >= -75) return WiFi2
if (rssi >= -85) return WiFi1; if (rssi >= -85) return WiFi1
return WiFi0; return WiFi0
}; }
const SvelteComponent = $derived(getWiFiIcon()); const SvelteComponent = $derived(getWiFiIcon())
</script> </script>
<div class="indicator"> <div class="indicator">
<div class="tooltip tooltip-left" data-tip={rssi + " dBm"}> <div class="tooltip tooltip-left" data-tip={rssi + ' dBm'}>
{#if showDBm} {#if showDBm}
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs"> <span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
{rssi} dBm {rssi} dBm
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { useFeatureFlags } from '$lib/stores'; import { useFeatureFlags } from '$lib/stores'
import { modals } from 'svelte-modals'; import { modals } from 'svelte-modals'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import { api } from '$lib/api'; import { api } from '$lib/api'
import { Cancel, Power } from '../icons'; import { Cancel, Power } from '../icons'
const features = useFeatureFlags(); const features = useFeatureFlags()
const postSleep = async () => await api.post('/api/system/sleep'); const postSleep = async () => await api.post('/api/system/sleep')
const confirmSleep = () => { const confirmSleep = () => {
modals.open(ConfirmDialog, { modals.open(ConfirmDialog, {
@@ -18,11 +18,11 @@
confirm: { label: 'Switch Off', icon: Power } confirm: { label: 'Switch Off', icon: Power }
}, },
onConfirm: () => { onConfirm: () => {
modals.close(); modals.close()
postSleep(); postSleep()
}
})
} }
});
};
</script> </script>
{#if $features.sleep} {#if $features.sleep}
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { mode, modes } from "$lib/stores"; import { mode, modes } from '$lib/stores'
const deactivate = async () => { const deactivate = async () => {
mode.set(modes.indexOf('deactivated')); mode.set(modes.indexOf('deactivated'))
}; }
</script> </script>
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button> <button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { MdiWeatherSunny, MdiMoonAndStars } from "../icons"; import { MdiWeatherSunny, MdiMoonAndStars } from '../icons'
</script> </script>
<label class="swap swap-rotate"> <label class="swap swap-rotate">
@@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Hamburger } from '../icons' import { Hamburger } from '../icons'
import { resolve } from '$app/paths'
</script> </script>
<div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800"> <div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800">
<div class="flex gap-2 p-2"> <div class="flex gap-2 p-2">
<a href="/"> <a href={resolve('/')}>
<Hamburger class="h-8 w-8" /> <Hamburger class="h-8 w-8" />
</a> </a>
</div> </div>
@@ -98,9 +98,11 @@
<div class="indicator flex-none"> <div class="indicator flex-none">
<button <button
class="btn btn-square btn-ghost h-9 w-9" class="btn btn-square btn-ghost h-9 w-9"
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}> onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span <span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"> class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>
{firmwareVersion} {firmwareVersion}
</span> </span>
<Firmware class="h-7 w-7" /> <Firmware class="h-7 w-7" />
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { selectedView, views } from "$lib/stores/application"; import { selectedView, views } from '$lib/stores/application'
import Selector from "../widget/Selector.svelte"; import Selector from '../widget/Selector.svelte'
</script> </script>
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} /> <Selector bind:selectedOption={$selectedView} options={$views.map(v => v.name)} />
+10 -8
View File
@@ -1,22 +1,24 @@
<script> <script>
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from '../icons'; import { error, info, success, warning } from '../icons'
/** @type {{theme?: any, icon?: any}} */ /** @type {{theme?: any, icon?: any}} */
let { theme = { let {
theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}, icon = { },
icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
} } = $props(); }
} = $props()
</script> </script>
<div class="toast toast-end mr-4 z-20"> <div class="toast toast-end mr-4 z-20">
@@ -1,4 +1,4 @@
import { writable, derived, type Writable } from 'svelte/store' import { writable } from 'svelte/store'
type StateType = 'info' | 'success' | 'warning' | 'error' type StateType = 'info' | 'success' | 'warning' | 'error'
@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { daisyColor } from '$lib/utilities'; import { daisyColor } from '$lib/utilities'
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js'
import { onMount } from 'svelte'; import { onMount } from 'svelte'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
let chartElement: HTMLCanvasElement; let chartElement: HTMLCanvasElement
let chart: Chart; let chart: Chart<'line', number[], number>
interface Props { interface Props {
label: any; label: string
data: number[]; data: number[]
title: any; title: string
} }
let { label, data, title }: Props = $props(); let { label, data, title }: Props = $props()
Chart.register(...registerables); Chart.register(...registerables)
onMount(() => { onMount(() => {
chart = new Chart(chartElement, { chart = new Chart(chartElement, {
@@ -30,36 +30,36 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data, data,
yAxisID: 'y', yAxisID: 'y'
}, }
], ]
}, },
options: { options: {
maintainAspectRatio: false, maintainAspectRatio: false,
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: {
display: true, display: true
}, },
tooltip: { tooltip: {
mode: 'index', mode: 'index',
intersect: false, intersect: false
}, }
}, },
elements: { elements: {
point: { point: {
radius: 0, radius: 0
}, }
}, },
scales: { scales: {
x: { x: {
grid: { grid: {
color: daisyColor('--bc', 10), color: daisyColor('--bc', 10)
}, },
ticks: { ticks: {
color: daisyColor('--bc'), color: daisyColor('--bc')
}, },
display: false, display: false
}, },
y: { y: {
type: 'linear', type: 'linear',
@@ -69,33 +69,34 @@
color: daisyColor('--bc'), color: daisyColor('--bc'),
font: { font: {
size: 16, size: 16,
weight: 'bold', weight: 'bold'
}, }
}, },
position: 'left', position: 'left',
min: 0, min: 0,
max: 100, max: 100,
grid: { color: daisyColor('--bc', 10) }, grid: { color: daisyColor('--bc', 10) },
ticks: { ticks: {
color: daisyColor('--bc'), color: daisyColor('--bc')
}, },
border: { color: daisyColor('--bc', 10) }, border: { color: daisyColor('--bc', 10) }
}, }
}, }
}, }
}); })
setInterval(() => { setInterval(() => {
chart.data.labels = data; chart.data.labels = data
chart.data.datasets[0].data = data; chart.data.datasets[0].data = data
}, 500); }, 500)
}); })
</script> </script>
<div class="w-full h-full overflow-x-auto"> <div class="w-full h-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={chartElement}></canvas> <canvas bind:this={chartElement}></canvas>
</div> </div>
</div> </div>
@@ -1,18 +1,19 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
options?: string[]; options?: string[]
selectedOption?: string; selectedOption?: string
change?: () => void; change?: () => void
[key: string]: any; [key: string]: unknown
} }
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props(); let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props()
</script> </script>
<select <select
bind:value={selectedOption} bind:value={selectedOption}
{...rest} {...rest}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"> class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
>
{#each options as option} {#each options as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
+530 -1
View File
@@ -53,6 +53,16 @@ export abstract class GaitState {
this.map_command(command) this.map_command(command)
this.body_state = body_state this.body_state = body_state
this.dt = dt / 1000 this.dt = dt / 1000
if (body_state.cumulative_x === undefined) {
body_state.cumulative_x = 0
body_state.cumulative_y = 0
body_state.cumulative_z = 0
body_state.cumulative_roll = 0
body_state.cumulative_pitch = 0
body_state.cumulative_yaw = 0
}
return body_state return body_state
} }
@@ -72,6 +82,11 @@ export abstract class GaitState {
export class IdleState extends GaitState { export class IdleState extends GaitState {
protected name = 'Idle' protected name = 'Idle'
step(body_state: body_state_t, command: ControllerCommand) {
super.step(body_state, command)
return body_state
}
} }
export class CalibrationState extends GaitState { export class CalibrationState extends GaitState {
@@ -79,6 +94,7 @@ export class CalibrationState extends GaitState {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) { step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command)
body_state.omega = 0 body_state.omega = 0
body_state.phi = 0 body_state.phi = 0
body_state.psi = 0 body_state.psi = 0
@@ -95,6 +111,7 @@ export class RestState extends GaitState {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
step(body_state: body_state_t, _command: ControllerCommand) { step(body_state: body_state_t, _command: ControllerCommand) {
super.step(body_state, _command)
body_state.omega = 0 body_state.omega = 0
body_state.phi = 0 body_state.phi = 0
body_state.psi = 0 body_state.psi = 0
@@ -110,6 +127,7 @@ export class StandState extends GaitState {
protected name = 'Stand' protected name = 'Stand'
step(body_state: body_state_t, command: ControllerCommand) { step(body_state: body_state_t, command: ControllerCommand) {
super.step(body_state, command)
body_state.omega = 0 body_state.omega = 0
body_state.phi = command.rx * 10 * (Math.PI / 2) body_state.phi = command.rx * 10 * (Math.PI / 2)
body_state.psi = command.ry * 10 * (Math.PI / 2) body_state.psi = command.ry * 10 * (Math.PI / 2)
@@ -125,7 +143,7 @@ export class BezierState extends GaitState {
protected phase = 0 protected phase = 0
protected phase_num = 0 protected phase_num = 0
protected step_length = 0 protected step_length = 0
protected stand_offset = 0.85 protected stand_offset = 0.75
protected mode: 'crawl' | 'trot' = 'trot' protected mode: 'crawl' | 'trot' = 'trot'
protected speed_factor = 1 protected speed_factor = 1
offset = [0, 0.5, 0.75, 0.25] offset = [0, 0.5, 0.75, 0.25]
@@ -135,6 +153,10 @@ export class BezierState extends GaitState {
protected shift_start_time = 0 protected shift_start_time = 0
protected current_shift_leg = -1 protected current_shift_leg = -1
protected last_body_state: body_state_t | null = null
protected cumulative_position = { x: 0, y: 0, z: 0 }
protected cumulative_orientation = { roll: 0, pitch: 0, yaw: 0 }
constructor() { constructor() {
super() super()
this.set_mode(this.mode) this.set_mode(this.mode)
@@ -174,6 +196,7 @@ export class BezierState extends GaitState {
this.update_phase() this.update_phase()
this.update_body_position() this.update_body_position()
this.update_feet_positions() this.update_feet_positions()
this.update_cumulative_position()
return this.body_state return this.body_state
} }
@@ -328,6 +351,51 @@ export class BezierState extends GaitState {
return this.body_state.feet[index] return this.body_state.feet[index]
} }
update_cumulative_position() {
if (this.last_body_state === null) {
this.last_body_state = { ...this.body_state }
this.body_state.cumulative_x = 0
this.body_state.cumulative_y = 0
this.body_state.cumulative_z = 0
this.body_state.cumulative_roll = 0
this.body_state.cumulative_pitch = 0
this.body_state.cumulative_yaw = 0
return
}
const m = this.gait_state
const moving = m.step_x !== 0 || m.step_z !== 0 || m.step_angle !== 0
if (moving) {
const step_displacement_x_local =
m.step_x * m.step_velocity * this.dt * this.speed_factor
const step_displacement_z_local =
m.step_z * m.step_velocity * this.dt * this.speed_factor
const step_displacement_yaw =
m.step_angle * m.step_velocity * this.dt * this.speed_factor
const cos_yaw = Math.cos(this.cumulative_orientation.yaw)
const sin_yaw = Math.sin(this.cumulative_orientation.yaw)
const step_displacement_x =
step_displacement_x_local * cos_yaw - step_displacement_z_local * sin_yaw
const step_displacement_z =
step_displacement_x_local * sin_yaw + step_displacement_z_local * cos_yaw
this.cumulative_position.x += step_displacement_x
this.cumulative_position.z += step_displacement_z
this.cumulative_orientation.yaw += step_displacement_yaw
}
this.body_state.cumulative_x = this.cumulative_position.x
this.body_state.cumulative_y = this.cumulative_position.y
this.body_state.cumulative_z = this.cumulative_position.z
this.body_state.cumulative_roll = this.cumulative_orientation.roll
this.body_state.cumulative_pitch = this.cumulative_orientation.pitch
this.body_state.cumulative_yaw = this.cumulative_orientation.yaw
this.last_body_state = { ...this.body_state }
}
} }
const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => { const stance_curve = (length: number, angle: number, depth: number, phase: number): number[] => {
@@ -422,4 +490,465 @@ const comb = (n: number, k: number): number => {
let c = 1 let c = 1
for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1) for (let i = 0; i < k; i++) c = (c * (n - i)) / (i + 1)
return c return c
};
/*
Units: meters, radians, seconds / beats
*/
// interface Options {
// controls: 'body' | 'legs' | 'both';
// extendable?: boolean; // if true, the animation can loop
// description?: string; // a description of the animation
// }
interface Frame {
time: number
position: number[]
orientation: number[]
feet?: number[][]
}
type Parameter = {
// name: string;
min: number
max: number
default: number
}
type Parameters = Record<string, Parameter>
interface Animation {
// options: Options = {};
parameters: Parameters
frames: Frame[]
loop?: boolean
}
const generateCircleAnimation = (
radius: number,
y: number,
duration: number,
segments: number
): Animation => {
const frames: Frame[] = []
const deltaTime = duration / segments
for (let i = 0; i <= segments; i++) {
const angle = (2 * Math.PI * i) / segments // Angle in radians
const x = radius * Math.cos(angle)
const z = radius * Math.sin(angle)
frames.push({
time: i * deltaTime,
position: [x, y, z],
orientation: [0, 0, 0]
})
}
return {
parameters: {
speed: { min: 0.1, max: 2, default: 1 },
x_offset: { min: -0.1, max: 0.1, default: 0 }
},
frames
}
}
const kinematicShowCaseGen = generateCircleAnimation(0.5, 0.7, 4000, 32)
const kinematicShowCase: Animation = {
loop: true,
parameters: {
speed: { min: 0.1, max: 2, default: 1 },
x_offset: { min: -0.1, max: 0.1, default: 0 }
},
frames: [
{
time: 0,
position: [0.5, 0.7, 0],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 500,
position: [0.3, 0.7, 0.3],
orientation: [0, 0, 0]
},
{
time: 1000,
position: [0, 0.7, 0.5],
orientation: [0, 0, 0]
},
{
time: 1500,
position: [-0.3, 0.7, 0.3],
orientation: [0, 0, 0]
},
{
time: 2000,
position: [-0.5, 0.7, 0],
orientation: [0, 0, 0]
},
{
time: 2500,
position: [-0.3, 0.7, -0.3],
orientation: [0, 0, 0]
},
{
time: 3000,
position: [0, 0.7, -0.5],
orientation: [0, 0, 0]
},
{
time: 3500,
position: [0.3, 0.7, -0.3],
orientation: [0, 0, 0]
},
{
time: 4000,
position: [0.5, 0.7, 0],
orientation: [0, 0, 0]
}
]
}
const stretch: Animation = {
loop: false,
parameters: {
speed: { min: 0.1, max: 2, default: 1 },
x_offset: { min: -0.1, max: 0.1, default: 0 }
},
frames: [
// Step forward
{
time: 0,
position: [0, 0.7, 0],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 250,
position: [0, 0.7, -0.2],
orientation: [0, 0, 0],
feet: [
[1.5, -0.5, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 500,
position: [0, 0.7, -0.2],
orientation: [0, 0, 0],
feet: [
[2, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 750,
position: [0, 0.7, 0.2],
orientation: [0, 0, 0],
feet: [
[2, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 1000,
position: [0, 0.7, 0.2],
orientation: [0, 0, 0],
feet: [
[2, -1, 1, 1],
[1.5, -0.5, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 1250,
position: [0, 0.7, 0.2],
orientation: [0, 0, 0],
feet: [
[2, -1, 1, 1],
[2, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 2500,
position: [0.5, 0.7, 0],
orientation: [0, 0, 25]
},
{
time: 4000,
position: [-0.7, 0.7, 0],
orientation: [0, 0, -20]
},
{
time: 5000,
position: [-0.7, 0.7, 0],
orientation: [0, 0, -20]
},
{
time: 6000,
position: [0, 0.7, 0],
orientation: [0, 0, 0]
},
{
time: 6000,
position: [-0.2, 0.7, -0.2],
orientation: [0, 0, 0],
feet: [
[2, -1, 1, 1],
[2, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 6500,
position: [-0.2, 0.7, -0.2],
orientation: [0, 0, 0],
feet: [
[0.5, -0.5, 1, 1],
[2, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 7000,
position: [-0.2, 0.7, 0.2],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[2, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 7500,
position: [-0.2, 0.7, 0.2],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[0.5, -0.5, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 8000,
position: [0, 0.7, 0],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
}
]
}
const pee: Animation = {
loop: false,
parameters: {
speed: { min: 0.1, max: 2, default: 1 },
x_offset: { min: -0.1, max: 0.1, default: 0 }
},
frames: [
{
time: 0,
position: [0, 0.7, 0],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 1000,
position: [0, 0.7, 0],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 2000,
position: [0.2, 0.7, 0.45],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 3000,
position: [0.2, 0.7, 0.45],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 4000,
position: [0.2, 0.7, 0.45],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, 0, -1, 1]
]
},
{
time: 5000,
position: [0.2, 0.7, 0.45],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
},
{
time: 6000,
position: [0, 0.7, 0],
orientation: [0, 0, 0],
feet: [
[1, -1, 1, 1],
[1, -1, -1, 1],
[-1, -1, 1, 1],
[-1, -1, -1, 1]
]
}
]
}
export class Animater extends GaitState {
protected name = 'Bezier'
time = 0
animation = [stretch, pee, kinematicShowCase][0]
speed = 1
xOffset = 0
begin() {
this.reset()
super.begin()
}
end() {
this.reset()
super.end()
}
reset() {
this.time = 0
}
step(body_state: body_state_t, command: ControllerCommand, dt: number = 0.02) {
return this.step_animation(body_state, dt)
}
setAnimation(animation: Animation) {
this.animation = animation
this.reset()
}
step_animation(body_state: body_state_t, dt: number = 0.02) {
this.dt = dt / 1000
const frames = this.animation.frames
const duration = frames[frames.length - 1].time
this.time += dt * this.speed
if (this.animation.loop !== false && this.time > duration) {
this.time = this.time % duration
} else if (this.time > duration) {
this.time = duration
}
const { prevFrame, nextFrame } = this.getBoundingFrames()
const t = this.getInterpolationFactor(prevFrame, nextFrame)
const position = this.interpolatePosition(prevFrame.position, nextFrame.position, t)
const orientation = this.interpolatePosition(prevFrame.orientation, nextFrame.orientation, t)
// Apply x_offset
// position[0] += this.xOffset;
body_state.xm = position[0]
body_state.ym = position[1]
body_state.zm = position[2]
body_state.omega = orientation[0]
body_state.phi = orientation[1]
body_state.psi = orientation[2]
if (prevFrame.feet && nextFrame.feet) {
for (let i = 0; i < 4; i++) {
body_state.feet[i] = this.interpolatePosition(prevFrame.feet[i], nextFrame.feet[i], t)
}
}
return body_state
}
private getBoundingFrames(): { prevFrame: Frame; nextFrame: Frame } {
const frames = this.animation.frames
for (let i = 0; i < frames.length - 1; i++) {
const prevFrame = frames[i]
const nextFrame = frames[i + 1]
if (this.time >= prevFrame.time && this.time <= nextFrame.time) {
return { prevFrame, nextFrame }
}
}
// Fallback (should not be reached if looping correctly)
return { prevFrame: frames[frames.length - 1], nextFrame: frames[0] }
}
private getInterpolationFactor(prevFrame: Frame, nextFrame: Frame): number {
const timeRange = nextFrame.time - prevFrame.time
const elapsed = this.time - prevFrame.time
return elapsed / timeRange
}
private interpolatePosition(pos1: number[], pos2: number[], t: number): number[] {
return pos1.map((val, index) => val + t * (pos2[index] - val))
}
} }
+17 -4
View File
@@ -6,6 +6,12 @@ export interface body_state_t {
ym: number ym: number
zm: number zm: number
feet: number[][] feet: number[][]
cumulative_x: number
cumulative_y: number
cumulative_z: number
cumulative_roll: number
cumulative_pitch: number
cumulative_yaw: number
} }
export interface position { export interface position {
@@ -101,11 +107,17 @@ export default class Kinematic {
pz = bz - mz pz = bz - mz
const lx = const lx =
this.invMountRot[0][0] * px + this.invMountRot[0][1] * py + this.invMountRot[0][2] * pz this.invMountRot[0][0] * px +
this.invMountRot[0][1] * py +
this.invMountRot[0][2] * pz
const ly = const ly =
this.invMountRot[1][0] * px + this.invMountRot[1][1] * py + this.invMountRot[1][2] * pz this.invMountRot[1][0] * px +
this.invMountRot[1][1] * py +
this.invMountRot[1][2] * pz
const lz = const lz =
this.invMountRot[2][0] * px + this.invMountRot[2][1] * py + this.invMountRot[2][2] * pz this.invMountRot[2][0] * px +
this.invMountRot[2][1] * py +
this.invMountRot[2][2] * pz
const xLocal = i % 2 === 1 ? -lx : lx const xLocal = i % 2 === 1 ? -lx : lx
return this.legIK(xLocal, ly, lz) return this.legIK(xLocal, ly, lz)
@@ -118,7 +130,8 @@ export default class Kinematic {
const H = sqrt(G * G + z * z) const H = sqrt(G * G + z * z)
const t1 = -atan2(y, x) - atan2(F, -this.coxa) const t1 = -atan2(y, x) - atan2(F, -this.coxa)
const D = const D =
(H * H - this.femur * this.femur - this.tibia * this.tibia) / (2 * this.femur * this.tibia) (H * H - this.femur * this.femur - this.tibia * this.tibia) /
(2 * this.femur * this.tibia)
const t3 = acos(max(-1, min(1, D))) const t3 = acos(max(-1, min(1, D)))
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3)) const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
return [t1, t2, t3] return [t1, t2, t3]
+8 -8
View File
@@ -58,14 +58,14 @@ export default class SceneBuilder {
public ground!: Mesh public ground!: Mesh
public renderer!: WebGLRenderer public renderer!: WebGLRenderer
public orbit: OrbitControls public orbit: OrbitControls
public callback: Function | undefined public callback: (() => void) | undefined
public gridHelper!: GridHelper public gridHelper!: GridHelper
public model!: URDFRobot public model!: URDFRobot
public liveStreamTexture!: CanvasTexture public liveStreamTexture!: CanvasTexture
private fog!: FogExp2 private fog!: FogExp2
private isLoaded: boolean = false private isLoaded: boolean = false
public isDragging: boolean = false public isDragging: boolean = false
highlightMaterial: any highlightMaterial: MeshPhongMaterial
sky!: Sky sky!: Sky
transformControl: TransformControls transformControl: TransformControls
public modelGroup!: Group public modelGroup!: Group
@@ -229,7 +229,7 @@ export default class SceneBuilder {
return this return this
} }
public addRenderCb = (callback: Function) => { public addRenderCb = (callback: () => void) => {
this.callback = callback this.callback = callback
return this return this
} }
@@ -275,7 +275,7 @@ export default class SceneBuilder {
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed' isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: any) => { const traverse = (c: Object3D) => {
if (c.type === 'Mesh') { if (c.type === 'Mesh') {
if (revert) { if (revert) {
c.material = c.__origMaterial c.material = c.__origMaterial
@@ -298,9 +298,9 @@ export default class SceneBuilder {
traverse(m) traverse(m)
} }
public addTransformControls = (model: any) => { public addTransformControls = (model: Object3D) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement) this.transformControl = new TransformControls(this.camera, this.renderer.domElement)
this.transformControl.addEventListener('dragging-changed', (event: any) => { this.transformControl.addEventListener('dragging-changed', (event: { value: boolean }) => {
this.orbit.enabled = !event.value this.orbit.enabled = !event.value
this.isDragging = !event.value this.isDragging = !event.value
}) })
@@ -310,7 +310,7 @@ export default class SceneBuilder {
return this return this
} }
public addModel = (model: any) => { public addModel = (model: URDFRobot) => {
this.modelGroup = new Group() this.modelGroup = new Group()
this.modelGroup.add(model) this.modelGroup.add(model)
this.model = model this.model = model
@@ -318,7 +318,7 @@ export default class SceneBuilder {
return this return this
} }
public addDragControl = (updateAngle: any) => { public addDragControl = (updateAngle: (angles: Record<string, number>) => void) => {
const highlightColor = '#FFFFFF' const highlightColor = '#FFFFFF'
const highlightMaterial = new MeshPhongMaterial({ const highlightMaterial = new MeshPhongMaterial({
shininess: 10, shininess: 10,
+31 -32
View File
@@ -1,54 +1,53 @@
import { Result } from '$lib/utilities/result'; import { Result } from '$lib/utilities/result'
import { browser } from '$app/environment'; import { browser } from '$app/environment'
class FileService { class FileService {
private dbPromise: Promise<Result<IDBDatabase, string>> | null = browser private dbPromise: Promise<Result<IDBDatabase, string>> | null =
? this.openDatabase() browser ? this.openDatabase() : null
: null;
private async openDatabase(): Promise<Result<IDBDatabase, string>> { private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => { return new Promise(resolve => {
const request = indexedDB.open('fileStorageDB', 1); const request = indexedDB.open('fileStorageDB', 1)
request.onupgradeneeded = () => { request.onupgradeneeded = () => {
request.result.createObjectStore('files'); request.result.createObjectStore('files')
}; }
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Error opening database')); request.onerror = () => resolve(Result.err('Error opening database'))
}); })
} }
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> { private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
if (!browser || !this.dbPromise) if (!browser || !this.dbPromise)
return Result.err('Not running in browser or DB not initialized'); return Result.err('Not running in browser or DB not initialized')
const dbResult = await this.dbPromise; const dbResult = await this.dbPromise
if (dbResult.isErr()) return Result.err('Database not initialized'); if (dbResult.isErr()) return Result.err('Database not initialized')
const store = dbResult.inner.transaction('files', mode).objectStore('files'); const store = dbResult.inner.transaction('files', mode).objectStore('files')
return Result.ok(store); return Result.ok(store)
} }
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> { public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
const storeResult = await this.getStore('readwrite'); const storeResult = await this.getStore('readwrite')
if (storeResult.isErr()) return Result.err('Failed to access store'); if (storeResult.isErr()) return Result.err('Failed to access store')
return new Promise((resolve) => { return new Promise(resolve => {
const request = storeResult.inner.put(file, key); const request = storeResult.inner.put(file, key)
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Failed to save file')); request.onerror = () => resolve(Result.err('Failed to save file'))
}); })
} }
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> { public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore('readonly'); const storeResult = await this.getStore('readonly')
if (storeResult.isErr()) return Result.err('Failed to access store'); if (storeResult.isErr()) return Result.err('Failed to access store')
return new Promise((resolve) => { return new Promise(resolve => {
const request = storeResult.inner.get(key); const request = storeResult.inner.get(key)
request.onsuccess = () => request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File not found')); resolve(request.result ? Result.ok(request.result) : Result.err('File not found'))
request.onerror = () => resolve(Result.err('Failed to retrieve file')); request.onerror = () => resolve(Result.err('Failed to retrieve file'))
}); })
} }
} }
export default browser ? new FileService() : null; export default browser ? new FileService() : null
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as fileService } from './file-service'; export { default as fileService } from './file-service'
export { default as resultService } from './result-service'; export { default as resultService } from './result-service'
+10 -10
View File
@@ -1,19 +1,19 @@
import { errorLogs, latestErrorLog } from '$lib/stores'; import { errorLogs, latestErrorLog } from '$lib/stores'
import type { Result } from '$lib/utilities'; import type { Result } from '$lib/utilities'
class ResultService { class ResultService {
public handleResult(result: Result<unknown, string>, tag?: string) { public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) { if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception }; const errorLogEntry = { tag, message: result.inner, exception: result.exception }
latestErrorLog.set(errorLogEntry); latestErrorLog.set(errorLogEntry)
errorLogs.update((entries) => { errorLogs.update(entries => {
entries.push(errorLogEntry); entries.push(errorLogEntry)
return entries; return entries
}); })
} }
return result; return result
} }
} }
export default new ResultService(); export default new ResultService()
+32 -18
View File
@@ -1,7 +1,7 @@
import { type Analytics } from '$lib/types/models'; import { type Analytics } from '$lib/types/models'
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
let analytics_data = { const analytics_data = {
uptime: <number[]>[], uptime: <number[]>[],
free_heap: <number[]>[], free_heap: <number[]>[],
total_heap: <number[]>[], total_heap: <number[]>[],
@@ -14,20 +14,22 @@ let analytics_data = {
cpu0_usage: <number[]>[], cpu0_usage: <number[]>[],
cpu1_usage: <number[]>[], cpu1_usage: <number[]>[],
cpu_usage: <number[]>[] cpu_usage: <number[]>[]
}; }
const maxAnalyticsData = 100; const maxAnalyticsData = 100
function createAnalytics() { function createAnalytics() {
const { subscribe, update } = writable(analytics_data); const { subscribe, update } = writable(analytics_data)
return { return {
subscribe, subscribe,
addData: (content: Analytics) => { addData: (content: Analytics) => {
update((analytics_data) => ({ update(analytics_data => ({
...analytics_data, ...analytics_data,
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData), uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].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( total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
-maxAnalyticsData -maxAnalyticsData
), ),
@@ -35,21 +37,33 @@ function createAnalytics() {
...analytics_data.used_heap, ...analytics_data.used_heap,
(content.total_heap - content.free_heap) / 1000 (content.total_heap - content.free_heap) / 1000
].slice(-maxAnalyticsData), ].slice(-maxAnalyticsData),
min_free_heap: [...analytics_data.min_free_heap, content.min_free_heap / 1000].slice( 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 -maxAnalyticsData
), ),
max_alloc_heap: [...analytics_data.max_alloc_heap, content.max_alloc_heap / 1000].slice( 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 -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) cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData)
})); }))
}
} }
};
} }
export const analytics = createAnalytics(); export const analytics = createAnalytics()
+27 -27
View File
@@ -1,47 +1,39 @@
import { persistentStore } from '$lib/utilities'; import { persistentStore } from '$lib/utilities'
import { get, type Writable } from 'svelte/store'; import { get, type Writable } from 'svelte/store'
import Visualization from '$lib/components/Visualization.svelte'; import Visualization from '$lib/components/Visualization.svelte'
import Stream from '$lib/components/Stream.svelte'; import Stream from '$lib/components/Stream.svelte'
import ChartWidget from '$lib/components/widget/ChartWidget.svelte'; import ChartWidget from '$lib/components/widget/ChartWidget.svelte'
export interface WidgetConfig { export interface WidgetConfig {
id: string | number; id: string | number
component: keyof typeof WidgetComponents; component: keyof typeof WidgetComponents
props?: Record<string, any>; props?: Record<string, unknown>
} }
export interface WidgetContainerConfig { export interface WidgetContainerConfig {
id: string | number; id: string | number
layout?: 'row' | 'column' | 'wrap'; layout?: 'row' | 'column' | 'wrap'
header?: string; header?: string
widgets: Array<WidgetConfig | WidgetContainerConfig>; widgets: Array<WidgetConfig | WidgetContainerConfig>
} }
export const isWidgetConfig = ( export const isWidgetConfig = (
widget: WidgetConfig | WidgetContainerConfig widget: WidgetConfig | WidgetContainerConfig
): widget is WidgetConfig => 'component' in widget; ): widget is WidgetConfig => 'component' in widget
export const WidgetComponents = { export const WidgetComponents = {
Visualization, Visualization,
Stream, Stream,
ChartWidget ChartWidget
}; }
interface View { interface View {
name: string; name: string
content: WidgetContainerConfig; content: WidgetContainerConfig
} }
const defaultViews: View[] = [ const defaultViews: View[] = [
{
name: 'Stream',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Stream' }]
}
},
{ {
name: '3D representation', name: '3D representation',
content: { content: {
@@ -50,6 +42,14 @@ const defaultViews: View[] = [
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }] widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
} }
}, },
{
name: 'Stream',
content: {
id: 'root',
layout: 'column',
widgets: [{ id: 2, component: 'Stream' }]
}
},
{ {
name: 'Split screen', name: 'Split screen',
content: { content: {
@@ -60,8 +60,8 @@ const defaultViews: View[] = [
] ]
} }
} }
]; ]
export const views: Writable<View[]> = persistentStore('views', defaultViews); export const views: Writable<View[]> = persistentStore('views', defaultViews)
export const selectedView = persistentStore('selected_view', get(views)[0].name); export const selectedView = persistentStore('selected_view', get(views)[0].name)
+7 -5
View File
@@ -3,7 +3,7 @@ import { notifications } from '$lib/components/toasts/notifications'
import Kinematic from '$lib/kinematic' import Kinematic from '$lib/kinematic'
import { persistentStore } from '$lib/utilities' import { persistentStore } from '$lib/utilities'
import { derived, type Writable } from 'svelte/store' import { derived, type Writable } from 'svelte/store'
import { base } from '$app/paths' import { resolve } from '$app/paths'
let featureFlagsStore: Writable<Record<string, boolean | string>> let featureFlagsStore: Writable<Record<string, boolean | string>>
@@ -22,10 +22,12 @@ export function useFeatureFlags() {
return featureFlagsStore return featureFlagsStore
} }
const base = resolve('/')
export const variants = { export const variants = {
SPOTMICRO_ESP32: { SPOTMICRO_ESP32: {
model: `${base}/spot_micro.urdf.xacro`, model: `${base}spot_micro.urdf.xacro`,
stl: `${base}/stl.zip`, stl: `${base}stl.zip`,
kinematics: { kinematics: {
coxa: 60.5 / 100, coxa: 60.5 / 100,
coxa_offset: 10 / 100, coxa_offset: 10 / 100,
@@ -36,8 +38,8 @@ export const variants = {
} }
}, },
SPOTMICRO_YERTLE: { SPOTMICRO_YERTLE: {
model: `${base}/yertle.URDF`, model: `${base}yertle.URDF`,
stl: `${base}/URDF.zip`, stl: `${base}URDF.zip`,
kinematics: { kinematics: {
coxa: 35 / 100, coxa: 35 / 100,
coxa_offset: 0 / 100, coxa_offset: 0 / 100,
+10 -10
View File
@@ -1,24 +1,24 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
export const isFullscreen = writable(false); export const isFullscreen = writable(false)
export function toggleFullscreen() { export function toggleFullscreen() {
isFullscreen.update((state) => { isFullscreen.update(state => {
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen(); !state ? document.documentElement.requestFullscreen() : document.exitFullscreen()
return !state; return !state
}); })
} }
export function enterFullscreen() { export function enterFullscreen() {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
document.documentElement.requestFullscreen(); document.documentElement.requestFullscreen()
isFullscreen.set(true); isFullscreen.set(true)
} }
} }
export function exitFullscreen() { export function exitFullscreen() {
if (document.fullscreenElement) { if (document.fullscreenElement) {
document.exitFullscreen(); document.exitFullscreen()
isFullscreen.set(false); isFullscreen.set(false)
} }
} }
+64 -24
View File
@@ -5,43 +5,83 @@ export type GamepadState = {
gamepads: Gamepad[] gamepads: Gamepad[]
} }
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => { const DEADZONE = 0.15
const update = () => { const dz = (x: number) => {
const hasGamepadAPI = 'getGamepads' in navigator const a = Math.abs(x)
if (!hasGamepadAPI) { if (a < DEADZONE) return 0
set({ available: false, gamepads: [] }) return ((a - DEADZONE) / (1 - DEADZONE)) * Math.sign(x)
return
} }
const gps = navigator.getGamepads?.() ?? [] let raf = 0
const validGamepads = gps.filter(Boolean) as Gamepad[] let running = false
set({
available: true, export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
gamepads: validGamepads const update = () => {
}) const pads = navigator.getGamepads?.() ?? []
const list = Array.from(pads)
.map(p => p || null)
.filter(Boolean) as Gamepad[]
set({ available: 'getGamepads' in navigator, gamepads: list })
raf = requestAnimationFrame(update) raf = requestAnimationFrame(update)
} }
window.addEventListener('gamepadconnected', update) const onConnect = () => update()
window.addEventListener('gamepaddisconnected', update) const onDisconnect = () => update()
let raf = requestAnimationFrame(update) const onVis = () => {
if (document.hidden) {
running = false
cancelAnimationFrame(raf)
} else if (!running) {
running = true
raf = requestAnimationFrame(update)
}
}
window.addEventListener('gamepadconnected', onConnect)
window.addEventListener('gamepaddisconnected', onDisconnect)
document.addEventListener('visibilitychange', onVis)
running = true
raf = requestAnimationFrame(update)
return () => { return () => {
running = false
cancelAnimationFrame(raf) cancelAnimationFrame(raf)
window.removeEventListener('gamepadconnected', update) window.removeEventListener('gamepadconnected', onConnect)
window.removeEventListener('gamepaddisconnected', update) window.removeEventListener('gamepaddisconnected', onDisconnect)
document.removeEventListener('visibilitychange', onVis)
} }
}) })
export const gamepad = derived(gamepads, $gamepads => export const gamepad = derived(gamepads, s =>
$gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null s.available && s.gamepads.length ? s.gamepads[0] : null
) )
export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0]) export const hasGamepad = derived(gamepads, s => s.available && s.gamepads.length > 0)
export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? []) export const gamepadAxes = derived(gamepad, g => (g ? g.axes.map(dz) : [0, 0, 0, 0]))
export const hasGamepad = derived( type ButtonEdge = { pressed: boolean; value: number; justPressed: boolean; justReleased: boolean }
gamepads, const prev = new Map<number, { pressed: boolean; value: number }[]>()
$gamepads => $gamepads.available && $gamepads.gamepads.length > 0
export const gamepadButtons = derived(gamepad, g => g?.buttons ?? [])
export const gamepadButtonsEdges = derived(gamepad, g => {
if (!g) return [] as ButtonEdge[]
const p = prev.get(g.index) || []
const out = g.buttons.map((b, i): ButtonEdge => {
const pr = p[i] || { pressed: false, value: 0 }
const pressed = !!b.pressed || b.value > 0.5
return {
pressed,
value: b.value,
justPressed: pressed && !pr.pressed,
justReleased: !pressed && pr.pressed
}
})
prev.set(
g.index,
out.map(x => ({ pressed: x.pressed, value: x.value }))
) )
return out
})
+12 -12
View File
@@ -1,7 +1,7 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import type { IMU } from '$lib/types/models'; import type { IMU } from '$lib/types/models'
const maxIMUData = 100; const maxIMUData = 100
export const imu = (() => { export const imu = (() => {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@@ -12,16 +12,16 @@ export const imu = (() => {
altitude: [] as number[], altitude: [] as number[],
pressure: [] as number[], pressure: [] as number[],
bmp_temp: [] as number[] bmp_temp: [] as number[]
}); })
const addData = (content: IMU) => { const addData = (content: IMU) => {
update(data => { update(data => {
(Object.keys(content) as (keyof IMU)[]).forEach(key => { ;(Object.keys(content) as (keyof IMU)[]).forEach(key => {
data[key] = [...data[key], content[key]].slice(-maxIMUData); data[key] = [...data[key], content[key]].slice(-maxIMUData)
}); })
return data; return data
}); })
}; }
return { subscribe, addData }; return { subscribe, addData }
})(); })()
+9 -9
View File
@@ -1,9 +1,9 @@
export * from './socket-store'; export * from './socket-store'
export * from './logging-store'; export * from './logging-store'
export * from './model-store'; export * from './model-store'
export * from './socket'; export * from './socket'
export * from './fullscreen'; export * from './fullscreen'
export * from './telemetry'; export * from './telemetry'
export * from './analytics'; export * from './analytics'
export * from './featureFlags'; export * from './featureFlags'
export * from './location-store'; export * from './location-store'
+5 -4
View File
@@ -1,5 +1,6 @@
import { persistentStore } from '$lib/utilities'; import { persistentStore } from '$lib/utilities'
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'; import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', ''); export const apiLocation =
PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '')
+6 -6
View File
@@ -1,11 +1,11 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store'
export interface errorLog { export interface errorLog {
message: unknown; message: unknown
tag?: string; tag?: string
exception?: unknown; exception?: unknown
} }
export const latestErrorLog: Writable<errorLog> = writable(); export const latestErrorLog: Writable<errorLog> = writable()
export const errorLogs: Writable<errorLog[]> = writable([]); export const errorLogs: Writable<errorLog[]> = writable([])
+11 -2
View File
@@ -8,7 +8,15 @@ export const jointNames = persistentStore('joint_names', <string[]>[])
export const model = writable() export const model = writable()
export const modes = ['deactivated', 'idle', 'calibration', 'rest', 'stand', 'walk'] as const export const modes = [
'deactivated',
'idle',
'calibration',
'rest',
'stand',
'walk',
'animate'
] as const
export type Modes = (typeof modes)[number] export type Modes = (typeof modes)[number]
@@ -18,7 +26,8 @@ export enum ModesEnum {
Calibration = 2, Calibration = 2,
Rest = 3, Rest = 3,
Stand = 4, Stand = 4,
Walk = 5 Walk = 5,
Animate = 6
} }
export enum WalkGaits { export enum WalkGaits {
+13 -13
View File
@@ -1,22 +1,22 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store'
import { type angles } from '$lib/types/models'; import { type angles } from '$lib/types/models'
export const servoAnglesOut: Writable<number[]> = writable([ export const servoAnglesOut: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]); ])
export const servoAngles: Writable<number[]> = writable([ export const servoAngles: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]); ])
export const logs = writable([] as string[]); export const logs = writable([] as string[])
export const mpu = writable({ heading: 0 }); export const mpu = writable({ heading: 0 })
export const sonar = writable([0, 0]); export const sonar = writable([0, 0])
export const distances = writable({}); export const distances = writable({})
export interface socketDataCollection { export interface socketDataCollection {
angles: Writable<angles>; angles: Writable<angles>
logs: Writable<string[]>; logs: Writable<string[]>
mpu: Writable<unknown>; mpu: Writable<unknown>
distances: Writable<unknown>; distances: Writable<unknown>
} }
export const socketData = { export const socketData = {
@@ -24,4 +24,4 @@ export const socketData = {
logs, logs,
mpu, mpu,
distances distances
}; }
+82 -82
View File
@@ -1,135 +1,135 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import { encode, decode } from '@msgpack/msgpack'; import { encode, decode } from '@msgpack/msgpack'
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const; const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number]; type SocketEvent = (typeof socketEvents)[number]
type SocketMessage = [number, string?, unknown?]; type SocketMessage = [number, string?, unknown?]
let useBinary = false; let useBinary = false
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => { const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer; useBinary = data instanceof ArrayBuffer
try { try {
if (useBinary) { if (useBinary) {
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage; return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage
} }
return JSON.parse(data as string); return JSON.parse(data as string)
} catch (error) { } catch (error) {
console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`); console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`)
}
return null
} }
return null;
};
const encodeMessage = (data: unknown) => { const encodeMessage = (data: unknown) => {
try { try {
return useBinary ? encode(data) : JSON.stringify(data); return useBinary ? encode(data) : JSON.stringify(data)
} catch (error) { } catch (error) {
console.error(`Could not encode data: ${data} - ${error}`); console.error(`Could not encode data: ${data} - ${error}`)
}
} }
};
function createWebSocket() { function createWebSocket() {
const listeners = new Map<string, Set<(data?: unknown) => void>>(); const listeners = new Map<string, Set<(data?: unknown) => void>>()
const { subscribe, set } = writable(false); const { subscribe, set } = writable(false)
const reconnectTimeoutTime = 5000; const reconnectTimeoutTime = 5000
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>; let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
let reconnectTimeoutId: ReturnType<typeof setTimeout>; let reconnectTimeoutId: ReturnType<typeof setTimeout>
let ws: WebSocket; let ws: WebSocket
let socketUrl: string | URL; let socketUrl: string | URL
function init(url: string | URL) { function init(url: string | URL) {
socketUrl = url; socketUrl = url
connect(); connect()
} }
function disconnect(reason: SocketEvent, event?: Event) { function disconnect(reason: SocketEvent, event?: Event) {
ws.close(); ws.close()
set(false); set(false)
clearTimeout(unresponsiveTimeoutId); clearTimeout(unresponsiveTimeoutId)
clearTimeout(reconnectTimeoutId); clearTimeout(reconnectTimeoutId)
listeners.get(reason)?.forEach(listener => listener(event)); listeners.get(reason)?.forEach(listener => listener(event))
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime); reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
} }
function connect() { function connect() {
ws = new WebSocket(socketUrl); ws = new WebSocket(socketUrl)
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer'
ws.onopen = ev => { ws.onopen = ev => {
ping(); ping()
useBinary = true; useBinary = true
ping(); ping()
set(true); set(true)
clearTimeout(reconnectTimeoutId); clearTimeout(reconnectTimeoutId)
listeners.get('open')?.forEach(listener => listener(ev)); listeners.get('open')?.forEach(listener => listener(ev))
for (const event of listeners.keys()) { for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue; if (socketEvents.includes(event as SocketEvent)) continue
subscribeToEvent(event); subscribeToEvent(event)
}
} }
};
ws.onmessage = frame => { ws.onmessage = frame => {
resetUnresponsiveCheck(); resetUnresponsiveCheck()
const message = decodeMessage(frame.data); const message = decodeMessage(frame.data)
if (!message) return; if (!message) return
const [, event, payload = undefined] = message; const [, event, payload = undefined] = message
if (event) listeners.get(event)?.forEach(listener => listener(payload)); if (event) listeners.get(event)?.forEach(listener => listener(payload))
}; }
ws.onerror = ev => disconnect('error', ev); ws.onerror = ev => disconnect('error', ev)
ws.onclose = ev => disconnect('close', ev); ws.onclose = ev => disconnect('close', ev)
} }
function unsubscribe(event: string, listener?: (data: unknown) => void) { function unsubscribe(event: string, listener?: (data: unknown) => void) {
const eventListeners = listeners.get(event); const eventListeners = listeners.get(event)
if (!eventListeners) return; if (!eventListeners) return
if (!eventListeners.size) { if (!eventListeners.size) {
unsubscribeToEvent(event); unsubscribeToEvent(event)
} }
if (listener) { if (listener) {
eventListeners?.delete(listener); eventListeners?.delete(listener)
} else { } else {
listeners.delete(event); listeners.delete(event)
} }
} }
function resetUnresponsiveCheck() { function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId); clearTimeout(unresponsiveTimeoutId)
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime); unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
} }
function sendEvent(event: string, data: unknown) { function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
send([2, event, data]); send([2, event, data])
} }
function unsubscribeToEvent(event: string) { function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
send([1, event]); send([1, event])
} }
function subscribeToEvent(event: string) { function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
send([0, event]); send([0, event])
} }
function send(data: unknown) { function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
const serialized = encodeMessage(data); const serialized = encodeMessage(data)
if (!serialized) { if (!serialized) {
console.error('Could not serialize data:', data); console.error('Could not serialize data:', data)
return; return
} }
ws.send(serialized); ws.send(serialized)
} }
function ping() { function ping() {
const serialized = encodeMessage([4]); const serialized = encodeMessage([4])
if (!serialized) { if (!serialized) {
console.error('Could not serialize message'); console.error('Could not serialize message')
return; return
} }
ws.send(serialized); ws.send(serialized)
} }
return { return {
@@ -137,24 +137,24 @@ function createWebSocket() {
sendEvent, sendEvent,
init, init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => { on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event); let eventListeners = listeners.get(event)
if (!eventListeners) { if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) { if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event); subscribeToEvent(event)
} }
eventListeners = new Set(); eventListeners = new Set()
listeners.set(event, eventListeners); listeners.set(event, eventListeners)
} }
eventListeners.add(listener as (data: unknown) => void); eventListeners.add(listener as (data: unknown) => void)
return () => { return () => {
unsubscribe(event, listener as (data: unknown) => void); unsubscribe(event, listener as (data: unknown) => void)
}; }
}, },
off: <T>(event: string, listener?: (data: T) => void) => { off: <T>(event: string, listener?: (data: T) => void) => {
unsubscribe(event, listener as (data: unknown) => void); unsubscribe(event, listener as (data: unknown) => void)
}, }
}; }
} }
export const socket = createWebSocket(); export const socket = createWebSocket()
+9 -9
View File
@@ -1,7 +1,7 @@
import type { DownloadOTA } from '$lib/types/models'; import type { DownloadOTA } from '$lib/types/models'
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
let telemetry_data = { const telemetry_data = {
rssi: { rssi: {
rssi: 0 rssi: 0
}, },
@@ -10,10 +10,10 @@ let telemetry_data = {
progress: 0, progress: 0,
error: '' error: ''
} }
}; }
function createTelemetry() { function createTelemetry() {
const { subscribe, set, update } = writable(telemetry_data); const { subscribe, update } = writable(telemetry_data)
return { return {
subscribe, subscribe,
@@ -21,15 +21,15 @@ function createTelemetry() {
update(telemetry_data => ({ update(telemetry_data => ({
...telemetry_data, ...telemetry_data,
rssi: { rssi: data } rssi: { rssi: data }
})); }))
}, },
setDownloadOTA: (data: DownloadOTA) => { setDownloadOTA: (data: DownloadOTA) => {
update(telemetry_data => ({ update(telemetry_data => ({
...telemetry_data, ...telemetry_data,
download_ota: { status: data.status, progress: data.progress, error: data.error } download_ota: { status: data.status, progress: data.progress, error: data.error }
})); }))
}
} }
};
} }
export const telemetry = createTelemetry(); export const telemetry = createTelemetry()
+15 -15
View File
@@ -1,17 +1,17 @@
declare module 'three/src/math/MathUtils' { declare module 'three/src/math/MathUtils' {
export function generateUUID(): string; export function generateUUID(): string
export function clamp(value: number, min: number, max: number): number; export function clamp(value: number, min: number, max: number): number
export function euclideanModulo(n: number, m: 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 mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number
export function lerp(x: number, y: number, t: number): number; export function lerp(x: number, y: number, t: number): number
export function smoothstep(x: number, min: number, max: number): number; export function smoothstep(x: number, min: number, max: number): number
export function smootherstep(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 randInt(low: number, high: number): number
export function randFloat(low: number, high: number): number; export function randFloat(low: number, high: number): number
export function randFloatSpread(range: number): number; export function randFloatSpread(range: number): number
export function degToRad(degrees: number): number; export function degToRad(degrees: number): number
export function radToDeg(radians: number): number; export function radToDeg(radians: number): number
export function isPowerOfTwo(value: number): boolean; export function isPowerOfTwo(value: number): boolean
export function ceilPowerOfTwo(value: number): number; export function ceilPowerOfTwo(value: number): number
export function floorPowerOfTwo(value: number): number; export function floorPowerOfTwo(value: number): number
} }
+9 -9
View File
@@ -1,14 +1,14 @@
declare module 'uzip' { declare module 'uzip' {
interface UZIP { interface UZIP {
parse(data: Uint8Array | ArrayBuffer): any; parse(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
compress(data: any): Uint8Array | ArrayBuffer; compress(data: Record<string, Uint8Array>): Uint8Array | ArrayBuffer
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer; compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
decompress(data: Uint8Array | ArrayBuffer): any; decompress(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer; decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
encode(data: any): Uint8Array | ArrayBuffer; encode(data: Record<string, Uint8Array>): Uint8Array | ArrayBuffer
decode(data: Uint8Array | ArrayBuffer): any; decode(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
} }
const uzip: UZIP; const uzip: UZIP
export default uzip; export default uzip
} }
+9 -9
View File
@@ -1,15 +1,15 @@
export class throttler { export class throttler {
private _throttlePause: boolean; private _throttlePause: boolean
constructor() { constructor() {
this._throttlePause = false; this._throttlePause = false
} }
throttle = (callback: Function, time: number) => { throttle = (callback: () => void, time: number) => {
if (this._throttlePause) return; if (this._throttlePause) return
this._throttlePause = true; this._throttlePause = true
setTimeout(() => { setTimeout(() => {
callback(); callback()
this._throttlePause = false; this._throttlePause = false
}, time); }, time)
}; }
} }
+5 -5
View File
@@ -1,6 +1,6 @@
export const daisyColor = (name: string, opacity: number = 100) => { export const daisyColor = (name: string, opacity: number = 100) => {
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
if (opacity >= 100) return color; if (opacity >= 100) return color
const alpha = Math.min(Math.max(opacity, 0), 100) / 100; const alpha = Math.min(Math.max(opacity, 0), 100) / 100
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`; return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`
}; }
+9 -9
View File
@@ -1,9 +1,9 @@
export * from './result'; export * from './result'
export * from './string-utilities'; export * from './string-utilities'
export * from './svelte-utilities'; export * from './svelte-utilities'
export * from './math-utilities'; export * from './math-utilities'
export * from './buffer-utilities'; export * from './buffer-utilities'
export * from './model-utilities'; export * from './model-utilities'
export * from './position-utilities'; export * from './position-utilities'
export * from './string-utilities'; export * from './string-utilities'
export * from './color-utilities'; export * from './color-utilities'
+13 -13
View File
@@ -1,18 +1,18 @@
export const toUint8 = (number: number, min: number, max: number) => { export const toUint8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number)); number = Math.max(min, Math.min(max, number))
let scaled = ((number - min) / (max - min)) * 255; const scaled = ((number - min) / (max - min)) * 255
return Math.round(scaled) & 0xff; return Math.round(scaled) & 0xff
}; }
export const toInt8 = (number: number, min: number, max: number) => { export const toInt8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number)); number = Math.max(min, Math.min(max, number))
let scaled = ((number - min) / (max - min)) * 255 - 128; const scaled = ((number - min) / (max - min)) * 255 - 128
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0; return Math.max(-128, Math.min(127, Math.round(scaled))) | 0
}; }
export const fromInt8 = (int8: number, min: number, max: number) => { export const fromInt8 = (int8: number, min: number, max: number) => {
int8 = Math.max(-128, Math.min(127, int8)); int8 = Math.max(-128, Math.min(127, int8))
const scaled = (int8 + 128) / 255; const scaled = (int8 + 128) / 255
const number = scaled * (max - min) + min; const number = scaled * (max - min) + min
return number; return number
}; }
+7 -4
View File
@@ -6,6 +6,7 @@ import { currentVariant, jointNames, model } from '$lib/stores'
import uzip from 'uzip' import uzip from 'uzip'
import { fileService } from '$lib/services' import { fileService } from '$lib/services'
import { get } from 'svelte/store' import { get } from 'svelte/store'
import { resolve } from '$app/paths'
let model_xml: XMLDocument let model_xml: XMLDocument
@@ -27,16 +28,18 @@ export const cacheModelFiles = async () => {
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][]) { for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
const url = new URL(path, window.location.href) const normalizedPath = path.startsWith('/') ? path : '/' + path
fileService?.saveFile(url.toString(), data) const resolvedUrl = resolve(normalizedPath as any)
fileService?.saveFile(resolvedUrl, data)
fileService?.saveFile(normalizedPath, data)
} }
} }
export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => { export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
const urdfLoader = new URDFLoader() 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') { if (typeof xml === 'string') {
xml = new window.DOMParser().parseFromString(xml, 'text/xml') xml = new window.DOMParser().parseFromString(xml, 'text/xml')
+35 -33
View File
@@ -1,30 +1,32 @@
class SunCalculator { class SunCalculator {
calculateSunElevation(lat: number = 55, lon: number = 12) { calculateSunElevation(lat: number = 55, lon: number = 12) {
const now = new Date(); const now = new Date()
const JD = this.getJulianDate(now); const JD = this.getJulianDate(now)
const solarDec = this.getSolarDeclination(JD); const solarDec = this.getSolarDeclination(JD)
const solarTime = this.getSolarTime(now, lon); const solarTime = this.getSolarTime(now, lon)
const hourAngle = (solarTime - 12) * 15; const hourAngle = (solarTime - 12) * 15
const elevation = Math.asin( const elevation = Math.asin(
Math.sin(this.degToRad(lat)) * Math.sin(solarDec) + Math.sin(this.degToRad(lat)) * Math.sin(solarDec) +
Math.cos(this.degToRad(lat)) * Math.cos(solarDec) * Math.cos(this.degToRad(hourAngle)) Math.cos(this.degToRad(lat)) *
); Math.cos(solarDec) *
Math.cos(this.degToRad(hourAngle))
)
return this.radToDeg(elevation); return this.radToDeg(elevation)
} }
getJulianDate(date: Date) { getJulianDate(date: Date) {
const Y = date.getUTCFullYear(); const Y = date.getUTCFullYear()
const M = date.getUTCMonth() + 1; const M = date.getUTCMonth() + 1
const D = const D =
date.getUTCDate() + date.getUTCDate() +
date.getUTCHours() / 24 + date.getUTCHours() / 24 +
date.getUTCMinutes() / 1440 + date.getUTCMinutes() / 1440 +
date.getUTCSeconds() / 86400; date.getUTCSeconds() / 86400
const A = Math.floor((14 - M) / 12); const A = Math.floor((14 - M) / 12)
const Y1 = Y + 4800 - A; const Y1 = Y + 4800 - A
const M1 = M + 12 * A - 3; const M1 = M + 12 * A - 3
return ( return (
D + D +
Math.floor((153 * M1 + 2) / 5) + Math.floor((153 * M1 + 2) / 5) +
@@ -33,33 +35,33 @@ class SunCalculator {
Math.floor(Y1 / 100) + Math.floor(Y1 / 100) +
Math.floor(Y1 / 400) - Math.floor(Y1 / 400) -
32045 32045
); )
} }
getSolarDeclination(JulianDate: number) { getSolarDeclination(JulianDate: number) {
const n = JulianDate - 2451545; const n = JulianDate - 2451545
const L = (280.46 + 0.9856474 * n) % 360; const L = (280.46 + 0.9856474 * n) % 360
const g = this.degToRad((357.528 + 0.9856003 * 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)); 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))); return Math.asin(Math.sin(lambda) * Math.sin(this.degToRad(23.44)))
} }
getSolarTime(date: Date, lon: number) { getSolarTime(date: Date, lon: number) {
const EoT = this.getEquationOfTime(date); const EoT = this.getEquationOfTime(date)
const offset = date.getTimezoneOffset() / 60; const offset = date.getTimezoneOffset() / 60
const standardMeridian = Math.round(lon / 15) * 15; const standardMeridian = Math.round(lon / 15) * 15
const solarTime = const solarTime =
date.getUTCHours() + date.getUTCHours() +
(date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 - (date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 -
offset; offset
return (solarTime + 24) % 24; return (solarTime + 24) % 24
} }
getEquationOfTime(date: Date) { getEquationOfTime(date: Date) {
const JD = this.getJulianDate(date); const JD = this.getJulianDate(date)
const n = JD - 2451545; const n = JD - 2451545
const g = this.degToRad((357.528 + 0.9856003 * n) % 360); const g = this.degToRad((357.528 + 0.9856003 * n) % 360)
const q = this.degToRad((280.46 + 0.9856474 * n) % 360); const q = this.degToRad((280.46 + 0.9856474 * n) % 360)
return ( return (
4 * 4 *
this.radToDeg( this.radToDeg(
@@ -69,16 +71,16 @@ class SunCalculator {
0.014615 * Math.cos(2 * q) - 0.014615 * Math.cos(2 * q) -
0.040849 * Math.sin(2 * g) 0.040849 * Math.sin(2 * g)
) )
); )
} }
degToRad(deg: number) { degToRad(deg: number) {
return deg * (Math.PI / 180); return deg * (Math.PI / 180)
} }
radToDeg(rad: number) { radToDeg(rad: number) {
return rad * (180 / Math.PI); return rad * (180 / Math.PI)
} }
} }
export const sunCalculator = new SunCalculator(); export const sunCalculator = new SunCalculator()
+9 -9
View File
@@ -1,18 +1,18 @@
export class Err<T, U> { export class Err<T, U> {
#inner: T; #inner: T
#exception?: U; #exception?: U
constructor(inner: T, exception?: U) { constructor(inner: T, exception?: U) {
this.#inner = inner; this.#inner = inner
this.#exception = exception; this.#exception = exception
} }
get inner(): T { get inner(): T {
return this.#inner; return this.#inner
} }
get exception(): U | undefined { get exception(): U | undefined {
return this.#exception; return this.#exception
} }
/** /**
@@ -20,7 +20,7 @@ export class Err<T, U> {
* @returns `true` if `Ok`; `false` if `Err` * @returns `true` if `Ok`; `false` if `Err`
*/ */
isOk(): false { isOk(): false {
return false; return false
} }
/** /**
@@ -28,7 +28,7 @@ export class Err<T, U> {
* @returns `true` if `Err`; `false` if `Ok` * @returns `true` if `Err`; `false` if `Ok`
*/ */
isErr(): this is Err<T, U> { isErr(): this is Err<T, U> {
return true; return true
} }
/** /**
@@ -37,6 +37,6 @@ export class Err<T, U> {
* @returns `Err(inner)` * @returns `Err(inner)`
*/ */
static new<E, F>(inner: E, exception: F): Err<E, F> { static new<E, F>(inner: E, exception: F): Err<E, F> {
return new Err<E, F>(inner, exception); return new Err<E, F>(inner, exception)
} }
} }
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './err'; export * from './err'
export * from './ok'; export * from './ok'
export * from './result'; export * from './result'
+7 -7
View File
@@ -1,12 +1,12 @@
export class Ok<T> { export class Ok<T> {
#inner: T; #inner: T
constructor(inner: T) { constructor(inner: T) {
this.#inner = inner; this.#inner = inner
} }
get inner(): T { get inner(): T {
return this.#inner; return this.#inner
} }
/** /**
@@ -14,7 +14,7 @@ export class Ok<T> {
* @returns `true` if `Ok`; `false` if `Err` * @returns `true` if `Ok`; `false` if `Err`
*/ */
isOk(): this is Ok<T> { isOk(): this is Ok<T> {
return true; return true
} }
/** /**
@@ -22,7 +22,7 @@ export class Ok<T> {
* @returns `true` if `Err`; `false` if `Ok` * @returns `true` if `Err`; `false` if `Ok`
*/ */
isErr(): false { isErr(): false {
return false; return false
} }
/** /**
@@ -31,7 +31,7 @@ export class Ok<T> {
* @returns `Ok(inner)` * @returns `Ok(inner)`
*/ */
static new<T>(inner: T): Ok<T> { static new<T>(inner: T): Ok<T> {
return new Ok<T>(inner); return new Ok<T>(inner)
} }
/** /**
@@ -39,6 +39,6 @@ export class Ok<T> {
* @returns `Ok(void)` * @returns `Ok(void)`
*/ */
static void(): Ok<void> { static void(): Ok<void> {
return new Ok(undefined); return new Ok(undefined)
} }
} }
+9 -9
View File
@@ -1,20 +1,20 @@
import { Err } from './err'; import { Err } from './err'
import { Ok } from './ok'; import { Ok } from './ok'
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>; export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>
export namespace Result { export const Result = {
/** /**
* @returns `Ok<T>` * @returns `Ok<T>`
*/ */
export function ok<T = unknown>(value: T) { ok<T = unknown>(value: T) {
return Ok.new(value); return Ok.new(value)
} },
/** /**
* @returns `Err<E, F>` * @returns `Err<E, F>`
*/ */
export function err<E = unknown, F = unknown>(error: E, exception?: F) { err<E = unknown, F = unknown>(error: E, exception?: F) {
return Err.new(error, exception); return Err.new(error, exception)
} }
} }
+10 -10
View File
@@ -1,16 +1,16 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import { browser } from '$app/environment'; import { browser } from '$app/environment'
export const persistentStore = <T>(key: string, initialValue: T) => { export const persistentStore = <T>(key: string, initialValue: T) => {
const savedValue = browser ? localStorage.getItem(key) : null; const savedValue = browser ? localStorage.getItem(key) : null
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue; const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue
const store = writable<T>(); const store = writable<T>()
store.subscribe(value => { store.subscribe(value => {
if (browser) localStorage.setItem(key, JSON.stringify(value)); if (browser) localStorage.setItem(key, JSON.stringify(value))
}); })
store.set(data); store.set(data)
return store; return store
}; }
+2 -1
View File
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state' import { page } from '$app/state'
import { resolve } from '$app/paths'
</script> </script>
<div class="flex justify-center items-center w-full h-full"> <div class="flex justify-center items-center w-full h-full">
<h1>{page.status} {page.error?.message}</h1> <h1>{page.status} {page.error?.message}</h1>
<span>Go to <a class="btn btn-primary" href="/">Home page</a></span> <span>Go to <a class="btn btn-primary" href={resolve('/')}>Home page</a></span>
</div> </div>
+9 -8
View File
@@ -18,7 +18,7 @@
servoAngles, servoAngles,
servoAnglesOut, servoAnglesOut,
socket, socket,
location, apiLocation,
useFeatureFlags, useFeatureFlags,
walkGait walkGait
} from '$lib/stores' } from '$lib/stores'
@@ -34,8 +34,8 @@
const features = useFeatureFlags() const features = useFeatureFlags()
onMount(async () => { onMount(async () => {
const ws = $location ? $location : window.location.host const ws = $apiLocation ? $apiLocation : window.location.host
socket.init(`ws://${ws}/api/ws/events`) socket.init(`ws://${ws}/api/ws`)
addEventListeners() addEventListeners()
@@ -83,7 +83,7 @@
telemetry.setRSSI(0) telemetry.setRSSI(0)
} }
const handleError = (data: any) => console.error(data) const handleError = (data: unknown) => console.error(data)
const handleAnalytics = (data: Analytics) => analytics.addData(data) const handleAnalytics = (data: Analytics) => analytics.addData(data)
@@ -115,14 +115,15 @@
</div> </div>
<Modals> <Modals>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
{#snippet backdrop()} {#snippet backdrop()}
<div <div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm" class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade transition:fade
onclick={modals.closeAll}> onclick={modals.closeAll}
</div> onkeydown={e => e.key === 'Escape' && modals.closeAll()}
role="button"
tabindex="0"
></div>
{/snippet} {/snippet}
</Modals> </Modals>
+14 -2
View File
@@ -6,8 +6,20 @@ const registerFetchIntercept = async () => {
const fileService = (await import('$lib/services/file-service')).default const fileService = (await import('$lib/services/file-service')).default
window.fetch = async (resource, config) => { window.fetch = async (resource, config) => {
const url = resource instanceof Request ? resource.url : resource.toString() 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) let file = await fileService?.getFile(url)
if (file?.isOk() && file.inner) return new Response(new Uint8Array(file.inner))
if (url.startsWith('http')) {
try {
const urlObj = new URL(url)
const pathOnly = urlObj.pathname
file = await fileService?.getFile(pathOnly)
if (file?.isOk() && file.inner) return new Response(new Uint8Array(file.inner))
} catch {}
}
return originalFetch(resource, config)
} }
} }
+5 -2
View File
@@ -3,11 +3,12 @@
import Visualization from '$lib/components/Visualization.svelte' import Visualization from '$lib/components/Visualization.svelte'
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { resolve } from '$app/paths'
onMount(() => { onMount(() => {
socket.subscribe(isConnected => { socket.subscribe(isConnected => {
if (isConnected) { if (isConnected) {
goto('/controller') goto(resolve('/controller'))
} }
}) })
}) })
@@ -21,7 +22,9 @@
<div class="card-body w-80"> <div class="card-body w-80">
<h2 class="card-title text-center text-2xl">Begin you journey</h2> <h2 class="card-title text-center text-2xl">Begin you journey</h2>
<p class="py-6 text-center"></p> <p class="py-6 text-center"></p>
<a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}> Add Robot Dog </a> <a class="btn btn-primary" href={resolve($socket ? '/controller' : '/connection')}>
Add Robot Dog
</a>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Connection from './Connection.svelte'; import Connection from './Connection.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'Connection' title: 'Connection'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
+7 -7
View File
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { WiFi } from '$lib/components/icons'; import { WiFi } from '$lib/components/icons'
import { location, socket } from '$lib/stores'; import { apiLocation, socket } from '$lib/stores'
const update = () => { const update = () => {
const ws = $location ? $location : window.location.host; const ws = $apiLocation ? $apiLocation : window.location.host
socket.init(`ws://${ws}/api/ws/events`); socket.init(`ws://${ws}/api/ws/events`)
}; }
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -19,7 +19,7 @@
<div class="flex"> <div class="flex">
<label class="label w-32" for="server">Address:</label> <label class="label w-32" for="server">Address:</label>
<input class="input" bind:value={$location} /> <input class="input" bind:value={$apiLocation} />
</div> </div>
<button class="btn btn-primary" onclick={update}>Update</button> <button class="btn btn-primary" onclick={update}>Update</button>
+2 -2
View File
@@ -1,3 +1,3 @@
export const load = async () => { export const load = async () => {
return { title: 'Controller' }; return { title: 'Controller' }
}; }
+41 -18
View File
@@ -9,14 +9,13 @@
modes, modes,
type Modes, type Modes,
ModesEnum, ModesEnum,
walkGaits,
WalkGaits, WalkGaits,
walkGait, walkGait,
walkGaitLabels walkGaitLabels
} from '$lib/stores' } from '$lib/stores'
import type { vector } from '$lib/types/models' import type { vector } from '$lib/types/models'
import { VerticalSlider } from '$lib/components/input' import { VerticalSlider } from '$lib/components/input'
import { gamepadAxes, hasGamepad } from '$lib/stores/gamepad' import { gamepadAxes, gamepadButtonsEdges, hasGamepad } from '$lib/stores/gamepad'
import { notifications } from '$lib/components/toasts/notifications' import { notifications } from '$lib/components/toasts/notifications'
let throttle = new throttler() let throttle = new throttler()
@@ -37,11 +36,25 @@
handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] }) handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] })
}) })
// TODO React to button press $effect(() => {
// $effect(() => { if (!$hasGamepad) return
// if ($gamepadButtons.length === 0) return const b = $gamepadButtonsEdges
// if (!b.length) return
// }) if (b[0]?.justPressed) mode.set(5)
if (b[1]?.justPressed) mode.set(4)
if (b[2]?.justPressed) mode.set(3)
if (b[3]?.justPressed) mode.set(0)
if (b[12]?.justPressed)
input.update(inputData => {
inputData['height'] = Math.min(inputData.height + 0.1, 1)
return inputData
})
if (b[13]?.justPressed)
input.update(inputData => {
inputData['height'] = Math.min(inputData.height - 0.1, 1)
return inputData
})
})
onMount(() => { onMount(() => {
left = nipplejs.create({ left = nipplejs.create({
@@ -61,9 +74,9 @@
}) })
left.on('move', (_, data) => handleJoyMove('left', data.vector)) left.on('move', (_, data) => handleJoyMove('left', data.vector))
left.on('end', (_, __) => handleJoyMove('left', { x: 0, y: 0 })) left.on('end', () => handleJoyMove('left', { x: 0, y: 0 }))
right.on('move', (_, data) => handleJoyMove('right', data.vector)) right.on('move', (_, data) => handleJoyMove('right', data.vector))
right.on('end', (_, __) => handleJoyMove('right', { x: 0, y: 0 })) right.on('end', () => handleJoyMove('right', { x: 0, y: 0 }))
}) })
const handleJoyMove = (key: 'left' | 'right', data: vector) => { const handleJoyMove = (key: 'left' | 'right', data: vector) => {
@@ -90,9 +103,11 @@
const down = event.type === 'keydown' const down = event.type === 'keydown'
input.update(data => { input.update(data => {
if (event.key === 'w') data.left.y = down ? 1 : 0 if (event.key === 'w') data.left.y = down ? 1 : 0
if (event.key === 'a') data.left.x = down ? 1 : 0 if (event.key === 'a') data.left.x = down ? -1 : 0
if (event.key === 's') data.left.y = down ? -1 : 0 if (event.key === 's') data.left.y = down ? -1 : 0
if (event.key === 'd') data.left.x = down ? -1 : 0 if (event.key === 'd') data.left.x = down ? 1 : 0
if (event.key === 'ArrowLeft') data.right.x = down ? 1 : 0
if (event.key === 'ArrowRight') data.right.x = down ? -1 : 0
return data return data
}) })
throttle.throttle(updateData, throttle_timing) throttle.throttle(updateData, throttle_timing)
@@ -135,22 +150,27 @@
<div class="flex justify-center w-full"></div> <div class="flex justify-center w-full"></div>
</div> </div>
<div class="absolute bottom-0 z-10 flex items-end"> <div class="absolute bottom-0 z-10 flex items-end">
<div class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl"> <div
class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl"
>
<VerticalSlider <VerticalSlider
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
oninput={(e: Event) => handleRange(e, 'height')} /> oninput={(e: Event) => handleRange(e, 'height')}
/>
<label for="height">Ht</label> <label for="height">Ht</label>
</div> </div>
<div <div
class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden"> class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden"
>
<div class="join"> <div class="join">
{#each modes as modeValue} {#each modes as modeValue}
<button <button
class="btn join-item" class="btn join-item"
class:btn-primary={$mode === modes.indexOf(modeValue)} class:btn-primary={$mode === modes.indexOf(modeValue)}
onclick={() => changeMode(modeValue)}> onclick={() => changeMode(modeValue)}
>
{capitalize(modeValue)} {capitalize(modeValue)}
</button> </button>
{/each} {/each}
@@ -163,7 +183,8 @@
<button <button
class="btn join-item btn-sm" class="btn join-item btn-sm"
class:btn-secondary={$walkGait === gaitValue} class:btn-secondary={$walkGait === gaitValue}
onclick={() => changeWalkGait(gaitValue)}> onclick={() => changeWalkGait(gaitValue)}
>
{walkGaitLabels[gaitValue]} {walkGaitLabels[gaitValue]}
</button> </button>
{/if} {/if}
@@ -180,7 +201,8 @@
step="0.01" step="0.01"
max="1" max="1"
oninput={e => handleRange(e, 's1')} oninput={e => handleRange(e, 's1')}
class="range range-sm range-primary" /> class="range range-sm range-primary"
/>
</div> </div>
<div> <div>
<label for="speed">Speed</label> <label for="speed">Speed</label>
@@ -191,7 +213,8 @@
step="0.01" step="0.01"
max="1" max="1"
oninput={e => handleRange(e, 'speed')} oninput={e => handleRange(e, 'speed')}
class="range range-sm range-primary" /> class="range range-sm range-primary"
/>
</div> </div>
</div> </div>
{/if} {/if}
+6 -5
View File
@@ -1,7 +1,8 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
import { goto } from '$app/navigation'; import { goto } from '$app/navigation'
import { resolve } from '$app/paths'
export const load = (async () => { export const load = (async () => {
goto('/'); goto(resolve('/'))
return; return
}) satisfies PageLoad; }) satisfies PageLoad
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Camera from './Camera.svelte'; import Camera from './Camera.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'Camera' title: 'Camera'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from "$lib/components/SettingsCard.svelte"; import SettingsCard from '$lib/components/SettingsCard.svelte'
import CameraSetting from './CameraSetting.svelte'; import CameraSetting from './CameraSetting.svelte'
import Stream from '$lib/components/Stream.svelte'; import Stream from '$lib/components/Stream.svelte'
import { Camera } from "$lib/components/icons"; import { Camera } from '$lib/components/icons'
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -1,13 +1,25 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api'; import { api } from '$lib/api'
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte'
import type { CameraSettings } from '$lib/types/models'; import type { CameraSettings } from '$lib/types/models'
let settings:CameraSettings = $state() let settings: CameraSettings = $state({
brightness: 0,
contrast: 0,
framesize: 0,
vflip: false,
hmirror: false,
special_effect: 0,
quality: 0,
saturation: 0,
sharpness: 0,
denoise: 0,
wb_mode: 0
})
const getCameraSettings = async () => { const getCameraSettings = async () => {
const result = await api.get<CameraSettings>('/api/camera/settings') const result = await api.get<CameraSettings>('/api/camera/settings')
if (result.isErr()) { if (result.isErr()) {
console.error("An error occurred", result.inner); console.error('An error occurred', result.inner)
return return
} }
settings = result.inner settings = result.inner
@@ -16,7 +28,7 @@
const updateCameraSettings = async () => { const updateCameraSettings = async () => {
const result = await api.post<CameraSettings>('/api/camera/settings', settings) const result = await api.post<CameraSettings>('/api/camera/settings', settings)
if (result.isErr()) { if (result.isErr()) {
console.error("An error occurred", result.inner); console.error('An error occurred', result.inner)
return return
} }
settings = result.inner settings = result.inner
@@ -25,23 +37,43 @@
{#await getCameraSettings()} {#await getCameraSettings()}
<Spinner /> <Spinner />
{:then _} {:then}
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<button class="btn btn-primary" type="button" onclick={updateCameraSettings}>Update camera settings</button> <button class="btn btn-primary" type="button" onclick={updateCameraSettings}
>Update camera settings</button
>
<label for="brightness"> <label for="brightness">
Brightness {settings.brightness} Brightness {settings.brightness}
<input type="range" min="-2" max="2" class="range range-xs" bind:value={settings.brightness}/> <input
type="range"
min="-2"
max="2"
class="range range-xs"
bind:value={settings.brightness}
/>
</label> </label>
<label for="contrast"> <label for="contrast">
Contrast {settings.contrast} Contrast {settings.contrast}
<input type="range" min="-2" max="2" class="range range-xs" bind:value={settings.contrast}/> <input
type="range"
min="-2"
max="2"
class="range range-xs"
bind:value={settings.contrast}
/>
</label> </label>
<label for="framesize"> <label for="framesize">
FrameSize {settings.framesize} FrameSize {settings.framesize}
<input type="range" min="0" max="10" class="range range-xs" bind:value={settings.framesize}/> <input
type="range"
min="0"
max="10"
class="range range-xs"
bind:value={settings.framesize}
/>
</label> </label>
<label class="cursor-pointer flex items-center justify-between"> <label class="cursor-pointer flex items-center justify-between">
@@ -56,7 +88,10 @@
<label for="special_effect" class="flex items-center"> <label for="special_effect" class="flex items-center">
<span class="basis-1/2">Special Effect</span> <span class="basis-1/2">Special Effect</span>
<select class="select select-bordered select-sm w-full max-w-xs" bind:value={settings.special_effect}> <select
class="select select-bordered select-sm w-full max-w-xs"
bind:value={settings.special_effect}
>
<option value={0}>No effect</option> <option value={0}>No effect</option>
<option value={1}>Negative</option> <option value={1}>Negative</option>
<option value={2}>Grayscale</option> <option value={2}>Grayscale</option>
+1 -1
View File
@@ -30,7 +30,7 @@
return () => socket.off(MessageTopic.i2cScan, handleScan) return () => socket.off(MessageTopic.i2cScan, handleScan)
}) })
const handleScan = (data: any) => { const handleScan = (data: { addresses: number[] }) => {
active_devices = data.addresses.map( active_devices = data.addresses.map(
(address: number) => (address: number) =>
i2cDevices.find(device => device.address === address) || { i2cDevices.find(device => device.address === address) || {
@@ -15,8 +15,8 @@
return () => socket.off(MessageTopic.peripheralSettings, handleSettings) return () => socket.off(MessageTopic.peripheralSettings, handleSettings)
}) })
const handleSettings = (data: any) => { const handleSettings = (data: Record<string, unknown>) => {
settings = data settings = data as PeripheralsConfiguration
} }
const handleSave = () => { const handleSave = () => {
@@ -56,7 +56,8 @@
max="48" max="48"
title="SDA pin number (0-48)" title="SDA pin number (0-48)"
disabled={!isEditing} disabled={!isEditing}
bind:value={settings.sda} /> bind:value={settings.sda}
/>
</label> </label>
<label for="scl" class="input validator"> <label for="scl" class="input validator">
SCL SCL
@@ -70,7 +71,8 @@
max="48" max="48"
title="SCL pin number (0-48)" title="SCL pin number (0-48)"
disabled={!isEditing} disabled={!isEditing}
bind:value={settings.scl} /> bind:value={settings.scl}
/>
</label> </label>
<label class="input validator" for="frequency"> <label class="input validator" for="frequency">
Frequency Frequency
@@ -83,14 +85,20 @@
max="430000" max="430000"
title="I2C frequency in Hz" title="I2C frequency in Hz"
disabled={!isEditing} disabled={!isEditing}
bind:value={settings.frequency} /> bind:value={settings.frequency}
/>
</label> </label>
<div> <div>
<button class="btn btn-outline btn-primary" onclick={() => (isEditing = !isEditing)}> <button
class="btn btn-outline btn-primary"
onclick={() => (isEditing = !isEditing)}
>
<Icon class="h-6 w-6" /> <Icon class="h-6 w-6" />
</button> </button>
{#if isEditing} {#if isEditing}
<button class="btn btn-outline btn-primary" onclick={handleSave}>Save</button> <button class="btn btn-outline btn-primary" onclick={handleSave}
>Save</button
>
{/if} {/if}
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import IMU from './imu.svelte'; import IMU from './imu.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'IMU' title: 'IMU'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
+9 -6
View File
@@ -173,7 +173,7 @@
}) })
} }
const updateChartData = (chart: Chart, data: number[], label: string) => { const updateChartData = (chart: Chart, data: number[]) => {
chart.data.labels = data chart.data.labels = data
chart.data.datasets[0].data = data chart.data.datasets[0].data = data
chart.options.scales!.y!.min = Math.min(...data) - 1 chart.options.scales!.y!.min = Math.min(...data) - 1
@@ -195,8 +195,8 @@
} }
if ($features.bmp) { if ($features.bmp) {
updateChartData(tempChart, $imu.bmp_temp, 'Temperature') updateChartData(tempChart, $imu.bmp_temp)
updateChartData(altitudeChart, $imu.altitude, 'Altitude') updateChartData(altitudeChart, $imu.altitude)
} }
} }
@@ -228,7 +228,8 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={angleChartElement}></canvas> <canvas bind:this={angleChartElement}></canvas>
</div> </div>
</div> </div>
@@ -238,14 +239,16 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={tempChartElement}></canvas> <canvas bind:this={tempChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={altitudeChartElement}></canvas> <canvas bind:this={altitudeChartElement}></canvas>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'Servo' title: 'Servo'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
@@ -3,7 +3,7 @@
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { RotateCw, RotateCcw } from '$lib/components/icons' import { RotateCw, RotateCcw } from '$lib/components/icons'
interface Props { interface Props {
data?: any data?: Record<string, unknown>
servoId?: number servoId?: number
pwm?: number pwm?: number
} }
@@ -70,7 +70,8 @@
onblur={syncConfig} onblur={syncConfig}
oninput={event => updateValue(event, index, 'center_pwm')} oninput={event => updateValue(event, index, 'center_pwm')}
min="80" min="80"
max="600" /> max="600"
/>
</td> </td>
<td> <td>
<input <input
@@ -81,13 +82,15 @@
onblur={syncConfig} onblur={syncConfig}
oninput={event => updateValue(event, index, 'center_angle')} oninput={event => updateValue(event, index, 'center_angle')}
min="-90" min="-90"
max="90" /> max="90"
/>
</td> </td>
<td> <td>
<button <button
class="btn btn-sm btn-ghost" class="btn btn-sm btn-ghost"
title="Toggle direction {servo.direction}" title="Toggle direction {servo.direction}"
onclick={() => toggleDirection(index)}> onclick={() => toggleDirection(index)}
>
{#if servo.direction === 1} {#if servo.direction === 1}
<RotateCw class="w-4 h-4 text-green-500" /> <RotateCw class="w-4 h-4 text-green-500" />
{:else} {:else}
@@ -104,7 +107,8 @@
onblur={syncConfig} onblur={syncConfig}
oninput={event => updateValue(event, index, 'conversion')} oninput={event => updateValue(event, index, 'conversion')}
min="0" min="0"
max="10" /> max="10"
/>
</td> </td>
</tr> </tr>
{/each} {/each}
@@ -41,7 +41,8 @@
max="600" max="600"
bind:value={pwm} bind:value={pwm}
oninput={updatePWM} oninput={updatePWM}
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" /> class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
<div class="flex flex-col"> <div class="flex flex-col">
<h2 class="text-lg">General servo configuration</h2> <h2 class="text-lg">General servo configuration</h2>
@@ -55,7 +56,8 @@
type="checkbox" type="checkbox"
class="toggle" class="toggle"
bind:checked={active} bind:checked={active}
onchange={active ? activateServo : deactivateServo} /> onchange={active ? activateServo : deactivateServo}
/>
</span> </span>
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<label for="servoId">Servo active {servoId}</label> <label for="servoId">Servo active {servoId}</label>
+6 -5
View File
@@ -1,7 +1,8 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
import { goto } from '$app/navigation'; import { goto } from '$app/navigation'
import { resolve } from '$app/paths'
export const load = (async () => { export const load = (async () => {
goto('/'); goto(resolve('/'))
return; return
}) satisfies PageLoad; }) satisfies PageLoad
+2 -1
View File
@@ -18,7 +18,8 @@
<button <button
class="opacity-0 group-hover:opacity-100 p-1 hover:text-red-500" class="opacity-0 group-hover:opacity-100 p-1 hover:text-red-500"
onclick={() => onDelete(name)}> onclick={() => onDelete(name)}
>
<TrashIcon class="w-4 h-4" /> <TrashIcon class="w-4 h-4" />
</button> </button>
</div> </div>

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