🎨 format
This commit is contained in:
+1
-1
@@ -28,4 +28,4 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
}
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
Vendored
+5
-1
@@ -1,3 +1,7 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["svelte.svelte-vscode", "bradlc.vscode-tailwindcss", "esbenp.prettier-vscode"]
|
"recommendations": [
|
||||||
|
"svelte.svelte-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-4
@@ -1,8 +1,8 @@
|
|||||||
declare module "app-env" {
|
declare module 'app-env' {
|
||||||
interface ENV {
|
interface ENV {
|
||||||
VITE_USE_HOST_NAME: boolean;
|
VITE_USE_HOST_NAME: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const appEnv: ENV;
|
const appEnv: ENV
|
||||||
export default appEnv;
|
export default appEnv
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Vendored
+1
-1
@@ -10,4 +10,4 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {};
|
export {}
|
||||||
|
|||||||
+4
-1
@@ -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%
|
||||||
|
|||||||
@@ -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
@@ -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}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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)} />
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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 }
|
||||||
})();
|
})()
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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', '')
|
||||||
|
|||||||
@@ -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([])
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Vendored
+15
-15
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+9
-9
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})`
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './err';
|
export * from './err'
|
||||||
export * from './ok';
|
export * from './ok'
|
||||||
export * from './result';
|
export * from './result'
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
export const load = async () => {
|
export const load = async () => {
|
||||||
return { title: 'Controller' };
|
return { title: 'Controller' }
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user