Initial commit

This commit is contained in:
SebastianOtt 2026-05-21 20:41:03 +02:00
commit b7a3b14e06
41 changed files with 27493 additions and 0 deletions

Binary file not shown.

Binary file not shown.

1
src/assets/astro.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" width="115" height="48"><path fill="#17191E" d="M7.78 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="url(#a)" d="M7.77 36.35C6.4 35.11 6 32.51 6.57 30.62c.99 1.2 2.35 1.57 3.75 1.78 2.18.33 4.31.2 6.33-.78.23-.12.44-.27.7-.42.18.55.23 1.1.17 1.67a4.56 4.56 0 0 1-1.94 3.23c-.43.32-.9.61-1.34.91-1.38.94-1.76 2.03-1.24 3.62l.05.17a3.63 3.63 0 0 1-1.6-1.38 3.87 3.87 0 0 1-.63-2.1c0-.37 0-.74-.05-1.1-.13-.9-.55-1.3-1.33-1.32a1.56 1.56 0 0 0-1.63 1.26c0 .06-.03.12-.05.2Z"/><path fill="#17191E" d="M.02 30.31s4.02-1.95 8.05-1.95l3.04-9.4c.11-.45.44-.76.82-.76.37 0 .7.31.82.76l3.04 9.4c4.77 0 8.05 1.95 8.05 1.95L17 11.71c-.2-.56-.53-.91-.98-.91H7.83c-.44 0-.76.35-.97.9L.02 30.31Zm42.37-5.97c0 1.64-2.05 2.62-4.88 2.62-1.85 0-2.5-.45-2.5-1.41 0-1 .8-1.49 2.65-1.49 1.67 0 3.09.03 4.73.23v.05Zm.03-2.04a21.37 21.37 0 0 0-4.37-.36c-5.32 0-7.82 1.25-7.82 4.18 0 3.04 1.71 4.2 5.68 4.2 3.35 0 5.63-.84 6.46-2.92h.14c-.03.5-.05 1-.05 1.4 0 1.07.18 1.16 1.06 1.16h4.15a16.9 16.9 0 0 1-.36-4c0-1.67.06-2.93.06-4.62 0-3.45-2.07-5.64-8.56-5.64-2.8 0-5.9.48-8.26 1.19.22.93.54 2.83.7 4.06 2.04-.96 4.95-1.37 7.2-1.37 3.11 0 3.97.71 3.97 2.15v.57Zm11.37 3c-.56.07-1.33.07-2.12.07-.83 0-1.6-.03-2.12-.1l-.02.58c0 2.85 1.87 4.52 8.45 4.52 6.2 0 8.2-1.64 8.2-4.55 0-2.74-1.33-4.09-7.2-4.39-4.58-.2-4.99-.7-4.99-1.28 0-.66.59-1 3.65-1 3.18 0 4.03.43 4.03 1.35v.2a46.13 46.13 0 0 1 4.24.03l.02-.55c0-3.36-2.8-4.46-8.2-4.46-6.08 0-8.13 1.49-8.13 4.39 0 2.6 1.64 4.23 7.48 4.48 4.3.14 4.77.62 4.77 1.28 0 .7-.7 1.03-3.71 1.03-3.47 0-4.35-.48-4.35-1.47v-.13Zm19.82-12.05a17.5 17.5 0 0 1-6.24 3.48c.03.84.03 2.4.03 3.24l1.5.02c-.02 1.63-.04 3.6-.04 4.9 0 3.04 1.6 5.32 6.58 5.32 2.1 0 3.5-.23 5.23-.6a43.77 43.77 0 0 1-.46-4.13c-1.03.34-2.34.53-3.78.53-2 0-2.82-.55-2.82-2.13 0-1.37 0-2.65.03-3.84 2.57.02 5.13.07 6.64.11-.02-1.18.03-2.9.1-4.04-2.2.04-4.65.07-6.68.07l.07-2.93h-.16Zm13.46 6.04a767.33 767.33 0 0 1 .07-3.18H82.6c.07 1.96.07 3.98.07 6.92 0 2.95-.03 4.99-.07 6.93h5.18c-.09-1.37-.11-3.68-.11-5.65 0-3.1 1.26-4 4.12-4 1.33 0 2.28.16 3.1.46.03-1.16.26-3.43.4-4.43-.86-.25-1.81-.41-2.96-.41-2.46-.03-4.26.98-5.1 3.38l-.17-.02Zm22.55 3.65c0 2.5-1.8 3.66-4.64 3.66-2.81 0-4.61-1.1-4.61-3.66s1.82-3.52 4.61-3.52c2.82 0 4.64 1.03 4.64 3.52Zm4.71-.11c0-4.96-3.87-7.18-9.35-7.18-5.5 0-9.23 2.22-9.23 7.18 0 4.94 3.49 7.59 9.21 7.59 5.77 0 9.37-2.65 9.37-7.6Z"/><defs><linearGradient id="a" x1="6.33" x2="19.43" y1="40.8" y2="34.6" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="1024" fill="none"><path fill="url(#a)" fill-rule="evenodd" d="M-217.58 475.75c91.82-72.02 225.52-29.38 341.2-44.74C240 415.56 372.33 315.14 466.77 384.9c102.9 76.02 44.74 246.76 90.31 366.31 29.83 78.24 90.48 136.14 129.48 210.23 57.92 109.99 169.67 208.23 155.9 331.77-13.52 121.26-103.42 264.33-224.23 281.37-141.96 20.03-232.72-220.96-374.06-196.99-151.7 25.73-172.68 330.24-325.85 315.72-128.6-12.2-110.9-230.73-128.15-358.76-12.16-90.14 65.87-176.25 44.1-264.57-26.42-107.2-167.12-163.46-176.72-273.45-10.15-116.29 33.01-248.75 124.87-320.79Z" clip-rule="evenodd" style="opacity:.154"/><path fill="url(#b)" fill-rule="evenodd" d="M1103.43 115.43c146.42-19.45 275.33-155.84 413.5-103.59 188.09 71.13 409 212.64 407.06 413.88-1.94 201.25-259.28 278.6-414.96 405.96-130 106.35-240.24 294.39-405.6 265.3-163.7-28.8-161.93-274.12-284.34-386.66-134.95-124.06-436-101.46-445.82-284.6-9.68-180.38 247.41-246.3 413.54-316.9 101.01-42.93 207.83 21.06 316.62 6.61Z" clip-rule="evenodd" style="opacity:.154"/><defs><linearGradient id="b" x1="373" x2="1995.44" y1="1100" y2="118.03" gradientUnits="userSpaceOnUse"><stop stop-color="#D83333"/><stop offset="1" stop-color="#F041FF"/></linearGradient><linearGradient id="a" x1="107.37" x2="1130.66" y1="1993.35" y2="1026.31" gradientUnits="userSpaceOnUse"><stop stop-color="#3245FF"/><stop offset="1" stop-color="#BC52EE"/></linearGradient></defs></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,689 @@
import { atom } from 'nanostores';
import pkg from "websocket";
const { w3cwebsocket } = pkg;
// Update Status Enum - matches backend enum in update_notifier.h
export enum UpdateStatus {
ERROR = -1,
STARTED = 0,
IN_PROGRESS = 1,
SUCCESS = 2,
NO_UPDATE = 3
}
export const $websocket = atom<WebSocket | null>(null);
export const $activePage = atom<string>('dashboard');
export const $freeInternalHeap = atom<number>(375 * 1024);
export const $totalInternalHeap = atom<number>(375 * 1024); // ESP32-P4 has 375KB internal heap (heap_caps_get_info mit MALLOC_CAP_INTERNAL)
export const $modulationInput = atom<number>(0);
export const $inputVoltage = atom<number>(0);
export const $driverTemp = atom<number>(0);
export const $counter = atom<number>(0);
export const $ref = atom<number>(0);
export const $dcdcState = atom<number>(0);
export const $dcdc_errorStatus = atom<string>('--');
export const $idCountDashboard = atom<number>(2);
export const $wsConnectionStatus = atom<string>('Disconnected');
export const $modulationInputMethod = atom<string>('--');
export const $gpioState = atom<{ [key: number]: boolean }>({
4: false,
5: false,
6: false,
7: false,
8: false,
9: false,
});
export const $validGpioKeys = atom<number[]>([4, 5, 6, 7, 8, 9]);
/* DCDC */
export const $dcdc_state = atom<number>(0);
export const $dcdc_state_string = atom<string>('--');
export const $dcdc_state_desc_string = atom<string>('--');
export const $dcdc_error_flag = atom<number>(0);
export const $POWERGOOD = atom<number>(0);
export const $POWERGOOD_string = atom<string>('--');
export const $ref_value = atom<number>(0);
/* LASER */
export const $laser_state = atom<number>(2); // LASER_STOP
export const $laser_state_string = atom<string>('--');
export const $laser_state_desc_string = atom<string>('--');
export const $is_REF_calibrated = atom<number>(0);
export const $has_duty_lookup_table = atom<number>(0);
export const $laser_error_flag = atom<number>(0);
export const $laser_error_flag_string = atom<string>('--');
export const $LASERGOOD = atom<number>(0);
export const $LASERGOOD_string = atom<string>('--');
export const $laser_set_temp = atom<number>(0);
export const $laser_max_temp = atom<number>(0);
export const $interlock = atom<number>(0);
export const $set_current = atom<number>(0);
export const $max_current = atom<number>(0);
export const $threshold_current = atom<number>(0);
export const $current_reading = atom<number>(0);
export const $current_reading_string = atom<string>('--');
export const $laser_temp = atom<number>(0);
export const $laser_temp_string = atom<string>('--');
/* TEMPERATURE CHART DATA */
export const $temperature_chart_data = atom<{
timestamps: string[];
driverTemps: number[];
laserTemps: number[];
}>({
timestamps: [],
driverTemps: [],
laserTemps: []
});
/* DRIVER */
export const $driver_temp = atom<number>(0);
export const $driver_temp_string = atom<string>('--');
export const $driver_max_temp = atom<number>(0);
export const $driver_max_temp_string = atom<string>('--');
export const $vcc_voltage = atom<number>(0);
export const $vcc_voltage_string = atom<string>('--');
export const $pd_voltage = atom<number>(0);
export const $pd_voltage_string = atom<string>('--');
/* CALIBRATION TABLE */
export const $calibration_table_data = atom<{
values: number[];
min_duty: number;
max_duty: number;
points_count: number;
}>
({
values: [],
min_duty: 0,
max_duty: 0,
points_count: 0,
});
/* UPDATE SYSTEM */
export interface UpdateInfo {
core: string;
currentVersion: string;
newVersion: string;
icon: string;
hasProgressBar: boolean;
updateAvailable: boolean;
}
// Neue Status-Definitionen
export enum TargetUpdateStatus {
WAITING = 'waiting',
IN_PROGRESS = 'in_progress',
COMPLETED = 'completed',
ERROR = 'error',
NO_UPDATE = 'no_update'
}
export enum TotalUpdateStatus {
INITIAL = 'initial',
STARTED = 'started',
UPDATING = 'updating',
SUCCESS = 'success',
ERROR = 'error',
RESTARTING = 'restarting'
}
export const $availableUpdates = atom<UpdateInfo[]>([]);
export const $updateChangelog = atom<string>('');
export const $updateInProgress = atom<boolean>(false);
// Separate progress stores for each update type
// [progress, status] where status uses UpdateStatus enum
export const $p4FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
export const $p4WebinterfaceProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
export const $stm32FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
export const $c6FirmwareProgress = atom<[number, TargetUpdateStatus]>([0, TargetUpdateStatus.NO_UPDATE]); // [progress, TargetUpdateStatus]
// Neuer Store für den Gesamt-Update-Status
export const $totalUpdateStatus = atom<TotalUpdateStatus>(TotalUpdateStatus.INITIAL);
export const $currentUpdateTargetIndex = atom<number>(0);
export const $updateErrorMessages = atom<{ [key: number]: string }>({});
export const $updatedTargets = atom<boolean[]>([false, false, false, false]);
// Add update state to global store
export const $updateState = atom<'initial' | 'updating' | 'success' | 'error' | 'reloading' | 'restarting'>('initial');
// Global modal state
export const $isUpdaterModalOpen = atom<boolean>(false);
export const $isSuccessPopupOpen = atom<boolean>(false);
export const $isErrorPopupOpen = atom<boolean>(false);
export const getDcdcStateString = (state: number): string => {
switch (state) {
case 0: return "IDLE";
case 1: return "INIT";
case 2: return "START";
case 3: return "RUN";
case 4: return "STOP";
case 5: return "ERROR";
default: return "UNKNOWN";
}
};
export const getDcdcStateDescString = (state: number): string => {
switch (state) {
case 0: return "DCDC is idle";
case 1: return "Initializing Voltage ramping";
case 2: return "Ramping to target voltage";
case 3: return "DCDC is running";
case 4: return "DCDC is stopped";
case 5: return "DCDC is in error state"; // TODO: add error description
default: return "UNKNOWN";
}
};
export const getLaserStateString = (state: number): string => {
switch (state) {
case 0: return "INTERNAL MODULATION";
case 1: return "EXTERNAL MODULATION";
case 2: return "STOP";
case 3: return "ERROR";
case 4: return "REF CALIBRATION";
case 5: return "LASER CALIBRATION";
default: return "UNKNOWN";
}
};
export const getLaserStateDescString = (state: number): string => {
switch (state) {
case 0: return "Internal laser modulation running";
case 1: return "External laser modulation running";
case 2: return "Laser stopped";
case 3: return "Laser in error state"; // TODO: add error description
case 4: return "Calibrating REF valie";
case 5: return "Calibrating laser";
default: return "Unknown laser state";
}
};
const setDCDCState = (state: number) => {
$dcdc_state.set(state);
$dcdc_state_string.set(getDcdcStateString(state));
$dcdc_state_desc_string.set(getDcdcStateDescString(state));
}
const setLaserState = (state: number) => {
$laser_state.set(state);
$laser_state_string.set(getLaserStateString(state));
$laser_state_desc_string.set(getLaserStateDescString(state));
}
const setPOWERGOOD = (state: number) => {
$POWERGOOD.set(state);
$POWERGOOD_string.set(state ? "Yes" : "No");
}
const setLASERGOOD = (state: number) => {
$LASERGOOD.set(state);
$LASERGOOD_string.set(state ? "Yes" : "No");
}
const setDcdcErrorStatus = (state: number) => {
$dcdc_error_flag.set(state);
$dcdc_errorStatus.set(state ? "Error" : "No Errors");
}
const setLaserErrorStatus = (state: number) => {
$laser_error_flag.set(state);
$laser_error_flag_string.set(state ? "Error" : "No Errors");
}
const setREF_value = (state: number) => {
$ref_value.set(state);
}
const setModulationInputMethod = (state: number) => {
$modulationInputMethod.set(state === 0 ? "INTERNAL MODULATION" : state === 1 ? "EXTERNAL MODULATION" : "---");
}
const setREF_calibrated = (state: number) => {
$is_REF_calibrated.set(state);
}
const setHAS_DUTY_LOOKUP_TABLE = (state: number) => {
$has_duty_lookup_table.set(state);
}
const setLaserSetTemp = (state: number) => {
$laser_set_temp.set(state);
}
const setLaserMaxTemp = (state: number) => {
$laser_max_temp.set(state);
}
const setInterlock = (state: number) => {
$interlock.set(state);
}
const setModulationInput = (state: number) => {
$modulationInput.set(state);
}
const setSetCurrent = (state: number) => {
$set_current.set(state);
}
const setMaxCurrent = (state: number) => {
$max_current.set(state);
}
const setThresholdCurrent = (state: number) => {
$threshold_current.set(state);
}
const setCurrentReading = (state: number) => {
$current_reading.set(state);
$current_reading_string.set(state + " mA");
}
const setLaserTemp = (state: number) => {
$laser_temp.set(state);
$laser_temp_string.set(state + " °C");
}
const setDriverTemp = (state: number) => {
$driver_temp.set(state);
$driver_temp_string.set(state + " °C");
}
const setDriverMaxTemp = (state: number) => {
$driver_max_temp.set(state);
$driver_max_temp_string.set(state + " °C");
}
// Temperature chart data management
const maxTemperatureDataPoints = 300; // 5 minutes worth of data at 1 second intervals
export const addTemperatureDataPoint = (driverTemp: number, laserTemp: number) => {
const currentData = $temperature_chart_data.get();
const now = new Date();
const timeString = now.toLocaleTimeString();
const newData = {
timestamps: [...currentData.timestamps, timeString],
driverTemps: [...currentData.driverTemps, driverTemp],
laserTemps: [...currentData.laserTemps, laserTemp]
};
// Remove old data points to maintain 5-minute window
if (newData.timestamps.length > maxTemperatureDataPoints) {
newData.timestamps = newData.timestamps.slice(-maxTemperatureDataPoints);
newData.driverTemps = newData.driverTemps.slice(-maxTemperatureDataPoints);
newData.laserTemps = newData.laserTemps.slice(-maxTemperatureDataPoints);
}
$temperature_chart_data.set(newData);
}
export const clearTemperatureData = () => {
$temperature_chart_data.set({
timestamps: [],
driverTemps: [],
laserTemps: []
});
}
const setVccVoltage = (state: number) => {
$vcc_voltage.set(state);
$vcc_voltage_string.set(state + " V");
}
const setPdVoltage = (state: number) => {
$pd_voltage.set(state);
$pd_voltage_string.set(state + " V");
}
const setCalibrationTableData = (values: number[], min_duty: number, max_duty: number, points_count: number) => {
$calibration_table_data.set({
values: values,
min_duty: min_duty,
max_duty: max_duty,
points_count: points_count
});
}
const setAvailableUpdates = (updates: UpdateInfo[]) => {
$availableUpdates.set(updates);
}
const setUpdateChangelog = (changelog: string) => {
$updateChangelog.set(changelog);
}
const setUpdateInProgress = (inProgress: boolean) => {
$updateInProgress.set(inProgress);
}
const setP4FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
$p4FirmwareProgress.set([progress, status]);
}
const setP4WebinterfaceProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
$p4WebinterfaceProgress.set([progress, status]);
}
const setStm32FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
$stm32FirmwareProgress.set([progress, status]);
}
const setC6FirmwareProgress = (progress: number, status: TargetUpdateStatus = TargetUpdateStatus.NO_UPDATE) => {
$c6FirmwareProgress.set([progress, status]);
}
const setTotalUpdateStatus = (status: TotalUpdateStatus) => {
console.log(`🎯 Total update status change: ${$totalUpdateStatus.get()} → ${status}`);
$totalUpdateStatus.set(status);
}
const startUpdate = () => {
console.log(`🚀 Starting update process`);
setTotalUpdateStatus(TotalUpdateStatus.STARTED);
}
const setCurrentUpdateTargetIndex = (index: number) => {
$currentUpdateTargetIndex.set(index);
}
const setUpdateErrorMessages = (errorMessages: { [key: number]: string }) => {
$updateErrorMessages.set(errorMessages);
}
const setUpdateState = (state: 'initial' | 'updating' | 'success' | 'error' | 'restarting') => {
console.log(`🎯 State change: ${$updateState.get()} → ${state}`);
$updateState.set(state);
// Synchronize with total update status
switch (state) {
case 'initial':
setTotalUpdateStatus(TotalUpdateStatus.INITIAL);
break;
case 'updating':
setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
break;
case 'success':
setTotalUpdateStatus(TotalUpdateStatus.SUCCESS);
break;
case 'error':
setTotalUpdateStatus(TotalUpdateStatus.ERROR);
break;
case 'restarting':
setTotalUpdateStatus(TotalUpdateStatus.UPDATING);
break;
}
}
const setUpdaterModalOpen = (open: boolean) => {
console.log(`🚀 Setting updater modal: ${open ? 'OPEN' : 'CLOSED'}`);
$isUpdaterModalOpen.set(open);
}
// reset all states to offiline states, when websocket is disconnected
const setWebSocketConnectionStatus = (status: string) => {
$wsConnectionStatus.set(status);
// set states to offline states! But do not reset calibration table/update states etc.
if (status === "Disconnected") {
$modulationInput.set(0);
$inputVoltage.set(0);
$driverTemp.set(0);
$counter.set(0);
$ref.set(0);
$dcdcState.set(0);
$dcdc_errorStatus.set('--');
$modulationInputMethod.set('--');
$gpioState.set({
4: false,
5: false,
6: false,
7: false,
8: false,
9: false,
});
/* DCDC */
$dcdc_state.set(0);
$dcdc_state_string.set('--');
$dcdc_state_desc_string.set('--');
$dcdc_error_flag.set(0);
$POWERGOOD.set(0);
$POWERGOOD_string.set('--');
$ref_value.set(0);
/* LASER */
$laser_state.set(2); // LASER_STOP
$laser_state_string.set('--');
$laser_state_desc_string.set('--');
$is_REF_calibrated.set(0);
$has_duty_lookup_table.set(0);
$laser_error_flag.set(0);
$laser_error_flag_string.set('--');
$LASERGOOD.set(0);
$LASERGOOD_string.set('--');
$laser_set_temp.set(0);
$laser_max_temp.set(0);
$interlock.set(0);
$set_current.set(0);
$max_current.set(0);
$threshold_current.set(0);
$current_reading.set(0);
$current_reading_string.set('--');
$laser_temp.set(0);
$laser_temp_string.set('--');
/* TEMPERATURE CHART DATA */
clearTemperatureData();
/* DRIVER */
$driver_temp.set(0);
$driver_temp_string.set('--');
$driver_max_temp.set(0);
$driver_max_temp_string.set('--');
$vcc_voltage.set(0);
$vcc_voltage_string.set('--');
$pd_voltage.set(0);
$pd_voltage_string.set('--');
}
}
const resetUpdateStatus = () => {
console.log("Resetting update status");
$updateState.set('initial');
$updateInProgress.set(false);
$updateErrorMessages.set({});
$currentUpdateTargetIndex.set(0);
$p4FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
$p4WebinterfaceProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
$stm32FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
$c6FirmwareProgress.set([0, TargetUpdateStatus.NO_UPDATE]);
$availableUpdates.set([]);
$updateChangelog.set('');
$updatedTargets.set([false, false, false, false]);
}
const setErrorPopupOpen = (isOpen: boolean) => {
console.log(`❌ Setting error popup: ${isOpen ? 'OPEN' : 'CLOSED'}`);
$isErrorPopupOpen.set(isOpen);
}
const setSuccessPopupOpen = (isOpen: boolean) => {
console.log(`🎉 Setting success popup: ${isOpen ? 'OPEN' : 'CLOSED'}`);
$isSuccessPopupOpen.set(isOpen);
}
const setFreeInternalHeap = (heap: number) => {
$freeInternalHeap.set(heap);
}
const setUpdatedTargets = (targets: boolean[]) => {
console.log("🎯 Setting updated targets:", targets);
$updatedTargets.set(targets);
}
export const stateSetters = {
setWebSocketConnectionStatus,
setDCDCState,
setLaserState,
setPOWERGOOD,
setLASERGOOD,
setDcdcErrorStatus,
setLaserErrorStatus,
setREF_value,
setModulationInputMethod,
setREF_calibrated,
setHAS_DUTY_LOOKUP_TABLE,
setLaserSetTemp,
setLaserMaxTemp,
setInterlock,
setModulationInput,
setSetCurrent,
setMaxCurrent,
setThresholdCurrent,
setCurrentReading,
setLaserTemp,
setDriverTemp,
setDriverMaxTemp,
setVccVoltage,
setPdVoltage,
setCalibrationTableData,
setAvailableUpdates,
setUpdateChangelog,
setUpdateInProgress,
setP4FirmwareProgress,
setP4WebinterfaceProgress,
setStm32FirmwareProgress,
setC6FirmwareProgress,
setTotalUpdateStatus,
startUpdate,
setCurrentUpdateTargetIndex,
setUpdateErrorMessages,
setUpdateState,
setUpdaterModalOpen,
setSuccessPopupOpen,
setErrorPopupOpen,
resetUpdateStatus,
setFreeInternalHeap,
setUpdatedTargets
};
const send_uart_cmd_without_data = (command: number) => {
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
const buffer = new Uint8Array([0xAB, command]);
$websocket.get()?.send(buffer);
} else {
console.error("WebSocket ist nicht verbunden");
}
};
const send_uart_cmd_with_data = (command: number, data: number) => {
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
const buffer = new Uint8Array(6);
buffer[0] = 0xAA;
buffer[1] = command;
buffer[2] = (data >> 24) & 0xFF;
buffer[3] = (data >> 16) & 0xFF;
buffer[4] = (data >> 8) & 0xFF;
buffer[5] = data & 0xFF;
$websocket.get()?.send(buffer);
}
};
const send_network_cmd = (command: number) => {
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
const buffer = new Uint8Array([0xBA, command]);
$websocket.get()?.send(buffer);
} else {
console.error("WebSocket ist nicht verbunden");
}
};
const send_firmware_update_cmd = (command: number) => {
if ($websocket.get() !== null && $websocket.get()?.readyState === $websocket.get()?.OPEN) {
const buffer = new Uint8Array([0xBB, command]);
$websocket.get()?.send(buffer);
} else {
console.error("WebSocket ist nicht verbunden");
}
}
export const start_DCDC = () => {
send_uart_cmd_without_data(0x00);
}
export const stop_DCDC = () => {
send_uart_cmd_without_data(0x01);
}
export const reset_DCDC = () => {
send_uart_cmd_without_data(0x02);
}
export const run_Laser_External = () => {
send_uart_cmd_without_data(0x03);
}
export const run_Laser_Internal = () => {
send_uart_cmd_without_data(0x04);
}
export const stop_Laser = () => {
send_uart_cmd_without_data(0x05);
}
export const reset_Laser = () => {
send_uart_cmd_without_data(0x06);
}
export const start_REF_Calibration = () => {
send_uart_cmd_without_data(0x07);
}
export const start_Laser_Calibration = () => {
send_uart_cmd_without_data(0x08);
}
export const set_Laser_Threshold = (threshold: number) => {
send_uart_cmd_with_data(0x09, threshold);
}
export const set_Laser_MAX_Current = (current: number) => {
send_uart_cmd_with_data(0x0A, current);
}
export const set_Laser_Set_Current = (current: number) => {
send_uart_cmd_with_data(0x0B, current);
}
export const set_Laser_Set_Temp = (temp: number) => {
send_uart_cmd_with_data(0x0C, temp);
}
export const set_Laser_Max_Temp = (temp: number) => {
send_uart_cmd_with_data(0x0D, temp);
}
export const set_Driver_Max_Temp = (temp: number) => {
send_uart_cmd_with_data(0x0E, temp);
}
export const get_calibration_table = () => {
console.log("Getting calibration table");
send_uart_cmd_without_data(0x0F);
}
export const set_REF_value = (value: number) => {
send_uart_cmd_with_data(0x10, value);
}
export const set_Overcurrent_Protection_Value = (value: number) => {
send_uart_cmd_with_data(0x11, value);
}
// UPDATE SYSTEM COMMANDS
export const request_available_updates = () => {
console.log("Requesting available updates");
send_firmware_update_cmd(0x00);
}
export const start_firmware_update_p4_fw = () => {
console.log("Starting P4 Firmware update");
send_firmware_update_cmd(0x02);
}
export const start_firmware_update_p4_web = () => {
console.log("Starting P4 Webinterface update");
send_firmware_update_cmd(0x03);
}
export const start_firmware_update_stm32_fw = () => {
console.log("Starting STM32 Firmware update");
send_firmware_update_cmd(0x04);
}
export const start_firmware_update_c6_fw = () => {
console.log("Starting C6 Firmware update");
send_firmware_update_cmd(0x05);
}
export const request_update_progress = () => {
console.log("Requesting update progress");
}

View file

@ -0,0 +1,45 @@
import { useStore } from '@nanostores/preact';
import { $wsConnectionStatus } from './AppProviderStore';
export default function ConnectionStatusBadge() {
const status = useStore($wsConnectionStatus);
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 (
<span
class={`badge ${colorClass} font-mono relative inline-block transition-all duration-200`}
style={{ width: '120px', minWidth: '120px', maxWidth: '120px', padding: 0 }}
>
{/* Unsichtbarer Platzhalter für feste Breite und Position */}
<span
class="opacity-0 select-none pointer-events-none"
style={{
display: 'inline-block',
width: '100%',
textAlign: 'left',
}}
>
{maxText}
</span>
{/* Sichtbarer Status absolut darüber */}
<span
class="absolute left-0 top-0 w-full h-full flex items-center"
style={{ justifyContent: 'left', paddingLeft: '0.75rem' /* ggf. anpassen */ }}
>
{status}
</span>
</span>
);
}

View file

@ -0,0 +1,234 @@
---
const pageTitle = "Laser Control";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
<!-- Control Panel -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- DCDC Controls -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">DCDC Control</h2>
<!-- Mode Selection -->
<div class="join w-full mb-4">
<button class="btn join-item flex-1" id="dcdc-start">Start</button>
<button class="btn join-item flex-1 btn-success" id="dcdc-run">Run</button>
<button class="btn join-item flex-1 btn-error" id="dcdc-stop">Stop</button>
<button class="btn join-item flex-1 btn-error error" id="dcdc-error">Error</button>
</div>
<div class="divider"></div>
<div class="form-control">
<label class="label">
<span class="label-text">Target Voltage (V)</span>
<span class="label-text-alt" id="voltage-display">25V</span>
</label>
<input type="range" min="10" max="50" value="25" class="range range-primary" step="1" id="voltage-slider" />
<div class="w-full flex justify-between text-xs px-2 mt-1">
<span>10V</span>
<span>20V</span>
<span>30V</span>
<span>40V</span>
<span>50V</span>
</div>
</div>
<div class="flex gap-4 mt-6">
<div class="badge badge-success gap-2 p-4">
<span class="font-semibold">Input Voltage:</span> <span id="input-voltage">24.5V</span>
</div>
<div class="badge badge-error gap-2 p-4">
<span class="font-semibold">PD:</span> <span id="pd-voltage">3.2V</span>
</div>
</div>
<div class="mt-6 flex gap-2">
<button class="btn btn-primary" id="apply-dcdc">Apply Settings</button>
<button class="btn btn-outline" id="reset-dcdc">Reset</button>
</div>
</div>
</div>
<!-- Laser Control -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Laser Control</h2>
<div class="join w-full mb-4">
<button class="btn join-item flex-1" id="laser-start">Start</button>
<button class="btn join-item flex-1 btn-success" id="laser-run">Run</button>
<button class="btn join-item flex-1 btn-error" id="laser-stop">Stop</button>
<button class="btn join-item flex-1 btn-error error" id="laser-error">Error</button>
</div>
<div class="divider"></div>
<div class="flex justify-between items-center mb-4">
<span class="text-lg">Interlock Status:</span>
<div class="badge badge-success gap-2 p-4" id="interlock-status">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 10.5V6.75a4.5 4.5 0 1 1 9 0v3.75M3.75 21.75h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H3.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>
<span>Unlocked</span>
</div>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Response Time</span>
</label>
<select class="select select-bordered w-full" id="response-time">
<option>Fast (High Bandwidth)</option>
<option selected>Normal</option>
<option>Slow (Low Noise)</option>
</select>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Stability Mode</span>
</label>
<div class="join w-full">
<button class="btn join-item flex-1 btn-active" id="stability-normal">Normal</button>
<button class="btn join-item flex-1" id="stability-high">High Stability</button>
<button class="btn join-item flex-1" id="stability-low">Low Noise</button>
</div>
</div>
<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>The laser mode is dependent on the DCDC status and safety settings.</span>
</div>
</div>
</div>
</div>
<!-- Safety Settings -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Safety Settings</h2>
<!-- Horizontal row of toggles -->
<div class="flex flex-wrap gap-4 mt-4">
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Interlock Check</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-interlock" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Temperature Protection</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-temp" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">OverCurrent Protection</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-overcurrent" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Input Voltage Protection</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-voltage" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Autostart DCDC</span>
<input type="checkbox" class="toggle toggle-error" id="toggle-autostart-dcdc" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Autostart Laser</span>
<input type="checkbox" class="toggle toggle-error" id="toggle-autostart-laser" />
</label>
</div>
</div>
<div class="mt-4">
<div class="alert alert-warning">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>Disabling safety features may damage your hardware or create hazardous conditions.</span>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Voltage slider interaction
const voltageSlider = document.getElementById('voltage-slider') as HTMLInputElement;
const voltageDisplay = document.getElementById('voltage-display');
if (voltageSlider && voltageDisplay) {
voltageSlider.addEventListener('input', function() {
voltageDisplay.textContent = this.value + 'V';
});
}
// DCDC button interactions
const dcdcButtons = {
start: document.getElementById('dcdc-start'),
run: document.getElementById('dcdc-run'),
stop: document.getElementById('dcdc-stop'),
error: document.getElementById('dcdc-error')
};
Object.entries(dcdcButtons).forEach(([action, btn]) => {
if (btn) {
btn.addEventListener('click', function() {
console.log('DCDC:', action);
// Send WebSocket command
});
}
});
// Laser button interactions
const laserButtons = {
start: document.getElementById('laser-start'),
run: document.getElementById('laser-run'),
stop: document.getElementById('laser-stop'),
error: document.getElementById('laser-error')
};
Object.entries(laserButtons).forEach(([action, btn]) => {
if (btn) {
btn.addEventListener('click', function() {
console.log('Laser:', action);
// Send WebSocket command
});
}
});
// Safety toggle interactions
const toggles = document.querySelectorAll('.toggle');
toggles.forEach(toggle => {
toggle.addEventListener('change', () => {
updateToggleClass(toggle as HTMLInputElement);
});
updateToggleClass(toggle as HTMLInputElement);
});
function updateToggleClass(toggle: HTMLInputElement) {
if (toggle.checked) {
toggle.classList.remove('toggle-error');
toggle.classList.add('toggle-success');
} else {
toggle.classList.remove('toggle-success');
toggle.classList.add('toggle-error');
}
}
});
</script>

View file

@ -0,0 +1,549 @@
---
import {
$wsConnectionStatus,
$dcdc_state_string,
$dcdc_state_desc_string,
$laser_state_string,
$laser_state_desc_string,
$laser_temp,
$laser_temp_string,
$driver_temp,
$driver_temp_string,
$driver_max_temp,
$driver_max_temp_string,
$temperature_chart_data,
addTemperatureDataPoint,
} from "./AppProviderStore";
const pageTitle = "Dashboard";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
<!-- Status Cards - Static HTML that gets updated via WebSocket -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 bg-base-100">
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200">
<div class="stat-title">DCDC State</div>
<div class="stat-value" id="dcdc-state-value">
{$dcdc_state_string.get()}
</div>
<div class="stat-desc flex items-center">
<span class="text-info mr-1" id="dcdc-state-icon">● </span>
<span id="dcdc-state-text">{$dcdc_state_desc_string.get()}</span>
</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200">
<div class="stat-title">Laser State</div>
<div class="stat-value" id="laser-state-value">
{$laser_state_string.get()}
</div>
<div class="stat-desc flex items-center">
<span class="text-error mr-1" id="laser-state-icon">●</span>
<span id="laser-state-text">{$laser_state_desc_string.get()}</span>
</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200">
<div class="stat-title">Driver Temperature</div>
<div class="stat-value" id="driver-temp-value1">{$driver_temp_string.get()}</div>
<div class="stat-desc flex items-center">
<span class="text-info mr-1" id="driver-max-temp-icon">●</span>
<span id="driver-max-temp-text">Maximal allowed: {$driver_max_temp_string.get()}</span>
</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200">
<div class="stat-title">WebSocket</div>
<div class="stat-value" id="ws-status-value">
{$wsConnectionStatus.get()}
</div>
<div class="stat-desc flex items-center" id="ws-status-desc">
<span class="text-error mr-1" id="ws-status-icon">●</span>
Connection status
</div>
</div>
</div>
</div>
<!-- Quick Actions & Controls - Interactive Parts -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- DCDC Controls -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">DCDC Control</h2>
<div class="flex flex-col gap-2">
<button class="btn btn-primary" id="dcdc-start-btn">Start DCDC</button
>
<button class="btn btn-warning" id="dcdc-stop-btn">Stop DCDC</button>
<button class="btn btn-outline" id="dcdc-reset-btn">Reset DCDC</button
>
</div>
<div class="mt-2">
<span class="text-sm"
>Power Good: <span id="power-good-status" class="font-bold">--</span
></span
>
</div>
</div>
</div>
<!-- Laser Controls -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Laser Control</h2>
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-3">
<button class="btn btn-primary flex-1 rounded-lg shadow-md transition hover:scale-105" id="ref-calibrate-btn">Calibrate REF</button>
<button class="btn btn-primary flex-1 rounded-lg shadow-md transition hover:scale-105" id="laser-calibrate-btn">Calibrate Laser</button>
</div>
<div class="flex flex-row gap-3">
<button
class="btn btn-success flex-1 rounded-lg shadow-md transition hover:scale-105"
id="laser-start-external-btn">Start Laser External</button
>
<button
class="btn btn-success flex-1 rounded-lg shadow-md transition hover:scale-105"
id="laser-start-internal-btn">Start Laser Internal</button
>
</div>
<button class="btn btn-error" id="laser-stop-btn">Stop Laser</button>
<button class="btn btn-outline" id="laser-reset-btn"
>Reset Laser</button
>
</div>
<div class="mt-2">
<span class="text-sm"
>Laser Good: <span id="laser-good-status" class="font-bold">--</span
></span
>
</div>
</div>
</div>
<!-- System Status -->
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-100 rounded-lg">
<h2 class="card-title">System Status</h2>
<div class="space-y-2">
<div class="flex justify-between">
<span>Driver Temp:</span>
<span id="driver-temp-value2" class="font-mono">--°C</span>
</div>
<div class="flex justify-between">
<span>VCC Voltage:</span>
<span id="vcc-voltage-value" class="font-mono">--V</span>
</div>
<div class="flex justify-between">
<span>PD Voltage:</span>
<span id="pd-voltage-value" class="font-mono">--V</span>
</div>
<div class="flex justify-between">
<span>Laser Current:</span>
<span id="laser-current-value" class="font-mono">--mA</span>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 gap-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-100 rounded-lg relative">
<h2 class="card-title">Calibration Table</h2>
<!-- Chart container with placeholder -->
<div class="flex-1 min-h-[300px] max-h-[500px] relative">
<!-- Placeholder when no data -->
<div id="chart-placeholder" class="flex items-center justify-center h-full bg-base-200 rounded-lg">
<div class="text-center">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-chart-line mx-auto"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 19l16 0" /><path d="M4 15l4 -6l4 2l4 -5l4 4" /></svg>
<div class="text-lg font-semibold">No Calibration Data</div>
<div class="text-sm text-base-content/70">Click the button below to request data from device</div>
</div>
</div>
<!-- Chart canvas (hidden initially) -->
<canvas id="calibration-table-canvas" class="hidden w-full h-full"></canvas>
</div>
<!-- Small button positioned bottom right -->
<button class="btn btn-primary btn-xs absolute bottom-2 right-2" id="calibration-table-btn">
Request Calibration Table
</button>
</div>
</div>
<!-- Temperature Chart -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-100 rounded-lg relative">
<h2 class="card-title">Temperature Monitor</h2>
<!-- Chart container -->
<div class="flex-1 min-h-[300px] max-h-[500px] relative">
<!-- Chart canvas -->
<canvas id="temperature-chart-canvas" class="w-full h-full"></canvas>
</div>
<!-- Status indicators -->
<div class="flex justify-between items-center mt-2 text-sm">
<div class="flex items-center gap-4">
<div class="flex items-center gap-1">
<div class="w-3 h-3 bg-blue-500 rounded"></div>
<span>Driver Temp: <span id="driver-temp-current" class="font-mono">--°C</span></span>
</div>
<div class="flex items-center gap-1">
<div class="w-3 h-3 bg-red-500 rounded"></div>
<span>Laser Temp: <span id="laser-temp-current" class="font-mono">--°C</span></span>
</div>
</div>
<div class="text-xs text-base-content/70">
Last 5 minutes
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Hydrate only the interactive parts -->
<script>
import {
$wsConnectionStatus,
$dcdc_state_string,
$dcdc_state_desc_string,
$laser_state_string,
$laser_state_desc_string,
start_DCDC,
$laser_temp,
$laser_temp_string,
$driver_temp,
$driver_temp_string,
$driver_max_temp,
$driver_max_temp_string,
$temperature_chart_data,
addTemperatureDataPoint,
$vcc_voltage,
$vcc_voltage_string,
$pd_voltage,
$pd_voltage_string,
$current_reading,
$current_reading_string,
$POWERGOOD_string,
$LASERGOOD_string,
stop_DCDC,
reset_DCDC,
run_Laser_External,
run_Laser_Internal,
stop_Laser,
reset_Laser,
get_calibration_table,
$calibration_table_data,
start_REF_Calibration,
start_Laser_Calibration,
} from "./AppProviderStore";
import { Chart, type ChartItem } from "chart.js/auto";
// subscribe to the wsConnectionStatus store and update the value of the ws-status-value element
$wsConnectionStatus.subscribe((value) => {
const el = document.getElementById("ws-status-value");
const elIcon = document.getElementById("ws-status-icon");
// overwrite the value of the ws-status-value element
if (el) el.textContent = value;
// overwrite the icon color of the ws-status-icon element
if (elIcon && value === "Connected") {
elIcon.classList.remove("text-error");
elIcon.classList.add("text-success");
} else if (elIcon && value === "Disconnected") {
elIcon.classList.remove("text-success");
elIcon.classList.add("text-error");
} else if (elIcon && value === "Error") {
elIcon.classList.remove("text-success");
elIcon.classList.add("text-error");
} else if (elIcon && value === "Failed") {
elIcon.classList.remove("text-success");
elIcon.classList.add("text-error");
} else if (elIcon && value === "Connecting") {
elIcon.classList.remove("text-error");
elIcon.classList.add("text-warning");
}
});
// subscribe to the dcdcStateString store and update the value of the dcdc-state-value element
$dcdc_state_string.subscribe((value) => {
const el = document.getElementById("dcdc-state-value");
console.log("dcdcStateString", value);
if (el) el.textContent = value;
});
// subscribe to the laserStateString store and update the value of the laser-state-value element
$laser_state_string.subscribe((value) => {
const el = document.getElementById("laser-state-value");
if (el) el.textContent = value;
});
$driver_temp.subscribe((value) => {
const el1 = document.getElementById("driver-temp-value1");
const el2 = document.getElementById("driver-temp-value2");
const elMax = document.getElementById("driver-max-temp-text");
if (el1) el1.textContent = $driver_temp_string.get();
if (el2) el2.textContent = $driver_temp_string.get();
});
$driver_max_temp_string.subscribe((value) => {
const elMax = document.getElementById("driver-max-temp-text");
if (elMax) elMax.textContent = "Maximal allowed: " + $driver_max_temp_string.get();
});
// Temperature chart subscriptions
$driver_temp.subscribe((value) => {
const el = document.getElementById("driver-temp-current");
if (el) el.textContent = $driver_temp_string.get();
// Add data point to temperature chart using store function
addTemperatureDataPoint($driver_temp.get(), $laser_temp.get());
});
$laser_temp.subscribe((value) => {
const el = document.getElementById("laser-temp-current");
if (el) el.textContent = $laser_temp_string.get();
// Temperature data will be added when driver temp updates to avoid duplicate entries
});
$vcc_voltage.subscribe((value) => {
const el = document.getElementById("vcc-voltage-value");
if (el) el.textContent = $vcc_voltage_string.get();
});
$pd_voltage_string.subscribe((value) => {
const el = document.getElementById("pd-voltage-value");
if (el) el.textContent = $pd_voltage_string.get();
});
$current_reading_string.subscribe((value) => {
const el = document.getElementById("laser-current-value");
if (el) el.textContent = $current_reading_string.get();
});
$POWERGOOD_string.subscribe((value) => {
const el = document.getElementById("power-good-status");
if (el) el.textContent = $POWERGOOD_string.get();
});
$LASERGOOD_string.subscribe((value) => {
const el = document.getElementById("laser-good-status");
if (el) el.textContent = $LASERGOOD_string.get();
});
$dcdc_state_desc_string.subscribe((value) => {
const el = document.getElementById("dcdc-state-text");
if (el) el.textContent = $dcdc_state_desc_string.get();
});
$laser_state_desc_string.subscribe((value) => {
const el = document.getElementById("laser-state-text");
if (el) el.textContent = $laser_state_desc_string.get();
});
// Button click handlers
document
.getElementById("dcdc-start-btn")
?.addEventListener("click", start_DCDC);
document
.getElementById("dcdc-stop-btn")
?.addEventListener("click", stop_DCDC);
document
.getElementById("dcdc-reset-btn")
?.addEventListener("click", reset_DCDC);
document
.getElementById("laser-start-external-btn")
?.addEventListener("click", run_Laser_External);
document
.getElementById("laser-start-internal-btn")
?.addEventListener("click", run_Laser_Internal);
document
.getElementById("laser-stop-btn")
?.addEventListener("click", stop_Laser);
document
.getElementById("laser-reset-btn")
?.addEventListener("click", reset_Laser);
document.getElementById("ref-calibrate-btn")?.addEventListener("click", start_REF_Calibration);
document.getElementById("laser-calibrate-btn")?.addEventListener("click", start_Laser_Calibration);
// Chart management
let chart: Chart | null = null;
const canvas = document.getElementById("calibration-table-canvas");
const placeholder = document.getElementById("chart-placeholder");
function showPlaceholder() {
if (placeholder) placeholder.classList.remove("hidden");
if (canvas) canvas.classList.add("hidden");
if (chart) {
chart.destroy();
chart = null;
}
}
function showChart() {
if (placeholder) placeholder.classList.add("hidden");
if (canvas) canvas.classList.remove("hidden");
}
function createChart(data: any) {
if (chart) {
chart.destroy();
}
chart = new Chart(canvas as ChartItem, {
type: "line",
data: {
labels: [500,1000,1500,2000,2500,3000,3500],
datasets: [
{
label: "Calibration Curve",
data: data,
fill: false,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0 // Disable animations to prevent text stretching during resize
},
scales: {
x: {
display: true,
title: {
display: true,
text: 'Duty Cycle'
}
},
y: {
display: true,
title: {
display: true,
text: 'Current'
},
min:0,
max:6000,
ticks: {
stepSize: 1000
}
}
}
},
});
}
// create chart when calibration table data is available
$calibration_table_data.subscribe((value) => {
console.log("Calibration table data updated");
createChart(value.values);
showChart();
});
// Request calibration data button
document.getElementById("calibration-table-btn")?.addEventListener("click", () => {
// reuest table
get_calibration_table();
});
// Temperature Chart Management
let temperatureChart: Chart | null = null;
const temperatureCanvas = document.getElementById("temperature-chart-canvas");
function createTemperatureChart() {
if (temperatureChart) {
temperatureChart.destroy();
}
const chartData = $temperature_chart_data.get();
temperatureChart = new Chart(temperatureCanvas as ChartItem, {
type: "line",
data: {
labels: chartData.timestamps,
datasets: [
{
label: "Driver Temperature",
data: chartData.driverTemps,
borderColor: "#3B82F6", // blue-500
backgroundColor: "#3B82F680",
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 5,
},
{
label: "Laser Temperature",
data: chartData.laserTemps,
borderColor: "#EF4444", // red-500
backgroundColor: "#EF444480",
fill: false,
tension: 0.1,
pointRadius: 0,
pointHoverRadius: 5,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0 // Disable animations for real-time performance
},
scales: {
x: {
display: true,
title: {
display: true,
text: 'Time'
},
ticks: {
maxTicksLimit: 10 // Show only every nth label to avoid crowding
}
},
y: {
display: true,
title: {
display: true,
text: 'Temperature (°C)'
},
min: 0,
max: 100,
ticks: {
stepSize: 10
}
}
},
plugins: {
legend: {
display: true,
position: 'top' as const
}
},
interaction: {
intersect: false,
mode: 'index' as const
}
},
});
}
// Initialize temperature chart
createTemperatureChart();
// Subscribe to temperature chart data updates
$temperature_chart_data.subscribe((chartData) => {
if (temperatureChart) {
temperatureChart.data.labels = chartData.timestamps;
temperatureChart.data.datasets[0].data = chartData.driverTemps;
temperatureChart.data.datasets[1].data = chartData.laserTemps;
temperatureChart.update('none'); // No animation for real-time
}
});
// Initialize with placeholder
showPlaceholder();
</script>

View file

@ -0,0 +1,283 @@
---
const pageTitle = "DCDC Control";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
<!-- Mode Control -->
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Mode Control</h2>
<div class="join w-full">
<button class="btn join-item flex-1 btn-primary" id="dcdc-start">Start</button>
<button class="btn join-item flex-1 btn-success" id="dcdc-run">Run</button>
<button class="btn join-item flex-1 btn-error" id="dcdc-stop">Stop</button>
<button class="btn join-item flex-1 btn-error error-striped" id="dcdc-error">Error</button>
</div>
</div>
</div>
<!-- Voltage Control & Monitoring -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Voltage Control</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Target Output Voltage</span>
<span class="label-text-alt" id="voltage-display">25V</span>
</label>
<input type="range" min="10" max="50" value="25" class="range range-primary" step="0.1" id="voltage-slider" />
<div class="w-full flex justify-between text-xs px-2 mt-1">
<span>10V</span>
<span>20V</span>
<span>30V</span>
<span>40V</span>
<span>50V</span>
</div>
</div>
<div class="grid grid-cols-2 gap-4 mt-6">
<div class="stat">
<div class="stat-title">Input Voltage</div>
<div class="stat-value text-sm" id="input-voltage">24.5V</div>
<div class="stat-desc">Range: 18-28V</div>
</div>
<div class="stat">
<div class="stat-title">Output Voltage</div>
<div class="stat-value text-sm" id="output-voltage">25.0V</div>
<div class="stat-desc" id="voltage-accuracy">±0.1V</div>
</div>
</div>
<div class="mt-4">
<button class="btn btn-primary mr-2" id="apply-voltage">Apply</button>
<button class="btn btn-outline" id="reset-voltage">Reset</button>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Current & Power</h2>
<div class="grid grid-cols-2 gap-4">
<div class="stat">
<div class="stat-title">Current</div>
<div class="stat-value text-sm" id="output-current">2.1A</div>
<div class="stat-desc">Max: 5.0A</div>
</div>
<div class="stat">
<div class="stat-title">Power</div>
<div class="stat-value text-sm" id="output-power">52.5W</div>
<div class="stat-desc">Max: 150W</div>
</div>
</div>
<div class="mt-4">
<div class="flex justify-between text-sm mb-1">
<span>Efficiency</span>
<span id="efficiency">92.5%</span>
</div>
<progress class="progress progress-success w-full" value="92.5" max="100" id="efficiency-progress"></progress>
</div>
<div class="mt-4">
<div class="flex justify-between text-sm mb-1">
<span>Temperature</span>
<span id="dcdc-temp">45.2°C</span>
</div>
<progress class="progress progress-warning w-full" value="45.2" max="85" id="temp-progress"></progress>
<div class="text-xs text-gray-500 mt-1">Warning at 85°C</div>
</div>
</div>
</div>
</div>
<!-- Protection Settings -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Protection Settings</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mt-4">
<div class="form-control">
<label class="label">
<span class="label-text">Over-Current Limit (A)</span>
</label>
<input type="range" min="1" max="6" value="5" step="0.1" class="range range-error" id="overcurrent-limit" />
<div class="w-full flex justify-between text-xs px-2">
<span>1A</span>
<span>3A</span>
<span>6A</span>
</div>
<div class="label">
<span class="label-text-alt" id="overcurrent-display">5.0A</span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Over-Temperature (°C)</span>
</label>
<input type="range" min="60" max="100" value="85" step="1" class="range range-error" id="overtemp-limit" />
<div class="w-full flex justify-between text-xs px-2">
<span>60°C</span>
<span>80°C</span>
<span>100°C</span>
</div>
<div class="label">
<span class="label-text-alt" id="overtemp-display">85°C</span>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Under-Voltage (V)</span>
</label>
<input type="range" min="15" max="22" value="18" step="0.1" class="range range-warning" id="undervoltage-limit" />
<div class="w-full flex justify-between text-xs px-2">
<span>15V</span>
<span>18V</span>
<span>22V</span>
</div>
<div class="label">
<span class="label-text-alt" id="undervoltage-display">18.0V</span>
</div>
</div>
</div>
<div class="flex flex-wrap gap-4 mt-6">
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Auto-Start on Power</span>
<input type="checkbox" class="toggle toggle-primary" id="toggle-autostart" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Soft-Start Enable</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-softstart" />
</label>
</div>
</div>
</div>
</div>
<!-- Status & Diagnostics -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Status & Diagnostics</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<h3 class="font-semibold mb-2">Operating Status</h3>
<div class="space-y-2">
<div class="flex justify-between">
<span>PWM Frequency:</span>
<span id="pwm-freq">100kHz</span>
</div>
<div class="flex justify-between">
<span>Duty Cycle:</span>
<span id="duty-cycle">65.2%</span>
</div>
<div class="flex justify-between">
<span>Switching Losses:</span>
<span id="switching-losses">2.1W</span>
</div>
<div class="flex justify-between">
<span>Uptime:</span>
<span id="dcdc-uptime">2h 45m</span>
</div>
</div>
</div>
<div>
<h3 class="font-semibold mb-2">Error Log</h3>
<div class="bg-base-200 p-3 rounded-lg h-32 overflow-y-auto" id="error-log">
<div class="text-sm space-y-1">
<div class="text-success">14:32 - System started</div>
<div class="text-info">14:33 - Voltage ramped to 25V</div>
<div class="text-warning">14:35 - Temp spike to 60°C</div>
<div class="text-success">14:36 - Normal operation</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.error-striped {
background-image: repeating-linear-gradient(45deg, hsl(var(--er)) 0, hsl(var(--er)) 10px, hsl(var(--er) / 0.8) 10px, hsl(var(--er) / 0.8) 20px);
animation: error-blink 0.25s linear infinite;
}
@keyframes error-blink {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Voltage slider
const voltageSlider = document.getElementById('voltage-slider') as HTMLInputElement;
const voltageDisplay = document.getElementById('voltage-display');
if (voltageSlider && voltageDisplay) {
voltageSlider.addEventListener('input', function() {
voltageDisplay.textContent = parseFloat(this.value).toFixed(1) + 'V';
});
}
// Protection limit sliders
const sliders = [
{ id: 'overcurrent-limit', display: 'overcurrent-display', unit: 'A', decimals: 1 },
{ id: 'overtemp-limit', display: 'overtemp-display', unit: '°C', decimals: 0 },
{ id: 'undervoltage-limit', display: 'undervoltage-display', unit: 'V', decimals: 1 }
];
sliders.forEach(slider => {
const element = document.getElementById(slider.id) as HTMLInputElement;
const display = document.getElementById(slider.display);
if (element && display) {
element.addEventListener('input', function() {
const value = parseFloat(this.value).toFixed(slider.decimals);
display.textContent = value + slider.unit;
});
}
});
// DCDC mode buttons
const modeButtons = ['dcdc-start', 'dcdc-run', 'dcdc-stop', 'dcdc-error'];
modeButtons.forEach(buttonId => {
const button = document.getElementById(buttonId);
if (button) {
button.addEventListener('click', function() {
console.log('DCDC mode:', buttonId.replace('dcdc-', ''));
// Send WebSocket command
});
}
});
// Function to update DCDC values (called from WebSocket)
(window as any).updateDcdcValues = function(data: any) {
const fields = [
'input-voltage', 'output-voltage', 'output-current', 'output-power',
'efficiency', 'dcdc-temp', 'pwm-freq', 'duty-cycle', 'switching-losses', 'dcdc-uptime'
];
fields.forEach(field => {
const element = document.getElementById(field);
if (element && data[field.replace('-', '_')]) {
element.textContent = data[field.replace('-', '_')];
}
});
};
});
</script>

View file

@ -0,0 +1,165 @@
import { useState, useEffect } from 'preact/hooks';
import { useStore } from '@nanostores/preact';
import { $wsConnectionStatus, $updateState } from './AppProviderStore';
import UpdaterModal from './UpdaterModal';
import UpdateSuccessPopup from './UpdateSuccessPopup';
import UpdateErrorPopup from './UpdateErrorPopup';
import { $isUpdaterModalOpen, $isSuccessPopupOpen, $isErrorPopupOpen, stateSetters } from './AppProviderStore';
// Extend window interface
declare global {
interface Window {
openStatusModal?: () => void;
openFirmwareModal?: () => void;
openUpdaterModal?: () => void;
}
}
export default function GlobalModals() {
const [isStatusModalOpen, setIsStatusModalOpen] = useState(false);
const [isFirmwareModalOpen, setIsFirmwareModalOpen] = useState(false);
const updateState = useStore($updateState);
const isUpdaterModalOpen = useStore($isUpdaterModalOpen);
const isSuccessPopupOpen = useStore($isSuccessPopupOpen);
const isErrorPopupOpen = useStore($isErrorPopupOpen);
// Auto-open success/error popups when update state changes
useEffect(() => {
console.log("🎭 GlobalModals useEffect triggered:", {
updateState,
isSuccessPopupOpen,
isErrorPopupOpen,
isUpdaterModalOpen
});
if (updateState === 'success' && !isSuccessPopupOpen) {
console.log("🟢 Opening SUCCESS popup - update completed");
stateSetters.setSuccessPopupOpen(true);
} else if (updateState === 'restarting') {
console.log("🔄 RESTARTING state - ensuring SUCCESS popup stays open for countdown");
// Ensure success popup is open for restarting state (it will show restart UI)
if (!isSuccessPopupOpen) {
console.log("🔄 Opening SUCCESS popup for RESTARTING state - will show countdown");
stateSetters.setSuccessPopupOpen(true);
} else {
console.log("🔄 SUCCESS popup already open - countdown should be visible");
}
} else if (updateState === 'error' && !isErrorPopupOpen) {
console.log("❌ Opening ERROR popup - update failed");
stateSetters.setErrorPopupOpen(true);
} else {
console.log("🎭 No modal action needed - conditions not met");
}
}, [updateState, isSuccessPopupOpen, isErrorPopupOpen]);
// Listen for global modal events
if (typeof window !== 'undefined') {
window.openStatusModal = () => setIsStatusModalOpen(true);
window.openFirmwareModal = () => setIsFirmwareModalOpen(true);
window.openUpdaterModal = () => {
if (!$isUpdaterModalOpen.get()) {
stateSetters.setUpdaterModalOpen(true);
}
};
}
return (
<>
{/* Status Modal */}
{isStatusModalOpen && (
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">System Status</h3>
<div class="overflow-x-auto">
<table class="table table-zebra">
<tbody>
<tr>
<td class="font-bold">WebSocket Status</td>
<td>{useStore($wsConnectionStatus)}</td>
</tr>
<tr>
<td class="font-bold">P4 Firmware Version</td>
<td>1.2.5</td>
</tr>
<tr>
<td class="font-bold">C6 Firmware Version</td>
<td>0.9.3</td>
</tr>
<tr>
<td class="font-bold">G4 Firmware Version</td>
<td>2.1.0</td>
</tr>
<tr>
<td class="font-bold">Activated Plugins</td>
<td>- none -</td>
</tr>
<tr>
<td class="font-bold">Serial Number</td>
<td>HydroBuddy-00001</td>
</tr>
<tr>
<td class="font-bold">Operating Hours</td>
<td>127.5 h</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-4 flex justify-between">
<button class="btn btn-primary">Customer Service</button>
<button class="btn" onClick={() => setIsStatusModalOpen(false)}>Close</button>
</div>
</div>
<div class="modal-backdrop" onClick={() => setIsStatusModalOpen(false)}></div>
</dialog>
)}
{/* Firmware Update Modal */}
{isFirmwareModalOpen && (
<dialog class="modal modal-open">
<div class="modal-box">
<h3 class="font-bold text-lg mb-4">Firmware Update</h3>
<div class="flex flex-col items-center justify-center py-6">
<span class="loading loading-spinner loading-lg text-primary"></span>
<p class="mt-4 text-center">Checking for firmware updates...</p>
</div>
<div class="modal-action">
<button class="btn" onClick={() => setIsFirmwareModalOpen(false)}>Close</button>
</div>
</div>
<div class="modal-backdrop" onClick={() => setIsFirmwareModalOpen(false)}></div>
</dialog>
)}
{/* Updater Modal */}
<UpdaterModal
isOpen={isUpdaterModalOpen}
onClose={() => { stateSetters.setUpdaterModalOpen(false); }}
/>
{/* Update Success Popup */}
<UpdateSuccessPopup
isOpen={isSuccessPopupOpen}
onClose={() => {
stateSetters.setSuccessPopupOpen(false);
// Reset update state after popup is closed
stateSetters.setUpdateState('initial');
}}
/>
{/* Update Error Popup */}
<UpdateErrorPopup
isOpen={isErrorPopupOpen}
onClose={() => {
stateSetters.setErrorPopupOpen(false);
// Reset update state after popup is closed
stateSetters.setUpdateState('initial');
}}
/>
</>
);
}

View file

@ -0,0 +1,144 @@
---
const pageTitle = "Logs";
---
<div class="flex flex-col gap-4">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold">System Logs</h1>
<div class="join">
<button class="btn join-item">Export</button>
<button class="btn join-item btn-primary">Refresh</button>
</div>
</div>
<div class="card bg-base-200 shadow-lg mb-4">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-lg">Filters</h2>
<form class="filter grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control">
<label class="label">
<span class="label-text">Log Level</span>
</label>
<div class="join w-full">
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="All" checked />
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Error" />
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Warning" />
<input class="btn join-item flex-1" type="radio" name="log-level" aria-label="Info" />
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Date Range</span>
</label>
<select class="select select-bordered w-full">
<option selected>Today</option>
<option>Last 3 Days</option>
<option>Last Week</option>
<option>Last Month</option>
<option>Custom Range</option>
</select>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Search</span>
</label>
<div class="join w-full">
<input type="text" placeholder="Search logs..." class="input input-bordered join-item w-full" />
<button class="btn join-item">Search</button>
</div>
</div>
<div class="col-span-1 md:col-span-3">
<button type="reset" class="btn btn-sm btn-outline">Reset Filters</button>
</div>
</form>
</div>
</div>
<div class="card bg-base-200 shadow-lg">
<div class="card-body bg-base-200 rounded-lg">
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Timestamp</th>
<th>Level</th>
<th>Component</th>
<th>Message</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>2025-03-15 14:32:45</td>
<td><div class="badge badge-error">ERROR</div></td>
<td>Driver</td>
<td>Failed to initialize temperature sensor</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
<tr>
<td>2025-03-15 14:30:12</td>
<td><div class="badge badge-warning">WARN</div></td>
<td>Modulator</td>
<td>PWM frequency outside optimal range</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
<tr>
<td>2025-03-15 14:28:05</td>
<td><div class="badge badge-info">INFO</div></td>
<td>System</td>
<td>User authentication successful</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
<tr>
<td>2025-03-15 14:25:59</td>
<td><div class="badge">DEBUG</div></td>
<td>Control</td>
<td>PID controller parameters updated</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
<tr>
<td>2025-03-15 14:20:33</td>
<td><div class="badge badge-info">INFO</div></td>
<td>System</td>
<td>Laser driver initialization complete</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
<tr>
<td>2025-03-15 14:18:22</td>
<td><div class="badge badge-success">SUCCESS</div></td>
<td>Hardware</td>
<td>All hardware components detected</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
<tr>
<td>2025-03-15 14:15:07</td>
<td><div class="badge badge-info">INFO</div></td>
<td>System</td>
<td>System boot initiated</td>
<td><button class="btn btn-xs btn-ghost">Details</button></td>
</tr>
</tbody>
</table>
</div>
<div class="flex justify-between items-center mt-4">
<div>
<span class="text-sm">Showing 1-7 of 42 entries</span>
</div>
<div class="join">
<button class="join-item btn btn-sm">«</button>
<button class="join-item btn btn-sm btn-active">1</button>
<button class="join-item btn btn-sm">2</button>
<button class="join-item btn btn-sm">3</button>
<button class="join-item btn btn-sm">»</button>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,103 @@
import { useStore } from '@nanostores/preact';
import { $freeInternalHeap, $totalInternalHeap } from './AppProviderStore';
interface MemoryProgressBarProps {
showDetails?: boolean;
className?: string;
}
export default function MemoryProgressBar({ showDetails = true, className = "" }: MemoryProgressBarProps) {
const freeInternalHeap = useStore($freeInternalHeap);
const totalInternalHeap = useStore($totalInternalHeap);
// Calculate used and percentage
const usedInternalHeap = totalInternalHeap - freeInternalHeap;
const usagePercentage = totalInternalHeap > 0 ? (usedInternalHeap / totalInternalHeap) * 100 : 0;
const freePercentage = 100 - usagePercentage;
// Determine color based on usage
const getProgressColor = (percentage: number) => {
if (percentage < 50) return 'bg-green-500';
if (percentage < 75) return 'bg-yellow-500';
if (percentage < 90) return 'bg-orange-500';
return 'bg-red-500';
};
const getTextColor = (percentage: number) => {
if (percentage < 50) return 'text-green-600';
if (percentage < 75) return 'text-yellow-600';
if (percentage < 90) return 'text-orange-600';
return 'text-red-600';
};
const formatBytes = (bytes: number) => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
};
// Create segments for the memory bar (like Linux htop)
const totalSegments = 20;
const usedSegments = Math.round((usagePercentage / 100) * totalSegments);
const getSegmentColor = (segmentIndex: number) => {
if (segmentIndex >= usedSegments) return 'bg-gray-300'; // Empty
const percentageAtSegment = (segmentIndex / totalSegments) * 100;
if (percentageAtSegment < 50) return 'bg-green-500';
if (percentageAtSegment < 75) return 'bg-yellow-500';
if (percentageAtSegment < 90) return 'bg-orange-500';
return 'bg-red-500';
};
return (
<div className={`space-y-2 ${className}`}>
{/* Linux-style Memory Bar with segments */}
<div className="flex items-center space-x-3">
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium text-gray-700">Internal Heap</span>
<div className="text-right">
<span className={`text-sm font-medium ${getTextColor(usagePercentage)}`}>
{usagePercentage.toFixed(1)}%
</span>
<div className="text-xs text-gray-600">
{formatBytes(usedInternalHeap)}/{formatBytes(totalInternalHeap)}
</div>
</div>
</div>
{/* Memory segments */}
<div className="flex space-x-0.5">
{Array.from({ length: totalSegments }, (_, i) => (
<div
key={i}
className={`h-3 flex-1 transition-colors duration-300 ${getSegmentColor(i)}`}
style={{ minWidth: '2px' }}
></div>
))}
</div>
</div>
</div>
{/* Memory Stats */}
{showDetails && (
<div className="grid grid-cols-3 gap-3 text-xs text-gray-600">
<div className="flex justify-between">
<span>Used:</span>
<span className="font-medium">{formatBytes(usedInternalHeap)}</span>
</div>
<div className="flex justify-between">
<span>Free:</span>
<span className="font-medium text-green-600">{formatBytes(freeInternalHeap)}</span>
</div>
<div className="flex justify-between">
<span>Total:</span>
<span className="font-medium">{formatBytes(totalInternalHeap)}</span>
</div>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,505 @@
---
// This is server-side - runs at build time
const pageTitle = "Modulation";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">Laser Modulation</h1>
<!-- Modulation Mode Selection -->
<div class="card bg-base-200 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Modulation Mode</h2>
<!-- Main Mode Tabs - Keep horizontal even on mobile -->
<div class="tabs tabs-boxed mb-4">
<button class="tab tab-active flex-1" id="internal-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 12l2 2 4-4"/>
<path d="M21 12c-1 0-3-1-3-3s2-3 3-3 3 1 3 3-2 3-3 3"/>
<path d="M3 12c1 0 3-1 3-3s-2-3-3-3-3 1-3 3 2 3 3 3"/>
<path d="M3 12h6m6 0h6"/>
</svg>
Internal
</button>
<button class="tab flex-1" id="external-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M6 3v12"/>
<path d="M18 3v12"/>
<path d="M6 21l12-9-12-3z"/>
</svg>
External
</button>
</div>
<!-- Internal Modes - Stack on mobile -->
<div id="internal-modes" class="mb-4">
<h3 class="font-medium mb-3">Internal Modulation Modes</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-2">
<button class="btn btn-active" id="cc-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
Constant Current
</button>
<button class="btn" id="cp-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2"/>
<path d="M12 20v2"/>
<path d="m4.93 4.93 1.41 1.41"/>
<path d="m17.66 17.66 1.41 1.41"/>
<path d="M2 12h2"/>
<path d="M20 12h2"/>
<path d="m6.34 17.66-1.41 1.41"/>
<path d="m19.07 4.93-1.41 1.41"/>
</svg>
Constant Power
</button>
<button class="btn" id="pattern-continuous-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h6l4 8 4-16 4 8h6"/>
</svg>
Pattern Continuous
</button>
<button class="btn" id="pattern-triggered-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 6l4-4 4 4"/>
<path d="M12 3v9"/>
<path d="M3 12h6l4 8 4-8h6"/>
</svg>
Pattern Triggered
</button>
</div>
</div>
<!-- External Modes - Stack on mobile -->
<div id="external-modes" class="mb-4 hidden">
<h3 class="font-medium mb-3">External Modulation Modes</h3>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button class="btn btn-active" id="ttl-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="M5 12v7"/>
<path d="M19 12v7"/>
<path d="M5 5v7"/>
<path d="M19 5v7"/>
</svg>
TTL
</button>
<button class="btn" id="mod2current-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 3v18h18"/>
<path d="M7 12l4-4 4 4 4-4"/>
</svg>
Mod2Current
</button>
<button class="btn" id="schmitt-trigger-tab" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h6l2-8 2 16 2-8h6"/>
</svg>
Schmitt Trigger
</button>
</div>
</div>
</div>
</div>
<!-- Mode Configuration Panels -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Constant Current Panel -->
<div id="cc-panel" class="card bg-base-200 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
Constant Current Mode
</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Current (A)</span>
<span class="label-text-alt">5.0A</span>
</label>
<input type="range" min="0" max="10" value="5" step="0.1" class="range range-primary" />
<div class="w-full flex justify-between text-xs px-2 mt-1">
<span>0A</span>
<span>2.5A</span>
<span>5.0A</span>
<span>7.5A</span>
<span>10.0A</span>
</div>
</div>
<div class="form-control mt-4">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Enable Soft Start</span>
<input type="checkbox" class="toggle toggle-success" checked onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Constant Current mode provides stable current output regardless of load changes.</span>
</div>
</div>
</div>
<!-- Constant Power Panel -->
<div id="cp-panel" class="card bg-base-200 shadow hidden">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2"/>
<path d="M12 20v2"/>
<path d="m4.93 4.93 1.41 1.41"/>
<path d="m17.66 17.66 1.41 1.41"/>
<path d="M2 12h2"/>
<path d="M20 12h2"/>
<path d="m6.34 17.66-1.41 1.41"/>
<path d="m19.07 4.93-1.41 1.41"/>
</svg>
Constant Power Mode
</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Power (W)</span>
<span class="label-text-alt">5.0W</span>
</label>
<input type="range" min="0" max="10" value="5" step="0.1" class="range range-secondary" />
<div class="w-full flex justify-between text-xs px-2 mt-1">
<span>0W</span>
<span>2.5W</span>
<span>5.0W</span>
<span>7.5W</span>
<span>10.0W</span>
</div>
</div>
<div class="form-control mt-4">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Enable Feedback Control</span>
<input type="checkbox" class="toggle toggle-success" checked onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Constant Power mode uses feedback from the photodiode to maintain stable output power.</span>
</div>
</div>
</div>
<!-- Pattern Continuous Panel -->
<div id="pattern-continuous-panel" class="card bg-base-200 shadow hidden">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h6l4 8 4-16 4 8h6"/>
</svg>
Pattern Continuous Mode
</h2>
<div class="bg-base-200 rounded-lg p-6">
<div class="flex items-center justify-center h-40">
<div class="text-center">
<svg class="w-16 h-16 mx-auto mb-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v20"/>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
</svg>
<p class="text-lg font-bold">Coming Soon</p>
<p class="text-sm mt-2">Pattern configuration interface under development</p>
</div>
</div>
</div>
</div>
</div>
<!-- Pattern Triggered Panel -->
<div id="pattern-triggered-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-warning hidden">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-warning">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 6l4-4 4 4"/>
<path d="M12 3v9"/>
<path d="M3 12h6l4 8 4-8h6"/>
</svg>
Pattern Triggered Mode
</h2>
<div class="bg-warning/5 border border-warning/20 rounded-lg p-6">
<div class="flex items-center justify-center h-40">
<div class="text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-warning opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M8 6l4-4 4 4"/>
<path d="M12 3v9"/>
<path d="M3 12h6l4 8 4-8h6"/>
</svg>
<p class="text-lg font-bold">Coming Soon</p>
<p class="text-sm mt-2">Triggered pattern configuration interface under development</p>
</div>
</div>
</div>
</div>
</div>
<!-- TTL Panel -->
<div id="ttl-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-info hidden">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-info">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="M5 12v7"/>
<path d="M19 12v7"/>
<path d="M5 5v7"/>
<path d="M19 5v7"/>
</svg>
TTL Mode
</h2>
<div class="bg-info/5 border border-info/20 rounded-lg p-6">
<div class="flex items-center justify-center h-40">
<div class="text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-info opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14"/>
<path d="M5 12v7"/>
<path d="M19 12v7"/>
<path d="M5 5v7"/>
<path d="M19 5v7"/>
</svg>
<p class="text-lg font-bold">Coming Soon</p>
<p class="text-sm mt-2">TTL interface configuration under development</p>
</div>
</div>
</div>
</div>
</div>
<!-- Mod2Current Panel -->
<div id="mod2current-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-success hidden">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-success">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 3v18h18"/>
<path d="M7 12l4-4 4 4 4-4"/>
</svg>
Mod2Current Mode
</h2>
<div class="bg-success/5 border border-success/20 rounded-lg p-4">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4">
<button class="btn btn-active" onclick="if('vibrate' in navigator) navigator.vibrate(15)">0-5V</button>
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">0-10V</button>
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Custom</button>
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Current Limit (A)</span>
<span class="label-text-alt">7.5A</span>
</label>
<input type="range" min="0" max="10" value="7.5" step="0.1" class="range range-success" />
</div>
</div>
<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Mod2Current converts an analog input voltage to a proportional current output.</span>
</div>
</div>
</div>
<!-- Schmitt Trigger Panel -->
<div id="schmitt-trigger-panel" class="card bg-base-200 shadow-xl border-l-4 border-l-error hidden">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-error">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h6l2-8 2 16 2-8h6"/>
</svg>
Schmitt Trigger Mode
</h2>
<div class="bg-error/5 border border-error/20 rounded-lg p-6">
<div class="flex items-center justify-center h-40">
<div class="text-center">
<svg class="w-16 h-16 mx-auto mb-4 text-error opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12h6l2-8 2 16 2-8h6"/>
</svg>
<p class="text-lg font-bold">Coming Soon</p>
<p class="text-sm mt-2">Schmitt Trigger configuration interface under development</p>
</div>
</div>
</div>
</div>
</div>
<!-- Advanced Parameters -->
<div class="card bg-base-200 shadow-xl border-l-4 border-l-primary">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/>
<path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/>
<path d="M12 2v2"/>
<path d="M12 22v-2"/>
<path d="m17 20.66-1-1.73"/>
<path d="M11 10.27 7 3.34"/>
<path d="m20.66 17-1.73-1"/>
<path d="m3.34 7 1.73 1"/>
<path d="M14 12h8"/>
<path d="M2 12h2"/>
<path d="m20.66 7-1.73 1"/>
<path d="m3.34 17 1.73-1"/>
<path d="m17 3.34-1 1.73"/>
<path d="m11 13.73-4 6.93"/>
</svg>
Advanced Parameters
</h2>
<div class="form-control">
<label class="label">
<span class="label-text">Response Time</span>
</label>
<label class="select select-bordered w-full flex items-center">
<svg class="w-4 h-4 opacity-70 absolute left-3" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12,6 12,12 16,14"/>
</svg>
<select class="pl-10">
<option>Fast (High Bandwidth)</option>
<option selected>Normal</option>
<option>Slow (Low Noise)</option>
</select>
</label>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Stability Mode</span>
</label>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
<button class="btn btn-active" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Normal</button>
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">High Stability</button>
<button class="btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Low Noise</button>
</div>
</div>
<div class="flex flex-col sm:flex-row justify-end mt-4 gap-2">
<button class="btn btn-outline" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Save Preset</button>
<button class="btn btn-primary" onclick="if('vibrate' in navigator) navigator.vibrate([100, 200, 100])">Apply</button>
</div>
</div>
</div>
</div>
</div>
<script>
// Toggle color management function
function updateToggleColor(toggle: HTMLInputElement) {
if (toggle.checked) {
toggle.classList.remove('toggle-error');
toggle.classList.add('toggle-success');
} else {
toggle.classList.remove('toggle-success');
toggle.classList.add('toggle-error');
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize toggle colors
const toggles = document.querySelectorAll('.toggle');
toggles.forEach(toggle => {
updateToggleColor(toggle as HTMLInputElement);
});
// Tab switching logic
const internalTab = document.getElementById('internal-tab');
const externalTab = document.getElementById('external-tab');
const internalModes = document.getElementById('internal-modes');
const externalModes = document.getElementById('external-modes');
// Mode panels
const panels = {
'cc': document.getElementById('cc-panel'),
'cp': document.getElementById('cp-panel'),
'pattern-continuous': document.getElementById('pattern-continuous-panel'),
'pattern-triggered': document.getElementById('pattern-triggered-panel'),
'ttl': document.getElementById('ttl-panel'),
'mod2current': document.getElementById('mod2current-panel'),
'schmitt-trigger': document.getElementById('schmitt-trigger-panel')
};
// Mode buttons
const modeButtons = {
'cc': document.getElementById('cc-tab'),
'cp': document.getElementById('cp-tab'),
'pattern-continuous': document.getElementById('pattern-continuous-tab'),
'pattern-triggered': document.getElementById('pattern-triggered-tab'),
'ttl': document.getElementById('ttl-tab'),
'mod2current': document.getElementById('mod2current-tab'),
'schmitt-trigger': document.getElementById('schmitt-trigger-tab')
};
// Show/hide panels
function showPanel(panelId: string) {
Object.values(panels).forEach(panel => panel?.classList.add('hidden'));
panels[panelId as keyof typeof panels]?.classList.remove('hidden');
// Update button states
Object.values(modeButtons).forEach(btn => btn?.classList.remove('btn-active'));
modeButtons[panelId as keyof typeof modeButtons]?.classList.add('btn-active');
}
// Tab switching
if (internalTab && externalTab && internalModes && externalModes) {
internalTab.addEventListener('click', function() {
internalTab.classList.add('tab-active');
externalTab.classList.remove('tab-active');
internalModes.classList.remove('hidden');
externalModes.classList.add('hidden');
showPanel('cc'); // Show default internal mode
});
externalTab.addEventListener('click', function() {
externalTab.classList.add('tab-active');
internalTab.classList.remove('tab-active');
externalModes.classList.remove('hidden');
internalModes.classList.add('hidden');
showPanel('ttl'); // Show default external mode
});
}
// Mode button event listeners
Object.entries(modeButtons).forEach(([mode, button]) => {
if (button) {
button.addEventListener('click', () => showPanel(mode));
}
});
// Stability mode buttons
const stabilityButtons = document.querySelectorAll('.grid .btn');
stabilityButtons.forEach(btn => {
btn.addEventListener('click', function(this: HTMLElement) {
// Only update buttons in the same grid
const grid = this.closest('.grid');
if (grid) {
grid.querySelectorAll('.btn').forEach(b => b.classList.remove('btn-active'));
this.classList.add('btn-active');
}
});
});
// Initialize with Constant Current mode
showPanel('cc');
});
</script>

View file

@ -0,0 +1,193 @@
---
// This is server-side - runs at build time
const pageTitle = "Monitoring";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
<!-- Real-time Metrics -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Temperature</h2>
<div class="flex items-center justify-center h-40">
<div class="text-center">
<div class="radial-progress text-primary" style="--value:65; --size:8rem;" id="temp-progress">65%</div>
<div class="mt-2" id="temp-reading">25.6°C</div>
</div>
</div>
<div class="flex justify-between text-sm mt-2">
<span>Min: <span id="temp-min">22.1°C</span></span>
<span>Max: <span id="temp-max">28.3°C</span></span>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Current</h2>
<div class="flex items-center justify-center h-40">
<div class="text-center">
<div class="radial-progress text-secondary" style="--value:42; --size:8rem;" id="current-progress">42%</div>
<div class="mt-2" id="current-reading">125mA</div>
</div>
</div>
<div class="flex justify-between text-sm mt-2">
<span>Min: <span id="current-min">0mA</span></span>
<span>Max: <span id="current-max">300mA</span></span>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Output Power</h2>
<div class="flex items-center justify-center h-40">
<div class="text-center">
<div class="radial-progress text-accent" style="--value:50; --size:8rem;" id="power-progress">50%</div>
<div class="mt-2" id="power-reading">500mW</div>
</div>
</div>
<div class="flex justify-between text-sm mt-2">
<span>Min: <span id="power-min">0mW</span></span>
<span>Max: <span id="power-max">1000mW</span></span>
</div>
</div>
</div>
</div>
<!-- System Health -->
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">System Health</h2>
<div class="overflow-x-auto">
<table class="table">
<tbody>
<tr>
<td>CPU Temperature</td>
<td class="w-full">
<div class="flex items-center gap-2">
<div class="w-full h-4 bg-base-300 rounded-lg relative">
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
<span>0°C</span>
<span>30°C</span>
<span>60°C</span>
<span>90°C</span>
<span>120°C</span>
</div>
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
style="width: 35.25%; background: linear-gradient(90deg, #3b82f6 0%, #22c55e 37.5%, #f59e0b 75%, #ef4444 100%);" id="cpu-temp-bar">
</div>
</div>
<span id="cpu-temp">42.3°C</span>
</div>
</td>
</tr>
<tr>
<td>GaN Temperature</td>
<td class="w-full">
<div class="flex items-center gap-2">
<div class="w-full h-4 bg-base-300 rounded-lg relative">
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
<span>0°C</span>
<span>30°C</span>
<span>60°C</span>
<span>90°C</span>
<span>120°C</span>
</div>
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
style="width: 54.8%; background: linear-gradient(90deg, #3b82f6 0%, #22c55e 37.5%, #f59e0b 75%, #ef4444 100%);" id="gan-temp-bar">
</div>
</div>
<span id="gan-temp">65.8°C</span>
</div>
</td>
</tr>
<tr>
<td>WiFi Signal Strength</td>
<td class="w-full">
<div class="flex items-center gap-2">
<div class="w-full h-4 bg-base-300 rounded-lg relative">
<div class="absolute inset-0 flex justify-between px-1 text-[10px] text-base-content z-10">
<span>-120dBm</span>
<span>-90dBm</span>
<span>-60dBm</span>
<span>-30dBm</span>
<span>0dBm</span>
</div>
<div class="absolute top-0 bottom-0 left-0 rounded-lg"
style="width: 65%; background: linear-gradient(90deg, #ef4444 0%, #f59e0b 40%, #22c55e 80%, #22c55e 100%);" id="wifi-signal-bar">
</div>
</div>
<span id="wifi-signal">-42dBm</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Historical Data -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Temperature History</h2>
<div class="flex items-center justify-center h-64 bg-base-200 rounded-box">
<p class="text-base-content opacity-50">Temperature Chart Placeholder</p>
</div>
</div>
</div>
<div class="card bg-base-200 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Power Output History</h2>
<div class="flex items-center justify-center h-64 bg-base-200 rounded-box">
<p class="text-base-content opacity-50">Power Chart Placeholder</p>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Update radial progress values
function updateRadialProgress(elementId: string, value: number) {
const element = document.getElementById(elementId);
if (element) {
element.style.setProperty('--value', value.toString());
element.textContent = value + '%';
}
}
// Function to update monitoring values (called from WebSocket)
(window as any).updateMonitoringValues = function(data: any) {
if (data.temperature) {
const tempProgress = Math.round((data.temperature / 100) * 100);
updateRadialProgress('temp-progress', tempProgress);
const tempReading = document.getElementById('temp-reading');
if (tempReading) tempReading.textContent = data.temperature + '°C';
}
if (data.current) {
const currentProgress = Math.round((data.current / 300) * 100);
updateRadialProgress('current-progress', currentProgress);
const currentReading = document.getElementById('current-reading');
if (currentReading) currentReading.textContent = data.current + 'mA';
}
if (data.power) {
const powerProgress = Math.round((data.power / 1000) * 100);
updateRadialProgress('power-progress', powerProgress);
const powerReading = document.getElementById('power-reading');
if (powerReading) powerReading.textContent = data.power + 'mW';
}
};
});
</script>

View file

@ -0,0 +1,723 @@
---
const pageTitle = "Network Configuration";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
<!-- Connection Status -->
<div class="flex flex-col gap-4">
<h2 class="text-xl font-bold">Connection Status</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs flex items-center gap-1">
Network Status
</div>
<div class="stat-value text-sm" id="wifi-status">Connected over WiFi</div>
<div class="stat-desc text-xs">Station Mode</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
</svg>
IP Address
</div>
<div class="stat-value text-sm" id="current-ip">192.168.1.158</div>
<div class="stat-desc text-xs">DHCP Assigned</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-radar opacity-70">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 10a2 2 0 0 1 1.678 .911l.053 .089h7.269l.117 .007a1 1 0 0 1 .883 .993c0 5.523 -4.477 10 -10 10a1 1 0 0 1 -1 -1v-7.269l-.089 -.053a2 2 0 0 1 -.906 -1.529l-.005 -.149a2 2 0 0 1 2 -2m9.428 -1.334a1 1 0 0 1 -1.884 .668a8 8 0 1 0 -10.207 10.218a1 1 0 0 1 -.666 1.886a10 10 0 1 1 12.757 -12.772m-4.628 -.266a1 1 0 0 1 -1.6 1.2a4 4 0 1 0 -5.6 5.6a1 1 0 0 1 -1.2 1.6a6 6 0 1 1 8.4 -8.4" />
</svg>
Hostname
</div>
<div class="stat-value text-sm" id="current-hostname">hydrobuddy.local</div>
<div class="stat-desc text-xs">mDNS Active</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cell-signal-5 opacity-70">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M20 20h-15.269a.731 .731 0 0 1 -.517 -1.249l14.537 -14.537a.731 .731 0 0 1 1.249 .517v15.269z" />
<path d="M16 7v13" />
<path d="M12 20v-9" />
<path d="M8 20v-5" />
</svg>
Signal Strength
</div>
<div class="stat-value text-sm" id="signal-strength">-42dBm</div>
<div class="stat-desc text-xs">
<progress class="progress progress-success w-full h-2 mt-1" value="64" max="100" id="signal-progress"></progress>
<div class="flex justify-between text-[10px] mt-1 opacity-70">
<span>-100dBm</span>
<span>-10dBm</span>
</div>
</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-devices opacity-70">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M13 9a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1v-10z" />
<path d="M18 8v-3a1 1 0 0 0 -1 -1h-13a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h9" />
<path d="M16 9h2" />
</svg>
Client Count
</div>
<div class="stat-value text-sm" id="client-count">2</div>
<div class="stat-desc text-xs">Connected Devices</div>
</div>
</div>
</div>
</div>
<!-- Network Info and WiFi Settings -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<!-- Network Info -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Network Info</h2>
<div class="space-y-3 mt-4">
<div class="flex justify-between items-center">
<span class="text-sm font-medium">Hostname</span>
<span class="text-sm" id="info-hostname">hydrobuddy.local</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium">IP Address</span>
<span class="text-sm" id="info-ip">192.168.1.158</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium">Subnet</span>
<span class="text-sm" id="info-subnet">255.255.255.0</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium">Gateway</span>
<span class="text-sm" id="info-gateway">192.168.1.1</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium">MAC Address</span>
<span class="text-sm" id="info-mac">AA:BB:CC:DD:EE:FF</span>
</div>
<div class="flex justify-between items-center">
<span class="text-sm font-medium">Connection Method</span>
<span class="text-sm badge badge-info" id="info-method">WiFi</span>
</div>
<div class="divider my-2"></div>
<div>
<h3 class="text-sm font-medium mb-3">Connected Devices</h3>
<div class="space-y-2">
<div class="flex justify-between items-center p-2 bg-base-300 rounded-lg">
<div class="flex items-center gap-2">
<span class="text-sm">💻</span>
<div>
<div class="text-sm font-medium">laptop-office</div>
<div class="text-xs opacity-70">192.168.1.102</div>
</div>
</div>
<button class="btn btn-xs btn-error" onclick="kickDevice('laptop-office', '192.168.1.102')" title="Disconnect device">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</button>
</div>
<div class="flex justify-between items-center p-2 bg-base-300 rounded-lg">
<div class="flex items-center gap-2">
<span class="text-sm">📱</span>
<div>
<div class="text-sm font-medium">smartphone-dev</div>
<div class="text-xs opacity-70">192.168.1.103</div>
</div>
</div>
<button class="btn btn-xs btn-error" onclick="kickDevice('smartphone-dev', '192.168.1.103')" title="Disconnect device">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</button>
</div>
<div class="flex justify-between items-center p-2 bg-base-300 rounded-lg">
<div class="flex items-center gap-2">
<span class="text-sm">💻</span>
<div>
<div class="text-sm font-medium">tablet-main</div>
<div class="text-xs opacity-70">192.168.1.104</div>
</div>
</div>
<button class="btn btn-xs btn-error" onclick="kickDevice('tablet-main', '192.168.1.104')" title="Disconnect device">
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18M6 6l12 12"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- WiFi Settings -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">WiFi Settings</h2>
<!-- Antenna Selection -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-switch">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 4l4 0l0 4" />
<path d="M14.75 9.25l4.25 -5.25" />
<path d="M5 19l4 -4" />
<path d="M15 19l4 0l0 -4" />
<path d="M5 5l14 14" />
</svg>
Antenna Selection
</span>
</label>
<div class="join">
<input class="join-item btn btn-sm" type="radio" name="antenna" aria-label="📶 Internal" checked id="antenna-internal" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
<input class="join-item btn btn-sm" type="radio" name="antenna" aria-label="🔌 External" id="antenna-external" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
</div>
</div>
<!-- Transmission Power -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-waves-electricity">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M3 12c.576 -.643 1.512 -1.017 2.5 -1c.988 -.017 1.924 .357 2.5 1c.576 .643 1.512 1.017 2.5 1c.988 .017 1.924 -.357 2.5 -1" />
<path d="M3 16c.576 -.643 1.512 -1.017 2.5 -1c.988 -.017 1.924 .357 2.5 1c.576 .643 1.512 1.017 2.5 1c.988 .017 1.924 -.357 2.5 -1" />
<path d="M3 8c.576 -.643 1.512 -1.017 2.5 -1c.988 -.017 1.924 .357 2.5 1c.576 .643 1.512 1.017 2.5 1c.988 .017 1.924 -.357 2.5 -1" />
<path d="M20 7l-3 5h4l-3 5" />
</svg>
Transmission Power
</span>
</label>
<div class="join w-full">
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="1mW" value="1" id="tx-power-1" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="10mW" value="10" id="tx-power-10" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="50mW" value="50" checked id="tx-power-50" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
<input class="join-item btn btn-sm flex-1" type="radio" name="tx-power" aria-label="100mW" value="100" id="tx-power-100" onchange="if('vibrate' in navigator) navigator.vibrate(15)" />
</div>
</div>
<!-- Mode Selection -->
<div class="form-control mb-4">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-home-signal">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 22v-2" />
<path d="M18 22v-4" />
<path d="M21 22v-6" />
<path d="M19 12.494v-.494h2l-9 -9l-9 9h2v7a2 2 0 0 0 2 2h4" />
<path d="M9 21v-6a2 2 0 0 1 2 -2h2a2 2 0 0 1 2 2v.5" />
</svg>
WiFi Mode
</span>
</label>
<div class="join w-full">
<button class="btn join-item flex-1" id="station-mode-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-router">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M3 13m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z" />
<path d="M17 17l0 .01" />
<path d="M13 17l0 .01" />
<path d="M15 13l0 -2" />
<path d="M11.75 8.75a4 4 0 0 1 6.5 0" />
<path d="M8.5 6.5a8 8 0 0 1 13 0" />
</svg>
Station
</button>
<button class="btn join-item flex-1 btn-active" id="ap-mode-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-wifi">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 18l.01 0" />
<path d="M9.172 15.172a4 4 0 0 1 5.656 0" />
<path d="M6.343 12.343a8 8 0 0 1 11.314 0" />
<path d="M3.515 9.515c4.686 -4.687 12.284 -4.687 17 0" />
</svg>
Access Point
</button>
</div>
</div>
<!-- Station Mode Settings -->
<div id="station-settings" class="space-y-4 hidden">
<div class="form-control">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-label">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M16.52 7h-10.52a2 2 0 0 0 -2 2v6a2 2 0 0 0 2 2h10.52a1 1 0 0 0 .78 -.375l3.7 -4.625l-3.7 -4.625a1 1 0 0 0 -.78 -.375" />
</svg>
Network SSID
</span>
</label>
<div class="join w-full">
<input type="text" class="input input-bordered join-item flex-1" placeholder="Enter SSID" id="station-ssid" />
<button class="btn btn-outline join-item" id="scan-networks-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15); openNetworkScanModal()">Scan</button>
</div>
</div>
<div class="form-control">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-key">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z" />
<path d="M15 9h.01" />
</svg>
Password
</span>
</label>
<input type="password" class="input input-bordered w-full" placeholder="WiFi Password" id="station-password" minlength="8" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shield-lock">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
<path d="M12 11m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M12 12l0 2.5" />
</svg>
Security
</span>
</label>
<select class="select select-bordered w-full" id="station-security">
<option value="wpa2" selected>WPA2</option>
<option value="wpa3">WPA3</option>
<option value="open">Open</option>
</select>
</div>
</div>
<!-- Access Point Mode Settings -->
<div id="ap-settings">
<div class="form-control mb-4">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-label">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M16.52 7h-10.52a2 2 0 0 0 -2 2v6a2 2 0 0 0 2 2h10.52a1 1 0 0 0 .78 -.375l3.7 -4.625l-3.7 -4.625a1 1 0 0 0 -.78 -.375" />
</svg>
Access Point SSID
</span>
</label>
<input type="text" class="input input-bordered w-full" value="Alpha-Driver-AP" id="ap-ssid" />
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-key">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M16.555 3.843l3.602 3.602a2.877 2.877 0 0 1 0 4.069l-2.643 2.643a2.877 2.877 0 0 1 -4.069 0l-.301 -.301l-6.558 6.558a2 2 0 0 1 -1.239 .578l-.175 .008h-1.172a1 1 0 0 1 -.993 -.883l-.007 -.117v-1.172a2 2 0 0 1 .467 -1.284l.119 -.13l.414 -.414h2v-2h2v-2l2.144 -2.144l-.301 -.301a2.877 2.877 0 0 1 0 -4.069l2.643 -2.643a2.877 2.877 0 0 1 4.069 0z" />
<path d="M15 9h.01" />
</svg>
Password
</span>
</label>
<input type="password" class="input input-bordered w-full" placeholder="Min. 8 characters" id="ap-password" minlength="8"
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}"
title="Must be more than 8 characters, including number, lowercase letter, uppercase letter" />
<p class="text-xs text-base-content/70 mt-1 hidden validator-hint">
Must be more than 8 characters, including<br/>
At least one number<br/>
At least one lowercase letter<br/>
At least one uppercase letter
</p>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">WiFi Channel</span>
</label>
<select class="select select-bordered w-full" id="ap-channel">
<option value="auto" selected>Auto</option>
<option value="1">Channel 1 (2.412 GHz)</option>
<option value="2">Channel 2 (2.417 GHz)</option>
<option value="3">Channel 3 (2.422 GHz)</option>
<option value="4">Channel 4 (2.427 GHz)</option>
<option value="5">Channel 5 (2.432 GHz)</option>
<option value="6">Channel 6 (2.437 GHz)</option>
<option value="7">Channel 7 (2.442 GHz)</option>
<option value="8">Channel 8 (2.447 GHz)</option>
<option value="9">Channel 9 (2.452 GHz)</option>
<option value="10">Channel 10 (2.457 GHz)</option>
<option value="11">Channel 11 (2.462 GHz)</option>
<option value="12">Channel 12 (2.467 GHz)</option>
<option value="13">Channel 13 (2.472 GHz)</option>
</select>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-shield-lock">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 3a12 12 0 0 0 8.5 3a12 12 0 0 1 -8.5 15a12 12 0 0 1 -8.5 -15a12 12 0 0 0 8.5 -3" />
<path d="M12 11m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
<path d="M12 12l0 2.5" />
</svg>
Security
</span>
</label>
<select class="select select-bordered w-full" id="ap-security">
<option value="wpa2" selected>WPA2</option>
<option value="wpa3">WPA3</option>
<option value="open">Open (Not Recommended)</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- General Network Settings -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">General Network Settings</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
<div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">IP Configuration</span>
</label>
<div class="join w-full">
<button class="btn join-item flex-1 btn-active" id="dhcp-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">DHCP</button>
<button class="btn join-item flex-1" id="static-btn" onclick="if('vibrate' in navigator) navigator.vibrate(15)">Static IP</button>
</div>
</div>
<!-- Static IP Settings -->
<div id="static-ip-settings" class="space-y-2 hidden bg-base-300 rounded-lg p-3 mb-4">
<div class="form-control">
<label class="label">
<span class="label-text">IP Address</span>
</label>
<input type="text" class="input input-bordered w-full" placeholder="192.168.1.100" id="static-ip-addr" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Subnet Mask</span>
</label>
<input type="text" class="input input-bordered w-full" value="255.255.255.0" id="subnet-mask" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">Gateway</span>
</label>
<input type="text" class="input input-bordered w-full" placeholder="192.168.1.1" id="gateway" />
</div>
<div class="form-control">
<label class="label">
<span class="label-text">DNS Server</span>
</label>
<input type="text" class="input input-bordered w-full" placeholder="8.8.8.8" id="dns-server" />
</div>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Hostname (mDNS)</span>
<span class="label-text-alt">Format: hydrobuddy.local</span>
</label>
<label class="input input-bordered flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-radar opacity-70">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M12 10a2 2 0 0 1 1.678 .911l.053 .089h7.269l.117 .007a1 1 0 0 1 .883 .993c0 5.523 -4.477 10 -10 10a1 1 0 0 1 -1 -1v-7.269l-.089 -.053a2 2 0 0 1 -.906 -1.529l-.005 -.149a2 2 0 0 1 2 -2m9.428 -1.334a1 1 0 0 1 -1.884 .668a8 8 0 1 0 -10.207 10.218a1 1 0 0 1 -.666 1.886a10 10 0 1 1 12.757 -12.772m-4.628 -.266a1 1 0 0 1 -1.6 1.2a4 4 0 1 0 -5.6 5.6a1 1 0 0 1 -1.2 1.6a6 6 0 1 1 8.4 -8.4" />
</svg>
<input type="text" class="grow" value="alpha" id="hostname" pattern="[a-zA-Z0-9-]+" title="Only letters, numbers, and hyphens allowed" />
<span class="opacity-70">.local</span>
</label>
<div class="label">
<span class="label-text-alt">Only letters, numbers, and hyphens allowed</span>
</div>
</div>
</div>
<div>
<div class="form-control mb-4">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Enable mDNS Discovery</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-mdns" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
<div class="form-control mb-4">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-refresh-dot">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
<path d="M12 12m-1 0a1 1 0 1 0 2 0a1 1 0 1 0 -2 0" />
</svg>
Auto-reconnect WiFi
</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-auto-reconnect" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Network Scan Modal -->
<dialog id="network-scan-modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Available Networks</h3>
<div class="py-4">
<div class="space-y-2" id="scanned-networks">
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('HomeNetwork_5G', 'WPA2')">
<div>
<div class="font-semibold">HomeNetwork_5G</div>
<div class="text-xs opacity-70">WPA2 • -35dBm</div>
</div>
<div class="text-xs">🔒</div>
</div>
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('OfficeWiFi', 'WPA3')">
<div>
<div class="font-semibold">OfficeWiFi</div>
<div class="text-xs opacity-70">WPA3 • -52dBm</div>
</div>
<div class="text-xs">🔒</div>
</div>
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('GuestNetwork', 'Open')">
<div>
<div class="font-semibold">GuestNetwork</div>
<div class="text-xs opacity-70">Open • -68dBm</div>
</div>
<div class="text-xs">📶</div>
</div>
<div class="flex justify-between items-center p-3 hover:bg-base-200 rounded cursor-pointer" onclick="selectNetwork('CafeWiFi', 'WPA2')">
<div>
<div class="font-semibold">CafeWiFi</div>
<div class="text-xs opacity-70">WPA2 • -71dBm</div>
</div>
<div class="text-xs">🔒</div>
</div>
</div>
</div>
<div class="modal-action">
<button class="btn btn-outline" onclick="refreshNetworkScan()">Refresh</button>
<button class="btn" onclick="closeNetworkScanModal()">Close</button>
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
// Toggle color management function
function updateToggleColor(toggle: HTMLInputElement) {
if (toggle.checked) {
toggle.classList.remove('toggle-error');
toggle.classList.add('toggle-success');
} else {
toggle.classList.remove('toggle-success');
toggle.classList.add('toggle-error');
}
}
// Device kick function
function kickDevice(deviceName: string, deviceIP: string) {
if ('vibrate' in navigator) navigator.vibrate([100, 50, 100]);
// Show confirmation
if (confirm(`Disconnect ${deviceName} (${deviceIP})?`)) {
console.log(`Kicking device: ${deviceName} at ${deviceIP}`);
// In a real implementation, you would make an API call here
// For now, we'll just show a toast or update the UI
const button = event?.target as HTMLElement;
const deviceRow = button?.closest('.flex');
if (deviceRow && deviceRow instanceof HTMLElement) {
deviceRow.style.opacity = '0.5';
setTimeout(() => {
deviceRow.remove();
// Update client count
const clientCount = document.getElementById('client-count');
if (clientCount) {
const current = parseInt(clientCount.textContent || '0');
clientCount.textContent = (current - 1).toString();
}
}, 1000);
}
}
}
// Network scan modal functions
function openNetworkScanModal() {
const modal = document.getElementById('network-scan-modal') as HTMLDialogElement;
modal?.showModal();
}
function closeNetworkScanModal() {
const modal = document.getElementById('network-scan-modal') as HTMLDialogElement;
modal?.close();
}
function selectNetwork(ssid: string, security: string) {
const ssidInput = document.getElementById('station-ssid') as HTMLInputElement;
const securitySelect = document.getElementById('station-security') as HTMLSelectElement;
if (ssidInput) ssidInput.value = ssid;
if (securitySelect) {
securitySelect.value = security.toLowerCase();
}
closeNetworkScanModal();
if ('vibrate' in navigator) navigator.vibrate(15);
}
function refreshNetworkScan() {
// Simulate refreshing the network list
if ('vibrate' in navigator) navigator.vibrate(15);
console.log('Refreshing network scan...');
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize toggle colors
const toggles = document.querySelectorAll('.toggle');
toggles.forEach(toggle => {
updateToggleColor(toggle as HTMLInputElement);
});
// Password validation
const passwordInputs = document.querySelectorAll('input[type="password"][pattern]');
passwordInputs.forEach(input => {
const hint = (input as HTMLInputElement).parentElement?.nextElementSibling;
input.addEventListener('focus', function() {
if (hint && hint.classList.contains('validator-hint')) {
hint.classList.remove('hidden');
}
});
input.addEventListener('blur', function() {
if (hint && hint.classList.contains('validator-hint') && (input as HTMLInputElement).validity.valid) {
hint.classList.add('hidden');
}
});
input.addEventListener('input', function() {
const isValid = (input as HTMLInputElement).validity.valid;
const parentElement = (input as HTMLInputElement).parentElement;
if (isValid) {
parentElement?.classList.remove('input-error');
parentElement?.classList.add('input-success');
if (hint) hint.classList.add('hidden');
} else {
parentElement?.classList.remove('input-success');
parentElement?.classList.add('input-error');
if (hint) hint.classList.remove('hidden');
}
});
});
// Update signal strength progress bar
function updateSignalStrength(dbm: number) {
const signalProgress = document.getElementById('signal-progress');
const signalStrength = document.getElementById('signal-strength');
if (signalProgress && signalStrength) {
// Convert dBm to percentage (-100dBm = 0%, -10dBm = 100%)
const percentage = Math.max(0, Math.min(100, ((dbm + 100) / 90) * 100));
(signalProgress as HTMLProgressElement).value = percentage;
signalStrength.textContent = dbm + 'dBm';
// Update progress bar color based on signal strength
signalProgress.className = 'progress w-full h-2 mt-1 ' +
(percentage >= 70 ? 'progress-success' :
percentage >= 40 ? 'progress-warning' : 'progress-error');
}
}
// Initialize with current signal strength
updateSignalStrength(-42);
// Mode switching
const stationBtn = document.getElementById('station-mode-btn');
const apBtn = document.getElementById('ap-mode-btn');
const stationSettings = document.getElementById('station-settings');
const apSettings = document.getElementById('ap-settings');
function switchToStation() {
stationBtn?.classList.add('btn-active');
apBtn?.classList.remove('btn-active');
stationSettings?.classList.remove('hidden');
apSettings?.classList.add('hidden');
}
function switchToAP() {
apBtn?.classList.add('btn-active');
stationBtn?.classList.remove('btn-active');
apSettings?.classList.remove('hidden');
stationSettings?.classList.add('hidden');
}
stationBtn?.addEventListener('click', switchToStation);
apBtn?.addEventListener('click', switchToAP);
// IP Configuration switching
const dhcpBtn = document.getElementById('dhcp-btn');
const staticBtn = document.getElementById('static-btn');
const staticSettings = document.getElementById('static-ip-settings');
dhcpBtn?.addEventListener('click', function() {
dhcpBtn.classList.add('btn-active');
staticBtn?.classList.remove('btn-active');
staticSettings?.classList.add('hidden');
});
staticBtn?.addEventListener('click', function() {
staticBtn.classList.add('btn-active');
dhcpBtn?.classList.remove('btn-active');
staticSettings?.classList.remove('hidden');
});
// Hostname validation
const hostnameInput = document.getElementById('hostname');
hostnameInput?.addEventListener('input', function() {
const value = (this as HTMLInputElement).value;
const valid = /^[a-zA-Z0-9-]+$/.test(value) && value.length > 0;
const parentLabel = this.closest('label');
if (!valid && value !== '') {
parentLabel?.classList.add('input-error');
} else {
parentLabel?.classList.remove('input-error');
}
});
// Initialize with Access Point mode
switchToAP();
});
</script>

View file

@ -0,0 +1,78 @@
---
// Import all page components
import DashboardPage from "./DashboardPage.astro";
import TestbenchPage from "./TestbenchPage.astro";
import MonitoringPage from "./MonitoringPage.astro";
import ControlPage from "./ControlPage.astro";
import DcdcPage from "./DcdcPage.astro";
import ModulationPage from "./ModulationPage.astro";
import LogsPage from "./LogsPage.astro";
import NetworkPage from "./NetworkPage.astro";
import SettingsPage from "./SettingsPage.astro";
import SecurityPage from "./SecurityPage.astro";
---
<div id="spa-content bg-base-100">
<!-- All pages pre-rendered, dashboard visible by default -->
<div id="page-dashboard" class="page-content">
<DashboardPage />
</div>
<div id="page-testbench" class="page-content" style="display: none;">
<TestbenchPage />
</div>
<div id="page-monitoring" class="page-content" style="display: none;">
<MonitoringPage />
</div>
<div id="page-control" class="page-content" style="display: none;">
<ControlPage />
</div>
<div id="page-dcdc" class="page-content" style="display: none;">
<DcdcPage />
</div>
<div id="page-modulation" class="page-content" style="display: none;">
<ModulationPage />
</div>
<div id="page-logs" class="page-content" style="display: none;">
<LogsPage />
</div>
<div id="page-network" class="page-content" style="display: none;">
<NetworkPage />
</div>
<div id="page-settings" class="page-content" style="display: none;">
<SettingsPage />
</div>
<div id="page-security" class="page-content" style="display: none;">
<SecurityPage />
</div>
</div>
<script>
// Navigation handling
window.navigateTo = function(path) {
// Hide all pages first
document.querySelectorAll('.page-content').forEach(page => {
(page as HTMLElement).style.display = 'none';
});
// Show the selected page
const pageId = `page-${path.slice(1)}`; // Remove leading slash and add 'page-' prefix
const selectedPage = document.getElementById(pageId) as HTMLElement;
if (selectedPage) {
// show navigated page
selectedPage.style.display = 'block';
} else {
// If no matching page found, show dashboard
const dashboard = document.getElementById('page-dashboard') as HTMLElement;
if (dashboard) {
dashboard.style.display = 'block';
}
}
};
// Show dashboard by default
const dashboard = document.getElementById('page-dashboard') as HTMLElement;
if (dashboard) {
dashboard.style.display = 'block';
}
</script>

View file

@ -0,0 +1,864 @@
---
const pageTitle = "Security";
---
<div class="flex flex-col gap-4">
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Web Access Security</h2>
<div class="w-full max-w-md mx-auto">
<input id="security-slider" type="range" min="0" max="3" value="0" class="range range-error" step="1" />
<div class="flex justify-between px-2.5 mt-2 text-xs">
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="0">
<!-- None/0: Alert triangle icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-alert-triangle"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 1.67c.955 0 1.845 .467 2.39 1.247l.105 .16l8.114 13.548a2.914 2.914 0 0 1 -2.307 4.363l-.195 .008h-16.225a2.914 2.914 0 0 1 -2.582 -4.2l.099 -.185l8.11 -13.538a2.914 2.914 0 0 1 2.491 -1.403zm.01 13.33l-.127 .007a1 1 0 0 0 0 1.986l.117 .007l.127 -.007a1 1 0 0 0 0 -1.986l-.117 -.007zm-.01 -7a1 1 0 0 0 -.993 .883l-.007 .117v4l.007 .117a1 1 0 0 0 1.986 0l.007 -.117v-4l-.007 -.117a1 1 0 0 0 -.993 -.883z" /></svg>
</span>
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="1">
<!-- Button icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-transition-bottom"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M21 17a1 1 0 0 1 1 1a4 4 0 0 1 -4 4h-12a4 4 0 0 1 -4 -4a1 1 0 0 1 2 0a2 2 0 0 0 2 2h12a2 2 0 0 0 1.995 -1.85l.005 -.15a1 1 0 0 1 1 -1m-9 1l-.082 -.004l-.119 -.016l-.111 -.03l-.111 -.044l-.098 -.052l-.096 -.067l-.09 -.08l-3 -3a1 1 0 0 1 1.414 -1.414l1.293 1.293v-4.586h-5a4 4 0 1 1 0 -8h12a4 4 0 1 1 0 8h-5v4.583l1.293 -1.29a1 1 0 0 1 1.32 -.083l.094 .083a1 1 0 0 1 0 1.414l-3 3l-.112 .097l-.11 .071l-.062 .031l-.081 .034l-.076 .024l-.149 .03z" /></svg>
</span>
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="2">
<!-- Pin icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M18 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M4 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M18 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /><path d="M11 17h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z" /></svg>
</span>
<span class="cursor-pointer hover:opacity-70 transition-opacity" data-slider-value="3">
<!-- Password icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-keyboard"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M2 6m0 2a2 2 0 0 1 2 -2h16a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-16a2 2 0 0 1 -2 -2z" /><path d="M6 10l0 .01" /><path d="M10 10l0 .01" /><path d="M14 10l0 .01" /><path d="M18 10l0 .01" /><path d="M6 14l0 .01" /><path d="M18 14l0 .01" /><path d="M10 14l4 .01" /></svg>
</span>
</div>
<div class="flex justify-center mt-4">
<span id="security-mode-badge" class="badge text-xl px-6 py-3 font-bold">None</span>
</div>
<div class="flex justify-center mt-2">
<span id="security-mode-explanation" class="text-center text-sm">No security, open access.</span>
</div>
<!-- Fixed height container for mode-specific content to ensure consistent Save button position -->
<div class="w-full flex flex-col items-center" style="min-height: 200px;">
<div id="none-warning" class="flex flex-col items-center mt-4" style="display:none;">
<svg xmlns="http://www.w3.org/2000/svg" width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-cloud-lock-open text-error"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M19 18a3.5 3.5 0 0 0 0 -7h-1c.397 -1.768 -.285 -3.593 -1.788 -4.787c-1.503 -1.193 -3.6 -1.575 -5.5 -1s-3.315 2.019 -3.712 3.787c-2.199 -.088 -4.155 1.326 -4.666 3.373c-.512 2.047 .564 4.154 2.566 5.027" /><path d="M8 15m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v3a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z" /><path d="M10 15v-2a2 2 0 0 1 3.736 -1" /></svg>
<span class="text-error font-bold text-base mt-2">NOT RECOMMENDED</span>
</div>
<div id="button-toggle-row" class="flex flex-col items-center mt-16" style="display:none;">
<label class="flex items-center gap-3">
<span>Allow permanent access to devices</span>
<input id="button-permanent-toggle" type="checkbox" class="toggle toggle-error" />
</label>
</div>
<div id="security-input-fields" class="flex flex-col items-center mt-4 w-full" style="display:none;">
<input id="security-input" type="password" maxlength="8" placeholder="Enter PIN/Password" class="input input-bordered mb-2 w-full" />
<input id="security-confirm" type="password" maxlength="8" placeholder="Confirm PIN/Password" class="input input-bordered mb-2 w-full" />
</div>
<!-- Spacer to push Save button to bottom of container -->
<div class="flex-1"></div>
<div class="flex justify-center mt-4">
<button id="security-save" class="btn btn-primary">Save</button>
</div>
</div>
</div>
</div>
</div>
<div id="security-modals-buttons" class="flex flex-col gap-2 items-start mt-4">
<button class="btn btn-accent" id="open-test-modal">Test Modal</button>
<button class="btn btn-primary" id="open-pin-modal">PIN Login</button>
<button class="btn btn-secondary" id="open-password-modal">Password Login</button>
<button class="btn btn-info" id="open-updater-modal">Software Update</button>
</div>
<!-- Development Controls for Updates -->
<div class="card bg-base-100 shadow mt-4">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Development Controls</h2>
<p class="text-sm text-base-content/70 mb-4">Toggle individual firmware update availability for testing purposes.</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<label class="cursor-pointer label justify-start gap-3">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked
id="toggle-alpha-core"
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
/>
<span class="label-text">Alpha-Core Update</span>
</label>
<label class="cursor-pointer label justify-start gap-3">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked
id="toggle-filesystem"
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
/>
<span class="label-text">Filesystem (Web Interface)</span>
</label>
<label class="cursor-pointer label justify-start gap-3">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked
id="toggle-wifi"
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
/>
<span class="label-text">WiFi Companion</span>
</label>
<label class="cursor-pointer label justify-start gap-3">
<input
type="checkbox"
class="toggle toggle-primary toggle-sm"
checked
id="toggle-laser-cpu"
onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)"
/>
<span class="label-text">Laser Core CPU</span>
</label>
</div>
<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>These controls simulate firmware update availability. In production, this information comes from the device via WebSocket.</span>
</div>
</div>
</div>
</div>
<!-- DaisyUI Modal for Test (Hold Button) -->
<dialog id="test-modal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<form id="test-modal-form" class="flex flex-col gap-4" method="dialog" onsubmit="return false;">
<h3 class="font-bold text-lg text-center">Device Registration</h3>
<label class="floating-label">
<div class="input input-bordered flex items-center gap-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-edit opacity-70">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M7 7h-1a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2 -2v-1"/>
<path d="M20.385 6.585a2.1 2.1 0 0 0 -2.97 -2.97l-8.415 8.385v3h3l8.385 -8.415z"/>
<path d="M16 5l3 3"/>
</svg>
<input type="text" id="device-name" class="grow bg-transparent border-none outline-none" placeholder=" " required />
<span>Device Name</span>
</div>
</label>
<div class="flex flex-col gap-2">
<div class="flex gap-4 items-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-devices">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M13 9a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v10a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1v-10z"/>
<path d="M18 8v-3a1 1 0 0 0 -1 -1h-13a1 1 0 0 0 -1 1v12a1 1 0 0 0 1 1h9"/>
<path d="M16 9h2"/>
</svg>
<span id="device-type" class="text-sm text-gray-500">Unknown</span>
</div>
<div class="flex gap-4 items-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-password-fingerprint">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M17 8c.788 1 1 2 1 3v1"/>
<path d="M9 11c0 -1.578 1.343 -3 3 -3s3 1.422 3 3v2"/>
<path d="M12 11v2"/>
<path d="M6 12v-1.397c-.006 -1.999 1.136 -3.849 2.993 -4.85a6.385 6.385 0 0 1 6.007 -.005"/>
<path d="M12 17v4"/>
<path d="M10 20l4 -2"/>
<path d="M10 18l4 2"/>
<path d="M5 17v4"/>
<path d="M3 20l4 -2"/>
<path d="M3 18l4 2"/>
<path d="M19 17v4"/>
<path d="M17 20l4 -2"/>
<path d="M17 18l4 2"/>
</svg>
<span class="text-sm text-gray-500">01234</span>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="font-semibold mb-1 flex items-center gap-2">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-hourglass-empty">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M6 20v-2a6 6 0 1 1 12 0v2a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1z"/>
<path d="M6 4v2a6 6 0 1 0 12 0v-2a1 1 0 0 0 -1 -1h-10a1 1 0 0 0 -1 1z"/>
</svg>
Access Expiration
</span>
<div class="w-full flex flex-col sm:flex-row gap-2">
<div class="join join-vertical lg:join-horizontal w-full">
<button type="button" id="exp-session" class="btn join-item btn-outline btn-active flex-1">Session</button>
<button type="button" id="exp-lifetime" class="btn join-item btn-outline flex-1">Lifetime</button>
</div>
</div>
</div>
<div class="flex flex-col items-center gap-2 mt-2">
<span class="text-sm text-gray-500">Hold the Unlock <b>Button on the Alpha Laserdriver</b> for 3 seconds to authenticate.</span>
<button type="button" id="hold-confirm" class="btn btn-error w-full transition-colors duration-300 text-lg" style="background-color: #dc2626;">
<span id="hold-text" class="text-lg">Hold to Authenticate</span>
</button>
<div id="success-message" class="hidden flex flex-col items-center text-success mt-2">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
<span class="success-detail">Device added for this session.</span>
</div>
</div>
</form>
</div>
</dialog>
<!-- PIN Modal -->
<dialog id="pin-modal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<form id="pin-modal-form" class="flex flex-col gap-6" method="dialog" onsubmit="return false;">
<h3 class="font-bold text-lg text-center">PIN Login</h3>
<label class="input input-bordered flex items-center gap-2">
<span class="flex items-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4"/></svg>
</span>
<input type="password" id="pin-input" class="grow" placeholder="Enter PIN" required maxlength="8" inputmode="numeric" pattern="[0-9]*" />
<button type="button" id="toggle-keypad" tabindex="-1" class="ml-2">
<svg id="keypad-open-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M18 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 3h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M4 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M18 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 10h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/><path d="M11 17h2a1 1 0 0 1 1 1v2a1 1 0 0 1 -1 1h-2a1 1 0 0 1 -1 -1v-2a1 1 0 0 1 1 -1z"/></svg>
<svg id="keypad-close-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-dialpad-off" style="display:none"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M7 7h-4v-4"/><path d="M17 3h4v4h-4z"/><path d="M10 6v-3h4v4h-3"/><path d="M3 10h4v4h-3"/><path d="M17 13v-3h4v4h-3"/><path d="M14 14h-4v-4"/><path d="M10 17h4v4h-4z"/><path d="M3 3l18 18"/></svg>
</button>
</label>
<div id="pin-keypad-container" class="w-full flex justify-center items-center" style="display:none;min-height:0;">
<div class="pin-keypad grid place-items-center w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md">
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">1</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">2</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">3</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">4</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">5</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">6</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">7</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">8</button>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">9</button>
<div></div>
<button type="button" class="btn btn-primary btn-outline btn-lg btn-square pin-key">0</button>
<div></div>
</div>
</div>
<button type="submit" id="pin-submit" class="btn btn-success w-full text-lg flex items-center justify-center gap-2">Login
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-login-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8v-2a2 2 0 0 1 2 -2h7a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-2"/><path d="M3 12h13l-3 -3"/><path d="M13 15l3 -3"/></svg>
</button>
<div id="pin-success-message" class="hidden flex flex-col items-center text-success mt-2">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<span>Access granted!</span>
</div>
</form>
</div>
</dialog>
<!-- Password Modal -->
<dialog id="password-modal" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<form id="password-modal-form" class="flex flex-col gap-6" method="dialog" onsubmit="return false;">
<h3 class="font-bold text-lg text-center">Password Login</h3>
<label class="input input-bordered flex items-center gap-2">
<span class="flex items-center">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-key"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14.52 2c1.029 0 2.015 .409 2.742 1.136l3.602 3.602a3.877 3.877 0 0 1 0 5.483l-2.643 2.643a3.88 3.88 0 0 1 -4.941 .452l-.105 -.078l-5.882 5.883a3 3 0 0 1 -1.68 .843l-.22 .027l-.221 .009h-1.172c-1.014 0 -1.867 -.759 -1.991 -1.823l-.009 -.177v-1.172c0 -.704 .248 -1.386 .73 -1.96l.149 -.161l.414 -.414a1 1 0 0 1 .707 -.293h1v-1a1 1 0 0 1 .883 -.993l.117 -.007h1v-1a1 1 0 0 1 .206 -.608l.087 -.1l1.468 -1.469l-.076 -.103a3.9 3.9 0 0 1 -.678 -1.963l-.007 -.236c0 -1.029 .409 -2.015 1.136 -2.742l2.643 -2.643a3.88 3.88 0 0 1 2.741 -1.136m.495 5h-.02a2 2 0 1 0 0 4h.02a2 2 0 1 0 0 -4"/></svg>
</span>
<input type="password" id="password-input" class="grow" placeholder="Enter Password" required maxlength="64" />
</label>
<button type="submit" id="password-submit" class="btn btn-success w-full text-lg flex items-center justify-center gap-2">Login
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-login-2"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8v-2a2 2 0 0 1 2 -2h7a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-7a2 2 0 0 1 -2 -2v-2"/><path d="M3 12h13l-3 -3"/><path d="M13 15l3 -3"/></svg>
</button>
<div id="password-success-message" class="hidden flex flex-col items-center text-success mt-2">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
<span>Access granted!</span>
</div>
</form>
</div>
</dialog>
<script>
// Toggle color management function
function updateToggleColor(toggle: HTMLInputElement) {
if (toggle.checked) {
toggle.classList.remove('toggle-error');
toggle.classList.add('toggle-success');
} else {
toggle.classList.remove('toggle-success');
toggle.classList.add('toggle-error');
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize toggle colors for all toggles
const allToggles = document.querySelectorAll('.toggle');
allToggles.forEach(toggle => {
updateToggleColor(toggle as HTMLInputElement);
});
/*
// Function to update mock update availability based on toggles
window.updateAvailability = function() {
// Get toggle states
const alphaCoreToggle = document.getElementById('toggle-alpha-core') as HTMLInputElement;
const filesystemToggle = document.getElementById('toggle-filesystem') as HTMLInputElement;
const wifiToggle = document.getElementById('toggle-wifi') as HTMLInputElement;
const laserCpuToggle = document.getElementById('toggle-laser-cpu') as HTMLInputElement;
// Update global mock data (this would be replaced by WebSocket in production)
if (typeof window.mockUpdates !== 'undefined') {
window.mockUpdates[0].updateAvailable = alphaCoreToggle?.checked || false;
window.mockUpdates[1].updateAvailable = filesystemToggle?.checked || false;
window.mockUpdates[2].updateAvailable = laserCpuToggle?.checked || false;
window.mockUpdates[3].updateAvailable = wifiToggle?.checked || false;
}
// Check if any updates are available
const hasUpdates = (alphaCoreToggle?.checked || false) ||
(filesystemToggle?.checked || false) ||
(wifiToggle?.checked || false) ||
(laserCpuToggle?.checked || false);
// Show/hide notification
const notification = document.getElementById('update-notification');
if (notification) {
if (hasUpdates) {
notification.classList.remove('hidden');
} else {
notification.classList.add('hidden');
}
}
// Show/hide sidebar update button
const updateLink = document.getElementById('update-link');
if (updateLink) {
if (hasUpdates) {
updateLink.style.display = '';
} else {
updateLink.style.display = 'none';
}
}
};*/
/*
// Add event listeners to all toggles
const toggles = ['toggle-alpha-core', 'toggle-filesystem', 'toggle-wifi', 'toggle-laser-cpu'];
toggles.forEach(toggleId => {
const toggle = document.getElementById(toggleId);
if (toggle) {
toggle.addEventListener('change', updateAvailability);
}
});
// Initialize update availability on page load
setTimeout(() => {
if (typeof window.updateAvailability === 'function') {
window.updateAvailability();
}
}, 100);*/
// Test Modal logic
const openTestModalBtn = document.getElementById('open-test-modal');
const testModal = document.getElementById('test-modal');
const holdBtn = document.getElementById('hold-confirm') as HTMLButtonElement | null;
const holdText = document.getElementById('hold-text');
const successMsg = document.getElementById('success-message');
let holdTimeout: number | undefined, holdStart: number, holdInterval: number | undefined;
const holdDuration = 3000; // 3 seconds
if (openTestModalBtn && testModal) {
openTestModalBtn.addEventListener('click', () => {
try {
if ((testModal as HTMLDialogElement).open) {
(testModal as HTMLDialogElement).close();
}
(testModal as HTMLDialogElement).showModal();
// Reset device name input without focusing
const deviceNameInput = document.getElementById('device-name') as HTMLInputElement | null;
if (deviceNameInput) {
deviceNameInput.value = '';
deviceNameInput.blur(); // Ensure no focus
}
if (holdBtn) {
holdBtn.disabled = false;
holdBtn.classList.remove('btn-success');
holdBtn.classList.add('btn-error');
holdBtn.style.backgroundColor = '#dc2626';
holdBtn.classList.remove('animate-stripes');
holdBtn.classList.add('text-black');
holdBtn.classList.remove('text-white');
holdBtn.classList.remove('breathing-orange');
}
if (holdText) {
holdText.textContent = 'Hold to Authenticate';
holdText.classList.add('text-black');
holdText.classList.remove('text-white');
}
if (successMsg) successMsg.classList.add('hidden');
} catch (error) {
console.error('Error opening test modal:', error);
}
});
}
if (holdBtn) {
function getHoldColor(progress: number): { color: string, textBlack: boolean } {
let hue: number, sat: number, light: number;
if (progress < 0.85) {
hue = 20 + (25 * (progress / 0.85));
sat = 100;
light = 45 + (10 * (progress / 0.85));
} else {
hue = 45 + (75 * ((progress - 0.85) / 0.15));
sat = 100;
light = 55;
}
const textBlack = hue >= 40;
return { color: `hsl(${hue}, ${sat}%, ${light}%)`, textBlack };
}
const setBtnColor = (color: string, breathing = false, textBlack = false) => {
holdBtn.style.backgroundColor = color;
if (breathing) {
holdBtn.classList.add('breathing-orange');
} else {
holdBtn.classList.remove('breathing-orange');
}
holdBtn.classList.add('text-black');
holdBtn.classList.remove('text-white');
if (holdText) {
holdText.classList.add('text-black');
holdText.classList.remove('text-white');
}
};
const updateProgress = () => {
try {
const elapsed = Date.now() - holdStart;
const progress = Math.min(elapsed / holdDuration, 1);
const { color, textBlack } = getHoldColor(progress);
if (progress < 1) {
setBtnColor(color, true, textBlack);
} else {
setBtnColor(getHoldColor(1).color, false, true);
holdBtn.disabled = true;
holdBtn.classList.remove('btn-error');
holdBtn.classList.add('btn-success');
if (holdText) holdText.textContent = 'Confirmed!';
if (successMsg) {
const detail = successMsg.querySelector('.success-detail') as HTMLElement | null;
if (detail) {
if (typeof expirationValue !== 'undefined' && expirationValue === 'lifetime') {
detail.textContent = 'Device added permanently.';
} else {
detail.textContent = 'Device added for this session.';
}
}
successMsg.classList.remove('hidden');
}
setTimeout(() => {
(testModal as HTMLDialogElement).close();
}, 3000);
}
} catch (error) {
console.error('Error updating progress:', error);
}
};
// Use both mouse and touch events for better mobile support
const startHold = () => {
try {
if (holdBtn.disabled) return;
holdStart = Date.now();
setBtnColor('hsl(20, 100%, 45%)', true, false);
holdInterval = window.setInterval(updateProgress, 50);
holdTimeout = window.setTimeout(() => {
updateProgress();
}, holdDuration);
if (holdText) holdText.textContent = 'Keep Holding...';
} catch (error) {
console.error('Error starting hold:', error);
}
};
const endHold = () => {
try {
if (holdBtn.disabled) return;
clearInterval(holdInterval);
clearTimeout(holdTimeout);
setBtnColor('#dc2626', false, false);
if (holdText) holdText.textContent = 'Hold to Authenticate';
} catch (error) {
console.error('Error ending hold:', error);
}
};
// Add both mouse and touch event listeners
holdBtn.addEventListener('mousedown', startHold);
holdBtn.addEventListener('touchstart', startHold, { passive: true });
holdBtn.addEventListener('mouseup', endHold);
holdBtn.addEventListener('touchend', endHold, { passive: true });
holdBtn.addEventListener('mouseleave', endHold);
holdBtn.addEventListener('touchcancel', endHold, { passive: true });
}
// Expiration button logic
const expSessionBtn = document.getElementById('exp-session');
const expLifetimeBtn = document.getElementById('exp-lifetime');
let expirationValue = 'session';
if (expSessionBtn && expLifetimeBtn) {
expSessionBtn.addEventListener('click', function() {
try {
expSessionBtn.classList.add('btn-active');
expLifetimeBtn.classList.remove('btn-active');
expirationValue = 'session';
} catch (error) {
console.error('Error setting session expiration:', error);
}
});
expLifetimeBtn.addEventListener('click', function() {
try {
expLifetimeBtn.classList.add('btn-active');
expSessionBtn.classList.remove('btn-active');
expirationValue = 'lifetime';
} catch (error) {
console.error('Error setting lifetime expiration:', error);
}
});
}
// PIN Modal logic
const openPinModalBtn = document.getElementById('open-pin-modal');
const pinModal = document.getElementById('pin-modal');
const pinForm = document.getElementById('pin-modal-form') as HTMLFormElement | null;
const pinInput = document.getElementById('pin-input') as HTMLInputElement | null;
const pinSuccessMsg = document.getElementById('pin-success-message');
const isMobile = /android|iphone|ipad|ipod|opera mini|iemobile|mobile/i.test(navigator.userAgent);
if (openPinModalBtn && pinModal) {
openPinModalBtn.addEventListener('click', () => {
try {
if ((pinModal as HTMLDialogElement).open) {
(pinModal as HTMLDialogElement).close();
}
(pinModal as HTMLDialogElement).showModal();
if (pinInput) pinInput.value = '';
if (pinSuccessMsg) pinSuccessMsg.classList.add('hidden');
const pinKeypadContainer = document.getElementById('pin-keypad-container');
const toggleKeypadBtn = document.getElementById('toggle-keypad');
if (isMobile) {
if (pinKeypadContainer) pinKeypadContainer.style.display = 'none';
if (toggleKeypadBtn) toggleKeypadBtn.style.display = 'none';
} else {
if (toggleKeypadBtn) toggleKeypadBtn.style.display = '';
}
} catch (error) {
console.error('Error opening PIN modal:', error);
}
});
}
// Keypad logic
const pinKeypadContainer = document.getElementById('pin-keypad-container');
const toggleKeypadBtn = document.getElementById('toggle-keypad');
const keypadOpenIcon = document.getElementById('keypad-open-icon');
const keypadCloseIcon = document.getElementById('keypad-close-icon');
let keypadOpen = false;
if (toggleKeypadBtn && pinKeypadContainer && keypadOpenIcon && keypadCloseIcon) {
toggleKeypadBtn.addEventListener('click', () => {
try {
keypadOpen = !keypadOpen;
pinKeypadContainer.style.display = keypadOpen ? '' : 'none';
keypadOpenIcon.style.display = keypadOpen ? 'none' : '';
keypadCloseIcon.style.display = keypadOpen ? '' : 'none';
} catch (error) {
console.error('Error toggling keypad:', error);
}
});
}
if (pinKeypadContainer && pinInput) {
pinKeypadContainer.addEventListener('click', (e) => {
try {
const t = e.target as HTMLButtonElement | null;
if (t && t.classList.contains('pin-key')) {
if (pinInput.value.length < 8) pinInput.value += t.textContent ?? '';
t.classList.add('btn-active', 'pulse-key');
setTimeout(() => {
t.classList.remove('btn-active', 'pulse-key');
}, 120);
}
} catch (error) {
console.error('Error handling keypad click:', error);
}
});
}
if (pinForm && pinModal && pinSuccessMsg) {
pinForm.addEventListener('submit', (e) => {
try {
e.preventDefault();
pinSuccessMsg.classList.remove('hidden');
setTimeout(() => {
(pinModal as HTMLDialogElement).close();
}, 1200);
} catch (error) {
console.error('Error submitting PIN form:', error);
}
});
}
// Password Modal logic
const openPasswordModalBtn = document.getElementById('open-password-modal');
const passwordModal = document.getElementById('password-modal');
const passwordForm = document.getElementById('password-modal-form') as HTMLFormElement | null;
const passwordInput = document.getElementById('password-input') as HTMLInputElement | null;
const passwordSuccessMsg = document.getElementById('password-success-message');
if (openPasswordModalBtn && passwordModal) {
openPasswordModalBtn.addEventListener('click', () => {
try {
if ((passwordModal as HTMLDialogElement).open) {
(passwordModal as HTMLDialogElement).close();
}
(passwordModal as HTMLDialogElement).showModal();
if (passwordInput) passwordInput.value = '';
if (passwordSuccessMsg) passwordSuccessMsg.classList.add('hidden');
} catch (error) {
console.error('Error opening password modal:', error);
}
});
}
if (passwordForm && passwordModal && passwordSuccessMsg) {
passwordForm.addEventListener('submit', (e) => {
try {
e.preventDefault();
passwordSuccessMsg.classList.remove('hidden');
setTimeout(() => {
(passwordModal as HTMLDialogElement).close();
}, 1200);
} catch (error) {
console.error('Error submitting password form:', error);
}
});
}
// Updater Modal logic
const openUpdaterModalBtn = document.getElementById('open-updater-modal');
if (openUpdaterModalBtn) {
openUpdaterModalBtn.addEventListener('click', () => {
try {
if (typeof window.openUpdaterModal === 'function') {
window.openUpdaterModal();
} else {
console.error('openUpdaterModal function not available');
}
} catch (error) {
console.error('Error opening updater modal:', error);
}
});
}
// Web Access Security slider logic
const slider = document.getElementById('security-slider');
const badge = document.getElementById('security-mode-badge');
const explanation = document.getElementById('security-mode-explanation');
const inputFields = document.getElementById('security-input-fields');
const input = document.getElementById('security-input');
const confirm = document.getElementById('security-confirm');
const saveBtn = document.getElementById('security-save');
const MODES = [
{ label: 'None', color: 'badge-error', slider: 'range-error', explain: 'No security, open access.', showInput: false },
{ label: 'Button', color: 'badge-info', slider: 'range-info', explain: 'Physical button press required to authenticate.', showInput: false },
{ label: 'PIN', color: 'badge-warning', slider: 'range-warning', explain: '4-6 digit numeric code required for access.', showInput: true, inputType: 'pin' },
{ label: 'Password', color: 'badge-success', slider: 'range-success', explain: 'Alphanumeric password required for access.', showInput: true, inputType: 'password' },
];
let lastMode = 0;
function updateSecurityUI(val: number) {
try {
const mode = MODES[val];
if (badge) {
badge.textContent = mode.label;
badge.className = `badge text-xl px-6 py-3 font-bold ${mode.color}`;
}
if (explanation) {
if (val === 1) {
explanation.textContent = 'Physical button press required to authenticate.';
} else {
explanation.textContent = mode.explain;
}
}
if (slider) slider.className = `range ${mode.slider}`;
if (inputFields && input && confirm) {
const inputEl = input as HTMLInputElement;
const confirmEl = confirm as HTMLInputElement;
if (mode.showInput) {
inputFields.style.display = '';
inputEl.type = mode.inputType === 'pin' ? 'password' : 'password';
inputEl.maxLength = mode.inputType === 'pin' ? 8 : 64;
inputEl.placeholder = mode.inputType === 'pin' ? 'Enter PIN' : 'Enter Password';
confirmEl.type = mode.inputType === 'pin' ? 'password' : 'password';
confirmEl.maxLength = mode.inputType === 'pin' ? 8 : 64;
confirmEl.placeholder = mode.inputType === 'pin' ? 'Confirm PIN' : 'Confirm Password';
} else {
inputFields.style.display = 'none';
inputEl.value = '';
confirmEl.value = '';
}
}
// Show/hide the none-warning icon and text
const noneWarning = document.getElementById('none-warning');
if (noneWarning) {
noneWarning.style.display = (val === 0) ? '' : 'none';
}
// Show/hide the button-toggle-row for BUTTON mode
const buttonToggleRow = document.getElementById('button-toggle-row');
if (buttonToggleRow) {
buttonToggleRow.style.display = (val === 1) ? '' : 'none';
}
// Remove Save button margin manipulation - no longer needed
lastMode = val;
} catch (error) {
console.error('Error updating security UI:', error);
}
}
if (slider) {
updateSecurityUI(Number((slider as HTMLInputElement).value));
slider.addEventListener('input', function() {
try {
updateSecurityUI(Number((slider as HTMLInputElement).value));
} catch (error) {
console.error('Error handling slider input:', error);
}
});
}
if (saveBtn) {
saveBtn.addEventListener('click', function(e) {
try {
e.preventDefault();
// TODO: Save logic
} catch (error) {
console.error('Error saving settings:', error);
}
});
}
// Add click listeners to icons to control slider
const iconContainers = document.querySelectorAll('[data-slider-value]');
iconContainers.forEach(icon => {
icon.addEventListener('click', function(this: HTMLElement) {
try {
const value = this.getAttribute('data-slider-value');
if (slider && value !== null) {
(slider as HTMLInputElement).value = value;
updateSecurityUI(Number(value));
}
} catch (error) {
console.error('Error handling icon click:', error);
}
});
});
});
</script>
<style>
@keyframes breathing {
0% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
50% { filter: brightness(1.35) drop-shadow(0 0 1.5rem #fbbf24); }
100% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
}
.breathing-orange {
animation: breathing 1.2s infinite;
}
.pin-keypad {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
width: 100%;
justify-items: center;
}
.pin-keypad .pin-key {
min-width: 3rem;
min-height: 3rem;
font-size: 1.25rem;
font-weight: 600;
border-radius: 0.75rem;
}
@media (max-width: 500px) {
.pin-keypad { max-width: 100%; }
.pin-keypad .pin-key { font-size: 1rem; min-width: 2.25rem; min-height: 2.25rem; }
}
#pin-keypad-container {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
#pin-modal-form {
gap: 1.5rem;
}
.pulse-key {
animation: pulse-btn 0.18s cubic-bezier(0.4,0,0.2,1);
}
@keyframes pulse-btn {
0% { filter: brightness(1.1); }
50% { filter: brightness(1.5); }
100% { filter: brightness(1.1); }
}
@keyframes security-fade {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
.animate-security-fade {
animation: security-fade 0.3s;
}
/* Fix hold button border to match background color */
#hold-confirm {
border: 2px solid transparent !important;
}
/* Floating label styles */
.floating-label {
position: relative;
display: flex;
flex-direction: column;
margin-bottom: 0.5rem;
}
.floating-label > div {
position: relative;
}
.floating-label span {
position: absolute;
left: 3rem;
top: 50%;
transform: translateY(-50%);
font-size: 0.875rem;
transition: all 0.2s ease-in-out;
pointer-events: none;
padding: 0 0.375rem;
z-index: 1;
border-radius: 0.25rem;
}
/* Use DaisyUI's standard label-text color class behavior */
.floating-label span {
@apply text-base-content/60 bg-base-100;
}
.floating-label input:focus ~ span,
.floating-label input:not(:placeholder-shown) ~ span {
top: 0;
transform: translateY(-50%);
font-size: 0.75rem;
left: 2.75rem;
@apply text-primary font-medium;
}
/* Ensure the input doesn't have autofocus */
.floating-label input {
transition: border-color 0.2s ease-in-out;
}
.floating-label input:focus {
border-color: hsl(var(--p));
}
</style>

View file

@ -0,0 +1,462 @@
---
const pageTitle = "System Settings";
---
<div class="flex flex-col gap-4">
<div class="flex justify-between items-center">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
</div>
<!-- About this Device -->
<div class="flex flex-col gap-4">
<h2 class="text-xl font-bold">About this Device</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6 gap-4">
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs">Device Model</div>
<div class="stat-value text-sm">Alpha GaN Driver</div>
<div class="stat-desc text-xs">50V 25A REV 2.3</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs">Serial Number</div>
<div class="stat-value text-sm" id="serial-number">Alpha-0001</div>
<div class="stat-desc text-xs">Lasertack GmbH</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs">Operating Hours</div>
<div class="stat-value text-sm" id="operating-hours">1,247</div>
<div class="stat-desc text-xs">Total Runtime</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs">Driver Core MCU</div>
<div class="stat-value text-sm" id="driver-firmware-version">FW1.00</div>
<div class="stat-desc text-xs">Build 05.07.2025</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs">Host MCU</div>
<div class="stat-value text-sm" id="companion-firmware-version">FW1.00</div>
<div class="stat-desc text-xs">Build 07.07.2025</div>
</div>
</div>
<div class="stats shadow bg-base-200 rounded-lg border-0">
<div class="stat bg-base-200 p-3">
<div class="stat-title text-xs">Wireless MCU</div>
<div class="stat-value text-sm" id="wireless-firmware-version">FW1.00</div>
<div class="stat-desc text-xs">Build 01.06.2025</div>
</div>
</div>
</div>
<!-- System Preferences -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">System Preferences</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
<div>
<h3 class="font-semibold mb-4">Time & Locale</h3>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Timezone</span>
</label>
<label class="input input-bordered flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M20.884 10.554a9 9 0 1 0 -10.337 10.328" />
<path d="M3.6 9h16.8" />
<path d="M3.6 15h6.9" />
<path d="M11.5 3a17 17 0 0 0 -1.502 14.954" />
<path d="M12.5 3a17 17 0 0 1 2.52 7.603" />
<path d="M18 18m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0" />
<path d="M18 16.5v1.5l.5 .5" />
</svg>
<select class="grow bg-transparent border-none outline-none" id="timezone">
<option value="UTC">UTC</option>
<option value="Europe/Berlin" selected>Europe/Berlin</option>
<option value="America/New_York">America/New_York</option>
<option value="Asia/Tokyo">Asia/Tokyo</option>
</select>
</label>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Time Format</span>
</label>
<div class="join w-full">
<button class="btn btn-outline join-item flex-1" id="time-12h" onclick="if('vibrate' in navigator) navigator.vibrate(15)">12 Hour</button>
<button class="btn btn-active join-item flex-1" id="time-24h" onclick="if('vibrate' in navigator) navigator.vibrate(15)">24 Hour</button>
</div>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Current Time</span>
</label>
<label class="input input-bordered flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-70">
<circle cx="12" cy="12" r="10"/>
<polyline points="12,6 12,12 16,14"/>
</svg>
<input type="text" class="grow" readonly id="current-time" />
</label>
</div>
</div>
<div>
<h3 class="font-semibold mb-4">Safety & Logging</h3>
<div class="space-y-4">
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Enable System Logging</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-logging" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Auto-save Settings</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-autosave" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Watchdog Timer</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-watchdog" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
<div class="form-control">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Temperature Monitoring</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-temp-monitoring" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Device Configuration -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Device Configuration</h2>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-4">
<div>
<h3 class="font-semibold mb-4">Hardware Settings</h3>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">CPU Frequency</span>
</label>
<select class="select select-bordered" id="cpu-frequency">
<option value="240">240 MHz (Default)</option>
<option value="160">160 MHz (Power Save)</option>
<option value="80">80 MHz (Low Power)</option>
</select>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Flash Frequency</span>
</label>
<select class="select select-bordered" id="flash-frequency">
<option value="80" selected>80 MHz</option>
<option value="40">40 MHz</option>
</select>
</div>
<div class="form-control mb-4">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Enable Brownout Detector</span>
<input type="checkbox" class="toggle toggle-success" checked id="toggle-brownout" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
</div>
<div>
<h3 class="font-semibold mb-4">Performance Settings</h3>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Task Watchdog Timeout</span>
</label>
<select class="select select-bordered" id="task-watchdog">
<option value="5" selected>5 seconds</option>
<option value="10">10 seconds</option>
<option value="30">30 seconds</option>
<option value="0">Disabled</option>
</select>
</div>
<div class="form-control mb-4">
<label class="label">
<span class="label-text">Stack Size (KB)</span>
</label>
<input type="range" min="4" max="16" value="8" step="1" class="range range-primary" id="stack-size" />
<div class="w-full flex justify-between text-xs px-2 mt-1">
<span>4KB</span>
<span>8KB</span>
<span>12KB</span>
<span>16KB</span>
</div>
</div>
<div class="form-control mb-4">
<label class="cursor-pointer label justify-start gap-4">
<span class="label-text">Enable Debug Output</span>
<input type="checkbox" class="toggle toggle-error" id="toggle-debug" onchange="updateToggleColor(this); if('vibrate' in navigator) navigator.vibrate(15)" />
</label>
</div>
</div>
</div>
</div>
</div>
<!-- Actions -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">System Actions</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mt-4">
<button class="btn btn-primary" id="save-settings" onclick="if('vibrate' in navigator) navigator.vibrate([100, 200, 100])">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Save Settings
</button>
<button class="btn btn-outline" id="export-config" onclick="if('vibrate' in navigator) navigator.vibrate(15)">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 3.75H6.912a2.25 2.25 0 00-2.15 1.588L2.35 13.177a2.25 2.25 0 00-.1.661V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15M2.25 13.5h3.86a2.25 2.25 0 012.012 1.244l.256.512a2.25 2.25 0 002.013 1.244h3.218a2.25 2.25 0 002.013-1.244l.256-.512a2.25 2.25 0 012.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 00-2.15-1.588H15" />
</svg>
Export Config
</button>
<button class="btn btn-warning" id="restart-system" onclick="if('vibrate' in navigator) navigator.vibrate([500, 200, 500, 200, 500])">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
</svg>
Restart
</button>
<button class="btn btn-error" id="factory-reset" onclick="if('vibrate' in navigator) navigator.vibrate([500, 200, 500, 200, 500])">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
Factory Reset
</button>
</div>
</div>
</div>
</div>
<script>
// Toggle color management function
function updateToggleColor(toggle: HTMLInputElement) {
if (toggle.checked) {
toggle.classList.remove('toggle-error');
toggle.classList.add('toggle-success');
} else {
toggle.classList.remove('toggle-success');
toggle.classList.add('toggle-error');
}
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize toggle colors
const toggles = document.querySelectorAll('.toggle');
toggles.forEach(toggle => {
updateToggleColor(toggle as HTMLInputElement);
});
// Update current time
function updateTime() {
const now = new Date();
const timeElement = document.getElementById('current-time');
if (timeElement) {
(timeElement as HTMLInputElement).value = now.toLocaleString();
}
}
// Update time every second
updateTime();
setInterval(updateTime, 1000);
// Time format buttons
const time12hBtn = document.getElementById('time-12h');
const time24hBtn = document.getElementById('time-24h');
if (time12hBtn && time24hBtn) {
time12hBtn.addEventListener('click', function() {
time12hBtn.classList.add('btn-active');
time24hBtn.classList.remove('btn-active');
});
time24hBtn.addEventListener('click', function() {
time24hBtn.classList.add('btn-active');
time12hBtn.classList.remove('btn-active');
});
}
// Stack size slider
const stackSizeSlider = document.getElementById('stack-size');
if (stackSizeSlider) {
stackSizeSlider.addEventListener('input', function() {
console.log('Stack size:', (this as HTMLInputElement).value + 'KB');
});
}
// Save settings
const saveSettingsBtn = document.getElementById('save-settings');
if (saveSettingsBtn) {
saveSettingsBtn.addEventListener('click', function() {
console.log('Saving settings...');
// Collect all settings and send via WebSocket
// Show success feedback
const originalText = saveSettingsBtn.textContent;
saveSettingsBtn.textContent = 'Saved!';
saveSettingsBtn.classList.add('btn-success');
setTimeout(() => {
saveSettingsBtn.textContent = originalText;
saveSettingsBtn.classList.remove('btn-success');
}, 2000);
});
}
// Export config
const exportConfigBtn = document.getElementById('export-config');
if (exportConfigBtn) {
exportConfigBtn.addEventListener('click', function() {
// Generate and download config file
const config = {
system: {
timezone: (document.getElementById('timezone') as HTMLSelectElement)?.value,
logging: (document.getElementById('toggle-logging') as HTMLInputElement)?.checked,
cpu_frequency: (document.getElementById('cpu-frequency') as HTMLSelectElement)?.value
}
};
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `alpha-driver-config-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
}
// Restart system
const restartBtn = document.getElementById('restart-system');
if (restartBtn) {
restartBtn.addEventListener('click', function() {
if (confirm('Are you sure you want to restart the system? This will temporarily interrupt operations.')) {
console.log('Restarting system...');
// Send restart command via WebSocket
}
});
}
// Factory reset
const factoryResetBtn = document.getElementById('factory-reset');
if (factoryResetBtn) {
factoryResetBtn.addEventListener('click', function() {
if (confirm('WARNING: This will reset all settings to factory defaults. This action cannot be undone.\n\nAre you sure you want to continue?')) {
console.log('Performing factory reset...');
// Send factory reset command via WebSocket
}
});
}
// Function to update settings from WebSocket
(window as any).updateSystemSettings = function(data: any) {
if (data.firmware_version) {
const firmwareEl = document.getElementById('firmware-version');
if (firmwareEl) firmwareEl.textContent = data.firmware_version;
}
if (data.mac_address) {
const macEl = document.getElementById('mac-address');
if (macEl) macEl.textContent = data.mac_address;
}
};
// Device Type detection
const deviceTypeEl = document.getElementById('device-type');
if (deviceTypeEl) {
let type = 'Unknown';
const ua = navigator.userAgent;
if (/android/i.test(ua)) type = 'Android Phone';
else if (/iPhone|iPad|iPod/i.test(ua)) type = 'iPhone';
else if (/Windows/i.test(ua)) type = 'Windows Computer';
else if (/Macintosh/i.test(ua)) type = 'Mac Computer';
else if (/Linux/i.test(ua)) type = 'Linux Computer';
deviceTypeEl.textContent = type;
}
});
</script>
<style>
@keyframes breathing {
0% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
50% { filter: brightness(1.35) drop-shadow(0 0 1.5rem #fbbf24); }
100% { filter: brightness(1) drop-shadow(0 0 0.5rem #f59e42); }
}
.breathing-orange {
animation: breathing 1.2s infinite;
}
.pin-keypad {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
width: 100%;
justify-items: center;
}
.pin-keypad .pin-key {
min-width: 3rem;
min-height: 3rem;
font-size: 1.25rem;
font-weight: 600;
border-radius: 0.75rem;
}
@media (max-width: 500px) {
.pin-keypad { max-width: 100%; }
.pin-keypad .pin-key { font-size: 1rem; min-width: 2.25rem; min-height: 2.25rem; }
}
#pin-keypad-container {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
#pin-modal-form {
gap: 1.5rem;
}
.pulse-key {
animation: pulse-btn 0.18s cubic-bezier(0.4,0,0.2,1);
}
@keyframes pulse-btn {
0% { filter: brightness(1.1); }
50% { filter: brightness(1.5); }
100% { filter: brightness(1.1); }
}
</style>

View file

@ -0,0 +1,246 @@
---
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: '<rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/>',
viewBox: "0 0 24 24",
},
{
path: "/testbench",
title: "Testbench",
icon: '<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/><circle cx="6" cy="8" r="1"/><circle cx="10" cy="8" r="1"/><circle cx="14" cy="8" r="1"/><circle cx="18" cy="8" r="1"/>',
viewBox: "0 0 24 24",
},
{
path: "/control",
title: "Control",
icon: '<line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/>',
viewBox: "0 0 24 24",
},
{
path: "/modulation",
title: "Laser",
icon: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
viewBox: "0 0 24 24",
},
{
path: "/dcdc",
title: "Power",
icon: '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
viewBox: "0 0 24 24",
},
{
path: "/logs",
title: "Logs",
icon: '<path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/>',
viewBox: "0 0 24 24",
},
];
// System section items
const systemNavigationItems: NavigationItem[] = [
{
path: "/network",
title: "Network",
icon: '<rect x="16" y="16" width="6" height="6" rx="1"/><rect x="2" y="16" width="6" height="6" rx="1"/><rect x="9" y="2" width="6" height="6" rx="1"/><path d="M5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3"/><path d="M12 12V8"/>',
viewBox: "0 0 24 24",
},
{
path: "/settings",
title: "Settings",
icon: '<path d="M12 20a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"/><path d="M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 2v2"/><path d="M12 22v-2"/><path d="m17 20.66-1-1.73"/><path d="M11 10.27 7 3.34"/><path d="m20.66 17-1.73-1"/><path d="m3.34 7 1.73 1"/><path d="M14 12h8"/><path d="M2 12h2"/><path d="m20.66 7-1.73 1"/><path d="m3.34 17 1.73-1"/><path d="m17 3.34-1 1.73"/><path d="m11 13.73-4 6.93"/>',
viewBox: "0 0 24 24",
},
{
path: "/security",
title: "Security",
icon: '<path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M11.998 2l.118 .007l.059 .008l.061 .013l.111 .034a.993 .993 0 0 1 .217 .112l.104 .082l.255 .218a11 11 0 0 0 7.189 2.537l.342 -.01a1 1 0 0 1 1.005 .717a13 13 0 0 1 -9.208 16.25a1 1 0 0 1 -.502 0a13 13 0 0 1 -9.209 -16.25a1 1 0 0 1 1.005 -.717a11 11 0 0 0 7.531 -2.527l.263 -.225l.096 -.075a.993 .993 0 0 1 .217 -.112l.112 -.034a.97 .97 0 0 1 .119 -.021l.115 -.007zm.002 7a2 2 0 0 0 -1.995 1.85l-.005 .15l.005 .15a2 2 0 0 0 .995 1.581v1.769l.007 .117a1 1 0 0 0 1.993 -.117l.001 -1.768a2 2 0 0 0 -1.001 -3.732z" />',
viewBox: "0 0 24 24",
},
];
---
<aside class="menu bg-base-200 w-70 min-h-full p-4 text-base-content">
<div class="flex items-center justify-center mb-6">
<div class="text-xl font-bold">Lasertack</div>
</div>
<ul class="menu menu-md text-lg">
<li>
<h2 class="menu-title text-base">Laser</h2>
<ul>
{
laserNavigationItems.map((item) => (
<li>
<a href={item.path} class="cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox={item.viewBox}
stroke-width="2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
class="w-5 h-5"
set:html={item.icon}
/>
{item.title}
</a>
</li>
))
}
</ul>
</li>
<li>
<h2 class="menu-title text-base">System</h2>
<ul>
{
systemNavigationItems.map((item) => (
<li>
<a href={item.path} class="cursor-pointer">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox={item.viewBox}
stroke-width="2"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
class="w-5 h-5"
set:html={item.icon}
/>
{item.title}
</a>
</li>
))
}
<li>
<a href="/modal" class="cursor-pointer flex items-center gap-2" id="status-link">
<span class="text-sm">Driver:</span>
<ConnectionStatusBadge client:load />
</a>
</li>
<li>
<a href="/modal" class="cursor-pointer flex items-center gap-2" id="testbench-status-link">
<span class="text-sm">Testbench:</span>
<TestbenchConnectionStatusBadge client:load />
</a>
</li>
<li>
<a href="/update" class="cursor-pointer" id="update-link">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-refresh-alert"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path>
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path>
<path d="M12 9l0 3"></path>
<path d="M12 15l.01 0"></path>
</svg>
<span id="update-status" class="text-neutral-500"
>No updates available</span
>
</a>
</li>
</ul>
</li>
</ul>
</aside>
<script>
// Type declaration for global functions
declare global {
interface Window {
handleNavigation: (path: string) => void;
openStatusModal?: () => void;
}
}
// Import the store
import { $availableUpdates } from "./AppProviderStore.tsx";
// Function to update the UI based on available updates
function updateUpdateStatus() {
const updateStatus = document.getElementById("update-status");
const updateLink = document.getElementById("update-link");
if (updateStatus && updateLink) {
const updates = $availableUpdates.get();
const hasUpdates = updates && updates.length > 0;
if (hasUpdates) {
updateStatus.textContent = "Update Available";
updateStatus.className = "text-success";
updateLink.classList.add("btn-success", "text-success");
} else {
updateStatus.textContent = "No updates available";
updateStatus.className = "text-neutral-500";
updateLink.classList.remove("btn-success", "text-success");
}
}
}
// Warten bis das DOM geladen ist
document.addEventListener("DOMContentLoaded", () => {
// Initial update
updateUpdateStatus();
// Subscribe to store changes
$availableUpdates.subscribe(() => {
updateUpdateStatus();
});
// Alle Links in der Sidebar abfangen
document.querySelectorAll('.menu a[href^="/"]').forEach((link) => {
link.addEventListener(
"click",
function (this: HTMLAnchorElement, e: Event) {
e.preventDefault();
const path = this.getAttribute("href");
if (path === "/modal") {
if (typeof window.openStatusModal === "function") {
window.openStatusModal();
}
return;
}
if (path === "/update") {
if (typeof window.openUpdaterModal === "function") {
window.openUpdaterModal();
}
return;
}
if (
window.handleNavigation &&
typeof window.handleNavigation === "function" &&
path
) {
window.handleNavigation(path);
// Optional: URL in der Adresszeile aktualisieren
//window.history.pushState({}, '', path);
}
}
);
});
});
</script>

View file

@ -0,0 +1,45 @@
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 (
<span
class={`badge ${colorClass} font-mono relative inline-block transition-all duration-200`}
style={{ width: '120px', minWidth: '120px', maxWidth: '120px', padding: 0 }}
>
{/* Unsichtbarer Platzhalter für feste Breite und Position */}
<span
class="opacity-0 select-none pointer-events-none"
style={{
display: 'inline-block',
width: '100%',
textAlign: 'left',
}}
>
{maxText}
</span>
{/* Sichtbarer Status absolut darüber */}
<span
class="absolute left-0 top-0 w-full h-full flex items-center"
style={{ justifyContent: 'left', paddingLeft: '0.75rem' /* ggf. anpassen */ }}
>
{status}
</span>
</span>
);
}

View file

@ -0,0 +1,692 @@
---
import TestbenchConnectionStatusBadge from "./TestbenchConnectionStatusBadge.tsx";
const pageTitle = "Test Bench Dashboard";
---
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">{pageTitle}</h1>
<!-- Board Connection Status -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title">Board Connection Status</h2>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<div class="badge badge-lg" id="board-status-badge">
<span class="mr-2">●</span>
<span id="board-status-text">Disconnected</span>
</div>
<div class="badge badge-lg badge-neutral" id="vcc-display">
VCC: 12V
</div>
<div class="flex items-center gap-2">
<span class="text-sm font-medium">Testbench:</span>
<TestbenchConnectionStatusBadge client:load />
</div>
</div>
<div class="flex items-center gap-2"></div>
</div>
<!-- Control Panel -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
<!-- Board Power Relay -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg p-4">
<h2 class="card-title text-lg mb-3">Power Control</h2>
<div class="flex items-center justify-between">
<span class="text-sm font-medium">Board Relay:</span>
<input
type="checkbox"
class="toggle border-red-600 bg-red-500 checked:border-orange-500 checked:bg-orange-400"
id="power-relay-toggle"
/>
</div>
<div class="flex items-center justify-between">
<span class="text-sm font-medium">GND Sense:</span>
<div class="badge badge-lg" id="gnd-sense-status">
<span class="mr-2">●</span>
<span id="gnd-sense-text">No Board Connected</span>
</div>
</div>
</div>
</div>
<!-- UART Routing -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg p-4">
<h2 class="card-title text-lg mb-3">UART Routing</h2>
<div class="join w-full">
<input
class="join-item btn flex-1"
type="radio"
name="uart_routing"
aria-label="ST"
id="uart-st"
checked
/>
<input
class="join-item btn flex-1"
type="radio"
name="uart_routing"
aria-label="P4"
id="uart-p4"
/>
<input
class="join-item btn flex-1"
type="radio"
name="uart_routing"
aria-label="C6"
id="uart-c6"
/>
</div>
</div>
</div>
<!-- Bootloader Control -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg p-4">
<h2 class="card-title text-lg mb-3">Bootloader Control</h2>
<div class="space-y-2">
<button
class="btn btn-error btn-sm w-full"
id="p4-bootloader-btn"
>
P4 Bootloader OFF
</button>
<button
class="btn btn-error btn-sm w-full"
id="c6-bootloader-btn"
>
C6 Bootloader OFF
</button>
</div>
</div>
</div>
<!-- Enable Control -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg p-4">
<h2 class="card-title text-lg mb-3">Enable Control</h2>
<div class="space-y-2">
<button class="btn btn-error btn-sm w-full" id="p4-enable-btn">
P4 Disabled
</button>
<button class="btn btn-error btn-sm w-full" id="c6-enable-btn">
C6 Disabled
</button>
<button class="btn btn-error btn-sm w-full" id="st-enable-btn">
ST Disabled
</button>
</div>
</div>
</div>
</div>
<!-- UART Console -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-lg mb-3">UART Console</h2>
<!-- Console Output -->
<div
class="mockup-code w-full h-64 overflow-y-auto mb-4"
id="uart-console"
>
<pre
data-prefix=">"
class="text-info"><code>UART Console Ready</code></pre>
<pre
data-prefix="RX"
class="text-success"><code>Waiting for data...</code></pre>
</div>
<!-- Input Controls -->
<div class="flex gap-2">
<input
type="text"
placeholder="Enter UART message"
class="input input-bordered flex-1"
id="uart-input"
/>
<button class="btn btn-primary" id="send-uart">Send</button>
<button class="btn btn-outline btn-sm" id="clear-console"
>Clear</button
>
</div>
</div>
</div>
<!-- Number Conversion Utility -->
<div class="card bg-base-100 shadow">
<div class="card-body bg-base-200 rounded-lg">
<h2 class="card-title text-lg mb-3">Number Conversion Utility</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Decimal Input -->
<div class="form-control">
<label class="label">
<span class="label-text">Decimal</span>
</label>
<input
type="number"
placeholder="255"
class="input input-bordered"
id="decimal-input"
/>
</div>
<!-- Hex Output -->
<div class="form-control">
<label class="label">
<span class="label-text">Hexadecimal</span>
</label>
<input
type="text"
placeholder="0xFF"
class="input input-bordered"
id="hex-output"
readonly
/>
</div>
<!-- Binary Output -->
<div class="form-control">
<label class="label">
<span class="label-text">Binary</span>
</label>
<input
type="text"
placeholder="11111111"
class="input input-bordered"
id="binary-output"
readonly
/>
</div>
</div>
<div class="flex gap-2 mt-4">
<button class="btn btn-outline btn-sm" id="copy-hex"
>Copy Hex</button
>
<button class="btn btn-outline btn-sm" id="copy-binary"
>Copy Binary</button
>
<button class="btn btn-outline btn-sm" id="clear-converter"
>Clear</button
>
</div>
</div>
</div>
</div>
<script>
import {
$testbenchStates,
sendCommandToBackend,
TESTBENCH_COMMANDS,
} from "./TestbenchStore.tsx";
import { $testbenchWebsocketConnectionStatus } from "./TestbenchStore.tsx";
// Extend Window interface for custom properties
declare global {
interface Window {
updateTestBenchStatus: (data: any) => void;
}
}
// State management
let currentUartRouting = "st"; // 'st', 'p4', 'c6' - default to ST
let p4BootloaderActive = false;
let c6BootloaderActive = false;
let p4Enabled = false;
let c6Enabled = false;
let stEnabled = false;
let boardConnected = false;
let powerRelayEnabled = false;
let gndSense = false;
let vccVoltage = 12; // Default VCC voltage
// Update board connection status
// Global function to update WebSocket connection status (for HydroBuddy address)
function updateWebSocketStatus(connected: boolean) {
const badge = document.getElementById("board-status-badge");
const text = document.getElementById("board-status-text");
if (badge && text) {
if (connected) {
badge.className = "badge badge-lg badge-success";
text.textContent = "Connected";
} else {
badge.className = "badge badge-lg badge-error";
text.textContent = "Disconnected";
}
}
}
// Global function to update board connection status (for GND Sense)
function updateBoardConnectionStatus(connected: boolean) {
boardConnected = connected;
const badge = document.getElementById("board-connection-badge");
const text = document.getElementById("board-connection-text");
if (badge && text) {
if (connected) {
badge.className = "badge badge-lg badge-success";
text.textContent = "Board Connected";
} else {
badge.className = "badge badge-lg badge-error";
text.textContent = "Board Disconnected";
}
}
}
// Update VCC voltage display
function updateVccDisplay(voltage: number) {
vccVoltage = voltage;
const vccDisplay = document.getElementById("vcc-display");
if (vccDisplay) {
vccDisplay.textContent = `VCC: ${voltage}V`;
}
}
// Update GND Sense status display (based on boardConnected status)
function updateGndSenseStatus(boardConnected: boolean) {
gndSense = !boardConnected; // GND Sense is LOW when board is connected
// Update both GND Sense display and Board Connection display
const gndBadge = document.getElementById("gnd-sense-status");
const gndText = document.getElementById("gnd-sense-text");
if (gndBadge && gndText) {
if (boardConnected) {
gndBadge.className = "badge badge-lg badge-success";
gndText.textContent = "Connected";
} else {
gndBadge.className = "badge badge-lg badge-error";
gndText.textContent = "Not Connected";
}
}
// Also update the board connection status
updateBoardConnectionStatus(boardConnected);
}
// Toggle power relay
function togglePowerRelay() {
if ($testbenchStates.get().powerRelayEnabled) {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_POWER_RELAY_OFF);
} else {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_POWER_RELAY_ON);
}
}
// Update UART routing display
function updateUartRoutingDisplay() {
const buttons = document.querySelectorAll('input[name="uart_routing"]');
const currentState = $testbenchStates.get();
// Update button selection
buttons.forEach((button) => {
const inputButton = button as HTMLInputElement;
if (inputButton) {
const label = inputButton.getAttribute("aria-label")?.toLowerCase();
if (label) {
inputButton.checked = label === currentState.uartRouting;
}
}
});
}
// Set UART routing
function setUartRouting(target: string) {
if (target === "st") {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_ST);
} else if (target === "c6") {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_C6);
} else if (target === "p4") {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_UART_ROUTING_P4);
}
console.log(`Clicked UART routing button: ${target}`);
}
// Update bootloader status
function updateBootloaderStatus(target: string, active: boolean) {
const button = document.getElementById(`${target}-bootloader-btn`);
if (!button) return;
if (active) {
button.textContent = `${target.toUpperCase()} Bootloader ON`;
button.className = "btn btn-success btn-sm w-full";
} else {
button.textContent = `${target.toUpperCase()} Bootloader OFF`;
button.className = "btn btn-error btn-sm w-full";
}
}
// Toggle bootloader
function toggleBootloader(target: string) {
if (target === "p4") {
if ($testbenchStates.get().p4BootloaderActive) {
// Turn off P4 bootloader
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_BOOTLOADER_OFF);
console.log("Turned off P4 bootloader");
} else {
// Turn on P4 bootloader, turn off C6 bootloader and enable P4
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_BOOTLOADER_ON);
console.log("Turned on P4 bootloader");
}
} else if (target === "c6") {
if ($testbenchStates.get().c6BootloaderActive) {
// Turn off C6 bootloader
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_BOOTLOADER_OFF);
console.log("Turned off C6 bootloader");
} else {
// Turn on C6 bootloader, turn off P4 bootloader and enable C6
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_BOOTLOADER_ON);
console.log("Turned on C6 bootloader");
}
}
}
// Update enable status based on testbench states
function updateEnableStatus(target: string, enabled: boolean) {
const button = document.getElementById(`${target}-enable-btn`);
if (!button) return;
// Get current testbench states
const states = $testbenchStates.get();
// Check if target is actually enabled in the store
const isActuallyEnabled = states[
`${target}Enabled` as keyof typeof states
] as boolean;
if (isActuallyEnabled) {
button.textContent = `${target.toUpperCase()} Enabled`;
button.className = "btn btn-success btn-sm w-full";
} else {
button.textContent = `${target.toUpperCase()} Disabled`;
button.className = "btn btn-error btn-sm w-full";
}
}
// Toggle enable
function toggleEnable(target: string) {
const currentState = $testbenchStates.get();
switch (target) {
case "p4":
if (!currentState.c6BootloaderActive) {
// Only allow if not in bootloader mode
if (currentState.p4Enabled) {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_DISABLED);
} else {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_P4_ENABLED);
}
}
break;
case "c6":
if (!currentState.p4BootloaderActive) {
// Only allow if not in bootloader mode
if (currentState.c6Enabled) {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_DISABLED);
} else {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_C6_ENABLED);
}
}
break;
case "st":
if (currentState.stEnabled) {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_ST_DISABLED);
} else {
sendCommandToBackend(TESTBENCH_COMMANDS.SET_ST_ENABLED);
}
break;
}
// Update UI based on current state
const state = $testbenchStates.get();
updateEnableStatus("p4", state.p4Enabled);
updateEnableStatus("c6", state.c6Enabled);
updateEnableStatus("st", state.stEnabled);
console.log(
`P4 Enable: ${state.p4Enabled}, C6 Enable: ${state.c6Enabled}, ST Enable: ${state.stEnabled}`
);
}
// Add message to console (global function)
function addConsoleMessage(
prefix: string,
message: string,
className = ""
) {
const uartConsole = document.getElementById("uart-console");
if (uartConsole) {
const pre = document.createElement("pre");
pre.setAttribute("data-prefix", prefix);
if (className) pre.className = className;
const code = document.createElement("code");
code.textContent = message;
pre.appendChild(code);
uartConsole.appendChild(pre);
uartConsole.scrollTop = uartConsole.scrollHeight;
}
}
// Event listeners
document.addEventListener("DOMContentLoaded", function () {
// UART Console functionality
const uartConsole = document.getElementById("uart-console");
const uartInput = document.getElementById("uart-input");
const sendButton = document.getElementById("send-uart");
const clearButton = document.getElementById("clear-console");
// Number conversion functionality
const decimalInput = document.getElementById("decimal-input");
const hexOutput = document.getElementById("hex-output");
const binaryOutput = document.getElementById("binary-output");
const copyHexBtn = document.getElementById("copy-hex");
const copyBinaryBtn = document.getElementById("copy-binary");
const clearConverterBtn = document.getElementById("clear-converter");
// Send UART message
function sendUartMessage() {
const input = uartInput as HTMLInputElement;
if (!input.value.trim()) return;
const message = input.value.trim();
// Display sent message
addConsoleMessage("TX", message, "text-warning");
// TODO: Send via WebSocket as text
console.log("Sending UART text:", message);
input.value = "";
}
// Clear console
function clearConsole() {
if (uartConsole) {
uartConsole.innerHTML =
'<pre data-prefix=">" class="text-info"><code>Console cleared</code></pre>';
}
}
// Event listeners for UART console
if (sendButton) {
sendButton.addEventListener("click", sendUartMessage);
}
if (clearButton) {
clearButton.addEventListener("click", clearConsole);
}
if (uartInput) {
uartInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
sendUartMessage();
}
});
}
// Event listeners for number conversion
if (decimalInput) {
decimalInput.addEventListener("input", updateConversion);
}
if (copyHexBtn) {
copyHexBtn.addEventListener("click", () => {
const hexOut = hexOutput as HTMLInputElement;
if (hexOut.value) copyToClipboard(hexOut.value);
});
}
if (copyBinaryBtn) {
copyBinaryBtn.addEventListener("click", () => {
const binOut = binaryOutput as HTMLInputElement;
if (binOut.value) copyToClipboard(binOut.value);
});
}
if (clearConverterBtn) {
clearConverterBtn.addEventListener("click", clearConverter);
}
// Number conversion functions
function updateConversion() {
const decInput = decimalInput as HTMLInputElement;
const hexOut = hexOutput as HTMLInputElement;
const binOut = binaryOutput as HTMLInputElement;
const decimal = parseInt(decInput.value);
if (isNaN(decimal) || decInput.value === "") {
hexOut.value = "";
binOut.value = "";
return;
}
// Convert to hex (with 0x prefix)
hexOut.value =
"0x" + decimal.toString(16).toUpperCase().padStart(2, "0");
// Convert to binary (8-bit)
binOut.value = decimal.toString(2).padStart(8, "0");
}
function copyToClipboard(text: string) {
navigator.clipboard
.writeText(text)
.then(() => {
console.log("Copied to clipboard:", text);
})
.catch((err) => {
console.error("Failed to copy:", err);
});
}
function clearConverter() {
const decInput = decimalInput as HTMLInputElement;
const hexOut = hexOutput as HTMLInputElement;
const binOut = binaryOutput as HTMLInputElement;
decInput.value = "";
hexOut.value = "";
binOut.value = "";
}
// Simulate incoming UART data (remove this when connecting to real WebSocket)
setTimeout(() => {
addConsoleMessage("RX", "Hello from device!", "text-success");
}, 2000);
// UART routing buttons
document
.querySelectorAll('input[name="uart_routing"]')
.forEach((tab) => {
const inputTab = tab as HTMLInputElement;
inputTab.addEventListener("change", function () {
if (this.checked) {
const target = this.getAttribute("aria-label")?.toLowerCase();
if (target) {
setUartRouting(target);
}
}
});
});
const p4BootloaderBtn = document.getElementById("p4-bootloader-btn");
const c6BootloaderBtn = document.getElementById("c6-bootloader-btn");
const p4EnableBtn = document.getElementById("p4-enable-btn");
const c6EnableBtn = document.getElementById("c6-enable-btn");
const stEnableBtn = document.getElementById("st-enable-btn");
const powerRelayToggle = document.getElementById("power-relay-toggle");
// Bootloader buttons
if (p4BootloaderBtn) {
p4BootloaderBtn.addEventListener("click", () =>
toggleBootloader("p4")
);
}
if (c6BootloaderBtn) {
c6BootloaderBtn.addEventListener("click", () =>
toggleBootloader("c6")
);
}
// Enable buttons
if (p4EnableBtn) {
p4EnableBtn.addEventListener("click", () => toggleEnable("p4"));
}
if (c6EnableBtn) {
c6EnableBtn.addEventListener("click", () => toggleEnable("c6"));
}
if (stEnableBtn) {
stEnableBtn.addEventListener("click", () => toggleEnable("st"));
}
// Power relay toggle
if (powerRelayToggle) {
powerRelayToggle.addEventListener("change", togglePowerRelay);
}
// Initialize display
updateWebSocketStatus(false);
updateVccDisplay(12);
updateUartRoutingDisplay();
updateBootloaderStatus("p4", false);
updateBootloaderStatus("c6", false);
updateEnableStatus("p4", false);
updateEnableStatus("c6", false);
updateEnableStatus("st", false);
});
// Subscribe to testbench state changes for automatic UI updates
$testbenchStates.subscribe((newStates) => {
// Update enable status buttons based on store state
updateEnableStatus("p4", newStates.p4Enabled);
updateEnableStatus("c6", newStates.c6Enabled);
updateEnableStatus("st", newStates.stEnabled);
// Update bootloader status
updateBootloaderStatus("p4", newStates.p4BootloaderActive);
updateBootloaderStatus("c6", newStates.c6BootloaderActive);
// Update UART routing
currentUartRouting = newStates.uartRouting;
updateUartRoutingDisplay();
// Update power relay
powerRelayEnabled = newStates.powerRelayEnabled;
const toggle = document.getElementById(
"power-relay-toggle"
) as HTMLInputElement;
if (toggle) {
toggle.checked = powerRelayEnabled;
}
updateGndSenseStatus(newStates.boardConnected);
});
</script>
</div>
</div>

View file

@ -0,0 +1,143 @@
import { atom } from 'nanostores';
import pkg from "websocket";
const { w3cwebsocket } = pkg;
export const $testbenchWebsocket = atom<WebSocket | null>(null);
export const $testbenchWebsocketConnectionStatus = atom<string>('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<Record<number, boolean>>({
[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<TestbenchState>({
// 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);
}
}
}

View file

@ -0,0 +1,227 @@
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<typeof setInterval> | null = null;
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
let missedHeartbeats = 0;
// Reconnect state
let isReconnecting = false;
let reconnectTimeout: ReturnType<typeof setTimeout> | 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");
};
}, []);
};

View file

@ -0,0 +1,186 @@
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 (
<IconCpu2 strokeWidth={2} />
);
case 'p4 firmware':
return (
<IconCpu strokeWidth={2} />
);
case 'c6':
return (
<IconWifi strokeWidth={2} />
);
default:
return (
<IconFileTypeJs strokeWidth={2} />
);
}
};
// 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 (
<dialog class="modal modal-open">
<div class="modal-box max-w-2xl">
<div class="flex flex-col items-center justify-center py-8">
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-alert-circle text-error mb-4">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M12 8v4" />
<path d="M12 16h.01" />
</svg>
<h3 class="font-bold text-lg text-error mb-2">Update Failed</h3>
<p class="text-center mb-6">One or more firmware updates failed to complete successfully.</p>
</div>
<div class="space-y-2 mb-6">
{availableUpdates.map((update, index) => {
const [currentProgress, status] = getProgressForUpdate(update.core);
const updateTargetIndex = getTargetIndex(update.core);
const errorMessage = updateErrorMessages[updateTargetIndex] || '';
const isFailed = status === -1;
return (
<div key={index} class={`flex items-center justify-between p-3 rounded-lg ${isFailed ? 'bg-error/10' : 'bg-success/10'}`}>
<div class="flex items-center gap-3">
{getUpdateIcon(update.core)}
<div class="flex-1">
<span class="font-medium">{update.core}</span>
{isFailed && errorMessage && (
<div class="text-sm text-error mt-1">{errorMessage}</div>
)}
</div>
</div>
<div class="flex items-center gap-2">
{isFailed ? (
<>
<span class="text-sm text-error">Failed</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-x text-error">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M18 6l-12 12" />
<path d="M6 6l12 12" />
</svg>
</>
) : (
<>
<span class="text-sm">v{update.newVersion}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12l2 2l4 -4" />
</svg>
</>
)}
</div>
</div>
);
})}
</div>
<div class="modal-action">
<button class="btn btn-outline" onClick={handleClose}>Close</button>
<button class="btn btn-primary" onClick={retryUpdate}>Retry Update</button>
</div>
</div>
<div class="modal-backdrop" onClick={handleClose}></div>
</dialog>
);
}

View file

@ -0,0 +1,135 @@
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 (
<IconCpu2 strokeWidth={2} />
);
case 'p4 firmware':
return (
<IconCpu strokeWidth={2} />
);
case 'c6':
return (
<IconWifi strokeWidth={2} />
);
default:
return (
<IconFileTypeJs strokeWidth={2} />
);
}
};
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 (
<dialog class="modal modal-open">
<div class="modal-box max-w-2xl">
<div class="flex flex-col items-center justify-center py-8">
{/* Always show success checkmark first */}
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success mb-4">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12l2 2l4 -4" />
</svg>
<h3 class="font-bold text-lg text-success mb-2">Update Completed Successfully!</h3>
<p class="text-center mb-6">All listed components have been updated successfully.</p>
{/* Show restarting section below if in restarting state */}
{updateState === 'restarting' && (
<div class="border-t border-base-300 pt-6 mt-2 w-full">
<div class="flex flex-col items-center">
<IconRefresh
size={48}
class="text-primary animate-spin mb-3"
strokeWidth={1.5}
/>
<h4 class="font-semibold text-md text-primary mb-2">Restarting Device...</h4>
<p class="text-center text-sm text-base-content/70">
The device is restarting to apply the firmware update. Please wait for reconnection.
</p>
</div>
</div>
)}
</div>
{/* Show update list in both success and restarting state */}
{(updateState === 'success' || updateState === 'restarting') && (
<div class="space-y-2 mb-6">
{availableUpdates.map((update, index) => (
<div key={index} class="flex items-center justify-between p-3 bg-success/10 rounded-lg">
<div class="flex items-center gap-3">
{getUpdateIcon(update.core)}
<span class="font-medium">{update.core}</span>
</div>
<div class="flex items-center gap-2">
<span class="text-sm">v{update.newVersion}</span>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check text-success">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12l2 2l4 -4" />
</svg>
</div>
</div>
))}
</div>
)}
{/* Button only in success state */}
{updateState === 'success' && (
<div class="modal-action">
<button class="btn btn-primary" onClick={handleClose}>OK</button>
</div>
)}
</div>
<div class="modal-backdrop" onClick={handleClose}></div>
</dialog>
);
}

View file

@ -0,0 +1,472 @@
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 (
<IconCpu2 strokeWidth={2} />
);
case 'p4 firmware':
return (
<IconCpu strokeWidth={2} />
);
case 'c6':
return (
<IconWifi strokeWidth={2} />
);
default:
return (
<IconFileTypeJs strokeWidth={2} />
);
}
};
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 (
<dialog class="modal modal-open">
<div class="modal-box max-w-4xl">
{updateState === 'initial' && (
<>
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-mobiledata">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M16 12v-8" />
<path d="M8 20v-8" />
<path d="M13 7l3 -3l3 3" />
<path d="M5 17l3 3l3 -3" />
</svg>
Over The Air Update Available
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Available Updates */}
<div>
<h4 class="font-semibold mb-3">Available Updates:</h4>
<div class="space-y-3">
{availableUpdates
.filter(update => update.updateAvailable)
.sort((a, b) => {
const indexA = getTargetIndex(a.core);
const indexB = getTargetIndex(b.core);
console.log(`🔄 Sorting: "${a.core}" (${indexA}) vs "${b.core}" (${indexB}) -> ${indexA - indexB}`);
return indexA - indexB;
})
.map((update, index) => (
<div key={index} class="flex items-center justify-between p-3 bg-base-200 rounded-lg">
<div class="flex items-center gap-3">
{getUpdateIcon(update.core)}
<div>
<div class="font-medium">{update.core}</div>
<div class="text-sm opacity-70">
{update.currentVersion} → {update.newVersion}
</div>
</div>
</div>
<div class="badge badge-success">Update</div>
</div>
))}
</div>
</div>
{/* Changelog */}
<div>
<h4 class="font-semibold mb-3 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor" class="icon icon-tabler icons-tabler-filled icon-tabler-exchange">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M19 3a3 3 0 0 1 1 5.83v4.17a6 6 0 0 1 -6 6h-.585l1.292 1.293a1 1 0 0 1 .083 1.32l-.083 .094a1 1 0 0 1 -1.414 0l-2.959 -2.959a1 1 0 0 1 -.238 -.32l-.038 -.091l-.028 -.094a.98 .98 0 0 1 .187 -.866l.076 -.084l3 -3a1 1 0 0 1 1.414 1.414l-1.293 1.293h.586a4 4 0 0 0 3.995 -3.8l.005 -.2v-4.171a3 3 0 0 1 -2 -2.829l.005 -.176a3 3 0 0 1 2.995 -2.824m-8.293 -.707l3 3a.98 .98 0 0 1 .263 .95l-.01 .031l-.003 .018l-.008 .018l-.007 .027l-.016 .035l-.01 .032l-.007 .01l-.005 .014a1 1 0 0 1 -.232 .316l-2.965 2.963a1 1 0 0 1 -1.32 .083l-.094 -.083a1 1 0 0 1 0 -1.414l1.291 -1.293h-.584a4 4 0 0 0 -3.995 3.8l-.005 .2v4.171a3.001 3.001 0 1 1 -4 2.829l.005 -.176a3 3 0 0 1 1.995 -2.654v-4.17a6 6 0 0 1 6 -6h.585l-1.292 -1.293a1 1 0 0 1 -.083 -1.32l.083 -.094a1 1 0 0 1 1.414 0" />
</svg>
Changelog
</h4>
<div class="bg-base-200 rounded-lg p-4 h-80 overflow-y-auto">
{updateChangelog ? (
<div class="prose prose-sm max-w-none [&>h2]:text-xl [&>h2]:font-semibold [&>h2]:mb-3 [&>h2]:mt-4 [&>h3]:text-lg [&>h3]:font-medium [&>h3]:mb-2 [&>h3]:mt-3 [&>ul]:list-disc [&>ul]:ml-6 [&>ul]:mb-3 [&>li]:mb-1 [&>p]:mb-3 [&>strong]:font-semibold"
dangerouslySetInnerHTML={{ __html: marked.parse(updateChangelog) as string }} />
) : (
<div class="text-center text-gray-500">
<p>No changelog available</p>
</div>
)}
</div>
</div>
</div>
{!hasAvailableUpdates(availableUpdates) && (
<div class="alert alert-info mt-4">
<span>No updates available.</span>
</div>
)}
<div class="alert alert-warning mt-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span>The device will restart automatically after the update. This process takes approximately 2-3 minutes.</span>
</div>
<div class="modal-action">
<button class="btn btn-outline" onClick={resetModal}>Cancel</button>
<button class="btn btn-primary" onClick={startUpdate} disabled={!hasAvailableUpdates(availableUpdates)}>Start Update</button>
</div>
</>
)}
{updateState === 'updating' && (
<>
<h3 class="font-bold text-lg mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-mobiledata">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M16 12v-8" />
<path d="M8 20v-8" />
<path d="M13 7l3 -3l3 3" />
<path d="M5 17l3 3l3 -3" />
</svg>
Updating Over The Air...
</h3>
<div class="space-y-4">
{availableUpdates
.filter(update => update.updateAvailable)
.sort((a, b) => {
const indexA = getTargetIndex(a.core);
const indexB = getTargetIndex(b.core);
console.log(`🔄 Sorting (updating): "${a.core}" (${indexA}) vs "${b.core}" (${indexB}) -> ${indexA - indexB}`);
return indexA - indexB;
})
.map((update, index) => {
// Enhanced: Get real progress and status for each target using updated_targets
const [currentProgress, status] = getProgressForUpdate(update.core);
const targetIndex = getTargetIndex(update.core);
const isCompletedFromServer = isTargetCompletedFromServer(update.core);
const isCurrentTarget = targetIndex === currentUpdateTargetIndex;
// Determine the actual state of this target
let targetState: 'completed' | 'current' | 'waiting' | 'error';
let displayProgress = 0;
let statusText = '';
if (status === UpdateStatus.ERROR) {
targetState = 'error';
statusText = 'Failed';
} else if (isCompletedFromServer || status === UpdateStatus.SUCCESS || currentProgress === 100) {
// Target is marked as completed in updated_targets array from server OR has success status
targetState = 'completed';
statusText = 'Complete';
displayProgress = 100;
} else if (status === UpdateStatus.STARTED || status === UpdateStatus.IN_PROGRESS) {
// Show progress for any target that has started or is in progress, regardless of currentUpdateTargetIndex
targetState = 'current';
displayProgress = currentProgress;
statusText = update.hasProgressBar ? `${currentProgress}%` : 'Updating...';
} else {
targetState = 'waiting';
statusText = 'Waiting';
displayProgress = 0;
}
return (
<div key={index} class="flex items-center gap-4 p-4 bg-base-200 rounded-lg">
{getUpdateIcon(update.core)}
<div class="flex-1">
<div class="flex justify-between items-center mb-2">
<span class="font-medium">{update.core}</span>
<span class="text-sm">
{targetState === 'completed' ? (
<span class="flex items-center gap-1 text-success">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-circle-check">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
<path d="M9 12l2 2l4 -4" />
</svg>
{statusText}
</span>
) : targetState === 'error' ? (
<span class="flex items-center gap-1 text-error">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/>
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
{statusText}
</span>
) : targetState === 'current' ? (
<span class="flex items-center gap-1 text-primary">
{statusText}
</span>
) : (
<span class="flex items-center gap-1 text-base-content/50">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-clock-pause">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20.942 13.018a9 9 0 1 0 -7.909 7.922" />
<path d="M12 7v5l2 2" />
<path d="M17 17v5" />
<path d="M21 17v5" />
</svg>
{statusText}
</span>
)}
</span>
</div>
{update.hasProgressBar ? (
<progress
class={`progress w-full ${
targetState === 'completed' ? 'progress-success' :
targetState === 'current' ? 'progress-primary' :
targetState === 'error' ? 'progress-error' :
'progress-base-300'
}`}
value={displayProgress}
max="100"
></progress>
) : (
<progress
class={`progress w-full ${
targetState === 'completed' ? 'progress-success' :
targetState === 'current' ? 'progress-primary' :
targetState === 'error' ? 'progress-error' :
'progress-base-300'
}`}
value={targetState === 'current' ? undefined : displayProgress}
max="100"
></progress>
)}
</div>
</div>
);
})}
</div>
<div class="alert alert-info mt-4">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Please do not power off the device during the update process.</span>
</div>
</>
)}
</div>
{(updateState === 'initial') && (
<div class="modal-backdrop" onClick={resetModal}></div>
)}
</dialog>
);
}

View file

@ -0,0 +1,692 @@
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';
// 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;
}
};
// 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<typeof setInterval> | null = null;
let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null;
let missedHeartbeats = 0;
// Reconnect state
let isReconnecting = false;
let reconnectTimeout: ReturnType<typeof setTimeout> | 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;
}
function clearReconnect() {
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);
}
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);
}
}
function startHeartbeat() {
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
// send request for available updates
setTimeout(() => {
if ($websocket.get() && ($websocket.get() as WebSocket).readyState === ($websocket.get() as WebSocket).OPEN) {
request_available_updates();
}
}, 1500);
}
}
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();
}
}
}
}
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);
}
}
function onError(error: Event) {
$wsConnectionStatus.set("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();
}
const connectWebSocket = () => {
// 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;
}
}
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");
};
}, []);
};

168
src/layouts/Layout.astro Normal file
View file

@ -0,0 +1,168 @@
---
import Sidebar from "../components/Sidebar.astro";
import GlobalModals from "../components/GlobalModals";
import MemoryProgressBar from "../components/MemoryProgressBar";
import "../styles/global.css";
import Router from "../components/Router.astro";
interface Props {
title?: string;
}
const { title = "Alpha Driver" } = Astro.props;
let currentTheme = "light";
let sidebarOpen = false;
---
<div class="drawer lg:drawer-open">
<input
id="drawer-toggle"
type="checkbox"
class="drawer-toggle"
checked={sidebarOpen}
/>
<div class="drawer-content flex flex-col">
{/* Top Navigation Bar */}
<div class="navbar bg-base-100 shadow-md">
<div class="flex-none lg:hidden">
<label for="drawer-toggle" class="btn btn-square btn-ghost">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block w-6 h-6 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</label>
</div>
<div class="flex-1">
<span class="text-xl font-audiowide">Alpha Driver Webinterface</span>
</div>
<div class="flex-1 flex justify-center">
<div class="w-80">
<MemoryProgressBar showDetails={false} className="text-xs" client:load />
</div>
</div>
<div class="flex-none">
{/* Theme select */}
<select
class="select select-bordered select-sm mr-2 theme-selector"
id="theme-selector"
value={currentTheme}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="goggles1">Goggles</option>
<option value="gogglesBW">Goggles B&W</option>
<option value="emerald">Nature</option>
<option value="dark-neon">Dark Neon</option>
<option value="retro">Sepia</option>
<option value="halloween">Chocolate</option>
<option value="winter">Winter</option>
</select>
</div>
</div>
{/* Main content */}
<main class="flex-1 p-4 overflow-x-auto" id="spa-main-content" >
{/* Global Update Notification */}
<div id="update-notification" class="alert alert-info mb-4 hidden">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-6 h-6">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4" />
<path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4" />
<path d="M12 9l0 3" />
<path d="M12 15l.01 0" />
</svg>
<span>Software updates are available for your device.</span>
<div>
<button class="btn btn-sm btn-success" onclick="if(typeof window.openUpdaterModal === 'function') window.openUpdaterModal()">
View Updates
</button>
<button class="btn btn-sm btn-ghost" onclick="document.getElementById('update-notification').classList.add('hidden')">
Dismiss
</button>
</div>
</div>
{/* Content will be loaded here by the router */}
<Router />
</main>
</div>
{/* Sidebar */}
<div class="drawer-side z-10">
<label for="drawer-toggle" aria-label="close sidebar" class="drawer-overlay"
></label>
<Sidebar />
</div>
{/* Global Modals */}
<GlobalModals client:load />
</div>
<script>
// Note: Update notification is now controlled by SecurityPage toggles
</script>
<script>
// Type declaration for global function
declare global {
interface Window {
handleNavigation: (path: string) => void;
navigateTo?: (path: string) => void;
}
}
// Theme handling
const themeSelector = document.getElementById(
"theme-selector",
) as HTMLSelectElement;
if (themeSelector) {
const savedTheme = localStorage.getItem("theme") || "light";
themeSelector.value = savedTheme;
document.documentElement.setAttribute("data-theme", savedTheme);
themeSelector.addEventListener("change", (e) => {
const target = e.target as HTMLSelectElement;
const theme = target.value;
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
});
}
// Drawer handling
const drawerToggle = document.getElementById(
"drawer-toggle",
) as HTMLInputElement;
if (drawerToggle) {
drawerToggle.addEventListener("change", (e) => {
const target = e.target as HTMLInputElement;
const checked = target.checked;
drawerToggle.checked = checked;
});
}
// Navigation handling
window.handleNavigation = function (path: string) {
console.log("handleNavigation", path);
// Add vibration feedback on mobile for page navigation
if ('vibrate' in navigator && window.innerWidth < 1024) {
navigator.vibrate(25);
}
if (drawerToggle) {
drawerToggle.checked = false;
}
if (typeof window !== "undefined" && window.navigateTo) {
window.navigateTo(path);
}
};
</script>

25
src/pages/index.astro Normal file
View file

@ -0,0 +1,25 @@
---
import { WebSocketProvider } from "../components/WebSocketProvider";
import { TestbenchWebSocketProvider } from "../components/TestbenchWebsocketProvider";
import Layout from "../layouts/Layout.astro";
import Router from "../components/Router.astro";
---
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>Testbench Webinterface</title>
<meta name="view-transition" content="same-origin" />
</head>
<body>
<Layout title="Alpha Driver - Dashboard" />
{/*<WebSocketProvider client:load />*/}
<WebSocketProvider client:load />
<TestbenchWebSocketProvider client:load />
</body>
</html>

239
src/styles/global.css Normal file
View file

@ -0,0 +1,239 @@
@font-face {
font-family: 'Varela Round';
src: url('../assets/VarelaRound-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Audiowide';
src: url('../assets/Audiowide-Regular.woff2') format('woff2');
font-weight: normal;
font-style: normal;
font-display: swap;
}
body {
font-family: 'Varela Round', sans-serif;
}
.font-audiowide, .navbar span.text-xl {
font-family: 'Audiowide', cursive;
}
/* Responsive improvements */
html, body {
overflow-x: hidden;
}
/* Faster drawer animations - 30% faster */
.drawer-side {
transition: transform 0.21s ease-out;
}
/* Fix join button corner radius issues */
.join > :not(:first-child):not(:last-child) {
border-radius: 0 !important;
}
.join > :first-child:not(:last-child) {
border-top-right-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
.join > :last-child:not(:first-child) {
border-top-left-radius: 0 !important;
border-bottom-left-radius: 0 !important;
}
/* Mobile responsive adjustments */
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr !important;
}
.stats {
grid-template-columns: 1fr;
}
.navbar span {
font-size: 1rem;
}
.card-body {
padding: 1rem;
}
/* Fix horizontal button groups on mobile - but NOT tabs or joins */
.join:not(.tabs):not(.btn-group) {
flex-direction: column;
width: 100%;
}
.join:not(.tabs):not(.btn-group) .btn {
border-radius: 0.5rem !important;
margin-bottom: 0.25rem;
}
.join:not(.tabs):not(.btn-group) .btn:first-child {
border-radius: 0.5rem !important;
}
.join:not(.tabs):not(.btn-group) .btn:last-child {
border-radius: 0.5rem !important;
margin-bottom: 0;
}
}
@media (max-width: 480px) {
.text-2xl {
font-size: 1.5rem;
}
.navbar {
padding: 0.5rem;
}
.theme-selector {
width: auto;
min-width: 80px;
}
}
/* Tap feedback for mobile */
.btn:active,
.menu a:active {
transform: scale(0.98);
transition: transform 0.1s ease;
}
/* Goggles B&W theme improvements */
[data-theme="gogglesBW"] {
/* Better sidebar menu contrast */
.menu li > * {
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.05);
}
.menu li > *:hover {
border-color: rgba(255, 255, 255, 0.4);
background-color: rgba(255, 255, 255, 0.1);
}
.menu li > *.active {
border-color: #ffffff;
background-color: rgba(255, 255, 255, 0.2);
color: #000000;
font-weight: bold;
}
/* Better button visibility with stronger outlines */
.btn {
border: 2px solid rgba(255, 255, 255, 0.4) !important;
background-color: rgba(255, 255, 255, 0.1) !important;
box-shadow: 0 1px 3px rgba(255, 255, 255, 0.2) !important;
}
.btn:hover {
border-color: rgba(255, 255, 255, 0.7) !important;
background-color: rgba(255, 255, 255, 0.2) !important;
box-shadow: 0 2px 6px rgba(255, 255, 255, 0.3) !important;
}
.btn-active, .btn.btn-active {
border-color: #ffffff !important;
background-color: rgba(255, 255, 255, 0.3) !important;
color: #000000 !important;
font-weight: bold !important;
}
/* Join buttons specific styling */
.join .btn {
border: 2px solid rgba(255, 255, 255, 0.4) !important;
background-color: rgba(255, 255, 255, 0.05) !important;
}
.join .btn:hover {
border-color: rgba(255, 255, 255, 0.7) !important;
background-color: rgba(255, 255, 255, 0.15) !important;
}
/* High contrast slider styles */
.range {
background-color: #444444;
}
.range::-webkit-slider-thumb {
background-color: #ffffff;
}
.range::-moz-range-thumb {
background-color: #ffffff;
}
/* Make progress bars and indicators more visible */
.range-primary {
--range-shdw: #ffffff;
}
/* Target voltage and similar sliders */
input[type="range"] {
background-color: #444444;
}
.progress::-webkit-progress-bar,
.progress::-moz-progress-bar {
background-color: #444444;
}
.progress::-webkit-progress-value,
.progress::-moz-progress-value {
background-color: #ffffff;
}
/* Ensure gradient charts use high contrast */
.absolute.top-0.bottom-0.left-0.rounded-lg {
background: white !important;
}
}
/* High contrast slider styles for GogglesBW theme (keeping original) */
[data-theme="gogglesBW"] .range {
background-color: #444444;
}
[data-theme="gogglesBW"] .range::-webkit-slider-thumb {
background-color: #ffffff;
}
[data-theme="gogglesBW"] .range::-moz-range-thumb {
background-color: #ffffff;
}
/* Make progress bars and indicators more visible */
[data-theme="gogglesBW"] .range-primary {
--range-shdw: #ffffff;
}
/* Target voltage and similar sliders in control/dcdc pages */
[data-theme="gogglesBW"] input[type="range"] {
background-color: #444444;
}
[data-theme="gogglesBW"] .progress::-webkit-progress-bar,
[data-theme="gogglesBW"] .progress::-moz-progress-bar {
background-color: #444444;
}
[data-theme="gogglesBW"] .progress::-webkit-progress-value,
[data-theme="gogglesBW"] .progress::-moz-progress-value {
background-color: #ffffff;
}
/* Ensure gradient charts use high contrast in GogglesBW theme */
[data-theme="gogglesBW"] .absolute.top-0.bottom-0.left-0.rounded-lg {
background: white !important;
}