✅ Fixes build warning and errors
This commit is contained in:
@@ -1,51 +1,40 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { focusTrap } from 'svelte-focus-trap';
|
import { focusTrap } from 'svelte-focus-trap';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { Check } from './icons';
|
import { Check } from './icons';
|
||||||
import { exitBeforeEnter } from 'svelte-modals';
|
import { exitBeforeEnter, type ModalProps } from 'svelte-modals';
|
||||||
|
|
||||||
// provided by <Modals />
|
let {
|
||||||
|
isOpen,
|
||||||
interface Props {
|
title,
|
||||||
isOpen: boolean;
|
message,
|
||||||
title: string;
|
onDismiss,
|
||||||
message: string;
|
labels = {
|
||||||
onDismiss: any;
|
dismiss: { label: 'Dismiss', icon: Check },
|
||||||
dismiss?: any;
|
},
|
||||||
}
|
}: ModalProps = $props();
|
||||||
|
|
||||||
let {
|
|
||||||
isOpen,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
onDismiss,
|
|
||||||
dismiss = { label: 'Dismiss', icon: Check }
|
|
||||||
}: Props = $props();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#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
|
<div
|
||||||
role="dialog"
|
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="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||||
transition:fly={{ y: 50 }}
|
<div class="divider my-2"></div>
|
||||||
use:exitBeforeEnter
|
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||||
use:focusTrap
|
<div class="divider my-2"></div>
|
||||||
>
|
<div class="flex justify-end gap-2">
|
||||||
<div
|
<button
|
||||||
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="btn btn-warning text-warning-content inline-flex items-center"
|
||||||
>
|
onclick={onDismiss}>
|
||||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
<labels.dismiss.icon class="mr-2 h-5 w-5" /><span>{labels.dismiss.label}</span>
|
||||||
<div class="divider my-2"></div>
|
</button>
|
||||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
</div>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,37 +1,40 @@
|
|||||||
<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 {
|
||||||
import Widget from './Widget.svelte';
|
WidgetComponents,
|
||||||
|
type WidgetContainerConfig,
|
||||||
|
isWidgetConfig,
|
||||||
|
} from '$lib/stores/application';
|
||||||
|
import Widget from './Widget.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
container: WidgetContainerConfig;
|
container: WidgetContainerConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { container }: Props = $props();
|
let { container }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full h-full flex flex-col overflow-hidden">
|
<div class="w-full h-full flex flex-col overflow-hidden">
|
||||||
<div
|
<div
|
||||||
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)}
|
{@const SvelteComponent = WidgetComponents[widget.component]}
|
||||||
{@const SvelteComponent = WidgetComponents[widget.component]}
|
<SvelteComponent {...widget.props} />
|
||||||
<SvelteComponent {...widget.props} />
|
{:else if widget.widgets}
|
||||||
{:else if widget.widgets}
|
<WidgetContainer container={widget} />
|
||||||
<WidgetContainer container={widget} />
|
{/if}
|
||||||
{/if}
|
</Widget>
|
||||||
</Widget>
|
{#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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,111 +1,109 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { modals } from 'svelte-modals';
|
import { modals } from 'svelte-modals';
|
||||||
import { notifications } from '$lib/components/toasts/notifications';
|
import { notifications } from '$lib/components/toasts/notifications';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import type { GithubRelease } from '$lib/types/models';
|
import type { GithubRelease } from '$lib/types/models';
|
||||||
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
import { useFeatureFlags } from '$lib/stores/featureFlags';
|
||||||
import { Cancel, CloudDown, Firmware } from '../icons';
|
import { Cancel, CloudDown, Firmware } from '../icons';
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
update?: boolean;
|
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('');
|
if (compareVersions(results.tag_name, $features.firmware_version as string) === 1) {
|
||||||
let firmwareDownloadLink: string = $state('');
|
// iterate over assets and find the correct one
|
||||||
|
for (let i = 0; i < results.assets.length; i++) {
|
||||||
async function getGithubAPI() {
|
// check if the asset is of type *.bin
|
||||||
const headers = {
|
if (
|
||||||
accept: 'application/vnd.github+json',
|
results.assets[i].name.includes('.bin') &&
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
results.assets[i].name.includes($features.firmware_built_target as string)
|
||||||
};
|
) {
|
||||||
const result = await api.get<GithubRelease>(
|
update = true;
|
||||||
`https://api.github.com/repos/${page.data.github}/releases/latest`,
|
firmwareVersion = results.tag_name;
|
||||||
{ headers }
|
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||||
);
|
notifications.info('Firmware update available.', 5000);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function postGithubDownload(url: string) {
|
async function postGithubDownload(url: string) {
|
||||||
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
const result = await api.post('/api/downloadUpdate', { download_url: url });
|
||||||
if (result.isErr()) {
|
if (result.isErr()) {
|
||||||
console.error('Error:', result.inner);
|
console.error('Error:', result.inner);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if ($features.download_firmware) {
|
if ($features.download_firmware) {
|
||||||
await getGithubAPI();
|
await getGithubAPI();
|
||||||
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
|
setInterval(async () => await getGithubAPI(), 60 * 60 * 1000); // once per hour
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function confirmGithubUpdate(url: string) {
|
function confirmGithubUpdate(url: string) {
|
||||||
modals.open(ConfirmDialog, {
|
modals.open(ConfirmDialog, {
|
||||||
title: 'Confirm flashing new firmware to the device',
|
title: 'Confirm flashing new firmware to the device',
|
||||||
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>
|
||||||
|
|
||||||
{#if update}
|
{#if update}
|
||||||
<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}
|
||||||
>
|
</span>
|
||||||
{firmwareVersion}
|
<Firmware class="h-7 w-7" />
|
||||||
</span>
|
</button>
|
||||||
<Firmware class="h-7 w-7" />
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,103 +1,101 @@
|
|||||||
<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 {
|
||||||
label: any;
|
label: any;
|
||||||
data: number[];
|
data: number[];
|
||||||
title: any;
|
title: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { label, data, title }: Props = $props();
|
let { label, data, title }: Props = $props();
|
||||||
|
|
||||||
Chart.register(...registerables);
|
Chart.register(...registerables);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
chart = new Chart(chartElement, {
|
chart = new Chart(chartElement, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: data,
|
labels: data,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label,
|
label,
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: daisyColor('--p'),
|
||||||
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',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: title,
|
text: title,
|
||||||
color: daisyColor('--bc'),
|
color: daisyColor('--bc'),
|
||||||
font: {
|
font: {
|
||||||
size: 16,
|
size: 16,
|
||||||
weight: 'bold'
|
weight: 'bold',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
position: 'left',
|
position: 'left',
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 100,
|
max: 100,
|
||||||
grid: { color: daisyColor('--bc', 10) },
|
grid: { color: daisyColor('--bc', 10) },
|
||||||
ticks: {
|
ticks: {
|
||||||
color: daisyColor('--bc')
|
color: daisyColor('--bc'),
|
||||||
},
|
},
|
||||||
border: { color: daisyColor('--bc', 10) }
|
border: { color: daisyColor('--bc', 10) },
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
chart.data.labels = data
|
chart.data.labels = data;
|
||||||
chart.data.datasets[0].data = data
|
chart.data.datasets[0].data = data;
|
||||||
}, 500);
|
}, 500);
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<div class="w-full h-full overflow-x-auto">
|
<div class="w-full h-full overflow-x-auto">
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1 h-60"
|
class="flex w-full flex-col space-y-1 h-60"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
>
|
<canvas bind:this={chartElement}></canvas>
|
||||||
<canvas bind:this={chartElement}></canvas>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
interface Props {
|
interface Props {
|
||||||
options?: string[];
|
options?: string[];
|
||||||
selectedOption?: string;
|
selectedOption?: string;
|
||||||
change: () => void;
|
change?: () => void;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<select
|
<select
|
||||||
bind:value={selectedOption}
|
bind:value={selectedOption}
|
||||||
{...rest}
|
{...rest}
|
||||||
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
|
class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}">
|
||||||
>
|
{#each options as option}
|
||||||
{#each options as option}
|
<option value={option}>{option}</option>
|
||||||
<option value={option}>{option}</option>
|
{/each}
|
||||||
{/each}
|
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ 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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return featureFlagsStore;
|
return featureFlagsStore;
|
||||||
}
|
}
|
||||||
@@ -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', '');
|
||||||
|
|||||||
@@ -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);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
+14
-14
@@ -2,21 +2,21 @@ export const prerender = false;
|
|||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
|
|
||||||
const registerFetchIntercept = async () => {
|
const registerFetchIntercept = async () => {
|
||||||
const { fetch: originalFetch } = window;
|
const { fetch: originalFetch } = window;
|
||||||
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);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const load = async () => {
|
export const load = async () => {
|
||||||
await registerFetchIntercept();
|
await registerFetchIntercept();
|
||||||
return {
|
return {
|
||||||
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',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,15 +1,7 @@
|
|||||||
<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">
|
||||||
<SystemMetrics />
|
<SystemMetrics />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,373 +1,369 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { Chart, registerables } from 'chart.js';
|
import { Chart, registerables } from 'chart.js';
|
||||||
|
|
||||||
import { daisyColor } from '$lib/utilities';
|
import { daisyColor } from '$lib/utilities';
|
||||||
import { analytics } from '$lib/stores/analytics';
|
import { analytics } from '$lib/stores/analytics';
|
||||||
import { Metrics } from '$lib/components/icons';
|
import { Metrics } from '$lib/components/icons';
|
||||||
|
|
||||||
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(() => {
|
||||||
cpuChart = new Chart(cpuChartElement, {
|
cpuChart = new Chart(cpuChartElement, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: $analytics.cpu_usage,
|
labels: $analytics.cpu_usage,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Cpu usage core 0',
|
label: 'Cpu usage core 0',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: daisyColor('--p'),
|
||||||
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',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: daisyColor('--p'),
|
||||||
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',
|
||||||
borderColor: daisyColor('--s'),
|
borderColor: daisyColor('--s'),
|
||||||
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',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Cpu usage [%]',
|
text: 'Cpu usage [%]',
|
||||||
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',
|
||||||
data: {
|
data: {
|
||||||
labels: $analytics.uptime,
|
labels: $analytics.uptime,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Used Heap',
|
label: 'Used Heap',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: daisyColor('--p'),
|
||||||
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',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Heap [kb]',
|
text: 'Heap [kb]',
|
||||||
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',
|
||||||
data: {
|
data: {
|
||||||
labels: $analytics.uptime,
|
labels: $analytics.uptime,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'File System Used',
|
label: 'File System Used',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: daisyColor('--p'),
|
||||||
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',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'File System [kb]',
|
text: 'File System [kb]',
|
||||||
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',
|
||||||
data: {
|
data: {
|
||||||
labels: $analytics.uptime,
|
labels: $analytics.uptime,
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Core Temperature',
|
label: 'Core Temperature',
|
||||||
borderColor: daisyColor('--p'),
|
borderColor: daisyColor('--p'),
|
||||||
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',
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Core Temperature [°C]',
|
text: 'Core Temperature [°C]',
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateData() {
|
function updateData() {
|
||||||
cpuChart.data.labels = $analytics.cpu_usage;
|
cpuChart.data.labels = $analytics.cpu_usage;
|
||||||
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
cpuChart.data.datasets[0].data = $analytics.cpu0_usage;
|
||||||
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
cpuChart.data.datasets[1].data = $analytics.cpu1_usage;
|
||||||
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
cpuChart.data.datasets[2].data = $analytics.cpu_usage;
|
||||||
cpuChart.update('none');
|
cpuChart.update('none');
|
||||||
|
|
||||||
heapChart.data.labels = $analytics.uptime;
|
heapChart.data.labels = $analytics.uptime;
|
||||||
heapChart.data.datasets[0].data = $analytics.used_heap;
|
heapChart.data.datasets[0].data = $analytics.used_heap;
|
||||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
heapChart.options.scales!.y!.max = Math.ceil($analytics.total_heap[0]);
|
||||||
heapChart.update('none');
|
heapChart.update('none');
|
||||||
|
|
||||||
filesystemChart.data.labels = $analytics.uptime;
|
filesystemChart.data.labels = $analytics.uptime;
|
||||||
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
filesystemChart.data.datasets[0].data = $analytics.fs_used;
|
||||||
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
heapChart.options.scales!.y!.max = Math.ceil($analytics.fs_total[0]);
|
||||||
filesystemChart.update('none');
|
filesystemChart.update('none');
|
||||||
|
|
||||||
temperatureChart.data.labels = $analytics.uptime;
|
temperatureChart.data.labels = $analytics.uptime;
|
||||||
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
temperatureChart.data.datasets[0].data = $analytics.core_temp;
|
||||||
temperatureChart.update('none');
|
temperatureChart.update('none');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
||||||
>
|
</div>
|
||||||
<canvas bind:this={filesystemChartElement}></canvas>
|
</div>
|
||||||
</div>
|
<div class="w-full overflow-x-auto">
|
||||||
</div>
|
<div
|
||||||
<div class="w-full overflow-x-auto">
|
class="flex w-full flex-col space-y-1 h-52"
|
||||||
<div
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
class="flex w-full flex-col space-y-1 h-52"
|
<canvas bind:this={temperatureChartElement}></canvas>
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
</div>
|
||||||
>
|
</div>
|
||||||
<canvas bind:this={temperatureChartElement}></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -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,89 +149,92 @@
|
|||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
{#await getSystemStatus()}
|
{#await getSystemStatus()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then}
|
||||||
<div
|
{#if systemInformation}
|
||||||
class="flex w-full flex-col space-y-1"
|
<div
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
class="flex w-full flex-col space-y-1"
|
||||||
<StatusItem
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
icon={CPU}
|
<StatusItem
|
||||||
title="Chip"
|
icon={CPU}
|
||||||
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
title="Chip"
|
||||||
|
description={`${systemInformation.cpu_type} Rev ${systemInformation.cpu_rev}`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={SDK}
|
icon={SDK}
|
||||||
title="SDK Version"
|
title="SDK Version"
|
||||||
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
description={`ESP-IDF ${systemInformation.sdk_version} / Arduino ${systemInformation.arduino_version}`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={CPP}
|
icon={CPP}
|
||||||
title="Firmware Version"
|
title="Firmware Version"
|
||||||
description={systemInformation.firmware_version} />
|
description={systemInformation.firmware_version} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Speed}
|
icon={Speed}
|
||||||
title="CPU Frequency"
|
title="CPU Frequency"
|
||||||
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
description={`${systemInformation.cpu_freq_mhz} MHz ${
|
||||||
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
systemInformation.cpu_cores == 2 ? 'Dual Core' : 'Single Core'
|
||||||
}`} />
|
}`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Heap}
|
icon={Heap}
|
||||||
title="Heap (Free / Max Alloc)"
|
title="Heap (Free / Max Alloc)"
|
||||||
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
description={`${systemInformation.free_heap} / ${systemInformation.max_alloc_heap} bytes`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Pyramid}
|
icon={Pyramid}
|
||||||
title="PSRAM (Size / Free)"
|
title="PSRAM (Size / Free)"
|
||||||
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
description={`${systemInformation.psram_size} / ${systemInformation.psram_size} bytes`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Sketch}
|
icon={Sketch}
|
||||||
title="Sketch (Used / Free)"
|
title="Sketch (Used / Free)"
|
||||||
description={`${(
|
description={`${(
|
||||||
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
|
||||||
100
|
100
|
||||||
).toFixed(1)} % of
|
).toFixed(1)} % of
|
||||||
${systemInformation.free_sketch_space / 1000000} MB used (${
|
${systemInformation.free_sketch_space / 1000000} MB used (${
|
||||||
(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000
|
(systemInformation.free_sketch_space - systemInformation.sketch_size) / 1000000
|
||||||
} MB free)`} />
|
} MB free)`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Flash}
|
icon={Flash}
|
||||||
title="Flash Chip (Size / Speed)"
|
title="Flash Chip (Size / Speed)"
|
||||||
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
description={`${systemInformation.flash_chip_size / 1000000} MB / ${
|
||||||
systemInformation.flash_chip_speed / 1000000
|
systemInformation.flash_chip_speed / 1000000
|
||||||
} MHz`} />
|
} MHz`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Folder}
|
icon={Folder}
|
||||||
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
|
||||||
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
).toFixed(1)} % of ${systemInformation.fs_total / 1000000} MB used (${
|
||||||
}
|
(systemInformation.fs_total - systemInformation.fs_used) / 1000000
|
||||||
|
}
|
||||||
MB free)`} />
|
MB free)`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Temperature}
|
icon={Temperature}
|
||||||
title="Core Temperature"
|
title="Core Temperature"
|
||||||
description={`${
|
description={`${
|
||||||
systemInformation.core_temp == 53.33 ?
|
systemInformation.core_temp == 53.33 ?
|
||||||
'NaN'
|
'NaN'
|
||||||
: systemInformation.core_temp.toFixed(2) + ' °C'
|
: systemInformation.core_temp.toFixed(2) + ' °C'
|
||||||
}`} />
|
}`} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Stopwatch}
|
icon={Stopwatch}
|
||||||
title="Uptime"
|
title="Uptime"
|
||||||
description={convertSeconds(systemInformation.uptime)} />
|
description={convertSeconds(systemInformation.uptime)} />
|
||||||
|
|
||||||
<StatusItem
|
<StatusItem
|
||||||
icon={Power}
|
icon={Power}
|
||||||
title="Reset Reason"
|
title="Reset Reason"
|
||||||
description={systemInformation.cpu_reset_reason} />
|
description={systemInformation.cpu_reset_reason} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,165 +1,154 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { modals } from 'svelte-modals';
|
import { modals } from 'svelte-modals';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
import Spinner from '$lib/components/Spinner.svelte';
|
import Spinner from '$lib/components/Spinner.svelte';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
|
|
||||||
import { compareVersions } from 'compare-versions';
|
import { compareVersions } from 'compare-versions';
|
||||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||||
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
import InfoDialog from '$lib/components/InfoDialog.svelte';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import { useFeatureFlags } from '$lib/stores';
|
import { useFeatureFlags } from '$lib/stores';
|
||||||
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
import { Error, Cancel, Check, CloudDown, Github, Prerelease } from '$lib/components/icons';
|
||||||
|
|
||||||
const features = useFeatureFlags();
|
const features = useFeatureFlags();
|
||||||
|
|
||||||
async function getGithubAPI() {
|
async function getGithubAPI() {
|
||||||
const headers = {
|
const headers = {
|
||||||
accept: 'application/vnd.github+json',
|
accept: 'application/vnd.github+json',
|
||||||
'X-GitHub-Api-Version': '2022-11-28'
|
'X-GitHub-Api-Version': '2022-11-28',
|
||||||
};
|
};
|
||||||
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
const result = await api.get(`https://api.github.com/repos/${page.data.github}/releases`, {
|
||||||
headers
|
headers,
|
||||||
|
});
|
||||||
|
if (result.isErr()) {
|
||||||
|
console.error('Error:', result.inner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return result.inner as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postGithubDownload(url: string) {
|
||||||
|
const result = await api.post('/api/firmware/download', { download_url: url });
|
||||||
|
if (result.isErr()) {
|
||||||
|
console.error('Error:', result.inner);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirmGithubUpdate(assets: any) {
|
||||||
|
let url = '';
|
||||||
|
// iterate over assets and find the correct one
|
||||||
|
for (let i = 0; i < assets.length; i++) {
|
||||||
|
// check if the asset is of type *.bin
|
||||||
|
if (
|
||||||
|
assets[i].name.includes('.bin') &&
|
||||||
|
assets[i].name.includes($features.firmware_built_target)
|
||||||
|
) {
|
||||||
|
url = assets[i].browser_download_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (url === '') {
|
||||||
|
// if no asset was found, use the first one
|
||||||
|
modals.open(InfoDialog, {
|
||||||
|
title: 'No matching firmware found',
|
||||||
|
message:
|
||||||
|
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
|
||||||
|
dismiss: { label: 'OK', icon: Check },
|
||||||
|
onDismiss: () => modals.close(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
});
|
});
|
||||||
if (result.isErr()) {
|
},
|
||||||
console.error('Error:', result.inner);
|
});
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
return result.inner as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function postGithubDownload(url: string) {
|
|
||||||
const result = await api.post('/api/firmware/download', { download_url: url });
|
|
||||||
if (result.isErr()) {
|
|
||||||
console.error('Error:', result.inner);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmGithubUpdate(assets: any) {
|
|
||||||
let url = '';
|
|
||||||
// iterate over assets and find the correct one
|
|
||||||
for (let i = 0; i < assets.length; i++) {
|
|
||||||
// check if the asset is of type *.bin
|
|
||||||
if (
|
|
||||||
assets[i].name.includes('.bin') &&
|
|
||||||
assets[i].name.includes($features.firmware_built_target)
|
|
||||||
) {
|
|
||||||
url = assets[i].browser_download_url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (url === '') {
|
|
||||||
// if no asset was found, use the first one
|
|
||||||
modals.open(InfoDialog, {
|
|
||||||
title: 'No matching firmware found',
|
|
||||||
message:
|
|
||||||
'No matching firmware was found for the current device. Upload the firmware manually or build from sources.',
|
|
||||||
dismiss: { label: 'OK', icon: Check },
|
|
||||||
onDismiss: () => modals.close()
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
<Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span>Github Firmware Manager</span>
|
<span>Github Firmware Manager</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#await getGithubAPI()}
|
{#await getGithubAPI()}
|
||||||
<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"
|
<table class="table w-full table-auto">
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
<thead>
|
||||||
>
|
<tr class="font-bold">
|
||||||
<table class="table w-full table-auto">
|
<th align="left">Release</th>
|
||||||
<thead>
|
<th align="center" class="hidden sm:block">Release Date</th>
|
||||||
<tr class="font-bold">
|
<th align="center">Experimental</th>
|
||||||
<th align="left">Release</th>
|
<th align="center">Install</th>
|
||||||
<th align="center" class="hidden sm:block">Release Date</th>
|
</tr>
|
||||||
<th align="center">Experimental</th>
|
</thead>
|
||||||
<th align="center">Install</th>
|
<tbody>
|
||||||
</tr>
|
{#each githubReleases as release}
|
||||||
</thead>
|
<tr
|
||||||
<tbody>
|
class={(
|
||||||
{#each githubReleases as release}
|
compareVersions($features.firmware_version as string, release.tag_name) === 0
|
||||||
<tr
|
) ?
|
||||||
class={(
|
'bg-primary text-primary-content'
|
||||||
compareVersions(
|
: 'bg-base-100 h-14'}>
|
||||||
$features.firmware_version,
|
<td align="left" class="text-base font-semibold">
|
||||||
release.tag_name
|
<a
|
||||||
) === 0
|
href={release.html_url}
|
||||||
) ?
|
class="link link-hover"
|
||||||
'bg-primary text-primary-content'
|
target="_blank"
|
||||||
: 'bg-base-100 h-14'}
|
rel="noopener noreferrer">{release.name}</a
|
||||||
>
|
></td>
|
||||||
<td align="left" class="text-base font-semibold">
|
<td align="center" class="hidden min-h-full align-middle sm:block">
|
||||||
<a
|
<div class="my-2">
|
||||||
href={release.html_url}
|
{new Intl.DateTimeFormat('en-GB', {
|
||||||
class="link link-hover"
|
dateStyle: 'medium',
|
||||||
target="_blank"
|
}).format(new Date(release.published_at))}
|
||||||
rel="noopener noreferrer">{release.name}</a
|
</div>
|
||||||
></td
|
</td>
|
||||||
>
|
<td align="center">
|
||||||
<td align="center" class="hidden min-h-full align-middle sm:block">
|
{#if release.prerelease}
|
||||||
<div class="my-2">
|
<Prerelease class="text-accent h-5 w-5" />
|
||||||
{new Intl.DateTimeFormat('en-GB', {
|
{/if}
|
||||||
dateStyle: 'medium'
|
</td>
|
||||||
}).format(new Date(release.published_at))}
|
<td align="center">
|
||||||
</div>
|
{#if compareVersions($features.firmware_version as string, release.tag_name) != 0}
|
||||||
</td>
|
<button
|
||||||
<td align="center">
|
class="btn btn-ghost btn-circle btn-sm"
|
||||||
{#if release.prerelease}
|
onclick={() => {
|
||||||
<Prerelease class="text-accent h-5 w-5" />
|
confirmGithubUpdate(release.assets);
|
||||||
{/if}
|
}}>
|
||||||
</td>
|
<CloudDown class="text-secondary h-6 w-6" />
|
||||||
<td align="center">
|
</button>
|
||||||
{#if compareVersions($features.firmware_version, release.tag_name) != 0}
|
{/if}
|
||||||
<button
|
</td>
|
||||||
class="btn btn-ghost btn-circle btn-sm"
|
</tr>
|
||||||
onclick={() => {
|
{/each}
|
||||||
confirmGithubUpdate(release.assets);
|
</tbody>
|
||||||
}}
|
</table>
|
||||||
>
|
</div>
|
||||||
<CloudDown class="text-secondary h-6 w-6" />
|
</div>
|
||||||
</button>
|
{:catch error}
|
||||||
{/if}
|
<div class="alert alert-error shadow-lg">
|
||||||
</td>
|
<Error class="h-6 w-6 shrink-0" />
|
||||||
</tr>
|
<span>Please connect to a network with internet access to perform a firmware update.</span>
|
||||||
{/each}
|
</div>
|
||||||
</tbody>
|
{/await}
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:catch error}
|
|
||||||
<div class="alert alert-error shadow-lg">
|
|
||||||
<Error class="h-6 w-6 shrink-0" />
|
|
||||||
<span
|
|
||||||
>Please connect to a network with internet access to perform a firmware update.</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -1,57 +1,56 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { modals } from 'svelte-modals';
|
import { modals } from 'svelte-modals';
|
||||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||||
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
import SettingsCard from '$lib/components/SettingsCard.svelte';
|
||||||
|
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
import { Cancel, OTA, Warning } from '$lib/components/icons';
|
||||||
|
|
||||||
let files: FileList = $state();
|
let files: FileList | undefined = $state();
|
||||||
|
|
||||||
async function uploadBIN() {
|
async function uploadBIN() {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', files[0]);
|
formData.append('file', files![0]);
|
||||||
const result = await api.post('/api/firmware', formData);
|
const result = await api.post('/api/firmware', formData);
|
||||||
if (result.isErr()) console.error('Error:', result.inner);
|
if (result.isErr()) console.error('Error:', result.inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmBinUpload() {
|
function confirmBinUpload() {
|
||||||
modals.open(ConfirmDialog, {
|
modals.open(ConfirmDialog, {
|
||||||
title: 'Confirm Flashing the Device',
|
title: 'Confirm Flashing the Device',
|
||||||
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>
|
||||||
|
|
||||||
<SettingsCard collapsible={false}>
|
<SettingsCard collapsible={false}>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
<OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<span>Upload Firmware</span>
|
<span>Upload Firmware</span>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
<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>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="binFile"
|
id="binFile"
|
||||||
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
class="file-input file-input-bordered file-input-secondary mt-4 w-full"
|
||||||
bind:files
|
bind:files
|
||||||
accept=".bin,.md5"
|
accept=".bin,.md5"
|
||||||
onchange={confirmBinUpload}
|
onchange={confirmBinUpload} />
|
||||||
/>
|
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
@@ -1,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,22 +162,24 @@
|
|||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
{#await getAPStatus()}
|
{#await getAPStatus()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then}
|
||||||
<div
|
{#if apStatus}
|
||||||
class="flex w-full flex-col space-y-1"
|
<div
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
class="flex w-full flex-col space-y-1"
|
||||||
<StatusItem
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
icon={AP}
|
<StatusItem
|
||||||
title="Status"
|
icon={AP}
|
||||||
variant={apStatusVariant[apStatus.status]}
|
title="Status"
|
||||||
description={apStatusDescription[apStatus.status]} />
|
variant={apStatusVariant[apStatus.status]}
|
||||||
|
description={apStatusDescription[apStatus.status]} />
|
||||||
|
|
||||||
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
<StatusItem icon={Home} title="IP Address" description={apStatus.ip_address} />
|
||||||
|
|
||||||
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
<StatusItem icon={MAC} title="MAC Address" description={apStatus.mac_address} />
|
||||||
|
|
||||||
<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,175 +190,176 @@
|
|||||||
</div>
|
</div>
|
||||||
{#await getAPSettings()}
|
{#await getAPSettings()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then}
|
||||||
<div
|
{#if apSettings}
|
||||||
class="flex flex-col gap-2 p-0"
|
<div
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
class="flex flex-col gap-2 p-0"
|
||||||
<form
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
<form
|
||||||
onsubmit={preventDefault(handleSubmitAP)}
|
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
|
||||||
novalidate
|
onsubmit={preventDefault(handleSubmitAP)}
|
||||||
bind:this={formField}>
|
novalidate
|
||||||
<div>
|
bind:this={formField}>
|
||||||
<label class="label" for="apmode">
|
<div>
|
||||||
<span class="label-text">Provide Access Point ...</span>
|
<label class="label" for="apmode">
|
||||||
</label>
|
<span class="label-text">Provide Access Point ...</span>
|
||||||
<select
|
</label>
|
||||||
class="select select-bordered w-full"
|
<select
|
||||||
id="apmode"
|
class="select select-bordered w-full"
|
||||||
bind:value={apSettings.provision_mode}>
|
id="apmode"
|
||||||
{#each provisionMode as mode}
|
bind:value={apSettings.provision_mode}>
|
||||||
<option value={mode.id}>
|
{#each provisionMode as mode}
|
||||||
{mode.text}
|
<option value={mode.id}>
|
||||||
</option>
|
{mode.text}
|
||||||
{/each}
|
</option>
|
||||||
</select>
|
{/each}
|
||||||
</div>
|
</select>
|
||||||
<div>
|
</div>
|
||||||
<label class="label" for="ssid">
|
<div>
|
||||||
<span class="label-text text-md">SSID</span>
|
<label class="label" for="ssid">
|
||||||
</label>
|
<span class="label-text text-md">SSID</span>
|
||||||
<input
|
</label>
|
||||||
type="text"
|
<input
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
type="text"
|
||||||
formErrors.ssid
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
) ?
|
formErrors.ssid
|
||||||
'border-error border-2'
|
) ?
|
||||||
: ''}"
|
'border-error border-2'
|
||||||
bind:value={apSettings.ssid}
|
: ''}"
|
||||||
id="ssid"
|
bind:value={apSettings.ssid}
|
||||||
min="2"
|
id="ssid"
|
||||||
max="32"
|
min="2"
|
||||||
required />
|
max="32"
|
||||||
<label class="label" for="ssid">
|
required />
|
||||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
<label class="label" for="ssid">
|
||||||
>SSID must be between 2 and 32 characters long</span>
|
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||||
</label>
|
>SSID must be between 2 and 32 characters long</span>
|
||||||
</div>
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="pwd">
|
<label class="label" for="pwd">
|
||||||
<span class="label-text text-md">Password</span>
|
<span class="label-text text-md">Password</span>
|
||||||
</label>
|
</label>
|
||||||
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
<PasswordInput bind:value={apSettings.password} id="pwd" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="channel">
|
<label class="label" for="channel">
|
||||||
<span class="label-text text-md">Preferred Channel</span>
|
<span class="label-text text-md">Preferred Channel</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="13"
|
max="13"
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
formErrors.channel
|
formErrors.channel
|
||||||
) ?
|
) ?
|
||||||
'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
bind:value={apSettings.channel}
|
bind:value={apSettings.channel}
|
||||||
id="channel"
|
id="channel"
|
||||||
required />
|
required />
|
||||||
<label class="label" for="channel">
|
<label class="label" for="channel">
|
||||||
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
|
||||||
>Must be channel 1 to 13</span>
|
>Must be channel 1 to 13</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="clients">
|
<label class="label" for="clients">
|
||||||
<span class="label-text text-md">Max Clients</span>
|
<span class="label-text text-md">Max Clients</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="8"
|
max="8"
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
formErrors.max_clients
|
formErrors.max_clients
|
||||||
) ?
|
) ?
|
||||||
'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
bind:value={apSettings.max_clients}
|
bind:value={apSettings.max_clients}
|
||||||
id="clients"
|
id="clients"
|
||||||
required />
|
required />
|
||||||
<label class="label" for="clients">
|
<label class="label" for="clients">
|
||||||
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
|
||||||
>Maximum 8 clients allowed</span>
|
>Maximum 8 clients allowed</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="localIP">
|
||||||
<span class="label-text text-md">Local IP</span>
|
<span class="label-text text-md">Local IP</span>
|
||||||
</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"
|
bind:value={apSettings.local_ip}
|
||||||
bind:value={apSettings.local_ip}
|
id="localIP"
|
||||||
id="localIP"
|
required />
|
||||||
required />
|
<label class="label" for="localIP">
|
||||||
<label class="label" for="localIP">
|
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
>Must be a valid IPv4 address</span>
|
||||||
>Must be a valid IPv4 address</span>
|
</label>
|
||||||
</label>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="gateway">
|
<label class="label" for="gateway">
|
||||||
<span class="label-text text-md">Gateway IP</span>
|
<span class="label-text text-md">Gateway IP</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
class="input input-bordered w-full {formErrors.gateway_ip ? 'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.gateway_ip}
|
bind:value={apSettings.gateway_ip}
|
||||||
id="gateway"
|
id="gateway"
|
||||||
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>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="subnet">
|
<label class="label" for="subnet">
|
||||||
<span class="label-text text-md">Subnet Mask</span>
|
<span class="label-text text-md">Subnet Mask</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
class="input input-bordered w-full {formErrors.subnet_mask ? 'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
minlength="7"
|
||||||
maxlength="15"
|
maxlength="15"
|
||||||
size="15"
|
size="15"
|
||||||
bind:value={apSettings.subnet_mask}
|
bind:value={apSettings.subnet_mask}
|
||||||
id="subnet"
|
id="subnet"
|
||||||
required />
|
required />
|
||||||
<label class="label" for="subnet">
|
<label class="label" for="subnet">
|
||||||
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
|
||||||
>Must be a valid IPv4 address</span>
|
>Must be a valid IPv4 address</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label class="label my-auto cursor-pointer justify-start gap-4">
|
<label class="label my-auto cursor-pointer justify-start gap-4">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={apSettings.ssid_hidden}
|
bind:checked={apSettings.ssid_hidden}
|
||||||
class="checkbox checkbox-primary" />
|
class="checkbox checkbox-primary" />
|
||||||
<span class="">Hide SSID</span>
|
<span class="">Hide SSID</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="place-self-end">
|
<div class="place-self-end">
|
||||||
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
<button class="btn btn-primary" type="submit">Apply Settings</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
+115
-131
@@ -1,147 +1,131 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { focusTrap } from 'svelte-focus-trap';
|
import { focusTrap } from 'svelte-focus-trap';
|
||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
import RssiIndicator from '$lib/components/statusbar/RSSIIndicator.svelte';
|
||||||
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;
|
const encryptionType = [
|
||||||
storeNetwork: any;
|
'Open',
|
||||||
|
'WEP',
|
||||||
|
'WPA PSK',
|
||||||
|
'WPA2 PSK',
|
||||||
|
'WPA WPA2 PSK',
|
||||||
|
'WPA2 Enterprise',
|
||||||
|
'WPA3 PSK',
|
||||||
|
'WPA2 WPA3 PSK',
|
||||||
|
'WAPI PSK',
|
||||||
|
];
|
||||||
|
|
||||||
|
let listOfNetworks: NetworkItem[] = $state([]);
|
||||||
|
|
||||||
|
let scanActive = $state(false);
|
||||||
|
|
||||||
|
let pollingId: ReturnType<typeof setTimeout> | number;
|
||||||
|
|
||||||
|
async function scanNetworks() {
|
||||||
|
scanActive = true;
|
||||||
|
await api.get('/api/wifi/scan');
|
||||||
|
if ((await pollingResults()) == false) {
|
||||||
|
pollingId = setInterval(() => pollingResults(), 1000);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let { isOpen, storeNetwork }: Props = $props();
|
async function pollingResults() {
|
||||||
|
const result = await api.get<NetworkList>('/api/wifi/networks');
|
||||||
const encryptionType = [
|
if (result.isErr()) {
|
||||||
'Open',
|
console.error(`Error occurred while fetching: `, result.inner);
|
||||||
'WEP',
|
return false;
|
||||||
'WPA PSK',
|
|
||||||
'WPA2 PSK',
|
|
||||||
'WPA WPA2 PSK',
|
|
||||||
'WPA2 Enterprise',
|
|
||||||
'WPA3 PSK',
|
|
||||||
'WPA2 WPA3 PSK',
|
|
||||||
'WAPI PSK'
|
|
||||||
];
|
|
||||||
|
|
||||||
let listOfNetworks: NetworkItem[] = $state([]);
|
|
||||||
|
|
||||||
let scanActive = $state(false);
|
|
||||||
|
|
||||||
let pollingId: number;
|
|
||||||
|
|
||||||
async function scanNetworks() {
|
|
||||||
scanActive = true;
|
|
||||||
await api.get('/api/wifi/scan');
|
|
||||||
if ((await pollingResults()) == false) {
|
|
||||||
pollingId = setInterval(() => pollingResults(), 1000);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
let response = result.inner;
|
||||||
async function pollingResults() {
|
listOfNetworks = response.networks;
|
||||||
const result = await api.get<NetworkList>('/api/wifi/networks');
|
scanActive = false;
|
||||||
if (result.isErr()) {
|
if (listOfNetworks.length) {
|
||||||
console.error(`Error occurred while fetching: `, result.inner);
|
clearInterval(pollingId);
|
||||||
return false;
|
pollingId = 0;
|
||||||
}
|
|
||||||
let response = result.inner;
|
|
||||||
listOfNetworks = response.networks;
|
|
||||||
scanActive = false;
|
|
||||||
if (listOfNetworks.length) {
|
|
||||||
clearInterval(pollingId);
|
|
||||||
pollingId = 0;
|
|
||||||
}
|
|
||||||
return listOfNetworks.length;
|
|
||||||
}
|
}
|
||||||
|
return listOfNetworks.length;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
scanNetworks();
|
scanNetworks();
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
if (pollingId) {
|
if (pollingId) {
|
||||||
clearInterval(pollingId);
|
clearInterval(pollingId);
|
||||||
pollingId = 0;
|
pollingId = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#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
|
<div
|
||||||
role="dialog"
|
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="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
|
||||||
transition:fly={{ y: 50 }}
|
<div class="divider my-2"></div>
|
||||||
use:exitBeforeEnter
|
<div class="overflow-y-auto">
|
||||||
use:focusTrap
|
{#if scanActive}<div 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" />
|
||||||
<div
|
<p class="mt-8 text-2xl">Scanning ...</p>
|
||||||
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"
|
</div>
|
||||||
>
|
{:else}
|
||||||
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
|
<ul class="menu">
|
||||||
<div class="divider my-2"></div>
|
{#each listOfNetworks as network, i}
|
||||||
<div class="overflow-y-auto">
|
<li>
|
||||||
{#if scanActive}<div
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
class="bg-base-100 flex flex-col items-center justify-center p-6"
|
<div
|
||||||
>
|
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
|
||||||
<AP class="text-secondary h-32 w-32 shrink animate-ping stroke-2" />
|
onclick={() => {
|
||||||
<p class="mt-8 text-2xl">Scanning ...</p>
|
storeNetwork(network.ssid);
|
||||||
|
}}
|
||||||
|
role="button"
|
||||||
|
tabindex="0">
|
||||||
|
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
||||||
|
<Network class="text-primary-content h-auto w-full scale-75" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="font-bold">{network.ssid}</div>
|
||||||
|
<div class="text-sm opacity-75">
|
||||||
|
Security: {encryptionType[network.encryption_type]}, Channel: {network.channel}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</div>
|
||||||
<ul class="menu">
|
<div class="grow"></div>
|
||||||
{#each listOfNetworks as network, i}
|
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
||||||
<li>
|
</div>
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
</li>
|
||||||
<div
|
{/each}
|
||||||
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
|
</ul>
|
||||||
onclick={() => {
|
{/if}
|
||||||
storeNetwork(network.ssid);
|
</div>
|
||||||
}}
|
<div class="divider my-2"></div>
|
||||||
role="button"
|
<div class="flex flex-wrap justify-end gap-2">
|
||||||
tabindex="0"
|
<button
|
||||||
>
|
class="btn btn-primary inline-flex flex-none items-center"
|
||||||
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
|
disabled={scanActive}
|
||||||
<Network
|
onclick={scanNetworks}>
|
||||||
class="text-primary-content h-auto w-full scale-75"
|
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
||||||
/>
|
</button>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="font-bold">{network.ssid}</div>
|
|
||||||
<div class="text-sm opacity-75">
|
|
||||||
Security: {encryptionType[network.encryption_type]},
|
|
||||||
Channel: {network.channel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grow"></div>
|
|
||||||
<RssiIndicator showDBm={true} rssi={network.rssi} />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="divider my-2"></div>
|
|
||||||
<div class="flex flex-wrap justify-end gap-2">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary inline-flex flex-none items-center"
|
|
||||||
disabled={scanActive}
|
|
||||||
onclick={scanNetworks}
|
|
||||||
>
|
|
||||||
<Reload class="mr-2 h-5 w-5" /><span>Scan again</span>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
+375
-369
@@ -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,53 +292,55 @@
|
|||||||
<div class="w-full overflow-x-auto">
|
<div class="w-full overflow-x-auto">
|
||||||
{#await getWifiStatus()}
|
{#await getWifiStatus()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then}
|
||||||
<div
|
{#if wifiStatus}
|
||||||
class="flex w-full flex-col space-y-1"
|
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
|
||||||
<StatusItem
|
|
||||||
icon={AP}
|
|
||||||
title="Status"
|
|
||||||
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
|
||||||
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
|
||||||
|
|
||||||
{#if wifiStatus.status === 3}
|
|
||||||
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
|
||||||
|
|
||||||
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
|
||||||
|
|
||||||
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
|
||||||
<button
|
|
||||||
class="btn btn-circle btn-ghost btn-sm modal-button"
|
|
||||||
onclick={() => {
|
|
||||||
showWifiDetails = !showWifiDetails
|
|
||||||
}}>
|
|
||||||
<Down
|
|
||||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
|
||||||
showWifiDetails
|
|
||||||
) ?
|
|
||||||
'rotate-180'
|
|
||||||
: ''}" />
|
|
||||||
</button>
|
|
||||||
</StatusItem>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Folds open -->
|
|
||||||
{#if showWifiDetails}
|
|
||||||
<div
|
<div
|
||||||
class="flex w-full flex-col space-y-1 pt-1"
|
class="flex w-full flex-col space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
<StatusItem
|
||||||
|
icon={AP}
|
||||||
|
title="Status"
|
||||||
|
variant={wifiStatus.status === 3 ? 'success' : 'error'}
|
||||||
|
description={wifiStatus.status === 3 ? 'Connected' : 'Inactive'} />
|
||||||
|
|
||||||
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
{#if wifiStatus.status === 3}
|
||||||
|
<StatusItem icon={SSID} title="SSID" description={wifiStatus.ssid} />
|
||||||
|
|
||||||
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
<StatusItem icon={Home} title="IP Address" description={wifiStatus.local_ip} />
|
||||||
|
|
||||||
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
<StatusItem icon={WiFi} title="RSSI" description={`${wifiStatus.rssi} dBm`}>
|
||||||
|
<button
|
||||||
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
class="btn btn-circle btn-ghost btn-sm modal-button"
|
||||||
|
onclick={() => {
|
||||||
|
showWifiDetails = !showWifiDetails;
|
||||||
|
}}>
|
||||||
|
<Down
|
||||||
|
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
|
||||||
|
showWifiDetails
|
||||||
|
) ?
|
||||||
|
'rotate-180'
|
||||||
|
: ''}" />
|
||||||
|
</button>
|
||||||
|
</StatusItem>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Folds open -->
|
||||||
|
{#if showWifiDetails}
|
||||||
|
<div
|
||||||
|
class="flex w-full flex-col space-y-1 pt-1"
|
||||||
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
|
<StatusItem icon={MAC} title="MAC Address" description={wifiStatus.mac_address} />
|
||||||
|
|
||||||
|
<StatusItem icon={Channel} title="Channel" description={wifiStatus.channel} />
|
||||||
|
|
||||||
|
<StatusItem icon={Gateway} title="Gateway IP" description={wifiStatus.gateway_ip} />
|
||||||
|
|
||||||
|
<StatusItem icon={Subnet} title="Subnet Mask" description={wifiStatus.subnet_mask} />
|
||||||
|
|
||||||
|
<StatusItem icon={DNS} title="DNS" description={wifiStatus.dns_ip_1} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
@@ -349,254 +352,257 @@
|
|||||||
</div>
|
</div>
|
||||||
{#await getWifiSettings()}
|
{#await getWifiSettings()}
|
||||||
<Spinner />
|
<Spinner />
|
||||||
{:then nothing}
|
{:then}
|
||||||
<div class="relative w-full overflow-visible">
|
{#if wifiSettings}
|
||||||
<button
|
<div class="relative w-full overflow-visible">
|
||||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
<button
|
||||||
onclick={() => {
|
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
|
||||||
if (checkNetworkList()) {
|
onclick={() => {
|
||||||
addNetwork()
|
if (checkNetworkList()) {
|
||||||
showNetworkEditor = true
|
addNetwork();
|
||||||
}
|
showNetworkEditor = true;
|
||||||
}}>
|
}
|
||||||
<Add class="h-6 w-6" /></button>
|
}}>
|
||||||
<button
|
<Add class="h-6 w-6" /></button>
|
||||||
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
<button
|
||||||
onclick={() => {
|
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
|
||||||
if (checkNetworkList()) {
|
onclick={() => {
|
||||||
scanForNetworks()
|
if (checkNetworkList()) {
|
||||||
showNetworkEditor = true
|
scanForNetworks();
|
||||||
}
|
showNetworkEditor = true;
|
||||||
}}>
|
}
|
||||||
<Scan class="h-6 w-6" /></button>
|
}}>
|
||||||
|
<Scan class="h-6 w-6" /></button>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="overflow-x-auto space-y-1"
|
class="overflow-x-auto space-y-1"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
<DragDropList
|
<DragDropList
|
||||||
id="networks"
|
id="networks"
|
||||||
type={VerticalDropZone}
|
type={VerticalDropZone}
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
</StatusItem>
|
</StatusItem>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</DragDropList>
|
</DragDropList>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="divider mb-0"></div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-2 p-0"
|
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
|
||||||
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
|
||||||
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<label class="label" for="channel">
|
|
||||||
<span class="label-text text-md">Host Name</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
min="1"
|
|
||||||
max="32"
|
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
|
||||||
formErrorhostname
|
|
||||||
) ?
|
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
bind:value={wifiSettings.hostname}
|
|
||||||
id="channel"
|
|
||||||
required />
|
|
||||||
<label class="label" for="channel">
|
|
||||||
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
|
||||||
>Host name must be between 2 and 32 characters long</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
bind:checked={wifiSettings.priority_RSSI}
|
|
||||||
class="checkbox checkbox-primary sm:-mb-5" />
|
|
||||||
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if showNetworkEditor}
|
<div class="divider mb-0"></div>
|
||||||
<div class="divider my-0"></div>
|
<div
|
||||||
<div
|
class="flex flex-col gap-2 p-0"
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
<form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
|
||||||
|
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="channel">
|
||||||
<span class="label-text text-md">SSID</span>
|
<span class="label-text text-md">Host Name</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
min="1"
|
||||||
|
max="32"
|
||||||
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
formErrors.ssid
|
formErrorhostname
|
||||||
) ?
|
) ?
|
||||||
'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
bind:value={networkEditable.ssid}
|
bind:value={wifiSettings.hostname}
|
||||||
id="ssid"
|
id="channel"
|
||||||
min="2"
|
|
||||||
max="32"
|
|
||||||
required />
|
required />
|
||||||
<label class="label" for="ssid">
|
<label class="label" for="channel">
|
||||||
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
<span class="label-text-alt text-error {formErrorhostname ? '' : 'hidden'}"
|
||||||
>SSID must be between 3 and 32 characters long</span>
|
>Host name must be between 2 and 32 characters long</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<label class="label inline-flex cursor-pointer content-end justify-start gap-4">
|
||||||
<label class="label" for="pwd">
|
|
||||||
<span class="label-text text-md">Password</span>
|
|
||||||
</label>
|
|
||||||
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
|
||||||
</div>
|
|
||||||
<label
|
|
||||||
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
bind:checked={static_ip_config}
|
bind:checked={wifiSettings.priority_RSSI}
|
||||||
class="checkbox checkbox-primary sm:-mb-5" />
|
class="checkbox checkbox-primary sm:-mb-5" />
|
||||||
<span class="sm:-mb-5">Static IP Config?</span>
|
<span class="sm:-mb-5">Connect to strongest WiFi</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{#if static_ip_config}
|
|
||||||
|
{#if showNetworkEditor}
|
||||||
|
<div class="divider my-0"></div>
|
||||||
<div
|
<div
|
||||||
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||||
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="ssid">
|
||||||
<span class="label-text text-md">Local IP</span>
|
<span class="label-text text-md">SSID</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="input input-bordered w-full {formErrors.local_ip ?
|
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
|
||||||
|
formErrors.ssid
|
||||||
|
) ?
|
||||||
'border-error border-2'
|
'border-error border-2'
|
||||||
: ''}"
|
: ''}"
|
||||||
minlength="7"
|
bind:value={networkEditable.ssid}
|
||||||
maxlength="15"
|
id="ssid"
|
||||||
size="15"
|
min="2"
|
||||||
bind:value={networkEditable.local_ip}
|
max="32"
|
||||||
id="localIP"
|
|
||||||
required />
|
required />
|
||||||
<label class="label" for="localIP">
|
<label class="label" for="ssid">
|
||||||
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
|
||||||
>Must be a valid IPv4 address</span>
|
>SSID must be between 3 and 32 characters long</span>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="label" for="gateway">
|
|
||||||
<span class="label-text text-md">Gateway IP</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full {formErrors.gateway_ip ?
|
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
minlength="7"
|
|
||||||
maxlength="15"
|
|
||||||
size="15"
|
|
||||||
bind:value={networkEditable.gateway_ip}
|
|
||||||
required />
|
|
||||||
<label class="label" for="gateway">
|
|
||||||
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
|
||||||
>Must be a valid IPv4 address</span>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label" for="subnet">
|
<label class="label" for="pwd">
|
||||||
<span class="label-text text-md">Subnet Mask</span>
|
<span class="label-text text-md">Password</span>
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full {formErrors.subnet_mask ?
|
|
||||||
'border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
minlength="7"
|
|
||||||
maxlength="15"
|
|
||||||
size="15"
|
|
||||||
bind:value={networkEditable.subnet_mask}
|
|
||||||
required />
|
|
||||||
<label class="label" for="subnet">
|
|
||||||
<span
|
|
||||||
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
|
||||||
Must be a valid IPv4 address
|
|
||||||
</span>
|
|
||||||
</label>
|
</label>
|
||||||
|
<PasswordInput bind:value={networkEditable.password} id="pwd" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<label
|
||||||
<label class="label" for="gateway">
|
class="label inline-flex cursor-pointer content-end justify-start gap-4 mt-2 sm:mb-4">
|
||||||
<span class="label-text text-md">DNS 1</span>
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="checkbox"
|
||||||
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
bind:checked={static_ip_config}
|
||||||
: ''}"
|
class="checkbox checkbox-primary sm:-mb-5" />
|
||||||
minlength="7"
|
<span class="sm:-mb-5">Static IP Config?</span>
|
||||||
maxlength="15"
|
</label>
|
||||||
size="15"
|
|
||||||
bind:value={networkEditable.dns_ip_1}
|
|
||||||
required />
|
|
||||||
<label class="label" for="gateway">
|
|
||||||
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
|
||||||
Must be a valid IPv4 address
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="label" for="subnet">
|
|
||||||
<span class="label-text text-md">DNS 2</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
|
||||||
: ''}"
|
|
||||||
minlength="7"
|
|
||||||
maxlength="15"
|
|
||||||
size="15"
|
|
||||||
bind:value={networkEditable.dns_ip_2}
|
|
||||||
required />
|
|
||||||
<label class="label" for="subnet">
|
|
||||||
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
|
||||||
Must be a valid IPv4 address
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{#if static_ip_config}
|
||||||
{/if}
|
<div
|
||||||
|
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
|
||||||
|
transition:slide|local={{ duration: 300, easing: cubicOut }}>
|
||||||
|
<div>
|
||||||
|
<label class="label" for="localIP">
|
||||||
|
<span class="label-text text-md">Local IP</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full {formErrors.local_ip ?
|
||||||
|
'border-error border-2'
|
||||||
|
: ''}"
|
||||||
|
minlength="7"
|
||||||
|
maxlength="15"
|
||||||
|
size="15"
|
||||||
|
bind:value={networkEditable.local_ip}
|
||||||
|
id="localIP"
|
||||||
|
required />
|
||||||
|
<label class="label" for="localIP">
|
||||||
|
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
|
||||||
|
>Must be a valid IPv4 address</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="divider mb-2 mt-0"></div>
|
<div>
|
||||||
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
<label class="label" for="gateway">
|
||||||
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
<span class="label-text text-md">Gateway IP</span>
|
||||||
{newNetwork ? 'Add Network' : 'Update Network'}
|
</label>
|
||||||
</button>
|
<input
|
||||||
<button class="btn btn-primary" type="button" onclick={validateHostName}>
|
type="text"
|
||||||
Apply Settings
|
class="input input-bordered w-full {formErrors.gateway_ip ?
|
||||||
</button>
|
'border-error border-2'
|
||||||
</div>
|
: ''}"
|
||||||
</form>
|
minlength="7"
|
||||||
</div>
|
maxlength="15"
|
||||||
|
size="15"
|
||||||
|
bind:value={networkEditable.gateway_ip}
|
||||||
|
required />
|
||||||
|
<label class="label" for="gateway">
|
||||||
|
<span
|
||||||
|
class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
|
||||||
|
>Must be a valid IPv4 address</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label" for="subnet">
|
||||||
|
<span class="label-text text-md">Subnet Mask</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full {formErrors.subnet_mask ?
|
||||||
|
'border-error border-2'
|
||||||
|
: ''}"
|
||||||
|
minlength="7"
|
||||||
|
maxlength="15"
|
||||||
|
size="15"
|
||||||
|
bind:value={networkEditable.subnet_mask}
|
||||||
|
required />
|
||||||
|
<label class="label" for="subnet">
|
||||||
|
<span
|
||||||
|
class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}">
|
||||||
|
Must be a valid IPv4 address
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label" for="gateway">
|
||||||
|
<span class="label-text text-md">DNS 1</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full {formErrors.dns_1 ? 'border-error border-2'
|
||||||
|
: ''}"
|
||||||
|
minlength="7"
|
||||||
|
maxlength="15"
|
||||||
|
size="15"
|
||||||
|
bind:value={networkEditable.dns_ip_1}
|
||||||
|
required />
|
||||||
|
<label class="label" for="gateway">
|
||||||
|
<span class="label-text-alt text-error {formErrors.dns_1 ? '' : 'hidden'}">
|
||||||
|
Must be a valid IPv4 address
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label" for="subnet">
|
||||||
|
<span class="label-text text-md">DNS 2</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full {formErrors.dns_2 ? 'border-error border-2'
|
||||||
|
: ''}"
|
||||||
|
minlength="7"
|
||||||
|
maxlength="15"
|
||||||
|
size="15"
|
||||||
|
bind:value={networkEditable.dns_ip_2}
|
||||||
|
required />
|
||||||
|
<label class="label" for="subnet">
|
||||||
|
<span class="label-text-alt text-error {formErrors.dns_2 ? '' : 'hidden'}">
|
||||||
|
Must be a valid IPv4 address
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="divider mb-2 mt-0"></div>
|
||||||
|
<div class="mx-4 flex flex-wrap justify-end gap-2">
|
||||||
|
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
|
||||||
|
{newNetwork ? 'Add Network' : 'Update Network'}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" type="button" onclick={validateHostName}>
|
||||||
|
Apply Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/await}
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
|
|||||||
Reference in New Issue
Block a user