Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b1aed91d9 | |||
| 923ea17702 | |||
| 3a401abfab |
+1
-1
@@ -28,4 +28,4 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
+2
-1
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"useTabs": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"arrowParens": "avoid",
|
||||
"experimentalTernaries": true,
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"svelteBracketNewLine": false,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
||||
Vendored
+1
-5
@@ -1,7 +1,3 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"svelte.svelte-vscode",
|
||||
"bradlc.vscode-tailwindcss",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
"recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
|
||||
}
|
||||
|
||||
Vendored
+4
-4
@@ -1,8 +1,8 @@
|
||||
declare module 'app-env' {
|
||||
declare module "app-env" {
|
||||
interface ENV {
|
||||
VITE_USE_HOST_NAME: boolean
|
||||
VITE_USE_HOST_NAME: boolean;
|
||||
}
|
||||
|
||||
const appEnv: ENV
|
||||
export default appEnv
|
||||
const appEnv: ENV;
|
||||
export default appEnv;
|
||||
}
|
||||
|
||||
+37
-37
@@ -16,50 +16,50 @@
|
||||
"test:unit": "vitest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/mdi": "^1.2.3",
|
||||
"@iconify-json/tabler": "^1.2.23",
|
||||
"@playwright/test": "^1.56.0",
|
||||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@sveltejs/kit": "^2.46.4",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.2.1",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/three": "^0.180.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"jsdom": "^27.0.0",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.0",
|
||||
"svelte": "^5.39.11",
|
||||
"svelte-check": "^4.3.3",
|
||||
"@iconify-json/mdi": "^1.1.64",
|
||||
"@iconify-json/tabler": "^1.1.109",
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/kit": "^2.5.27",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@types/eslint": "^8.56.0",
|
||||
"@types/three": "^0.162.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.45.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"svelte-focus-trap": "^1.2.0",
|
||||
"tailwindcss": "^4.1.14",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-icons": "^22.4.2",
|
||||
"vite": "^7.1.9",
|
||||
"vitest": "^3.2.4"
|
||||
"tailwindcss": "^4.0.12",
|
||||
"tslib": "^2.6.1",
|
||||
"typescript": "^5.5.0",
|
||||
"unplugin-icons": "^0.18.5",
|
||||
"vite": "^6.2.1",
|
||||
"vitest": "^1.2.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^3.1.2",
|
||||
"@niku/vite-env-caster": "^1.1.2",
|
||||
"@sveltejs/adapter-auto": "^6.1.1",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"chart.js": "^4.5.0",
|
||||
"compare-versions": "^6.1.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"daisyui": "^5.2.0",
|
||||
"nipplejs": "^0.10.2",
|
||||
"@niku/vite-env-caster": "^1.0.2",
|
||||
"@sveltejs/adapter-auto": "^4.0.0",
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"chart.js": "^4.4.2",
|
||||
"compare-versions": "^6.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"daisyui": "^5.0.0",
|
||||
"nipplejs": "^0.10.1",
|
||||
"svelte-dnd-list": "^0.1.8",
|
||||
"svelte-modals": "^2.0.1",
|
||||
"three": "^0.180.0",
|
||||
"urdf-loader": "^0.12.6",
|
||||
"svelte-modals": "^2.0.0",
|
||||
"three": "^0.162.0",
|
||||
"urdf-loader": "^0.12.1",
|
||||
"uzip": "^0.20201231.0",
|
||||
"xacro-parser": "^0.3.10"
|
||||
"xacro-parser": "^0.3.9"
|
||||
},
|
||||
"packageManager": "pnpm@9.3.0"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test'
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
webServer: {
|
||||
@@ -7,6 +7,6 @@ const config: PlaywrightTestConfig = {
|
||||
},
|
||||
testDir: 'tests/integration',
|
||||
testMatch: /(.+\.)?(test|spec)\.[jt]s/
|
||||
}
|
||||
};
|
||||
|
||||
export default config
|
||||
export default config;
|
||||
|
||||
Generated
+2181
-1895
File diff suppressed because it is too large
Load Diff
@@ -23,14 +23,6 @@
|
||||
--base-content: oklch(0.3 0.012 256);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#nipple_0_0,
|
||||
#nipple_1_1 {
|
||||
z-index: 10 !important;
|
||||
|
||||
Vendored
+1
-1
@@ -10,4 +10,4 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
export {};
|
||||
|
||||
+1
-4
@@ -3,10 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/logo512.png" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
%sveltekit.head%
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('sum test', () => {
|
||||
it('adds 1 + 2 to equal 3', () => {
|
||||
expect(1 + 2).toBe(3);
|
||||
});
|
||||
});
|
||||
+34
-33
@@ -1,22 +1,22 @@
|
||||
import { get } from 'svelte/store'
|
||||
import { Err, Ok, type Result } from './utilities'
|
||||
import { apiLocation } from './stores'
|
||||
import { get } from 'svelte/store';
|
||||
import { Err, Ok, type Result } from './utilities';
|
||||
import { location } from './stores';
|
||||
|
||||
export const api = {
|
||||
get<TResponse>(endpoint: string, params?: RequestInit) {
|
||||
return sendRequest<TResponse>(endpoint, 'GET', null, params)
|
||||
},
|
||||
export namespace api {
|
||||
export function get<TResponse>(endpoint: string, params?: RequestInit) {
|
||||
return sendRequest<TResponse>(endpoint, 'GET', null, params);
|
||||
}
|
||||
|
||||
post<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'POST', data)
|
||||
},
|
||||
export function post<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'POST', data);
|
||||
}
|
||||
|
||||
put<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'PUT', data)
|
||||
},
|
||||
export function put<TResponse>(endpoint: string, data?: unknown) {
|
||||
return sendRequest<TResponse>(endpoint, 'PUT', data);
|
||||
}
|
||||
|
||||
remove<TResponse>(endpoint: string) {
|
||||
return sendRequest<TResponse>(endpoint, 'DELETE')
|
||||
export function remove<TResponse>(endpoint: string) {
|
||||
return sendRequest<TResponse>(endpoint, 'DELETE');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ async function sendRequest<TResponse>(
|
||||
data?: unknown,
|
||||
params?: RequestInit
|
||||
): Promise<Result<TResponse, Error>> {
|
||||
endpoint = resolveUrl(endpoint)
|
||||
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined
|
||||
endpoint = resolveUrl(endpoint);
|
||||
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
|
||||
|
||||
const request = {
|
||||
...params,
|
||||
@@ -38,42 +38,43 @@ async function sendRequest<TResponse>(
|
||||
Authorization: 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let response
|
||||
let response;
|
||||
|
||||
try {
|
||||
response = await fetch(endpoint, request)
|
||||
} catch {
|
||||
return Err.new(new Error(), 'An error has occurred')
|
||||
response = await fetch(endpoint, request);
|
||||
} catch (error) {
|
||||
return Err.new(new Error(), 'An error has occurred');
|
||||
}
|
||||
|
||||
const isResponseOk = response.status >= 200 && response.status < 400
|
||||
const isResponseOk = response.status >= 200 && response.status < 400;
|
||||
if (!isResponseOk) {
|
||||
if (response.status === 401) {
|
||||
return Err.new(new ApiError(response), 'User was not authorized')
|
||||
return Err.new(new ApiError(response), 'User was not authorized');
|
||||
}
|
||||
return Err.new(new ApiError(response), 'An error has occurred')
|
||||
return Err.new(new ApiError(response), 'An error has occurred');
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type')
|
||||
const contentType =
|
||||
response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
const data = await response.json()
|
||||
return Ok.new(data as TResponse)
|
||||
const data = await response.json();
|
||||
return Ok.new(data as TResponse);
|
||||
} else {
|
||||
// Handle empty object as response
|
||||
return Ok.new(null as TResponse)
|
||||
return Ok.new(null as TResponse);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveUrl(url: string): string {
|
||||
if (url.startsWith('http') || !get(apiLocation)) return url
|
||||
const protocol = window.location.protocol
|
||||
return `${protocol}//${get(apiLocation)}${url.startsWith('/') ? '' : '/'}${url}`
|
||||
if (url.startsWith('http') || !get(location)) return url;
|
||||
const protocol = window.location.protocol;
|
||||
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||
}
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(public readonly response: Response) {
|
||||
super(`${response.status}`)
|
||||
super(`${response.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { Down } from './icons'
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { Down } from './icons';
|
||||
|
||||
function openCollapsible() {
|
||||
open = !open
|
||||
open = !open;
|
||||
if (open) {
|
||||
opened()
|
||||
opened();
|
||||
} else {
|
||||
closed()
|
||||
closed();
|
||||
}
|
||||
}
|
||||
|
||||
let { icon, title, children, open, opened, closed, class: klass } = $props()
|
||||
let { icon, title, children, open, opened, closed, class: klass } = $props();
|
||||
</script>
|
||||
|
||||
<div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
|
||||
@@ -23,20 +23,15 @@
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
use:focusTrap>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-error inline-flex items-center"
|
||||
onclick={() => modals.close()}
|
||||
>
|
||||
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
|
||||
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
|
||||
|
||||
@@ -1,61 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { telemetry } from '$lib/stores/telemetry'
|
||||
import { Cancel } from './icons'
|
||||
import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals'
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import { Cancel } from './icons';
|
||||
import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals';
|
||||
|
||||
// provided by <Modals />
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
let { isOpen }: Props = $props()
|
||||
let { isOpen }: Props = $props();
|
||||
|
||||
let updating = $state(true)
|
||||
let updating = $state(true);
|
||||
|
||||
let progress = $state(0)
|
||||
let progress = $state(0);
|
||||
$effect(() => {
|
||||
if ($telemetry.download_ota.status == 'progress') {
|
||||
progress = $telemetry.download_ota.progress
|
||||
progress = $telemetry.download_ota.progress;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($telemetry.download_ota.status == 'error') {
|
||||
updating = false
|
||||
updating = false;
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let message = $state('Preparing ...')
|
||||
let message = $state('Preparing ...');
|
||||
|
||||
$effect(() => {
|
||||
if ($telemetry.download_ota.status == 'progress') {
|
||||
message = 'Downloading ...'
|
||||
message = 'Downloading ...';
|
||||
} else if ($telemetry.download_ota.status == 'error') {
|
||||
message = $telemetry.download_ota.error
|
||||
message = $telemetry.download_ota.error;
|
||||
} else if ($telemetry.download_ota.status == 'finished') {
|
||||
message = 'Restarting ...'
|
||||
progress = 0
|
||||
message = 'Restarting ...';
|
||||
progress = 0;
|
||||
// Reload page after 5 sec
|
||||
setTimeout(() => {
|
||||
modals.closeAll()
|
||||
location.reload()
|
||||
}, 5000)
|
||||
modals.closeAll();
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
onBeforeClose(() => {
|
||||
if (updating) {
|
||||
// prevents modal from closing
|
||||
return false
|
||||
return false;
|
||||
} else {
|
||||
$telemetry.download_ota.status = 'idle'
|
||||
$telemetry.download_ota.error = ''
|
||||
$telemetry.download_ota.progress = 0
|
||||
return true
|
||||
$telemetry.download_ota.status = 'idle';
|
||||
$telemetry.download_ota.error = '';
|
||||
$telemetry.download_ota.progress = 0;
|
||||
return true;
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
@@ -89,8 +89,8 @@
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
disabled={updating}
|
||||
onclick={() => {
|
||||
modals.closeAll()
|
||||
location.reload()
|
||||
modals.closeAll();
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { Check } from './icons'
|
||||
import { exitBeforeEnter, type ModalProps } from 'svelte-modals'
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { Check } from './icons';
|
||||
import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
@@ -10,9 +10,9 @@
|
||||
message,
|
||||
onDismiss,
|
||||
labels = {
|
||||
dismiss: { label: 'Dismiss', icon: Check }
|
||||
}
|
||||
}: ModalProps = $props()
|
||||
dismiss: { label: 'Dismiss', icon: Check },
|
||||
},
|
||||
}: ModalProps = $props();
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
@@ -21,11 +21,9 @@
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
use:focusTrap>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
@@ -33,8 +31,7 @@
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import * as THREE from 'three'
|
||||
import { imu } from '$lib/stores/imu'
|
||||
import SceneBuilder from '$lib/sceneBuilder'
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import * as THREE from 'three';
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import SceneBuilder from '$lib/sceneBuilder';
|
||||
|
||||
let canvas: HTMLCanvasElement
|
||||
let sceneBuilder: SceneBuilder
|
||||
let cube: THREE.Mesh
|
||||
let targetRotation = new THREE.Euler()
|
||||
let lastUpdateTime = 0
|
||||
const LERP_SPEED = 5 // rotations per second
|
||||
let canvas: HTMLCanvasElement;
|
||||
let sceneBuilder: SceneBuilder;
|
||||
let cube: THREE.Mesh;
|
||||
let targetRotation = new THREE.Euler();
|
||||
let lastUpdateTime = 0;
|
||||
const LERP_SPEED = 5; // rotations per second
|
||||
|
||||
const initThreeJS = () => {
|
||||
sceneBuilder = new SceneBuilder()
|
||||
@@ -18,59 +18,59 @@
|
||||
.addOrbitControls(1, 10, false)
|
||||
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
|
||||
.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({
|
||||
color: 0x00ff00,
|
||||
transparent: true,
|
||||
opacity: 0.8
|
||||
})
|
||||
cube = new THREE.Mesh(geometry, material)
|
||||
sceneBuilder.scene.add(cube)
|
||||
opacity: 0.8,
|
||||
});
|
||||
cube = new THREE.Mesh(geometry, material);
|
||||
sceneBuilder.scene.add(cube);
|
||||
|
||||
sceneBuilder.addRenderCb(() => {
|
||||
if (!cube) return
|
||||
const currentTime = performance.now()
|
||||
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
|
||||
lastUpdateTime = currentTime
|
||||
if (!cube) return;
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - lastUpdateTime) / 1000; // convert to seconds
|
||||
lastUpdateTime = currentTime;
|
||||
|
||||
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
|
||||
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
|
||||
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
|
||||
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
|
||||
})
|
||||
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime);
|
||||
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor);
|
||||
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor);
|
||||
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor);
|
||||
});
|
||||
|
||||
sceneBuilder.startRenderLoop()
|
||||
}
|
||||
sceneBuilder.startRenderLoop();
|
||||
};
|
||||
|
||||
const updateOrientation = () => {
|
||||
if (!cube) return
|
||||
if (!cube) return;
|
||||
|
||||
const y = -$imu.x[$imu.x.length - 1] || 0
|
||||
const x = $imu.y[$imu.y.length - 1] || 0
|
||||
const z = -$imu.z[$imu.z.length - 1] || 0
|
||||
const y = -$imu.x[$imu.x.length - 1] || 0;
|
||||
const x = $imu.y[$imu.y.length - 1] || 0;
|
||||
const z = -$imu.z[$imu.z.length - 1] || 0;
|
||||
|
||||
targetRotation.set(
|
||||
THREE.MathUtils.degToRad(x),
|
||||
THREE.MathUtils.degToRad(y),
|
||||
THREE.MathUtils.degToRad(z)
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
initThreeJS()
|
||||
})
|
||||
initThreeJS();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
sceneBuilder?.renderer?.dispose()
|
||||
})
|
||||
sceneBuilder?.renderer?.dispose();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($imu) {
|
||||
updateOrientation()
|
||||
updateOrientation();
|
||||
}
|
||||
})
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
|
||||
|
||||
@@ -11,23 +11,14 @@
|
||||
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>
|
||||
|
||||
{#if collapsible}
|
||||
<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
|
||||
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">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
@@ -36,33 +27,26 @@
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
open = !open
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
open
|
||||
) ?
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ?
|
||||
'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
: ''}" />
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Loader } from './icons'
|
||||
import { Loader } from "./icons";
|
||||
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center justify-center p-6">
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
<script lang="ts">
|
||||
import type { ComponentType } from 'svelte'
|
||||
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||
|
||||
const {
|
||||
@@ -11,12 +9,12 @@
|
||||
class: klass = '',
|
||||
children = null
|
||||
} = $props<{
|
||||
icon?: ComponentType
|
||||
icon?: any
|
||||
title: string
|
||||
description?: string | number
|
||||
variant?: Variant
|
||||
class?: string
|
||||
children?: () => ComponentType
|
||||
children?: () => any
|
||||
}>()
|
||||
|
||||
const Icon = $derived(icon)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte'
|
||||
import { apiLocation } from '$lib/stores'
|
||||
import { onDestroy } from 'svelte';
|
||||
import { location } from '$lib/stores';
|
||||
|
||||
let source = $state(`${$apiLocation}/api/camera/stream`)
|
||||
let source = $state(`${$location}/api/camera/stream`);
|
||||
|
||||
onDestroy(() => (source = '#'))
|
||||
onDestroy(() => (source = '#'));
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import { error, info, success, warning } from './icons'
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { error, info, success, warning } from './icons';
|
||||
|
||||
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let {
|
||||
theme = {
|
||||
let { theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
},
|
||||
icon = {
|
||||
}, icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
}
|
||||
} = $props()
|
||||
} } = $props();
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
BufferGeometry,
|
||||
Line,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
type Object3D,
|
||||
SphereGeometry,
|
||||
Vector3,
|
||||
type Object3DEventMap,
|
||||
Color
|
||||
type NormalBufferAttributes,
|
||||
type Object3DEventMap
|
||||
} from 'three'
|
||||
import {
|
||||
ModesEnum,
|
||||
@@ -21,9 +24,15 @@
|
||||
jointNames,
|
||||
currentKinematic,
|
||||
walkGait,
|
||||
walkGaits,
|
||||
walkGaitToMode
|
||||
} from '$lib/stores'
|
||||
import { populateModelCache, throttler, getToeWorldPositions } from '$lib/utilities'
|
||||
import {
|
||||
extractFootColor,
|
||||
populateModelCache,
|
||||
throttler,
|
||||
getToeWorldPositions
|
||||
} from '$lib/utilities'
|
||||
import SceneBuilder from '$lib/sceneBuilder'
|
||||
import { lerp, degToRad } from 'three/src/math/MathUtils'
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
|
||||
@@ -34,20 +43,14 @@
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
interface Props {
|
||||
defaultColor?: string | null
|
||||
sky?: boolean
|
||||
orbit?: boolean
|
||||
panel?: boolean
|
||||
debug?: boolean
|
||||
ground?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
defaultColor = '#0091ff',
|
||||
orbit = false,
|
||||
panel = true,
|
||||
debug = false,
|
||||
ground = true
|
||||
}: Props = $props()
|
||||
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
|
||||
|
||||
let sceneManager = $state(new SceneBuilder())
|
||||
let canvas: HTMLCanvasElement
|
||||
@@ -57,6 +60,8 @@
|
||||
let gui_panel: GUI
|
||||
let Throttler = new throttler()
|
||||
|
||||
let feet_trace = new Array(4).fill([])
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
let target: Object3D<Object3DEventMap>
|
||||
|
||||
let target_position = { x: 0, z: 0, yaw: 0 }
|
||||
@@ -82,13 +87,7 @@
|
||||
xm: 0,
|
||||
ym: 0.5,
|
||||
zm: 0,
|
||||
feet: kinematic.getDefaultFeetPos(),
|
||||
cumulative_x: 0,
|
||||
cumulative_y: 0,
|
||||
cumulative_z: 0,
|
||||
cumulative_roll: 0,
|
||||
cumulative_pitch: 0,
|
||||
cumulative_yaw: 0
|
||||
feet: kinematic.getDefaultFeetPos()
|
||||
}
|
||||
|
||||
let settings = {
|
||||
@@ -106,7 +105,7 @@
|
||||
xm: 0,
|
||||
ym: 0.7,
|
||||
zm: 0,
|
||||
Background: defaultColor
|
||||
Background: 'black'
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
@@ -151,7 +150,7 @@
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
visibility.add(settings, 'Target position')
|
||||
visibility.add(settings, 'Smooth motion')
|
||||
visibility.addColor(settings, 'Background').onChange(setSceneBackground).listen()
|
||||
visibility.addColor(settings, 'Background')
|
||||
}
|
||||
|
||||
const updateKinematicPosition = () => {
|
||||
@@ -165,14 +164,9 @@
|
||||
])
|
||||
}
|
||||
|
||||
const setSceneBackground = (c: string | null) => (sceneManager.scene.background = new Color(c!))
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
|
||||
Throttler.throttle(
|
||||
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))),
|
||||
100
|
||||
)
|
||||
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
|
||||
}
|
||||
|
||||
const createScene = async () => {
|
||||
@@ -183,7 +177,7 @@
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model as URDFRobot)
|
||||
.addModel($model)
|
||||
.addTransformControls(sceneManager.model)
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
@@ -197,13 +191,32 @@
|
||||
sceneManager.scene.add(target)
|
||||
|
||||
if (debug) {
|
||||
sceneManager.addDragControl(angles => {
|
||||
Object.entries(angles).forEach(([name, angle]) => {
|
||||
updateAngles(name, angle)
|
||||
})
|
||||
})
|
||||
sceneManager.addDragControl(updateAngles)
|
||||
}
|
||||
if (defaultColor) setSceneBackground(settings['Background'] || defaultColor)
|
||||
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 = () => {
|
||||
@@ -215,13 +228,7 @@
|
||||
xm: settings.xm,
|
||||
ym: settings.ym,
|
||||
zm: settings.zm,
|
||||
feet: body_state.feet,
|
||||
cumulative_x: body_state.cumulative_x,
|
||||
cumulative_y: body_state.cumulative_y,
|
||||
cumulative_z: body_state.cumulative_z,
|
||||
cumulative_roll: body_state.cumulative_roll,
|
||||
cumulative_pitch: body_state.cumulative_pitch,
|
||||
cumulative_yaw: body_state.cumulative_yaw
|
||||
feet: body_state.feet
|
||||
}
|
||||
|
||||
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
|
||||
@@ -232,26 +239,12 @@
|
||||
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
|
||||
|
||||
const cumulativeYaw = body_state.cumulative_yaw
|
||||
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
|
||||
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
|
||||
|
||||
const cosYaw = Math.cos(cumulativeYaw)
|
||||
const sinYaw = Math.sin(cumulativeYaw)
|
||||
const rotatedXm = settings.xm * cosYaw - settings.zm * sinYaw
|
||||
const rotatedZm = settings.xm * sinYaw + settings.zm * cosYaw
|
||||
|
||||
robot.position.x = smooth(robot.position.x, -rotatedZm - body_state.cumulative_z * 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)
|
||||
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
|
||||
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
|
||||
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
|
||||
}
|
||||
|
||||
const update_camera = (robot: URDFRobot) => {
|
||||
@@ -312,6 +305,7 @@
|
||||
|
||||
const toes = getToeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
update_camera(robot)
|
||||
update_gait()
|
||||
calculate_kinematics()
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { MdiEyeOffOutline, MdiEyeOutline } from '../icons'
|
||||
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons";
|
||||
|
||||
interface Props {
|
||||
show?: boolean
|
||||
value?: string
|
||||
id?: string
|
||||
show?: boolean;
|
||||
value?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props()
|
||||
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props();
|
||||
|
||||
let type = $derived(show ? 'text' : 'password')
|
||||
let type = $derived(show ? 'text' : 'password');
|
||||
|
||||
const handleInput = (e: Event) => (value = (e.target as HTMLInputElement).value)
|
||||
const handleInput = (e: any) => value = e.target.value
|
||||
|
||||
const togglePassword = () => (show = !show)
|
||||
const togglePassword = () => show = !show
|
||||
</script>
|
||||
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
value?: number
|
||||
oninput?: (value: number) => void
|
||||
value?: any
|
||||
oninput?: any
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -24,8 +24,7 @@
|
||||
{max}
|
||||
{step}
|
||||
bind:value
|
||||
{...rest}
|
||||
/>
|
||||
{...rest} />
|
||||
|
||||
<style>
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as PasswordInput } from './InputPassword.svelte'
|
||||
export { default as VerticalSlider } from './VerticalSlider.svelte'
|
||||
export { default as PasswordInput } from './InputPassword.svelte';
|
||||
export { default as VerticalSlider } from './VerticalSlider.svelte';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props()
|
||||
let { children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="box-border overflow-hidden flex-1">
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import WidgetContainer from './WidgetContainer.svelte'
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import {
|
||||
WidgetComponents,
|
||||
type WidgetContainerConfig,
|
||||
isWidgetConfig
|
||||
} from '$lib/stores/application'
|
||||
import Widget from './Widget.svelte'
|
||||
isWidgetConfig,
|
||||
} from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
|
||||
interface Props {
|
||||
container: WidgetContainerConfig
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
|
||||
let { container }: Props = $props()
|
||||
let { container }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||
@@ -19,8 +19,7 @@
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
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)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
@@ -33,8 +32,8 @@
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}
|
||||
></div>
|
||||
class:divider-horizontal={container.layout === 'column'}>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Github } from '../icons'
|
||||
import { Github } from "../icons";
|
||||
|
||||
interface Props {
|
||||
github: { url: string; version: string; active?: boolean; href?: string }
|
||||
github: any;
|
||||
}
|
||||
|
||||
let { github }: Props = $props()
|
||||
let { github }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if github.active}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<script>
|
||||
import logo from '$lib/assets/logo512.png'
|
||||
import { resolve } from '$app/paths'
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
|
||||
/** @type {{appName: any}} */
|
||||
let { appName } = $props()
|
||||
let { appName } = $props();
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={resolve('/')}
|
||||
href="/"
|
||||
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
|
||||
@@ -33,11 +33,9 @@
|
||||
|
||||
const github = { href: 'https://github.com/' + page.data.github, active: true }
|
||||
|
||||
import type { ComponentType } from 'svelte'
|
||||
|
||||
type menuItem = {
|
||||
title: string
|
||||
icon: ComponentType
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
@@ -147,10 +145,7 @@
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: withBase('/system/update'),
|
||||
feature:
|
||||
$features.ota ||
|
||||
$features.upload_firmware ||
|
||||
$features.download_firmware
|
||||
feature: $features.ota || $features.upload_firmware || $features.download_firmware
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -174,7 +169,7 @@
|
||||
setActiveMenuItem(page.data.title)
|
||||
})
|
||||
|
||||
const updateMenu = (event: CustomEvent) => {
|
||||
const updateMenu = (event: any) => {
|
||||
setActiveMenuItem(event.details)
|
||||
}
|
||||
</script>
|
||||
@@ -186,8 +181,7 @@
|
||||
{menuItems}
|
||||
select={updateMenu}
|
||||
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
|
||||
level="0"
|
||||
/>
|
||||
level="0" />
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import MenuList from './MenuList.svelte'
|
||||
import type { ComponentType } from 'svelte'
|
||||
|
||||
type MenuItem = {
|
||||
title: string
|
||||
icon: ComponentType
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
@@ -19,7 +17,7 @@
|
||||
</script>
|
||||
|
||||
<ul class={klass + ' menu w-full'}>
|
||||
{#each menuItems as MenuItem[] as menuItem (menuItem.title)}
|
||||
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
<li>
|
||||
{#if menuItem.submenu}
|
||||
@@ -29,12 +27,7 @@
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
<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>
|
||||
</details>
|
||||
{:else}
|
||||
@@ -44,8 +37,7 @@
|
||||
class:bg-base-100={menuItem.active}
|
||||
class:text-lg={level === 0}
|
||||
class:text-md={level === 1}
|
||||
onclick={() => selectMenuItem(menuItem.title)}
|
||||
>
|
||||
onclick={() => selectMenuItem(menuItem.title)}>
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</a>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { isFullscreen, toggleFullscreen } from '$lib/stores'
|
||||
import { MdiFullscreenExit, MdiFullscreen } from '../icons'
|
||||
import { isFullscreen, toggleFullscreen } from '$lib/stores';
|
||||
import { MdiFullscreenExit, MdiFullscreen } from '../icons';
|
||||
|
||||
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen)
|
||||
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
|
||||
</script>
|
||||
|
||||
<button onclick={toggleFullscreen}>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from '../icons'
|
||||
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
|
||||
|
||||
interface Props {
|
||||
showDBm?: boolean
|
||||
rssi?: number
|
||||
showDBm?: boolean;
|
||||
rssi?: number;
|
||||
}
|
||||
|
||||
let { showDBm = false, rssi = 0 }: Props = $props()
|
||||
let { showDBm = false, rssi = 0 }: Props = $props();
|
||||
|
||||
const getWiFiIcon = () => {
|
||||
if (rssi === 0) return WifiOff
|
||||
if (rssi >= -55) return WiFi
|
||||
if (rssi >= -75) return WiFi2
|
||||
if (rssi >= -85) return WiFi1
|
||||
return WiFi0
|
||||
}
|
||||
if (rssi === 0) return WifiOff;
|
||||
if (rssi >= -55) return WiFi;
|
||||
if (rssi >= -75) return WiFi2;
|
||||
if (rssi >= -85) return WiFi1;
|
||||
return WiFi0;
|
||||
};
|
||||
|
||||
const SvelteComponent = $derived(getWiFiIcon())
|
||||
const SvelteComponent = $derived(getWiFiIcon());
|
||||
</script>
|
||||
|
||||
<div class="indicator">
|
||||
<div class="tooltip tooltip-left" data-tip={rssi + ' dBm'}>
|
||||
<div class="tooltip tooltip-left" data-tip={rssi + " dBm"}>
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi} dBm
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { useFeatureFlags } from '$lib/stores'
|
||||
import { modals } from 'svelte-modals'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import { api } from '$lib/api'
|
||||
import { Cancel, Power } from '../icons'
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, Power } from '../icons';
|
||||
|
||||
const features = useFeatureFlags()
|
||||
const features = useFeatureFlags();
|
||||
|
||||
const postSleep = async () => await api.post('/api/system/sleep')
|
||||
const postSleep = async () => await api.post('/api/system/sleep');
|
||||
|
||||
const confirmSleep = () => {
|
||||
modals.open(ConfirmDialog, {
|
||||
@@ -18,11 +18,11 @@
|
||||
confirm: { label: 'Switch Off', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close()
|
||||
postSleep()
|
||||
}
|
||||
})
|
||||
modals.close();
|
||||
postSleep();
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if $features.sleep}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { mode, modes } from '$lib/stores'
|
||||
import { mode, modes } from "$lib/stores";
|
||||
|
||||
const deactivate = async () => {
|
||||
mode.set(modes.indexOf('deactivated'))
|
||||
}
|
||||
mode.set(modes.indexOf('deactivated'));
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { MdiWeatherSunny, MdiMoonAndStars } from '../icons'
|
||||
import { MdiWeatherSunny, MdiMoonAndStars } from "../icons";
|
||||
</script>
|
||||
|
||||
<label class="swap swap-rotate">
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { Hamburger } from '../icons'
|
||||
import { resolve } from '$app/paths'
|
||||
import {Hamburger} from '../icons'
|
||||
</script>
|
||||
|
||||
<div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800">
|
||||
<div class="flex gap-2 p-2">
|
||||
<a href={resolve('/')}>
|
||||
<Hamburger class="h-8 w-8" />
|
||||
<a href="/">
|
||||
<Hamburger class="h-8 w-8"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,11 +98,9 @@
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}>
|
||||
<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}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { selectedView, views } from '$lib/stores/application'
|
||||
import Selector from '../widget/Selector.svelte'
|
||||
import { selectedView, views } from "$lib/stores/application";
|
||||
import Selector from "../widget/Selector.svelte";
|
||||
</script>
|
||||
|
||||
<Selector bind:selectedOption={$selectedView} options={$views.map(v => v.name)} />
|
||||
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} />
|
||||
@@ -1,24 +1,22 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import { error, info, success, warning } from '../icons'
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { error, info, success, warning } from '../icons';
|
||||
|
||||
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let {
|
||||
theme = {
|
||||
let { theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
},
|
||||
icon = {
|
||||
}, icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
}
|
||||
} = $props()
|
||||
} } = $props();
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4 z-20">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable, derived, type Writable } from 'svelte/store'
|
||||
|
||||
type StateType = 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { daisyColor } from '$lib/utilities'
|
||||
import { Chart, registerables } from 'chart.js'
|
||||
import { onMount } from 'svelte'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { daisyColor } from '$lib/utilities';
|
||||
import { Chart, registerables } from 'chart.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { slide } from 'svelte/transition';
|
||||
|
||||
let chartElement: HTMLCanvasElement
|
||||
let chart: Chart<'line', number[], number>
|
||||
let chartElement: HTMLCanvasElement;
|
||||
let chart: Chart;
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
data: number[]
|
||||
title: string
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
|
||||
let { label, data, title }: Props = $props()
|
||||
let { label, data, title }: Props = $props();
|
||||
|
||||
Chart.register(...registerables)
|
||||
Chart.register(...registerables);
|
||||
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
@@ -30,36 +30,36 @@
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
color: daisyColor('--bc', 10),
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
@@ -69,34 +69,33 @@
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
}, 500)
|
||||
})
|
||||
chart.data.labels = data;
|
||||
chart.data.datasets[0].data = data;
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: string[]
|
||||
selectedOption?: string
|
||||
change?: () => void
|
||||
[key: string]: unknown
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change?: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props()
|
||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||
</script>
|
||||
|
||||
<select
|
||||
bind:value={selectedOption}
|
||||
{...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}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
|
||||
+1
-69
@@ -53,16 +53,6 @@ export abstract class GaitState {
|
||||
this.map_command(command)
|
||||
this.body_state = body_state
|
||||
this.dt = dt / 1000
|
||||
|
||||
if (body_state.cumulative_x === undefined) {
|
||||
body_state.cumulative_x = 0
|
||||
body_state.cumulative_y = 0
|
||||
body_state.cumulative_z = 0
|
||||
body_state.cumulative_roll = 0
|
||||
body_state.cumulative_pitch = 0
|
||||
body_state.cumulative_yaw = 0
|
||||
}
|
||||
|
||||
return body_state
|
||||
}
|
||||
|
||||
@@ -82,11 +72,6 @@ export abstract class GaitState {
|
||||
|
||||
export class IdleState extends GaitState {
|
||||
protected name = 'Idle'
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand) {
|
||||
super.step(body_state, command)
|
||||
return body_state
|
||||
}
|
||||
}
|
||||
|
||||
export class CalibrationState extends GaitState {
|
||||
@@ -94,7 +79,6 @@ export class CalibrationState extends GaitState {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
step(body_state: body_state_t, _command: ControllerCommand) {
|
||||
super.step(body_state, _command)
|
||||
body_state.omega = 0
|
||||
body_state.phi = 0
|
||||
body_state.psi = 0
|
||||
@@ -111,7 +95,6 @@ export class RestState extends GaitState {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
step(body_state: body_state_t, _command: ControllerCommand) {
|
||||
super.step(body_state, _command)
|
||||
body_state.omega = 0
|
||||
body_state.phi = 0
|
||||
body_state.psi = 0
|
||||
@@ -127,7 +110,6 @@ export class StandState extends GaitState {
|
||||
protected name = 'Stand'
|
||||
|
||||
step(body_state: body_state_t, command: ControllerCommand) {
|
||||
super.step(body_state, command)
|
||||
body_state.omega = 0
|
||||
body_state.phi = command.rx * 10 * (Math.PI / 2)
|
||||
body_state.psi = command.ry * 10 * (Math.PI / 2)
|
||||
@@ -143,7 +125,7 @@ export class BezierState extends GaitState {
|
||||
protected phase = 0
|
||||
protected phase_num = 0
|
||||
protected step_length = 0
|
||||
protected stand_offset = 0.75
|
||||
protected stand_offset = 0.85
|
||||
protected mode: 'crawl' | 'trot' = 'trot'
|
||||
protected speed_factor = 1
|
||||
offset = [0, 0.5, 0.75, 0.25]
|
||||
@@ -153,10 +135,6 @@ export class BezierState extends GaitState {
|
||||
protected shift_start_time = 0
|
||||
protected current_shift_leg = -1
|
||||
|
||||
protected last_body_state: body_state_t | null = null
|
||||
protected cumulative_position = { x: 0, y: 0, z: 0 }
|
||||
protected cumulative_orientation = { roll: 0, pitch: 0, yaw: 0 }
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.set_mode(this.mode)
|
||||
@@ -196,7 +174,6 @@ export class BezierState extends GaitState {
|
||||
this.update_phase()
|
||||
this.update_body_position()
|
||||
this.update_feet_positions()
|
||||
this.update_cumulative_position()
|
||||
return this.body_state
|
||||
}
|
||||
|
||||
@@ -351,51 +328,6 @@ export class BezierState extends GaitState {
|
||||
|
||||
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[] => {
|
||||
|
||||
@@ -6,12 +6,6 @@ export interface body_state_t {
|
||||
ym: number
|
||||
zm: number
|
||||
feet: number[][]
|
||||
cumulative_x: number
|
||||
cumulative_y: number
|
||||
cumulative_z: number
|
||||
cumulative_roll: number
|
||||
cumulative_pitch: number
|
||||
cumulative_yaw: number
|
||||
}
|
||||
|
||||
export interface position {
|
||||
@@ -107,17 +101,11 @@ export default class Kinematic {
|
||||
pz = bz - mz
|
||||
|
||||
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 =
|
||||
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 =
|
||||
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
|
||||
return this.legIK(xLocal, ly, lz)
|
||||
@@ -130,8 +118,7 @@ export default class Kinematic {
|
||||
const H = sqrt(G * G + z * z)
|
||||
const t1 = -atan2(y, x) - atan2(F, -this.coxa)
|
||||
const D =
|
||||
(H * H - this.femur * this.femur - this.tibia * this.tibia) /
|
||||
(2 * this.femur * this.tibia)
|
||||
(H * H - this.femur * this.femur - this.tibia * this.tibia) / (2 * this.femur * this.tibia)
|
||||
const t3 = acos(max(-1, min(1, D)))
|
||||
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
|
||||
return [t1, t2, t3]
|
||||
|
||||
+42
-10
@@ -17,16 +17,18 @@ import {
|
||||
MeshPhongMaterial,
|
||||
EquirectangularReflectionMapping,
|
||||
ACESFilmicToneMapping,
|
||||
MathUtils,
|
||||
Group,
|
||||
MeshBasicMaterial,
|
||||
RepeatWrapping,
|
||||
Object3D
|
||||
RepeatWrapping
|
||||
} from 'three'
|
||||
import { Sky } from 'three/addons/objects/Sky.js'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { TransformControls } from 'three/examples/jsm/controls/TransformControls'
|
||||
import { Reflector } from 'three/examples/jsm/objects/Reflector.js'
|
||||
import { type URDFJoint, type URDFMimicJoint, type URDFRobot } from 'urdf-loader'
|
||||
import { PointerURDFDragControls } from 'urdf-loader/src/URDFDragControls'
|
||||
import { sunCalculator } from './utilities/position-utilities'
|
||||
|
||||
export const addScene = () => new Scene()
|
||||
|
||||
@@ -56,13 +58,15 @@ export default class SceneBuilder {
|
||||
public ground!: Mesh
|
||||
public renderer!: WebGLRenderer
|
||||
public orbit: OrbitControls
|
||||
public callback: (() => void) | undefined
|
||||
public callback: Function | undefined
|
||||
public gridHelper!: GridHelper
|
||||
public model!: URDFRobot
|
||||
public liveStreamTexture!: CanvasTexture
|
||||
private fog!: FogExp2
|
||||
private isLoaded: boolean = false
|
||||
public isDragging: boolean = false
|
||||
highlightMaterial: any
|
||||
sky!: Sky
|
||||
transformControl: TransformControls
|
||||
public modelGroup!: Group
|
||||
|
||||
@@ -85,6 +89,34 @@ export default class SceneBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
public addSky = () => {
|
||||
this.sky = new Sky()
|
||||
this.sky.scale.setScalar(450000)
|
||||
this.scene.add(this.sky)
|
||||
const effectController = {
|
||||
turbidity: 10,
|
||||
rayleigh: 3,
|
||||
mieCoefficient: 0.005,
|
||||
mieDirectionalG: 0.7,
|
||||
elevation: sunCalculator.calculateSunElevation(),
|
||||
azimuth: 200,
|
||||
exposure: this.renderer.toneMappingExposure
|
||||
}
|
||||
const uniforms = this.sky.material.uniforms
|
||||
uniforms['turbidity'].value = effectController.turbidity
|
||||
uniforms['rayleigh'].value = effectController.rayleigh
|
||||
uniforms['mieCoefficient'].value = effectController.mieCoefficient
|
||||
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG
|
||||
this.renderer.toneMappingExposure = 0.5
|
||||
const phi = MathUtils.degToRad(90 - effectController.elevation)
|
||||
const theta = MathUtils.degToRad(effectController.azimuth)
|
||||
const sun = new Vector3()
|
||||
|
||||
sun.setFromSphericalCoords(1, phi, theta)
|
||||
uniforms['sunPosition'].value.copy(sun)
|
||||
return this
|
||||
}
|
||||
|
||||
public addPerspectiveCamera = (options: position) => {
|
||||
this.camera = new PerspectiveCamera()
|
||||
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0)
|
||||
@@ -197,7 +229,7 @@ export default class SceneBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
public addRenderCb = (callback: () => void) => {
|
||||
public addRenderCb = (callback: Function) => {
|
||||
this.callback = callback
|
||||
return this
|
||||
}
|
||||
@@ -243,7 +275,7 @@ export default class SceneBuilder {
|
||||
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'
|
||||
|
||||
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
|
||||
const traverse = (c: Object3D) => {
|
||||
const traverse = (c: any) => {
|
||||
if (c.type === 'Mesh') {
|
||||
if (revert) {
|
||||
c.material = c.__origMaterial
|
||||
@@ -266,9 +298,9 @@ export default class SceneBuilder {
|
||||
traverse(m)
|
||||
}
|
||||
|
||||
public addTransformControls = (model: Object3D) => {
|
||||
public addTransformControls = (model: any) => {
|
||||
this.transformControl = new TransformControls(this.camera, this.renderer.domElement)
|
||||
this.transformControl.addEventListener('dragging-changed', (event: { value: boolean }) => {
|
||||
this.transformControl.addEventListener('dragging-changed', (event: any) => {
|
||||
this.orbit.enabled = !event.value
|
||||
this.isDragging = !event.value
|
||||
})
|
||||
@@ -278,7 +310,7 @@ export default class SceneBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
public addModel = (model: URDFRobot) => {
|
||||
public addModel = (model: any) => {
|
||||
this.modelGroup = new Group()
|
||||
this.modelGroup.add(model)
|
||||
this.model = model
|
||||
@@ -286,7 +318,7 @@ export default class SceneBuilder {
|
||||
return this
|
||||
}
|
||||
|
||||
public addDragControl = (updateAngle: (angles: Record<string, number>) => void) => {
|
||||
public addDragControl = (updateAngle: any) => {
|
||||
const highlightColor = '#FFFFFF'
|
||||
const highlightMaterial = new MeshPhongMaterial({
|
||||
shininess: 10,
|
||||
@@ -302,7 +334,7 @@ export default class SceneBuilder {
|
||||
)
|
||||
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
|
||||
this.setJointValue(joint.name, angle)
|
||||
updateAngle({ [joint.name]: angle })
|
||||
updateAngle(joint.name, angle)
|
||||
}
|
||||
dragControls.onDragStart = () => {
|
||||
this.orbit.enabled = false
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
import { Result } from '$lib/utilities/result'
|
||||
import { browser } from '$app/environment'
|
||||
import { Result } from '$lib/utilities/result';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
class FileService {
|
||||
private dbPromise: Promise<Result<IDBDatabase, string>> | null =
|
||||
browser ? this.openDatabase() : null
|
||||
private dbPromise: Promise<Result<IDBDatabase, string>> | null = browser
|
||||
? this.openDatabase()
|
||||
: null;
|
||||
|
||||
private async openDatabase(): Promise<Result<IDBDatabase, string>> {
|
||||
return new Promise(resolve => {
|
||||
const request = indexedDB.open('fileStorageDB', 1)
|
||||
return new Promise((resolve) => {
|
||||
const request = indexedDB.open('fileStorageDB', 1);
|
||||
|
||||
request.onupgradeneeded = () => {
|
||||
request.result.createObjectStore('files')
|
||||
}
|
||||
request.onsuccess = () => resolve(Result.ok(request.result))
|
||||
request.onerror = () => resolve(Result.err('Error opening database'))
|
||||
})
|
||||
request.result.createObjectStore('files');
|
||||
};
|
||||
request.onsuccess = () => resolve(Result.ok(request.result));
|
||||
request.onerror = () => resolve(Result.err('Error opening database'));
|
||||
});
|
||||
}
|
||||
|
||||
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
|
||||
if (!browser || !this.dbPromise)
|
||||
return Result.err('Not running in browser or DB not initialized')
|
||||
const dbResult = await this.dbPromise
|
||||
if (dbResult.isErr()) return Result.err('Database not initialized')
|
||||
const store = dbResult.inner.transaction('files', mode).objectStore('files')
|
||||
return Result.ok(store)
|
||||
return Result.err('Not running in browser or DB not initialized');
|
||||
const dbResult = await this.dbPromise;
|
||||
if (dbResult.isErr()) return Result.err('Database not initialized');
|
||||
const store = dbResult.inner.transaction('files', mode).objectStore('files');
|
||||
return Result.ok(store);
|
||||
}
|
||||
|
||||
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
|
||||
const storeResult = await this.getStore('readwrite')
|
||||
if (storeResult.isErr()) return Result.err('Failed to access store')
|
||||
const storeResult = await this.getStore('readwrite');
|
||||
if (storeResult.isErr()) return Result.err('Failed to access store');
|
||||
|
||||
return new Promise(resolve => {
|
||||
const request = storeResult.inner.put(file, key)
|
||||
request.onsuccess = () => resolve(Result.ok(request.result))
|
||||
request.onerror = () => resolve(Result.err('Failed to save file'))
|
||||
})
|
||||
return new Promise((resolve) => {
|
||||
const request = storeResult.inner.put(file, key);
|
||||
request.onsuccess = () => resolve(Result.ok(request.result));
|
||||
request.onerror = () => resolve(Result.err('Failed to save file'));
|
||||
});
|
||||
}
|
||||
|
||||
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
|
||||
const storeResult = await this.getStore('readonly')
|
||||
if (storeResult.isErr()) return Result.err('Failed to access store')
|
||||
const storeResult = await this.getStore('readonly');
|
||||
if (storeResult.isErr()) return Result.err('Failed to access store');
|
||||
|
||||
return new Promise(resolve => {
|
||||
const request = storeResult.inner.get(key)
|
||||
return new Promise((resolve) => {
|
||||
const request = storeResult.inner.get(key);
|
||||
request.onsuccess = () =>
|
||||
resolve(request.result ? Result.ok(request.result) : Result.err('File not found'))
|
||||
request.onerror = () => resolve(Result.err('Failed to retrieve file'))
|
||||
})
|
||||
resolve(request.result ? Result.ok(request.result) : Result.err('File not found'));
|
||||
request.onerror = () => resolve(Result.err('Failed to retrieve file'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default browser ? new FileService() : null
|
||||
export default browser ? new FileService() : null;
|
||||
@@ -1,2 +1,2 @@
|
||||
export { default as fileService } from './file-service'
|
||||
export { default as resultService } from './result-service'
|
||||
export { default as fileService } from './file-service';
|
||||
export { default as resultService } from './result-service';
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { errorLogs, latestErrorLog } from '$lib/stores'
|
||||
import type { Result } from '$lib/utilities'
|
||||
import { errorLogs, latestErrorLog } from '$lib/stores';
|
||||
import type { Result } from '$lib/utilities';
|
||||
|
||||
class ResultService {
|
||||
public handleResult(result: Result<unknown, string>, tag?: string) {
|
||||
if (result.isErr()) {
|
||||
const errorLogEntry = { tag, message: result.inner, exception: result.exception }
|
||||
latestErrorLog.set(errorLogEntry)
|
||||
errorLogs.update(entries => {
|
||||
entries.push(errorLogEntry)
|
||||
return entries
|
||||
})
|
||||
const errorLogEntry = { tag, message: result.inner, exception: result.exception };
|
||||
latestErrorLog.set(errorLogEntry);
|
||||
errorLogs.update((entries) => {
|
||||
entries.push(errorLogEntry);
|
||||
return entries;
|
||||
});
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export default new ResultService()
|
||||
export default new ResultService();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type Analytics } from '$lib/types/models'
|
||||
import { writable } from 'svelte/store'
|
||||
import { type Analytics } from '$lib/types/models';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const analytics_data = {
|
||||
let analytics_data = {
|
||||
uptime: <number[]>[],
|
||||
free_heap: <number[]>[],
|
||||
total_heap: <number[]>[],
|
||||
@@ -14,22 +14,20 @@ const analytics_data = {
|
||||
cpu0_usage: <number[]>[],
|
||||
cpu1_usage: <number[]>[],
|
||||
cpu_usage: <number[]>[]
|
||||
}
|
||||
};
|
||||
|
||||
const maxAnalyticsData = 100
|
||||
const maxAnalyticsData = 100;
|
||||
|
||||
function createAnalytics() {
|
||||
const { subscribe, update } = writable(analytics_data)
|
||||
const { subscribe, update } = writable(analytics_data);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
addData: (content: Analytics) => {
|
||||
update(analytics_data => ({
|
||||
update((analytics_data) => ({
|
||||
...analytics_data,
|
||||
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
|
||||
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(
|
||||
-maxAnalyticsData
|
||||
),
|
||||
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(-maxAnalyticsData),
|
||||
total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
|
||||
-maxAnalyticsData
|
||||
),
|
||||
@@ -37,33 +35,21 @@ function createAnalytics() {
|
||||
...analytics_data.used_heap,
|
||||
(content.total_heap - content.free_heap) / 1000
|
||||
].slice(-maxAnalyticsData),
|
||||
min_free_heap: [
|
||||
...analytics_data.min_free_heap,
|
||||
content.min_free_heap / 1000
|
||||
].slice(-maxAnalyticsData),
|
||||
max_alloc_heap: [
|
||||
...analytics_data.max_alloc_heap,
|
||||
content.max_alloc_heap / 1000
|
||||
].slice(-maxAnalyticsData),
|
||||
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(
|
||||
min_free_heap: [...analytics_data.min_free_heap, content.min_free_heap / 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(
|
||||
max_alloc_heap: [...analytics_data.max_alloc_heap, content.max_alloc_heap / 1000].slice(
|
||||
-maxAnalyticsData
|
||||
),
|
||||
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(-maxAnalyticsData),
|
||||
fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(-maxAnalyticsData),
|
||||
core_temp: [...analytics_data.core_temp, content.core_temp].slice(-maxAnalyticsData),
|
||||
cpu0_usage: [...analytics_data.cpu0_usage, content.cpu0_usage].slice(-maxAnalyticsData),
|
||||
cpu1_usage: [...analytics_data.cpu1_usage, content.cpu1_usage].slice(-maxAnalyticsData),
|
||||
cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData)
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const analytics = createAnalytics()
|
||||
export const analytics = createAnalytics();
|
||||
|
||||
@@ -1,47 +1,39 @@
|
||||
import { persistentStore } from '$lib/utilities'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
import { get, type Writable } from 'svelte/store';
|
||||
|
||||
import Visualization from '$lib/components/Visualization.svelte'
|
||||
import Stream from '$lib/components/Stream.svelte'
|
||||
import ChartWidget from '$lib/components/widget/ChartWidget.svelte'
|
||||
import Visualization from '$lib/components/Visualization.svelte';
|
||||
import Stream from '$lib/components/Stream.svelte';
|
||||
import ChartWidget from '$lib/components/widget/ChartWidget.svelte';
|
||||
|
||||
export interface WidgetConfig {
|
||||
id: string | number
|
||||
component: keyof typeof WidgetComponents
|
||||
props?: Record<string, unknown>
|
||||
id: string | number;
|
||||
component: keyof typeof WidgetComponents;
|
||||
props?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface WidgetContainerConfig {
|
||||
id: string | number
|
||||
layout?: 'row' | 'column' | 'wrap'
|
||||
header?: string
|
||||
widgets: Array<WidgetConfig | WidgetContainerConfig>
|
||||
id: string | number;
|
||||
layout?: 'row' | 'column' | 'wrap';
|
||||
header?: string;
|
||||
widgets: Array<WidgetConfig | WidgetContainerConfig>;
|
||||
}
|
||||
|
||||
export const isWidgetConfig = (
|
||||
widget: WidgetConfig | WidgetContainerConfig
|
||||
): widget is WidgetConfig => 'component' in widget
|
||||
): widget is WidgetConfig => 'component' in widget;
|
||||
|
||||
export const WidgetComponents = {
|
||||
Visualization,
|
||||
Stream,
|
||||
ChartWidget
|
||||
}
|
||||
};
|
||||
|
||||
interface View {
|
||||
name: string
|
||||
content: WidgetContainerConfig
|
||||
name: string;
|
||||
content: WidgetContainerConfig;
|
||||
}
|
||||
|
||||
const defaultViews: View[] = [
|
||||
{
|
||||
name: '3D representation',
|
||||
content: {
|
||||
id: 'root',
|
||||
layout: 'column',
|
||||
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Stream',
|
||||
content: {
|
||||
@@ -50,6 +42,14 @@ const defaultViews: View[] = [
|
||||
widgets: [{ id: 2, component: 'Stream' }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '3D representation',
|
||||
content: {
|
||||
id: 'root',
|
||||
layout: 'column',
|
||||
widgets: [{ id: 2, component: 'Visualization', props: { debug: true } }]
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Split screen',
|
||||
content: {
|
||||
@@ -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);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { notifications } from '$lib/components/toasts/notifications'
|
||||
import Kinematic from '$lib/kinematic'
|
||||
import { persistentStore } from '$lib/utilities'
|
||||
import { derived, type Writable } from 'svelte/store'
|
||||
import { resolve } from '$app/paths'
|
||||
import { base } from '$app/paths'
|
||||
|
||||
let featureFlagsStore: Writable<Record<string, boolean | string>>
|
||||
|
||||
@@ -22,12 +22,10 @@ export function useFeatureFlags() {
|
||||
return featureFlagsStore
|
||||
}
|
||||
|
||||
const base = resolve('/')
|
||||
|
||||
export const variants = {
|
||||
SPOTMICRO_ESP32: {
|
||||
model: `${base}spot_micro.urdf.xacro`,
|
||||
stl: `${base}stl.zip`,
|
||||
model: `${base}/spot_micro.urdf.xacro`,
|
||||
stl: `${base}/stl.zip`,
|
||||
kinematics: {
|
||||
coxa: 60.5 / 100,
|
||||
coxa_offset: 10 / 100,
|
||||
@@ -38,8 +36,8 @@ export const variants = {
|
||||
}
|
||||
},
|
||||
SPOTMICRO_YERTLE: {
|
||||
model: `${base}yertle.URDF`,
|
||||
stl: `${base}URDF.zip`,
|
||||
model: `${base}/yertle.URDF`,
|
||||
stl: `${base}/URDF.zip`,
|
||||
kinematics: {
|
||||
coxa: 35 / 100,
|
||||
coxa_offset: 0 / 100,
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const isFullscreen = writable(false)
|
||||
export const isFullscreen = writable(false);
|
||||
|
||||
export function toggleFullscreen() {
|
||||
isFullscreen.update(state => {
|
||||
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen()
|
||||
return !state
|
||||
})
|
||||
isFullscreen.update((state) => {
|
||||
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen();
|
||||
return !state;
|
||||
});
|
||||
}
|
||||
|
||||
export function enterFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen()
|
||||
isFullscreen.set(true)
|
||||
document.documentElement.requestFullscreen();
|
||||
isFullscreen.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
export function exitFullscreen() {
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen()
|
||||
isFullscreen.set(false)
|
||||
document.exitFullscreen();
|
||||
isFullscreen.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,83 +5,43 @@ export type GamepadState = {
|
||||
gamepads: Gamepad[]
|
||||
}
|
||||
|
||||
const DEADZONE = 0.15
|
||||
const dz = (x: number) => {
|
||||
const a = Math.abs(x)
|
||||
if (a < DEADZONE) return 0
|
||||
return ((a - DEADZONE) / (1 - DEADZONE)) * Math.sign(x)
|
||||
}
|
||||
|
||||
let raf = 0
|
||||
let running = false
|
||||
|
||||
export const gamepads = readable<GamepadState>({ available: false, gamepads: [] }, set => {
|
||||
const update = () => {
|
||||
const pads = navigator.getGamepads?.() ?? []
|
||||
const list = Array.from(pads)
|
||||
.map(p => p || null)
|
||||
.filter(Boolean) as Gamepad[]
|
||||
set({ available: 'getGamepads' in navigator, gamepads: list })
|
||||
const hasGamepadAPI = 'getGamepads' in navigator
|
||||
if (!hasGamepadAPI) {
|
||||
set({ available: false, gamepads: [] })
|
||||
return
|
||||
}
|
||||
|
||||
const gps = navigator.getGamepads?.() ?? []
|
||||
const validGamepads = gps.filter(Boolean) as Gamepad[]
|
||||
set({
|
||||
available: true,
|
||||
gamepads: validGamepads
|
||||
})
|
||||
raf = requestAnimationFrame(update)
|
||||
}
|
||||
|
||||
const onConnect = () => update()
|
||||
const onDisconnect = () => update()
|
||||
const onVis = () => {
|
||||
if (document.hidden) {
|
||||
running = false
|
||||
cancelAnimationFrame(raf)
|
||||
} else if (!running) {
|
||||
running = true
|
||||
raf = requestAnimationFrame(update)
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('gamepadconnected', onConnect)
|
||||
window.addEventListener('gamepaddisconnected', onDisconnect)
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
|
||||
running = true
|
||||
raf = requestAnimationFrame(update)
|
||||
window.addEventListener('gamepadconnected', update)
|
||||
window.addEventListener('gamepaddisconnected', update)
|
||||
let raf = requestAnimationFrame(update)
|
||||
|
||||
return () => {
|
||||
running = false
|
||||
cancelAnimationFrame(raf)
|
||||
window.removeEventListener('gamepadconnected', onConnect)
|
||||
window.removeEventListener('gamepaddisconnected', onDisconnect)
|
||||
document.removeEventListener('visibilitychange', onVis)
|
||||
window.removeEventListener('gamepadconnected', update)
|
||||
window.removeEventListener('gamepaddisconnected', update)
|
||||
}
|
||||
})
|
||||
|
||||
export const gamepad = derived(gamepads, s =>
|
||||
s.available && s.gamepads.length ? s.gamepads[0] : null
|
||||
export const gamepad = derived(gamepads, $gamepads =>
|
||||
$gamepads.available && $gamepads.gamepads.length > 0 ? $gamepads.gamepads[0] : null
|
||||
)
|
||||
|
||||
export const hasGamepad = derived(gamepads, s => s.available && s.gamepads.length > 0)
|
||||
export const gamepadAxes = derived(gamepad, $gamepad => $gamepad?.axes ?? [0, 0, 0, 0])
|
||||
|
||||
export const gamepadAxes = derived(gamepad, g => (g ? g.axes.map(dz) : [0, 0, 0, 0]))
|
||||
export const gamepadButtons = derived(gamepad, $gamepad => $gamepad?.buttons ?? [])
|
||||
|
||||
type ButtonEdge = { pressed: boolean; value: number; justPressed: boolean; justReleased: boolean }
|
||||
const prev = new Map<number, { pressed: boolean; value: number }[]>()
|
||||
|
||||
export const gamepadButtons = derived(gamepad, g => g?.buttons ?? [])
|
||||
|
||||
export const gamepadButtonsEdges = derived(gamepad, g => {
|
||||
if (!g) return [] as ButtonEdge[]
|
||||
const p = prev.get(g.index) || []
|
||||
const out = g.buttons.map((b, i): ButtonEdge => {
|
||||
const pr = p[i] || { pressed: false, value: 0 }
|
||||
const pressed = !!b.pressed || b.value > 0.5
|
||||
return {
|
||||
pressed,
|
||||
value: b.value,
|
||||
justPressed: pressed && !pr.pressed,
|
||||
justReleased: !pressed && pr.pressed
|
||||
}
|
||||
})
|
||||
prev.set(
|
||||
g.index,
|
||||
out.map(x => ({ pressed: x.pressed, value: x.value }))
|
||||
)
|
||||
return out
|
||||
})
|
||||
export const hasGamepad = derived(
|
||||
gamepads,
|
||||
$gamepads => $gamepads.available && $gamepads.gamepads.length > 0
|
||||
)
|
||||
|
||||
+13
-26
@@ -1,7 +1,7 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import type { IMUMsg } from '$lib/types/models'
|
||||
import { writable } from 'svelte/store';
|
||||
import type { IMU } from '$lib/types/models';
|
||||
|
||||
const maxIMUData = 100
|
||||
const maxIMUData = 100;
|
||||
|
||||
export const imu = (() => {
|
||||
const { subscribe, update } = writable({
|
||||
@@ -12,29 +12,16 @@ export const imu = (() => {
|
||||
altitude: [] as number[],
|
||||
pressure: [] as number[],
|
||||
bmp_temp: [] as number[]
|
||||
})
|
||||
});
|
||||
|
||||
const addData = (content: IMUMsg) => {
|
||||
const addData = (content: IMU) => {
|
||||
update(data => {
|
||||
if (content.imu && content.imu[4]) {
|
||||
data.x = [...data.x, content.imu[0]].slice(-maxIMUData)
|
||||
data.y = [...data.y, content.imu[1]].slice(-maxIMUData)
|
||||
data.z = [...data.z, content.imu[2]].slice(-maxIMUData)
|
||||
}
|
||||
(Object.keys(content) as (keyof IMU)[]).forEach(key => {
|
||||
data[key] = [...data[key], content[key]].slice(-maxIMUData);
|
||||
});
|
||||
return data;
|
||||
});
|
||||
};
|
||||
|
||||
if (content.mag && content.mag[4]) {
|
||||
data.heading = [...data.heading, content.mag[3]].slice(-maxIMUData)
|
||||
}
|
||||
|
||||
if (content.bmp && content.bmp[3]) {
|
||||
data.pressure = [...data.pressure, content.bmp[0]].slice(-maxIMUData)
|
||||
data.altitude = [...data.altitude, content.bmp[1]].slice(-maxIMUData)
|
||||
data.bmp_temp = [...data.bmp_temp, content.bmp[2]].slice(-maxIMUData)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
}
|
||||
|
||||
return { subscribe, addData }
|
||||
})()
|
||||
return { subscribe, addData };
|
||||
})();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export * from './socket-store'
|
||||
export * from './logging-store'
|
||||
export * from './model-store'
|
||||
export * from './socket'
|
||||
export * from './fullscreen'
|
||||
export * from './telemetry'
|
||||
export * from './analytics'
|
||||
export * from './featureFlags'
|
||||
export * from './location-store'
|
||||
export * from './socket-store';
|
||||
export * from './logging-store';
|
||||
export * from './model-store';
|
||||
export * from './socket';
|
||||
export * from './fullscreen';
|
||||
export * from './telemetry';
|
||||
export * from './analytics';
|
||||
export * from './featureFlags';
|
||||
export * from './location-store';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { persistentStore } from '$lib/utilities'
|
||||
import { writable } from 'svelte/store'
|
||||
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
|
||||
import { persistentStore } from '$lib/utilities';
|
||||
import { writable } from 'svelte/store';
|
||||
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public';
|
||||
|
||||
export const apiLocation =
|
||||
PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '')
|
||||
export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export interface errorLog {
|
||||
message: unknown
|
||||
tag?: string
|
||||
exception?: unknown
|
||||
message: unknown;
|
||||
tag?: string;
|
||||
exception?: unknown;
|
||||
}
|
||||
|
||||
export const latestErrorLog: Writable<errorLog> = writable()
|
||||
export const latestErrorLog: Writable<errorLog> = writable();
|
||||
|
||||
export const errorLogs: Writable<errorLog[]> = writable([])
|
||||
export const errorLogs: Writable<errorLog[]> = writable([]);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { writable, type Writable } from 'svelte/store'
|
||||
import { type angles } from '$lib/types/models'
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
import { type angles } from '$lib/types/models';
|
||||
|
||||
export const servoAnglesOut: Writable<number[]> = writable([
|
||||
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
|
||||
])
|
||||
]);
|
||||
export const servoAngles: Writable<number[]> = writable([
|
||||
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
|
||||
])
|
||||
export const logs = writable([] as string[])
|
||||
export const mpu = writable({ heading: 0 })
|
||||
export const sonar = writable([0, 0])
|
||||
export const distances = writable({})
|
||||
]);
|
||||
export const logs = writable([] as string[]);
|
||||
export const mpu = writable({ heading: 0 });
|
||||
export const sonar = writable([0, 0]);
|
||||
export const distances = writable({});
|
||||
|
||||
export interface socketDataCollection {
|
||||
angles: Writable<angles>
|
||||
logs: Writable<string[]>
|
||||
mpu: Writable<unknown>
|
||||
distances: Writable<unknown>
|
||||
angles: Writable<angles>;
|
||||
logs: Writable<string[]>;
|
||||
mpu: Writable<unknown>;
|
||||
distances: Writable<unknown>;
|
||||
}
|
||||
|
||||
export const socketData = {
|
||||
@@ -24,4 +24,4 @@ export const socketData = {
|
||||
logs,
|
||||
mpu,
|
||||
distances
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,135 +1,135 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { encode, decode } from '@msgpack/msgpack'
|
||||
import { writable } from 'svelte/store';
|
||||
import { encode, decode } from '@msgpack/msgpack';
|
||||
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
|
||||
type SocketEvent = (typeof socketEvents)[number]
|
||||
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const;
|
||||
type SocketEvent = (typeof socketEvents)[number];
|
||||
|
||||
type SocketMessage = [number, string?, unknown?]
|
||||
type SocketMessage = [number, string?, unknown?];
|
||||
|
||||
let useBinary = false
|
||||
let useBinary = false;
|
||||
|
||||
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
|
||||
useBinary = data instanceof ArrayBuffer
|
||||
useBinary = data instanceof ArrayBuffer;
|
||||
|
||||
try {
|
||||
if (useBinary) {
|
||||
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage
|
||||
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage;
|
||||
}
|
||||
return JSON.parse(data as string)
|
||||
return JSON.parse(data as string);
|
||||
} 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) => {
|
||||
try {
|
||||
return useBinary ? encode(data) : JSON.stringify(data)
|
||||
return useBinary ? encode(data) : JSON.stringify(data);
|
||||
} catch (error) {
|
||||
console.error(`Could not encode data: ${data} - ${error}`)
|
||||
console.error(`Could not encode data: ${data} - ${error}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function createWebSocket() {
|
||||
const listeners = new Map<string, Set<(data?: unknown) => void>>()
|
||||
const { subscribe, set } = writable(false)
|
||||
const reconnectTimeoutTime = 5000
|
||||
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
|
||||
let reconnectTimeoutId: ReturnType<typeof setTimeout>
|
||||
let ws: WebSocket
|
||||
let socketUrl: string | URL
|
||||
const listeners = new Map<string, Set<(data?: unknown) => void>>();
|
||||
const { subscribe, set } = writable(false);
|
||||
const reconnectTimeoutTime = 5000;
|
||||
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>;
|
||||
let reconnectTimeoutId: ReturnType<typeof setTimeout>;
|
||||
let ws: WebSocket;
|
||||
let socketUrl: string | URL;
|
||||
|
||||
function init(url: string | URL) {
|
||||
socketUrl = url
|
||||
connect()
|
||||
socketUrl = url;
|
||||
connect();
|
||||
}
|
||||
|
||||
function disconnect(reason: SocketEvent, event?: Event) {
|
||||
ws.close()
|
||||
set(false)
|
||||
clearTimeout(unresponsiveTimeoutId)
|
||||
clearTimeout(reconnectTimeoutId)
|
||||
listeners.get(reason)?.forEach(listener => listener(event))
|
||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
|
||||
ws.close();
|
||||
set(false);
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get(reason)?.forEach(listener => listener(event));
|
||||
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function connect() {
|
||||
ws = new WebSocket(socketUrl)
|
||||
ws.binaryType = 'arraybuffer'
|
||||
ws = new WebSocket(socketUrl);
|
||||
ws.binaryType = 'arraybuffer';
|
||||
ws.onopen = ev => {
|
||||
ping()
|
||||
useBinary = true
|
||||
ping()
|
||||
set(true)
|
||||
clearTimeout(reconnectTimeoutId)
|
||||
listeners.get('open')?.forEach(listener => listener(ev))
|
||||
ping();
|
||||
useBinary = true;
|
||||
ping();
|
||||
set(true);
|
||||
clearTimeout(reconnectTimeoutId);
|
||||
listeners.get('open')?.forEach(listener => listener(ev));
|
||||
for (const event of listeners.keys()) {
|
||||
if (socketEvents.includes(event as SocketEvent)) continue
|
||||
subscribeToEvent(event)
|
||||
}
|
||||
if (socketEvents.includes(event as SocketEvent)) continue;
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
};
|
||||
ws.onmessage = frame => {
|
||||
resetUnresponsiveCheck()
|
||||
const message = decodeMessage(frame.data)
|
||||
if (!message) return
|
||||
const [, event, payload = undefined] = message
|
||||
if (event) listeners.get(event)?.forEach(listener => listener(payload))
|
||||
}
|
||||
ws.onerror = ev => disconnect('error', ev)
|
||||
ws.onclose = ev => disconnect('close', ev)
|
||||
resetUnresponsiveCheck();
|
||||
const message = decodeMessage(frame.data);
|
||||
if (!message) return;
|
||||
const [, event, payload = undefined] = message;
|
||||
if (event) listeners.get(event)?.forEach(listener => listener(payload));
|
||||
};
|
||||
ws.onerror = ev => disconnect('error', ev);
|
||||
ws.onclose = ev => disconnect('close', ev);
|
||||
}
|
||||
|
||||
function unsubscribe(event: string, listener?: (data: unknown) => void) {
|
||||
const eventListeners = listeners.get(event)
|
||||
if (!eventListeners) return
|
||||
const eventListeners = listeners.get(event);
|
||||
if (!eventListeners) return;
|
||||
|
||||
if (!eventListeners.size) {
|
||||
unsubscribeToEvent(event)
|
||||
unsubscribeToEvent(event);
|
||||
}
|
||||
if (listener) {
|
||||
eventListeners?.delete(listener)
|
||||
eventListeners?.delete(listener);
|
||||
} else {
|
||||
listeners.delete(event)
|
||||
listeners.delete(event);
|
||||
}
|
||||
}
|
||||
|
||||
function resetUnresponsiveCheck() {
|
||||
clearTimeout(unresponsiveTimeoutId)
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
|
||||
clearTimeout(unresponsiveTimeoutId);
|
||||
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime);
|
||||
}
|
||||
|
||||
function sendEvent(event: string, data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
send([2, event, data])
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([2, event, data]);
|
||||
}
|
||||
|
||||
function unsubscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
send([1, event])
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([1, event]);
|
||||
}
|
||||
|
||||
function subscribeToEvent(event: string) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
send([0, event])
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
send([0, event]);
|
||||
}
|
||||
|
||||
function send(data: unknown) {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
const serialized = encodeMessage(data)
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
||||
const serialized = encodeMessage(data);
|
||||
if (!serialized) {
|
||||
console.error('Could not serialize data:', data)
|
||||
return
|
||||
console.error('Could not serialize data:', data);
|
||||
return;
|
||||
}
|
||||
ws.send(serialized)
|
||||
ws.send(serialized);
|
||||
}
|
||||
|
||||
function ping() {
|
||||
const serialized = encodeMessage([4])
|
||||
const serialized = encodeMessage([4]);
|
||||
if (!serialized) {
|
||||
console.error('Could not serialize message')
|
||||
return
|
||||
console.error('Could not serialize message');
|
||||
return;
|
||||
}
|
||||
ws.send(serialized)
|
||||
ws.send(serialized);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -137,24 +137,24 @@ function createWebSocket() {
|
||||
sendEvent,
|
||||
init,
|
||||
on: <T>(event: string, listener: (data: T) => void): (() => void) => {
|
||||
let eventListeners = listeners.get(event)
|
||||
let eventListeners = listeners.get(event);
|
||||
if (!eventListeners) {
|
||||
if (!socketEvents.includes(event as SocketEvent)) {
|
||||
subscribeToEvent(event)
|
||||
subscribeToEvent(event);
|
||||
}
|
||||
eventListeners = new Set()
|
||||
listeners.set(event, eventListeners)
|
||||
eventListeners = new Set();
|
||||
listeners.set(event, eventListeners);
|
||||
}
|
||||
eventListeners.add(listener as (data: unknown) => void)
|
||||
eventListeners.add(listener as (data: unknown) => void);
|
||||
|
||||
return () => {
|
||||
unsubscribe(event, listener as (data: unknown) => void)
|
||||
}
|
||||
unsubscribe(event, listener as (data: unknown) => 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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { DownloadOTA } from '$lib/types/models'
|
||||
import { writable } from 'svelte/store'
|
||||
import type { DownloadOTA } from '$lib/types/models';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
const telemetry_data = {
|
||||
let telemetry_data = {
|
||||
rssi: {
|
||||
rssi: 0
|
||||
},
|
||||
@@ -10,10 +10,10 @@ const telemetry_data = {
|
||||
progress: 0,
|
||||
error: ''
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function createTelemetry() {
|
||||
const { subscribe, update } = writable(telemetry_data)
|
||||
const { subscribe, set, update } = writable(telemetry_data);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
@@ -21,15 +21,15 @@ function createTelemetry() {
|
||||
update(telemetry_data => ({
|
||||
...telemetry_data,
|
||||
rssi: { rssi: data }
|
||||
}))
|
||||
}));
|
||||
},
|
||||
setDownloadOTA: (data: DownloadOTA) => {
|
||||
update(telemetry_data => ({
|
||||
...telemetry_data,
|
||||
download_ota: { status: data.status, progress: data.progress, error: data.error }
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const telemetry = createTelemetry()
|
||||
export const telemetry = createTelemetry();
|
||||
|
||||
Vendored
+15
-15
@@ -1,17 +1,17 @@
|
||||
declare module 'three/src/math/MathUtils' {
|
||||
export function generateUUID(): string
|
||||
export function clamp(value: number, min: number, max: number): number
|
||||
export function euclideanModulo(n: number, m: number): number
|
||||
export function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number
|
||||
export function lerp(x: number, y: number, t: number): number
|
||||
export function smoothstep(x: number, min: number, max: number): number
|
||||
export function smootherstep(x: number, min: number, max: number): number
|
||||
export function randInt(low: number, high: number): number
|
||||
export function randFloat(low: number, high: number): number
|
||||
export function randFloatSpread(range: number): number
|
||||
export function degToRad(degrees: number): number
|
||||
export function radToDeg(radians: number): number
|
||||
export function isPowerOfTwo(value: number): boolean
|
||||
export function ceilPowerOfTwo(value: number): number
|
||||
export function floorPowerOfTwo(value: number): number
|
||||
export function generateUUID(): string;
|
||||
export function clamp(value: number, min: number, max: number): number;
|
||||
export function euclideanModulo(n: number, m: number): number;
|
||||
export function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number;
|
||||
export function lerp(x: number, y: number, t: number): number;
|
||||
export function smoothstep(x: number, min: number, max: number): number;
|
||||
export function smootherstep(x: number, min: number, max: number): number;
|
||||
export function randInt(low: number, high: number): number;
|
||||
export function randFloat(low: number, high: number): number;
|
||||
export function randFloatSpread(range: number): number;
|
||||
export function degToRad(degrees: number): number;
|
||||
export function radToDeg(radians: number): number;
|
||||
export function isPowerOfTwo(value: number): boolean;
|
||||
export function ceilPowerOfTwo(value: number): number;
|
||||
export function floorPowerOfTwo(value: number): number;
|
||||
}
|
||||
|
||||
@@ -154,12 +154,6 @@ export type IMU = {
|
||||
pressure: number
|
||||
}
|
||||
|
||||
export type IMUMsg = {
|
||||
imu: [number, number, number, number, boolean]
|
||||
mag: [number, number, number, number, boolean]
|
||||
bmp: [number, number, number, boolean]
|
||||
}
|
||||
|
||||
export interface I2CDevice {
|
||||
address: number
|
||||
part_number: string
|
||||
|
||||
Vendored
+9
-9
@@ -1,14 +1,14 @@
|
||||
declare module 'uzip' {
|
||||
interface UZIP {
|
||||
parse(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
|
||||
compress(data: Record<string, Uint8Array>): Uint8Array | ArrayBuffer
|
||||
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
|
||||
decompress(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
|
||||
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
|
||||
encode(data: Record<string, Uint8Array>): Uint8Array | ArrayBuffer
|
||||
decode(data: Uint8Array | ArrayBuffer): Record<string, Uint8Array>
|
||||
parse(data: Uint8Array | ArrayBuffer): any;
|
||||
compress(data: any): Uint8Array | ArrayBuffer;
|
||||
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer;
|
||||
decompress(data: Uint8Array | ArrayBuffer): any;
|
||||
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer;
|
||||
encode(data: any): Uint8Array | ArrayBuffer;
|
||||
decode(data: Uint8Array | ArrayBuffer): any;
|
||||
}
|
||||
|
||||
const uzip: UZIP
|
||||
export default uzip
|
||||
const uzip: UZIP;
|
||||
export default uzip;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
export class throttler {
|
||||
private _throttlePause: boolean
|
||||
private _throttlePause: boolean;
|
||||
constructor() {
|
||||
this._throttlePause = false
|
||||
this._throttlePause = false;
|
||||
}
|
||||
throttle = (callback: () => void, time: number) => {
|
||||
if (this._throttlePause) return
|
||||
throttle = (callback: Function, time: number) => {
|
||||
if (this._throttlePause) return;
|
||||
|
||||
this._throttlePause = true
|
||||
this._throttlePause = true;
|
||||
setTimeout(() => {
|
||||
callback()
|
||||
this._throttlePause = false
|
||||
}, time)
|
||||
}
|
||||
callback();
|
||||
this._throttlePause = false;
|
||||
}, time);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const daisyColor = (name: string, opacity: number = 100) => {
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
||||
if (opacity >= 100) return color
|
||||
const alpha = Math.min(Math.max(opacity, 0), 100) / 100
|
||||
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`
|
||||
}
|
||||
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
||||
if (opacity >= 100) return color;
|
||||
const alpha = Math.min(Math.max(opacity, 0), 100) / 100;
|
||||
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`;
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export * from './result'
|
||||
export * from './string-utilities'
|
||||
export * from './svelte-utilities'
|
||||
export * from './math-utilities'
|
||||
export * from './buffer-utilities'
|
||||
export * from './model-utilities'
|
||||
export * from './string-utilities'
|
||||
export * from './color-utilities'
|
||||
export * from './result';
|
||||
export * from './string-utilities';
|
||||
export * from './svelte-utilities';
|
||||
export * from './math-utilities';
|
||||
export * from './buffer-utilities';
|
||||
export * from './model-utilities';
|
||||
export * from './position-utilities';
|
||||
export * from './string-utilities';
|
||||
export * from './color-utilities';
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
export const toUint8 = (number: number, min: number, max: number) => {
|
||||
number = Math.max(min, Math.min(max, number))
|
||||
const scaled = ((number - min) / (max - min)) * 255
|
||||
return Math.round(scaled) & 0xff
|
||||
}
|
||||
number = Math.max(min, Math.min(max, number));
|
||||
let scaled = ((number - min) / (max - min)) * 255;
|
||||
return Math.round(scaled) & 0xff;
|
||||
};
|
||||
|
||||
export const toInt8 = (number: number, min: number, max: number) => {
|
||||
number = Math.max(min, Math.min(max, number))
|
||||
const scaled = ((number - min) / (max - min)) * 255 - 128
|
||||
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0
|
||||
}
|
||||
number = Math.max(min, Math.min(max, number));
|
||||
let scaled = ((number - min) / (max - min)) * 255 - 128;
|
||||
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0;
|
||||
};
|
||||
|
||||
export const fromInt8 = (int8: number, min: number, max: number) => {
|
||||
int8 = Math.max(-128, Math.min(127, int8))
|
||||
const scaled = (int8 + 128) / 255
|
||||
const number = scaled * (max - min) + min
|
||||
return number
|
||||
}
|
||||
int8 = Math.max(-128, Math.min(127, int8));
|
||||
const scaled = (int8 + 128) / 255;
|
||||
const number = scaled * (max - min) + min;
|
||||
return number;
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { currentVariant, jointNames, model } from '$lib/stores'
|
||||
import uzip from 'uzip'
|
||||
import { fileService } from '$lib/services'
|
||||
import { get } from 'svelte/store'
|
||||
import { resolve } from '$app/paths'
|
||||
|
||||
let model_xml: XMLDocument
|
||||
|
||||
@@ -28,18 +27,16 @@ export const cacheModelFiles = async () => {
|
||||
const files = uzip.parse(await data.arrayBuffer())
|
||||
|
||||
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
|
||||
const normalizedPath = path.startsWith('/') ? path : '/' + path
|
||||
const resolvedUrl = resolve(normalizedPath as any)
|
||||
fileService?.saveFile(resolvedUrl, data)
|
||||
fileService?.saveFile(normalizedPath, data)
|
||||
const url = new URL(path, window.location.href)
|
||||
fileService?.saveFile(url.toString(), data)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[]], string>> => {
|
||||
const urdfLoader = new URDFLoader()
|
||||
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
|
||||
|
||||
let xml =
|
||||
url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
|
||||
let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
|
||||
|
||||
if (typeof xml === 'string') {
|
||||
xml = new window.DOMParser().parseFromString(xml, 'text/xml')
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
class SunCalculator {
|
||||
calculateSunElevation(lat: number = 55, lon: number = 12) {
|
||||
const now = new Date();
|
||||
const JD = this.getJulianDate(now);
|
||||
const solarDec = this.getSolarDeclination(JD);
|
||||
const solarTime = this.getSolarTime(now, lon);
|
||||
|
||||
const hourAngle = (solarTime - 12) * 15;
|
||||
const elevation = Math.asin(
|
||||
Math.sin(this.degToRad(lat)) * Math.sin(solarDec) +
|
||||
Math.cos(this.degToRad(lat)) * Math.cos(solarDec) * Math.cos(this.degToRad(hourAngle))
|
||||
);
|
||||
|
||||
return this.radToDeg(elevation);
|
||||
}
|
||||
|
||||
getJulianDate(date: Date) {
|
||||
const Y = date.getUTCFullYear();
|
||||
const M = date.getUTCMonth() + 1;
|
||||
const D =
|
||||
date.getUTCDate() +
|
||||
date.getUTCHours() / 24 +
|
||||
date.getUTCMinutes() / 1440 +
|
||||
date.getUTCSeconds() / 86400;
|
||||
const A = Math.floor((14 - M) / 12);
|
||||
const Y1 = Y + 4800 - A;
|
||||
const M1 = M + 12 * A - 3;
|
||||
return (
|
||||
D +
|
||||
Math.floor((153 * M1 + 2) / 5) +
|
||||
365 * Y1 +
|
||||
Math.floor(Y1 / 4) -
|
||||
Math.floor(Y1 / 100) +
|
||||
Math.floor(Y1 / 400) -
|
||||
32045
|
||||
);
|
||||
}
|
||||
|
||||
getSolarDeclination(JulianDate: number) {
|
||||
const n = JulianDate - 2451545;
|
||||
const L = (280.46 + 0.9856474 * n) % 360;
|
||||
const g = this.degToRad((357.528 + 0.9856003 * n) % 360);
|
||||
const lambda = this.degToRad(L + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g));
|
||||
return Math.asin(Math.sin(lambda) * Math.sin(this.degToRad(23.44)));
|
||||
}
|
||||
|
||||
getSolarTime(date: Date, lon: number) {
|
||||
const EoT = this.getEquationOfTime(date);
|
||||
const offset = date.getTimezoneOffset() / 60;
|
||||
const standardMeridian = Math.round(lon / 15) * 15;
|
||||
const solarTime =
|
||||
date.getUTCHours() +
|
||||
(date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 -
|
||||
offset;
|
||||
return (solarTime + 24) % 24;
|
||||
}
|
||||
|
||||
getEquationOfTime(date: Date) {
|
||||
const JD = this.getJulianDate(date);
|
||||
const n = JD - 2451545;
|
||||
const g = this.degToRad((357.528 + 0.9856003 * n) % 360);
|
||||
const q = this.degToRad((280.46 + 0.9856474 * n) % 360);
|
||||
return (
|
||||
4 *
|
||||
this.radToDeg(
|
||||
0.000075 +
|
||||
0.001868 * Math.cos(q) -
|
||||
0.032077 * Math.sin(g) -
|
||||
0.014615 * Math.cos(2 * q) -
|
||||
0.040849 * Math.sin(2 * g)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
degToRad(deg: number) {
|
||||
return deg * (Math.PI / 180);
|
||||
}
|
||||
|
||||
radToDeg(rad: number) {
|
||||
return rad * (180 / Math.PI);
|
||||
}
|
||||
}
|
||||
|
||||
export const sunCalculator = new SunCalculator();
|
||||
@@ -1,18 +1,18 @@
|
||||
export class Err<T, U> {
|
||||
#inner: T
|
||||
#exception?: U
|
||||
#inner: T;
|
||||
#exception?: U;
|
||||
|
||||
constructor(inner: T, exception?: U) {
|
||||
this.#inner = inner
|
||||
this.#exception = exception
|
||||
this.#inner = inner;
|
||||
this.#exception = exception;
|
||||
}
|
||||
|
||||
get inner(): T {
|
||||
return this.#inner
|
||||
return this.#inner;
|
||||
}
|
||||
|
||||
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`
|
||||
*/
|
||||
isOk(): false {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export class Err<T, U> {
|
||||
* @returns `true` if `Err`; `false` if `Ok`
|
||||
*/
|
||||
isErr(): this is Err<T, U> {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,6 +37,6 @@ export class Err<T, U> {
|
||||
* @returns `Err(inner)`
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './err'
|
||||
export * from './ok'
|
||||
export * from './result'
|
||||
export * from './err';
|
||||
export * from './ok';
|
||||
export * from './result';
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
export class Ok<T> {
|
||||
#inner: T
|
||||
#inner: T;
|
||||
|
||||
constructor(inner: T) {
|
||||
this.#inner = inner
|
||||
this.#inner = inner;
|
||||
}
|
||||
|
||||
get inner(): T {
|
||||
return this.#inner
|
||||
return this.#inner;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -14,7 +14,7 @@ export class Ok<T> {
|
||||
* @returns `true` if `Ok`; `false` if `Err`
|
||||
*/
|
||||
isOk(): this is Ok<T> {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ export class Ok<T> {
|
||||
* @returns `true` if `Err`; `false` if `Ok`
|
||||
*/
|
||||
isErr(): false {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ export class Ok<T> {
|
||||
* @returns `Ok(inner)`
|
||||
*/
|
||||
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)`
|
||||
*/
|
||||
static void(): Ok<void> {
|
||||
return new Ok(undefined)
|
||||
return new Ok(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import { Err } from './err'
|
||||
import { Ok } from './ok'
|
||||
import { Err } from './err';
|
||||
import { Ok } from './ok';
|
||||
|
||||
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>
|
||||
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>;
|
||||
|
||||
export const Result = {
|
||||
export namespace Result {
|
||||
/**
|
||||
* @returns `Ok<T>`
|
||||
*/
|
||||
ok<T = unknown>(value: T) {
|
||||
return Ok.new(value)
|
||||
},
|
||||
export function ok<T = unknown>(value: T) {
|
||||
return Ok.new(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `Err<E, F>`
|
||||
*/
|
||||
err<E = unknown, F = unknown>(error: E, exception?: F) {
|
||||
return Err.new(error, exception)
|
||||
export function err<E = unknown, F = unknown>(error: E, exception?: F) {
|
||||
return Err.new(error, exception);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { writable } from 'svelte/store'
|
||||
import { browser } from '$app/environment'
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment';
|
||||
|
||||
export const persistentStore = <T>(key: string, initialValue: T) => {
|
||||
const savedValue = browser ? localStorage.getItem(key) : null
|
||||
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue
|
||||
const store = writable<T>()
|
||||
const savedValue = browser ? localStorage.getItem(key) : null;
|
||||
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue;
|
||||
const store = writable<T>();
|
||||
|
||||
store.subscribe(value => {
|
||||
if (browser) localStorage.setItem(key, JSON.stringify(value))
|
||||
})
|
||||
if (browser) localStorage.setItem(key, JSON.stringify(value));
|
||||
});
|
||||
|
||||
store.set(data)
|
||||
store.set(data);
|
||||
|
||||
return store
|
||||
}
|
||||
return store;
|
||||
};
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { resolve } from '$app/paths'
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center items-center w-full h-full">
|
||||
<h1>{page.status} {page.error?.message}</h1>
|
||||
<span>Go to <a class="btn btn-primary" href={resolve('/')}>Home page</a></span>
|
||||
<span>Go to <a class="btn btn-primary" href="/">Home page</a></span>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
servoAngles,
|
||||
servoAnglesOut,
|
||||
socket,
|
||||
apiLocation,
|
||||
location,
|
||||
useFeatureFlags,
|
||||
walkGait
|
||||
} from '$lib/stores'
|
||||
@@ -34,8 +34,8 @@
|
||||
const features = useFeatureFlags()
|
||||
|
||||
onMount(async () => {
|
||||
const ws = $apiLocation ? $apiLocation : window.location.host
|
||||
socket.init(`ws://${ws}/api/ws`)
|
||||
const ws = $location ? $location : window.location.host
|
||||
socket.init(`ws://${ws}/api/ws/events`)
|
||||
|
||||
addEventListeners()
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
telemetry.setRSSI(0)
|
||||
}
|
||||
|
||||
const handleError = (data: unknown) => console.error(data)
|
||||
const handleError = (data: any) => console.error(data)
|
||||
|
||||
const handleAnalytics = (data: Analytics) => analytics.addData(data)
|
||||
|
||||
@@ -115,15 +115,14 @@
|
||||
</div>
|
||||
|
||||
<Modals>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
{#snippet backdrop()}
|
||||
<div
|
||||
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
|
||||
transition:fade
|
||||
onclick={modals.closeAll}
|
||||
onkeydown={e => e.key === 'Escape' && modals.closeAll()}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
></div>
|
||||
onclick={modals.closeAll}>
|
||||
</div>
|
||||
{/snippet}
|
||||
</Modals>
|
||||
|
||||
|
||||
@@ -6,20 +6,8 @@ const registerFetchIntercept = async () => {
|
||||
const fileService = (await import('$lib/services/file-service')).default
|
||||
window.fetch = async (resource, config) => {
|
||||
const url = resource instanceof Request ? resource.url : resource.toString()
|
||||
|
||||
let file = await fileService?.getFile(url)
|
||||
if (file?.isOk() && file.inner) return new Response(new Uint8Array(file.inner))
|
||||
|
||||
if (url.startsWith('http')) {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
const pathOnly = urlObj.pathname
|
||||
file = await fileService?.getFile(pathOnly)
|
||||
if (file?.isOk() && file.inner) return new Response(new Uint8Array(file.inner))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return originalFetch(resource, config)
|
||||
const file = await fileService?.getFile(url)
|
||||
return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
import Visualization from '$lib/components/Visualization.svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import { onMount } from 'svelte'
|
||||
import { resolve } from '$app/paths'
|
||||
|
||||
onMount(() => {
|
||||
socket.subscribe(isConnected => {
|
||||
if (isConnected) {
|
||||
goto(resolve('/controller'))
|
||||
goto('/controller')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -17,14 +16,12 @@
|
||||
<div class="hero bg-base-100 h-screen">
|
||||
<div class="card md:card-side bg-base-200 shadow-2xl flex justify-center items-center">
|
||||
<div class="w-64 h-64">
|
||||
<Visualization defaultColor={null} orbit panel={false} ground={false} />
|
||||
<Visualization sky={false} orbit panel={false} ground={false} />
|
||||
</div>
|
||||
<div class="card-body w-80">
|
||||
<h2 class="card-title text-center text-2xl">Begin you journey</h2>
|
||||
<p class="py-6 text-center"></p>
|
||||
<a class="btn btn-primary" href={resolve($socket ? '/controller' : '/connection')}>
|
||||
Add Robot Dog
|
||||
</a>
|
||||
<a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}> Add Robot Dog </a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Connection from './Connection.svelte'
|
||||
import Connection from './Connection.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'Connection'
|
||||
}
|
||||
}) satisfies PageLoad
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import { WiFi } from '$lib/components/icons'
|
||||
import { apiLocation, socket } from '$lib/stores'
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||
import { WiFi } from '$lib/components/icons';
|
||||
import { location, socket } from '$lib/stores';
|
||||
|
||||
const update = () => {
|
||||
const ws = $apiLocation ? $apiLocation : window.location.host
|
||||
socket.init(`ws://${ws}/api/ws/events`)
|
||||
}
|
||||
const ws = $location ? $location : window.location.host;
|
||||
socket.init(`ws://${ws}/api/ws/events`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="flex">
|
||||
<label class="label w-32" for="server">Address:</label>
|
||||
<input class="input" bind:value={$apiLocation} />
|
||||
<input class="input" bind:value={$location} />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary" onclick={update}>Update</button>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export const load = async () => {
|
||||
return { title: 'Controller' }
|
||||
}
|
||||
return { title: 'Controller' };
|
||||
};
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
modes,
|
||||
type Modes,
|
||||
ModesEnum,
|
||||
walkGaits,
|
||||
WalkGaits,
|
||||
walkGait,
|
||||
walkGaitLabels
|
||||
} from '$lib/stores'
|
||||
import type { vector } from '$lib/types/models'
|
||||
import { VerticalSlider } from '$lib/components/input'
|
||||
import { gamepadAxes, gamepadButtonsEdges, hasGamepad } from '$lib/stores/gamepad'
|
||||
import { gamepadAxes, hasGamepad } from '$lib/stores/gamepad'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
|
||||
let throttle = new throttler()
|
||||
@@ -36,25 +37,11 @@
|
||||
handleJoyMove('right', { x: $gamepadAxes[2], y: $gamepadAxes[3] })
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (!$hasGamepad) return
|
||||
const b = $gamepadButtonsEdges
|
||||
if (!b.length) return
|
||||
if (b[0]?.justPressed) mode.set(5)
|
||||
if (b[1]?.justPressed) mode.set(4)
|
||||
if (b[2]?.justPressed) mode.set(3)
|
||||
if (b[3]?.justPressed) mode.set(0)
|
||||
if (b[12]?.justPressed)
|
||||
input.update(inputData => {
|
||||
inputData['height'] = Math.min(inputData.height + 0.1, 1)
|
||||
return inputData
|
||||
})
|
||||
if (b[13]?.justPressed)
|
||||
input.update(inputData => {
|
||||
inputData['height'] = Math.min(inputData.height - 0.1, 1)
|
||||
return inputData
|
||||
})
|
||||
})
|
||||
// TODO React to button press
|
||||
// $effect(() => {
|
||||
// if ($gamepadButtons.length === 0) return
|
||||
//
|
||||
// })
|
||||
|
||||
onMount(() => {
|
||||
left = nipplejs.create({
|
||||
@@ -74,9 +61,9 @@
|
||||
})
|
||||
|
||||
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('end', () => handleJoyMove('right', { x: 0, y: 0 }))
|
||||
right.on('end', (_, __) => handleJoyMove('right', { x: 0, y: 0 }))
|
||||
})
|
||||
|
||||
const handleJoyMove = (key: 'left' | 'right', data: vector) => {
|
||||
@@ -103,11 +90,9 @@
|
||||
const down = event.type === 'keydown'
|
||||
input.update(data => {
|
||||
if (event.key === 'w') data.left.y = down ? 1 : 0
|
||||
if (event.key === 'a') data.left.x = down ? -1 : 0
|
||||
if (event.key === 'a') data.left.x = down ? 1 : 0
|
||||
if (event.key === 's') data.left.y = down ? -1 : 0
|
||||
if (event.key === 'd') data.left.x = down ? 1 : 0
|
||||
if (event.key === 'ArrowLeft') data.right.x = down ? 1 : 0
|
||||
if (event.key === 'ArrowRight') data.right.x = down ? -1 : 0
|
||||
if (event.key === 'd') data.left.x = down ? -1 : 0
|
||||
return data
|
||||
})
|
||||
throttle.throttle(updateData, throttle_timing)
|
||||
@@ -150,27 +135,22 @@
|
||||
<div class="flex justify-center w-full"></div>
|
||||
</div>
|
||||
<div class="absolute bottom-0 z-10 flex items-end">
|
||||
<div
|
||||
class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl"
|
||||
>
|
||||
<div class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl">
|
||||
<VerticalSlider
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
oninput={(e: Event) => handleRange(e, 'height')}
|
||||
/>
|
||||
oninput={(e: Event) => handleRange(e, 'height')} />
|
||||
<label for="height">Ht</label>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden"
|
||||
>
|
||||
class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden">
|
||||
<div class="join">
|
||||
{#each modes as modeValue}
|
||||
<button
|
||||
class="btn join-item"
|
||||
class:btn-primary={$mode === modes.indexOf(modeValue)}
|
||||
onclick={() => changeMode(modeValue)}
|
||||
>
|
||||
onclick={() => changeMode(modeValue)}>
|
||||
{capitalize(modeValue)}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -183,8 +163,7 @@
|
||||
<button
|
||||
class="btn join-item btn-sm"
|
||||
class:btn-secondary={$walkGait === gaitValue}
|
||||
onclick={() => changeWalkGait(gaitValue)}
|
||||
>
|
||||
onclick={() => changeWalkGait(gaitValue)}>
|
||||
{walkGaitLabels[gaitValue]}
|
||||
</button>
|
||||
{/if}
|
||||
@@ -201,8 +180,7 @@
|
||||
step="0.01"
|
||||
max="1"
|
||||
oninput={e => handleRange(e, 's1')}
|
||||
class="range range-sm range-primary"
|
||||
/>
|
||||
class="range range-sm range-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="speed">Speed</label>
|
||||
@@ -213,8 +191,7 @@
|
||||
step="0.01"
|
||||
max="1"
|
||||
oninput={e => handleRange(e, 'speed')}
|
||||
class="range range-sm range-primary"
|
||||
/>
|
||||
class="range range-sm range-primary" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import { goto } from '$app/navigation'
|
||||
import { resolve } from '$app/paths'
|
||||
import type { PageLoad } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export const load = (async () => {
|
||||
goto(resolve('/'))
|
||||
return
|
||||
}) satisfies PageLoad
|
||||
goto('/');
|
||||
return;
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Camera from './Camera.svelte'
|
||||
import Camera from './Camera.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'Camera'
|
||||
}
|
||||
}) satisfies PageLoad
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import SettingsCard from '$lib/components/SettingsCard.svelte'
|
||||
import CameraSetting from './CameraSetting.svelte'
|
||||
import Stream from '$lib/components/Stream.svelte'
|
||||
import { Camera } from '$lib/components/icons'
|
||||
import SettingsCard from "$lib/components/SettingsCard.svelte";
|
||||
import CameraSetting from './CameraSetting.svelte';
|
||||
import Stream from '$lib/components/Stream.svelte';
|
||||
import { Camera } from "$lib/components/icons";
|
||||
</script>
|
||||
|
||||
<SettingsCard collapsible={false}>
|
||||
@@ -10,7 +10,7 @@
|
||||
<Camera class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
|
||||
{/snippet}
|
||||
{#snippet title()}
|
||||
<span>Camera</span>
|
||||
<span >Camera</span>
|
||||
{/snippet}
|
||||
<Stream />
|
||||
<CameraSetting />
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { api } from '$lib/api'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import type { CameraSettings } from '$lib/types/models'
|
||||
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
|
||||
})
|
||||
import { api } from '$lib/api';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import type { CameraSettings } from '$lib/types/models';
|
||||
let settings:CameraSettings = $state()
|
||||
|
||||
const getCameraSettings = async () => {
|
||||
const result = await api.get<CameraSettings>('/api/camera/settings')
|
||||
if (result.isErr()) {
|
||||
console.error('An error occurred', result.inner)
|
||||
if (result.isErr()){
|
||||
console.error("An error occurred", result.inner);
|
||||
return
|
||||
}
|
||||
settings = result.inner
|
||||
@@ -27,8 +15,8 @@
|
||||
|
||||
const updateCameraSettings = async () => {
|
||||
const result = await api.post<CameraSettings>('/api/camera/settings', settings)
|
||||
if (result.isErr()) {
|
||||
console.error('An error occurred', result.inner)
|
||||
if (result.isErr()){
|
||||
console.error("An error occurred", result.inner);
|
||||
return
|
||||
}
|
||||
settings = result.inner
|
||||
@@ -37,43 +25,23 @@
|
||||
|
||||
{#await getCameraSettings()}
|
||||
<Spinner />
|
||||
{:then}
|
||||
{:then _}
|
||||
<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">
|
||||
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 for="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 for="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 class="cursor-pointer flex items-center justify-between">
|
||||
@@ -88,10 +56,7 @@
|
||||
|
||||
<label for="special_effect" class="flex items-center">
|
||||
<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={1}>Negative</option>
|
||||
<option value={2}>Grayscale</option>
|
||||
|
||||
@@ -16,11 +16,6 @@
|
||||
part_number: 'MPU6050',
|
||||
name: 'Six-Axis (Gyro + Accelerometer) MEMS MotionTracking™ Devices'
|
||||
},
|
||||
{
|
||||
address: 105,
|
||||
part_number: 'ICM20948',
|
||||
name: 'Nine-Axis (Gyro + Accelerometer + Magnetometer) MEMS MotionTracking™ Device'
|
||||
},
|
||||
{ address: 115, part_number: 'PAJ7620U2', name: 'Gesture sensor' },
|
||||
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
|
||||
]
|
||||
@@ -35,7 +30,7 @@
|
||||
return () => socket.off(MessageTopic.i2cScan, handleScan)
|
||||
})
|
||||
|
||||
const handleScan = (data: { addresses: number[] }) => {
|
||||
const handleScan = (data: any) => {
|
||||
active_devices = data.addresses.map(
|
||||
(address: number) =>
|
||||
i2cDevices.find(device => device.address === address) || {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
return () => socket.off(MessageTopic.peripheralSettings, handleSettings)
|
||||
})
|
||||
|
||||
const handleSettings = (data: Record<string, unknown>) => {
|
||||
settings = data as PeripheralsConfiguration
|
||||
const handleSettings = (data: any) => {
|
||||
settings = data
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
@@ -56,8 +56,7 @@
|
||||
max="48"
|
||||
title="SDA pin number (0-48)"
|
||||
disabled={!isEditing}
|
||||
bind:value={settings.sda}
|
||||
/>
|
||||
bind:value={settings.sda} />
|
||||
</label>
|
||||
<label for="scl" class="input validator">
|
||||
SCL
|
||||
@@ -71,8 +70,7 @@
|
||||
max="48"
|
||||
title="SCL pin number (0-48)"
|
||||
disabled={!isEditing}
|
||||
bind:value={settings.scl}
|
||||
/>
|
||||
bind:value={settings.scl} />
|
||||
</label>
|
||||
<label class="input validator" for="frequency">
|
||||
Frequency
|
||||
@@ -85,20 +83,14 @@
|
||||
max="430000"
|
||||
title="I2C frequency in Hz"
|
||||
disabled={!isEditing}
|
||||
bind:value={settings.frequency}
|
||||
/>
|
||||
bind:value={settings.frequency} />
|
||||
</label>
|
||||
<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" />
|
||||
</button>
|
||||
{#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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import IMU from './imu.svelte'
|
||||
import IMU from './imu.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'IMU'
|
||||
}
|
||||
}) satisfies PageLoad
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { slide } from 'svelte/transition'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import { socket } from '$lib/stores'
|
||||
import { MessageTopic, type IMUMsg } from '$lib/types/models'
|
||||
import { MessageTopic, type IMU } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Rotate3d } from '$lib/components/icons'
|
||||
|
||||
@@ -18,12 +18,10 @@
|
||||
let angleChartElement: HTMLCanvasElement
|
||||
let tempChartElement: HTMLCanvasElement
|
||||
let altitudeChartElement: HTMLCanvasElement
|
||||
let magnetometerChartElement: HTMLCanvasElement
|
||||
|
||||
let angleChart: Chart
|
||||
let tempChart: Chart
|
||||
let altitudeChart: Chart
|
||||
let magnetometerChart: Chart
|
||||
|
||||
const getChartColors = () => {
|
||||
const style = getComputedStyle(document.body)
|
||||
@@ -173,40 +171,9 @@
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
magnetometerChart = new Chart(magnetometerChartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
datasets: [
|
||||
{
|
||||
label: 'Heading',
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primary,
|
||||
borderWidth: 2,
|
||||
data: $imu.heading,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
...baseConfig,
|
||||
scales: {
|
||||
...baseConfig.scales,
|
||||
y: {
|
||||
...baseConfig.scales.y,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Heading [°]',
|
||||
color: colors.background,
|
||||
font: { size: 16, weight: 'bold' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateChartData = (chart: Chart, data: number[]) => {
|
||||
const updateChartData = (chart: Chart, data: number[], label: string) => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
chart.options.scales!.y!.min = Math.min(...data) - 1
|
||||
@@ -227,18 +194,14 @@
|
||||
angleChart.update('none')
|
||||
}
|
||||
|
||||
if ($features.mag) {
|
||||
updateChartData(magnetometerChart, $imu.heading)
|
||||
}
|
||||
|
||||
if ($features.bmp) {
|
||||
updateChartData(tempChart, $imu.bmp_temp)
|
||||
updateChartData(altitudeChart, $imu.altitude)
|
||||
updateChartData(tempChart, $imu.bmp_temp, 'Temperature')
|
||||
updateChartData(altitudeChart, $imu.altitude, 'Altitude')
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
socket.on(MessageTopic.imu, (data: IMUMsg) => {
|
||||
socket.on(MessageTopic.imu, (data: IMU) => {
|
||||
console.log(data)
|
||||
imu.addData(data)
|
||||
})
|
||||
@@ -265,38 +228,24 @@
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={angleChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $features.mag}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={magnetometerChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $features.bmp}
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={tempChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={altitudeChartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async () => {
|
||||
return {
|
||||
title: 'Servo'
|
||||
}
|
||||
}) satisfies PageLoad
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { onMount } from 'svelte'
|
||||
import { RotateCw, RotateCcw } from '$lib/components/icons'
|
||||
interface Props {
|
||||
data?: Record<string, unknown>
|
||||
data?: any
|
||||
servoId?: number
|
||||
pwm?: number
|
||||
}
|
||||
@@ -70,8 +70,7 @@
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'center_pwm')}
|
||||
min="80"
|
||||
max="600"
|
||||
/>
|
||||
max="600" />
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
@@ -82,15 +81,13 @@
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'center_angle')}
|
||||
min="-90"
|
||||
max="90"
|
||||
/>
|
||||
max="90" />
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-sm btn-ghost"
|
||||
title="Toggle direction {servo.direction}"
|
||||
onclick={() => toggleDirection(index)}
|
||||
>
|
||||
onclick={() => toggleDirection(index)}>
|
||||
{#if servo.direction === 1}
|
||||
<RotateCw class="w-4 h-4 text-green-500" />
|
||||
{:else}
|
||||
@@ -107,8 +104,7 @@
|
||||
onblur={syncConfig}
|
||||
oninput={event => updateValue(event, index, 'conversion')}
|
||||
min="0"
|
||||
max="10"
|
||||
/>
|
||||
max="10" />
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -41,8 +41,7 @@
|
||||
max="600"
|
||||
bind:value={pwm}
|
||||
oninput={updatePWM}
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
|
||||
/>
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" />
|
||||
|
||||
<div class="flex flex-col">
|
||||
<h2 class="text-lg">General servo configuration</h2>
|
||||
@@ -56,8 +55,7 @@
|
||||
type="checkbox"
|
||||
class="toggle"
|
||||
bind:checked={active}
|
||||
onchange={active ? activateServo : deactivateServo}
|
||||
/>
|
||||
onchange={active ? activateServo : deactivateServo} />
|
||||
</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<label for="servoId">Servo active {servoId}</label>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import { goto } from '$app/navigation'
|
||||
import { resolve } from '$app/paths'
|
||||
import type { PageLoad } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export const load = (async () => {
|
||||
goto(resolve('/'))
|
||||
return
|
||||
}) satisfies PageLoad
|
||||
goto('/');
|
||||
return;
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user