-
-
{pageTitle}
-
-
-
-
-
About this Device
-
-
-
-
-
Device Model
-
Alpha GaN Driver
-
50V 25A REV 2.3
-
-
-
-
-
Serial Number
-
Alpha-0001
-
Lasertack GmbH
-
-
-
-
-
Operating Hours
-
1,247
-
Total Runtime
-
-
-
-
-
Driver Core MCU
-
FW1.00
-
Build 05.07.2025
-
-
-
-
-
Host MCU
-
FW1.00
-
Build 07.07.2025
-
-
-
-
-
Wireless MCU
-
FW1.00
-
Build 01.06.2025
-
-
-
-
-
-
-
-
System Preferences
-
-
-
-
Time & Locale
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Device Configuration
-
-
-
-
Hardware Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Performance Settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
System Actions
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/Sidebar.astro b/src/components/Sidebar.astro
deleted file mode 100644
index 7b87434..0000000
--- a/src/components/Sidebar.astro
+++ /dev/null
@@ -1,246 +0,0 @@
----
-import ConnectionStatusBadge from "./ConnectionStatusBadge.tsx";
-import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
-import { $availableUpdates } from "./AppProviderStore.tsx";
-interface NavigationItem {
- path: string;
- title: string;
- icon: string;
- viewBox?: string;
-}
-
-// Laser section items
-const laserNavigationItems: NavigationItem[] = [
- {
- path: "/dashboard",
- title: "Dashboard",
- icon: '
',
- viewBox: "0 0 24 24",
- },
- {
- path: "/testbench",
- title: "Testbench",
- icon: '
',
- viewBox: "0 0 24 24",
- },
- {
- path: "/control",
- title: "Control",
- icon: '
',
- viewBox: "0 0 24 24",
- },
- {
- path: "/modulation",
- title: "Laser",
- icon: '
',
- viewBox: "0 0 24 24",
- },
- {
- path: "/dcdc",
- title: "Power",
- icon: '
',
- viewBox: "0 0 24 24",
- },
-
- {
- path: "/logs",
- title: "Logs",
- icon: '
',
- viewBox: "0 0 24 24",
- },
-];
-
-// System section items
-const systemNavigationItems: NavigationItem[] = [
- {
- path: "/network",
- title: "Network",
- icon: '
',
- viewBox: "0 0 24 24",
- },
- {
- path: "/settings",
- title: "Settings",
- icon: '
',
- viewBox: "0 0 24 24",
- },
- {
- path: "/security",
- title: "Security",
- icon: '
',
- viewBox: "0 0 24 24",
- },
-];
----
-
-
-
-
diff --git a/src/components/StationConfigModal.astro b/src/components/StationConfigModal.astro
new file mode 100644
index 0000000..bfd73a7
--- /dev/null
+++ b/src/components/StationConfigModal.astro
@@ -0,0 +1,944 @@
+---
+import IconEye from "../icons/IconEye.astro";
+import IconEyeClosed from "../icons/IconEyeClosed.astro";
+---
+
+
+
+
diff --git a/src/components/TestbenchConnectionStatusBadge.tsx b/src/components/TestbenchConnectionStatusBadge.tsx
deleted file mode 100644
index c47da24..0000000
--- a/src/components/TestbenchConnectionStatusBadge.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useStore } from '@nanostores/preact';
-import { $testbenchWebsocketConnectionStatus } from './TestbenchStore';
-
-export default function TestbenchConnectionStatusBadge() {
- const status = useStore($testbenchWebsocketConnectionStatus);
-
- let colorClass = 'bg-gray-400 text-white';
- if (status.startsWith('Connected')) colorClass = 'bg-green-500 text-white';
- else if (status.startsWith('Connecting')) colorClass = 'bg-yellow-400 text-black';
- else if (
- status.startsWith('Error') ||
- status.startsWith('Failed') ||
- status.startsWith('Disconnected')
- )
- colorClass = 'bg-red-500 text-white';
-
- // Passe die Breite ggf. an den lΓ€ngsten Status an!
- const maxText = 'Connecting ...';
-
- return (
-
- {/* Unsichtbarer Platzhalter fΓΌr feste Breite und Position */}
-
- {maxText}
-
- {/* Sichtbarer Status absolut darΓΌber */}
-
- {status}
-
-
- );
-}
diff --git a/src/components/TestbenchPage.astro b/src/components/TestbenchPage.astro
deleted file mode 100644
index 2cb64a9..0000000
--- a/src/components/TestbenchPage.astro
+++ /dev/null
@@ -1,692 +0,0 @@
----
-import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
-const pageTitle = "Test Bench Dashboard";
----
-
-
-
{pageTitle}
-
-
-
-
-
Board Connection Status
-
-
-
- β
- Disconnected
-
-
- VCC: 12V
-
-
- Testbench:
-
-
-
-
-
-
-
-
-
-
-
-
Power Control
-
- Board Relay:
-
-
-
-
GND Sense:
-
- β
- No Board Connected
-
-
-
-
-
-
-
-
-
-
-
-
Bootloader Control
-
-
-
-
-
-
-
-
-
-
-
Enable Control
-
-
-
-
-
-
-
-
-
-
-
-
-
UART Console
-
-
-
-
UART Console Ready
-
Waiting for data...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Number Conversion Utility
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/TestbenchStore.tsx b/src/components/TestbenchStore.tsx
deleted file mode 100644
index 9ca8c4d..0000000
--- a/src/components/TestbenchStore.tsx
+++ /dev/null
@@ -1,143 +0,0 @@
-import { atom } from 'nanostores';
-import pkg from "websocket";
-const { w3cwebsocket } = pkg;
-
-export const $testbenchWebsocket = atom
(null);
-export const $testbenchWebsocketConnectionStatus = atom('Disconnected');
-
-
-export function setTestbenchWebsocket(websocket: WebSocket) {
- $testbenchWebsocket.set(websocket);
-}
-
-export function setTestbenchWebsocketConnectionStatus(status: string) {
- $testbenchWebsocketConnectionStatus.set(status);
-}
-
-export const TESTBENCH_COMMANDS = {
- SET_UART_ROUTING_ST: 0,
- SET_UART_ROUTING_C6: 1,
- SET_UART_ROUTING_P4: 2,
- SET_P4_BOOTLOADER_ON: 3,
- SET_P4_BOOTLOADER_OFF: 4,
- SET_C6_BOOTLOADER_ON: 5,
- SET_C6_BOOTLOADER_OFF: 6,
- SET_P4_ENABLED: 7,
- SET_P4_DISABLED: 8,
- SET_C6_ENABLED: 9,
- SET_C6_DISABLED: 10,
- SET_ST_ENABLED: 11,
- SET_ST_DISABLED: 12,
- SET_POWER_RELAY_ON: 13,
- SET_POWER_RELAY_OFF: 14,
-};
-
-
-// GPIO Pin Mapping Constants
-export const GPIO_PINS = {
- // Communication
- SDA: 1,
- SCL: 2,
- DTR: 3,
- RTS: 14,
- FTDI_UART_TX: 17,
- FTDI_UART_RX: 18,
-
- // Switches
- SW_A: 4,
- SW_B: 5,
- SW_C: 6,
- SW_D: 7,
- // Route switches
- SW_ST_C6: 12,
- SW_C6_P4: 13,
-
- // Power & Sensing
- BOARD_POWER: 15,
- GND_SENSE: 16,
-
- // P4 Bootloader Pins
- P4_35: 47,
- P4_36: 21,
- P4_ENABLE: 9,
-
- // STM32
- STM32_RESET: 38,
-
- // C6 Control
- C6_ENABLE: 10,
- C6_BOOT: 8,
-} as const;
-
-// GPIO State Store
-export const $gpioStates = atom>({
- [GPIO_PINS.SW_A]: false,
- [GPIO_PINS.SW_B]: false,
- [GPIO_PINS.SW_C]: false,
- [GPIO_PINS.SW_D]: false,
- [GPIO_PINS.BOARD_POWER]: false,
- [GPIO_PINS.GND_SENSE]: true,
- [GPIO_PINS.P4_35]: true,
- [GPIO_PINS.P4_36]: false,
- [GPIO_PINS.STM32_RESET]: false,
- [GPIO_PINS.C6_ENABLE]: false,
- [GPIO_PINS.C6_BOOT]: true,
-});
-
-// Testbench State Type
-export type TestbenchState = {
- boardConnected: boolean;
- vccVoltage: number;
- powerRelayEnabled: boolean;
- uartRouting: 'st' | 'p4' | 'c6';
- p4BootloaderActive: boolean;
- c6BootloaderActive: boolean;
- p4Enabled: boolean;
- c6Enabled: boolean;
- stEnabled: boolean;
- gndSense: boolean;
-};
-
-// Testbench Control States
-export const $testbenchStates = atom({
- // Board Connection
- boardConnected: false,
- vccVoltage: 12,
-
- // Power Control
- powerRelayEnabled: false,
-
- // UART Routing
- uartRouting: 'p4',
-
- // Bootloader States
- p4BootloaderActive: false,
- c6BootloaderActive: false,
-
- // Enable States
- p4Enabled: false,
- c6Enabled: false,
- stEnabled: false,
- gndSense: true,
-});
-
-export function getGpioState(pin: number): boolean {
- return $gpioStates.get()[pin] || false;
-}
-
-export function sendCommandToBackend(command: number) {
- const websocket = $testbenchWebsocket.get();
- if (websocket && websocket.readyState === WebSocket.OPEN) {
- const message = {
- message_type: "testbench_command",
- command: command
- };
-
- try {
- websocket.send(JSON.stringify(message));
- console.log("Testbench command sent:", command);
- } catch (error) {
- console.error("Failed to send Testbench command:", error);
- }
- }
-}
\ No newline at end of file
diff --git a/src/components/TestbenchWebsocketProvider.tsx b/src/components/TestbenchWebsocketProvider.tsx
deleted file mode 100644
index dac2169..0000000
--- a/src/components/TestbenchWebsocketProvider.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import { useEffect } from 'preact/hooks';
-import { $gpioStates, $testbenchWebsocket, $testbenchWebsocketConnectionStatus, GPIO_PINS,
- setTestbenchWebsocketConnectionStatus, $testbenchStates } from './TestbenchStore';
-
-// Heartbeat configuration
-const HEARTBEAT_INTERVAL = 1500; // Send ping every 1 second
-const HEARTBEAT_TIMEOUT = 700; // Wait 1 second for pong response
-const MAX_MISSED_HEARTBEATS = 5; // Close connection after 5 missed pongs
-
-// Heartbeat state
-let heartbeatInterval: ReturnType | null = null;
-let heartbeatTimeout: ReturnType | null = null;
-let missedHeartbeats = 0;
-
-// Reconnect state
-let isReconnecting = false;
-let reconnectTimeout: ReturnType | null = null;
-
-// Restart detection state
-let disconnectDetected = false;
-
-
-function onMessage(message: MessageEvent) {
- //console.log("WebSocket message received:", message.data.toString());
- // Verarbeite JSON Nachrichten
- const dataFromServer = JSON.parse(message.data.toString());
- if (dataFromServer.message_type === "pong") {
- // reset missed heartbeats and clear pending timeout
- if (heartbeatTimeout) {
- clearTimeout(heartbeatTimeout);
- heartbeatTimeout = null;
- }
- missedHeartbeats = 0;
- console.log("Heartbeat pong received");
- }
- else if (dataFromServer.message_type === "initial_testbench_state_response" || dataFromServer.message_type === "testbench_state_response") {
- console.log("Testbench state response received");
- console.log("Testbench state:", dataFromServer);
- if (dataFromServer.gpio_states) {
-
- $gpioStates.set({
- [GPIO_PINS.SW_A]: dataFromServer.gpio_states["4"],
- [GPIO_PINS.SW_B]: dataFromServer.gpio_states["5"],
- [GPIO_PINS.SW_C]: dataFromServer.gpio_states["6"],
- [GPIO_PINS.SW_D]: dataFromServer.gpio_states["7"],
- [GPIO_PINS.BOARD_POWER]: dataFromServer.gpio_states["15"],
- [GPIO_PINS.GND_SENSE]: dataFromServer.gpio_states["16"],
- [GPIO_PINS.P4_35]: dataFromServer.gpio_states["47"],
- [GPIO_PINS.P4_36]: dataFromServer.gpio_states["21"],
- [GPIO_PINS.STM32_RESET]: dataFromServer.gpio_states["38"],
- [GPIO_PINS.C6_ENABLE]: dataFromServer.gpio_states["10"],
- [GPIO_PINS.C6_BOOT]: dataFromServer.gpio_states["8"],
- });
- $testbenchStates.set({
- boardConnected: dataFromServer.testbenchStates.boardConnected,
- vccVoltage: dataFromServer.testbenchStates.vccVoltage,
- powerRelayEnabled: dataFromServer.testbenchStates.powerRelayEnabled,
- uartRouting: dataFromServer.testbenchStates.uartRouting,
- p4BootloaderActive: dataFromServer.testbenchStates.p4BootloaderActive,
- c6BootloaderActive: dataFromServer.testbenchStates.c6BootloaderActive,
- p4Enabled: dataFromServer.testbenchStates.p4Enabled,
- c6Enabled: dataFromServer.testbenchStates.c6Enabled,
- stEnabled: dataFromServer.testbenchStates.stEnabled,
- gndSense: dataFromServer.testbenchStates.gndSense,
- });
- }
-
- }
-
-}
-
-const connectWebSocket = () => {
- // Clear any existing reconnect attempts
- clearReconnect();
-
- const testbenchWSaddress = "ws://testbench.local/ws";
- console.log(`Verbinde mit: ${testbenchWSaddress}`);
- if ($testbenchWebsocket.get() !== null) {
- ($testbenchWebsocket.get() as WebSocket).close(1000);
- $testbenchWebsocket.set(null);
- }
- $testbenchWebsocket.set(new WebSocket(testbenchWSaddress));
- if ($testbenchWebsocket.get() !== null) {
- ($testbenchWebsocket.get() as WebSocket).onopen = onOpen;
-
- ($testbenchWebsocket.get() as WebSocket).onmessage = onMessage;
-
- ($testbenchWebsocket.get() as WebSocket).onerror = onError;
-
- ($testbenchWebsocket.get() as WebSocket).onclose = onClose;
- }
-}
-
-function clearReconnect() {
- if (reconnectTimeout) {
- clearTimeout(reconnectTimeout);
- reconnectTimeout = null;
- }
- isReconnecting = false;
-}
-
-function onOpen() {
- setTestbenchWebsocketConnectionStatus("Connected");
- console.log("WebSocket erfolgreich verbunden");
- missedHeartbeats = 0;
-
- // Start heartbeat mechanism
- startHeartbeat();
- // Sende initiale Statusanfrage
- if ($testbenchWebsocket.get() !== null) {
- const initial_state_request = JSON.stringify({ message_type: "initial_testbench_state_request" });
- try {
- ($testbenchWebsocket.get() as WebSocket).send(initial_state_request);
- } catch (error) {
- console.error("Failed to send initial testbench state request:", error);
- }
- }
-}
-
-function onClose(event: CloseEvent) {
- $testbenchWebsocketConnectionStatus.set("Disconnected");
- console.log("Testbench WebSocket closed with code:", event.code);
- console.log("Testbench WebSocket close reason:", event.reason);
- console.log("Testbench WebSocket clean close:", event.wasClean);
- clearHeartbeat();
- if (event.code === 1000) {
- console.log("Normal closure");
- }
- scheduleReconnect();
-}
-
-function startHeartbeat() {
- clearHeartbeat(); // Clear any existing heartbeat
-
- heartbeatInterval = setInterval(() => {
- sendHeartbeat();
- }, HEARTBEAT_INTERVAL);
-}
-
-function clearHeartbeat() {
- if (heartbeatInterval) {
- clearInterval(heartbeatInterval);
- heartbeatInterval = null;
- }
- if (heartbeatTimeout) {
- clearTimeout(heartbeatTimeout);
- heartbeatTimeout = null;
- }
- missedHeartbeats = 0;
-}
-
-
-function sendHeartbeat() {
- if ($testbenchWebsocket.get() && ($testbenchWebsocket.get() as WebSocket).readyState === ($testbenchWebsocket.get() as WebSocket).OPEN) {
- // Send ping message
- const pingMessage = JSON.stringify({ message_type: "ping" });
- try {
- ($testbenchWebsocket.get() as WebSocket).send(pingMessage);
- } catch (error) {
- console.error("Failed to send heartbeat ping:", error);
- missedHeartbeats++;
- return;
- }
-
- // Set timeout for pong response
- if (heartbeatTimeout) {
- clearTimeout(heartbeatTimeout);
- heartbeatTimeout = null;
- }
- heartbeatTimeout = setTimeout(() => {
- missedHeartbeats++;
- if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
- console.error("Too many missed heartbeats, closing connection");
-
- // Wichtig: Erst Heartbeat stoppen
- clearHeartbeat();
-
- // Dann WebSocket schlieΓen und null setzen
-
- if ($testbenchWebsocket.get()) {
- ($testbenchWebsocket.get() as WebSocket).close();
- setTestbenchWebsocketConnectionStatus("Disconnected");
- $testbenchWebsocket.set(null);
- }
- }
- }, HEARTBEAT_TIMEOUT);
- }
-}
-
-function onError(error: Event) {
- $testbenchWebsocketConnectionStatus.set("Disconnected");
- console.log("WebSocket error:", error);
- clearHeartbeat();
- scheduleReconnect();
-}
-
-
-function scheduleReconnect() {
- if (isReconnecting) {
- console.log("Reconnect already scheduled, skipping");
- return;
- }
-
- isReconnecting = true;
- console.log("Scheduling reconnect in 1 second");
- reconnectTimeout = setTimeout(() => {
- connectWebSocket();
- isReconnecting = false;
- }, 1000);
-}
-
-export const TestbenchWebSocketProvider = () => {
-
- // Auto-connect WebSocket at first render
- useEffect(() => {
- console.log("Component mounted, starting WebSocket connection...");
- connectWebSocket();
- // Cleanup beim Unmounting
- return () => {
- clearHeartbeat();
- clearReconnect();
- ($testbenchWebsocket.get() as WebSocket).close();
- $testbenchWebsocket.set(null);
- $testbenchWebsocketConnectionStatus.set("Disconnected");
- };
- }, []);
-};
diff --git a/src/components/UpdateErrorPopup.tsx b/src/components/UpdateErrorPopup.tsx
deleted file mode 100644
index 0a99252..0000000
--- a/src/components/UpdateErrorPopup.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { useStore } from '@nanostores/preact';
-import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs } from '@tabler/icons-preact';
-import {
- $availableUpdates,
- $updateState,
- $updateErrorMessages,
- $p4FirmwareProgress,
- $p4WebinterfaceProgress,
- $stm32FirmwareProgress,
- $c6FirmwareProgress,
- stateSetters,
- start_firmware_update_p4_fw,
- start_firmware_update_p4_web,
- start_firmware_update_stm32_fw,
- start_firmware_update_c6_fw,
- type UpdateInfo,
- request_available_updates
-} from './AppProviderStore';
-
-interface UpdateErrorPopupProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-// Function to get the appropriate icon for each update type
-const getUpdateIcon = (core: string) => {
- switch (core.toLowerCase()) {
- case 'stm32':
- return (
-
- );
- case 'p4 firmware':
- return (
-
- );
- case 'c6':
- return (
-
- );
- default:
- return (
-
- );
- }
-};
-
-// Helper function to get progress for a specific update type
-const getProgressForUpdate = (core: string) => {
- switch (core.toLowerCase()) {
- case 'p4 firmware': return $p4FirmwareProgress.get();
- case 'p4 webinterface': return $p4WebinterfaceProgress.get();
- case 'stm32': return $stm32FirmwareProgress.get();
- case 'c6': return $c6FirmwareProgress.get();
- default: return [0, 0];
- }
-};
-
-// Map core types to their target indices
-const getTargetIndex = (core: string) => {
- switch (core.toLowerCase()) {
- case 'p4 webinterface': return 0; // P4 webinterface (first)
- case 'p4 firmware': return 1; // P4 firmware (second)
- case 'stm32': return 2; // STM32 firmware
- case 'c6': return 3; // C6 firmware (last)
- default: return -1;
- }
-};
-
-export default function UpdateErrorPopup({ isOpen, onClose }: UpdateErrorPopupProps) {
- const availableUpdates = useStore($availableUpdates);
- const updateState = useStore($updateState);
- const updateErrorMessages = useStore($updateErrorMessages);
-
- // Only show if update state is error
- if (!isOpen || updateState !== 'error') return null;
-
- const handleClose = () => {
- // Don't reset update state here
- onClose();
- // reset update status:
- stateSetters.resetUpdateStatus();
- // query available updates again!
- request_available_updates();
- };
-
- const retryUpdate = () => {
- // Reset state and start update again
- stateSetters.setUpdateState('updating');
- stateSetters.setUpdateErrorMessages({});
- stateSetters.setUpdateInProgress(false);
- stateSetters.setCurrentUpdateTargetIndex(0);
- stateSetters.setP4FirmwareProgress(0, 0);
- stateSetters.setP4WebinterfaceProgress(0, 0);
- stateSetters.setStm32FirmwareProgress(0, 0);
- stateSetters.setC6FirmwareProgress(0, 0);
-
- // Start the update process again
- const updatesToProcess = availableUpdates.filter(update => update.updateAvailable);
- if (updatesToProcess.length > 0) {
- const firstUpdate = updatesToProcess[0];
- switch (firstUpdate.core.toLowerCase()) {
- case 'stm32':
- start_firmware_update_stm32_fw();
- break;
- case 'p4 webinterface':
- start_firmware_update_p4_web();
- break;
- case 'p4 firmware':
- start_firmware_update_p4_fw();
- break;
- case 'c6':
- start_firmware_update_c6_fw();
- break;
- }
- }
- // close the popup
- onClose();
- };
-
- return (
-
- );
-}
diff --git a/src/components/UpdateSuccessPopup.tsx b/src/components/UpdateSuccessPopup.tsx
deleted file mode 100644
index d90a4b0..0000000
--- a/src/components/UpdateSuccessPopup.tsx
+++ /dev/null
@@ -1,135 +0,0 @@
-import { useStore } from '@nanostores/preact';
-import { useEffect, useState, useRef } from 'preact/hooks';
-import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs, IconRefresh } from '@tabler/icons-preact';
-import {
- $availableUpdates,
- $updateState,
- stateSetters,
- type UpdateInfo
-} from './AppProviderStore';
-
-interface UpdateSuccessPopupProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-// Function to get the appropriate icon for each update type
-const getUpdateIcon = (core: string) => {
- switch (core.toLowerCase()) {
- case 'stm32':
- return (
-
- );
- case 'p4 firmware':
- return (
-
- );
- case 'c6':
- return (
-
- );
- default:
- return (
-
- );
- }
-};
-
-export default function UpdateSuccessPopup({ isOpen, onClose }: UpdateSuccessPopupProps) {
- const availableUpdates = useStore($availableUpdates);
- const updateState = useStore($updateState);
- const [showRestarting, setShowRestarting] = useState(false);
-
- // Only show if explicitly opened via isOpen prop (don't auto-close based on updateState)
- if (!isOpen) return null;
-
- // Handle restarting state - just show animation, no timer
- useEffect(() => {
- console.log("π UpdateSuccessPopup useEffect triggered:", {
- updateState,
- isOpen,
- showRestarting
- });
-
- if (updateState === 'restarting') {
- console.log("π RESTARTING state detected - showing restart animation");
- setShowRestarting(true);
- } else {
- console.log("π Non-restarting state - hiding restart animation");
- setShowRestarting(false);
- }
- }, [updateState]);
-
- const handleClose = () => {
- // Don't reset update state here if we're restarting
- if (updateState !== 'restarting') {
- onClose();
- }
- // reset update status:
- stateSetters.resetUpdateStatus();
- };
-
- return (
-
- );
-}
diff --git a/src/components/UpdaterModal.tsx b/src/components/UpdaterModal.tsx
deleted file mode 100644
index 68b01bb..0000000
--- a/src/components/UpdaterModal.tsx
+++ /dev/null
@@ -1,472 +0,0 @@
-import { useState, useEffect } from 'preact/hooks';
-import { marked } from 'marked';
-import { useStore } from '@nanostores/preact';
-import { IconCpu2, IconWifi, IconCpu, IconFileTypeJs } from '@tabler/icons-preact';
-import {
- $availableUpdates,
- $updateChangelog,
- $updateInProgress,
- $p4FirmwareProgress,
- $p4WebinterfaceProgress,
- $stm32FirmwareProgress,
- $c6FirmwareProgress,
- $currentUpdateTargetIndex,
- $updateErrorMessages,
- $updateState,
- $updatedTargets,
- stateSetters,
- request_update_progress,
- start_firmware_update_p4_fw,
- start_firmware_update_p4_web,
- start_firmware_update_stm32_fw,
- start_firmware_update_c6_fw,
- type UpdateInfo,
- UpdateStatus,
- TargetUpdateStatus
-} from './AppProviderStore';
-
-// Configure marked with basic options
-marked.setOptions({
- breaks: true,
- gfm: true,
-});
-
-interface UpdaterModalProps {
- isOpen: boolean;
- onClose: () => void;
-}
-
-// Function to check if any updates are available
-const hasAvailableUpdates = (updates: UpdateInfo[]) => updates.some(update => update.updateAvailable);
-
-// Function to get the appropriate icon for each update type
-const getUpdateIcon = (core: string) => {
- switch (core.toLowerCase()) {
- case 'stm32':
- return (
-
- );
- case 'p4 firmware':
- return (
-
- );
- case 'c6':
- return (
-
- );
- default:
- return (
-
- );
- }
-};
-
-export default function UpdaterModal({ isOpen, onClose }: UpdaterModalProps) {
- const updateState = useStore($updateState);
- const updateInProgress = useStore($updateInProgress);
- const [currentUpdateIndex, setCurrentUpdateIndex] = useState(0);
- const [progress, setProgress] = useState(0);
-
- // Auto-close modal when updates are completed or failed
- useEffect(() => {
- console.log("π UpdaterModal useEffect triggered:", {
- updateState,
- isOpen
- });
-
- if (updateState === 'success' || updateState === 'error' || updateState === 'restarting') {
- if (isOpen) {
- console.log(`πͺ Closing updater modal due to state: '${updateState}'`);
- onClose();
- } else {
- console.log(`πͺ Updater modal already closed for state: '${updateState}'`);
- }
- } else {
- console.log("π Updater modal stays open - state:", updateState);
- }
- }, [updateState, onClose]); // Removed updateInProgress to prevent loop
-
- // Use useStore for reactive updates
- const availableUpdates = useStore($availableUpdates);
- const updateChangelog = useStore($updateChangelog);
- const currentUpdateTargetIndex = useStore($currentUpdateTargetIndex);
- const updateErrorMessages = useStore($updateErrorMessages);
-
- // Enhanced: Use updated_targets from server response via global state
- const updatedTargetsFromServer = useStore($updatedTargets);
-
- // Use reactive stores for all progress values
- const p4FirmwareProgress = useStore($p4FirmwareProgress);
- const p4WebinterfaceProgress = useStore($p4WebinterfaceProgress);
- const stm32FirmwareProgress = useStore($stm32FirmwareProgress);
- const c6FirmwareProgress = useStore($c6FirmwareProgress);
-
- // Helper function to get progress for a specific update type - now using reactive stores
- const getProgressForUpdate = (core: string) => {
- switch (core.toLowerCase()) {
- case 'p4 firmware': return p4FirmwareProgress;
- case 'p4 webinterface': return p4WebinterfaceProgress;
- case 'stm32': return stm32FirmwareProgress;
- case 'c6': return c6FirmwareProgress;
- default: return [0, 0];
- }
- };
-
- // Enhanced: Check if a target is completed using updated_targets array from server
- const isTargetCompletedFromServer = (core: string) => {
- const targetIndex = getTargetIndex(core);
- return targetIndex >= 0 && targetIndex < updatedTargetsFromServer.length
- ? updatedTargetsFromServer[targetIndex]
- : false;
- };
-
- // Map core types to their target indices
- const getTargetIndex = (core: string) => {
- const coreLower = core.toLowerCase();
- console.log(`π getTargetIndex called with: "${core}" -> "${coreLower}"`);
-
- switch (coreLower) {
- case 'p4 webinterface':
- console.log(`β
P4 webinterface -> index 1`);
- return 1; // P4 webinterface (second)
- case 'p4 firmware':
- console.log(`β
P4 firmware -> index 2`);
- return 2; // P4 firmware (third)
- case 'stm32':
- console.log(`β
STM32 -> index 0`);
- return 0; // STM32 firmware (first)
- case 'c6':
- console.log(`β
C6 -> index 3`);
- return 3; // C6 firmware (last)
- default:
- console.warn(`β Unknown core type: "${core}" -> "${coreLower}" -> index -1`);
- return -1;
- }
- };
-
- const startUpdate = async () => {
- stateSetters.setUpdateState('updating');
-
- const updatesToProcess = availableUpdates.filter(update => update.updateAvailable);
-
- for (let i = 0; i < updatesToProcess.length; i++) {
- setCurrentUpdateIndex(i);
- const currentUpdate = updatesToProcess[i];
-
- const updateTargetIndex = getTargetIndex(currentUpdate.core);
-
- // Start the appropriate update based on the core type
- switch (currentUpdate.core.toLowerCase()) {
- case 'stm32':
- start_firmware_update_stm32_fw();
- break;
- case 'p4 webinterface':
- start_firmware_update_p4_web();
- break;
- case 'p4 firmware':
- start_firmware_update_p4_fw();
- break;
- case 'c6':
- start_firmware_update_c6_fw();
- break;
- default:
- console.warn(`Unknown update type: ${currentUpdate.core}`);
- continue;
- }
-
- // Wait for the update to complete by monitoring the progress
- if (currentUpdate.hasProgressBar) {
- // For updates with progress bar, wait for completion
- let updateCompleted = false;
- let updateFailed = false;
-
- while (!updateCompleted && !updateFailed) {
- // Get progress for this specific update type - need to use .get() here for current values in async function
- const [currentProgress, status] = (() => {
- switch (currentUpdate.core.toLowerCase()) {
- case 'p4 firmware': return $p4FirmwareProgress.get();
- case 'p4 webinterface': return $p4WebinterfaceProgress.get();
- case 'stm32': return $stm32FirmwareProgress.get();
- case 'c6': return $c6FirmwareProgress.get();
- default: return [0, 0];
- }
- })();
-
- setProgress(currentProgress);
- // Check error status first
- if (status === UpdateStatus.ERROR) {
- updateFailed = true;
- console.error(`Update failed for ${currentUpdate.core}`);
- stateSetters.setUpdateState('error');
- return;
- } else if (status === UpdateStatus.SUCCESS) {
- updateCompleted = true;
- break;
- }
- // Fallback checks
- if (currentProgress === 100) {
- updateCompleted = true;
- }
- await new Promise(resolve => setTimeout(resolve, 100));
- }
- } else {
- // For updates without progress bar, show indeterminate progress for 3 seconds
- setProgress(0);
- await new Promise(resolve => setTimeout(resolve, 3000));
- }
- }
-
- // Only show success if we reached here (all updates completed)
- stateSetters.setUpdateState('success');
-
- // Clear all error messages on successful completion
- stateSetters.setUpdateErrorMessages({});
- };
-
- const resetModal = () => {
- stateSetters.setUpdateState('initial');
- setCurrentUpdateIndex(0);
- setProgress(0);
-
- stateSetters.setUpdateErrorMessages({});
- stateSetters.setUpdateInProgress(false);
- stateSetters.setCurrentUpdateTargetIndex(0);
- stateSetters.setP4FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
- stateSetters.setP4WebinterfaceProgress(0, TargetUpdateStatus.NO_UPDATE);
- stateSetters.setStm32FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
- stateSetters.setC6FirmwareProgress(0, TargetUpdateStatus.NO_UPDATE);
- stateSetters.setUpdaterModalOpen(false);
- onClose();
- };
-
- if (!isOpen) return null;
-
- return (
-
- );
-}
\ No newline at end of file
diff --git a/src/components/WebSocketAtom.ts b/src/components/WebSocketAtom.ts
new file mode 100644
index 0000000..9c312d9
--- /dev/null
+++ b/src/components/WebSocketAtom.ts
@@ -0,0 +1,3 @@
+import { atom } from "nanostores";
+
+export const $websocket = atom(null);
diff --git a/src/components/WebSocketProvider.tsx b/src/components/WebSocketProvider.tsx
index 8227859..e5ad77f 100644
--- a/src/components/WebSocketProvider.tsx
+++ b/src/components/WebSocketProvider.tsx
@@ -1,692 +1,255 @@
-import { useCallback, useEffect } from 'preact/hooks';
-import { $isUpdaterModalOpen, $isSuccessPopupOpen, $isErrorPopupOpen, $websocket, $wsConnectionStatus, $updateErrorMessages, $p4FirmwareProgress, $p4WebinterfaceProgress, $stm32FirmwareProgress, $c6FirmwareProgress, $updateState, $updatedTargets, $availableUpdates, type UpdateInfo, UpdateStatus, TargetUpdateStatus, TotalUpdateStatus } from './AppProviderStore';
-import { stateSetters, request_available_updates } from './AppProviderStore';
-import UpdaterModal from './UpdaterModal';
+import { useEffect } from "preact/hooks";
+import { $websocket } from "./WebSocketAtom";
+import { stateSetters } from "./AppProviderStore";
+import {
+ $scanned_wifi_networks,
+ $current_network_configuration,
+ $network_status_display,
+ $network_config_error,
+ request_network_config,
+ type WiFiNetwork,
+ authmodeToSecurity,
+ ConnectionMode,
+} from "./NetworkManagerStore";
-// Helper function to get target index for update types
-const getTargetIndex = (core: string) => {
- switch (core.toLowerCase()) {
- case 'p4 webinterface': return 1; // P4 webinterface (first)
- case 'p4 firmware': return 2; // P4 firmware (second)
- case 'stm32': return 0; // STM32 firmware
- case 'c6': return 3; // C6 firmware (last)
- default: return -1;
- }
-};
+export { $websocket } from "./WebSocketAtom";
// Heartbeat configuration
-const HEARTBEAT_INTERVAL = 1500; // Send ping every 1 second
-const HEARTBEAT_TIMEOUT = 700; // Wait 1 second for pong response
+const HEARTBEAT_INTERVAL = 1500; // Send ping every 1.5 seconds
+const HEARTBEAT_TIMEOUT = 1000; // Wait 1 second for pong response
const MAX_MISSED_HEARTBEATS = 5; // Close connection after 5 missed pongs
// Heartbeat state
let heartbeatInterval: ReturnType | null = null;
let heartbeatTimeout: ReturnType | null = null;
let missedHeartbeats = 0;
+let _lastPingSentAt = 0;
// Reconnect state
let isReconnecting = false;
let reconnectTimeout: ReturnType | null = null;
-let reload_site_after_webinterface_update = false;
-
-// Restart detection state
-let disconnectDetected = false;
-let heartbeatsAfterReconnect = 0;
-const HEARTBEATS_NEEDED_AFTER_RESTART = 3;
function clearHeartbeat() {
- if (heartbeatInterval) {
- clearInterval(heartbeatInterval);
- heartbeatInterval = null;
- }
- if (heartbeatTimeout) {
- clearTimeout(heartbeatTimeout);
- heartbeatTimeout = null;
- }
- missedHeartbeats = 0;
+ if (heartbeatInterval) {
+ clearInterval(heartbeatInterval);
+ heartbeatInterval = null;
+ }
+ if (heartbeatTimeout) {
+ clearTimeout(heartbeatTimeout);
+ heartbeatTimeout = null;
+ }
+ missedHeartbeats = 0;
}
function clearReconnect() {
- if (reconnectTimeout) {
- clearTimeout(reconnectTimeout);
- reconnectTimeout = null;
- }
- isReconnecting = false;
+ if (reconnectTimeout) {
+ clearTimeout(reconnectTimeout);
+ reconnectTimeout = null;
+ }
+ isReconnecting = false;
}
function scheduleReconnect() {
- if (isReconnecting) {
- ////console.log("Reconnect already scheduled, skipping");
- return;
- }
-
- isReconnecting = true;
- ////console.log("Scheduling reconnect in 1 second");
- reconnectTimeout = setTimeout(() => {
- connectWebSocket();
- isReconnecting = false;
- }, 1000);
+ if (isReconnecting) {
+ return;
+ }
+ isReconnecting = true;
+ reconnectTimeout = setTimeout(() => {
+ connectWebSocket();
+ isReconnecting = false;
+ }, 1000);
}
function sendHeartbeat() {
- if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
- // Send ping message
- const pingMessage = JSON.stringify({ message_type: "ping" });
- try {
- ($websocket.get() as WebSocket).send(pingMessage);
- } catch (error) {
- console.error("Failed to send heartbeat ping:", error);
- missedHeartbeats++;
- return;
- }
-
- // Set timeout for pong response
- if (heartbeatTimeout) {
- clearTimeout(heartbeatTimeout);
- heartbeatTimeout = null;
- }
- heartbeatTimeout = setTimeout(() => {
- missedHeartbeats++;
- if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
- console.error("Too many missed heartbeats, closing connection");
-
- // Check if we're in restarting state - this indicates device restart
- const currentUpdateState = $updateState.get();
- if (currentUpdateState === 'restarting') {
- //console.log("π Disconnect detected during restart - marking for restart detection");
- disconnectDetected = true;
- heartbeatsAfterReconnect = 0;
- }
-
- // Wichtig: Erst Heartbeat stoppen
- clearHeartbeat();
-
- // Dann WebSocket schlieΓen und null setzen
-
- if ($websocket.get()) {
- ($websocket.get() as WebSocket).close();
- stateSetters.setWebSocketConnectionStatus("Disconnected");
- $websocket.set(null);
- }
- }
- }, HEARTBEAT_TIMEOUT);
+ const ws = $websocket.get();
+ if (ws && (ws as WebSocket).readyState === (ws as WebSocket).OPEN) {
+ // Send ping message via WebSocket
+ const pingMessage = JSON.stringify({ message_type: "ping" });
+ try {
+ (ws as WebSocket).send(pingMessage);
+ _lastPingSentAt = performance.now();
+ } catch (error) {
+ missedHeartbeats++;
+ return;
}
+
+ // Set timeout for pong response
+ if (heartbeatTimeout) {
+ clearTimeout(heartbeatTimeout);
+ heartbeatTimeout = null;
+ }
+ heartbeatTimeout = setTimeout(() => {
+ missedHeartbeats++;
+ if (missedHeartbeats >= MAX_MISSED_HEARTBEATS) {
+ // First stop the heartbeat, then close + null the socket
+ clearHeartbeat();
+ const cur = $websocket.get();
+ if (cur) {
+ (cur as WebSocket).close();
+ stateSetters.setWebSocketConnectionStatus("Disconnected");
+ $websocket.set(null);
+ }
+ }
+ }, HEARTBEAT_TIMEOUT);
+ }
}
function startHeartbeat() {
- clearHeartbeat(); // Clear any existing heartbeat
-
- heartbeatInterval = setInterval(() => {
- sendHeartbeat();
- }, HEARTBEAT_INTERVAL);
+ clearHeartbeat(); // Clear any existing heartbeat
+ heartbeatInterval = setInterval(() => {
+ sendHeartbeat();
+ }, HEARTBEAT_INTERVAL);
}
function onOpen() {
- stateSetters.setWebSocketConnectionStatus("Connected");
- //console.log("WebSocket erfolgreich verbunden");
- missedHeartbeats = 0;
-
- // If we reconnected after detecting a restart, start counting heartbeats
- if (disconnectDetected) {
- //console.log("π Reconnected after restart - starting heartbeat counter");
- heartbeatsAfterReconnect = 0;
- }
-
- // Start heartbeat mechanism
- startHeartbeat();
- // Sende initiale Statusanfrage
- if ($websocket.get() !== null) {
- const initial_state_request = JSON.stringify({ message_type: "initial_state_request" });
- try {
- ($websocket.get() as WebSocket).send(initial_state_request);
- } catch (error) {
- console.error("Failed to send initial state request:", error);
- }
-
- // Request update status immediately to catch any missed completion messages
- setTimeout(() => {
- if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
- const update_status_request = JSON.stringify({ message_type: "request_update_status" });
- try {
- ($websocket.get() as WebSocket).send(update_status_request);
- //console.log("Requested update status on reconnection");
- } catch (error) {
- console.error("Failed to send update status request:", error);
- }
- }
- }, 500); // Reduced from 1000ms to 500ms for faster response
+ stateSetters.setWebSocketConnectionStatus("Connected");
+ missedHeartbeats = 0;
- // send request for available updates
- setTimeout(() => {
- if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
- request_available_updates();
- }
- }, 1500);
- }
+ // Start heartbeat mechanism
+ startHeartbeat();
+
+ // Ask the device for its current network config so the UI knows whether to show
+ // the first-time wizard or the configured overview.
+ setTimeout(() => {
+ request_network_config();
+ }, 100);
}
function onMessage(message: MessageEvent) {
- ////console.log("WebSocket message received:", message.data.toString());
- // Verarbeite JSON Nachrichten
- const dataFromServer = JSON.parse(message.data.toString());
- if (dataFromServer.message_type === "pong") {
- // reset missed heartbeats and clear pending timeout
- if (heartbeatTimeout) {
- clearTimeout(heartbeatTimeout);
- heartbeatTimeout = null;
- }
- missedHeartbeats = 0;
- //console.log("Heartbeat pong received");
-
- // Count heartbeats after reconnect for restart detection
- if (disconnectDetected) {
- heartbeatsAfterReconnect++;
- //console.log(`π Heartbeat ${heartbeatsAfterReconnect}/${HEARTBEATS_NEEDED_AFTER_RESTART} after restart`);
-
- if (heartbeatsAfterReconnect >= HEARTBEATS_NEEDED_AFTER_RESTART) {
- //console.log("π Device restart completed - closing success popup and resetting state");
- disconnectDetected = false;
- disconnectDetected = false;
- heartbeatsAfterReconnect = 0;
-
- // Close success popup and reset state
- stateSetters.setSuccessPopupOpen(false);
- stateSetters.setUpdateState('initial');
- if (reload_site_after_webinterface_update) {
- // Hard reload ohne Cache
- //window.location.reload();
- }
- }
- }
+ // Verarbeite JSON Nachrichten
+ const dataFromServer = JSON.parse(message.data.toString());
+
+ if (dataFromServer.message_type === "pong") {
+ // reset missed heartbeats and clear pending timeout
+ if (heartbeatTimeout) {
+ clearTimeout(heartbeatTimeout);
+ heartbeatTimeout = null;
}
- else if (dataFromServer.message_type === "state_update" || dataFromServer.message_type === "initial_state_response") {
-
- // If this is the initial state response, also request update status
- if (dataFromServer.message_type === "initial_state_response") {
- //console.log("π Initial state received, requesting update status...");
- setTimeout(() => {
- if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
- const update_status_request = JSON.stringify({ message_type: "request_update_status" });
- try {
- ($websocket.get() as WebSocket).send(update_status_request);
- //console.log("Requested update status after initial state");
- } catch (error) {
- console.error("Failed to send update status request:", error);
- }
- }
- }, 200); // Short delay to ensure connection is stable
- }
-
- if (dataFromServer.free_internal_heap) {
- stateSetters.setFreeInternalHeap(dataFromServer.free_internal_heap);
- }
- // DCDC State aktualisieren
- if (dataFromServer.dcdcStateMachine) {
- stateSetters.setDCDCState(dataFromServer.dcdcStateMachine.dcdc_state);
- stateSetters.setPOWERGOOD(dataFromServer.dcdcStateMachine.POWERGOOD);
- stateSetters.setDcdcErrorStatus(dataFromServer.dcdcStateMachine.error_flag);
- stateSetters.setREF_value(dataFromServer.dcdcStateMachine.ref_value);
- }
-
- // Laser State aktualisieren
- if (dataFromServer.laserStateMachine) {
- // Laser state update
- stateSetters.setLaserState(dataFromServer.laserStateMachine.laser_state);
- stateSetters.setModulationInputMethod(dataFromServer.laserStateMachine.laser_state);
- stateSetters.setREF_calibrated(dataFromServer.laserStateMachine.is_REF_calibrated);
- stateSetters.setHAS_DUTY_LOOKUP_TABLE(dataFromServer.laserStateMachine.has_duty_lookup_table);
- stateSetters.setLaserErrorStatus(dataFromServer.laserStateMachine.error_flag);
- stateSetters.setLASERGOOD(dataFromServer.laserStateMachine.LASERGOOD);
- stateSetters.setLaserSetTemp(dataFromServer.laserStateMachine.set_temp);
- stateSetters.setLaserMaxTemp(dataFromServer.laserStateMachine.max_temp);
- stateSetters.setInterlock(dataFromServer.laserStateMachine.interlock);
- stateSetters.setModulationInput(dataFromServer.laserStateMachine.modulationInput);
- stateSetters.setSetCurrent(dataFromServer.laserStateMachine.set_current);
- stateSetters.setMaxCurrent(dataFromServer.laserStateMachine.max_current);
- stateSetters.setThresholdCurrent(dataFromServer.laserStateMachine.threshold_current);
- stateSetters.setCurrentReading(dataFromServer.laserStateMachine.current_reading);
- stateSetters.setLaserTemp(dataFromServer.laserStateMachine.laser_temp);
- }
-
- // Driver State aktualisieren
- if (dataFromServer.driverStateMachine) {
- stateSetters.setDriverTemp(dataFromServer.driverStateMachine.driver_temp);
- stateSetters.setDriverMaxTemp(dataFromServer.driverStateMachine.max_temp);
- stateSetters.setVccVoltage(dataFromServer.driverStateMachine.vcc_voltage);
- stateSetters.setPdVoltage(dataFromServer.driverStateMachine.pd_voltage);
- }
- } else if (dataFromServer.message_type === "calibration_table") {
- // Log raw data to see the actual format of values
- //console.log("Raw calibration message:", message.data.toString().substring(0, 500) + "...");
- // Parse values and format to always show with decimal places
- const parsedValues = dataFromServer.values ?
- dataFromServer.values.map((val: any) => {
- // Convert to number and keep as float
- const numVal = Number(val);
-
- return numVal;
- }) : [];
-
- // Erstelle einen neuen Array mit korrekten Indizes
- // FΓΌlle Indizes 0-499 mit Nullen und fΓΌge dann die tatsΓ€chlichen Werte ein
- const minDuty = dataFromServer.min_duty || 500;
- const completedValues = [];
-
- // FΓΌlle Nullen fΓΌr Indizes von 0 bis (minDuty-1)
- for (let i = 0; i < minDuty; i++) {
- completedValues.push(0);
- }
- for (let i = 0; i < parsedValues.length; i++) {
- completedValues.push(parsedValues[i]);
- }
-
- // Store values in state - use the completed values with correct indices
- stateSetters.setCalibrationTableData(
- completedValues,
- minDuty,
- dataFromServer.max_duty || 3500, (dataFromServer.points_count || parsedValues.length) + minDuty
- );
- } else if (dataFromServer.message_type === "update_information") {
- //console.log("Update information received");
- //console.log("Raw update_information data:", dataFromServer);
- // Update Information
- if (dataFromServer.update_available) {
- const infos: UpdateInfo[] = [];
- if (dataFromServer.update_available.p4_firmware) {
- infos.push({
- core: 'p4 firmware',
- currentVersion: dataFromServer.update_available.p4_firmware.current_version,
- newVersion: dataFromServer.update_available.p4_firmware.new_version,
- icon: 'p4 firmware',
- hasProgressBar: true,
- updateAvailable: true,
- });
- //console.log("P4 Firmware update available");
- }
- if (dataFromServer.update_available.p4_webinterface) {
- infos.push({
- core: "p4 webinterface",
- currentVersion: dataFromServer.update_available.p4_webinterface.current_version,
- newVersion: dataFromServer.update_available.p4_webinterface.new_version,
- icon: "p4 webinterface",
- hasProgressBar: true,
- updateAvailable: true,
- });
- //console.log("P4 Webinterface update available");
- }
- if (dataFromServer.update_available.stm32_firmware) {
- infos.push({
- core: "STM32",
- currentVersion: dataFromServer.update_available.stm32_firmware.current_version,
- newVersion: dataFromServer.update_available.stm32_firmware.new_version,
- icon: "stm32",
- hasProgressBar: true,
- updateAvailable: true,
- });
- //console.log("STM32 Firmware update available");
- }
- if (dataFromServer.update_available.c6_firmware) {
- infos.push({
- core: "C6",
- currentVersion: dataFromServer.update_available.c6_firmware.current_version,
- newVersion: dataFromServer.update_available.c6_firmware.new_version,
- icon: "c6",
- hasProgressBar: true,
- updateAvailable: true,
- });
- //console.log("C6 Firmware update available");
- }
- stateSetters.setAvailableUpdates(infos);
- stateSetters.setUpdateChangelog(dataFromServer.changelog);
- }
- // Alte einzelne Progress-Message-Handler wurden durch complete_update_status ersetzt
-
- } else if (dataFromServer.message_type === "complete_update_status") {
- //console.log("π Complete update status received:", dataFromServer);
-
- // Check if we're currently in restarting state - if so, ignore status updates
- const currentUpdateState = $updateState.get() as any;
- if (currentUpdateState === 'restarting') {
- //console.log("π Ignoring complete update status during RESTARTING state - countdown in progress");
- return;
- }
-
- // Process the complete status information for all targets
- if (dataFromServer.targets) {
- const targets = dataFromServer.targets;
-
- // STM32 Status
- if (targets.stm32) {
- const status = targets.stm32.status;
- const progress = targets.stm32.progress || 0;
- const updateAvailable = targets.stm32.update_available || false;
-
- // Convert string status to TargetUpdateStatus enum
- let statusEnum = TargetUpdateStatus.NO_UPDATE;
- switch (status) {
- case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
- case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
- case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
- case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- }
-
- stateSetters.setStm32FirmwareProgress(progress, statusEnum);
- //console.log(`π STM32: status=${status}, progress=${progress}, available=${updateAvailable}`);
- }
-
- // P4 Webinterface Status
- if (targets.p4_webinterface) {
- const status = targets.p4_webinterface.status;
- const progress = targets.p4_webinterface.progress || 0;
- const updateAvailable = targets.p4_webinterface.update_available || false;
-
- let statusEnum = TargetUpdateStatus.NO_UPDATE;
- switch (status) {
- case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
- case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
- case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
- case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- }
-
- stateSetters.setP4WebinterfaceProgress(progress, statusEnum);
- //console.log(`π P4 Webinterface: status=${status}, progress=${progress}, available=${updateAvailable}`);
- }
-
- // P4 Firmware Status
- if (targets.p4_firmware) {
- const status = targets.p4_firmware.status;
- const progress = targets.p4_firmware.progress || 0;
- const updateAvailable = targets.p4_firmware.update_available || false;
-
- let statusEnum = TargetUpdateStatus.NO_UPDATE;
- switch (status) {
- case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
- case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
- case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
- case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- }
-
- stateSetters.setP4FirmwareProgress(progress, statusEnum);
- //console.log(`π P4 Firmware: status=${status}, progress=${progress}, available=${updateAvailable}`);
- }
-
- // C6 Status
- if (targets.c6) {
- const status = targets.c6.status;
- const progress = targets.c6.progress || 0;
- const updateAvailable = targets.c6.update_available || false;
-
- let statusEnum = TargetUpdateStatus.NO_UPDATE;
- switch (status) {
- case 'waiting': statusEnum = TargetUpdateStatus.WAITING; break;
- case 'started': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'in_progress': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- case 'completed': statusEnum = TargetUpdateStatus.COMPLETED; break;
- case 'error': statusEnum = TargetUpdateStatus.ERROR; break;
- case 'no_update': statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- case 'restarting': statusEnum = TargetUpdateStatus.IN_PROGRESS; break;
- default: statusEnum = TargetUpdateStatus.NO_UPDATE; break;
- }
-
- stateSetters.setC6FirmwareProgress(progress, statusEnum);
- //console.log(`π C6: status=${status}, progress=${progress}, available=${updateAvailable}`);
- }
- }
-
- // Check if there's an ongoing update
- if (dataFromServer.update_in_progress) {
- //console.log("π Update in progress detected, setting total_update_status to updating");
- stateSetters.setUpdateInProgress(true);
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
-
- // Use the total_update_status from backend if available
- if (dataFromServer.total_update_status !== undefined) {
- const totalStatus = dataFromServer.total_update_status;
- //console.log("π Backend total_update_status:", totalStatus);
-
- switch (totalStatus) {
- case 0: // INITIAL
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
- break;
- case 1: // STARTED
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.STARTED);
- break;
- case 2: // UPDATING
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
- break;
- case 3: // SUCCESS
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
- break;
- case 4: // ERROR
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.ERROR);
- break;
- case 5: // RESTARTING
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.RESTARTING);
- break;
- default:
- console.warn("Unknown total_update_status:", totalStatus);
- break;
- }
- }
-
- // Open the updater modal, if it is not already open and no success/error popups are showing
- const isModalOpen = $isUpdaterModalOpen.get();
- const isSuccessPopupOpen = $isSuccessPopupOpen.get();
- const isErrorPopupOpen = $isErrorPopupOpen.get();
- /*console.log("π― Modal decision matrix:", {
- isModalOpen,
- totalUpdateStatus: currentUpdateState,
- isSuccessPopupOpen,
- isErrorPopupOpen,
- shouldOpen: !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen && currentUpdateState !== 'success' && currentUpdateState !== 'restarting'
- });*/
-
- // Don't open updater modal if success/error popups are open or we're in success/restarting state
- const shouldOpenModal = !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen &&
- currentUpdateState !== 'success' && currentUpdateState !== 'restarting';
- if (shouldOpenModal) {
- //console.log("π Opening updater modal - conditions met");
- stateSetters.setUpdaterModalOpen(true);
- } else {
- //console.log("π« Skipping updater modal - conditions not met");
- }
- } else {
- //console.log("β
No update in progress, checking if all available updates are completed");
-
- // Check if all targets with update_available: true are completed
- let allAvailableUpdatesCompleted = true;
- let hasAnyAvailableUpdates = false;
-
- if (dataFromServer.targets.stm32 && dataFromServer.targets.stm32.update_available) {
- hasAnyAvailableUpdates = true;
- if (dataFromServer.targets.stm32.status !== 'completed') {
- allAvailableUpdatesCompleted = false;
- }
- }
- if (dataFromServer.targets.p4_webinterface && dataFromServer.targets.p4_webinterface.update_available) {
- hasAnyAvailableUpdates = true;
- if (dataFromServer.targets.p4_webinterface.status !== 'completed') {
- allAvailableUpdatesCompleted = false;
- }
- }
- if (dataFromServer.targets.p4_firmware && dataFromServer.targets.p4_firmware.update_available) {
- hasAnyAvailableUpdates = true;
- if (dataFromServer.targets.p4_firmware.status !== 'completed') {
- allAvailableUpdatesCompleted = false;
- }
- }
- if (dataFromServer.targets.c6 && dataFromServer.targets.c6.update_available) {
- hasAnyAvailableUpdates = true;
- if (dataFromServer.targets.c6.status !== 'completed') {
- allAvailableUpdatesCompleted = false;
- }
- }
-
- if (hasAnyAvailableUpdates && allAvailableUpdatesCompleted) {
- //console.log("π All available updates completed successfully, setting total_update_status to success");
- stateSetters.setUpdateInProgress(false);
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
- } else if (!hasAnyAvailableUpdates) {
- //console.log("βΉοΈ No updates available, resetting total_update_status to initial");
- stateSetters.setUpdateInProgress(false);
- if (currentUpdateState !== 'success' && currentUpdateState !== 'error' && currentUpdateState !== 'restarting') {
- stateSetters.setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
- }
- } else {
- //console.log("β³ Some available updates still in progress, keeping current total_update_status");
- }
- }
-
- } else if (dataFromServer.message_type === "response_update_status") {
- console.log("π Update status response received:", dataFromServer);
-
- // Check if we're currently in restarting state - if so, ignore status updates
- const currentUpdateState = $updateState.get() as any;
- if (currentUpdateState === 'restarting') {
- //console.log("π Ignoring update status during RESTARTING state - countdown in progress");
- return;
- }
-
- // Always process the complete status information, regardless of update_in_progress
- //console.log("π Processing complete update status for all targets");
-
- // Set progress and status for each update type if provided
- if (dataFromServer.p4_firmware_progress !== undefined) {
- const status = dataFromServer.p4_firmware_status || 0;
- stateSetters.setP4FirmwareProgress(dataFromServer.p4_firmware_progress, status);
- //console.log(`π P4 Firmware: progress=${dataFromServer.p4_firmware_progress}, status=${status}`);
- }
- if (dataFromServer.p4_webinterface_progress !== undefined) {
- const status = dataFromServer.p4_webinterface_status || 0;
- stateSetters.setP4WebinterfaceProgress(dataFromServer.p4_webinterface_progress, status);
- //console.log(`π P4 Webinterface: progress=${dataFromServer.p4_webinterface_progress}, status=${status}`);
- }
- if (dataFromServer.stm32_firmware_progress !== undefined) {
- const status = dataFromServer.stm32_firmware_status || 0;
- stateSetters.setStm32FirmwareProgress(dataFromServer.stm32_firmware_progress, status);
- //console.log(`π STM32 Firmware: progress=${dataFromServer.stm32_firmware_progress}, status=${status}`);
- }
- if (dataFromServer.c6_firmware_progress !== undefined) {
- const status = dataFromServer.c6_firmware_status || 0;
- stateSetters.setC6FirmwareProgress(dataFromServer.c6_firmware_progress, status);
- //console.log(`π C6 Firmware: progress=${dataFromServer.c6_firmware_progress}, status=${status}`);
- }
-
- // Store updated_targets array from server for UpdaterModal
- if (dataFromServer.updated_targets && Array.isArray(dataFromServer.updated_targets)) {
- stateSetters.setUpdatedTargets(dataFromServer.updated_targets);
- //console.log("π― Updated targets:", dataFromServer.updated_targets);
- }
-
- // Set current target if provided
- if (dataFromServer.current_target !== undefined) {
- stateSetters.setCurrentUpdateTargetIndex(dataFromServer.current_target);
- //console.log("π― Current target:", dataFromServer.current_target);
- }
-
- // Check if there's an ongoing update
- if (dataFromServer.update_in_progress) {
- //console.log("π Update in progress detected, setting state to updating");
- stateSetters.setUpdateInProgress(true);
- stateSetters.setUpdateState("updating");
-
- // Open the updater modal, if it is not already open and no success/error popups are showing
- const isModalOpen = $isUpdaterModalOpen.get();
- const isSuccessPopupOpen = $isSuccessPopupOpen.get();
- const isErrorPopupOpen = $isErrorPopupOpen.get();
- /*console.log("π― Modal decision matrix:", {
- isModalOpen,
- updateState: currentUpdateState,
- isSuccessPopupOpen,
- isErrorPopupOpen,
- shouldOpen: !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen && currentUpdateState !== 'success' && currentUpdateState !== 'restarting'
- });*/
-
- // Don't open updater modal if success/error popups are open or we're in success/restarting state
- const shouldOpenModal = !isModalOpen && !isSuccessPopupOpen && !isErrorPopupOpen &&
- currentUpdateState !== 'success' && currentUpdateState !== 'restarting';
- if (shouldOpenModal) {
- //console.log("π Opening updater modal - conditions met");
- stateSetters.setUpdaterModalOpen(true);
- } else {
- //console.log("π« Skipping updater modal - conditions not met");
- }
- }
-
- } else {
- // Unbekannter Nachrichtentyp β korrektes Feld protokollieren
- console.warn(`[${new Date().toISOString()}] Unbekannter Nachrichtentyp: ${dataFromServer.message_type ?? 'undefined'}`);
- console.debug('Payload:', dataFromServer);
+ missedHeartbeats = 0;
+ if ((window as any).__wsDebug && _lastPingSentAt > 0)
+ console.log(`[WS] pong RTT=${(performance.now() - _lastPingSentAt).toFixed(1)}ms`);
+ } else if (dataFromServer.message_type === "wifi_networks_response") {
+ const networks: WiFiNetwork[] = (dataFromServer.networks ?? []).map((n: any) => ({
+ SSID: n.ssid ?? "",
+ RSSI: n.rssi ?? 0,
+ Channel: n.channel ?? 0,
+ SecurityType: authmodeToSecurity(n.authmode ?? 0),
+ }));
+ $scanned_wifi_networks.set(networks);
+ } else if (dataFromServer.message_type === "network_status_update") {
+ $network_status_display.set({
+ Connection_Mode: dataFromServer.Connection_Mode ?? ConnectionMode.CONNECTION_MODE_UNKNOWN,
+ AP_Channel: dataFromServer.AP_Channel ?? 0,
+ IP_Address_Station: dataFromServer.IP_Address_Station ?? "",
+ Gateway_IP_Address_Station: dataFromServer.Gateway_IP_Address_Station ?? "",
+ Netmask_Station: dataFromServer.Netmask_Station ?? "",
+ MDNS_Hostname: dataFromServer.MDNS_Hostname ?? "",
+ MAC_Address_WiFi: dataFromServer.MAC_Address_WiFi ?? "",
+ rssi: dataFromServer.rssi ?? null,
+ });
+ } else if (dataFromServer.message_type === "network_config_response") {
+ console.log("[WS] network_config_response received:", dataFromServer);
+ $current_network_configuration.set({
+ Connection_Mode: dataFromServer.Connection_Mode ?? ConnectionMode.CONNECTION_MODE_UNKNOWN,
+ DHCP_Mode_Station: dataFromServer.DHCP_Mode_Station ?? true,
+ AP_Channel: dataFromServer.AP_Channel ?? 0,
+ IP_Address_Station: dataFromServer.IP_Address_Station ?? "",
+ Gateway_IP_Address_Station: dataFromServer.Gateway_IP_Address_Station ?? "",
+ Netmask_Station: dataFromServer.Netmask_Station ?? "",
+ Static_Cfg_IP_Address_Station: dataFromServer.Static_Cfg_IP_Address_Station ?? "",
+ Static_Cfg_Gateway_IP_Address_Station: dataFromServer.Static_Cfg_Gateway_IP_Address_Station ?? "",
+ Static_Cfg_Netmask_Station: dataFromServer.Static_Cfg_Netmask_Station ?? "",
+ SSID_Station: dataFromServer.SSID_Station ?? "",
+ Password_Station: dataFromServer.Password_Station ?? "",
+ SSID_AP: dataFromServer.SSID_AP ?? "",
+ Password_AP: dataFromServer.Password_AP ?? "",
+ MDNS_Hostname: dataFromServer.MDNS_Hostname ?? "hydrobuddy",
+ });
+ // Update MAC in status display β config response is the first message that carries it
+ if (dataFromServer.MAC_Address_WiFi) {
+ $network_status_display.set({
+ ...$network_status_display.get(),
+ MAC_Address_WiFi: dataFromServer.MAC_Address_WiFi || $network_status_display.get().MAC_Address_WiFi,
+ });
}
+ console.log("[WS] $current_network_configuration updated:", $current_network_configuration.get());
+ } else if (dataFromServer.message_type === "network_config_failed") {
+ const failedSsid: string = dataFromServer.failed_ssid ?? "";
+ const reason: string = dataFromServer.reason ?? "unknown";
+ const connectionMode: ConnectionMode = dataFromServer.connection_mode ?? ConnectionMode.CONNECTION_MODE_UNKNOWN;
+ console.warn(`[WS] network_config_failed: ssid='${failedSsid}' reason='${reason}'`);
+ $network_config_error.set({ reason, failed_ssid: failedSsid, connection_mode: connectionMode });
+ } else if (dataFromServer.message_type === "error_popup") {
+ console.warn(`[error_popup] ${dataFromServer.error ?? dataFromServer.message ?? "(no detail)"}`);
+ } else {
+ // Unbekannter Nachrichtentyp - korrektes Feld protokollieren
+ console.warn(`[${new Date().toISOString()}] Unbekannter Nachrichtentyp: ${dataFromServer.message_type ?? "undefined"}`);
+ console.debug("Payload:", dataFromServer);
+ }
}
function onError(error: Event) {
- $wsConnectionStatus.set("Disconnected");
- //console.log("WebSocket error:", error);
- clearHeartbeat();
- scheduleReconnect();
+ stateSetters.setWebSocketConnectionStatus("Disconnected");
+ console.log("WebSocket error:", error);
+ clearHeartbeat();
+ scheduleReconnect();
}
function onClose(event: CloseEvent) {
- $wsConnectionStatus.set("Disconnected");
- //console.log("WebSocket closed with code:", event.code);
- //console.log("Close reason:", event.reason);
- //console.log("Clean close:", event.wasClean);
- clearHeartbeat();
- if (event.code === 1000) {
- //console.log("Normal closure");
- }
- scheduleReconnect();
+ stateSetters.setWebSocketConnectionStatus("Disconnected");
+ console.log("WebSocket closed with code:", event.code);
+ console.log("Close reason:", event.reason);
+ console.log("Clean close:", event.wasClean);
+ clearHeartbeat();
+ if (event.code === 1000) {
+ console.log("Normal closure");
+ }
+ scheduleReconnect();
+}
+
+function getWsAddress(): string {
+ // Derive WS address from the page's origin so it works with any hostname or IP.
+ const h = window.location.hostname;
+ if (h === "localhost" || h === "127.0.0.1") {
+ return (import.meta.env.PUBLIC_DEV_WS_HOST as string | undefined) ?? "ws://hydrobuddy.local/ws";
+ }
+ return `ws://${h}/ws`;
}
const connectWebSocket = () => {
- // Clear any existing reconnect attempts
- clearReconnect();
- clearHeartbeat();
+ // Clear any existing reconnect attempts
+ clearReconnect();
+ clearHeartbeat();
- const wsAddress = `ws://hydrobuddy.local/ws`;
- //console.log(`Verbinde mit: ${wsAddress}`);
- if ($websocket.get() !== null) {
- ($websocket.get() as WebSocket).close(1000);
- $websocket.set(null);
- }
- $websocket.set(new WebSocket(wsAddress));
- if ($websocket.get() !== null) {
- ($websocket.get() as WebSocket).onopen = onOpen;
-
- ($websocket.get() as WebSocket).onmessage = onMessage;
-
- ($websocket.get() as WebSocket).onerror = onError;
-
- ($websocket.get() as WebSocket).onclose = onClose;
- }
-}
+ const wsAddress = getWsAddress();
+ console.log(`Verbinde mit: ${wsAddress}`);
+ if ($websocket.get() !== null) {
+ ($websocket.get() as WebSocket).close(1000);
+ $websocket.set(null);
+ }
+ $websocket.set(new WebSocket(wsAddress));
+ const ws = $websocket.get();
+ if (ws !== null) {
+ (ws as WebSocket).onopen = onOpen;
+ (ws as WebSocket).onmessage = onMessage;
+ (ws as WebSocket).onerror = onError;
+ (ws as WebSocket).onclose = onClose;
+ }
+};
export const WebSocketProvider = () => {
-
- // Auto-connect WebSocket at first render
- useEffect(() => {
- //console.log("Component mounted, starting WebSocket connection...");
- connectWebSocket();
- // Cleanup beim Unmounting
- return () => {
- clearHeartbeat();
- clearReconnect();
- ($websocket.get() as WebSocket).close();
- $websocket.set(null);
- $wsConnectionStatus.set("Disconnected");
- };
- }, []);
+ // Auto-connect WebSocket at first render
+ useEffect(() => {
+ console.log("Component mounted, starting WebSocket connection...");
+ connectWebSocket();
+ // Cleanup beim Unmounting
+ return () => {
+ clearHeartbeat();
+ clearReconnect();
+ const cur = $websocket.get();
+ if (cur) (cur as WebSocket).close();
+ $websocket.set(null);
+ stateSetters.setWebSocketConnectionStatus("Disconnected");
+ };
+ }, []);
};
diff --git a/src/components/WifiSetupWizard.astro b/src/components/WifiSetupWizard.astro
new file mode 100644
index 0000000..e20310d
--- /dev/null
+++ b/src/components/WifiSetupWizard.astro
@@ -0,0 +1,508 @@
+---
+import { IconPlant } from "@tabler/icons-preact";
+---
+
+
+
+
+
+
+
Let's get you online
+
+
+ Three quick steps and your HydroBuddy will be on the network.
+
+
+
+
+ - Name
+ - WiFi
+ - Connect
+
+
+
+
+
+
What should we call your device?
+
+ Pick a name β you'll reach the web interface at http://<name>.local
+ from any device on the same network.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Pick a WiFi network
+
Scan and tap the network you want to join.
+
+
+
+
+
+
+
+ Press Scan to discover nearby networks.
+
+
+
+
+
+
+ Hidden network? Type the name manually
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Connect to -
+
Enter the network password. Leave empty for open networks.
+
+
+
+
+
+ Advanced: Static IP (optional)
+
+
+
+
+
+ When you apply, the device will reboot its WiFi and briefly disconnect. You'll need to switch your
+ phone/laptop to the same network and reopen http://hydrobuddy.local.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/helper_scripts/IP_Subnetmask_validation.tsx b/src/components/helper_scripts/IP_Subnetmask_validation.tsx
new file mode 100644
index 0000000..1b85b0c
--- /dev/null
+++ b/src/components/helper_scripts/IP_Subnetmask_validation.tsx
@@ -0,0 +1,74 @@
+
+export function is_valid_ip_format(ip_str: string) {
+ if (
+ /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(
+ ip_str
+ )
+ ) {
+ return true;
+ }
+ return false;
+}
+
+// Convert IP address string to 32-bit integer
+export function ipToNumber(ip: string): number {
+ const parts = ip.split(".").map(Number);
+ return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3];
+}
+
+// Validate that netmask is valid (contiguous 1s followed by 0s)
+export function isValidNetmask(netmask: string): boolean {
+ if (!is_valid_ip_format(netmask)) return false;
+
+ const maskNum = ipToNumber(netmask);
+ // Check if mask is valid: all 1s followed by all 0s
+ // Invert the mask, add 1, and check if result is a power of 2
+ const inverted = ~maskNum >>> 0; // Use unsigned right shift to handle 32-bit
+ const isValid = (inverted & (inverted + 1)) === 0;
+
+ // Also check that mask is not all 0s or all 1s
+ return isValid && maskNum !== 0 && maskNum !== 0xFFFFFFFF;
+}
+
+// Check if netmask is practical for network use (has space for at least IP + Gateway)
+// Returns true if netmask allows at least 2 host addresses
+export function isPracticalNetmask(netmask: string): boolean {
+ if (!isValidNetmask(netmask)) return false;
+
+ const maskNum = ipToNumber(netmask);
+ const inverted = ~maskNum >>> 0;
+ const hostBits = Math.log2(inverted + 1);
+
+ // Need at least 2 host addresses (IP + Gateway), so hostBits must be >= 1
+ // /32 (255.255.255.255) has 0 host bits - not practical
+ // /31 (255.255.255.254) has 1 host bit but only 2 addresses total (network + broadcast) - not practical for IP + Gateway
+ // /30 (255.255.255.252) has 2 host bits = 4 addresses (network + 2 hosts + broadcast) - minimum practical
+ return hostBits >= 2;
+}
+
+// Calculate network address from IP and netmask
+export function getNetworkAddress(ip: string, netmask: string): number {
+ return ipToNumber(ip) & ipToNumber(netmask);
+}
+
+// Calculate broadcast address from IP and netmask
+export function getBroadcastAddress(ip: string, netmask: string): number {
+ const networkAddr = getNetworkAddress(ip, netmask);
+ const invertedMask = ~ipToNumber(netmask) >>> 0;
+ return networkAddr | invertedMask;
+}
+
+// Check if IP is in valid host range (not network address, not broadcast address)
+export function isInValidHostRange(ip: string, netmask: string): boolean {
+ const ipNum = ipToNumber(ip);
+ const networkAddr = getNetworkAddress(ip, netmask);
+ const broadcastAddr = getBroadcastAddress(ip, netmask);
+
+ // IP must not be network address (all host bits 0) or broadcast address (all host bits 1)
+ return ipNum !== networkAddr && ipNum !== broadcastAddr;
+}
+
+// Check if two IPs are in the same subnet
+export function isInSameSubnet(ip1: string, ip2: string, netmask: string): boolean {
+ return getNetworkAddress(ip1, netmask) === getNetworkAddress(ip2, netmask);
+}
\ No newline at end of file
diff --git a/src/components/helper_scripts/daisy_toasts.tsx b/src/components/helper_scripts/daisy_toasts.tsx
new file mode 100644
index 0000000..f2bc3b9
--- /dev/null
+++ b/src/components/helper_scripts/daisy_toasts.tsx
@@ -0,0 +1,140 @@
+function showToast(message: string, type: "info" | "success" | "warning" | "error" = "info") {
+ // Determine the correct parent: if a