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
+32 -43
View File
@@ -1,51 +1,40 @@
<script lang="ts">
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Check } from './icons';
import { exitBeforeEnter } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition';
import { Check } from './icons';
import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
// provided by <Modals />
interface Props {
isOpen: boolean;
title: string;
message: string;
onDismiss: any;
dismiss?: any;
}
let {
isOpen,
title,
message,
onDismiss,
dismiss = { label: 'Dismiss', icon: Check }
}: Props = $props();
let {
isOpen,
title,
message,
onDismiss,
labels = {
dismiss: { label: 'Dismiss', icon: Check },
},
}: ModalProps = $props();
</script>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap>
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }}
use:exitBeforeEnter
use:focusTrap
>
<div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button
class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}
>
<dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span>
</button>
</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">
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2"></div>
<div class="flex justify-end gap-2">
<button
class="btn btn-warning text-warning-content inline-flex items-center"
onclick={onDismiss}>
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
</button>
</div>
</div>
</div>
{/if}
@@ -1,15 +1,15 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte'
import * as THREE from 'three'
import { imu } from '$lib/stores/imu'
import SceneBuilder from '$lib/sceneBuilder'
import { onMount, onDestroy } from 'svelte';
import * as THREE from 'three';
import { imu } from '$lib/stores/imu';
import SceneBuilder from '$lib/sceneBuilder';
let canvas: HTMLCanvasElement = $state()
let sceneBuilder: SceneBuilder
let cube: THREE.Mesh
let targetRotation = new THREE.Euler()
let lastUpdateTime = 0
const LERP_SPEED = 5 // rotations per second
let canvas: HTMLCanvasElement;
let sceneBuilder: SceneBuilder;
let cube: THREE.Mesh;
let targetRotation = new THREE.Euler();
let lastUpdateTime = 0;
const LERP_SPEED = 5; // rotations per second
const initThreeJS = () => {
sceneBuilder = new SceneBuilder()
@@ -18,59 +18,59 @@
.addOrbitControls(1, 10, false)
.addAmbientLight({ color: 0x404040, intensity: 0.5 })
.addDirectionalLight({ color: 0xffffff, intensity: 3, x: 10, y: 20, z: 7 })
.fillParent()
.fillParent();
const geometry = new THREE.BoxGeometry(1, 1, 1)
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshPhongMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.8
})
cube = new THREE.Mesh(geometry, material)
sceneBuilder.scene.add(cube)
opacity: 0.8,
});
cube = new THREE.Mesh(geometry, material);
sceneBuilder.scene.add(cube);
sceneBuilder.addRenderCb(() => {
if (!cube) return
const currentTime = performance.now()
const deltaTime = (currentTime - lastUpdateTime) / 1000 // convert to seconds
lastUpdateTime = currentTime
if (!cube) return;
const currentTime = performance.now();
const deltaTime = (currentTime - lastUpdateTime) / 1000; // convert to seconds
lastUpdateTime = currentTime;
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime)
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor)
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor)
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor)
})
const lerpFactor = Math.min(1, LERP_SPEED * deltaTime);
cube.rotation.x = THREE.MathUtils.lerp(cube.rotation.x, targetRotation.x, lerpFactor);
cube.rotation.y = THREE.MathUtils.lerp(cube.rotation.y, targetRotation.y, lerpFactor);
cube.rotation.z = THREE.MathUtils.lerp(cube.rotation.z, targetRotation.z, lerpFactor);
});
sceneBuilder.startRenderLoop()
}
sceneBuilder.startRenderLoop();
};
const updateOrientation = () => {
if (!cube) return
if (!cube) return;
const y = -$imu.x[$imu.x.length - 1] || 0
const x = $imu.y[$imu.y.length - 1] || 0
const z = -$imu.z[$imu.z.length - 1] || 0
const y = -$imu.x[$imu.x.length - 1] || 0;
const x = $imu.y[$imu.y.length - 1] || 0;
const z = -$imu.z[$imu.z.length - 1] || 0;
targetRotation.set(
THREE.MathUtils.degToRad(x),
THREE.MathUtils.degToRad(y),
THREE.MathUtils.degToRad(z)
)
}
);
};
onMount(() => {
initThreeJS()
})
initThreeJS();
});
onDestroy(() => {
sceneBuilder?.renderer?.dispose()
})
sceneBuilder?.renderer?.dispose();
});
$effect(() => {
if ($imu) {
updateOrientation()
updateOrientation();
}
})
});
</script>
<div class="h-60 w-60 border-2 border-base-300 rounded-md">
+168 -166
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte'
import { onDestroy, onMount } from 'svelte';
import {
BufferGeometry,
Line,
@@ -10,8 +10,8 @@
SphereGeometry,
Vector3,
type NormalBufferAttributes,
type Object3DEventMap
} from 'three'
type Object3DEventMap,
} from 'three';
import {
ModesEnum,
kinematicData,
@@ -21,56 +21,55 @@
servoAnglesOut,
servoAngles,
mpu,
jointNames
} from '$lib/stores'
jointNames,
} from '$lib/stores';
import {
extractFootColor,
populateModelCache,
throttler,
getToeWorldPositions
} from '$lib/utilities'
import SceneBuilder from '$lib/sceneBuilder'
import { lerp, degToRad } from 'three/src/math/MathUtils'
import { GUI } from 'three/addons/libs/lil-gui.module.min.js'
import Kinematic, { type body_state_t } from '$lib/kinematic'
getToeWorldPositions,
} from '$lib/utilities';
import SceneBuilder from '$lib/sceneBuilder';
import { lerp, degToRad } from 'three/src/math/MathUtils';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
import Kinematic, { type body_state_t } from '$lib/kinematic';
import {
BezierState,
CalibrationState,
EightPhaseWalkState,
FourPhaseWalkState,
IdleState,
RestState,
StandState
} from '$lib/gait'
import { radToDeg } from 'three/src/math/MathUtils.js'
import type { URDFRobot } from 'urdf-loader'
import { get } from 'svelte/store'
StandState,
} from '$lib/gait';
import { radToDeg } from 'three/src/math/MathUtils.js';
import type { URDFRobot } from 'urdf-loader';
import { get } from 'svelte/store';
interface Props {
sky?: boolean
orbit?: boolean
panel?: boolean
debug?: boolean
ground?: boolean
sky?: boolean;
orbit?: boolean;
panel?: boolean;
debug?: 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 canvas: HTMLCanvasElement = $state()
let sceneManager = $state(new SceneBuilder());
let canvas: HTMLCanvasElement;
let currentModelAngles: number[] = new Array(12).fill(0)
let modelTargetAngles: number[] = new Array(12).fill(0)
let gui_panel: GUI
let Throttler = new throttler()
let currentModelAngles: number[] = new Array(12).fill(0);
let modelTargetAngles: number[] = new Array(12).fill(0);
let gui_panel: GUI;
let Throttler = new throttler();
let feet_trace = new Array(4).fill([])
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
let target: Object3D<Object3DEventMap>
let feet_trace = new Array(4).fill([]);
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = [];
let target: Object3D<Object3DEventMap>;
let target_position = { x: 0, z: 0, yaw: 0 }
let target_position = { x: 0, z: 0, yaw: 0 };
let kinematic = new Kinematic()
let kinematic = new Kinematic();
let planners = {
[ModesEnum.Deactivated]: new IdleState(),
@@ -79,11 +78,11 @@
[ModesEnum.Rest]: new RestState(),
[ModesEnum.Stand]: new StandState(),
[ModesEnum.Crawl]: new EightPhaseWalkState(),
[ModesEnum.Walk]: new BezierState()
}
let lastTick = performance.now()
[ModesEnum.Walk]: new BezierState(),
};
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 = {
omega: 0,
@@ -92,8 +91,8 @@
xm: 0,
ym: 0.5,
zm: 0,
feet: kinematic.getDefaultFeetPos()
}
feet: kinematic.getDefaultFeetPos(),
};
let settings = {
'Internal kinematic': true,
@@ -110,52 +109,52 @@
xm: 0,
ym: 0.7,
zm: 0,
Background: 'black'
}
Background: 'black',
};
onMount(async () => {
await populateModelCache()
await createScene()
servoAngles.subscribe(updateAnglesFromStore)
if (panel) createPanel()
})
await populateModelCache();
await createScene();
servoAngles.subscribe(updateAnglesFromStore);
if (panel) createPanel();
});
onDestroy(() => {
canvas.remove()
gui_panel?.destroy()
})
canvas.remove();
gui_panel?.destroy();
});
const updateAnglesFromStore = (angles: number[]) => {
if (sceneManager.isDragging) return
if (settings['Internal kinematic']) return
modelTargetAngles = angles
}
if (sceneManager.isDragging) return;
if (settings['Internal kinematic']) return;
modelTargetAngles = angles;
};
const createPanel = () => {
gui_panel = new GUI({ width: 310 })
gui_panel.close()
gui_panel.domElement.id = 'three-gui-panel'
gui_panel = new GUI({ width: 310 });
gui_panel.close();
gui_panel.domElement.id = 'three-gui-panel';
const general = gui_panel.addFolder('General')
general.add(settings, 'Internal kinematic')
general.add(settings, 'Robot transform controls')
general.add(settings, 'Auto orient robot')
const general = gui_panel.addFolder('General');
general.add(settings, 'Internal kinematic');
general.add(settings, 'Robot transform controls');
general.add(settings, 'Auto orient robot');
const kinematic = gui_panel.addFolder('Kinematics')
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen()
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen()
const kinematic = gui_panel.addFolder('Kinematics');
kinematic.add(settings, 'omega', -20, 20).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'phi', -30, 30).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'psi', -20, 15).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'xm', -1, 1).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'ym', 0, 1).onChange(updateKinematicPosition).listen();
kinematic.add(settings, 'zm', -1.3, 1.3).onChange(updateKinematicPosition).listen();
const visibility = gui_panel.addFolder('Visualization')
visibility.add(settings, 'Trace feet')
visibility.add(settings, 'Trace points', 1, 1000, 1)
visibility.add(settings, 'Target position')
visibility.add(settings, 'Smooth motion')
visibility.addColor(settings, 'Background')
}
const visibility = gui_panel.addFolder('Visualization');
visibility.add(settings, 'Trace feet');
visibility.add(settings, 'Trace points', 1, 1000, 1);
visibility.add(settings, 'Target position');
visibility.add(settings, 'Smooth motion');
visibility.addColor(settings, 'Background');
};
const updateKinematicPosition = () => {
kinematicData.set([
@@ -164,14 +163,17 @@
settings.psi,
settings.xm,
settings.ym,
settings.zm
])
}
settings.zm,
]);
};
const updateAngles = (name: string, angle: number) => {
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI)
Throttler.throttle(() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))), 100)
}
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
Throttler.throttle(
() => servoAnglesOut.set(modelTargetAngles.map(num => Math.round(num))),
100
);
};
const createScene = async () => {
sceneManager
@@ -185,46 +187,46 @@
.addTransformControls(sceneManager.model)
.fillParent()
.addRenderCb(render)
.startRenderLoop()
.startRenderLoop();
if (ground) sceneManager.addGroundPlane()
if (ground) sceneManager.addGroundPlane();
const geometry = new SphereGeometry(0.1, 32, 16)
const material = new MeshBasicMaterial({ color: 0xffff00 })
target = new Mesh(geometry, material)
sceneManager.scene.add(target)
const geometry = new SphereGeometry(0.1, 32, 16);
const material = new MeshBasicMaterial({ color: 0xffff00 });
target = new Mesh(geometry, material);
sceneManager.scene.add(target);
if (debug) {
sceneManager.addDragControl(updateAngles)
sceneManager.addDragControl(updateAngles);
}
if (sky) sceneManager.addSky()
if (sky) sceneManager.addSky();
for (let i = 0; i < 4; i++) {
const geometry = new BufferGeometry()
const material = new LineBasicMaterial({ color: extractFootColor() })
const line = new Line(geometry, material)
trace_lines.push(geometry)
sceneManager.scene.add(line)
const geometry = new BufferGeometry();
const material = new LineBasicMaterial({ color: extractFootColor() });
const line = new Line(geometry, material);
trace_lines.push(geometry);
sceneManager.scene.add(line);
}
}
};
const renderTraceLines = (foot_positions: Vector3[]) => {
if (!settings['Trace feet']) {
if (!feet_trace.length) return
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
feet_trace = new Array(4).fill([])
return
if (!feet_trace.length) return;
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)));
feet_trace = new Array(4).fill([]);
return;
}
trace_lines.forEach((line, i) => {
feet_trace[i].push(foot_positions[i])
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
line.setFromPoints(feet_trace[i])
})
}
feet_trace[i].push(foot_positions[i]);
feet_trace[i] = feet_trace[i].slice(-settings['Trace points']);
line.setFromPoints(feet_trace[i]);
});
};
const calculate_kinematics = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
if (sceneManager.isDragging || !settings['Internal kinematic']) return;
const position: body_state_t = {
omega: settings.omega,
phi: settings.phi,
@@ -232,37 +234,37 @@
xm: settings.xm,
ym: settings.ym,
zm: settings.zm,
feet: body_state.feet
}
feet: body_state.feet,
};
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]))
modelTargetAngles = new_angles
}
let new_angles = kinematic.calcIK(position).map((x, i) => radToDeg(x * dir[i]));
modelTargetAngles = new_angles;
};
const orient_robot = (robot: URDFRobot, toes: Vector3[]) => {
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y))
if (settings['Robot transform controls'] || !settings['Auto orient robot']) return;
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1)
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1)
robot.position.z = smooth(robot.position.z, -settings.xm, 0.1);
robot.position.x = smooth(robot.position.x, -settings.zm, 0.1);
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1)
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1)
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1)
}
robot.rotation.z = smooth(robot.rotation.z, degToRad(-settings.phi + $mpu.heading + 90), 0.1);
robot.rotation.y = smooth(robot.rotation.y, degToRad(settings.omega), 0.1);
robot.rotation.x = smooth(robot.rotation.x, degToRad(settings.psi - 90), 0.1);
};
const update_camera = (robot: URDFRobot) => {
if (!settings['Fix camera on robot']) return
sceneManager.orbit.target = robot.position.clone()
}
if (!settings['Fix camera on robot']) return;
sceneManager.orbit.target = robot.position.clone();
};
const smooth = (start: number, end: number, amount: number) => {
return settings['Smooth motion'] ? lerp(start, end, amount) : end
}
return settings['Smooth motion'] ? lerp(start, end, amount) : end;
};
const update_gait = () => {
if (sceneManager.isDragging || !settings['Internal kinematic']) return
const controlData = get(outControllerData)
if (sceneManager.isDragging || !settings['Internal kinematic']) return;
const controlData = get(outControllerData);
const data = {
stop: controlData[0],
lx: controlData[1],
@@ -271,67 +273,67 @@
ry: controlData[4],
h: controlData[5],
s: controlData[6],
s1: controlData[7]
}
body_state.ym = ((data.h + 127) * 0.35) / 100
s1: controlData[7],
};
body_state.ym = ((data.h + 127) * 0.35) / 100;
let planner = planners[get(mode)]
const delta = performance.now() - lastTick
lastTick = performance.now()
let planner = planners[get(mode)];
const delta = performance.now() - lastTick;
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.phi = body_state.phi
settings.psi = body_state.psi
settings.xm = body_state.xm
settings.ym = body_state.ym
settings.zm = body_state.zm
}
settings.omega = body_state.omega;
settings.phi = body_state.phi;
settings.psi = body_state.psi;
settings.xm = body_state.xm;
settings.ym = body_state.ym;
settings.zm = body_state.zm;
};
const update_robot_position = (robot: URDFRobot) => {
if (!settings['Robot transform controls']) return
settings.omega = radToDeg(robot.rotation.y)
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90
settings.psi = radToDeg(robot.rotation.x) + 90
settings.xm = robot.position.z * 100
settings.zm = -robot.position.x * 100
}
if (!settings['Robot transform controls']) return;
settings.omega = radToDeg(robot.rotation.y);
settings.phi = radToDeg(robot.rotation.z) + $mpu.heading - 90;
settings.psi = radToDeg(robot.rotation.x) + 90;
settings.xm = robot.position.z * 100;
settings.zm = -robot.position.x * 100;
};
const updateTargetPosition = () => {
target.visible = settings['Target position']
target.position.x = smooth(target.position.x, target_position.x, 0.5)
target.position.z = smooth(target.position.z, target_position.z, 0.5)
}
target.visible = settings['Target position'];
target.position.x = smooth(target.position.x, target_position.x, 0.5);
target.position.z = smooth(target.position.z, target_position.z, 0.5);
};
const render = () => {
const robot = sceneManager.model
if (!robot) return
const robot = sceneManager.model;
if (!robot) return;
const toes = getToeWorldPositions(robot)
const toes = getToeWorldPositions(robot);
renderTraceLines(toes)
update_camera(robot)
update_gait()
calculate_kinematics()
update_robot_position(robot)
renderTraceLines(toes);
update_camera(robot);
update_gait();
calculate_kinematics();
update_robot_position(robot);
sceneManager.transformControl.showX = settings['Robot transform controls']
sceneManager.transformControl.showY = settings['Robot transform controls']
sceneManager.transformControl.showZ = settings['Robot transform controls']
sceneManager.transformControl.showX = settings['Robot transform controls'];
sceneManager.transformControl.showY = settings['Robot transform controls'];
sceneManager.transformControl.showZ = settings['Robot transform controls'];
for (let i = 0; i < $jointNames.length; i++) {
currentModelAngles[i] = smooth(
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
modelTargetAngles[i],
0.1
)
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]))
);
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
}
orient_robot(robot, toes)
updateTargetPosition()
}
orient_robot(robot, toes);
updateTargetPosition();
};
</script>
<svelte:window onresize={sceneManager.fillParent} />
@@ -1,37 +1,40 @@
<script lang="ts">
import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
import Widget from './Widget.svelte';
import WidgetContainer from './WidgetContainer.svelte';
import {
WidgetComponents,
type WidgetContainerConfig,
isWidgetConfig,
} from '$lib/stores/application';
import Widget from './Widget.svelte';
interface Props {
container: WidgetContainerConfig;
}
interface Props {
container: WidgetContainerConfig;
}
let { container }: Props = $props();
let { container }: Props = $props();
</script>
<div class="w-full h-full flex flex-col overflow-hidden">
<div
class="flex w-full h-full"
class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}
>
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
{@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets}
<WidgetContainer container={widget} />
{/if}
</Widget>
{#if index !== container.widgets.length - 1}
<div
class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}
></div>
{/if}
{/each}
</div>
<div
class="flex w-full h-full"
class:flex-row={container.layout === 'column'}
class:flex-col={container.layout === 'row'}
class:flex-wrap={container.layout === 'wrap'}>
{#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget>
{#if isWidgetConfig(widget)}
{@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets}
<WidgetContainer container={widget} />
{/if}
</Widget>
{#if index !== container.widgets.length - 1}
<div
class="divider bg-base-300 m-0"
class:divider-horizontal={container.layout === 'column'}>
</div>
{/if}
{/each}
</div>
</div>
+53 -53
View File
@@ -1,9 +1,9 @@
<script lang="ts">
import { page } from '$app/state'
import { useFeatureFlags } from '$lib/stores/featureFlags'
import GithubButton from '../menu/GithubButton.svelte'
import LogoButton from '../menu/LogoButton.svelte'
import MenuList from '../menu/MenuList.svelte'
import { page } from '$app/state';
import { useFeatureFlags } from '$lib/stores/featureFlags';
import GithubButton from '../menu/GithubButton.svelte';
import LogoButton from '../menu/LogoButton.svelte';
import MenuList from '../menu/MenuList.svelte';
import {
Connection,
Settings,
@@ -20,28 +20,28 @@
AP,
Copyright,
Metrics,
DNS
} from '$lib/components/icons'
import appEnv from 'app-env'
DNS,
} from '$lib/components/icons';
import { PUBLIC_VITE_USE_HOST_NAME } from '$env/static/public';
const features = useFeatureFlags()
const features = useFeatureFlags();
const appName = page.data.app_name
const appName = page.data.app_name;
const copyright = page.data.copyright
const copyright = page.data.copyright;
const github = { href: 'https://github.com/' + page.data.github, active: true }
const github = { href: 'https://github.com/' + page.data.github, active: true };
type menuItem = {
title: string
icon: ConstructorOfATypedSvelteComponent
href?: string
feature: boolean
active?: boolean
submenu?: menuItem[]
}
title: string;
icon: ConstructorOfATypedSvelteComponent;
href?: string;
feature: boolean;
active?: boolean;
submenu?: menuItem[];
};
let menuItems = $state<menuItem[]>([])
let menuItems = $state<menuItem[]>([]);
$effect(() => {
menuItems = [
@@ -49,13 +49,13 @@
title: 'Connection',
icon: WiFi,
href: '/connection',
feature: !appEnv.VITE_USE_HOST_NAME
feature: !PUBLIC_VITE_USE_HOST_NAME,
},
{
title: 'Controller',
icon: MdiController,
href: '/controller',
feature: true
feature: true,
},
{
title: 'Peripherals',
@@ -66,27 +66,27 @@
title: 'I2C',
icon: Connection,
href: '/peripherals/i2c',
feature: true
feature: true,
},
{
title: 'Camera',
icon: Camera,
href: '/peripherals/camera',
feature: $features.camera
feature: $features.camera,
},
{
title: 'Servo',
icon: MotorOutline,
href: '/peripherals/servo',
feature: true
feature: true,
},
{
title: 'IMU',
icon: Rotate3d,
href: '/peripherals/imu',
feature: $features.imu || $features.mag || $features.bmp
}
]
feature: $features.imu || $features.mag || $features.bmp,
},
],
},
{
title: 'WiFi',
@@ -97,21 +97,21 @@
title: 'WiFi Station',
icon: Router,
href: '/wifi/sta',
feature: true
feature: true,
},
{
title: 'Access Point',
icon: AP,
href: '/wifi/ap',
feature: true
feature: true,
},
{
title: 'mDNS',
icon: DNS,
href: '/wifi/mdns',
feature: true
}
]
feature: true,
},
],
},
{
title: 'System',
@@ -122,51 +122,51 @@
title: 'System Status',
icon: Health,
href: '/system/status',
feature: true
feature: true,
},
{
title: 'File System',
icon: Folder,
href: '/system/filesystem',
feature: true
feature: true,
},
{
title: 'System Metrics',
icon: Metrics,
href: '/system/metrics',
feature: true
feature: true,
},
{
title: 'Firmware Update',
icon: Update,
href: '/system/update',
feature: $features.ota || $features.upload_firmware || $features.download_firmware
}
]
}
] as menuItem[]
})
feature: $features.ota || $features.upload_firmware || $features.download_firmware,
},
],
},
] as menuItem[];
});
const { menuClicked } = $props()
const { menuClicked } = $props();
function setActiveMenuItem(targetTitle: string) {
menuItems.forEach(item => {
item.active = item.title === targetTitle
item.active = item.title === targetTitle;
item.submenu?.forEach(subItem => {
subItem.active = subItem.title === targetTitle
})
})
menuItems = menuItems
menuClicked()
subItem.active = subItem.title === targetTitle;
});
});
menuItems = menuItems;
menuClicked();
}
$effect(() => {
setActiveMenuItem(page.data.title)
})
setActiveMenuItem(page.data.title);
});
const updateMenu = (event: any) => {
setActiveMenuItem(event.details)
}
setActiveMenuItem(event.details);
};
</script>
<div class="flex h-full w-80 flex-col p-4 bg-base-200 text-base-content">
@@ -1,111 +1,109 @@
<script lang="ts">
import { page } from '$app/state';
import { modals } from 'svelte-modals';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/types/models';
import { useFeatureFlags } from '$lib/stores/featureFlags';
import { Cancel, CloudDown, Firmware } from '../icons';
import { page } from '$app/state';
import { modals } from 'svelte-modals';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/types/models';
import { useFeatureFlags } from '$lib/stores/featureFlags';
import { Cancel, CloudDown, Firmware } from '../icons';
const features = useFeatureFlags();
const features = useFeatureFlags();
interface Props {
update?: boolean;
interface Props {
update?: boolean;
}
let { update = $bindable(false) }: Props = $props();
let firmwareVersion: string = $state('');
let firmwareDownloadLink: string = $state('');
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
};
const result = await api.get<GithubRelease>(
`https://api.github.com/repos/${page.data.github}/releases/latest`,
{ headers }
);
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
console.warn('Error: Could not find releases in the repository');
return;
}
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
let { update = $bindable(false) }: Props = $props();
const results = result.inner;
update = false;
firmwareVersion = '';
let firmwareVersion: string = $state('');
let firmwareDownloadLink: string = $state('');
async function getGithubAPI() {
const headers = {
accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28'
};
const result = await api.get<GithubRelease>(
`https://api.github.com/repos/${page.data.github}/releases/latest`,
{ headers }
);
if (result.inner.message === '404' || result.inner.message == 'Not Found') {
console.warn('Error: Could not find releases in the repository');
return;
}
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
const results = result.inner;
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $features.firmware_version) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($features.firmware_built_target)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($features.firmware_built_target as string)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
}
}
async function postGithubDownload(url: string) {
const result = await api.post('/api/downloadUpdate', { download_url: url });
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
async function postGithubDownload(url: string) {
const result = await api.post('/api/downloadUpdate', { download_url: url });
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
}
onMount(async () => {
if ($features.download_firmware) {
await getGithubAPI();
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
}
});
onMount(async () => {
if ($features.download_firmware) {
await getGithubAPI();
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
}
});
function confirmGithubUpdate(url: string) {
modals.open(ConfirmDialog, {
title: 'Confirm flashing new firmware to the device',
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Update', icon: CloudDown }
},
onConfirm: () => {
postGithubDownload(url);
modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll()
});
}
function confirmGithubUpdate(url: string) {
modals.open(ConfirmDialog, {
title: 'Confirm flashing new firmware to the device',
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Update', icon: CloudDown },
},
onConfirm: () => {
postGithubDownload(url);
modals.open(GithubUpdateDialog, {
onConfirm: () => modals.closeAll(),
});
}
},
});
}
</script>
{#if update}
<div class="indicator flex-none">
<button
class="btn btn-square btn-ghost h-9 w-9"
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
>
{firmwareVersion}
</span>
<Firmware class="h-7 w-7" />
</button>
</div>
<div class="indicator flex-none">
<button
class="btn btn-square btn-ghost h-9 w-9"
onclick={() => confirmGithubUpdate(firmwareDownloadLink)}>
<span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1">
{firmwareVersion}
</span>
<Firmware class="h-7 w-7" />
</button>
</div>
{/if}
@@ -1,103 +1,101 @@
<script lang="ts">
import { daisyColor } from "$lib/utilities";
import { Chart, registerables } from "chart.js";
import { onMount } from "svelte";
import { cubicOut } from "svelte/easing";
import { slide } from "svelte/transition";
import { daisyColor } from '$lib/utilities';
import { Chart, registerables } from 'chart.js';
import { onMount } from 'svelte';
import { cubicOut } from 'svelte/easing';
import { slide } from 'svelte/transition';
let chartElement: HTMLCanvasElement = $state();
let chart: Chart;
let chartElement: HTMLCanvasElement;
let chart: Chart;
interface Props {
label: any;
data: number[];
title: any;
}
interface Props {
label: any;
data: number[];
title: any;
}
let { label, data, title }: Props = $props();
let { label, data, title }: Props = $props();
Chart.register(...registerables);
Chart.register(...registerables);
onMount(() => {
chart = new Chart(chartElement, {
type: 'line',
data: {
labels: data,
datasets: [
{
label,
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
data,
yAxisID: 'y'
},
]
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true
},
tooltip: {
mode: 'index',
intersect: false
}
},
elements: {
point: {
radius: 0
}
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10)
},
ticks: {
color: daisyColor('--bc')
},
display: false
},
y: {
type: 'linear',
title: {
display: true,
text: title,
color: daisyColor('--bc'),
font: {
size: 16,
weight: 'bold'
}
},
position: 'left',
min: 0,
max: 100,
grid: { color: daisyColor('--bc', 10) },
ticks: {
color: daisyColor('--bc')
},
border: { color: daisyColor('--bc', 10) }
}
}
}
});
onMount(() => {
chart = new Chart(chartElement, {
type: 'line',
data: {
labels: data,
datasets: [
{
label,
borderColor: daisyColor('--p'),
backgroundColor: daisyColor('--p', 50),
borderWidth: 2,
data,
yAxisID: 'y',
},
],
},
options: {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
display: true,
},
tooltip: {
mode: 'index',
intersect: false,
},
},
elements: {
point: {
radius: 0,
},
},
scales: {
x: {
grid: {
color: daisyColor('--bc', 10),
},
ticks: {
color: daisyColor('--bc'),
},
display: false,
},
y: {
type: 'linear',
title: {
display: true,
text: title,
color: daisyColor('--bc'),
font: {
size: 16,
weight: 'bold',
},
},
position: 'left',
min: 0,
max: 100,
grid: { color: daisyColor('--bc', 10) },
ticks: {
color: daisyColor('--bc'),
},
border: { color: daisyColor('--bc', 10) },
},
},
},
});
setInterval(() => {
chart.data.labels = data
chart.data.datasets[0].data = data
}, 500);
})
setInterval(() => {
chart.data.labels = data;
chart.data.datasets[0].data = data;
}, 500);
});
</script>
<div class="w-full h-full overflow-x-auto">
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}
>
<canvas bind:this={chartElement}></canvas>
</div>
</div>
<div
class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }}>
<canvas bind:this={chartElement}></canvas>
</div>
</div>
+13 -14
View File
@@ -1,20 +1,19 @@
<script lang="ts">
interface Props {
options?: string[];
selectedOption?: string;
change: () => void;
[key: string]: any;
}
interface Props {
options?: string[];
selectedOption?: string;
change?: () => void;
[key: string]: any;
}
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
</script>
<select
bind:value={selectedOption}
{...rest}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
>
{#each options as option}
<option value={option}>{option}</option>
{/each}
bind:value={selectedOption}
{...rest}
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}">
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>