Fixes build warning and errors

This commit is contained in:
Rune Harlyk
2025-07-10 21:32:28 +02:00
committed by Rune Harlyk
parent c8ee64d7f4
commit d529eaa201
22 changed files with 2053 additions and 2116 deletions
+9 -20
View File
@@ -2,25 +2,17 @@
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 } from 'svelte-modals'; import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
title: string;
message: string;
onDismiss: any;
dismiss?: any;
}
let { let {
isOpen, isOpen,
title, title,
message, message,
onDismiss, onDismiss,
dismiss = { label: 'Dismiss', icon: Check } labels = {
}: Props = $props(); dismiss: { label: 'Dismiss', icon: Check },
},
}: ModalProps = $props();
</script> </script>
{#if isOpen} {#if isOpen}
@@ -29,11 +21,9 @@
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>
@@ -41,9 +31,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>
<dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
</button> </button>
</div> </div>
</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 = $state() 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">
+168 -166
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte';
import { import {
BufferGeometry, BufferGeometry,
Line, Line,
@@ -10,8 +10,8 @@
SphereGeometry, SphereGeometry,
Vector3, Vector3,
type NormalBufferAttributes, type NormalBufferAttributes,
type Object3DEventMap type Object3DEventMap,
} from 'three' } from 'three';
import { import {
ModesEnum, ModesEnum,
kinematicData, kinematicData,
@@ -21,56 +21,55 @@
servoAnglesOut, servoAnglesOut,
servoAngles, servoAngles,
mpu, mpu,
jointNames jointNames,
} from '$lib/stores' } from '$lib/stores';
import { import {
extractFootColor, extractFootColor,
populateModelCache, populateModelCache,
throttler, throttler,
getToeWorldPositions getToeWorldPositions,
} from '$lib/utilities' } from '$lib/utilities';
import SceneBuilder from '$lib/sceneBuilder' import SceneBuilder from '$lib/sceneBuilder';
import { lerp, degToRad } from 'three/src/math/MathUtils' import { lerp, degToRad } from 'three/src/math/MathUtils';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js' import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import Kinematic, { type body_state_t } from '$lib/kinematic' import Kinematic, { type body_state_t } from '$lib/kinematic';
import { import {
BezierState, BezierState,
CalibrationState, CalibrationState,
EightPhaseWalkState, EightPhaseWalkState,
FourPhaseWalkState,
IdleState, IdleState,
RestState, RestState,
StandState StandState,
} from '$lib/gait' } from '$lib/gait';
import { radToDeg } from 'three/src/math/MathUtils.js' import { radToDeg } from 'three/src/math/MathUtils.js';
import type { URDFRobot } from 'urdf-loader' import type { URDFRobot } from 'urdf-loader';
import { get } from 'svelte/store' import { get } from 'svelte/store';
interface Props { interface Props {
sky?: boolean sky?: boolean;
orbit?: boolean orbit?: boolean;
panel?: boolean panel?: boolean;
debug?: boolean debug?: boolean;
ground?: boolean ground?: boolean;
} }
let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props() let { sky = true, orbit = false, panel = true, debug = false, ground = true }: Props = $props();
let sceneManager = $state(new SceneBuilder()) let sceneManager = $state(new SceneBuilder());
let canvas: HTMLCanvasElement = $state() let canvas: HTMLCanvasElement;
let currentModelAngles: number[] = new Array(12).fill(0) let currentModelAngles: number[] = new Array(12).fill(0);
let modelTargetAngles: number[] = new Array(12).fill(0) let modelTargetAngles: number[] = new Array(12).fill(0);
let gui_panel: GUI let gui_panel: GUI;
let Throttler = new throttler() let Throttler = new throttler();
let feet_trace = new Array(4).fill([]) let feet_trace = new Array(4).fill([]);
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = [] let trace_lines: BufferGeometry<NormalBufferAttributes>[] = [];
let target: Object3D<Object3DEventMap> let target: Object3D<Object3DEventMap>;
let target_position = { x: 0, z: 0, yaw: 0 } let target_position = { x: 0, z: 0, yaw: 0 };
let kinematic = new Kinematic() let kinematic = new Kinematic();
let planners = { let planners = {
[ModesEnum.Deactivated]: new IdleState(), [ModesEnum.Deactivated]: new IdleState(),
@@ -79,11 +78,11 @@
[ModesEnum.Rest]: new RestState(), [ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(), [ModesEnum.Stand]: new StandState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(), [ModesEnum.Crawl]: new EightPhaseWalkState(),
[ModesEnum.Walk]: new BezierState() [ModesEnum.Walk]: new BezierState(),
} };
let lastTick = performance.now() let lastTick = performance.now();
const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1] const dir = [1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, -1];
let body_state = { let body_state = {
omega: 0, omega: 0,
@@ -92,8 +91,8 @@
xm: 0, xm: 0,
ym: 0.5, ym: 0.5,
zm: 0, zm: 0,
feet: kinematic.getDefaultFeetPos() feet: kinematic.getDefaultFeetPos(),
} };
let settings = { let settings = {
'Internal kinematic': true, 'Internal kinematic': true,
@@ -110,52 +109,52 @@
xm: 0, xm: 0,
ym: 0.7, ym: 0.7,
zm: 0, zm: 0,
Background: 'black' Background: 'black',
} };
onMount(async () => { onMount(async () => {
await populateModelCache() await populateModelCache();
await createScene() await createScene();
servoAngles.subscribe(updateAnglesFromStore) servoAngles.subscribe(updateAnglesFromStore);
if (panel) createPanel() if (panel) createPanel();
}) });
onDestroy(() => { onDestroy(() => {
canvas.remove() canvas.remove();
gui_panel?.destroy() gui_panel?.destroy();
}) });
const updateAnglesFromStore = (angles: number[]) => { const updateAnglesFromStore = (angles: number[]) => {
if (sceneManager.isDragging) return if (sceneManager.isDragging) return;
if (settings['Internal kinematic']) return if (settings['Internal kinematic']) return;
modelTargetAngles = angles modelTargetAngles = angles;
} };
const createPanel = () => { const createPanel = () => {
gui_panel = new GUI({ width: 310 }) gui_panel = new GUI({ width: 310 });
gui_panel.close() gui_panel.close();
gui_panel.domElement.id = 'three-gui-panel' gui_panel.domElement.id = 'three-gui-panel';
const general = gui_panel.addFolder('General') const general = gui_panel.addFolder('General');
general.add(settings, 'Internal kinematic') general.add(settings, 'Internal kinematic');
general.add(settings, 'Robot transform controls') general.add(settings, 'Robot transform controls');
general.add(settings, 'Auto orient robot') general.add(settings, 'Auto orient robot');
const kinematic = gui_panel.addFolder('Kinematics') const kinematic = gui_panel.addFolder('Kinematics');
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen() kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen() kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen() kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen() kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen() kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen() kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen();
const visibility = gui_panel.addFolder('Visualization') const visibility = gui_panel.addFolder('Visualization');
visibility.add(settings, 'Trace feet') visibility.add(settings, 'Trace feet');
visibility.add(settings, 'Trace points', 1, 1000, 1) visibility.add(settings, 'Trace points', 1, 1000, 1);
visibility.add(settings, 'Target position') visibility.add(settings, 'Target position');
visibility.add(settings, 'Smooth motion') visibility.add(settings, 'Smooth motion');
visibility.addColor(settings, 'Background') visibility.addColor(settings, 'Background');
} };
const updateKinematicPosition = () => { const updateKinematicPosition = () => {
kinematicData.set([ kinematicData.set([
@@ -164,14 +163,17 @@
settings.psi, settings.psi,
settings.xm, settings.xm,
settings.ym, settings.ym,
settings.zm settings.zm,
]) ]);
} };
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 () => {
sceneManager sceneManager
@@ -185,46 +187,46 @@
.addTransformControls(sceneManager.model) .addTransformControls(sceneManager.model)
.fillParent() .fillParent()
.addRenderCb(render) .addRenderCb(render)
.startRenderLoop() .startRenderLoop();
if (ground) sceneManager.addGroundPlane() if (ground) sceneManager.addGroundPlane();
const geometry = new SphereGeometry(0.1, 32, 16) const geometry = new SphereGeometry(0.1, 32, 16);
const material = new MeshBasicMaterial({ color: 0xffff00 }) const material = new MeshBasicMaterial({ color: 0xffff00 });
target = new Mesh(geometry, material) target = new Mesh(geometry, material);
sceneManager.scene.add(target) sceneManager.scene.add(target);
if (debug) { if (debug) {
sceneManager.addDragControl(updateAngles) sceneManager.addDragControl(updateAngles);
} }
if (sky) sceneManager.addSky() if (sky) sceneManager.addSky();
for (let i = 0; i < 4; i++) { for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry() const geometry = new BufferGeometry();
const material = new LineBasicMaterial({ color: extractFootColor() }) const material = new LineBasicMaterial({ color: extractFootColor() });
const line = new Line(geometry, material) const line = new Line(geometry, material);
trace_lines.push(geometry) trace_lines.push(geometry);
sceneManager.scene.add(line) sceneManager.scene.add(line);
}
} }
};
const renderTraceLines = (foot_positions: Vector3[]) => { const renderTraceLines = (foot_positions: Vector3[]) => {
if (!settings['Trace feet']) { if (!settings['Trace feet']) {
if (!feet_trace.length) return if (!feet_trace.length) return;
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1))) trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)));
feet_trace = new Array(4).fill([]) feet_trace = new Array(4).fill([]);
return return;
} }
trace_lines.forEach((line, i) => { trace_lines.forEach((line, i) => {
feet_trace[i].push(foot_positions[i]) feet_trace[i].push(foot_positions[i]);
feet_trace[i] = feet_trace[i].slice(-settings['Trace points']) feet_trace[i] = feet_trace[i].slice(-settings['Trace points']);
line.setFromPoints(feet_trace[i]) line.setFromPoints(feet_trace[i]);
}) });
} };
const calculate_kinematics = () => { const calculate_kinematics = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return if (sceneManager.isDragging || !settings['Internal kinematic']) return;
const position: body_state_t = { const position: body_state_t = {
omega: settings.omega, omega: settings.omega,
phi: settings.phi, phi: settings.phi,
@@ -232,37 +234,37 @@
xm: settings.xm, xm: settings.xm,
ym: settings.ym, ym: settings.ym,
zm: settings.zm, zm: settings.zm,
feet: body_state.feet feet: body_state.feet,
} };
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i])) let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]));
modelTargetAngles = new_angles modelTargetAngles = new_angles;
} };
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => { const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return if (settings['Robot transform controls'] || !settings['Auto orient robot']) return;
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y)) robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1) 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);
} };
const update_camera = (robot: URDFRobot) => { const update_camera = (robot: URDFRobot) => {
if (!settings['Fix camera on robot']) return if (!settings['Fix camera on robot']) return;
sceneManager.orbit.target = robot.position.clone() sceneManager.orbit.target = robot.position.clone();
} };
const smooth = (start: number, end: number, amount: number) => { const smooth = (start: number, end: number, amount: number) => {
return settings['Smooth motion'] ? lerp(start, end, amount) : end return settings['Smooth motion'] ? lerp(start, end, amount) : end;
} };
const update_gait = () => { const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return if (sceneManager.isDragging || !settings['Internal kinematic']) return;
const controlData = get(outControllerData) const controlData = get(outControllerData);
const data = { const data = {
stop: controlData[0], stop: controlData[0],
lx: controlData[1], lx: controlData[1],
@@ -271,67 +273,67 @@
ry: controlData[4], ry: controlData[4],
h: controlData[5], h: controlData[5],
s: controlData[6], s: controlData[6],
s1: controlData[7] s1: controlData[7],
} };
body_state.ym = ((data.h + 127) * 0.35) / 100 body_state.ym = ((data.h + 127) * 0.35) / 100;
let planner = planners[get(mode)] let planner = planners[get(mode)];
const delta = performance.now() - lastTick const delta = performance.now() - lastTick;
lastTick = performance.now() lastTick = performance.now();
body_state = planner.step(body_state, data, delta) body_state = planner.step(body_state, data, delta);
settings.omega = body_state.omega settings.omega = body_state.omega;
settings.phi = body_state.phi settings.phi = body_state.phi;
settings.psi = body_state.psi settings.psi = body_state.psi;
settings.xm = body_state.xm settings.xm = body_state.xm;
settings.ym = body_state.ym settings.ym = body_state.ym;
settings.zm = body_state.zm settings.zm = body_state.zm;
} };
const update_robot_position = (robot: URDFRobot) => { const update_robot_position = (robot: URDFRobot) => {
if (!settings['Robot transform controls']) return if (!settings['Robot transform controls']) return;
settings.omega = radToDeg(robot.rotation.y) settings.omega = radToDeg(robot.rotation.y);
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90 settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90;
settings.psi = radToDeg(robot.rotation.x) + 90 settings.psi = radToDeg(robot.rotation.x) + 90;
settings.xm = robot.position.z * 100 settings.xm = robot.position.z * 100;
settings.zm = -robot.position.x * 100 settings.zm = -robot.position.x * 100;
} };
const updateTargetPosition = () => { const updateTargetPosition = () => {
target.visible = settings['Target position'] target.visible = settings['Target position'];
target.position.x = smooth(target.position.x, target_position.x, 0.5) target.position.x = smooth(target.position.x, target_position.x, 0.5);
target.position.z = smooth(target.position.z, target_position.z, 0.5) target.position.z = smooth(target.position.z, target_position.z, 0.5);
} };
const render = () => { const render = () => {
const robot = sceneManager.model const robot = sceneManager.model;
if (!robot) return if (!robot) return;
const toes = getToeWorldPositions(robot) const toes = getToeWorldPositions(robot);
renderTraceLines(toes) renderTraceLines(toes);
update_camera(robot) update_camera(robot);
update_gait() update_gait();
calculate_kinematics() calculate_kinematics();
update_robot_position(robot) update_robot_position(robot);
sceneManager.transformControl.showX = settings['Robot transform controls'] sceneManager.transformControl.showX = settings['Robot transform controls'];
sceneManager.transformControl.showY = settings['Robot transform controls'] sceneManager.transformControl.showY = settings['Robot transform controls'];
sceneManager.transformControl.showZ = settings['Robot transform controls'] sceneManager.transformControl.showZ = settings['Robot transform controls'];
for (let i = 0; i < $jointNames.length; i++) { for (let i = 0; i < $jointNames.length; i++) {
currentModelAngles[i] = smooth( currentModelAngles[i] = smooth(
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI), (robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
modelTargetAngles[i], modelTargetAngles[i],
0.1 0.1
) );
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i])) robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
} }
orient_robot(robot, toes) orient_robot(robot, toes);
updateTargetPosition() updateTargetPosition();
} };
</script> </script>
<svelte:window onresize={sceneManager.fillParent} /> <svelte:window onresize={sceneManager.fillParent} />
@@ -1,6 +1,10 @@
<script lang="ts"> <script lang="ts">
import WidgetContainer from './WidgetContainer.svelte'; import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application'; import {
WidgetComponents,
type WidgetContainerConfig,
isWidgetConfig,
} from '$lib/stores/application';
import Widget from './Widget.svelte'; import Widget from './Widget.svelte';
interface Props { interface Props {
@@ -15,8 +19,7 @@
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)}
@@ -29,8 +32,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>
+53 -53
View File
@@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/state' import { page } from '$app/state';
import { useFeatureFlags } from '$lib/stores/featureFlags' import { useFeatureFlags } from '$lib/stores/featureFlags';
import GithubButton from '../menu/GithubButton.svelte' import GithubButton from '../menu/GithubButton.svelte';
import LogoButton from '../menu/LogoButton.svelte' import LogoButton from '../menu/LogoButton.svelte';
import MenuList from '../menu/MenuList.svelte' import MenuList from '../menu/MenuList.svelte';
import { import {
Connection, Connection,
Settings, Settings,
@@ -20,28 +20,28 @@
AP, AP,
Copyright, Copyright,
Metrics, Metrics,
DNS DNS,
} from '$lib/components/icons' } from '$lib/components/icons';
import appEnv from 'app-env' import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public';
const features = useFeatureFlags() const features = useFeatureFlags();
const appName = page.data.app_name const appName = page.data.app_name;
const copyright = page.data.copyright const copyright = page.data.copyright;
const github = { href: 'https://github.com/' + page.data.github, active: true } const github = { href: 'https://github.com/' + page.data.github, active: true };
type menuItem = { type menuItem = {
title: string title: string;
icon: ConstructorOfATypedSvelteComponent icon: ConstructorOfATypedSvelteComponent;
href?: string href?: string;
feature: boolean feature: boolean;
active?: boolean active?: boolean;
submenu?: menuItem[] submenu?: menuItem[];
} };
let menuItems = $state<menuItem[]>([]) let menuItems = $state<menuItem[]>([]);
$effect(() => { $effect(() => {
menuItems = [ menuItems = [
@@ -49,13 +49,13 @@
title: 'Connection', title: 'Connection',
icon: WiFi, icon: WiFi,
href: '/connection', href: '/connection',
feature: !appEnv.VITE_USE_HOST_NAME feature: !PUBLIC_VITE_USE_HOST_NAME,
}, },
{ {
title: 'Controller', title: 'Controller',
icon: MdiController, icon: MdiController,
href: '/controller', href: '/controller',
feature: true feature: true,
}, },
{ {
title: 'Peripherals', title: 'Peripherals',
@@ -66,27 +66,27 @@
title: 'I2C', title: 'I2C',
icon: Connection, icon: Connection,
href: '/peripherals/i2c', href: '/peripherals/i2c',
feature: true feature: true,
}, },
{ {
title: 'Camera', title: 'Camera',
icon: Camera, icon: Camera,
href: '/peripherals/camera', href: '/peripherals/camera',
feature: $features.camera feature: $features.camera,
}, },
{ {
title: 'Servo', title: 'Servo',
icon: MotorOutline, icon: MotorOutline,
href: '/peripherals/servo', href: '/peripherals/servo',
feature: true feature: true,
}, },
{ {
title: 'IMU', title: 'IMU',
icon: Rotate3d, icon: Rotate3d,
href: '/peripherals/imu', href: '/peripherals/imu',
feature: $features.imu || $features.mag || $features.bmp feature: $features.imu || $features.mag || $features.bmp,
} },
] ],
}, },
{ {
title: 'WiFi', title: 'WiFi',
@@ -97,21 +97,21 @@
title: 'WiFi Station', title: 'WiFi Station',
icon: Router, icon: Router,
href: '/wifi/sta', href: '/wifi/sta',
feature: true feature: true,
}, },
{ {
title: 'Access Point', title: 'Access Point',
icon: AP, icon: AP,
href: '/wifi/ap', href: '/wifi/ap',
feature: true feature: true,
}, },
{ {
title: 'mDNS', title: 'mDNS',
icon: DNS, icon: DNS,
href: '/wifi/mdns', href: '/wifi/mdns',
feature: true feature: true,
} },
] ],
}, },
{ {
title: 'System', title: 'System',
@@ -122,51 +122,51 @@
title: 'System Status', title: 'System Status',
icon: Health, icon: Health,
href: '/system/status', href: '/system/status',
feature: true feature: true,
}, },
{ {
title: 'File System', title: 'File System',
icon: Folder, icon: Folder,
href: '/system/filesystem', href: '/system/filesystem',
feature: true feature: true,
}, },
{ {
title: 'System Metrics', title: 'System Metrics',
icon: Metrics, icon: Metrics,
href: '/system/metrics', href: '/system/metrics',
feature: true feature: true,
}, },
{ {
title: 'Firmware Update', title: 'Firmware Update',
icon: Update, icon: Update,
href: '/system/update', href: '/system/update',
feature: $features.ota || $features.upload_firmware || $features.download_firmware feature: $features.ota || $features.upload_firmware || $features.download_firmware,
} },
] ],
} },
] as menuItem[] ] as menuItem[];
}) });
const { menuClicked } = $props() const { menuClicked } = $props();
function setActiveMenuItem(targetTitle: string) { function setActiveMenuItem(targetTitle: string) {
menuItems.forEach(item => { menuItems.forEach(item => {
item.active = item.title === targetTitle item.active = item.title === targetTitle;
item.submenu?.forEach(subItem => { item.submenu?.forEach(subItem => {
subItem.active = subItem.title === targetTitle subItem.active = subItem.title === targetTitle;
}) });
}) });
menuItems = menuItems menuItems = menuItems;
menuClicked() menuClicked();
} }
$effect(() => { $effect(() => {
setActiveMenuItem(page.data.title) setActiveMenuItem(page.data.title);
}) });
const updateMenu = (event: any) => { const updateMenu = (event: any) => {
setActiveMenuItem(event.details) setActiveMenuItem(event.details);
} };
</script> </script>
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content"> <div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
@@ -25,7 +25,7 @@
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<GithubRelease>( const result = await api.get<GithubRelease>(
`https://api.github.com/repos/${page.data.github}/releases/latest`, `https://api.github.com/repos/${page.data.github}/releases/latest`,
@@ -44,13 +44,13 @@
update = false; update = false;
firmwareVersion = ''; firmwareVersion = '';
if (compareVersions(results.tag_name, $features.firmware_version) === 1) { if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
// iterate over assets and find the correct one // iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) { for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin // check if the asset is of type *.bin
if ( if (
results.assets[i].name.includes('.bin') && results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($features.firmware_built_target) results.assets[i].name.includes($features.firmware_built_target as string)
) { ) {
update = true; update = true;
firmwareVersion = results.tag_name; firmwareVersion = results.tag_name;
@@ -82,14 +82,14 @@
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>
@@ -98,11 +98,9 @@
<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,11 +1,11 @@
<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 = $state(); let chartElement: HTMLCanvasElement;
let chart: Chart; let chart: Chart;
interface Props { interface Props {
@@ -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,35 +69,33 @@
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>
@@ -2,7 +2,7 @@
interface Props { interface Props {
options?: string[]; options?: string[];
selectedOption?: string; selectedOption?: string;
change: () => void; change?: () => void;
[key: string]: any; [key: string]: any;
} }
@@ -12,8 +12,7 @@
<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}
+3 -3
View File
@@ -2,13 +2,13 @@ import { api } from '$lib/api';
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import { writable, type Writable } from 'svelte/store'; import { writable, type Writable } from 'svelte/store';
let featureFlagsStore: Writable<Record<string, boolean>>; let featureFlagsStore: Writable<Record<string, boolean | string>>;
export function useFeatureFlags() { export function useFeatureFlags() {
if (!featureFlagsStore) { if (!featureFlagsStore) {
featureFlagsStore = writable<Record<string, boolean>>({}); featureFlagsStore = writable<Record<string, boolean | string>>({});
api.get<Record<string, boolean>>('/api/features').then((result) => { api.get<Record<string, boolean>>('/api/features').then(result => {
if (result.isOk()) featureFlagsStore.set(result.inner); if (result.isOk()) featureFlagsStore.set(result.inner);
else { else {
notifications.error('Feature flag could not be fetched', 2500); notifications.error('Feature flag could not be fetched', 2500);
+2 -2
View File
@@ -1,5 +1,5 @@
import { persistentStore } from '$lib/utilities'; import { persistentStore } from '$lib/utilities';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import appEnv from 'app-env'; import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public';
export const location = appEnv.VITE_USE_HOST_NAME ? writable('') : persistentStore('location', ''); export const location = PUBLIC_VITE_USE_HOST_NAME ? writable('') : persistentStore('location', '');
-27
View File
@@ -1,27 +0,0 @@
import { readable } from 'svelte/store';
export const heading = readable(0, (set) => {
const updateHeading = (e: any) => {
let alpha;
if (e.webkitCompassHeading) alpha = e.webkitCompassHeading;
else if (e.alpha) alpha = e.alpha;
else {
let q = e.target.quaternion;
alpha =
Math.atan2(2 * q[0] * q[1] + 2 * q[2] * q[3], 1 - 2 * q[1] * q[1] - 2 * q[2] * q[2]) *
(180 / Math.PI);
if (alpha < 0) alpha += 360;
}
set(alpha);
};
if ('AbsoluteOrientationSensor' in window) {
var sensor = new window.AbsoluteOrientationSensor({ frequency: 60 }) as any;
sensor.addEventListener('reading', updateHeading);
sensor.start();
} else if (window.DeviceMotionEvent) window.addEventListener('deviceorientation', updateHeading);
return () => {
if ('AbsoluteOrientationSensor' in window) sensor.removeEventListener('reading', updateHeading);
window.addEventListener('deviceorientation', updateHeading);
};
});
+1 -1
View File
@@ -17,7 +17,7 @@ const decodeMessage = (data: string | ArrayBuffer): SocketMessage | null => {
} }
return JSON.parse(data as string); return JSON.parse(data as string);
} catch (error) { } catch (error) {
console.error(`Could not decode data: ${data} - ${error}`); console.error(`Could not decode data: ${new Uint8Array(data as ArrayBuffer)} - ${error}`);
} }
return null; return null;
}; };
+3 -3
View File
@@ -6,8 +6,8 @@ const registerFetchIntercept = async () => {
const fileService = (await import('$lib/services/file-service')).default; const fileService = (await import('$lib/services/file-service')).default;
window.fetch = async (resource, config) => { window.fetch = async (resource, config) => {
let url = resource instanceof Request ? resource.url : resource.toString(); let url = resource instanceof Request ? resource.url : resource.toString();
let file = await fileService.getFile(url); let file = await fileService?.getFile(url);
return file.isOk() ? new Response(file.inner) : originalFetch(resource, config); return file?.isOk() ? new Response(file.inner) : originalFetch(resource, config);
}; };
}; };
@@ -17,6 +17,6 @@ export const load = async () => {
title: 'Spot micro controller', title: 'Spot micro controller',
github: 'runeharlyk/SpotMicroESP32-Leika', github: 'runeharlyk/SpotMicroESP32-Leika',
app_name: 'Spot Micro Controller', app_name: 'Spot Micro Controller',
copyright: '2024 Rune Harlyk' copyright: '2025 Rune Harlyk',
}; };
}; };
+88 -88
View File
@@ -1,51 +1,51 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte' import SettingsCard from '$lib/components/SettingsCard.svelte';
import { imu } from '$lib/stores/imu' import { imu } from '$lib/stores/imu';
import { Chart, registerables } from 'chart.js' import { Chart, registerables } from 'chart.js';
import { cubicOut } from 'svelte/easing' import { cubicOut } from 'svelte/easing';
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition';
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte';
import { socket } from '$lib/stores' import { socket } from '$lib/stores';
import type { IMU } from '$lib/types/models' import type { IMU } from '$lib/types/models';
import { useFeatureFlags } from '$lib/stores/featureFlags' import { useFeatureFlags } from '$lib/stores/featureFlags';
import { Rotate3d } from '$lib/components/icons' import { Rotate3d } from '$lib/components/icons';
Chart.register(...registerables) Chart.register(...registerables);
const features = useFeatureFlags() const features = useFeatureFlags();
let intervalId: number let intervalId: ReturnType<typeof setInterval> | number;
let angleChartElement: HTMLCanvasElement = $state() let angleChartElement: HTMLCanvasElement;
let tempChartElement: HTMLCanvasElement = $state() let tempChartElement: HTMLCanvasElement;
let altitudeChartElement: HTMLCanvasElement = $state() let altitudeChartElement: HTMLCanvasElement;
let angleChart: Chart let angleChart: Chart;
let tempChart: Chart let tempChart: Chart;
let altitudeChart: Chart let altitudeChart: Chart;
const getChartColors = () => { const getChartColors = () => {
const style = getComputedStyle(document.body) const style = getComputedStyle(document.body);
return { return {
primary: style.getPropertyValue('--color-primary'), primary: style.getPropertyValue('--color-primary'),
secondary: style.getPropertyValue('--color-secondary'), secondary: style.getPropertyValue('--color-secondary'),
accent: style.getPropertyValue('--color-accent'), accent: style.getPropertyValue('--color-accent'),
background: style.getPropertyValue('--color-background') background: style.getPropertyValue('--color-background'),
} };
} };
const createBaseChartConfig = (bgColor: string) => ({ const createBaseChartConfig = (bgColor: string) => ({
maintainAspectRatio: false, maintainAspectRatio: false,
responsive: true, responsive: true,
plugins: { plugins: {
legend: { display: true }, legend: { display: true },
tooltip: { mode: 'index', intersect: false } tooltip: { mode: 'index', intersect: false },
}, },
elements: { point: { radius: 1 } }, elements: { point: { radius: 1 } },
scales: { scales: {
x: { x: {
grid: { color: bgColor }, grid: { color: bgColor },
ticks: { color: bgColor }, ticks: { color: bgColor },
display: false display: false,
}, },
y: { y: {
type: 'linear', type: 'linear',
@@ -54,14 +54,14 @@
max: 10, max: 10,
grid: { color: bgColor }, grid: { color: bgColor },
ticks: { color: bgColor }, ticks: { color: bgColor },
border: { color: bgColor } border: { color: bgColor },
} },
} },
}) });
const initializeCharts = () => { const initializeCharts = () => {
const colors = getChartColors() const colors = getChartColors();
const baseConfig = createBaseChartConfig(colors.background) const baseConfig = createBaseChartConfig(colors.background);
angleChart = new Chart(angleChartElement, { angleChart = new Chart(angleChartElement, {
type: 'line', type: 'line',
@@ -73,7 +73,7 @@
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderWidth: 2, borderWidth: 2,
data: $imu.x, data: $imu.x,
yAxisID: 'y' yAxisID: 'y',
}, },
{ {
label: 'y', label: 'y',
@@ -81,7 +81,7 @@
backgroundColor: colors.secondary, backgroundColor: colors.secondary,
borderWidth: 2, borderWidth: 2,
data: $imu.y, data: $imu.y,
yAxisID: 'y' yAxisID: 'y',
}, },
{ {
label: 'z', label: 'z',
@@ -89,9 +89,9 @@
backgroundColor: colors.accent, backgroundColor: colors.accent,
borderWidth: 2, borderWidth: 2,
data: $imu.z, data: $imu.z,
yAxisID: 'y' yAxisID: 'y',
} },
] ],
}, },
options: { options: {
...baseConfig, ...baseConfig,
@@ -103,12 +103,12 @@
display: true, display: true,
text: 'Angle [°]', text: 'Angle [°]',
color: colors.background, color: colors.background,
font: { size: 16, weight: 'bold' } font: { size: 16, weight: 'bold' },
} },
} },
} },
} },
}) });
tempChart = new Chart(tempChartElement, { tempChart = new Chart(tempChartElement, {
type: 'line', type: 'line',
@@ -120,9 +120,9 @@
backgroundColor: colors.secondary, backgroundColor: colors.secondary,
borderWidth: 2, borderWidth: 2,
data: $imu.bmp_temp, data: $imu.bmp_temp,
yAxisID: 'y' yAxisID: 'y',
} },
] ],
}, },
options: { options: {
...baseConfig, ...baseConfig,
@@ -134,12 +134,12 @@
display: true, display: true,
text: 'Temperature [C°]', text: 'Temperature [C°]',
color: colors.background, color: colors.background,
font: { size: 16, weight: 'bold' } font: { size: 16, weight: 'bold' },
} },
} },
} },
} },
}) });
altitudeChart = new Chart(altitudeChartElement, { altitudeChart = new Chart(altitudeChartElement, {
type: 'line', type: 'line',
@@ -151,9 +151,9 @@
backgroundColor: colors.primary, backgroundColor: colors.primary,
borderWidth: 2, borderWidth: 2,
data: $imu.altitude, data: $imu.altitude,
yAxisID: 'y' yAxisID: 'y',
} },
] ],
}, },
options: { options: {
...baseConfig, ...baseConfig,
@@ -165,60 +165,60 @@
display: true, display: true,
text: 'Altitude [M]', text: 'Altitude [M]',
color: colors.background, color: colors.background,
font: { size: 16, weight: 'bold' } font: { size: 16, weight: 'bold' },
} },
} },
} },
} },
}) });
} };
const updateChartData = (chart: Chart, data: number[], label: string) => { const updateChartData = (chart: Chart, data: number[], label: string) => {
chart.data.labels = data chart.data.labels = data;
chart.data.datasets[0].data = data chart.data.datasets[0].data = data;
chart.options.scales!.y!.min = Math.min(...data) - 1 chart.options.scales!.y!.min = Math.min(...data) - 1;
chart.options.scales!.y!.max = Math.max(...data) + 1 chart.options.scales!.y!.max = Math.max(...data) + 1;
chart.update('none') chart.update('none');
} };
const updateData = () => { const updateData = () => {
if ($features.imu) { if ($features.imu) {
angleChart.data.labels = $imu.x angleChart.data.labels = $imu.x;
angleChart.data.datasets[0].data = $imu.x angleChart.data.datasets[0].data = $imu.x;
angleChart.data.datasets[1].data = $imu.y angleChart.data.datasets[1].data = $imu.y;
angleChart.data.datasets[2].data = $imu.z angleChart.data.datasets[2].data = $imu.z;
const allValues = [...$imu.x, ...$imu.y, ...$imu.z] const allValues = [...$imu.x, ...$imu.y, ...$imu.z];
angleChart.options.scales!.y!.min = Math.min(...allValues) - 1 angleChart.options.scales!.y!.min = Math.min(...allValues) - 1;
angleChart.options.scales!.y!.max = Math.max(...allValues) + 1 angleChart.options.scales!.y!.max = Math.max(...allValues) + 1;
angleChart.update('none') angleChart.update('none');
} }
if ($features.bmp) { if ($features.bmp) {
updateChartData(tempChart, $imu.bmp_temp, 'Temperature') updateChartData(tempChart, $imu.bmp_temp, 'Temperature');
updateChartData(altitudeChart, $imu.altitude, 'Altitude') updateChartData(altitudeChart, $imu.altitude, 'Altitude');
}
} }
};
onMount(() => { onMount(() => {
socket.on('imu', (data: IMU) => { socket.on('imu', (data: IMU) => {
console.log(data) console.log(data);
imu.addData(data) imu.addData(data);
}) });
initializeCharts() initializeCharts();
intervalId = setInterval(updateData, 200) intervalId = setInterval(updateData, 200);
}) });
onDestroy(() => { onDestroy(() => {
socket.off('imu') socket.off('imu');
clearInterval(intervalId) clearInterval(intervalId);
}) });
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
{#snippet icon()} {#snippet icon()}
<Rotate3d class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> <Rotate3d class="flex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<span>IMU</span> <span>IMU</span>
@@ -1,13 +1,5 @@
<script lang="ts"> <script lang="ts">
import SystemMetrics from './SystemMetrics.svelte'; import SystemMetrics from './SystemMetrics.svelte';
import { goto } from '$app/navigation';
import { useFeatureFlags } from '$lib/stores/featureFlags';
const features = useFeatureFlags();
if (!$features.analytics) {
goto('/');
}
</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">
@@ -12,16 +12,16 @@
Chart.register(...registerables); Chart.register(...registerables);
let cpuChartElement: HTMLCanvasElement = $state(); let cpuChartElement: HTMLCanvasElement;
let cpuChart: Chart; let cpuChart: Chart;
let heapChartElement: HTMLCanvasElement = $state(); let heapChartElement: HTMLCanvasElement;
let heapChart: Chart; let heapChart: Chart;
let filesystemChartElement: HTMLCanvasElement = $state(); let filesystemChartElement: HTMLCanvasElement;
let filesystemChart: Chart; let filesystemChart: Chart;
let temperatureChartElement: HTMLCanvasElement = $state(); let temperatureChartElement: HTMLCanvasElement;
let temperatureChart: Chart; let temperatureChart: Chart;
onMount(() => { onMount(() => {
@@ -36,7 +36,7 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data: $analytics.cpu0_usage, data: $analytics.cpu0_usage,
yAxisID: 'y' yAxisID: 'y',
}, },
{ {
label: 'Cpu usage core 1', label: 'Cpu usage core 1',
@@ -44,7 +44,7 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data: $analytics.cpu1_usage, data: $analytics.cpu1_usage,
yAxisID: 'y' yAxisID: 'y',
}, },
{ {
label: 'Cpu usage total', label: 'Cpu usage total',
@@ -52,36 +52,36 @@
backgroundColor: daisyColor('--s', 50), backgroundColor: daisyColor('--s', 50),
borderWidth: 2, borderWidth: 2,
data: $analytics.cpu_usage, data: $analytics.cpu_usage,
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',
@@ -91,20 +91,20 @@
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) }
}
}
}
}); });
heapChart = new Chart(heapChartElement, { heapChart = new Chart(heapChartElement, {
type: 'line', type: 'line',
@@ -117,37 +117,37 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data: $analytics.used_heap, data: $analytics.used_heap,
fill:true, fill: true,
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',
@@ -157,20 +157,20 @@
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: Math.round($analytics.total_heap[0]), max: Math.round($analytics.total_heap[0]),
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) }
}
}
}
}); });
filesystemChart = new Chart(filesystemChartElement, { filesystemChart = new Chart(filesystemChartElement, {
type: 'line', type: 'line',
@@ -183,37 +183,37 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data: $analytics.fs_used, data: $analytics.fs_used,
fill:true, fill: true,
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',
@@ -223,20 +223,20 @@
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: Math.round($analytics.fs_total[0]), max: Math.round($analytics.fs_total[0]),
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) }
}
}
}
}); });
temperatureChart = new Chart(temperatureChartElement, { temperatureChart = new Chart(temperatureChartElement, {
type: 'line', type: 'line',
@@ -249,36 +249,36 @@
backgroundColor: daisyColor('--p', 50), backgroundColor: daisyColor('--p', 50),
borderWidth: 2, borderWidth: 2,
data: $analytics.core_temp, data: $analytics.core_temp,
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',
@@ -288,20 +288,20 @@
color: daisyColor('--bc'), color: daisyColor('--bc'),
font: { font: {
size: 16, size: 16,
weight: 'bold' weight: 'bold',
} },
}, },
position: 'left', position: 'left',
suggestedMin: 20, suggestedMin: 20,
suggestedMax: 100, suggestedMax: 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(updateData, 500); setInterval(updateData, 500);
}); });
@@ -334,14 +334,13 @@
<Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> <Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet} {/snippet}
{#snippet title()} {#snippet title()}
<span >System Metrics</span> <span>System Metrics</span>
{/snippet} {/snippet}
<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={cpuChartElement}></canvas> <canvas bind:this={cpuChartElement}></canvas>
</div> </div>
</div> </div>
@@ -349,24 +348,21 @@
<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>
@@ -1,16 +1,16 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte' import { onDestroy, onMount } from 'svelte';
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 Spinner from '$lib/components/Spinner.svelte' import Spinner from '$lib/components/Spinner.svelte';
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing' import { cubicOut } from 'svelte/easing';
import type { SystemInformation, Analytics } from '$lib/types/models' import type { SystemInformation, Analytics } from '$lib/types/models';
import { socket } from '$lib/stores/socket' import { socket } from '$lib/stores/socket';
import { api } from '$lib/api' import { api } from '$lib/api';
import { convertSeconds } from '$lib/utilities' import { convertSeconds } from '$lib/utilities';
import { useFeatureFlags } from '$lib/stores/featureFlags' import { useFeatureFlags } from '$lib/stores/featureFlags';
import { import {
Cancel, Cancel,
Power, Power,
@@ -27,37 +27,42 @@
Flash, Flash,
Folder, Folder,
Temperature, Temperature,
Stopwatch Stopwatch,
} from '$lib/components/icons' } from '$lib/components/icons';
import StatusItem from '$lib/components/StatusItem.svelte' import StatusItem from '$lib/components/StatusItem.svelte';
import ActionButton from './ActionButton.svelte' import ActionButton from './ActionButton.svelte';
const features = useFeatureFlags() const features = useFeatureFlags();
let systemInformation: SystemInformation = $state() let systemInformation: SystemInformation | null = $state(null);
async function getSystemStatus() { async function getSystemStatus() {
const result = await api.get<SystemInformation>('/api/system/status') const result = await api.get<SystemInformation>('/api/system/status');
if (result.isErr()) { if (result.isErr()) {
console.error('Error:', result.inner) console.error('Error:', result.inner);
return return;
} }
systemInformation = result.inner systemInformation = result.inner;
return systemInformation return systemInformation;
} }
const postFactoryReset = async () => await api.post('/api/system/reset') const postFactoryReset = async () => await api.post('/api/system/reset');
const postSleep = async () => await api.post('api/sleep') const postSleep = async () => await api.post('api/sleep');
onMount(() => socket.on('analytics', handleSystemData)) onMount(() => socket.on('analytics', handleSystemData));
onDestroy(() => socket.off('analytics', handleSystemData)) onDestroy(() => socket.off('analytics', handleSystemData));
const handleSystemData = (data: Analytics) => {
if (systemInformation) {
systemInformation = {
...systemInformation,
...(data as unknown as SystemInformation),
};
}
};
const handleSystemData = (data: Analytics) => const postRestart = async () => await api.post('/api/system/restart');
(systemInformation = { ...systemInformation, ...data })
const postRestart = async () => await api.post('/api/system/restart')
function confirmRestart() { function confirmRestart() {
modals.open(ConfirmDialog, { modals.open(ConfirmDialog, {
@@ -65,13 +70,13 @@
message: 'Are you sure you want to restart the device?', message: 'Are you sure you want to restart the device?',
labels: { labels: {
cancel: { label: 'Abort', icon: Cancel }, cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Restart', icon: Power } confirm: { label: 'Restart', icon: Power },
}, },
onConfirm: () => { onConfirm: () => {
modals.close() modals.close();
postRestart() postRestart();
} },
}) });
} }
function confirmReset() { function confirmReset() {
@@ -80,13 +85,13 @@
message: 'Are you sure you want to reset the device to its factory defaults?', message: 'Are you sure you want to reset the device to its factory defaults?',
labels: { labels: {
cancel: { label: 'Abort', icon: Cancel }, cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Factory Reset', icon: FactoryReset } confirm: { label: 'Factory Reset', icon: FactoryReset },
}, },
onConfirm: () => { onConfirm: () => {
modals.close() modals.close();
postFactoryReset() postFactoryReset();
} },
}) });
} }
function confirmSleep() { function confirmSleep() {
@@ -95,21 +100,21 @@
message: 'Are you sure you want to put the device into sleep?', message: 'Are you sure you want to put the device into sleep?',
labels: { labels: {
cancel: { label: 'Abort', icon: Cancel }, cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Sleep', icon: Sleep } confirm: { label: 'Sleep', icon: Sleep },
}, },
onConfirm: () => { onConfirm: () => {
modals.close() modals.close();
postSleep() postSleep();
} },
}) });
} }
interface ActionButtonDef { interface ActionButtonDef {
icon: any icon: any;
label: string label: string;
onClick: () => void onClick: () => void;
type?: string type?: string;
condition?: () => boolean condition?: () => boolean;
} }
const actionButtons: ActionButtonDef[] = [ const actionButtons: ActionButtonDef[] = [
@@ -117,20 +122,20 @@
icon: Sleep, icon: Sleep,
label: 'Sleep', label: 'Sleep',
onClick: confirmSleep, onClick: confirmSleep,
condition: () => Boolean($features.sleep) condition: () => Boolean($features.sleep),
}, },
{ {
icon: Power, icon: Power,
label: 'Restart', label: 'Restart',
onClick: confirmRestart onClick: confirmRestart,
}, },
{ {
icon: FactoryReset, icon: FactoryReset,
label: 'Factory Reset', label: 'Factory Reset',
onClick: confirmReset, onClick: confirmReset,
type: 'secondary' type: 'secondary',
} },
] ];
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -144,7 +149,8 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getSystemStatus()} {#await getSystemStatus()}
<Spinner /> <Spinner />
{:then nothing} {:then}
{#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 }}>
@@ -201,9 +207,10 @@
<StatusItem <StatusItem
icon={Folder} icon={Folder}
title="File System (Used / Total)" title="File System (Used / Total)"
description={`${((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed( description={`${(
1 (systemInformation.fs_used / systemInformation.fs_total) *
)} % of ${systemInformation.fs_total / 1000000} MB used (${ 100
).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)`} />
@@ -227,6 +234,7 @@
title="Reset Reason" title="Reset Reason"
description={systemInformation.cpu_reset_reason} /> description={systemInformation.cpu_reset_reason} />
</div> </div>
{/if}
{/await} {/await}
</div> </div>
@@ -19,10 +19,10 @@
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`, { const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
headers headers,
}); });
if (result.isErr()) { if (result.isErr()) {
console.error('Error:', result.inner); console.error('Error:', result.inner);
@@ -58,7 +58,7 @@
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;
} }
@@ -68,14 +68,14 @@
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,10 +91,7 @@
<Spinner /> <Spinner />
{:then githubReleases} {:then githubReleases}
<div class="relative w-full overflow-visible"> <div class="relative w-full overflow-visible">
<div <div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}>
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">
@@ -108,26 +105,21 @@
{#each githubReleases as release} {#each githubReleases as release}
<tr <tr
class={( class={(
compareVersions( compareVersions($features.firmware_version as string, release.tag_name) === 0
$features.firmware_version,
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>
@@ -137,13 +129,12 @@
{/if} {/if}
</td> </td>
<td align="center"> <td align="center">
{#if compareVersions($features.firmware_version, release.tag_name) != 0} {#if compareVersions($features.firmware_version as string, release.tag_name) != 0}
<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}
@@ -157,9 +148,7 @@
{: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 <span>Please connect to a network with internet access to perform a firmware update.</span>
>Please connect to a network with internet access to perform a firmware update.</span
>
</div> </div>
{/await} {/await}
</SettingsCard> </SettingsCard>
@@ -6,11 +6,11 @@
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 = $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);
} }
@@ -21,12 +21,12 @@
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 >Uploading a new firmware (.bin) file will replace the existing firmware. You may upload a
a (.md5) file first to verify the uploaded firmware. (.md5) file first to verify the uploaded firmware.
</span> </span>
</div> </div>
@@ -52,6 +52,5 @@
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>
+77 -76
View File
@@ -1,74 +1,71 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy' import { preventDefault } from 'svelte/legacy';
import { onMount, onDestroy } from 'svelte' import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition' import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing' import { cubicOut } from 'svelte/easing';
import { PasswordInput } from '$lib/components/input' import { PasswordInput } from '$lib/components/input';
import SettingsCard from '$lib/components/SettingsCard.svelte' import SettingsCard from '$lib/components/SettingsCard.svelte';
import { notifications } from '$lib/components/toasts/notifications' import { notifications } from '$lib/components/toasts/notifications';
import Spinner from '$lib/components/Spinner.svelte' import Spinner from '$lib/components/Spinner.svelte';
import type { ApSettings, ApStatus } from '$lib/types/models' import type { ApSettings, ApStatus } from '$lib/types/models';
import { api } from '$lib/api' import { api } from '$lib/api';
import { useFeatureFlags } from '$lib/stores' import { AP, Devices, Home, MAC } from '$lib/components/icons';
import { AP, Devices, Home, MAC } from '$lib/components/icons' import StatusItem from '$lib/components/StatusItem.svelte';
import StatusItem from '$lib/components/StatusItem.svelte'
const features = useFeatureFlags() let apSettings: ApSettings | null = $state(null);
let apStatus: ApStatus | null = $state(null);
let apSettings: ApSettings = $state() let formField: any = $state();
let apStatus: ApStatus = $state()
let formField: any = $state()
async function getAPStatus() { async function getAPStatus() {
const result = await api.get<ApStatus>('/api/wifi/ap/status') const result = await api.get<ApStatus>('/api/wifi/ap/status');
if (result.isErr()) { if (result.isErr()) {
console.error('Error:', result.inner) console.error('Error:', result.inner);
return return;
} }
apStatus = result.inner apStatus = result.inner;
return apStatus return apStatus;
} }
async function getAPSettings() { async function getAPSettings() {
const result = await api.get<ApSettings>('/api/wifi/ap/settings') const result = await api.get<ApSettings>('/api/wifi/ap/settings');
if (result.isErr()) { if (result.isErr()) {
console.error('Error:', result.inner) console.error('Error:', result.inner);
return return;
} }
apSettings = result.inner apSettings = result.inner;
return apSettings return apSettings;
} }
const interval = setInterval(async () => { const interval = setInterval(async () => {
getAPStatus() getAPStatus();
}, 5000) }, 5000);
onDestroy(() => clearInterval(interval)) onDestroy(() => clearInterval(interval));
onMount(getAPSettings) onMount(getAPSettings);
let provisionMode = [ let provisionMode = [
{ {
id: 0, id: 0,
text: `Always` text: `Always`,
}, },
{ {
id: 1, id: 1,
text: `When WiFi Disconnected` text: `When WiFi Disconnected`,
}, },
{ {
id: 2, id: 2,
text: `Never` text: `Never`,
} },
] ];
type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning' type Variant = 'success' | 'error' | 'primary' | 'info' | 'warning';
let apStatusVariant: Variant[] = ['success', 'error', 'warning'] let apStatusVariant: Variant[] = ['success', 'error', 'warning'];
let apStatusDescription = ['Active', 'Inactive', 'Lingering'] let apStatusDescription = ['Active', 'Inactive', 'Lingering'];
let formErrors = $state({ let formErrors = $state({
ssid: false, ssid: false,
@@ -76,80 +73,81 @@
max_clients: false, max_clients: false,
local_ip: false, local_ip: false,
gateway_ip: false, gateway_ip: false,
subnet_mask: false subnet_mask: false,
}) });
async function postAPSettings(data: ApSettings) { async function postAPSettings(data: ApSettings) {
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data) const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
if (result.isErr()) { if (result.isErr()) {
notifications.error('User not authorized.', 3000) notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner) console.error('Error:', result.inner);
return return;
} }
notifications.success('Access Point settings updated.', 3000) notifications.success('Access Point settings updated.', 3000);
apSettings = result.inner apSettings = result.inner;
} }
function handleSubmitAP() { function handleSubmitAP() {
let valid = true if (!apSettings) return;
let valid = true;
// Validate SSID // Validate SSID
if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) { if (apSettings.ssid.length < 3 || apSettings.ssid.length > 32) {
valid = false valid = false;
formErrors.ssid = true formErrors.ssid = true;
} else { } else {
formErrors.ssid = false formErrors.ssid = false;
} }
// Validate Channel // Validate Channel
let channel = Number(apSettings.channel) let channel = Number(apSettings.channel);
if (1 > channel || channel > 13) { if (1 > channel || channel > 13) {
valid = false valid = false;
formErrors.channel = true formErrors.channel = true;
} else { } else {
formErrors.channel = false formErrors.channel = false;
} }
// Validate max_clients // Validate max_clients
let maxClients = Number(apSettings.max_clients) let maxClients = Number(apSettings.max_clients);
if (1 > maxClients || maxClients > 8) { if (1 > maxClients || maxClients > 8) {
valid = false valid = false;
formErrors.max_clients = true formErrors.max_clients = true;
} else { } else {
formErrors.max_clients = false formErrors.max_clients = false;
} }
// RegEx for IPv4 // RegEx for IPv4
const regexExp = const regexExp =
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/ /\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
// Validate gateway IP // Validate gateway IP
if (!regexExp.test(apSettings.gateway_ip)) { if (!regexExp.test(apSettings.gateway_ip)) {
valid = false valid = false;
formErrors.gateway_ip = true formErrors.gateway_ip = true;
} else { } else {
formErrors.gateway_ip = false formErrors.gateway_ip = false;
} }
// Validate Subnet Mask // Validate Subnet Mask
if (!regexExp.test(apSettings.subnet_mask)) { if (!regexExp.test(apSettings.subnet_mask)) {
valid = false valid = false;
formErrors.subnet_mask = true formErrors.subnet_mask = true;
} else { } else {
formErrors.subnet_mask = false formErrors.subnet_mask = false;
} }
// Validate local IP // Validate local IP
if (!regexExp.test(apSettings.local_ip)) { if (!regexExp.test(apSettings.local_ip)) {
valid = false valid = false;
formErrors.local_ip = true formErrors.local_ip = true;
} else { } else {
formErrors.local_ip = false formErrors.local_ip = false;
} }
// Submit JSON to REST API // Submit JSON to REST API
if (valid) { if (valid) {
postAPSettings(apSettings) postAPSettings(apSettings);
} }
} }
</script> </script>
@@ -164,7 +162,8 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getAPStatus()} {#await getAPStatus()}
<Spinner /> <Spinner />
{:then nothing} {:then}
{#if apStatus}
<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 }}>
@@ -180,6 +179,7 @@
<StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} /> <StatusItem icon={Devices} title="AP Clients" description={apStatus.station_num} />
</div> </div>
{/if}
{/await} {/await}
</div> </div>
@@ -190,7 +190,8 @@
</div> </div>
{#await getAPSettings()} {#await getAPSettings()}
<Spinner /> <Spinner />
{:then nothing} {:then}
{#if apSettings}
<div <div
class="flex flex-col gap-2 p-0" class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }}> transition:slide|local={{ duration: 300, easing: cubicOut }}>
@@ -292,9 +293,8 @@
</label> </label>
<input <input
type="text" type="text"
class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2' : ( class="input input-bordered w-full {formErrors.local_ip ? 'border-error border-2'
'' : ''}"
)}"
minlength="7" minlength="7"
maxlength="15" maxlength="15"
size="15" size="15"
@@ -359,6 +359,7 @@
</div> </div>
</form> </form>
</div> </div>
{/if}
{/await} {/await}
</div> </div>
</SettingsCard> </SettingsCard>
+12 -28
View File
@@ -6,15 +6,9 @@
import type { NetworkItem, NetworkList } from '$lib/types/models'; import type { NetworkItem, NetworkList } from '$lib/types/models';
import { api } from '$lib/api'; import { api } from '$lib/api';
import { AP, Network, Reload, Cancel } from '$lib/components/icons'; import { AP, Network, Reload, Cancel } from '$lib/components/icons';
import { modals, exitBeforeEnter } from 'svelte-modals'; import { modals, exitBeforeEnter, type ModalProps } from 'svelte-modals';
// provided by <Modals /> let { isOpen, storeNetwork }: ModalProps = $props();
interface Props {
isOpen: boolean;
storeNetwork: any;
}
let { isOpen, storeNetwork }: Props = $props();
const encryptionType = [ const encryptionType = [
'Open', 'Open',
@@ -25,14 +19,14 @@
'WPA2 Enterprise', 'WPA2 Enterprise',
'WPA3 PSK', 'WPA3 PSK',
'WPA2 WPA3 PSK', 'WPA2 WPA3 PSK',
'WAPI PSK' 'WAPI PSK',
]; ];
let listOfNetworks: NetworkItem[] = $state([]); let listOfNetworks: NetworkItem[] = $state([]);
let scanActive = $state(false); let scanActive = $state(false);
let pollingId: number; let pollingId: ReturnType<typeof setTimeout> | number;
async function scanNetworks() { async function scanNetworks() {
scanActive = true; scanActive = true;
@@ -77,17 +71,13 @@
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="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" class="bg-base-100 rounded-box pointer-events-auto flex max-h-full 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">Scan Networks</h2> <h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
<div class="divider my-2"></div> <div class="divider my-2"></div>
<div class="overflow-y-auto"> <div class="overflow-y-auto">
{#if scanActive}<div {#if scanActive}<div class="bg-base-100 flex flex-col items-center justify-center p-6">
class="bg-base-100 flex flex-col items-center justify-center p-6"
>
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" /> <AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
<p class="mt-8 text-2xl">Scanning ...</p> <p class="mt-8 text-2xl">Scanning ...</p>
</div> </div>
@@ -102,18 +92,14 @@
storeNetwork(network.ssid); storeNetwork(network.ssid);
}} }}
role="button" role="button"
tabindex="0" tabindex="0">
>
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0"> <div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Network <Network class="text-primary-content h-auto w-full scale-75" />
class="text-primary-content h-auto w-full scale-75"
/>
</div> </div>
<div> <div>
<div class="font-bold">{network.ssid}</div> <div class="font-bold">{network.ssid}</div>
<div class="text-sm opacity-75"> <div class="text-sm opacity-75">
Security: {encryptionType[network.encryption_type]}, Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
Channel: {network.channel}
</div> </div>
</div> </div>
<div class="grow"></div> <div class="grow"></div>
@@ -129,16 +115,14 @@
<button <button
class="btn btn-primary inline-flex flex-none items-center" class="btn btn-primary inline-flex flex-none items-center"
disabled={scanActive} disabled={scanActive}
onclick={scanNetworks} onclick={scanNetworks}>
>
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span> <Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
</button> </button>
<div class="grow"></div> <div class="grow"></div>
<button <button
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"
onclick={() => modals.close()} 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>
</div> </div>
+134 -128
View File
@@ -1,19 +1,19 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte' import { onMount, onDestroy } from 'svelte';
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 { notifications } from '$lib/components/toasts/notifications' import { notifications } from '$lib/components/toasts/notifications';
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list' import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
import SettingsCard from '$lib/components/SettingsCard.svelte' import SettingsCard from '$lib/components/SettingsCard.svelte';
import { PasswordInput } from '$lib/components/input' import { PasswordInput } from '$lib/components/input';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte' import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import ScanNetworks from './Scan.svelte' import ScanNetworks from './Scan.svelte';
import Spinner from '$lib/components/Spinner.svelte' import Spinner from '$lib/components/Spinner.svelte';
import InfoDialog from '$lib/components/InfoDialog.svelte' import InfoDialog from '$lib/components/InfoDialog.svelte';
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models' import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models';
import { socket } from '$lib/stores' import { socket } from '$lib/stores';
import { api } from '$lib/api' import { api } from '$lib/api';
import { import {
Cancel, Cancel,
Delete, Delete,
@@ -31,9 +31,9 @@
DNS, DNS,
Add, Add,
Scan, Scan,
Edit Edit,
} from '$lib/components/icons' } from '$lib/components/icons';
import StatusItem from '$lib/components/StatusItem.svelte' import StatusItem from '$lib/components/StatusItem.svelte';
let networkEditable: KnownNetworkItem = $state({ let networkEditable: KnownNetworkItem = $state({
ssid: '', ssid: '',
@@ -43,22 +43,22 @@
subnet_mask: undefined, subnet_mask: undefined,
gateway_ip: undefined, gateway_ip: undefined,
dns_ip_1: undefined, dns_ip_1: undefined,
dns_ip_2: undefined dns_ip_2: undefined,
}) });
let static_ip_config = $state(false) let static_ip_config = $state(false);
let newNetwork: boolean = $state(true) let newNetwork: boolean = $state(true);
let showNetworkEditor: boolean = $state(false) let showNetworkEditor: boolean = $state(false);
let wifiStatus: WifiStatus = $state() let wifiStatus: WifiStatus | null = $state(null);
let wifiSettings: WifiSettings = $state() let wifiSettings: WifiSettings | null = $state(null);
let dndNetworkList: KnownNetworkItem[] = $state([]) let dndNetworkList: KnownNetworkItem[] = $state([]);
let showWifiDetails = $state(false) let showWifiDetails = $state(false);
let formField: any = $state() let formField: any = $state();
let formErrors = $state({ let formErrors = $state({
ssid: false, ssid: false,
@@ -66,156 +66,157 @@
gateway_ip: false, gateway_ip: false,
subnet_mask: false, subnet_mask: false,
dns_1: false, dns_1: false,
dns_2: false dns_2: false,
}) });
let formErrorhostname = $state(false) let formErrorhostname = $state(false);
async function getWifiStatus() { async function getWifiStatus() {
const result = await api.get<WifiStatus>('/api/wifi/sta/status') const result = await api.get<WifiStatus>('/api/wifi/sta/status');
if (result.isErr()) { if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner) console.error(`Error occurred while fetching: `, result.inner);
return return;
} }
wifiStatus = result.inner wifiStatus = result.inner;
return wifiStatus return wifiStatus;
} }
async function getWifiSettings() { async function getWifiSettings() {
const result = await api.get<WifiSettings>('/api/wifi/sta/settings') const result = await api.get<WifiSettings>('/api/wifi/sta/settings');
if (result.isErr()) { if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner) console.error(`Error occurred while fetching: `, result.inner);
return return;
} }
wifiSettings = result.inner wifiSettings = result.inner;
dndNetworkList = wifiSettings.wifi_networks dndNetworkList = wifiSettings.wifi_networks;
return wifiSettings return wifiSettings;
} }
onDestroy(() => socket.off('WiFiSettings')) onDestroy(() => socket.off('WiFiSettings'));
onMount(() => { onMount(() => {
socket.on<WifiSettings>('WiFiSettings', data => { socket.on<WifiSettings>('WiFiSettings', data => {
wifiSettings = data wifiSettings = data;
dndNetworkList = wifiSettings.wifi_networks dndNetworkList = wifiSettings.wifi_networks;
}) });
}) });
async function postWiFiSettings(data: WifiSettings) { async function postWiFiSettings(data: WifiSettings) {
const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data) const result = await api.post<WifiSettings>('/api/wifi/sta/settings', data);
if (result.isErr()) { if (result.isErr()) {
console.error(`Error occurred while fetching: `, result.inner) console.error(`Error occurred while fetching: `, result.inner);
notifications.error('User not authorized.', 3000) notifications.error('User not authorized.', 3000);
return return;
} }
wifiSettings = result.inner wifiSettings = result.inner;
notifications.success('Wi-Fi settings updated.', 3000) notifications.success('Wi-Fi settings updated.', 3000);
} }
function validateHostName() { function validateHostName() {
if (!wifiSettings) return false;
if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) { if (wifiSettings.hostname.length < 3 || wifiSettings.hostname.length > 32) {
formErrorhostname = true formErrorhostname = true;
} else { } else {
formErrorhostname = false formErrorhostname = false;
// Update global wifiSettings object // Update global wifiSettings object
wifiSettings.wifi_networks = dndNetworkList wifiSettings.wifi_networks = dndNetworkList;
// Post to REST API // Post to REST API
postWiFiSettings(wifiSettings) postWiFiSettings(wifiSettings);
console.log(wifiSettings) console.log(wifiSettings);
} }
} }
function validateWiFiForm(event: SubmitEvent) { function validateWiFiForm(event: SubmitEvent) {
event.preventDefault() event.preventDefault();
let valid = true let valid = true;
// Validate SSID // Validate SSID
if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) { if (networkEditable.ssid.length < 3 || networkEditable.ssid.length > 32) {
valid = false valid = false;
formErrors.ssid = true formErrors.ssid = true;
} else { } else {
formErrors.ssid = false formErrors.ssid = false;
} }
networkEditable.static_ip_config = static_ip_config networkEditable.static_ip_config = static_ip_config;
if (networkEditable.static_ip_config) { if (networkEditable.static_ip_config) {
// RegEx for IPv4 // RegEx for IPv4
const regexExp = const regexExp =
/\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/ /\b(?:(?:2(?:[0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9])\.){3}(?:(?:2([0-4][0-9]|5[0-5])|[0-1]?[0-9]?[0-9]))\b/;
// Validate gateway IP // Validate gateway IP
if (!regexExp.test(networkEditable.gateway_ip!)) { if (!regexExp.test(networkEditable.gateway_ip!)) {
valid = false valid = false;
formErrors.gateway_ip = true formErrors.gateway_ip = true;
} else { } else {
formErrors.gateway_ip = false formErrors.gateway_ip = false;
} }
// Validate Subnet Mask // Validate Subnet Mask
if (!regexExp.test(networkEditable.subnet_mask!)) { if (!regexExp.test(networkEditable.subnet_mask!)) {
valid = false valid = false;
formErrors.subnet_mask = true formErrors.subnet_mask = true;
} else { } else {
formErrors.subnet_mask = false formErrors.subnet_mask = false;
} }
// Validate local IP // Validate local IP
if (!regexExp.test(networkEditable.local_ip!)) { if (!regexExp.test(networkEditable.local_ip!)) {
valid = false valid = false;
formErrors.local_ip = true formErrors.local_ip = true;
} else { } else {
formErrors.local_ip = false formErrors.local_ip = false;
} }
// Validate DNS 1 // Validate DNS 1
if (!regexExp.test(networkEditable.dns_ip_1!)) { if (!regexExp.test(networkEditable.dns_ip_1!)) {
valid = false valid = false;
formErrors.dns_1 = true formErrors.dns_1 = true;
} else { } else {
formErrors.dns_1 = false formErrors.dns_1 = false;
} }
// Validate DNS 2 // Validate DNS 2
if (!regexExp.test(networkEditable.dns_ip_2!)) { if (!regexExp.test(networkEditable.dns_ip_2!)) {
valid = false valid = false;
formErrors.dns_2 = true formErrors.dns_2 = true;
} else { } else {
formErrors.dns_2 = false formErrors.dns_2 = false;
} }
} else { } else {
formErrors.local_ip = false formErrors.local_ip = false;
formErrors.subnet_mask = false formErrors.subnet_mask = false;
formErrors.gateway_ip = false formErrors.gateway_ip = false;
formErrors.dns_1 = false formErrors.dns_1 = false;
formErrors.dns_2 = false formErrors.dns_2 = false;
} }
// Submit JSON to REST API // Submit JSON to REST API
if (valid) { if (valid) {
if (newNetwork) { if (newNetwork) {
dndNetworkList.push(networkEditable) dndNetworkList.push(networkEditable);
} else { } else {
dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable) dndNetworkList.splice(dndNetworkList.indexOf(networkEditable), 1, networkEditable);
} }
addNetwork() addNetwork();
dndNetworkList = [...dndNetworkList] //Trigger reactivity dndNetworkList = [...dndNetworkList]; //Trigger reactivity
showNetworkEditor = false showNetworkEditor = false;
} }
} }
function scanForNetworks() { function scanForNetworks() {
modals.open(ScanNetworks, { modals.open(ScanNetworks, {
storeNetwork: (network: string) => { storeNetwork: (network: string) => {
addNetwork() addNetwork();
networkEditable.ssid = network networkEditable.ssid = network;
showNetworkEditor = true showNetworkEditor = true;
modals.close() modals.close();
} },
}) });
} }
function addNetwork() { function addNetwork() {
newNetwork = true newNetwork = true;
networkEditable = { networkEditable = {
ssid: '', ssid: '',
password: '', password: '',
@@ -224,14 +225,14 @@
subnet_mask: undefined, subnet_mask: undefined,
gateway_ip: undefined, gateway_ip: undefined,
dns_ip_1: undefined, dns_ip_1: undefined,
dns_ip_2: undefined dns_ip_2: undefined,
} };
} }
function handleEdit(index: number) { function handleEdit(index: number) {
newNetwork = false newNetwork = false;
showNetworkEditor = true showNetworkEditor = true;
networkEditable = dndNetworkList[index] networkEditable = dndNetworkList[index];
} }
function confirmDelete(index: number) { function confirmDelete(index: number) {
@@ -240,20 +241,20 @@
message: 'Are you sure you want to delete this network?', message: 'Are you sure you want to delete this network?',
labels: { labels: {
cancel: { label: 'Cancel', icon: Cancel }, cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'Delete', icon: Delete } confirm: { label: 'Delete', icon: Delete },
}, },
onConfirm: () => { onConfirm: () => {
// Check if network is currently been edited and delete as well // Check if network is currently been edited and delete as well
if (dndNetworkList[index].ssid === networkEditable.ssid) { if (dndNetworkList[index].ssid === networkEditable.ssid) {
addNetwork() addNetwork();
} }
// Remove network from array // Remove network from array
dndNetworkList.splice(index, 1) dndNetworkList.splice(index, 1);
dndNetworkList = [...dndNetworkList] //Trigger reactivity dndNetworkList = [...dndNetworkList]; //Trigger reactivity
showNetworkEditor = false showNetworkEditor = false;
modals.close() modals.close();
} },
}) });
} }
function checkNetworkList() { function checkNetworkList() {
@@ -263,21 +264,21 @@
message: message:
'You have reached the maximum number of networks. Please delete one to add another.', 'You have reached the maximum number of networks. Please delete one to add another.',
dismiss: { label: 'OK', icon: Check }, dismiss: { label: 'OK', icon: Check },
onDismiss: () => modals.close() onDismiss: () => modals.close(),
}) });
return false return false;
} else { } else {
return true return true;
} }
} }
function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) { function onDrop({ detail: { from, to } }: CustomEvent<DropEvent>) {
if (!to || from === to) { if (!to || from === to) {
return return;
} }
dndNetworkList = reorder(dndNetworkList, from.index, to.index) dndNetworkList = reorder(dndNetworkList, from.index, to.index);
console.log(dndNetworkList) console.log(dndNetworkList);
} }
</script> </script>
@@ -291,7 +292,8 @@
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getWifiStatus()} {#await getWifiStatus()}
<Spinner /> <Spinner />
{:then nothing} {:then}
{#if wifiStatus}
<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 }}>
@@ -310,7 +312,7 @@
<button <button
class="btn btn-circle btn-ghost btn-sm modal-button" class="btn btn-circle btn-ghost btn-sm modal-button"
onclick={() => { onclick={() => {
showWifiDetails = !showWifiDetails showWifiDetails = !showWifiDetails;
}}> }}>
<Down <Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {( class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
@@ -339,6 +341,7 @@
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} /> <StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
</div> </div>
{/if} {/if}
{/if}
{/await} {/await}
</div> </div>
@@ -349,14 +352,15 @@
</div> </div>
{#await getWifiSettings()} {#await getWifiSettings()}
<Spinner /> <Spinner />
{:then nothing} {:then}
{#if wifiSettings}
<div class="relative w-full overflow-visible"> <div class="relative w-full overflow-visible">
<button <button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
onclick={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
addNetwork() addNetwork();
showNetworkEditor = true showNetworkEditor = true;
} }
}}> }}>
<Add class="h-6 w-6" /></button> <Add class="h-6 w-6" /></button>
@@ -364,8 +368,8 @@
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
onclick={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
scanForNetworks() scanForNetworks();
showNetworkEditor = true showNetworkEditor = true;
} }
}}> }}>
<Scan class="h-6 w-6" /></button> <Scan class="h-6 w-6" /></button>
@@ -379,19 +383,19 @@
itemSize={60} itemSize={60}
itemCount={dndNetworkList.length} itemCount={dndNetworkList.length}
on:drop={onDrop}> on:drop={onDrop}>
{#snippet children({ index })} {#snippet children({ index }: { index: number })}
<StatusItem icon={Router} title={dndNetworkList[index].ssid}> <StatusItem icon={Router} title={dndNetworkList[index].ssid}>
<div class="space-x-0 px-0 mx-0"> <div class="space-x-0 px-0 mx-0">
<button <button
class="btn btn-ghost btn-sm" class="btn btn-ghost btn-sm"
onclick={() => { onclick={() => {
handleEdit(index) handleEdit(index);
}}> }}>
<Edit class="h-6 w-6" /></button> <Edit class="h-6 w-6" /></button>
<button <button
class="btn btn-ghost btn-sm" class="btn btn-ghost btn-sm"
onclick={() => { onclick={() => {
confirmDelete(index) confirmDelete(index);
}}> }}>
<Delete class="text-error h-6 w-6" /> <Delete class="text-error h-6 w-6" />
</button> </button>
@@ -519,7 +523,8 @@
bind:value={networkEditable.gateway_ip} bind:value={networkEditable.gateway_ip}
required /> required />
<label class="label" for="gateway"> <label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}" <span
class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span> >Must be a valid IPv4 address</span>
</label> </label>
</div> </div>
@@ -597,6 +602,7 @@
</div> </div>
</form> </form>
</div> </div>
{/if}
{/await} {/await}
</div> </div>
</SettingsCard> </SettingsCard>