🎨 format
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -1,43 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { focusTrap } from 'svelte-focus-trap'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { Cancel, Check } from '$lib/components/icons'
|
||||
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals'
|
||||
import { focusTrap } from 'svelte-focus-trap'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { Cancel, Check } from '$lib/components/icons'
|
||||
import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals'
|
||||
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
labels = {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'OK', icon: Check }
|
||||
}
|
||||
}: ModalProps = $props()
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onConfirm,
|
||||
labels = {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'OK', icon: Check }
|
||||
}
|
||||
}: ModalProps = $props()
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
{@const SvelteComponent = labels?.confirm.icon}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
{@const SvelteComponent = labels?.confirm.icon}
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}>
|
||||
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
|
||||
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-error inline-flex items-center"
|
||||
onclick={() => modals.close()}
|
||||
>
|
||||
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
|
||||
<SvelteComponent class="mr-2 h-5 w-5" /><span>{labels?.confirm.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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,40 +1,43 @@
|
||||
<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,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
labels = {
|
||||
dismiss: { label: 'Dismiss', icon: Check },
|
||||
},
|
||||
}: ModalProps = $props();
|
||||
let {
|
||||
isOpen,
|
||||
title,
|
||||
message,
|
||||
onDismiss,
|
||||
labels = {
|
||||
dismiss: { label: 'Dismiss', icon: Check }
|
||||
}
|
||||
}: ModalProps = $props()
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg">
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onDismiss}>
|
||||
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
use:exitBeforeEnter
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2"></div>
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2"></div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
onclick={onDismiss}
|
||||
>
|
||||
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,78 +1,78 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import * as THREE from 'three';
|
||||
import { imu } from '$lib/stores/imu';
|
||||
import SceneBuilder from '$lib/sceneBuilder';
|
||||
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()
|
||||
.addRenderer({ canvas: canvas, antialias: true, alpha: true })
|
||||
.addPerspectiveCamera({ x: 2, y: 0, z: 2 })
|
||||
.addOrbitControls(1, 10, false)
|
||||
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
|
||||
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
|
||||
.fillParent();
|
||||
const initThreeJS = () => {
|
||||
sceneBuilder = new SceneBuilder()
|
||||
.addRenderer({ canvas: canvas, antialias: true, alpha: true })
|
||||
.addPerspectiveCamera({ x: 2, y: 0, z: 2 })
|
||||
.addOrbitControls(1, 10, false)
|
||||
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
|
||||
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
|
||||
.fillParent()
|
||||
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1);
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: 0x00ff00,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
});
|
||||
cube = new THREE.Mesh(geometry, material);
|
||||
sceneBuilder.scene.add(cube);
|
||||
const geometry = new THREE.BoxGeometry(1, 1, 1)
|
||||
const material = new THREE.MeshPhongMaterial({
|
||||
color: 0x00ff00,
|
||||
transparent: true,
|
||||
opacity: 0.8
|
||||
})
|
||||
cube = new THREE.Mesh(geometry, material)
|
||||
sceneBuilder.scene.add(cube)
|
||||
|
||||
sceneBuilder.addRenderCb(() => {
|
||||
if (!cube) return;
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - lastUpdateTime) / 1000; // convert to seconds
|
||||
lastUpdateTime = currentTime;
|
||||
sceneBuilder.addRenderCb(() => {
|
||||
if (!cube) return
|
||||
const currentTime = performance.now()
|
||||
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
|
||||
lastUpdateTime = currentTime
|
||||
|
||||
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime);
|
||||
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor);
|
||||
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor);
|
||||
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor);
|
||||
});
|
||||
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
|
||||
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
|
||||
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
|
||||
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
|
||||
})
|
||||
|
||||
sceneBuilder.startRenderLoop();
|
||||
};
|
||||
|
||||
const updateOrientation = () => {
|
||||
if (!cube) return;
|
||||
|
||||
const y = -$imu.x[$imu.x.length - 1] || 0;
|
||||
const x = $imu.y[$imu.y.length - 1] || 0;
|
||||
const z = -$imu.z[$imu.z.length - 1] || 0;
|
||||
|
||||
targetRotation.set(
|
||||
THREE.MathUtils.degToRad(x),
|
||||
THREE.MathUtils.degToRad(y),
|
||||
THREE.MathUtils.degToRad(z)
|
||||
);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
initThreeJS();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
sceneBuilder?.renderer?.dispose();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if ($imu) {
|
||||
updateOrientation();
|
||||
sceneBuilder.startRenderLoop()
|
||||
}
|
||||
});
|
||||
|
||||
const updateOrientation = () => {
|
||||
if (!cube) return
|
||||
|
||||
const y = -$imu.x[$imu.x.length - 1] || 0
|
||||
const x = $imu.y[$imu.y.length - 1] || 0
|
||||
const z = -$imu.z[$imu.z.length - 1] || 0
|
||||
|
||||
targetRotation.set(
|
||||
THREE.MathUtils.degToRad(x),
|
||||
THREE.MathUtils.degToRad(y),
|
||||
THREE.MathUtils.degToRad(z)
|
||||
)
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
initThreeJS()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
sceneBuilder?.renderer?.dispose()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if ($imu) {
|
||||
updateOrientation()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
|
||||
<canvas class="w-full h-full" bind:this={canvas}></canvas>
|
||||
<canvas class="w-full h-full" bind:this={canvas}></canvas>
|
||||
</div>
|
||||
|
||||
@@ -1,60 +1,76 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { Down } from './icons'
|
||||
interface Props {
|
||||
open?: boolean
|
||||
collapsible?: boolean
|
||||
icon?: import('svelte').Snippet
|
||||
title?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
right?: import('svelte').Snippet
|
||||
}
|
||||
import { slide } from 'svelte/transition'
|
||||
import { cubicOut } from 'svelte/easing'
|
||||
import { Down } from './icons'
|
||||
interface Props {
|
||||
open?: boolean
|
||||
collapsible?: boolean
|
||||
icon?: import('svelte').Snippet
|
||||
title?: import('svelte').Snippet
|
||||
children?: import('svelte').Snippet
|
||||
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">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
open = !open
|
||||
}}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ?
|
||||
'rotate-180'
|
||||
: ''}" />
|
||||
</button>
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
onclick={() => {
|
||||
open = !open
|
||||
}}
|
||||
>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||
open
|
||||
) ?
|
||||
'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg">
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
{@render right?.()}
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
<span class="inline-flex items-baseline">
|
||||
{@render icon?.()}
|
||||
{@render title?.()}
|
||||
</span>
|
||||
{@render right?.()}
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Loader } from "./icons";
|
||||
|
||||
import { Loader } from './icons'
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center justify-center p-6">
|
||||
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
|
||||
<p class="text-xl">Loading...</p>
|
||||
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
|
||||
<p class="text-xl">Loading...</p>
|
||||
</div>
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
<script lang="ts">
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning'
|
||||
|
||||
const {
|
||||
icon,
|
||||
title,
|
||||
description = '',
|
||||
variant = 'primary',
|
||||
class: klass = '',
|
||||
children = null
|
||||
} = $props<{
|
||||
icon?: any
|
||||
title: string
|
||||
description?: string | number
|
||||
variant?: Variant
|
||||
class?: string
|
||||
children?: () => any
|
||||
}>()
|
||||
const {
|
||||
icon,
|
||||
title,
|
||||
description = '',
|
||||
variant = 'primary',
|
||||
class: klass = '',
|
||||
children = null
|
||||
} = $props<{
|
||||
icon?: any
|
||||
title: string
|
||||
description?: string | number
|
||||
variant?: Variant
|
||||
class?: string
|
||||
children?: () => any
|
||||
}>()
|
||||
|
||||
const Icon = $derived(icon)
|
||||
const Icon = $derived(icon)
|
||||
|
||||
const variants: Record<Variant, [string, string]> = {
|
||||
success: ['bg-success', 'text-success-content'],
|
||||
error: ['bg-error', 'text-error-content'],
|
||||
primary: ['bg-primary', 'text-primary-content'],
|
||||
info: ['bg-info', 'text-info-content'],
|
||||
warning: ['bg-warning', 'text-warning-content']
|
||||
}
|
||||
const variants: Record<Variant, [string, string]> = {
|
||||
success: ['bg-success', 'text-success-content'],
|
||||
error: ['bg-error', 'text-error-content'],
|
||||
primary: ['bg-primary', 'text-primary-content'],
|
||||
info: ['bg-info', 'text-info-content'],
|
||||
warning: ['bg-warning', 'text-warning-content']
|
||||
}
|
||||
|
||||
const variantKey: Variant = (variant as Variant) in variants ? (variant as Variant) : 'primary'
|
||||
const [bgColor, textColor] = variants[variantKey]
|
||||
const variantKey: Variant = (variant as Variant) in variants ? (variant as Variant) : 'primary'
|
||||
const [bgColor, textColor] = variants[variantKey]
|
||||
</script>
|
||||
|
||||
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2 {klass}">
|
||||
{#if icon}
|
||||
<div class="mask mask-hexagon {bgColor} h-auto w-10 flex-none">
|
||||
<Icon class="{textColor} h-auto w-full scale-75" />
|
||||
{#if icon}
|
||||
<div class="mask mask-hexagon {bgColor} h-auto w-10 flex-none">
|
||||
<Icon class="{textColor} h-auto w-full scale-75" />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grow">
|
||||
<div class="font-bold">{title}</div>
|
||||
<div class="text-sm opacity-75 grow">{description}</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grow">
|
||||
<div class="font-bold">{title}</div>
|
||||
<div class="text-sm opacity-75 grow">{description}</div>
|
||||
</div>
|
||||
{@render children?.()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import { location } from '$lib/stores';
|
||||
import { onDestroy } from 'svelte'
|
||||
import { location } from '$lib/stores'
|
||||
|
||||
let source = $state(`${$location}/api/camera/stream`);
|
||||
let source = $state(`${$location}/api/camera/stream`)
|
||||
|
||||
onDestroy(() => (source = '#'));
|
||||
onDestroy(() => (source = '#'))
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { error, info, success, warning } from './icons';
|
||||
import { flip } from 'svelte/animate'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import { error, info, success, warning } from './icons'
|
||||
|
||||
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let { theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
}, icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
} } = $props();
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let {
|
||||
theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
},
|
||||
icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
}
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{@const SvelteComponent = icon[notification.type]}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<SvelteComponent class="h-6 w-6 shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{@const SvelteComponent = icon[notification.type]}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<SvelteComponent class="h-6 w-6 shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,332 +1,339 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
BufferGeometry,
|
||||
Line,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
type Object3D,
|
||||
SphereGeometry,
|
||||
Vector3,
|
||||
type NormalBufferAttributes,
|
||||
type Object3DEventMap
|
||||
} from 'three'
|
||||
import {
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
model,
|
||||
outControllerData,
|
||||
servoAnglesOut,
|
||||
servoAngles,
|
||||
mpu,
|
||||
jointNames,
|
||||
currentKinematic,
|
||||
walkGait,
|
||||
walkGaits,
|
||||
walkGaitToMode
|
||||
} from '$lib/stores'
|
||||
import {
|
||||
extractFootColor,
|
||||
populateModelCache,
|
||||
throttler,
|
||||
getToeWorldPositions
|
||||
} from '$lib/utilities'
|
||||
import SceneBuilder from '$lib/sceneBuilder'
|
||||
import { lerp, degToRad } from 'three/src/math/MathUtils'
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
|
||||
import { type body_state_t } from '$lib/kinematic'
|
||||
import { BezierState, CalibrationState, IdleState, RestState, StandState } from '$lib/gait'
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js'
|
||||
import type { URDFRobot } from 'urdf-loader'
|
||||
import { get } from 'svelte/store'
|
||||
import { onDestroy, onMount } from 'svelte'
|
||||
import {
|
||||
BufferGeometry,
|
||||
Line,
|
||||
LineBasicMaterial,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
type Object3D,
|
||||
SphereGeometry,
|
||||
Vector3,
|
||||
type NormalBufferAttributes,
|
||||
type Object3DEventMap
|
||||
} from 'three'
|
||||
import {
|
||||
ModesEnum,
|
||||
kinematicData,
|
||||
mode,
|
||||
model,
|
||||
outControllerData,
|
||||
servoAnglesOut,
|
||||
servoAngles,
|
||||
mpu,
|
||||
jointNames,
|
||||
currentKinematic,
|
||||
walkGait,
|
||||
walkGaits,
|
||||
walkGaitToMode
|
||||
} from '$lib/stores'
|
||||
import {
|
||||
extractFootColor,
|
||||
populateModelCache,
|
||||
throttler,
|
||||
getToeWorldPositions
|
||||
} from '$lib/utilities'
|
||||
import SceneBuilder from '$lib/sceneBuilder'
|
||||
import { lerp, degToRad } from 'three/src/math/MathUtils'
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
|
||||
import { type body_state_t } from '$lib/kinematic'
|
||||
import { BezierState, CalibrationState, IdleState, RestState, StandState } from '$lib/gait'
|
||||
import { radToDeg } from 'three/src/math/MathUtils.js'
|
||||
import type { URDFRobot } from 'urdf-loader'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
interface Props {
|
||||
sky?: boolean
|
||||
orbit?: boolean
|
||||
panel?: boolean
|
||||
debug?: boolean
|
||||
ground?: boolean
|
||||
}
|
||||
|
||||
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
|
||||
|
||||
let sceneManager = $state(new SceneBuilder())
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
let currentModelAngles: number[] = new Array(12).fill(0)
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
let gui_panel: GUI
|
||||
let Throttler = new throttler()
|
||||
|
||||
let feet_trace = new Array(4).fill([])
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
let target: Object3D<Object3DEventMap>
|
||||
|
||||
let target_position = { x: 0, z: 0, yaw: 0 }
|
||||
|
||||
let kinematic = get(currentKinematic)
|
||||
|
||||
let planners = {
|
||||
[ModesEnum.Deactivated]: new IdleState(),
|
||||
[ModesEnum.Idle]: new IdleState(),
|
||||
[ModesEnum.Calibration]: new CalibrationState(),
|
||||
[ModesEnum.Rest]: new RestState(),
|
||||
[ModesEnum.Stand]: new StandState(),
|
||||
[ModesEnum.Walk]: new BezierState()
|
||||
}
|
||||
let lastTick = performance.now()
|
||||
|
||||
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
|
||||
|
||||
let body_state = {
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.5,
|
||||
zm: 0,
|
||||
feet: kinematic.getDefaultFeetPos()
|
||||
}
|
||||
|
||||
let settings = {
|
||||
'Internal kinematic': true,
|
||||
'Robot transform controls': false,
|
||||
'Auto orient robot': true,
|
||||
'Trace feet': debug,
|
||||
'Target position': false,
|
||||
'Trace points': 30,
|
||||
'Fix camera on robot': true,
|
||||
'Smooth motion': true,
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.7,
|
||||
zm: 0,
|
||||
Background: 'black'
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await populateModelCache()
|
||||
await createScene()
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
walkGait.subscribe(gait => planners[ModesEnum.Walk].set_mode(walkGaitToMode(gait)))
|
||||
if (panel) createPanel()
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
canvas.remove()
|
||||
gui_panel?.destroy()
|
||||
})
|
||||
|
||||
const updateAnglesFromStore = (angles: number[]) => {
|
||||
if (sceneManager.isDragging) return
|
||||
if (settings['Internal kinematic']) return
|
||||
modelTargetAngles = angles
|
||||
}
|
||||
|
||||
const createPanel = () => {
|
||||
gui_panel = new GUI({ width: 310 })
|
||||
gui_panel.close()
|
||||
gui_panel.domElement.id = 'three-gui-panel'
|
||||
|
||||
const general = gui_panel.addFolder('General')
|
||||
general.add(settings, 'Internal kinematic')
|
||||
general.add(settings, 'Robot transform controls')
|
||||
general.add(settings, 'Auto orient robot')
|
||||
|
||||
const kinematic = gui_panel.addFolder('Kinematics')
|
||||
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
|
||||
|
||||
const visibility = gui_panel.addFolder('Visualization')
|
||||
visibility.add(settings, 'Trace feet')
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
visibility.add(settings, 'Target position')
|
||||
visibility.add(settings, 'Smooth motion')
|
||||
visibility.addColor(settings, 'Background')
|
||||
}
|
||||
|
||||
const updateKinematicPosition = () => {
|
||||
kinematicData.set([
|
||||
settings.omega,
|
||||
settings.phi,
|
||||
settings.psi,
|
||||
settings.xm,
|
||||
settings.ym,
|
||||
settings.zm
|
||||
])
|
||||
}
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
|
||||
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
|
||||
}
|
||||
|
||||
const createScene = async () => {
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(2, 20, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model)
|
||||
.addTransformControls(sceneManager.model)
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
.startRenderLoop()
|
||||
|
||||
if (ground) sceneManager.addGroundPlane()
|
||||
|
||||
const geometry = new SphereGeometry(0.1, 32, 16)
|
||||
const material = new MeshBasicMaterial({ color: 0xffff00 })
|
||||
target = new Mesh(geometry, material)
|
||||
sceneManager.scene.add(target)
|
||||
|
||||
if (debug) {
|
||||
sceneManager.addDragControl(updateAngles)
|
||||
}
|
||||
if (sky) sceneManager.addSky()
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry()
|
||||
const material = new LineBasicMaterial({ color: 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
|
||||
interface Props {
|
||||
sky?: boolean
|
||||
orbit?: boolean
|
||||
panel?: boolean
|
||||
debug?: boolean
|
||||
ground?: boolean
|
||||
}
|
||||
|
||||
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])
|
||||
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props()
|
||||
|
||||
let sceneManager = $state(new SceneBuilder())
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
let currentModelAngles: number[] = new Array(12).fill(0)
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
let gui_panel: GUI
|
||||
let Throttler = new throttler()
|
||||
|
||||
let feet_trace = new Array(4).fill([])
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
let target: Object3D<Object3DEventMap>
|
||||
|
||||
let target_position = { x: 0, z: 0, yaw: 0 }
|
||||
|
||||
let kinematic = get(currentKinematic)
|
||||
|
||||
let planners = {
|
||||
[ModesEnum.Deactivated]: new IdleState(),
|
||||
[ModesEnum.Idle]: new IdleState(),
|
||||
[ModesEnum.Calibration]: new CalibrationState(),
|
||||
[ModesEnum.Rest]: new RestState(),
|
||||
[ModesEnum.Stand]: new StandState(),
|
||||
[ModesEnum.Walk]: new BezierState()
|
||||
}
|
||||
let lastTick = performance.now()
|
||||
|
||||
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1]
|
||||
|
||||
let body_state = {
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.5,
|
||||
zm: 0,
|
||||
feet: kinematic.getDefaultFeetPos()
|
||||
}
|
||||
|
||||
let settings = {
|
||||
'Internal kinematic': true,
|
||||
'Robot transform controls': false,
|
||||
'Auto orient robot': true,
|
||||
'Trace feet': debug,
|
||||
'Target position': false,
|
||||
'Trace points': 30,
|
||||
'Fix camera on robot': true,
|
||||
'Smooth motion': true,
|
||||
omega: 0,
|
||||
phi: 0,
|
||||
psi: 0,
|
||||
xm: 0,
|
||||
ym: 0.7,
|
||||
zm: 0,
|
||||
Background: 'black'
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await populateModelCache()
|
||||
await createScene()
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
walkGait.subscribe(gait => planners[ModesEnum.Walk].set_mode(walkGaitToMode(gait)))
|
||||
if (panel) createPanel()
|
||||
})
|
||||
}
|
||||
|
||||
const calculate_kinematics = () => {
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const position: body_state_t = {
|
||||
omega: settings.omega,
|
||||
phi: settings.phi,
|
||||
psi: settings.psi,
|
||||
xm: settings.xm,
|
||||
ym: settings.ym,
|
||||
zm: settings.zm,
|
||||
feet: body_state.feet
|
||||
onDestroy(() => {
|
||||
canvas.remove()
|
||||
gui_panel?.destroy()
|
||||
})
|
||||
|
||||
const updateAnglesFromStore = (angles: number[]) => {
|
||||
if (sceneManager.isDragging) return
|
||||
if (settings['Internal kinematic']) return
|
||||
modelTargetAngles = angles
|
||||
}
|
||||
|
||||
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
|
||||
modelTargetAngles = new_angles
|
||||
}
|
||||
const createPanel = () => {
|
||||
gui_panel = new GUI({ width: 310 })
|
||||
gui_panel.close()
|
||||
gui_panel.domElement.id = 'three-gui-panel'
|
||||
|
||||
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
|
||||
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
|
||||
const general = gui_panel.addFolder('General')
|
||||
general.add(settings, 'Internal kinematic')
|
||||
general.add(settings, 'Robot transform controls')
|
||||
general.add(settings, 'Auto orient robot')
|
||||
|
||||
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
|
||||
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
|
||||
const kinematic = gui_panel.addFolder('Kinematics')
|
||||
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
|
||||
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
|
||||
|
||||
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
|
||||
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
|
||||
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
|
||||
}
|
||||
|
||||
const update_camera = (robot: URDFRobot) => {
|
||||
if (!settings['Fix camera on robot']) return
|
||||
sceneManager.orbit.target = robot.position.clone()
|
||||
}
|
||||
|
||||
const smooth = (start: number, end: number, amount: number) => {
|
||||
return settings['Smooth motion'] ? lerp(start, end, amount) : end
|
||||
}
|
||||
|
||||
const update_gait = () => {
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const controlData = get(outControllerData)
|
||||
const data = {
|
||||
lx: controlData[0],
|
||||
ly: controlData[1],
|
||||
rx: controlData[2],
|
||||
ry: controlData[3],
|
||||
h: controlData[4],
|
||||
s: controlData[5],
|
||||
s1: controlData[6]
|
||||
}
|
||||
body_state.ym = data.h
|
||||
|
||||
let planner = planners[get(mode)]
|
||||
const delta = performance.now() - lastTick
|
||||
lastTick = performance.now()
|
||||
|
||||
body_state = planner.step(body_state, data, delta)
|
||||
|
||||
settings.omega = body_state.omega
|
||||
settings.phi = body_state.phi
|
||||
settings.psi = body_state.psi
|
||||
settings.xm = body_state.xm
|
||||
settings.ym = body_state.ym
|
||||
settings.zm = body_state.zm
|
||||
}
|
||||
|
||||
const update_robot_position = (robot: URDFRobot) => {
|
||||
if (!settings['Robot transform controls']) return
|
||||
settings.omega = radToDeg(robot.rotation.y)
|
||||
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
|
||||
settings.psi = radToDeg(robot.rotation.x) + 90
|
||||
settings.xm = robot.position.z * 100
|
||||
settings.zm = -robot.position.x * 100
|
||||
}
|
||||
|
||||
const updateTargetPosition = () => {
|
||||
target.visible = settings['Target position']
|
||||
target.position.x = smooth(target.position.x, target_position.x, 0.5)
|
||||
target.position.z = smooth(target.position.z, target_position.z, 0.5)
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
const robot = sceneManager.model
|
||||
if (!robot) return
|
||||
|
||||
const toes = getToeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
update_camera(robot)
|
||||
update_gait()
|
||||
calculate_kinematics()
|
||||
update_robot_position(robot)
|
||||
|
||||
sceneManager.transformControl.showX = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showY = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showZ = settings['Robot transform controls']
|
||||
|
||||
for (let i = 0; i < $jointNames.length; i++) {
|
||||
currentModelAngles[i] = smooth(
|
||||
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
||||
modelTargetAngles[i],
|
||||
0.1
|
||||
)
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
|
||||
const visibility = gui_panel.addFolder('Visualization')
|
||||
visibility.add(settings, 'Trace feet')
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
visibility.add(settings, 'Target position')
|
||||
visibility.add(settings, 'Smooth motion')
|
||||
visibility.addColor(settings, 'Background')
|
||||
}
|
||||
|
||||
orient_robot(robot, toes)
|
||||
updateTargetPosition()
|
||||
}
|
||||
const updateKinematicPosition = () => {
|
||||
kinematicData.set([
|
||||
settings.omega,
|
||||
settings.phi,
|
||||
settings.psi,
|
||||
settings.xm,
|
||||
settings.ym,
|
||||
settings.zm
|
||||
])
|
||||
}
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
|
||||
Throttler.throttle(
|
||||
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))),
|
||||
100
|
||||
)
|
||||
}
|
||||
|
||||
const createScene = async () => {
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(2, 20, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 3 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.5 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model)
|
||||
.addTransformControls(sceneManager.model)
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
.startRenderLoop()
|
||||
|
||||
if (ground) sceneManager.addGroundPlane()
|
||||
|
||||
const geometry = new SphereGeometry(0.1, 32, 16)
|
||||
const material = new MeshBasicMaterial({ color: 0xffff00 })
|
||||
target = new Mesh(geometry, material)
|
||||
sceneManager.scene.add(target)
|
||||
|
||||
if (debug) {
|
||||
sceneManager.addDragControl(updateAngles)
|
||||
}
|
||||
if (sky) sceneManager.addSky()
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry()
|
||||
const material = new LineBasicMaterial({ color: 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 = () => {
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const position: body_state_t = {
|
||||
omega: settings.omega,
|
||||
phi: settings.phi,
|
||||
psi: settings.psi,
|
||||
xm: settings.xm,
|
||||
ym: settings.ym,
|
||||
zm: settings.zm,
|
||||
feet: body_state.feet
|
||||
}
|
||||
|
||||
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
|
||||
modelTargetAngles = new_angles
|
||||
}
|
||||
|
||||
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
|
||||
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
|
||||
|
||||
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
|
||||
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
|
||||
|
||||
robot.rotation.z = smooth(
|
||||
robot.rotation.z,
|
||||
degToRad(-settings.phi + $mpu.heading + 90),
|
||||
0.1
|
||||
)
|
||||
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
|
||||
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
|
||||
}
|
||||
|
||||
const update_camera = (robot: URDFRobot) => {
|
||||
if (!settings['Fix camera on robot']) return
|
||||
sceneManager.orbit.target = robot.position.clone()
|
||||
}
|
||||
|
||||
const smooth = (start: number, end: number, amount: number) => {
|
||||
return settings['Smooth motion'] ? lerp(start, end, amount) : end
|
||||
}
|
||||
|
||||
const update_gait = () => {
|
||||
if (sceneManager.isDragging || !settings['Internal kinematic']) return
|
||||
const controlData = get(outControllerData)
|
||||
const data = {
|
||||
lx: controlData[0],
|
||||
ly: controlData[1],
|
||||
rx: controlData[2],
|
||||
ry: controlData[3],
|
||||
h: controlData[4],
|
||||
s: controlData[5],
|
||||
s1: controlData[6]
|
||||
}
|
||||
body_state.ym = data.h
|
||||
|
||||
let planner = planners[get(mode)]
|
||||
const delta = performance.now() - lastTick
|
||||
lastTick = performance.now()
|
||||
|
||||
body_state = planner.step(body_state, data, delta)
|
||||
|
||||
settings.omega = body_state.omega
|
||||
settings.phi = body_state.phi
|
||||
settings.psi = body_state.psi
|
||||
settings.xm = body_state.xm
|
||||
settings.ym = body_state.ym
|
||||
settings.zm = body_state.zm
|
||||
}
|
||||
|
||||
const update_robot_position = (robot: URDFRobot) => {
|
||||
if (!settings['Robot transform controls']) return
|
||||
settings.omega = radToDeg(robot.rotation.y)
|
||||
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
|
||||
settings.psi = radToDeg(robot.rotation.x) + 90
|
||||
settings.xm = robot.position.z * 100
|
||||
settings.zm = -robot.position.x * 100
|
||||
}
|
||||
|
||||
const updateTargetPosition = () => {
|
||||
target.visible = settings['Target position']
|
||||
target.position.x = smooth(target.position.x, target_position.x, 0.5)
|
||||
target.position.z = smooth(target.position.z, target_position.z, 0.5)
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
const robot = sceneManager.model
|
||||
if (!robot) return
|
||||
|
||||
const toes = getToeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
update_camera(robot)
|
||||
update_gait()
|
||||
calculate_kinematics()
|
||||
update_robot_position(robot)
|
||||
|
||||
sceneManager.transformControl.showX = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showY = settings['Robot transform controls']
|
||||
sceneManager.transformControl.showZ = settings['Robot transform controls']
|
||||
|
||||
for (let i = 0; i < $jointNames.length; i++) {
|
||||
currentModelAngles[i] = smooth(
|
||||
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
||||
modelTargetAngles[i],
|
||||
0.1
|
||||
)
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
|
||||
}
|
||||
|
||||
orient_robot(robot, toes)
|
||||
updateTargetPosition()
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onresize={sceneManager.fillParent} />
|
||||
|
||||
@@ -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 type = $derived(show ? 'text' : 'password');
|
||||
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props()
|
||||
|
||||
const handleInput = (e: any) => value = e.target.value
|
||||
let type = $derived(show ? 'text' : 'password')
|
||||
|
||||
const togglePassword = () => show = !show
|
||||
const handleInput = (e: any) => (value = e.target.value)
|
||||
|
||||
const togglePassword = () => (show = !show)
|
||||
</script>
|
||||
|
||||
<label class="input input-bordered flex items-center gap-2">
|
||||
@@ -23,4 +23,4 @@
|
||||
<MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" />
|
||||
<MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" />
|
||||
</div>
|
||||
</label>
|
||||
</label>
|
||||
|
||||
@@ -1,34 +1,35 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
value?: any
|
||||
oninput?: any
|
||||
}
|
||||
interface Props {
|
||||
min?: number
|
||||
max?: number
|
||||
step?: number
|
||||
value?: any
|
||||
oninput?: any
|
||||
}
|
||||
|
||||
let {
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
value = $bindable((max - min) / 2),
|
||||
...rest
|
||||
}: Props = $props()
|
||||
let {
|
||||
min = 0,
|
||||
max = 100,
|
||||
step = 1,
|
||||
value = $bindable((max - min) / 2),
|
||||
...rest
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
style="writing-mode: vertical-lr; direction: rtl"
|
||||
class="cursor-pointer"
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
bind:value
|
||||
{...rest} />
|
||||
type="range"
|
||||
style="writing-mode: vertical-lr; direction: rtl"
|
||||
class="cursor-pointer"
|
||||
{min}
|
||||
{max}
|
||||
{step}
|
||||
bind:value
|
||||
{...rest}
|
||||
/>
|
||||
|
||||
<style>
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
background: oklch(var(--p) / 1);
|
||||
border-radius: var(--rounded-box, 1rem);
|
||||
}
|
||||
input[type='range']::-webkit-slider-runnable-track {
|
||||
background: oklch(var(--p) / 1);
|
||||
border-radius: var(--rounded-box, 1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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,40 +1,41 @@
|
||||
<script lang="ts">
|
||||
import WidgetContainer from './WidgetContainer.svelte';
|
||||
import {
|
||||
WidgetComponents,
|
||||
type WidgetContainerConfig,
|
||||
isWidgetConfig,
|
||||
} from '$lib/stores/application';
|
||||
import Widget from './Widget.svelte';
|
||||
import WidgetContainer from './WidgetContainer.svelte'
|
||||
import {
|
||||
WidgetComponents,
|
||||
type WidgetContainerConfig,
|
||||
isWidgetConfig
|
||||
} from '$lib/stores/application'
|
||||
import Widget from './Widget.svelte'
|
||||
|
||||
interface Props {
|
||||
container: WidgetContainerConfig;
|
||||
}
|
||||
interface Props {
|
||||
container: WidgetContainerConfig
|
||||
}
|
||||
|
||||
let { container }: Props = $props();
|
||||
let { container }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full h-full"
|
||||
class:flex-row={container.layout === 'column'}
|
||||
class:flex-col={container.layout === 'row'}
|
||||
class:flex-wrap={container.layout === 'wrap'}
|
||||
>
|
||||
{#each container.widgets as widget, index (widget.id + '-' + index)}
|
||||
<Widget>
|
||||
{#if isWidgetConfig(widget)}
|
||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||
<SvelteComponent {...widget.props} />
|
||||
{:else if widget.widgets}
|
||||
<WidgetContainer container={widget} />
|
||||
{/if}
|
||||
</Widget>
|
||||
{#if index !== container.widgets.length - 1}
|
||||
<div
|
||||
class="divider bg-base-300 m-0"
|
||||
class:divider-horizontal={container.layout === 'column'}
|
||||
></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { Github } from "../icons";
|
||||
import { Github } from '../icons'
|
||||
|
||||
interface Props {
|
||||
github: any;
|
||||
}
|
||||
interface Props {
|
||||
github: any
|
||||
}
|
||||
|
||||
let { github }: Props = $props();
|
||||
let { github }: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if github.active}
|
||||
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer">
|
||||
<a href={github.href} class="btn btn-ghost" target="_blank" rel="noopener noreferrer">
|
||||
<Github class="h-5 w-5" />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script>
|
||||
import logo from '$lib/assets/logo512.png';
|
||||
import logo from '$lib/assets/logo512.png'
|
||||
|
||||
/** @type {{appName: any}} */
|
||||
let { appName } = $props();
|
||||
/** @type {{appName: any}} */
|
||||
let { appName } = $props()
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
|
||||
>
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||
<a href="/" class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]">
|
||||
<img src={logo} alt="Logo" class="h-12 w-12" />
|
||||
<h1 class="px-4 text-2xl font-bold">{appName}</h1>
|
||||
</a>
|
||||
|
||||
@@ -1,194 +1,198 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { base } from '$app/paths'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import GithubButton from '../menu/GithubButton.svelte'
|
||||
import LogoButton from '../menu/LogoButton.svelte'
|
||||
import MenuList from '../menu/MenuList.svelte'
|
||||
import {
|
||||
Connection,
|
||||
Settings,
|
||||
MdiController,
|
||||
Devices,
|
||||
Camera,
|
||||
Rotate3d,
|
||||
MotorOutline,
|
||||
Health,
|
||||
Folder,
|
||||
Update,
|
||||
WiFi,
|
||||
Router,
|
||||
AP,
|
||||
Copyright,
|
||||
Metrics,
|
||||
DNS
|
||||
} from '$lib/components/icons'
|
||||
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
|
||||
import { page } from '$app/state'
|
||||
import { base } from '$app/paths'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import GithubButton from '../menu/GithubButton.svelte'
|
||||
import LogoButton from '../menu/LogoButton.svelte'
|
||||
import MenuList from '../menu/MenuList.svelte'
|
||||
import {
|
||||
Connection,
|
||||
Settings,
|
||||
MdiController,
|
||||
Devices,
|
||||
Camera,
|
||||
Rotate3d,
|
||||
MotorOutline,
|
||||
Health,
|
||||
Folder,
|
||||
Update,
|
||||
WiFi,
|
||||
Router,
|
||||
AP,
|
||||
Copyright,
|
||||
Metrics,
|
||||
DNS
|
||||
} from '$lib/components/icons'
|
||||
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
|
||||
|
||||
const features = useFeatureFlags()
|
||||
const features = useFeatureFlags()
|
||||
|
||||
const appName = page.data.app_name
|
||||
const appName = page.data.app_name
|
||||
|
||||
const copyright = page.data.copyright
|
||||
const copyright = page.data.copyright
|
||||
|
||||
const github = { href: 'https://github.com/' + page.data.github, active: true }
|
||||
const github = { href: 'https://github.com/' + page.data.github, active: true }
|
||||
|
||||
type menuItem = {
|
||||
title: string
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
submenu?: menuItem[]
|
||||
}
|
||||
type menuItem = {
|
||||
title: string
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
submenu?: menuItem[]
|
||||
}
|
||||
|
||||
function withBase(path: string) {
|
||||
return `${base}${path.startsWith('/') ? path : '/' + path}`
|
||||
}
|
||||
function withBase(path: string) {
|
||||
return `${base}${path.startsWith('/') ? path : '/' + path}`
|
||||
}
|
||||
|
||||
let menuItems = $state<menuItem[]>([])
|
||||
let menuItems = $state<menuItem[]>([])
|
||||
|
||||
$effect(() => {
|
||||
menuItems = [
|
||||
{
|
||||
title: 'Connection',
|
||||
icon: WiFi,
|
||||
href: withBase('/connection'),
|
||||
feature: !PUBLIC_VITE_USE_HOST_NAME
|
||||
},
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: withBase('/controller'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Peripherals',
|
||||
icon: Devices,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'I2C',
|
||||
icon: Connection,
|
||||
href: withBase('/peripherals/i2c'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Camera',
|
||||
icon: Camera,
|
||||
href: withBase('/peripherals/camera'),
|
||||
feature: $features.camera
|
||||
},
|
||||
{
|
||||
title: 'Servo',
|
||||
icon: MotorOutline,
|
||||
href: withBase('/peripherals/servo'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'IMU',
|
||||
icon: Rotate3d,
|
||||
href: withBase('/peripherals/imu'),
|
||||
feature: $features.imu || $features.mag || $features.bmp
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
icon: WiFi,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: withBase('/wifi/sta'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: withBase('/wifi/ap'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'mDNS',
|
||||
icon: DNS,
|
||||
href: withBase('/wifi/mdns'),
|
||||
feature: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
icon: Settings,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: withBase('/system/status'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'File System',
|
||||
icon: Folder,
|
||||
href: withBase('/system/filesystem'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: withBase('/system/metrics'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: withBase('/system/update'),
|
||||
feature: $features.ota || $features.upload_firmware || $features.download_firmware
|
||||
}
|
||||
]
|
||||
}
|
||||
] as menuItem[]
|
||||
})
|
||||
|
||||
const { menuClicked } = $props()
|
||||
|
||||
function setActiveMenuItem(targetTitle: string) {
|
||||
menuItems.forEach(item => {
|
||||
item.active = item.title === targetTitle
|
||||
item.submenu?.forEach(subItem => {
|
||||
subItem.active = subItem.title === targetTitle
|
||||
})
|
||||
$effect(() => {
|
||||
menuItems = [
|
||||
{
|
||||
title: 'Connection',
|
||||
icon: WiFi,
|
||||
href: withBase('/connection'),
|
||||
feature: !PUBLIC_VITE_USE_HOST_NAME
|
||||
},
|
||||
{
|
||||
title: 'Controller',
|
||||
icon: MdiController,
|
||||
href: withBase('/controller'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Peripherals',
|
||||
icon: Devices,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'I2C',
|
||||
icon: Connection,
|
||||
href: withBase('/peripherals/i2c'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Camera',
|
||||
icon: Camera,
|
||||
href: withBase('/peripherals/camera'),
|
||||
feature: $features.camera
|
||||
},
|
||||
{
|
||||
title: 'Servo',
|
||||
icon: MotorOutline,
|
||||
href: withBase('/peripherals/servo'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'IMU',
|
||||
icon: Rotate3d,
|
||||
href: withBase('/peripherals/imu'),
|
||||
feature: $features.imu || $features.mag || $features.bmp
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'WiFi',
|
||||
icon: WiFi,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'WiFi Station',
|
||||
icon: Router,
|
||||
href: withBase('/wifi/sta'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Access Point',
|
||||
icon: AP,
|
||||
href: withBase('/wifi/ap'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'mDNS',
|
||||
icon: DNS,
|
||||
href: withBase('/wifi/mdns'),
|
||||
feature: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'System',
|
||||
icon: Settings,
|
||||
feature: true,
|
||||
submenu: [
|
||||
{
|
||||
title: 'System Status',
|
||||
icon: Health,
|
||||
href: withBase('/system/status'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'File System',
|
||||
icon: Folder,
|
||||
href: withBase('/system/filesystem'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'System Metrics',
|
||||
icon: Metrics,
|
||||
href: withBase('/system/metrics'),
|
||||
feature: true
|
||||
},
|
||||
{
|
||||
title: 'Firmware Update',
|
||||
icon: Update,
|
||||
href: withBase('/system/update'),
|
||||
feature:
|
||||
$features.ota ||
|
||||
$features.upload_firmware ||
|
||||
$features.download_firmware
|
||||
}
|
||||
]
|
||||
}
|
||||
] as menuItem[]
|
||||
})
|
||||
menuItems = menuItems
|
||||
menuClicked()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setActiveMenuItem(page.data.title)
|
||||
})
|
||||
const { menuClicked } = $props()
|
||||
|
||||
const updateMenu = (event: any) => {
|
||||
setActiveMenuItem(event.details)
|
||||
}
|
||||
function setActiveMenuItem(targetTitle: string) {
|
||||
menuItems.forEach(item => {
|
||||
item.active = item.title === targetTitle
|
||||
item.submenu?.forEach(subItem => {
|
||||
subItem.active = subItem.title === targetTitle
|
||||
})
|
||||
})
|
||||
menuItems = menuItems
|
||||
menuClicked()
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setActiveMenuItem(page.data.title)
|
||||
})
|
||||
|
||||
const updateMenu = (event: any) => {
|
||||
setActiveMenuItem(event.details)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
|
||||
<LogoButton {appName} />
|
||||
<LogoButton {appName} />
|
||||
|
||||
<MenuList
|
||||
{menuItems}
|
||||
select={updateMenu}
|
||||
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
|
||||
level="0" />
|
||||
<MenuList
|
||||
{menuItems}
|
||||
select={updateMenu}
|
||||
class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
|
||||
level="0"
|
||||
/>
|
||||
|
||||
<div class="divider my-0"></div>
|
||||
<div class="divider my-0"></div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<GithubButton {github} />
|
||||
<div class="flex items-center justify-end text-sm gap-2">
|
||||
<Copyright class="h-4 w-4" />{copyright}
|
||||
<div class="flex items-center justify-between">
|
||||
<GithubButton {github} />
|
||||
<div class="flex items-center justify-end text-sm gap-2">
|
||||
<Copyright class="h-4 w-4" />{copyright}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,54 @@
|
||||
<script lang="ts">
|
||||
import MenuList from './MenuList.svelte'
|
||||
type MenuItem = {
|
||||
title: string
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
submenu?: MenuItem[]
|
||||
}
|
||||
import MenuList from './MenuList.svelte'
|
||||
type MenuItem = {
|
||||
title: string
|
||||
icon: ConstructorOfATypedSvelteComponent
|
||||
href?: string
|
||||
feature: boolean
|
||||
active?: boolean
|
||||
submenu?: MenuItem[]
|
||||
}
|
||||
|
||||
let { level, menuItems, select, class: klass } = $props()
|
||||
let { level, menuItems, select, class: klass } = $props()
|
||||
|
||||
const selectMenuItem = (title: string) => {
|
||||
select(title)
|
||||
}
|
||||
const selectMenuItem = (title: string) => {
|
||||
select(title)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul class={klass + ' menu w-full'}>
|
||||
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
<li>
|
||||
{#if menuItem.submenu}
|
||||
<details open={menuItem.submenu.some(subItem => subItem.active)}>
|
||||
<summary class="font-bold">
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
<div class="pl-4">
|
||||
<MenuList menuItems={menuItem.submenu} level={level + 1} {select} class={klass} />
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a
|
||||
href={menuItem.href}
|
||||
class="font-bold"
|
||||
class:bg-base-100={menuItem.active}
|
||||
class:text-lg={level === 0}
|
||||
class:text-md={level === 1}
|
||||
onclick={() => selectMenuItem(menuItem.title)}>
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</a>
|
||||
{#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
|
||||
{#if menuItem.feature}
|
||||
<li>
|
||||
{#if menuItem.submenu}
|
||||
<details open={menuItem.submenu.some(subItem => subItem.active)}>
|
||||
<summary class="font-bold">
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</summary>
|
||||
<div class="pl-4">
|
||||
<MenuList
|
||||
menuItems={menuItem.submenu}
|
||||
level={level + 1}
|
||||
{select}
|
||||
class={klass}
|
||||
/>
|
||||
</div>
|
||||
</details>
|
||||
{:else}
|
||||
<a
|
||||
href={menuItem.href}
|
||||
class="font-bold"
|
||||
class:bg-base-100={menuItem.active}
|
||||
class:text-lg={level === 0}
|
||||
class:text-md={level === 1}
|
||||
onclick={() => selectMenuItem(menuItem.title)}
|
||||
>
|
||||
<menuItem.icon class="h-6 w-6" />
|
||||
{menuItem.title}
|
||||
</a>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { isFullscreen, toggleFullscreen } from '$lib/stores';
|
||||
import { MdiFullscreenExit, MdiFullscreen } from '../icons';
|
||||
import { isFullscreen, toggleFullscreen } from '$lib/stores'
|
||||
import { MdiFullscreenExit, MdiFullscreen } from '../icons'
|
||||
|
||||
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
|
||||
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen)
|
||||
</script>
|
||||
|
||||
<button onclick={toggleFullscreen}>
|
||||
<SvelteComponent class="h-7 w-7" />
|
||||
</button>
|
||||
</button>
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
|
||||
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from '../icons'
|
||||
|
||||
interface Props {
|
||||
showDBm?: boolean;
|
||||
rssi?: number;
|
||||
}
|
||||
interface Props {
|
||||
showDBm?: boolean
|
||||
rssi?: number
|
||||
}
|
||||
|
||||
let { showDBm = false, rssi = 0 }: Props = $props();
|
||||
let { showDBm = false, rssi = 0 }: Props = $props()
|
||||
|
||||
const getWiFiIcon = () => {
|
||||
if (rssi === 0) return WifiOff;
|
||||
if (rssi >= -55) return WiFi;
|
||||
if (rssi >= -75) return WiFi2;
|
||||
if (rssi >= -85) return WiFi1;
|
||||
return WiFi0;
|
||||
};
|
||||
const getWiFiIcon = () => {
|
||||
if (rssi === 0) return WifiOff
|
||||
if (rssi >= -55) return WiFi
|
||||
if (rssi >= -75) return WiFi2
|
||||
if (rssi >= -85) return WiFi1
|
||||
return WiFi0
|
||||
}
|
||||
|
||||
const SvelteComponent = $derived(getWiFiIcon());
|
||||
const SvelteComponent = $derived(getWiFiIcon())
|
||||
</script>
|
||||
|
||||
<div class="indicator">
|
||||
<div class="tooltip tooltip-left" data-tip={rssi + " dBm"}>
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi} dBm
|
||||
</span>
|
||||
{/if}
|
||||
<div class="h-7 w-7">
|
||||
<SvelteComponent class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tooltip tooltip-left" data-tip={rssi + ' dBm'}>
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi} dBm
|
||||
</span>
|
||||
{/if}
|
||||
<div class="h-7 w-7">
|
||||
<SvelteComponent class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { useFeatureFlags } from '$lib/stores';
|
||||
import { modals } from 'svelte-modals';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { api } from '$lib/api';
|
||||
import { Cancel, Power } from '../icons';
|
||||
import { useFeatureFlags } from '$lib/stores'
|
||||
import { modals } from 'svelte-modals'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import { api } from '$lib/api'
|
||||
import { Cancel, Power } from '../icons'
|
||||
|
||||
const features = useFeatureFlags();
|
||||
const features = useFeatureFlags()
|
||||
|
||||
const postSleep = async () => await api.post('/api/system/sleep');
|
||||
const postSleep = async () => await api.post('/api/system/sleep')
|
||||
|
||||
const confirmSleep = () => {
|
||||
modals.open(ConfirmDialog, {
|
||||
@@ -18,11 +18,11 @@
|
||||
confirm: { label: 'Switch Off', icon: Power }
|
||||
},
|
||||
onConfirm: () => {
|
||||
modals.close();
|
||||
postSleep();
|
||||
modals.close()
|
||||
postSleep()
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $features.sleep}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { mode, modes } from "$lib/stores";
|
||||
import { mode, modes } from '$lib/stores'
|
||||
|
||||
const deactivate = async () => {
|
||||
mode.set(modes.indexOf('deactivated'));
|
||||
};
|
||||
mode.set(modes.indexOf('deactivated'))
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
|
||||
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { MdiWeatherSunny, MdiMoonAndStars } from "../icons";
|
||||
import { MdiWeatherSunny, MdiMoonAndStars } from '../icons'
|
||||
</script>
|
||||
|
||||
<label class="swap swap-rotate">
|
||||
<input type="checkbox" value="light" class="theme-controller" />
|
||||
<MdiWeatherSunny class="swap-off h-7 w-7" />
|
||||
<MdiMoonAndStars class="swap-on h-7 w-7" />
|
||||
</label>
|
||||
</label>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {Hamburger} from '../icons'
|
||||
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">
|
||||
<div class="flex gap-2 p-2">
|
||||
<a href="/">
|
||||
<Hamburger class="h-8 w-8"/>
|
||||
<Hamburger class="h-8 w-8" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topbar {
|
||||
height: 50px;
|
||||
}
|
||||
.topbar {
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,109 +1,111 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { modals } from 'svelte-modals'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'
|
||||
import { compareVersions } from 'compare-versions'
|
||||
import { onMount } from 'svelte'
|
||||
import { api } from '$lib/api'
|
||||
import type { GithubRelease } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Cancel, CloudDown, Firmware } from '../icons'
|
||||
import { page } from '$app/state'
|
||||
import { modals } from 'svelte-modals'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'
|
||||
import { compareVersions } from 'compare-versions'
|
||||
import { onMount } from 'svelte'
|
||||
import { api } from '$lib/api'
|
||||
import type { GithubRelease } from '$lib/types/models'
|
||||
import { useFeatureFlags } from '$lib/stores/featureFlags'
|
||||
import { Cancel, CloudDown, Firmware } from '../icons'
|
||||
|
||||
const features = useFeatureFlags()
|
||||
const features = useFeatureFlags()
|
||||
|
||||
interface Props {
|
||||
update?: boolean
|
||||
}
|
||||
|
||||
let { update = $bindable(false) }: Props = $props()
|
||||
|
||||
let firmwareVersion: string = $state('')
|
||||
let firmwareDownloadLink: string = $state('')
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
)
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository')
|
||||
return
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
interface Props {
|
||||
update?: boolean
|
||||
}
|
||||
|
||||
const results = result.inner
|
||||
update = false
|
||||
firmwareVersion = ''
|
||||
let { update = $bindable(false) }: Props = $props()
|
||||
|
||||
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target as string)
|
||||
) {
|
||||
update = true
|
||||
firmwareVersion = results.tag_name
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url
|
||||
notifications.info('Firmware update available.', 5000)
|
||||
let firmwareVersion: string = $state('')
|
||||
let firmwareDownloadLink: string = $state('')
|
||||
|
||||
async function getGithubAPI() {
|
||||
const headers = {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
const result = await api.get<GithubRelease>(
|
||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
||||
{ headers }
|
||||
)
|
||||
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
|
||||
console.warn('Error: Could not find releases in the repository')
|
||||
return
|
||||
}
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url })
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
}
|
||||
const results = result.inner
|
||||
update = false
|
||||
firmwareVersion = ''
|
||||
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI()
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000) // once per hour
|
||||
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($features.firmware_built_target as string)
|
||||
) {
|
||||
update = true
|
||||
firmwareVersion = results.tag_name
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url
|
||||
notifications.info('Firmware update available.', 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url)
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
})
|
||||
}
|
||||
async function postGithubDownload(url: string) {
|
||||
const result = await api.post('/api/downloadUpdate', { download_url: url })
|
||||
if (result.isErr()) {
|
||||
console.error('Error:', result.inner)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if ($features.download_firmware) {
|
||||
await getGithubAPI()
|
||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000) // once per hour
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
modals.open(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url)
|
||||
modals.open(GithubUpdateDialog, {
|
||||
onConfirm: () => modals.closeAll()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1">
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="indicator flex-none">
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>
|
||||
{firmwareVersion}
|
||||
</span>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { selectedView, views } from "$lib/stores/application";
|
||||
import Selector from "../widget/Selector.svelte";
|
||||
import { selectedView, views } from '$lib/stores/application'
|
||||
import Selector from '../widget/Selector.svelte'
|
||||
</script>
|
||||
|
||||
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} />
|
||||
<Selector bind:selectedOption={$selectedView} options={$views.map(v => v.name)} />
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state'
|
||||
import { telemetry } from '$lib/stores/telemetry'
|
||||
import { page } from '$app/state'
|
||||
import { telemetry } from '$lib/stores/telemetry'
|
||||
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte'
|
||||
import UpdateIndicator from '$lib/components/statusbar/UpdateIndicator.svelte'
|
||||
import SleepButton from './SleepButton.svelte'
|
||||
import ThemeButton from './ThemeButton.svelte'
|
||||
import FullscreenButton from './FullscreenButton.svelte'
|
||||
import StopButton from './StopButton.svelte'
|
||||
import ViewSelector from './ViewSelector.svelte'
|
||||
import { Hamburger } from '../icons'
|
||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte'
|
||||
import UpdateIndicator from '$lib/components/statusbar/UpdateIndicator.svelte'
|
||||
import SleepButton from './SleepButton.svelte'
|
||||
import ThemeButton from './ThemeButton.svelte'
|
||||
import FullscreenButton from './FullscreenButton.svelte'
|
||||
import StopButton from './StopButton.svelte'
|
||||
import ViewSelector from './ViewSelector.svelte'
|
||||
import { Hamburger } from '../icons'
|
||||
</script>
|
||||
|
||||
<div class="navbar bg-base-300 sticky top-0 z-10 h-12 min-h-fit drop-shadow-lg lg:h-16 gap-2 pr-0">
|
||||
<div class="flex flex-1 gap-2">
|
||||
<label for="main-menu" class="btn btn-ghost btn-circle btn-sm drawer-button">
|
||||
<Hamburger class="h-6 w-auto" />
|
||||
</label>
|
||||
{#if page.data.title === 'Controller'}
|
||||
<ViewSelector />
|
||||
{:else}
|
||||
<h1 class="px-2 text-xl font-bold lg:text-2xl">{page.data.title}</h1>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-1 gap-2">
|
||||
<label for="main-menu" class="btn btn-ghost btn-circle btn-sm drawer-button">
|
||||
<Hamburger class="h-6 w-auto" />
|
||||
</label>
|
||||
{#if page.data.title === 'Controller'}
|
||||
<ViewSelector />
|
||||
{:else}
|
||||
<h1 class="px-2 text-xl font-bold lg:text-2xl">{page.data.title}</h1>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<UpdateIndicator />
|
||||
<UpdateIndicator />
|
||||
|
||||
<FullscreenButton />
|
||||
<FullscreenButton />
|
||||
|
||||
<ThemeButton />
|
||||
<ThemeButton />
|
||||
|
||||
<RssiIndicator rssi={$telemetry.rssi.rssi} />
|
||||
<RssiIndicator rssi={$telemetry.rssi.rssi} />
|
||||
|
||||
<SleepButton />
|
||||
<SleepButton />
|
||||
|
||||
<StopButton />
|
||||
<StopButton />
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,37 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import { error, info, success, warning } from '../icons';
|
||||
import { flip } from 'svelte/animate'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { notifications } from '$lib/components/toasts/notifications'
|
||||
import { error, info, success, warning } from '../icons'
|
||||
|
||||
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let { theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
}, icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
} } = $props();
|
||||
/** @type {{theme?: any, icon?: any}} */
|
||||
let {
|
||||
theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
},
|
||||
icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
}
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4 z-20">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{@const SvelteComponent = icon[notification.type]}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<SvelteComponent class="h-6 w-6 shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{#each $notifications as notification (notification.id)}
|
||||
{@const SvelteComponent = icon[notification.type]}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<SvelteComponent class="h-6 w-6 shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -3,40 +3,40 @@ import { writable, derived, type Writable } from 'svelte/store'
|
||||
type StateType = 'info' | 'success' | 'warning' | 'error'
|
||||
|
||||
type State = {
|
||||
id: string
|
||||
type: StateType
|
||||
message: string
|
||||
id: string
|
||||
type: StateType
|
||||
message: string
|
||||
}
|
||||
|
||||
function createNotificationStore() {
|
||||
const state: State[] = []
|
||||
const notifications = writable(state)
|
||||
const { subscribe } = notifications
|
||||
const state: State[] = []
|
||||
const notifications = writable(state)
|
||||
const { subscribe } = notifications
|
||||
|
||||
function send(message: string, type: StateType = 'info', timeout: number) {
|
||||
const id = generateId()
|
||||
setTimeout(() => {
|
||||
notifications.update(state => {
|
||||
return state.filter(n => n.id !== id)
|
||||
})
|
||||
}, timeout)
|
||||
notifications.update(state => {
|
||||
return [...state, { id, type, message }]
|
||||
})
|
||||
}
|
||||
function send(message: string, type: StateType = 'info', timeout: number) {
|
||||
const id = generateId()
|
||||
setTimeout(() => {
|
||||
notifications.update(state => {
|
||||
return state.filter(n => n.id !== id)
|
||||
})
|
||||
}, timeout)
|
||||
notifications.update(state => {
|
||||
return [...state, { id, type, message }]
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
|
||||
}
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number = 4000) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number = 4000) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number = 4000) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number = 4000) => send(msg, 'success', timeout)
|
||||
}
|
||||
}
|
||||
|
||||
function generateId() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9)
|
||||
return '_' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore()
|
||||
|
||||
@@ -1,101 +1,102 @@
|
||||
<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;
|
||||
let chartElement: HTMLCanvasElement
|
||||
let chart: Chart
|
||||
|
||||
interface Props {
|
||||
label: any;
|
||||
data: number[];
|
||||
title: any;
|
||||
}
|
||||
interface Props {
|
||||
label: any
|
||||
data: number[]
|
||||
title: any
|
||||
}
|
||||
|
||||
let { label, data, title }: Props = $props();
|
||||
let { label, data, title }: Props = $props()
|
||||
|
||||
Chart.register(...registerables);
|
||||
Chart.register(...registerables)
|
||||
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y',
|
||||
},
|
||||
],
|
||||
},
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10),
|
||||
onMount(() => {
|
||||
chart = new Chart(chartElement, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data,
|
||||
datasets: [
|
||||
{
|
||||
label,
|
||||
borderColor: daisyColor('--p'),
|
||||
backgroundColor: daisyColor('--p', 50),
|
||||
borderWidth: 2,
|
||||
data,
|
||||
yAxisID: 'y'
|
||||
}
|
||||
]
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
display: false,
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold',
|
||||
},
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc'),
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
options: {
|
||||
maintainAspectRatio: false,
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
elements: {
|
||||
point: {
|
||||
radius: 0
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: daisyColor('--bc', 10)
|
||||
},
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
display: false
|
||||
},
|
||||
y: {
|
||||
type: 'linear',
|
||||
title: {
|
||||
display: true,
|
||||
text: title,
|
||||
color: daisyColor('--bc'),
|
||||
font: {
|
||||
size: 16,
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
position: 'left',
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: daisyColor('--bc', 10) },
|
||||
ticks: {
|
||||
color: daisyColor('--bc')
|
||||
},
|
||||
border: { color: daisyColor('--bc', 10) }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setInterval(() => {
|
||||
chart.data.labels = data;
|
||||
chart.data.datasets[0].data = data;
|
||||
}, 500);
|
||||
});
|
||||
setInterval(() => {
|
||||
chart.data.labels = data
|
||||
chart.data.datasets[0].data = data
|
||||
}, 500)
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full overflow-x-auto">
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col space-y-1 h-60"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<canvas bind:this={chartElement}></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: string[];
|
||||
selectedOption?: string;
|
||||
change?: () => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
interface Props {
|
||||
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 || ''}">
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
bind:value={selectedOption}
|
||||
{...rest}
|
||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option}>{option}</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user