Makes socket messages event typed

This commit is contained in:
Rune Harlyk
2025-07-11 18:59:07 +02:00
parent c5901c65b3
commit 98f3fc674b
9 changed files with 382 additions and 359 deletions
+17
View File
@@ -1,3 +1,20 @@
export enum Topics {
imu = 'imu',
mode = 'mode',
input = 'input',
analytics = 'analytics',
position = 'position',
angles = 'angles',
i2cScan = 'i2cScan',
peripheralSettings = 'peripheralSettings',
otastatus = 'otastatus',
servoState = 'servoState',
servoPWM = 'servoPWM',
WiFiSettings = 'WiFiSettings',
sonar = 'sonar',
rssi = 'rssi'
}
export type vector = { x: number; y: number } export type vector = { x: number; y: number }
export interface ControllerInput { export interface ControllerInput {
+54 -54
View File
@@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte'
import { page } from '$app/state'; import { page } from '$app/state'
import { Modals, modals } from 'svelte-modals'; import { Modals, modals } from 'svelte-modals'
import Toast from '$lib/components/toasts/Toast.svelte'; import Toast from '$lib/components/toasts/Toast.svelte'
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications'
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition'
import '../app.css'; import '../app.css'
import Menu from '../lib/components/menu/Menu.svelte'; import Menu from '../lib/components/menu/Menu.svelte'
import Statusbar from '../lib/components/statusbar/statusbar.svelte'; import Statusbar from '../lib/components/statusbar/statusbar.svelte'
import { import {
telemetry, telemetry,
analytics, analytics,
@@ -19,75 +19,75 @@
servoAnglesOut, servoAnglesOut,
socket, socket,
location, location,
useFeatureFlags, useFeatureFlags
} from '$lib/stores'; } from '$lib/stores'
import type { Analytics, DownloadOTA } from '$lib/types/models'; import { Topics, type Analytics, type DownloadOTA } from '$lib/types/models'
interface Props { interface Props {
children?: import('svelte').Snippet; children?: import('svelte').Snippet
} }
let { children }: Props = $props(); let { children }: Props = $props()
const features = useFeatureFlags(); const features = useFeatureFlags()
onMount(async () => { onMount(async () => {
const ws = $location ? $location : window.location.host; const ws = $location ? $location : window.location.host
socket.init(`ws://${ws}/api/ws/events`); socket.init(`ws://${ws}/api/ws/events`)
addEventListeners(); addEventListeners()
outControllerData.subscribe(data => socket.sendEvent('input', data)); outControllerData.subscribe(data => socket.sendEvent(Topics.input, data))
mode.subscribe(data => socket.sendEvent('mode', data)); mode.subscribe(data => socket.sendEvent(Topics.mode, data))
servoAnglesOut.subscribe(data => socket.sendEvent('angles', data)); servoAnglesOut.subscribe(data => socket.sendEvent(Topics.angles, data))
kinematicData.subscribe(data => socket.sendEvent('position', data)); kinematicData.subscribe(data => socket.sendEvent(Topics.position, data))
}); })
onDestroy(() => { onDestroy(() => {
removeEventListeners(); removeEventListeners()
}); })
const addEventListeners = () => { const addEventListeners = () => {
socket.on('open', handleOpen); socket.on('open', handleOpen)
socket.on('close', handleClose); socket.on('close', handleClose)
socket.on('error', handleError); socket.on('error', handleError)
socket.on('rssi', handleNetworkStatus); socket.on(Topics.rssi, handleNetworkStatus)
socket.on('mode', (data: ModesEnum) => mode.set(data)); socket.on(Topics.mode, (data: ModesEnum) => mode.set(data))
socket.on('analytics', handleAnalytics); socket.on(Topics.analytics, handleAnalytics)
socket.on('angles', (angles: number[]) => { socket.on(Topics.angles, (angles: number[]) => {
if (angles.length) servoAngles.set(angles); if (angles.length) servoAngles.set(angles)
}); })
features.subscribe(data => { features.subscribe(data => {
if (data?.download_firmware) socket.on('otastatus', handleOAT); if (data?.download_firmware) socket.on(Topics.otastatus, handleOAT)
if (data?.sonar) socket.on('sonar', data => console.log(data)); if (data?.sonar) socket.on(Topics.sonar, data => console.log(data))
}); })
}; }
const removeEventListeners = () => { const removeEventListeners = () => {
socket.off('analytics', handleAnalytics); socket.off(Topics.analytics, handleAnalytics)
socket.off('open', handleOpen); socket.off('open', handleOpen)
socket.off('close', handleClose); socket.off('close', handleClose)
socket.off('rssi', handleNetworkStatus); socket.off(Topics.rssi, handleNetworkStatus)
socket.off('otastatus', handleOAT); socket.off(Topics.otastatus, handleOAT)
}; }
const handleOpen = () => { const handleOpen = () => {
notifications.success('Connection to device established', 5000); notifications.success('Connection to device established', 5000)
}; }
const handleClose = () => { const handleClose = () => {
notifications.error('Connection to device lost', 5000); notifications.error('Connection to device lost', 5000)
telemetry.setRSSI(0); telemetry.setRSSI(0)
}; }
const handleError = (data: any) => console.error(data); const handleError = (data: any) => console.error(data)
const handleAnalytics = (data: Analytics) => analytics.addData(data); const handleAnalytics = (data: Analytics) => analytics.addData(data)
const handleNetworkStatus = (data: number) => telemetry.setRSSI(data); const handleNetworkStatus = (data: number) => telemetry.setRSSI(data)
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data); const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data)
let menuOpen = $state(false); let menuOpen = $state(false)
</script> </script>
<svelte:head> <svelte:head>
+23 -23
View File
@@ -1,31 +1,31 @@
<script lang="ts"> <script lang="ts">
import Controls from './Controls.svelte'; import Controls from './Controls.svelte'
import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte'; import WidgetContainer from '$lib/components/layout/WidgetContainer.svelte'
import { selectedView, views } from '$lib/stores/application'; import { selectedView, views } from '$lib/stores/application'
import { onMount } from 'svelte'; import { onMount } from 'svelte'
import { mpu, socket } from '$lib/stores'; import { mpu, socket } from '$lib/stores'
import { imu } from '$lib/stores/imu'; import { imu } from '$lib/stores/imu'
import type { IMU } from '$lib/types/models'; import { Topics, type IMU } from '$lib/types/models'
let layout = $derived($views.find(v => v.name === $selectedView)!); let layout = $derived($views.find(v => v.name === $selectedView)!)
onMount(() => { onMount(() => {
socket.on('imu', (data: IMU) => { socket.on(Topics.imu, (data: IMU) => {
imu.addData(data); imu.addData(data)
if (data.heading) if (data.heading)
mpu.update(mpuData => { mpu.update(mpuData => {
mpuData.heading = data.heading; mpuData.heading = data.heading
console.log(data.heading); console.log(data.heading)
return mpuData; return mpuData
}); })
}); })
}); })
</script> </script>
<div class="absolute top-0 select-none w-screen h-screen"> <div class="absolute top-0 select-none w-screen h-screen">
<Controls /> <Controls />
<div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12"> <div class="absolute w-full h-screen top-0 overflow-hidden lg:pt-16 pt-12">
<WidgetContainer container={layout.content} /> <WidgetContainer container={layout.content} />
</div> </div>
</div> </div>
+4 -4
View File
@@ -2,7 +2,7 @@
import SettingsCard from '$lib/components/SettingsCard.svelte' import SettingsCard from '$lib/components/SettingsCard.svelte'
import { onMount } from 'svelte' import { onMount } from 'svelte'
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
import type { I2CDevice } from '$lib/types/models' import { Topics, type I2CDevice } from '$lib/types/models'
import { Connection } from '$lib/components/icons' import { Connection } from '$lib/components/icons'
import I2CSetting from './i2cSetting.svelte' import I2CSetting from './i2cSetting.svelte'
@@ -24,9 +24,9 @@
let isLoading = $state(false) let isLoading = $state(false)
onMount(() => { onMount(() => {
socket.on('i2cScan', handleScan) socket.on(Topics.i2cScan, handleScan)
triggerScan() triggerScan()
return () => socket.off('i2cScan', handleScan) return () => socket.off(Topics.i2cScan, handleScan)
}) })
const handleScan = (data: any) => { const handleScan = (data: any) => {
@@ -43,7 +43,7 @@
const triggerScan = () => { const triggerScan = () => {
isLoading = true isLoading = true
socket.sendEvent('i2cScan', '') socket.sendEvent(Topics.i2cScan, '')
} }
</script> </script>
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { Cancel, Edit, EditOff, Power } from '$lib/components/icons' import { Cancel, Edit, EditOff, Power } from '$lib/components/icons'
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
import type { PeripheralsConfiguration } from '$lib/types/models' import { Topics, type PeripheralsConfiguration } from '$lib/types/models'
import { onMount } from 'svelte' import { 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'
@@ -10,9 +10,9 @@
let isEditing = $state(false) let isEditing = $state(false)
onMount(() => { onMount(() => {
socket.on('peripheralSettings', handleSettings) socket.on(Topics.peripheralSettings, handleSettings)
socket.sendEvent('peripheralSettings', '') socket.sendEvent(Topics.peripheralSettings, '')
return () => socket.off('peripheralSettings', handleSettings) return () => socket.off(Topics.peripheralSettings, handleSettings)
}) })
const handleSettings = (data: any) => { const handleSettings = (data: any) => {
@@ -30,7 +30,7 @@
}, },
onConfirm: () => { onConfirm: () => {
modals.close() modals.close()
socket.sendEvent('peripheralSettings', settings) socket.sendEvent(Topics.peripheralSettings, settings)
} }
}) })
} }
+88 -88
View File
@@ -1,51 +1,51 @@
<script lang="ts"> <script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte'
import { imu } from '$lib/stores/imu'; import { imu } from '$lib/stores/imu'
import { Chart, registerables } from 'chart.js'; import { Chart, registerables } from 'chart.js'
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing'
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition'
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte'
import { socket } from '$lib/stores'; import { socket } from '$lib/stores'
import type { IMU } from '$lib/types/models'; import { Topics, 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: ReturnType<typeof setInterval> | number; let intervalId: ReturnType<typeof setInterval> | number
let angleChartElement: HTMLCanvasElement; let angleChartElement: HTMLCanvasElement
let tempChartElement: HTMLCanvasElement; let tempChartElement: HTMLCanvasElement
let altitudeChartElement: HTMLCanvasElement; 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' as const, intersect: false }, tooltip: { mode: 'index' as const, 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' as const, type: 'linear' as const,
@@ -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,55 +165,55 @@
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(Topics.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(Topics.imu)
clearInterval(intervalId); clearInterval(intervalId)
}); })
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { socket } from '$lib/stores' import { socket } from '$lib/stores'
import { Topics } from '$lib/types/models'
import { throttler as Throttler } from '$lib/utilities' import { throttler as Throttler } from '$lib/utilities'
let { servoId = $bindable(0), pwm = $bindable(306) } = $props() let { servoId = $bindable(0), pwm = $bindable(306) } = $props()
@@ -11,16 +12,16 @@
const throttler = new Throttler() const throttler = new Throttler()
const activateServo = () => { const activateServo = () => {
socket.sendEvent('servoState', { active: 1 }) socket.sendEvent(Topics.servoState, { active: 1 })
} }
const deactivateServo = () => { const deactivateServo = () => {
socket.sendEvent('servoState', { active: 0 }) socket.sendEvent(Topics.servoState, { active: 0 })
} }
const updatePWM = () => { const updatePWM = () => {
throttler.throttle(() => { throttler.throttle(() => {
socket.sendEvent('servoPWM', { servo_id: servoId, pwm }) socket.sendEvent(Topics.servoPWM, { servo_id: servoId, pwm })
}, 10) }, 10)
} }
@@ -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, type Analytics, Topics } 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,42 +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 | null = $state(null); 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(Topics.analytics, handleSystemData))
onDestroy(() => socket.off('analytics', handleSystemData)); onDestroy(() => socket.off(Topics.analytics, handleSystemData))
const handleSystemData = (data: Analytics) => { const handleSystemData = (data: Analytics) => {
if (systemInformation) { if (systemInformation) {
systemInformation = { systemInformation = {
...systemInformation, ...systemInformation,
...(data as unknown as SystemInformation), ...(data as unknown as SystemInformation)
}; }
} }
}; }
const postRestart = async () => await api.post('/api/system/restart'); const postRestart = async () => await api.post('/api/system/restart')
function confirmRestart() { function confirmRestart() {
modals.open(ConfirmDialog, { modals.open(ConfirmDialog, {
@@ -70,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() {
@@ -85,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() {
@@ -100,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[] = [
@@ -122,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}>
+131 -126
View File
@@ -1,19 +1,24 @@
<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 {
import { socket } from '$lib/stores'; Topics,
import { api } from '$lib/api'; type KnownNetworkItem,
type WifiSettings,
type WifiStatus
} from '$lib/types/models'
import { socket } from '$lib/stores'
import { api } from '$lib/api'
import { import {
Cancel, Cancel,
Delete, Delete,
@@ -31,9 +36,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 +48,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 | null = $state(null); let wifiStatus: WifiStatus | null = $state(null)
let wifiSettings: WifiSettings | null = $state(null); 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,157 +71,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(Topics.WiFiSettings))
onMount(() => { onMount(() => {
socket.on<WifiSettings>('WiFiSettings', data => { socket.on<WifiSettings>(Topics.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) 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: '',
@@ -225,14 +230,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) {
@@ -241,20 +246,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() {
@@ -264,21 +269,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>
@@ -312,7 +317,7 @@
<button <button
class="btn btn-circle btn-ghost btn-sm modal-button" class="btn btn-circle btn-ghost btn-sm modal-button"
onclick={() => { onclick={() => {
showWifiDetails = !showWifiDetails; showWifiDetails = !showWifiDetails
}}> }}>
<Down <Down
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {( class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
@@ -359,8 +364,8 @@
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
onclick={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
addNetwork(); addNetwork()
showNetworkEditor = true; showNetworkEditor = true
} }
}}> }}>
<Add class="h-6 w-6" /></button> <Add class="h-6 w-6" /></button>
@@ -368,8 +373,8 @@
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
onclick={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
scanForNetworks(); scanForNetworks()
showNetworkEditor = true; showNetworkEditor = true
} }
}}> }}>
<Scan class="h-6 w-6" /></button> <Scan class="h-6 w-6" /></button>
@@ -389,13 +394,13 @@
<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>