Firmware update modal added

This commit is contained in:
SebastianOtt 2026-05-28 00:45:18 +02:00
parent a326853d45
commit f9c7ab2b50
8 changed files with 1020 additions and 2 deletions

View file

@ -1,6 +1,12 @@
import { useEffect } from "preact/hooks";
import { $websocket } from "./WebSocketAtom";
import { stateSetters } from "./AppProviderStore";
import {
stateSetters,
LITTLEFS_VERSION,
type UpdateInfo,
type UpdateStatus,
type UpdateTarget,
} from "./AppProviderStore";
import {
$scanned_wifi_networks,
$current_network_configuration,
@ -11,6 +17,7 @@ import {
authmodeToSecurity,
ConnectionMode,
} from "./NetworkManagerStore";
import { send_webinterface_version } from "./ota_update_components/OtaCommands";
export { $websocket } from "./WebSocketAtom";
@ -113,6 +120,71 @@ function onOpen() {
setTimeout(() => {
request_network_config();
}, 100);
// Inform the firmware which webinterface version this bundle was built with.
// The firmware uses this as update_info_s3_webinterface.currentVersion for the
// OTA check, so independent FS-only updates work correctly. See
// set_webinterface_version handler in websocket_server.cpp.
setTimeout(() => {
console.log("[OTA] sending LITTLEFS_VERSION=", LITTLEFS_VERSION);
send_webinterface_version(LITTLEFS_VERSION);
}, 200);
}
// Map FW status strings (target_update_status_t -> getStatusString in
// update_notifier.cpp) to the web app's UpdateStatus union.
function mapStatus(s: string | undefined): UpdateStatus {
switch (s) {
case "no_update":
case "started":
case "in_progress":
case "completed":
case "error":
case "restarting":
case "waiting":
return s;
default:
console.warn("[OTA] unknown status string from FW:", s);
return "no_update";
}
}
function isUpdateTarget(s: string | undefined): s is UpdateTarget {
return s === "s3_firmware" || s === "s3_webinterface";
}
// Translate an `update_progress` frame into atom updates + drive the modal
// state machine. Called from onMessage.
function handleUpdateProgress(target: UpdateTarget, progress: number, status: UpdateStatus, errorMsg?: string) {
console.log(`[OTA] progress: target=${target} progress=${progress} status=${status}`,
errorMsg ? `error=${errorMsg}` : "");
if (target === "s3_firmware") {
stateSetters.setS3FirmwareProgress(progress, status);
} else {
stateSetters.setS3WebinterfaceProgress(progress, status);
}
if (status === "error") {
if (errorMsg) stateSetters.setUpdateErrorFor(target, errorMsg);
stateSetters.setUpdateState("error");
} else if (status === "restarting") {
stateSetters.setUpdateState("restarting");
} else if (status === "started" || status === "in_progress") {
stateSetters.setUpdateState("updating");
} else if (status === "completed") {
// Completion of one target doesn't necessarily mean the whole sequence is
// done — the firmware sends per-target completed frames and a final
// restart frame when the firmware part lands. We let device_restarting /
// a separate aggregate trigger flip to 'success'.
if (target === "s3_firmware") {
// FW always restarts after its own update → next frame will be
// device_restarting. Mark as restarting now so the success popup picks
// up the right copy.
stateSetters.setUpdateState("restarting");
} else {
// Web-only successful update: no restart, switch straight to success.
stateSetters.setUpdateState("success");
}
}
}
function onMessage(message: MessageEvent) {
@ -181,6 +253,52 @@ function onMessage(message: MessageEvent) {
$network_config_error.set({ reason, failed_ssid: failedSsid, connection_mode: connectionMode });
} else if (dataFromServer.message_type === "error_popup") {
console.warn(`[error_popup] ${dataFromServer.error ?? dataFromServer.message ?? "(no detail)"}`);
} else if (dataFromServer.message_type === "update_information") {
// Server-side response to FIRMWARE_UPDATE_CMD_REQUEST_AVAILABLE_UPDATES.
// Shape: { update_available: { s3_firmware?: {...}, s3_webinterface?: {...} }, changelog }
console.log("[OTA] update_information received", dataFromServer);
const ua = dataFromServer.update_available ?? {};
const updates: UpdateInfo[] = [
{
target: "s3_webinterface",
label: "Webinterface",
currentVersion: ua.s3_webinterface?.current_version ?? LITTLEFS_VERSION,
newVersion: ua.s3_webinterface?.new_version ?? "",
updateAvailable: !!ua.s3_webinterface?.updateAvailable,
},
{
target: "s3_firmware",
label: "S3 Firmware",
currentVersion: ua.s3_firmware?.current_version ?? "",
newVersion: ua.s3_firmware?.new_version ?? "",
updateAvailable: !!ua.s3_firmware?.updateAvailable,
},
];
stateSetters.setAvailableUpdates(updates);
stateSetters.setUpdateChangelog(dataFromServer.changelog ?? "");
if (updates.some((u) => u.updateAvailable)) {
console.log("[OTA] updates available — opening UpdaterModal");
stateSetters.setUpdaterModalOpen(true);
} else {
console.log("[OTA] no updates available");
}
} else if (dataFromServer.message_type === "update_progress") {
if (!isUpdateTarget(dataFromServer.target)) {
console.warn("[OTA] update_progress with unknown target:", dataFromServer.target);
} else {
handleUpdateProgress(
dataFromServer.target,
Number(dataFromServer.progress ?? 0),
mapStatus(dataFromServer.status),
dataFromServer.error,
);
}
} else if (dataFromServer.message_type === "update_in_progress") {
console.log("[OTA] update_in_progress=", dataFromServer.in_progress, "target=", dataFromServer.target);
stateSetters.setUpdateInProgress(!!dataFromServer.in_progress);
} else if (dataFromServer.message_type === "device_restarting") {
console.log("[OTA] device_restarting received — FW is rebooting");
stateSetters.setUpdateState("restarting");
} else {
// Unbekannter Nachrichtentyp - korrektes Feld protokollieren
console.warn(`[${new Date().toISOString()}] Unbekannter Nachrichtentyp: ${dataFromServer.message_type ?? "undefined"}`);