🎨 format

This commit is contained in:
Rune Harlyk
2025-10-11 10:42:32 +02:00
parent 4d51b9f556
commit 91a7b170fe
139 changed files with 6645 additions and 6317 deletions
+1 -1
View File
@@ -28,4 +28,4 @@ module.exports = {
} }
} }
] ]
}; }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"useTabs": false, "useTabs": false,
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 4,
"trailingComma": "none", "trailingComma": "none",
"arrowParens": "avoid", "arrowParens": "avoid",
"experimentalTernaries": true, "experimentalTernaries": true,
+5 -1
View File
@@ -1,3 +1,7 @@
{ {
"recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"] "recommendations": [
"svelte.svelte-vscode",
"bradlc.vscode-tailwindcss",
"esbenp.prettier-vscode"
]
} }
+4 -4
View File
@@ -1,8 +1,8 @@
declare module "app-env" { declare module 'app-env' {
interface ENV { interface ENV {
VITE_USE_HOST_NAME: boolean; VITE_USE_HOST_NAME: boolean
} }
const appEnv: ENV; const appEnv: ENV
export default appEnv; export default appEnv
} }
+3 -3
View File
@@ -1,4 +1,4 @@
import type { PlaywrightTestConfig } from '@playwright/test'; import type { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = { const config: PlaywrightTestConfig = {
webServer: { webServer: {
@@ -7,6 +7,6 @@ const config: PlaywrightTestConfig = {
}, },
testDir: 'tests/integration', testDir: 'tests/integration',
testMatch: /(.+\.)?(test|spec)\.[jt]s/ testMatch: /(.+\.)?(test|spec)\.[jt]s/
}; }
export default config; export default config
+8
View File
@@ -23,6 +23,14 @@
--base-content: oklch(0.3 0.012 256); --base-content: oklch(0.3 0.012 256);
} }
button {
cursor: pointer;
}
button:disabled {
cursor: not-allowed;
}
#nipple_0_0, #nipple_0_0,
#nipple_1_1 { #nipple_1_1 {
z-index: 10 !important; z-index: 10 !important;
+1 -1
View File
@@ -10,4 +10,4 @@ declare global {
} }
} }
export {}; export {}
+4 -1
View File
@@ -3,7 +3,10 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/logo512.png" /> <link rel="icon" href="%sveltekit.assets%/logo512.png" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" /> <meta
name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"
/>
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
%sveltekit.head% %sveltekit.head%
-7
View File
@@ -1,7 +0,0 @@
import { describe, it, expect } from 'vitest';
describe('sum test', () => {
it('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
});
+24 -25
View File
@@ -1,22 +1,22 @@
import { get } from 'svelte/store'; import { get } from 'svelte/store'
import { Err, Ok, type Result } from './utilities'; import { Err, Ok, type Result } from './utilities'
import { location } from './stores'; import { location } from './stores'
export namespace api { export namespace api {
export function get<TResponse>(endpoint: string, params?: RequestInit) { export function get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params); return sendRequest<TResponse>(endpoint, 'GET', null, params)
} }
export function post<TResponse>(endpoint: string, data?: unknown) { export function post<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'POST', data); return sendRequest<TResponse>(endpoint, 'POST', data)
} }
export function put<TResponse>(endpoint: string, data?: unknown) { export function put<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'PUT', data); return sendRequest<TResponse>(endpoint, 'PUT', data)
} }
export function remove<TResponse>(endpoint: string) { export function remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE'); return sendRequest<TResponse>(endpoint, 'DELETE')
} }
} }
@@ -26,8 +26,8 @@ async function sendRequest<TResponse>(
data?: unknown, data?: unknown,
params?: RequestInit params?: RequestInit
): Promise<Result<TResponse, Error>> { ): Promise<Result<TResponse, Error>> {
endpoint = resolveUrl(endpoint); endpoint = resolveUrl(endpoint)
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined; const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined
const request = { const request = {
...params, ...params,
@@ -38,43 +38,42 @@ async function sendRequest<TResponse>(
Authorization: 'Basic', Authorization: 'Basic',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
}; }
let response; let response
try { try {
response = await fetch(endpoint, request); response = await fetch(endpoint, request)
} catch (error) { } catch (error) {
return Err.new(new Error(), 'An error has occurred'); return Err.new(new Error(), 'An error has occurred')
} }
const isResponseOk = response.status >= 200 && response.status < 400; const isResponseOk = response.status >= 200 && response.status < 400
if (!isResponseOk) { if (!isResponseOk) {
if (response.status === 401) { if (response.status === 401) {
return Err.new(new ApiError(response), 'User was not authorized'); return Err.new(new ApiError(response), 'User was not authorized')
} }
return Err.new(new ApiError(response), 'An error has occurred'); return Err.new(new ApiError(response), 'An error has occurred')
} }
const contentType = const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type')
response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) { if (contentType && contentType.includes('application/json')) {
const data = await response.json(); const data = await response.json()
return Ok.new(data as TResponse); return Ok.new(data as TResponse)
} else { } else {
// Handle empty object as response // Handle empty object as response
return Ok.new(null as TResponse); return Ok.new(null as TResponse)
} }
} }
function resolveUrl(url: string): string { function resolveUrl(url: string): string {
if (url.startsWith('http') || !get(location)) return url; if (url.startsWith('http') || !get(location)) return url
const protocol = window.location.protocol; const protocol = window.location.protocol
return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`; return `${protocol}//${get(location)}${url.startsWith('/') ? '' : '/'}${url}`
} }
export class ApiError extends Error { export class ApiError extends Error {
constructor(public readonly response: Response) { constructor(public readonly response: Response) {
super(`${response.status}`); super(`${response.status}`)
} }
} }
+7 -7
View File
@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { Down } from './icons'; import { Down } from './icons'
function openCollapsible() { function openCollapsible() {
open = !open; open = !open
if (open) { if (open) {
opened(); opened()
} else { } else {
closed(); closed()
} }
} }
let { icon, title, children, open, opened, closed, class: klass } = $props(); let { icon, title, children, open, opened, closed, class: klass } = $props()
</script> </script>
<div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden"> <div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
+8 -3
View File
@@ -23,15 +23,20 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
use:exitBeforeEnter use:exitBeforeEnter
use:focusTrap> use:focusTrap
>
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"> class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}> <button
class="btn btn-error inline-flex items-center"
onclick={() => modals.close()}
>
<labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span> <labels.cancel.icon class="mr-2 h-5 w-5" /><span>{labels?.cancel.label}</span>
</button> </button>
<button class="btn btn-primary inline-flex items-center" onclick={onConfirm}> <button class="btn btn-primary inline-flex items-center" onclick={onConfirm}>
@@ -1,61 +1,61 @@
<script lang="ts"> <script lang="ts">
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { telemetry } from '$lib/stores/telemetry'; import { telemetry } from '$lib/stores/telemetry'
import { Cancel } from './icons'; import { Cancel } from './icons'
import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals'; import { modals, exitBeforeEnter, onBeforeClose } from 'svelte-modals'
// provided by <Modals /> // provided by <Modals />
interface Props { interface Props {
isOpen: boolean; isOpen: boolean
} }
let { isOpen }: Props = $props(); let { isOpen }: Props = $props()
let updating = $state(true); let updating = $state(true)
let progress = $state(0); let progress = $state(0)
$effect(() => { $effect(() => {
if ($telemetry.download_ota.status == 'progress') { if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress; progress = $telemetry.download_ota.progress
} }
}); })
$effect(() => { $effect(() => {
if ($telemetry.download_ota.status == 'error') { if ($telemetry.download_ota.status == 'error') {
updating = false; updating = false
} }
}); })
let message = $state('Preparing ...'); let message = $state('Preparing ...')
$effect(() => { $effect(() => {
if ($telemetry.download_ota.status == 'progress') { if ($telemetry.download_ota.status == 'progress') {
message = 'Downloading ...'; message = 'Downloading ...'
} else if ($telemetry.download_ota.status == 'error') { } else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error; message = $telemetry.download_ota.error
} else if ($telemetry.download_ota.status == 'finished') { } else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...'; message = 'Restarting ...'
progress = 0; progress = 0
// Reload page after 5 sec // Reload page after 5 sec
setTimeout(() => { setTimeout(() => {
modals.closeAll(); modals.closeAll()
location.reload(); location.reload()
}, 5000); }, 5000)
} }
}); })
onBeforeClose(() => { onBeforeClose(() => {
if (updating) { if (updating) {
// prevents modal from closing // prevents modal from closing
return false; return false
} else { } else {
$telemetry.download_ota.status = 'idle'; $telemetry.download_ota.status = 'idle'
$telemetry.download_ota.error = ''; $telemetry.download_ota.error = ''
$telemetry.download_ota.progress = 0; $telemetry.download_ota.progress = 0
return true; return true
} }
}); })
</script> </script>
{#if isOpen} {#if isOpen}
@@ -89,8 +89,8 @@
class="btn btn-warning text-warning-content inline-flex flex-none items-center" class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating} disabled={updating}
onclick={() => { onclick={() => {
modals.closeAll(); modals.closeAll()
location.reload(); location.reload()
}} }}
> >
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button <Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
+13 -10
View File
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { Check } from './icons'; import { Check } from './icons'
import { exitBeforeEnter, type ModalProps } from 'svelte-modals'; import { exitBeforeEnter, type ModalProps } from 'svelte-modals'
let { let {
isOpen, isOpen,
@@ -10,9 +10,9 @@
message, message,
onDismiss, onDismiss,
labels = { labels = {
dismiss: { label: 'Dismiss', icon: Check }, dismiss: { label: 'Dismiss', icon: Check }
}, }
}: ModalProps = $props(); }: ModalProps = $props()
</script> </script>
{#if isOpen} {#if isOpen}
@@ -21,9 +21,11 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
use:exitBeforeEnter use:exitBeforeEnter
use:focusTrap> use:focusTrap
>
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"> class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <p class="text-base-content mb-1 text-start">{message}</p>
@@ -31,7 +33,8 @@
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <button
class="btn btn-warning text-warning-content inline-flex items-center" class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}> onclick={onDismiss}
>
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span> <labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
</button> </button>
</div> </div>
@@ -1,15 +1,15 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte'
import * as THREE from 'three'; import * as THREE from 'three'
import { imu } from '$lib/stores/imu'; import { imu } from '$lib/stores/imu'
import SceneBuilder from '$lib/sceneBuilder'; import SceneBuilder from '$lib/sceneBuilder'
let canvas: HTMLCanvasElement; let canvas: HTMLCanvasElement
let sceneBuilder: SceneBuilder; let sceneBuilder: SceneBuilder
let cube: THREE.Mesh; let cube: THREE.Mesh
let targetRotation = new THREE.Euler(); let targetRotation = new THREE.Euler()
let lastUpdateTime = 0; let lastUpdateTime = 0
const LERP_SPEED = 5; // rotations per second const LERP_SPEED = 5 // rotations per second
const initThreeJS = () => { const initThreeJS = () => {
sceneBuilder = new SceneBuilder() sceneBuilder = new SceneBuilder()
@@ -18,59 +18,59 @@
.addOrbitControls(1, 10, false) .addOrbitControls(1, 10, false)
.addAmbientLight({ color: 0x404040, intensity: 0.5 }) .addAmbientLight({ color: 0x404040, intensity: 0.5 })
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 }) .addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
.fillParent(); .fillParent()
const geometry = new THREE.BoxGeometry(1, 1, 1); const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshPhongMaterial({ const material = new THREE.MeshPhongMaterial({
color: 0x00ff00, color: 0x00ff00,
transparent: true, transparent: true,
opacity: 0.8, opacity: 0.8
}); })
cube = new THREE.Mesh(geometry, material); cube = new THREE.Mesh(geometry, material)
sceneBuilder.scene.add(cube); sceneBuilder.scene.add(cube)
sceneBuilder.addRenderCb(() => { sceneBuilder.addRenderCb(() => {
if (!cube) return; if (!cube) return
const currentTime = performance.now(); const currentTime = performance.now()
const deltaTime = (currentTime - lastUpdateTime) / 1000; // convert to seconds const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
lastUpdateTime = currentTime; lastUpdateTime = currentTime
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime); const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor); cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor); cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor); cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
}); })
sceneBuilder.startRenderLoop(); sceneBuilder.startRenderLoop()
}; }
const updateOrientation = () => { const updateOrientation = () => {
if (!cube) return; if (!cube) return
const y = -$imu.x[$imu.x.length - 1] || 0; const y = -$imu.x[$imu.x.length - 1] || 0
const x = $imu.y[$imu.y.length - 1] || 0; const x = $imu.y[$imu.y.length - 1] || 0
const z = -$imu.z[$imu.z.length - 1] || 0; const z = -$imu.z[$imu.z.length - 1] || 0
targetRotation.set( targetRotation.set(
THREE.MathUtils.degToRad(x), THREE.MathUtils.degToRad(x),
THREE.MathUtils.degToRad(y), THREE.MathUtils.degToRad(y),
THREE.MathUtils.degToRad(z) THREE.MathUtils.degToRad(z)
); )
}; }
onMount(() => { onMount(() => {
initThreeJS(); initThreeJS()
}); })
onDestroy(() => { onDestroy(() => {
sceneBuilder?.renderer?.dispose(); sceneBuilder?.renderer?.dispose()
}); })
$effect(() => { $effect(() => {
if ($imu) { if ($imu) {
updateOrientation(); updateOrientation()
} }
}); })
</script> </script>
<div class="h-60 w-60 border-2 border-base-300 rounded-md"> <div class="h-60 w-60 border-2 border-base-300 rounded-md">
+25 -9
View File
@@ -11,14 +11,23 @@
right?: import('svelte').Snippet right?: import('svelte').Snippet
} }
let { open = $bindable(true), collapsible = true, icon, title, children, right }: Props = $props() let {
open = $bindable(true),
collapsible = true,
icon,
title,
children,
right
}: Props = $props()
</script> </script>
{#if collapsible} {#if collapsible}
<div <div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"> class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div <div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
{@render icon?.()} {@render icon?.()}
{@render title?.()} {@render title?.()}
@@ -27,26 +36,33 @@
class="btn btn-circle btn-ghost btn-sm" class="btn btn-circle btn-ghost btn-sm"
onclick={() => { onclick={() => {
open = !open open = !open
}}> }}
>
<Down <Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open ? class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
open
) ?
'rotate-180' 'rotate-180'
: ''}" /> : ''}"
/>
</button> </button>
</div> </div>
{#if open} {#if open}
<div <div
class="flex flex-col gap-2 p-4 pt-0" class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()} {@render children?.()}
</div> </div>
{/if} {/if}
</div> </div>
{:else} {:else}
<div <div
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"> class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
>
<div <div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
>
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
{@render icon?.()} {@render icon?.()}
{@render title?.()} {@render title?.()}
+1 -2
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Loader } from "./icons"; import { Loader } from './icons'
</script> </script>
<div class="flex h-full w-full flex-col items-center justify-center p-6"> <div class="flex h-full w-full flex-col items-center justify-center p-6">
+4 -4
View File
@@ -1,10 +1,10 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte'
import { location } from '$lib/stores'; import { location } from '$lib/stores'
let source = $state(`${$location}/api/camera/stream`); let source = $state(`${$location}/api/camera/stream`)
onDestroy(() => (source = '#')); onDestroy(() => (source = '#'))
</script> </script>
<div class="w-full h-full"> <div class="w-full h-full">
+10 -8
View File
@@ -1,22 +1,24 @@
<script> <script>
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from './icons'; import { error, info, success, warning } from './icons'
/** @type {{theme?: any, icon?: any}} */ /** @type {{theme?: any, icon?: any}} */
let { theme = { let {
theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}, icon = { },
icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
} } = $props(); }
} = $props()
</script> </script>
<div class="toast toast-end mr-4"> <div class="toast toast-end mr-4">
+9 -2
View File
@@ -166,7 +166,10 @@
const updateAngles = (name: string, angle: number) => { const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI) modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100) Throttler.throttle(
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))),
100
)
} }
const createScene = async () => { const createScene = async () => {
@@ -242,7 +245,11 @@
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1) robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
robot.position.x = smooth(robot.position.x, -settings.zm, 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.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.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1) robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
} }
@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons"; import { MdiEyeOffOutline, MdiEyeOutline } from '../icons'
interface Props { interface Props {
show?: boolean; show?: boolean
value?: string; value?: string
id?: string; id?: string
} }
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props(); let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props()
let type = $derived(show ? 'text' : 'password'); let type = $derived(show ? 'text' : 'password')
const handleInput = (e: any) => value = e.target.value const handleInput = (e: any) => (value = e.target.value)
const togglePassword = () => show = !show const togglePassword = () => (show = !show)
</script> </script>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex items-center gap-2">
@@ -24,7 +24,8 @@
{max} {max}
{step} {step}
bind:value bind:value
{...rest} /> {...rest}
/>
<style> <style>
input[type='range']::-webkit-slider-runnable-track { input[type='range']::-webkit-slider-runnable-track {
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as PasswordInput } from './InputPassword.svelte'; export { default as PasswordInput } from './InputPassword.svelte'
export { default as VerticalSlider } from './VerticalSlider.svelte'; export { default as VerticalSlider } from './VerticalSlider.svelte'
+2 -2
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
children?: import('svelte').Snippet; children?: import('svelte').Snippet
} }
let { children }: Props = $props(); let { children }: Props = $props()
</script> </script>
<div class="box-border overflow-hidden flex-1"> <div class="box-border overflow-hidden flex-1">
@@ -1,17 +1,17 @@
<script lang="ts"> <script lang="ts">
import WidgetContainer from './WidgetContainer.svelte'; import WidgetContainer from './WidgetContainer.svelte'
import { import {
WidgetComponents, WidgetComponents,
type WidgetContainerConfig, type WidgetContainerConfig,
isWidgetConfig, isWidgetConfig
} from '$lib/stores/application'; } from '$lib/stores/application'
import Widget from './Widget.svelte'; import Widget from './Widget.svelte'
interface Props { interface Props {
container: WidgetContainerConfig; container: WidgetContainerConfig
} }
let { container }: Props = $props(); let { container }: Props = $props()
</script> </script>
<div class="w-full h-full flex flex-col overflow-hidden"> <div class="w-full h-full flex flex-col overflow-hidden">
@@ -19,7 +19,8 @@
class="flex w-full h-full" class="flex w-full h-full"
class:flex-row={container.layout === 'column'} class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'} class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}> class:flex-wrap={container.layout === 'wrap'}
>
{#each container.widgets as widget, index (widget.id + '-' + index)} {#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget> <Widget>
{#if isWidgetConfig(widget)} {#if isWidgetConfig(widget)}
@@ -32,8 +33,8 @@
{#if index !== container.widgets.length - 1} {#if index !== container.widgets.length - 1}
<div <div
class="divider bg-base-300 m-0" class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}> class:divider-horizontal={container.layout === 'column'}
</div> ></div>
{/if} {/if}
{/each} {/each}
</div> </div>
@@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Github } from "../icons"; import { Github } from '../icons'
interface Props { interface Props {
github: any; github: any
} }
let { github }: Props = $props(); let { github }: Props = $props()
</script> </script>
{#if github.active} {#if github.active}
@@ -1,14 +1,11 @@
<script> <script>
import logo from '$lib/assets/logo512.png'; import logo from '$lib/assets/logo512.png'
/** @type {{appName: any}} */ /** @type {{appName: any}} */
let { appName } = $props(); let { appName } = $props()
</script> </script>
<a <a href="/" class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]">
href="/"
class="rounded-box mb-4 flex items-center hover:scale-[1.02] active:scale-[0.98]"
>
<img src={logo} alt="Logo" class="h-12 w-12" /> <img src={logo} alt="Logo" class="h-12 w-12" />
<h1 class="px-4 text-2xl font-bold">{appName}</h1> <h1 class="px-4 text-2xl font-bold">{appName}</h1>
</a> </a>
+6 -2
View File
@@ -145,7 +145,10 @@
title: 'Firmware Update', title: 'Firmware Update',
icon: Update, icon: Update,
href: withBase('/system/update'), href: withBase('/system/update'),
feature: $features.ota || $features.upload_firmware || $features.download_firmware feature:
$features.ota ||
$features.upload_firmware ||
$features.download_firmware
} }
] ]
} }
@@ -181,7 +184,8 @@
{menuItems} {menuItems}
select={updateMenu} select={updateMenu}
class="grow flex-nowrap overflow-y-auto overflow-x-hidden" class="grow flex-nowrap overflow-y-auto overflow-x-hidden"
level="0" /> level="0"
/>
<div class="divider my-0"></div> <div class="divider my-0"></div>
+8 -2
View File
@@ -27,7 +27,12 @@
{menuItem.title} {menuItem.title}
</summary> </summary>
<div class="pl-4"> <div class="pl-4">
<MenuList menuItems={menuItem.submenu} level={level + 1} {select} class={klass} /> <MenuList
menuItems={menuItem.submenu}
level={level + 1}
{select}
class={klass}
/>
</div> </div>
</details> </details>
{:else} {:else}
@@ -37,7 +42,8 @@
class:bg-base-100={menuItem.active} class:bg-base-100={menuItem.active}
class:text-lg={level === 0} class:text-lg={level === 0}
class:text-md={level === 1} class:text-md={level === 1}
onclick={() => selectMenuItem(menuItem.title)}> onclick={() => selectMenuItem(menuItem.title)}
>
<menuItem.icon class="h-6 w-6" /> <menuItem.icon class="h-6 w-6" />
{menuItem.title} {menuItem.title}
</a> </a>
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { isFullscreen, toggleFullscreen } from '$lib/stores'; import { isFullscreen, toggleFullscreen } from '$lib/stores'
import { MdiFullscreenExit, MdiFullscreen } from '../icons'; import { MdiFullscreenExit, MdiFullscreen } from '../icons'
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen); const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen)
</script> </script>
<button onclick={toggleFullscreen}> <button onclick={toggleFullscreen}>
@@ -1,26 +1,26 @@
<script lang="ts"> <script lang="ts">
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons"; import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from '../icons'
interface Props { interface Props {
showDBm?: boolean; showDBm?: boolean
rssi?: number; rssi?: number
} }
let { showDBm = false, rssi = 0 }: Props = $props(); let { showDBm = false, rssi = 0 }: Props = $props()
const getWiFiIcon = () => { const getWiFiIcon = () => {
if (rssi === 0) return WifiOff; if (rssi === 0) return WifiOff
if (rssi >= -55) return WiFi; if (rssi >= -55) return WiFi
if (rssi >= -75) return WiFi2; if (rssi >= -75) return WiFi2
if (rssi >= -85) return WiFi1; if (rssi >= -85) return WiFi1
return WiFi0; return WiFi0
}; }
const SvelteComponent = $derived(getWiFiIcon()); const SvelteComponent = $derived(getWiFiIcon())
</script> </script>
<div class="indicator"> <div class="indicator">
<div class="tooltip tooltip-left" data-tip={rssi + " dBm"}> <div class="tooltip tooltip-left" data-tip={rssi + ' dBm'}>
{#if showDBm} {#if showDBm}
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs"> <span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
{rssi} dBm {rssi} dBm
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { useFeatureFlags } from '$lib/stores'; import { useFeatureFlags } from '$lib/stores'
import { modals } from 'svelte-modals'; import { modals } from 'svelte-modals'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import { api } from '$lib/api'; import { api } from '$lib/api'
import { Cancel, Power } from '../icons'; import { Cancel, Power } from '../icons'
const features = useFeatureFlags(); const features = useFeatureFlags()
const postSleep = async () => await api.post('/api/system/sleep'); const postSleep = async () => await api.post('/api/system/sleep')
const confirmSleep = () => { const confirmSleep = () => {
modals.open(ConfirmDialog, { modals.open(ConfirmDialog, {
@@ -18,11 +18,11 @@
confirm: { label: 'Switch Off', icon: Power } confirm: { label: 'Switch Off', icon: Power }
}, },
onConfirm: () => { onConfirm: () => {
modals.close(); modals.close()
postSleep(); postSleep()
}
})
} }
});
};
</script> </script>
{#if $features.sleep} {#if $features.sleep}
@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import { mode, modes } from "$lib/stores"; import { mode, modes } from '$lib/stores'
const deactivate = async () => { const deactivate = async () => {
mode.set(modes.indexOf('deactivated')); mode.set(modes.indexOf('deactivated'))
}; }
</script> </script>
<button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button> <button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { MdiWeatherSunny, MdiMoonAndStars } from "../icons"; import { MdiWeatherSunny, MdiMoonAndStars } from '../icons'
</script> </script>
<label class="swap swap-rotate"> <label class="swap swap-rotate">
@@ -98,9 +98,11 @@
<div class="indicator flex-none"> <div class="indicator flex-none">
<button <button
class="btn btn-square btn-ghost h-9 w-9" class="btn btn-square btn-ghost h-9 w-9"
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}> onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span <span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"> class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>
{firmwareVersion} {firmwareVersion}
</span> </span>
<Firmware class="h-7 w-7" /> <Firmware class="h-7 w-7" />
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { selectedView, views } from "$lib/stores/application"; import { selectedView, views } from '$lib/stores/application'
import Selector from "../widget/Selector.svelte"; import Selector from '../widget/Selector.svelte'
</script> </script>
<Selector bind:selectedOption={$selectedView} options={$views.map((v) => v.name)} /> <Selector bind:selectedOption={$selectedView} options={$views.map(v => v.name)} />
+10 -8
View File
@@ -1,22 +1,24 @@
<script> <script>
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate'
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import { error, info, success, warning } from '../icons'; import { error, info, success, warning } from '../icons'
/** @type {{theme?: any, icon?: any}} */ /** @type {{theme?: any, icon?: any}} */
let { theme = { let {
theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}, icon = { },
icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
} } = $props(); }
} = $props()
</script> </script>
<div class="toast toast-end mr-4 z-20"> <div class="toast toast-end mr-4 z-20">
@@ -1,22 +1,22 @@
<script lang="ts"> <script lang="ts">
import { daisyColor } from '$lib/utilities'; import { daisyColor } from '$lib/utilities'
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js'
import { onMount } from 'svelte'; import { onMount } from 'svelte'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
let chartElement: HTMLCanvasElement; let chartElement: HTMLCanvasElement
let chart: Chart; let chart: Chart
interface Props { interface Props {
label: any; label: any
data: number[]; data: number[]
title: any; title: any
} }
let { label, data, title }: Props = $props(); let { label, data, title }: Props = $props()
Chart.register(...registerables); Chart.register(...registerables)
onMount(() => { onMount(() => {
chart = new Chart(chartElement, { chart = new Chart(chartElement, {
@@ -30,36 +30,36 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data, data,
yAxisID: 'y', yAxisID: 'y'
}, }
], ]
}, },
options: { options: {
maintainAspectRatio: false, maintainAspectRatio: false,
responsive: true, responsive: true,
plugins: { plugins: {
legend: { legend: {
display: true, display: true
}, },
tooltip: { tooltip: {
mode: 'index', mode: 'index',
intersect: false, intersect: false
}, }
}, },
elements: { elements: {
point: { point: {
radius: 0, radius: 0
}, }
}, },
scales: { scales: {
x: { x: {
grid: { grid: {
color: daisyColor('--bc', 10), color: daisyColor('--bc', 10)
}, },
ticks: { ticks: {
color: daisyColor('--bc'), color: daisyColor('--bc')
}, },
display: false, display: false
}, },
y: { y: {
type: 'linear', type: 'linear',
@@ -69,33 +69,34 @@
color: daisyColor('--bc'), color: daisyColor('--bc'),
font: { font: {
size: 16, size: 16,
weight: 'bold', weight: 'bold'
}, }
}, },
position: 'left', position: 'left',
min: 0, min: 0,
max: 100, max: 100,
grid: { color: daisyColor('--bc', 10) }, grid: { color: daisyColor('--bc', 10) },
ticks: { ticks: {
color: daisyColor('--bc'), color: daisyColor('--bc')
}, },
border: { color: daisyColor('--bc', 10) }, border: { color: daisyColor('--bc', 10) }
}, }
}, }
}, }
}); })
setInterval(() => { setInterval(() => {
chart.data.labels = data; chart.data.labels = data
chart.data.datasets[0].data = data; chart.data.datasets[0].data = data
}, 500); }, 500)
}); })
</script> </script>
<div class="w-full h-full overflow-x-auto"> <div class="w-full h-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={chartElement}></canvas> <canvas bind:this={chartElement}></canvas>
</div> </div>
</div> </div>
@@ -1,18 +1,19 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
options?: string[]; options?: string[]
selectedOption?: string; selectedOption?: string
change?: () => void; change?: () => void
[key: string]: any; [key: string]: any
} }
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props(); let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props()
</script> </script>
<select <select
bind:value={selectedOption} bind:value={selectedOption}
{...rest} {...rest}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"> class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
>
{#each options as option} {#each options as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
{/each} {/each}
+11 -4
View File
@@ -101,11 +101,17 @@ export default class Kinematic {
pz = bz - mz pz = bz - mz
const lx = const lx =
this.invMountRot[0][0] * px + this.invMountRot[0][1] * py + this.invMountRot[0][2] * pz this.invMountRot[0][0] * px +
this.invMountRot[0][1] * py +
this.invMountRot[0][2] * pz
const ly = const ly =
this.invMountRot[1][0] * px + this.invMountRot[1][1] * py + this.invMountRot[1][2] * pz this.invMountRot[1][0] * px +
this.invMountRot[1][1] * py +
this.invMountRot[1][2] * pz
const lz = const lz =
this.invMountRot[2][0] * px + this.invMountRot[2][1] * py + this.invMountRot[2][2] * pz this.invMountRot[2][0] * px +
this.invMountRot[2][1] * py +
this.invMountRot[2][2] * pz
const xLocal = i % 2 === 1 ? -lx : lx const xLocal = i % 2 === 1 ? -lx : lx
return this.legIK(xLocal, ly, lz) return this.legIK(xLocal, ly, lz)
@@ -118,7 +124,8 @@ export default class Kinematic {
const H = sqrt(G * G + z * z) const H = sqrt(G * G + z * z)
const t1 = -atan2(y, x) - atan2(F, -this.coxa) const t1 = -atan2(y, x) - atan2(F, -this.coxa)
const D = const D =
(H * H - this.femur * this.femur - this.tibia * this.tibia) / (2 * this.femur * this.tibia) (H * H - this.femur * this.femur - this.tibia * this.tibia) /
(2 * this.femur * this.tibia)
const t3 = acos(max(-1, min(1, D))) const t3 = acos(max(-1, min(1, D)))
const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3)) const t2 = atan2(z, G) - atan2(this.tibia * sin(t3), this.femur + this.tibia * cos(t3))
return [t1, t2, t3] return [t1, t2, t3]
+31 -32
View File
@@ -1,54 +1,53 @@
import { Result } from '$lib/utilities/result'; import { Result } from '$lib/utilities/result'
import { browser } from '$app/environment'; import { browser } from '$app/environment'
class FileService { class FileService {
private dbPromise: Promise<Result<IDBDatabase, string>> | null = browser private dbPromise: Promise<Result<IDBDatabase, string>> | null =
? this.openDatabase() browser ? this.openDatabase() : null
: null;
private async openDatabase(): Promise<Result<IDBDatabase, string>> { private async openDatabase(): Promise<Result<IDBDatabase, string>> {
return new Promise((resolve) => { return new Promise(resolve => {
const request = indexedDB.open('fileStorageDB', 1); const request = indexedDB.open('fileStorageDB', 1)
request.onupgradeneeded = () => { request.onupgradeneeded = () => {
request.result.createObjectStore('files'); request.result.createObjectStore('files')
}; }
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Error opening database')); request.onerror = () => resolve(Result.err('Error opening database'))
}); })
} }
private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> { private async getStore(mode: IDBTransactionMode): Promise<Result<IDBObjectStore, string>> {
if (!browser || !this.dbPromise) if (!browser || !this.dbPromise)
return Result.err('Not running in browser or DB not initialized'); return Result.err('Not running in browser or DB not initialized')
const dbResult = await this.dbPromise; const dbResult = await this.dbPromise
if (dbResult.isErr()) return Result.err('Database not initialized'); if (dbResult.isErr()) return Result.err('Database not initialized')
const store = dbResult.inner.transaction('files', mode).objectStore('files'); const store = dbResult.inner.transaction('files', mode).objectStore('files')
return Result.ok(store); return Result.ok(store)
} }
public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> { public async saveFile(key: string, file: Uint8Array): Promise<Result<IDBValidKey, string>> {
const storeResult = await this.getStore('readwrite'); const storeResult = await this.getStore('readwrite')
if (storeResult.isErr()) return Result.err('Failed to access store'); if (storeResult.isErr()) return Result.err('Failed to access store')
return new Promise((resolve) => { return new Promise(resolve => {
const request = storeResult.inner.put(file, key); const request = storeResult.inner.put(file, key)
request.onsuccess = () => resolve(Result.ok(request.result)); request.onsuccess = () => resolve(Result.ok(request.result))
request.onerror = () => resolve(Result.err('Failed to save file')); request.onerror = () => resolve(Result.err('Failed to save file'))
}); })
} }
public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> { public async getFile(key: string): Promise<Result<Uint8Array | undefined, string>> {
const storeResult = await this.getStore('readonly'); const storeResult = await this.getStore('readonly')
if (storeResult.isErr()) return Result.err('Failed to access store'); if (storeResult.isErr()) return Result.err('Failed to access store')
return new Promise((resolve) => { return new Promise(resolve => {
const request = storeResult.inner.get(key); const request = storeResult.inner.get(key)
request.onsuccess = () => request.onsuccess = () =>
resolve(request.result ? Result.ok(request.result) : Result.err('File not found')); resolve(request.result ? Result.ok(request.result) : Result.err('File not found'))
request.onerror = () => resolve(Result.err('Failed to retrieve file')); request.onerror = () => resolve(Result.err('Failed to retrieve file'))
}); })
} }
} }
export default browser ? new FileService() : null; export default browser ? new FileService() : null
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as fileService } from './file-service'; export { default as fileService } from './file-service'
export { default as resultService } from './result-service'; export { default as resultService } from './result-service'
+10 -10
View File
@@ -1,19 +1,19 @@
import { errorLogs, latestErrorLog } from '$lib/stores'; import { errorLogs, latestErrorLog } from '$lib/stores'
import type { Result } from '$lib/utilities'; import type { Result } from '$lib/utilities'
class ResultService { class ResultService {
public handleResult(result: Result<unknown, string>, tag?: string) { public handleResult(result: Result<unknown, string>, tag?: string) {
if (result.isErr()) { if (result.isErr()) {
const errorLogEntry = { tag, message: result.inner, exception: result.exception }; const errorLogEntry = { tag, message: result.inner, exception: result.exception }
latestErrorLog.set(errorLogEntry); latestErrorLog.set(errorLogEntry)
errorLogs.update((entries) => { errorLogs.update(entries => {
entries.push(errorLogEntry); entries.push(errorLogEntry)
return entries; return entries
}); })
} }
return result; return result
} }
} }
export default new ResultService(); export default new ResultService()
+31 -17
View File
@@ -1,5 +1,5 @@
import { type Analytics } from '$lib/types/models'; import { type Analytics } from '$lib/types/models'
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
let analytics_data = { let analytics_data = {
uptime: <number[]>[], uptime: <number[]>[],
@@ -14,20 +14,22 @@ let analytics_data = {
cpu0_usage: <number[]>[], cpu0_usage: <number[]>[],
cpu1_usage: <number[]>[], cpu1_usage: <number[]>[],
cpu_usage: <number[]>[] cpu_usage: <number[]>[]
}; }
const maxAnalyticsData = 100; const maxAnalyticsData = 100
function createAnalytics() { function createAnalytics() {
const { subscribe, update } = writable(analytics_data); const { subscribe, update } = writable(analytics_data)
return { return {
subscribe, subscribe,
addData: (content: Analytics) => { addData: (content: Analytics) => {
update((analytics_data) => ({ update(analytics_data => ({
...analytics_data, ...analytics_data,
uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData), uptime: [...analytics_data.uptime, content.uptime].slice(-maxAnalyticsData),
free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(-maxAnalyticsData), free_heap: [...analytics_data.free_heap, content.free_heap / 1000].slice(
-maxAnalyticsData
),
total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice( total_heap: [...analytics_data.total_heap, content.total_heap / 1000].slice(
-maxAnalyticsData -maxAnalyticsData
), ),
@@ -35,21 +37,33 @@ function createAnalytics() {
...analytics_data.used_heap, ...analytics_data.used_heap,
(content.total_heap - content.free_heap) / 1000 (content.total_heap - content.free_heap) / 1000
].slice(-maxAnalyticsData), ].slice(-maxAnalyticsData),
min_free_heap: [...analytics_data.min_free_heap, content.min_free_heap / 1000].slice( min_free_heap: [
...analytics_data.min_free_heap,
content.min_free_heap / 1000
].slice(-maxAnalyticsData),
max_alloc_heap: [
...analytics_data.max_alloc_heap,
content.max_alloc_heap / 1000
].slice(-maxAnalyticsData),
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(
-maxAnalyticsData -maxAnalyticsData
), ),
max_alloc_heap: [...analytics_data.max_alloc_heap, content.max_alloc_heap / 1000].slice( fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(
-maxAnalyticsData
),
core_temp: [...analytics_data.core_temp, content.core_temp].slice(
-maxAnalyticsData
),
cpu0_usage: [...analytics_data.cpu0_usage, content.cpu0_usage].slice(
-maxAnalyticsData
),
cpu1_usage: [...analytics_data.cpu1_usage, content.cpu1_usage].slice(
-maxAnalyticsData -maxAnalyticsData
), ),
fs_used: [...analytics_data.fs_used, content.fs_used / 1000].slice(-maxAnalyticsData),
fs_total: [...analytics_data.fs_total, content.fs_total / 1000].slice(-maxAnalyticsData),
core_temp: [...analytics_data.core_temp, content.core_temp].slice(-maxAnalyticsData),
cpu0_usage: [...analytics_data.cpu0_usage, content.cpu0_usage].slice(-maxAnalyticsData),
cpu1_usage: [...analytics_data.cpu1_usage, content.cpu1_usage].slice(-maxAnalyticsData),
cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData) cpu_usage: [...analytics_data.cpu_usage, content.cpu_usage].slice(-maxAnalyticsData)
})); }))
}
} }
};
} }
export const analytics = createAnalytics(); export const analytics = createAnalytics()
+19 -19
View File
@@ -1,36 +1,36 @@
import { persistentStore } from '$lib/utilities'; import { persistentStore } from '$lib/utilities'
import { get, type Writable } from 'svelte/store'; import { get, type Writable } from 'svelte/store'
import Visualization from '$lib/components/Visualization.svelte'; import Visualization from '$lib/components/Visualization.svelte'
import Stream from '$lib/components/Stream.svelte'; import Stream from '$lib/components/Stream.svelte'
import ChartWidget from '$lib/components/widget/ChartWidget.svelte'; import ChartWidget from '$lib/components/widget/ChartWidget.svelte'
export interface WidgetConfig { export interface WidgetConfig {
id: string | number; id: string | number
component: keyof typeof WidgetComponents; component: keyof typeof WidgetComponents
props?: Record<string, any>; props?: Record<string, any>
} }
export interface WidgetContainerConfig { export interface WidgetContainerConfig {
id: string | number; id: string | number
layout?: 'row' | 'column' | 'wrap'; layout?: 'row' | 'column' | 'wrap'
header?: string; header?: string
widgets: Array<WidgetConfig | WidgetContainerConfig>; widgets: Array<WidgetConfig | WidgetContainerConfig>
} }
export const isWidgetConfig = ( export const isWidgetConfig = (
widget: WidgetConfig | WidgetContainerConfig widget: WidgetConfig | WidgetContainerConfig
): widget is WidgetConfig => 'component' in widget; ): widget is WidgetConfig => 'component' in widget
export const WidgetComponents = { export const WidgetComponents = {
Visualization, Visualization,
Stream, Stream,
ChartWidget ChartWidget
}; }
interface View { interface View {
name: string; name: string
content: WidgetContainerConfig; content: WidgetContainerConfig
} }
const defaultViews: View[] = [ const defaultViews: View[] = [
@@ -60,8 +60,8 @@ const defaultViews: View[] = [
] ]
} }
} }
]; ]
export const views: Writable<View[]> = persistentStore('views', defaultViews); export const views: Writable<View[]> = persistentStore('views', defaultViews)
export const selectedView = persistentStore('selected_view', get(views)[0].name); export const selectedView = persistentStore('selected_view', get(views)[0].name)
+10 -10
View File
@@ -1,24 +1,24 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
export const isFullscreen = writable(false); export const isFullscreen = writable(false)
export function toggleFullscreen() { export function toggleFullscreen() {
isFullscreen.update((state) => { isFullscreen.update(state => {
!state ? document.documentElement.requestFullscreen() : document.exitFullscreen(); !state ? document.documentElement.requestFullscreen() : document.exitFullscreen()
return !state; return !state
}); })
} }
export function enterFullscreen() { export function enterFullscreen() {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
document.documentElement.requestFullscreen(); document.documentElement.requestFullscreen()
isFullscreen.set(true); isFullscreen.set(true)
} }
} }
export function exitFullscreen() { export function exitFullscreen() {
if (document.fullscreenElement) { if (document.fullscreenElement) {
document.exitFullscreen(); document.exitFullscreen()
isFullscreen.set(false); isFullscreen.set(false)
} }
} }
+12 -12
View File
@@ -1,7 +1,7 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import type { IMU } from '$lib/types/models'; import type { IMU } from '$lib/types/models'
const maxIMUData = 100; const maxIMUData = 100
export const imu = (() => { export const imu = (() => {
const { subscribe, update } = writable({ const { subscribe, update } = writable({
@@ -12,16 +12,16 @@ export const imu = (() => {
altitude: [] as number[], altitude: [] as number[],
pressure: [] as number[], pressure: [] as number[],
bmp_temp: [] as number[] bmp_temp: [] as number[]
}); })
const addData = (content: IMU) => { const addData = (content: IMU) => {
update(data => { update(data => {
(Object.keys(content) as (keyof IMU)[]).forEach(key => { ;(Object.keys(content) as (keyof IMU)[]).forEach(key => {
data[key] = [...data[key], content[key]].slice(-maxIMUData); data[key] = [...data[key], content[key]].slice(-maxIMUData)
}); })
return data; return data
}); })
}; }
return { subscribe, addData }; return { subscribe, addData }
})(); })()
+9 -9
View File
@@ -1,9 +1,9 @@
export * from './socket-store'; export * from './socket-store'
export * from './logging-store'; export * from './logging-store'
export * from './model-store'; export * from './model-store'
export * from './socket'; export * from './socket'
export * from './fullscreen'; export * from './fullscreen'
export * from './telemetry'; export * from './telemetry'
export * from './analytics'; export * from './analytics'
export * from './featureFlags'; export * from './featureFlags'
export * from './location-store'; export * from './location-store'
+4 -4
View File
@@ -1,5 +1,5 @@
import { persistentStore } from '$lib/utilities'; import { persistentStore } from '$lib/utilities'
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'; import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public'
export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', ''); export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '')
+6 -6
View File
@@ -1,11 +1,11 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store'
export interface errorLog { export interface errorLog {
message: unknown; message: unknown
tag?: string; tag?: string
exception?: unknown; exception?: unknown
} }
export const latestErrorLog: Writable<errorLog> = writable(); export const latestErrorLog: Writable<errorLog> = writable()
export const errorLogs: Writable<errorLog[]> = writable([]); export const errorLogs: Writable<errorLog[]> = writable([])
+13 -13
View File
@@ -1,22 +1,22 @@
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store'
import { type angles } from '$lib/types/models'; import { type angles } from '$lib/types/models'
export const servoAnglesOut: Writable<number[]> = writable([ export const servoAnglesOut: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]); ])
export const servoAngles: Writable<number[]> = writable([ export const servoAngles: Writable<number[]> = writable([
0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90 0, 45, -90, 0, 45, -90, 0, 45, -90, 0, 45, -90
]); ])
export const logs = writable([] as string[]); export const logs = writable([] as string[])
export const mpu = writable({ heading: 0 }); export const mpu = writable({ heading: 0 })
export const sonar = writable([0, 0]); export const sonar = writable([0, 0])
export const distances = writable({}); export const distances = writable({})
export interface socketDataCollection { export interface socketDataCollection {
angles: Writable<angles>; angles: Writable<angles>
logs: Writable<string[]>; logs: Writable<string[]>
mpu: Writable<unknown>; mpu: Writable<unknown>
distances: Writable<unknown>; distances: Writable<unknown>
} }
export const socketData = { export const socketData = {
@@ -24,4 +24,4 @@ export const socketData = {
logs, logs,
mpu, mpu,
distances distances
}; }
+82 -82
View File
@@ -1,135 +1,135 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import { encode, decode } from '@msgpack/msgpack'; import { encode, decode } from '@msgpack/msgpack'
const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const; const socketEvents = ['open', 'close', 'error', 'message', 'unresponsive'] as const
type SocketEvent = (typeof socketEvents)[number]; type SocketEvent = (typeof socketEvents)[number]
type SocketMessage = [number, string?, unknown?]; type SocketMessage = [number, string?, unknown?]
let useBinary = false; let useBinary = false
const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => { const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
useBinary = data instanceof ArrayBuffer; useBinary = data instanceof ArrayBuffer
try { try {
if (useBinary) { if (useBinary) {
return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage; return decode(new Uint8Array(data as ArrayBuffer)) as SocketMessage
} }
return JSON.parse(data as string); return JSON.parse(data as string)
} catch (error) { } catch (error) {
console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`); console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`)
}
return null
} }
return null;
};
const encodeMessage = (data: unknown) => { const encodeMessage = (data: unknown) => {
try { try {
return useBinary ? encode(data) : JSON.stringify(data); return useBinary ? encode(data) : JSON.stringify(data)
} catch (error) { } catch (error) {
console.error(`Could not encode data: ${data} - ${error}`); console.error(`Could not encode data: ${data} - ${error}`)
}
} }
};
function createWebSocket() { function createWebSocket() {
const listeners = new Map<string, Set<(data?: unknown) => void>>(); const listeners = new Map<string, Set<(data?: unknown) => void>>()
const { subscribe, set } = writable(false); const { subscribe, set } = writable(false)
const reconnectTimeoutTime = 5000; const reconnectTimeoutTime = 5000
let unresponsiveTimeoutId: ReturnType<typeof setTimeout>; let unresponsiveTimeoutId: ReturnType<typeof setTimeout>
let reconnectTimeoutId: ReturnType<typeof setTimeout>; let reconnectTimeoutId: ReturnType<typeof setTimeout>
let ws: WebSocket; let ws: WebSocket
let socketUrl: string | URL; let socketUrl: string | URL
function init(url: string | URL) { function init(url: string | URL) {
socketUrl = url; socketUrl = url
connect(); connect()
} }
function disconnect(reason: SocketEvent, event?: Event) { function disconnect(reason: SocketEvent, event?: Event) {
ws.close(); ws.close()
set(false); set(false)
clearTimeout(unresponsiveTimeoutId); clearTimeout(unresponsiveTimeoutId)
clearTimeout(reconnectTimeoutId); clearTimeout(reconnectTimeoutId)
listeners.get(reason)?.forEach(listener => listener(event)); listeners.get(reason)?.forEach(listener => listener(event))
reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime); reconnectTimeoutId = setTimeout(connect, reconnectTimeoutTime)
} }
function connect() { function connect() {
ws = new WebSocket(socketUrl); ws = new WebSocket(socketUrl)
ws.binaryType = 'arraybuffer'; ws.binaryType = 'arraybuffer'
ws.onopen = ev => { ws.onopen = ev => {
ping(); ping()
useBinary = true; useBinary = true
ping(); ping()
set(true); set(true)
clearTimeout(reconnectTimeoutId); clearTimeout(reconnectTimeoutId)
listeners.get('open')?.forEach(listener => listener(ev)); listeners.get('open')?.forEach(listener => listener(ev))
for (const event of listeners.keys()) { for (const event of listeners.keys()) {
if (socketEvents.includes(event as SocketEvent)) continue; if (socketEvents.includes(event as SocketEvent)) continue
subscribeToEvent(event); subscribeToEvent(event)
}
} }
};
ws.onmessage = frame => { ws.onmessage = frame => {
resetUnresponsiveCheck(); resetUnresponsiveCheck()
const message = decodeMessage(frame.data); const message = decodeMessage(frame.data)
if (!message) return; if (!message) return
const [, event, payload = undefined] = message; const [, event, payload = undefined] = message
if (event) listeners.get(event)?.forEach(listener => listener(payload)); if (event) listeners.get(event)?.forEach(listener => listener(payload))
}; }
ws.onerror = ev => disconnect('error', ev); ws.onerror = ev => disconnect('error', ev)
ws.onclose = ev => disconnect('close', ev); ws.onclose = ev => disconnect('close', ev)
} }
function unsubscribe(event: string, listener?: (data: unknown) => void) { function unsubscribe(event: string, listener?: (data: unknown) => void) {
const eventListeners = listeners.get(event); const eventListeners = listeners.get(event)
if (!eventListeners) return; if (!eventListeners) return
if (!eventListeners.size) { if (!eventListeners.size) {
unsubscribeToEvent(event); unsubscribeToEvent(event)
} }
if (listener) { if (listener) {
eventListeners?.delete(listener); eventListeners?.delete(listener)
} else { } else {
listeners.delete(event); listeners.delete(event)
} }
} }
function resetUnresponsiveCheck() { function resetUnresponsiveCheck() {
clearTimeout(unresponsiveTimeoutId); clearTimeout(unresponsiveTimeoutId)
unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime); unresponsiveTimeoutId = setTimeout(() => disconnect('unresponsive'), reconnectTimeoutTime)
} }
function sendEvent(event: string, data: unknown) { function sendEvent(event: string, data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
send([2, event, data]); send([2, event, data])
} }
function unsubscribeToEvent(event: string) { function unsubscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
send([1, event]); send([1, event])
} }
function subscribeToEvent(event: string) { function subscribeToEvent(event: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
send([0, event]); send([0, event])
} }
function send(data: unknown) { function send(data: unknown) {
if (!ws || ws.readyState !== WebSocket.OPEN) return; if (!ws || ws.readyState !== WebSocket.OPEN) return
const serialized = encodeMessage(data); const serialized = encodeMessage(data)
if (!serialized) { if (!serialized) {
console.error('Could not serialize data:', data); console.error('Could not serialize data:', data)
return; return
} }
ws.send(serialized); ws.send(serialized)
} }
function ping() { function ping() {
const serialized = encodeMessage([4]); const serialized = encodeMessage([4])
if (!serialized) { if (!serialized) {
console.error('Could not serialize message'); console.error('Could not serialize message')
return; return
} }
ws.send(serialized); ws.send(serialized)
} }
return { return {
@@ -137,24 +137,24 @@ function createWebSocket() {
sendEvent, sendEvent,
init, init,
on: <T>(event: string, listener: (data: T) => void): (() => void) => { on: <T>(event: string, listener: (data: T) => void): (() => void) => {
let eventListeners = listeners.get(event); let eventListeners = listeners.get(event)
if (!eventListeners) { if (!eventListeners) {
if (!socketEvents.includes(event as SocketEvent)) { if (!socketEvents.includes(event as SocketEvent)) {
subscribeToEvent(event); subscribeToEvent(event)
} }
eventListeners = new Set(); eventListeners = new Set()
listeners.set(event, eventListeners); listeners.set(event, eventListeners)
} }
eventListeners.add(listener as (data: unknown) => void); eventListeners.add(listener as (data: unknown) => void)
return () => { return () => {
unsubscribe(event, listener as (data: unknown) => void); unsubscribe(event, listener as (data: unknown) => void)
}; }
}, },
off: <T>(event: string, listener?: (data: T) => void) => { off: <T>(event: string, listener?: (data: T) => void) => {
unsubscribe(event, listener as (data: unknown) => void); unsubscribe(event, listener as (data: unknown) => void)
}, }
}; }
} }
export const socket = createWebSocket(); export const socket = createWebSocket()
+8 -8
View File
@@ -1,5 +1,5 @@
import type { DownloadOTA } from '$lib/types/models'; import type { DownloadOTA } from '$lib/types/models'
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
let telemetry_data = { let telemetry_data = {
rssi: { rssi: {
@@ -10,10 +10,10 @@ let telemetry_data = {
progress: 0, progress: 0,
error: '' error: ''
} }
}; }
function createTelemetry() { function createTelemetry() {
const { subscribe, set, update } = writable(telemetry_data); const { subscribe, set, update } = writable(telemetry_data)
return { return {
subscribe, subscribe,
@@ -21,15 +21,15 @@ function createTelemetry() {
update(telemetry_data => ({ update(telemetry_data => ({
...telemetry_data, ...telemetry_data,
rssi: { rssi: data } rssi: { rssi: data }
})); }))
}, },
setDownloadOTA: (data: DownloadOTA) => { setDownloadOTA: (data: DownloadOTA) => {
update(telemetry_data => ({ update(telemetry_data => ({
...telemetry_data, ...telemetry_data,
download_ota: { status: data.status, progress: data.progress, error: data.error } download_ota: { status: data.status, progress: data.progress, error: data.error }
})); }))
}
} }
};
} }
export const telemetry = createTelemetry(); export const telemetry = createTelemetry()
+15 -15
View File
@@ -1,17 +1,17 @@
declare module 'three/src/math/MathUtils' { declare module 'three/src/math/MathUtils' {
export function generateUUID(): string; export function generateUUID(): string
export function clamp(value: number, min: number, max: number): number; export function clamp(value: number, min: number, max: number): number
export function euclideanModulo(n: number, m: number): number; export function euclideanModulo(n: number, m: number): number
export function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number; export function mapLinear(x: number, a1: number, a2: number, b1: number, b2: number): number
export function lerp(x: number, y: number, t: number): number; export function lerp(x: number, y: number, t: number): number
export function smoothstep(x: number, min: number, max: number): number; export function smoothstep(x: number, min: number, max: number): number
export function smootherstep(x: number, min: number, max: number): number; export function smootherstep(x: number, min: number, max: number): number
export function randInt(low: number, high: number): number; export function randInt(low: number, high: number): number
export function randFloat(low: number, high: number): number; export function randFloat(low: number, high: number): number
export function randFloatSpread(range: number): number; export function randFloatSpread(range: number): number
export function degToRad(degrees: number): number; export function degToRad(degrees: number): number
export function radToDeg(radians: number): number; export function radToDeg(radians: number): number
export function isPowerOfTwo(value: number): boolean; export function isPowerOfTwo(value: number): boolean
export function ceilPowerOfTwo(value: number): number; export function ceilPowerOfTwo(value: number): number
export function floorPowerOfTwo(value: number): number; export function floorPowerOfTwo(value: number): number
} }
+9 -9
View File
@@ -1,14 +1,14 @@
declare module 'uzip' { declare module 'uzip' {
interface UZIP { interface UZIP {
parse(data: Uint8Array | ArrayBuffer): any; parse(data: Uint8Array | ArrayBuffer): any
compress(data: any): Uint8Array | ArrayBuffer; compress(data: any): Uint8Array | ArrayBuffer
compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer; compressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
decompress(data: Uint8Array | ArrayBuffer): any; decompress(data: Uint8Array | ArrayBuffer): any
decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer; decompressRaw(data: Uint8Array | ArrayBuffer): Uint8Array | ArrayBuffer
encode(data: any): Uint8Array | ArrayBuffer; encode(data: any): Uint8Array | ArrayBuffer
decode(data: Uint8Array | ArrayBuffer): any; decode(data: Uint8Array | ArrayBuffer): any
} }
const uzip: UZIP; const uzip: UZIP
export default uzip; export default uzip
} }
+8 -8
View File
@@ -1,15 +1,15 @@
export class throttler { export class throttler {
private _throttlePause: boolean; private _throttlePause: boolean
constructor() { constructor() {
this._throttlePause = false; this._throttlePause = false
} }
throttle = (callback: Function, time: number) => { throttle = (callback: Function, time: number) => {
if (this._throttlePause) return; if (this._throttlePause) return
this._throttlePause = true; this._throttlePause = true
setTimeout(() => { setTimeout(() => {
callback(); callback()
this._throttlePause = false; this._throttlePause = false
}, time); }, time)
}; }
} }
+5 -5
View File
@@ -1,6 +1,6 @@
export const daisyColor = (name: string, opacity: number = 100) => { export const daisyColor = (name: string, opacity: number = 100) => {
const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); const color = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
if (opacity >= 100) return color; if (opacity >= 100) return color
const alpha = Math.min(Math.max(opacity, 0), 100) / 100; const alpha = Math.min(Math.max(opacity, 0), 100) / 100
return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`; return `${color.replace(/(\/\s*\d+(\.\d+)?\))|\)$/, '')} / ${alpha})`
}; }
+9 -9
View File
@@ -1,9 +1,9 @@
export * from './result'; export * from './result'
export * from './string-utilities'; export * from './string-utilities'
export * from './svelte-utilities'; export * from './svelte-utilities'
export * from './math-utilities'; export * from './math-utilities'
export * from './buffer-utilities'; export * from './buffer-utilities'
export * from './model-utilities'; export * from './model-utilities'
export * from './position-utilities'; export * from './position-utilities'
export * from './string-utilities'; export * from './string-utilities'
export * from './color-utilities'; export * from './color-utilities'
+13 -13
View File
@@ -1,18 +1,18 @@
export const toUint8 = (number: number, min: number, max: number) => { export const toUint8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number)); number = Math.max(min, Math.min(max, number))
let scaled = ((number - min) / (max - min)) * 255; let scaled = ((number - min) / (max - min)) * 255
return Math.round(scaled) & 0xff; return Math.round(scaled) & 0xff
}; }
export const toInt8 = (number: number, min: number, max: number) => { export const toInt8 = (number: number, min: number, max: number) => {
number = Math.max(min, Math.min(max, number)); number = Math.max(min, Math.min(max, number))
let scaled = ((number - min) / (max - min)) * 255 - 128; let scaled = ((number - min) / (max - min)) * 255 - 128
return Math.max(-128, Math.min(127, Math.round(scaled))) | 0; return Math.max(-128, Math.min(127, Math.round(scaled))) | 0
}; }
export const fromInt8 = (int8: number, min: number, max: number) => { export const fromInt8 = (int8: number, min: number, max: number) => {
int8 = Math.max(-128, Math.min(127, int8)); int8 = Math.max(-128, Math.min(127, int8))
const scaled = (int8 + 128) / 255; const scaled = (int8 + 128) / 255
const number = scaled * (max - min) + min; const number = scaled * (max - min) + min
return number; return number
}; }
+2 -1
View File
@@ -36,7 +36,8 @@ export const loadModel = async (url: string): Promise<Result<[URDFRobot, string[
const urdfLoader = new URDFLoader() const urdfLoader = new URDFLoader()
urdfLoader.workingPath = LoaderUtils.extractUrlBase(url) urdfLoader.workingPath = LoaderUtils.extractUrlBase(url)
let xml = url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text()) let xml =
url.endsWith('.xacro') ? await loadXacro(url) : await fetch(url).then(res => res.text())
if (typeof xml === 'string') { if (typeof xml === 'string') {
xml = new window.DOMParser().parseFromString(xml, 'text/xml') xml = new window.DOMParser().parseFromString(xml, 'text/xml')
+35 -33
View File
@@ -1,30 +1,32 @@
class SunCalculator { class SunCalculator {
calculateSunElevation(lat: number = 55, lon: number = 12) { calculateSunElevation(lat: number = 55, lon: number = 12) {
const now = new Date(); const now = new Date()
const JD = this.getJulianDate(now); const JD = this.getJulianDate(now)
const solarDec = this.getSolarDeclination(JD); const solarDec = this.getSolarDeclination(JD)
const solarTime = this.getSolarTime(now, lon); const solarTime = this.getSolarTime(now, lon)
const hourAngle = (solarTime - 12) * 15; const hourAngle = (solarTime - 12) * 15
const elevation = Math.asin( const elevation = Math.asin(
Math.sin(this.degToRad(lat)) * Math.sin(solarDec) + Math.sin(this.degToRad(lat)) * Math.sin(solarDec) +
Math.cos(this.degToRad(lat)) * Math.cos(solarDec) * Math.cos(this.degToRad(hourAngle)) Math.cos(this.degToRad(lat)) *
); Math.cos(solarDec) *
Math.cos(this.degToRad(hourAngle))
)
return this.radToDeg(elevation); return this.radToDeg(elevation)
} }
getJulianDate(date: Date) { getJulianDate(date: Date) {
const Y = date.getUTCFullYear(); const Y = date.getUTCFullYear()
const M = date.getUTCMonth() + 1; const M = date.getUTCMonth() + 1
const D = const D =
date.getUTCDate() + date.getUTCDate() +
date.getUTCHours() / 24 + date.getUTCHours() / 24 +
date.getUTCMinutes() / 1440 + date.getUTCMinutes() / 1440 +
date.getUTCSeconds() / 86400; date.getUTCSeconds() / 86400
const A = Math.floor((14 - M) / 12); const A = Math.floor((14 - M) / 12)
const Y1 = Y + 4800 - A; const Y1 = Y + 4800 - A
const M1 = M + 12 * A - 3; const M1 = M + 12 * A - 3
return ( return (
D + D +
Math.floor((153 * M1 + 2) / 5) + Math.floor((153 * M1 + 2) / 5) +
@@ -33,33 +35,33 @@ class SunCalculator {
Math.floor(Y1 / 100) + Math.floor(Y1 / 100) +
Math.floor(Y1 / 400) - Math.floor(Y1 / 400) -
32045 32045
); )
} }
getSolarDeclination(JulianDate: number) { getSolarDeclination(JulianDate: number) {
const n = JulianDate - 2451545; const n = JulianDate - 2451545
const L = (280.46 + 0.9856474 * n) % 360; const L = (280.46 + 0.9856474 * n) % 360
const g = this.degToRad((357.528 + 0.9856003 * n) % 360); const g = this.degToRad((357.528 + 0.9856003 * n) % 360)
const lambda = this.degToRad(L + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g)); const lambda = this.degToRad(L + 1.915 * Math.sin(g) + 0.02 * Math.sin(2 * g))
return Math.asin(Math.sin(lambda) * Math.sin(this.degToRad(23.44))); return Math.asin(Math.sin(lambda) * Math.sin(this.degToRad(23.44)))
} }
getSolarTime(date: Date, lon: number) { getSolarTime(date: Date, lon: number) {
const EoT = this.getEquationOfTime(date); const EoT = this.getEquationOfTime(date)
const offset = date.getTimezoneOffset() / 60; const offset = date.getTimezoneOffset() / 60
const standardMeridian = Math.round(lon / 15) * 15; const standardMeridian = Math.round(lon / 15) * 15
const solarTime = const solarTime =
date.getUTCHours() + date.getUTCHours() +
(date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 - (date.getUTCMinutes() + (4 * (standardMeridian - lon) + EoT)) / 60 -
offset; offset
return (solarTime + 24) % 24; return (solarTime + 24) % 24
} }
getEquationOfTime(date: Date) { getEquationOfTime(date: Date) {
const JD = this.getJulianDate(date); const JD = this.getJulianDate(date)
const n = JD - 2451545; const n = JD - 2451545
const g = this.degToRad((357.528 + 0.9856003 * n) % 360); const g = this.degToRad((357.528 + 0.9856003 * n) % 360)
const q = this.degToRad((280.46 + 0.9856474 * n) % 360); const q = this.degToRad((280.46 + 0.9856474 * n) % 360)
return ( return (
4 * 4 *
this.radToDeg( this.radToDeg(
@@ -69,16 +71,16 @@ class SunCalculator {
0.014615 * Math.cos(2 * q) - 0.014615 * Math.cos(2 * q) -
0.040849 * Math.sin(2 * g) 0.040849 * Math.sin(2 * g)
) )
); )
} }
degToRad(deg: number) { degToRad(deg: number) {
return deg * (Math.PI / 180); return deg * (Math.PI / 180)
} }
radToDeg(rad: number) { radToDeg(rad: number) {
return rad * (180 / Math.PI); return rad * (180 / Math.PI)
} }
} }
export const sunCalculator = new SunCalculator(); export const sunCalculator = new SunCalculator()
+9 -9
View File
@@ -1,18 +1,18 @@
export class Err<T, U> { export class Err<T, U> {
#inner: T; #inner: T
#exception?: U; #exception?: U
constructor(inner: T, exception?: U) { constructor(inner: T, exception?: U) {
this.#inner = inner; this.#inner = inner
this.#exception = exception; this.#exception = exception
} }
get inner(): T { get inner(): T {
return this.#inner; return this.#inner
} }
get exception(): U | undefined { get exception(): U | undefined {
return this.#exception; return this.#exception
} }
/** /**
@@ -20,7 +20,7 @@ export class Err<T, U> {
* @returns `true` if `Ok`; `false` if `Err` * @returns `true` if `Ok`; `false` if `Err`
*/ */
isOk(): false { isOk(): false {
return false; return false
} }
/** /**
@@ -28,7 +28,7 @@ export class Err<T, U> {
* @returns `true` if `Err`; `false` if `Ok` * @returns `true` if `Err`; `false` if `Ok`
*/ */
isErr(): this is Err<T, U> { isErr(): this is Err<T, U> {
return true; return true
} }
/** /**
@@ -37,6 +37,6 @@ export class Err<T, U> {
* @returns `Err(inner)` * @returns `Err(inner)`
*/ */
static new<E, F>(inner: E, exception: F): Err<E, F> { static new<E, F>(inner: E, exception: F): Err<E, F> {
return new Err<E, F>(inner, exception); return new Err<E, F>(inner, exception)
} }
} }
+3 -3
View File
@@ -1,3 +1,3 @@
export * from './err'; export * from './err'
export * from './ok'; export * from './ok'
export * from './result'; export * from './result'
+7 -7
View File
@@ -1,12 +1,12 @@
export class Ok<T> { export class Ok<T> {
#inner: T; #inner: T
constructor(inner: T) { constructor(inner: T) {
this.#inner = inner; this.#inner = inner
} }
get inner(): T { get inner(): T {
return this.#inner; return this.#inner
} }
/** /**
@@ -14,7 +14,7 @@ export class Ok<T> {
* @returns `true` if `Ok`; `false` if `Err` * @returns `true` if `Ok`; `false` if `Err`
*/ */
isOk(): this is Ok<T> { isOk(): this is Ok<T> {
return true; return true
} }
/** /**
@@ -22,7 +22,7 @@ export class Ok<T> {
* @returns `true` if `Err`; `false` if `Ok` * @returns `true` if `Err`; `false` if `Ok`
*/ */
isErr(): false { isErr(): false {
return false; return false
} }
/** /**
@@ -31,7 +31,7 @@ export class Ok<T> {
* @returns `Ok(inner)` * @returns `Ok(inner)`
*/ */
static new<T>(inner: T): Ok<T> { static new<T>(inner: T): Ok<T> {
return new Ok<T>(inner); return new Ok<T>(inner)
} }
/** /**
@@ -39,6 +39,6 @@ export class Ok<T> {
* @returns `Ok(void)` * @returns `Ok(void)`
*/ */
static void(): Ok<void> { static void(): Ok<void> {
return new Ok(undefined); return new Ok(undefined)
} }
} }
+5 -5
View File
@@ -1,20 +1,20 @@
import { Err } from './err'; import { Err } from './err'
import { Ok } from './ok'; import { Ok } from './ok'
export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>; export type Result<T = unknown, E = unknown, F = unknown> = Ok<T> | Err<E, F>
export namespace Result { export namespace Result {
/** /**
* @returns `Ok<T>` * @returns `Ok<T>`
*/ */
export function ok<T = unknown>(value: T) { export function ok<T = unknown>(value: T) {
return Ok.new(value); return Ok.new(value)
} }
/** /**
* @returns `Err<E, F>` * @returns `Err<E, F>`
*/ */
export function err<E = unknown, F = unknown>(error: E, exception?: F) { export function err<E = unknown, F = unknown>(error: E, exception?: F) {
return Err.new(error, exception); return Err.new(error, exception)
} }
} }
+10 -10
View File
@@ -1,16 +1,16 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store'
import { browser } from '$app/environment'; import { browser } from '$app/environment'
export const persistentStore = <T>(key: string, initialValue: T) => { export const persistentStore = <T>(key: string, initialValue: T) => {
const savedValue = browser ? localStorage.getItem(key) : null; const savedValue = browser ? localStorage.getItem(key) : null
const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue; const data: T = savedValue !== null ? JSON.parse(savedValue) : initialValue
const store = writable<T>(); const store = writable<T>()
store.subscribe(value => { store.subscribe(value => {
if (browser) localStorage.setItem(key, JSON.stringify(value)); if (browser) localStorage.setItem(key, JSON.stringify(value))
}); })
store.set(data); store.set(data)
return store; return store
}; }
+2 -2
View File
@@ -121,8 +121,8 @@
<div <div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm" class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur-sm"
transition:fade transition:fade
onclick={modals.closeAll}> onclick={modals.closeAll}
</div> ></div>
{/snippet} {/snippet}
</Modals> </Modals>
+3 -1
View File
@@ -7,7 +7,9 @@ const registerFetchIntercept = async () => {
window.fetch = async (resource, config) => { window.fetch = async (resource, config) => {
const url = resource instanceof Request ? resource.url : resource.toString() const url = resource instanceof Request ? resource.url : resource.toString()
const file = await fileService?.getFile(url) const file = await fileService?.getFile(url)
return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config) return file?.isOk() && file.inner ?
new Response(new Uint8Array(file.inner))
: originalFetch(resource, config)
} }
} }
+3 -1
View File
@@ -21,7 +21,9 @@
<div class="card-body w-80"> <div class="card-body w-80">
<h2 class="card-title text-center text-2xl">Begin you journey</h2> <h2 class="card-title text-center text-2xl">Begin you journey</h2>
<p class="py-6 text-center"></p> <p class="py-6 text-center"></p>
<a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}> Add Robot Dog </a> <a class="btn btn-primary" href={$socket ? '/controller' : '/connection'}>
Add Robot Dog
</a>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Connection from './Connection.svelte'; import Connection from './Connection.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'Connection' title: 'Connection'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
+6 -6
View File
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { WiFi } from '$lib/components/icons'; import { WiFi } from '$lib/components/icons'
import { location, socket } from '$lib/stores'; import { location, socket } from '$lib/stores'
const update = () => { const update = () => {
const ws = $location ? $location : window.location.host; const ws = $location ? $location : window.location.host
socket.init(`ws://${ws}/api/ws/events`); socket.init(`ws://${ws}/api/ws/events`)
}; }
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
+2 -2
View File
@@ -1,3 +1,3 @@
export const load = async () => { export const load = async () => {
return { title: 'Controller' }; return { title: 'Controller' }
}; }
+15 -7
View File
@@ -135,22 +135,27 @@
<div class="flex justify-center w-full"></div> <div class="flex justify-center w-full"></div>
</div> </div>
<div class="absolute bottom-0 z-10 flex items-end"> <div class="absolute bottom-0 z-10 flex items-end">
<div class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl"> <div
class="flex items-center flex-col bg-base-300 bg-opacity-50 p-3 pb-2 gap-2 rounded-tr-xl"
>
<VerticalSlider <VerticalSlider
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
oninput={(e: Event) => handleRange(e, 'height')} /> oninput={(e: Event) => handleRange(e, 'height')}
/>
<label for="height">Ht</label> <label for="height">Ht</label>
</div> </div>
<div <div
class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden"> class="flex items-end gap-4 bg-base-300 bg-opacity-50 h-min rounded-tr-xl pl-0 p-3 portrait:hidden"
>
<div class="join"> <div class="join">
{#each modes as modeValue} {#each modes as modeValue}
<button <button
class="btn join-item" class="btn join-item"
class:btn-primary={$mode === modes.indexOf(modeValue)} class:btn-primary={$mode === modes.indexOf(modeValue)}
onclick={() => changeMode(modeValue)}> onclick={() => changeMode(modeValue)}
>
{capitalize(modeValue)} {capitalize(modeValue)}
</button> </button>
{/each} {/each}
@@ -163,7 +168,8 @@
<button <button
class="btn join-item btn-sm" class="btn join-item btn-sm"
class:btn-secondary={$walkGait === gaitValue} class:btn-secondary={$walkGait === gaitValue}
onclick={() => changeWalkGait(gaitValue)}> onclick={() => changeWalkGait(gaitValue)}
>
{walkGaitLabels[gaitValue]} {walkGaitLabels[gaitValue]}
</button> </button>
{/if} {/if}
@@ -180,7 +186,8 @@
step="0.01" step="0.01"
max="1" max="1"
oninput={e => handleRange(e, 's1')} oninput={e => handleRange(e, 's1')}
class="range range-sm range-primary" /> class="range range-sm range-primary"
/>
</div> </div>
<div> <div>
<label for="speed">Speed</label> <label for="speed">Speed</label>
@@ -191,7 +198,8 @@
step="0.01" step="0.01"
max="1" max="1"
oninput={e => handleRange(e, 'speed')} oninput={e => handleRange(e, 'speed')}
class="range range-sm range-primary" /> class="range range-sm range-primary"
/>
</div> </div>
</div> </div>
{/if} {/if}
+5 -5
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
import { goto } from '$app/navigation'; import { goto } from '$app/navigation'
export const load = (async () => { export const load = (async () => {
goto('/'); goto('/')
return; return
}) satisfies PageLoad; }) satisfies PageLoad
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import Camera from './Camera.svelte'; import Camera from './Camera.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'Camera' title: 'Camera'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
@@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from "$lib/components/SettingsCard.svelte"; import SettingsCard from '$lib/components/SettingsCard.svelte'
import CameraSetting from './CameraSetting.svelte'; import CameraSetting from './CameraSetting.svelte'
import Stream from '$lib/components/Stream.svelte'; import Stream from '$lib/components/Stream.svelte'
import { Camera } from "$lib/components/icons"; import { Camera } from '$lib/components/icons'
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api'; import { api } from '$lib/api'
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte'
import type { CameraSettings } from '$lib/types/models'; import type { CameraSettings } from '$lib/types/models'
let settings: CameraSettings = $state() let settings: CameraSettings = $state()
const getCameraSettings = async () => { const getCameraSettings = async () => {
const result = await api.get<CameraSettings>('/api/camera/settings') const result = await api.get<CameraSettings>('/api/camera/settings')
if (result.isErr()) { if (result.isErr()) {
console.error("An error occurred", result.inner); console.error('An error occurred', result.inner)
return return
} }
settings = result.inner settings = result.inner
@@ -16,7 +16,7 @@
const updateCameraSettings = async () => { const updateCameraSettings = async () => {
const result = await api.post<CameraSettings>('/api/camera/settings', settings) const result = await api.post<CameraSettings>('/api/camera/settings', settings)
if (result.isErr()) { if (result.isErr()) {
console.error("An error occurred", result.inner); console.error('An error occurred', result.inner)
return return
} }
settings = result.inner settings = result.inner
@@ -27,21 +27,41 @@
<Spinner /> <Spinner />
{:then _} {:then _}
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<button class="btn btn-primary" type="button" onclick={updateCameraSettings}>Update camera settings</button> <button class="btn btn-primary" type="button" onclick={updateCameraSettings}
>Update camera settings</button
>
<label for="brightness"> <label for="brightness">
Brightness {settings.brightness} Brightness {settings.brightness}
<input type="range" min="-2" max="2" class="range range-xs" bind:value={settings.brightness}/> <input
type="range"
min="-2"
max="2"
class="range range-xs"
bind:value={settings.brightness}
/>
</label> </label>
<label for="contrast"> <label for="contrast">
Contrast {settings.contrast} Contrast {settings.contrast}
<input type="range" min="-2" max="2" class="range range-xs" bind:value={settings.contrast}/> <input
type="range"
min="-2"
max="2"
class="range range-xs"
bind:value={settings.contrast}
/>
</label> </label>
<label for="framesize"> <label for="framesize">
FrameSize {settings.framesize} FrameSize {settings.framesize}
<input type="range" min="0" max="10" class="range range-xs" bind:value={settings.framesize}/> <input
type="range"
min="0"
max="10"
class="range range-xs"
bind:value={settings.framesize}
/>
</label> </label>
<label class="cursor-pointer flex items-center justify-between"> <label class="cursor-pointer flex items-center justify-between">
@@ -56,7 +76,10 @@
<label for="special_effect" class="flex items-center"> <label for="special_effect" class="flex items-center">
<span class="basis-1/2">Special Effect</span> <span class="basis-1/2">Special Effect</span>
<select class="select select-bordered select-sm w-full max-w-xs" bind:value={settings.special_effect}> <select
class="select select-bordered select-sm w-full max-w-xs"
bind:value={settings.special_effect}
>
<option value={0}>No effect</option> <option value={0}>No effect</option>
<option value={1}>Negative</option> <option value={1}>Negative</option>
<option value={2}>Grayscale</option> <option value={2}>Grayscale</option>
@@ -56,7 +56,8 @@
max="48" max="48"
title="SDA pin number (0-48)" title="SDA pin number (0-48)"
disabled={!isEditing} disabled={!isEditing}
bind:value={settings.sda} /> bind:value={settings.sda}
/>
</label> </label>
<label for="scl" class="input validator"> <label for="scl" class="input validator">
SCL SCL
@@ -70,7 +71,8 @@
max="48" max="48"
title="SCL pin number (0-48)" title="SCL pin number (0-48)"
disabled={!isEditing} disabled={!isEditing}
bind:value={settings.scl} /> bind:value={settings.scl}
/>
</label> </label>
<label class="input validator" for="frequency"> <label class="input validator" for="frequency">
Frequency Frequency
@@ -83,14 +85,20 @@
max="430000" max="430000"
title="I2C frequency in Hz" title="I2C frequency in Hz"
disabled={!isEditing} disabled={!isEditing}
bind:value={settings.frequency} /> bind:value={settings.frequency}
/>
</label> </label>
<div> <div>
<button class="btn btn-outline btn-primary" onclick={() => (isEditing = !isEditing)}> <button
class="btn btn-outline btn-primary"
onclick={() => (isEditing = !isEditing)}
>
<Icon class="h-6 w-6" /> <Icon class="h-6 w-6" />
</button> </button>
{#if isEditing} {#if isEditing}
<button class="btn btn-outline btn-primary" onclick={handleSave}>Save</button> <button class="btn btn-outline btn-primary" onclick={handleSave}
>Save</button
>
{/if} {/if}
</div> </div>
</div> </div>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import IMU from './imu.svelte'; import IMU from './imu.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'IMU' title: 'IMU'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
+6 -3
View File
@@ -228,7 +228,8 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={angleChartElement}></canvas> <canvas bind:this={angleChartElement}></canvas>
</div> </div>
</div> </div>
@@ -238,14 +239,16 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={tempChartElement}></canvas> <canvas bind:this={tempChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={altitudeChartElement}></canvas> <canvas bind:this={altitudeChartElement}></canvas>
</div> </div>
</div> </div>
+3 -3
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { return {
title: 'Servo' title: 'Servo'
}; }
}) satisfies PageLoad; }) satisfies PageLoad
@@ -70,7 +70,8 @@
onblur={syncConfig} onblur={syncConfig}
oninput={event => updateValue(event, index, 'center_pwm')} oninput={event => updateValue(event, index, 'center_pwm')}
min="80" min="80"
max="600" /> max="600"
/>
</td> </td>
<td> <td>
<input <input
@@ -81,13 +82,15 @@
onblur={syncConfig} onblur={syncConfig}
oninput={event => updateValue(event, index, 'center_angle')} oninput={event => updateValue(event, index, 'center_angle')}
min="-90" min="-90"
max="90" /> max="90"
/>
</td> </td>
<td> <td>
<button <button
class="btn btn-sm btn-ghost" class="btn btn-sm btn-ghost"
title="Toggle direction {servo.direction}" title="Toggle direction {servo.direction}"
onclick={() => toggleDirection(index)}> onclick={() => toggleDirection(index)}
>
{#if servo.direction === 1} {#if servo.direction === 1}
<RotateCw class="w-4 h-4 text-green-500" /> <RotateCw class="w-4 h-4 text-green-500" />
{:else} {:else}
@@ -104,7 +107,8 @@
onblur={syncConfig} onblur={syncConfig}
oninput={event => updateValue(event, index, 'conversion')} oninput={event => updateValue(event, index, 'conversion')}
min="0" min="0"
max="10" /> max="10"
/>
</td> </td>
</tr> </tr>
{/each} {/each}
@@ -41,7 +41,8 @@
max="600" max="600"
bind:value={pwm} bind:value={pwm}
oninput={updatePWM} oninput={updatePWM}
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" /> class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/>
<div class="flex flex-col"> <div class="flex flex-col">
<h2 class="text-lg">General servo configuration</h2> <h2 class="text-lg">General servo configuration</h2>
@@ -55,7 +56,8 @@
type="checkbox" type="checkbox"
class="toggle" class="toggle"
bind:checked={active} bind:checked={active}
onchange={active ? activateServo : deactivateServo} /> onchange={active ? activateServo : deactivateServo}
/>
</span> </span>
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<label for="servoId">Servo active {servoId}</label> <label for="servoId">Servo active {servoId}</label>
+5 -5
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
import { goto } from '$app/navigation'; import { goto } from '$app/navigation'
export const load = (async () => { export const load = (async () => {
goto('/'); goto('/')
return; return
}) satisfies PageLoad; }) satisfies PageLoad
+2 -1
View File
@@ -18,7 +18,8 @@
<button <button
class="opacity-0 group-hover:opacity-100 p-1 hover:text-red-500" class="opacity-0 group-hover:opacity-100 p-1 hover:text-red-500"
onclick={() => onDelete(name)}> onclick={() => onDelete(name)}
>
<TrashIcon class="w-4 h-4" /> <TrashIcon class="w-4 h-4" />
</button> </button>
</div> </div>
@@ -106,7 +106,10 @@
<FileIcon class="w-4 h-4" /> <FileIcon class="w-4 h-4" />
New File New File
</button> </button>
<button class="btn btn-sm btn-primary flex items-center gap-2" onclick={openNewFolderDialog}> <button
class="btn btn-sm btn-primary flex items-center gap-2"
onclick={openNewFolderDialog}
>
<Add class="w-4 h-4" /> <Add class="w-4 h-4" />
New Folder New Folder
</button> </button>
@@ -117,7 +120,8 @@
<div class="flex flex-col md:flex-row gap-4 w-full"> <div class="flex flex-col md:flex-row gap-4 w-full">
<!-- File Tree --> <!-- File Tree -->
<div <div
class="w-full md:w-[300px] md:min-w-[300px] md:max-w-[300px] border-b md:border-b-0 md:border-r pb-4 md:pb-0 md:pr-4"> class="w-full md:w-[300px] md:min-w-[300px] md:max-w-[300px] border-b md:border-b-0 md:border-r pb-4 md:pb-0 md:pr-4"
>
{#await getFiles()} {#await getFiles()}
<Spinner /> <Spinner />
{:then files} {:then files}
@@ -126,19 +130,25 @@
files={files.root} files={files.root}
expanded expanded
selected={updateSelected} selected={updateSelected}
onDelete={deleteFile} /> onDelete={deleteFile}
/>
{/await} {/await}
</div> </div>
<!-- File Content --> <!-- File Content -->
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
{#if filename} {#if filename}
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-2"> <div
class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-2"
>
<h3 class="text-lg font-semibold truncate">{filename}</h3> <h3 class="text-lg font-semibold truncate">{filename}</h3>
<div class="flex gap-2"> <div class="flex gap-2">
{#if isEditing} {#if isEditing}
<button class="btn btn-sm btn-primary" onclick={saveContent}>Save</button> <button class="btn btn-sm btn-primary" onclick={saveContent}>Save</button>
<button class="btn btn-sm btn-secondary" onclick={() => (isEditing = false)}> <button
class="btn btn-sm btn-secondary"
onclick={() => (isEditing = false)}
>
Cancel Cancel
</button> </button>
{:else} {:else}
@@ -158,7 +168,8 @@
{#if isEditing} {#if isEditing}
<textarea <textarea
class="w-full h-[300px] sm:h-[500px] font-mono p-2 bg-gray-800 text-white" class="w-full h-[300px] sm:h-[500px] font-mono p-2 bg-gray-800 text-white"
bind:value={content}></textarea> bind:value={content}
></textarea>
{:else} {:else}
<pre <pre
class="bg-gray-800 p-4 rounded overflow-auto max-h-[300px] sm:max-h-[500px]">{content}</pre> class="bg-gray-800 p-4 rounded overflow-auto max-h-[300px] sm:max-h-[500px]">{content}</pre>
@@ -20,19 +20,25 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
use:exitBeforeEnter use:exitBeforeEnter
use:focusTrap> use:focusTrap
>
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"> class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">Create New File</h2> <h2 class="text-base-content text-start text-2xl font-bold">Create New File</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<input <input
type="text" type="text"
class="input input-bordered w-full" class="input input-bordered w-full"
placeholder="File name" placeholder="File name"
bind:value={fileName} /> bind:value={fileName}
/>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}> <button
class="btn btn-error inline-flex items-center"
onclick={() => modals.close()}
>
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span> <Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
</button> </button>
<button class="btn btn-primary inline-flex items-center" onclick={handleCreate}> <button class="btn btn-primary inline-flex items-center" onclick={handleCreate}>
@@ -20,19 +20,25 @@
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
use:exitBeforeEnter use:exitBeforeEnter
use:focusTrap> use:focusTrap
>
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"> class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">Create New Folder</h2> <h2 class="text-base-content text-start text-2xl font-bold">Create New Folder</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<input <input
type="text" type="text"
class="input input-bordered w-full" class="input input-bordered w-full"
placeholder="Folder name" placeholder="Folder name"
bind:value={folderName} /> bind:value={folderName}
/>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button class="btn btn-error inline-flex items-center" onclick={() => modals.close()}> <button
class="btn btn-error inline-flex items-center"
onclick={() => modals.close()}
>
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span> <Cancel class="mr-2 h-5 w-5" /><span>Cancel</span>
</button> </button>
<button class="btn btn-primary inline-flex items-center" onclick={handleCreate}> <button class="btn btn-primary inline-flex items-center" onclick={handleCreate}>
+1 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import SystemMetrics from './SystemMetrics.svelte'; import SystemMetrics from './SystemMetrics.svelte'
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,5 +1,5 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { title: 'System Metrics' }; return { title: 'System Metrics' }
}) satisfies PageLoad; }) satisfies PageLoad
@@ -250,21 +250,24 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={heapChartElement}></canvas> <canvas bind:this={heapChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-52" class="flex w-full flex-col space-y-1 h-52"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={filesystemChartElement}></canvas> <canvas bind:this={filesystemChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-52" class="flex w-full flex-col space-y-1 h-52"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={temperatureChartElement}></canvas> <canvas bind:this={temperatureChartElement}></canvas>
</div> </div>
</div> </div>
@@ -153,38 +153,45 @@
{#if systemInformation} {#if systemInformation}
<div <div
class="flex w-full flex-col space-y-1" class="flex w-full flex-col space-y-1"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<StatusItem <StatusItem
icon={CPU} icon={CPU}
title="Chip" title="Chip"
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} /> description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`}
/>
<StatusItem <StatusItem
icon={SDK} icon={SDK}
title="SDK Version" title="SDK Version"
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} /> description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`}
/>
<StatusItem <StatusItem
icon={CPP} icon={CPP}
title="Firmware Version" title="Firmware Version"
description={systemInformation.firmware_version} /> description={systemInformation.firmware_version}
/>
<StatusItem <StatusItem
icon={Speed} icon={Speed}
title="CPU Frequency" title="CPU Frequency"
description={`${systemInformation.cpu_freq_mhz} MHz ${ description={`${systemInformation.cpu_freq_mhz} MHz ${
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core' systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
}`} /> }`}
/>
<StatusItem <StatusItem
icon={Heap} icon={Heap}
title="Heap (Free / Max Alloc)" title="Heap (Free / Max Alloc)"
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} /> description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`}
/>
<StatusItem <StatusItem
icon={Pyramid} icon={Pyramid}
title="PSRAM (Size / Free)" title="PSRAM (Size / Free)"
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} /> description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`}
/>
<StatusItem <StatusItem
icon={Sketch} icon={Sketch}
@@ -195,14 +202,16 @@
).toFixed(1)} % of ).toFixed(1)} % of
${systemInformation.free_sketch_space / 1000000} MB used (${ ${systemInformation.free_sketch_space / 1000000} MB used (${
(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000 (systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000
} MB free)`} /> } MB free)`}
/>
<StatusItem <StatusItem
icon={Flash} icon={Flash}
title="Flash Chip (Size / Speed)" title="Flash Chip (Size / Speed)"
description={`${systemInformation.flash_chip_size / 1000000} MB / ${ description={`${systemInformation.flash_chip_size / 1000000} MB / ${
systemInformation.flash_chip_speed / 1000000 systemInformation.flash_chip_speed / 1000000
} MHz`} /> } MHz`}
/>
<StatusItem <StatusItem
icon={Folder} icon={Folder}
@@ -213,7 +222,8 @@
).toFixed(1)} % of ${systemInformation.fs_total / 1000000} MB used (${ ).toFixed(1)} % of ${systemInformation.fs_total / 1000000} MB used (${
(systemInformation.fs_total - systemInformation.fs_used) / 1000000 (systemInformation.fs_total - systemInformation.fs_used) / 1000000
} }
MB free)`} /> MB free)`}
/>
<StatusItem <StatusItem
icon={Temperature} icon={Temperature}
@@ -222,17 +232,20 @@
systemInformation.core_temp == 53.33 ? systemInformation.core_temp == 53.33 ?
'NaN' 'NaN'
: systemInformation.core_temp.toFixed(2) + ' °C' : systemInformation.core_temp.toFixed(2) + ' °C'
}`} /> }`}
/>
<StatusItem <StatusItem
icon={Stopwatch} icon={Stopwatch}
title="Uptime" title="Uptime"
description={convertSeconds(systemInformation.uptime)} /> description={convertSeconds(systemInformation.uptime)}
/>
<StatusItem <StatusItem
icon={Power} icon={Power}
title="Reset Reason" title="Reset Reason"
description={systemInformation.cpu_reset_reason} /> description={systemInformation.cpu_reset_reason}
/>
</div> </div>
{/if} {/if}
{/await} {/await}
@@ -245,7 +258,8 @@
onclick={button.onClick} onclick={button.onClick}
icon={button.icon} icon={button.icon}
label={button.label} label={button.label}
type={button.type || 'primary'} /> type={button.type || 'primary'}
/>
{/if} {/if}
{/each} {/each}
</div> </div>
+4 -4
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import UploadFirmware from './UploadFirmware.svelte'; import UploadFirmware from './UploadFirmware.svelte'
import GithubFirmwareManager from './GithubFirmwareManager.svelte'; import GithubFirmwareManager from './GithubFirmwareManager.svelte'
import { useFeatureFlags } from '$lib/stores'; import { useFeatureFlags } from '$lib/stores'
const features = useFeatureFlags(); const features = useFeatureFlags()
</script> </script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8"> <div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
+3 -3
View File
@@ -1,5 +1,5 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
export const load = (async () => { export const load = (async () => {
return { title: 'Firmware Update' }; return { title: 'Firmware Update' }
}) satisfies PageLoad; }) satisfies PageLoad
@@ -1,46 +1,46 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state'; import { page } from '$app/state'
import { modals } from 'svelte-modals'; import { modals } from 'svelte-modals'
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte'
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions'
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'; import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'
import InfoDialog from '$lib/components/InfoDialog.svelte'; import InfoDialog from '$lib/components/InfoDialog.svelte'
import { api } from '$lib/api'; import { api } from '$lib/api'
import { useFeatureFlags } from '$lib/stores'; import { useFeatureFlags } from '$lib/stores'
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons'; import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons'
const features = useFeatureFlags(); const features = useFeatureFlags()
async function getGithubAPI() { async function getGithubAPI() {
const headers = { const headers = {
accept: 'application/vnd.github+json', accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28', 'X-GitHub-Api-Version': '2022-11-28'
};
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
headers,
});
if (result.isErr()) {
console.error('Error:', result.inner);
return;
} }
return result.inner as any; const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
headers
})
if (result.isErr()) {
console.error('Error:', result.inner)
return
}
return result.inner as any
} }
async function postGithubDownload(url: string) { async function postGithubDownload(url: string) {
const result = await api.post('/api/firmware/download', { download_url: url }); const result = await api.post('/api/firmware/download', { download_url: url })
if (result.isErr()) { if (result.isErr()) {
console.error('Error:', result.inner); console.error('Error:', result.inner)
return; return
} }
} }
function confirmGithubUpdate(assets: any) { function confirmGithubUpdate(assets: any) {
let url = ''; let url = ''
// iterate over assets and find the correct one // iterate over assets and find the correct one
for (let i = 0; i < assets.length; i++) { for (let i = 0; i < assets.length; i++) {
// check if the asset is of type *.bin // check if the asset is of type *.bin
@@ -48,7 +48,7 @@
assets[i].name.includes('.bin') && assets[i].name.includes('.bin') &&
assets[i].name.includes($features.firmware_built_target) assets[i].name.includes($features.firmware_built_target)
) { ) {
url = assets[i].browser_download_url; url = assets[i].browser_download_url
} }
} }
if (url === '') { if (url === '') {
@@ -58,9 +58,9 @@
message: message:
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.', 'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
dismiss: { label: 'OK', icon: Check }, dismiss: { label: 'OK', icon: Check },
onDismiss: () => modals.close(), onDismiss: () => modals.close()
}); })
return; return
} }
modals.open(ConfirmDialog, { modals.open(ConfirmDialog, {
@@ -68,15 +68,15 @@
message: 'Are you sure you want to overwrite the existing firmware with a new one?', message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: { labels: {
cancel: { label: 'Abort', icon: Cancel }, cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Update', icon: CloudDown }, confirm: { label: 'Update', icon: CloudDown }
}, },
onConfirm: () => { onConfirm: () => {
postGithubDownload(url); postGithubDownload(url)
modals.open(GithubUpdateDialog, { modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll(), onConfirm: () => modals.closeAll()
}); })
}, }
}); })
} }
</script> </script>
@@ -91,7 +91,10 @@
<Spinner /> <Spinner />
{:then githubReleases} {:then githubReleases}
<div class="relative w-full overflow-visible"> <div class="relative w-full overflow-visible">
<div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}> <div
class="overflow-x-auto"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<table class="table w-full table-auto"> <table class="table w-full table-auto">
<thead> <thead>
<tr class="font-bold"> <tr class="font-bold">
@@ -105,21 +108,26 @@
{#each githubReleases as release} {#each githubReleases as release}
<tr <tr
class={( class={(
compareVersions($features.firmware_version as string, release.tag_name) === 0 compareVersions(
$features.firmware_version as string,
release.tag_name
) === 0
) ? ) ?
'bg-primary text-primary-content' 'bg-primary text-primary-content'
: 'bg-base-100 h-14'}> : 'bg-base-100 h-14'}
>
<td align="left" class="text-base font-semibold"> <td align="left" class="text-base font-semibold">
<a <a
href={release.html_url} href={release.html_url}
class="link link-hover" class="link link-hover"
target="_blank" target="_blank"
rel="noopener noreferrer">{release.name}</a rel="noopener noreferrer">{release.name}</a
></td> ></td
>
<td align="center" class="hidden min-h-full align-middle sm:block"> <td align="center" class="hidden min-h-full align-middle sm:block">
<div class="my-2"> <div class="my-2">
{new Intl.DateTimeFormat('en-GB', { {new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium', dateStyle: 'medium'
}).format(new Date(release.published_at))} }).format(new Date(release.published_at))}
</div> </div>
</td> </td>
@@ -133,8 +141,9 @@
<button <button
class="btn btn-ghost btn-circle btn-sm" class="btn btn-ghost btn-circle btn-sm"
onclick={() => { onclick={() => {
confirmGithubUpdate(release.assets); confirmGithubUpdate(release.assets)
}}> }}
>
<CloudDown class="text-secondary h-6 w-6" /> <CloudDown class="text-secondary h-6 w-6" />
</button> </button>
{/if} {/if}
@@ -148,7 +157,9 @@
{:catch error} {:catch error}
<div class="alert alert-error shadow-lg"> <div class="alert alert-error shadow-lg">
<Error class="h-6 w-6 shrink-0" /> <Error class="h-6 w-6 shrink-0" />
<span>Please connect to a network with internet access to perform a firmware update.</span> <span
>Please connect to a network with internet access to perform a firmware update.</span
>
</div> </div>
{/await} {/await}
</SettingsCard> </SettingsCard>
@@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import { modals } from 'svelte-modals'; import { modals } from 'svelte-modals'
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { api } from '$lib/api'; import { api } from '$lib/api'
import { Cancel, OTA, Warning } from '$lib/components/icons'; import { Cancel, OTA, Warning } from '$lib/components/icons'
let files: FileList | undefined = $state(); let files: FileList | undefined = $state()
async function uploadBIN() { async function uploadBIN() {
const formData = new FormData(); const formData = new FormData()
formData.append('file', files![0]); formData.append('file', files![0])
const result = await api.post('/api/firmware', formData); const result = await api.post('/api/firmware', formData)
if (result.isErr()) console.error('Error:', result.inner); if (result.isErr()) console.error('Error:', result.inner)
} }
function confirmBinUpload() { function confirmBinUpload() {
@@ -21,13 +21,13 @@
message: 'Are you sure you want to overwrite the existing firmware with a new one?', message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: { labels: {
cancel: { label: 'Abort', icon: Cancel }, cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Upload', icon: OTA }, confirm: { label: 'Upload', icon: OTA }
}, },
onConfirm: () => { onConfirm: () => {
modals.close(); modals.close()
uploadBIN(); uploadBIN()
}, }
}); })
} }
</script> </script>
@@ -41,8 +41,8 @@
<div class="alert alert-warning shadow-lg"> <div class="alert alert-warning shadow-lg">
<Warning class="h-6 w-6 shrink-0" /> <Warning class="h-6 w-6 shrink-0" />
<span <span
>Uploading a new firmware (.bin) file will replace the existing firmware. You may upload a >Uploading a new firmware (.bin) file will replace the existing firmware. You may upload
(.md5) file first to verify the uploaded firmware. a (.md5) file first to verify the uploaded firmware.
</span> </span>
</div> </div>
@@ -52,5 +52,6 @@
class="file-input file-input-bordered file-input-secondary mt-4 w-full" class="file-input file-input-bordered file-input-secondary mt-4 w-full"
bind:files bind:files
accept=".bin,.md5" accept=".bin,.md5"
onchange={confirmBinUpload} /> onchange={confirmBinUpload}
/>
</SettingsCard> </SettingsCard>
+5 -5
View File
@@ -1,7 +1,7 @@
import type { PageLoad } from './$types'; import type { PageLoad } from './$types'
import { goto } from '$app/navigation'; import { goto } from '$app/navigation'
export const load = (async () => { export const load = (async () => {
goto('/'); goto('/')
return; return
}) satisfies PageLoad; }) satisfies PageLoad

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