From c2929a65ae909c0668be33ff8350bc850171640e Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 16:34:26 +0200 Subject: [PATCH 1/4] enable ota_update webserver path again take latest ota code from `develop` branch and apply to legacy branch --- rust/src/webserver/mod.rs | 124 +++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 49 deletions(-) diff --git a/rust/src/webserver/mod.rs b/rust/src/webserver/mod.rs index 8133de3..7f128aa 100644 --- a/rust/src/webserver/mod.rs +++ b/rust/src/webserver/mod.rs @@ -30,59 +30,80 @@ use core::result::Result::Ok; use core::sync::atomic::{AtomicBool, Ordering}; use edge_http::io::server::{Connection, Handler, Server}; use edge_http::Method; -use edge_nal::TcpBind; use edge_nal::io::{Read, Write}; +use edge_nal::TcpBind; use edge_nal_embassy::{Tcp, TcpBuffers}; use embassy_net::Stack; use embassy_time::Instant; -use log::info; +use log::{error, info}; -// fn ota( -// request: &mut Request<&mut EspHttpConnection>, -// ) -> Result, anyhow::Error> { -// let mut board = BOARD_ACCESS.lock().unwrap(); -// let mut ota = OtaUpdate::begin()?; -// log::info!("start ota"); -// -// //having a larger buffer is not really faster, requires more stack and prevents the progress bar from working ;) -// const BUFFER_SIZE: usize = 512; -// let mut buffer: [u8; BUFFER_SIZE] = [0; BUFFER_SIZE]; -// let mut total_read: usize = 0; -// let mut lastiter = 0; -// loop { -// let read = request.read(&mut buffer)?; -// total_read += read; -// let to_write = &buffer[0..read]; -// //delay for watchdog and wifi stuff -// board.board_hal.get_esp().delay.delay_ms(1); -// -// let iter = (total_read / 1024) % 8; -// if iter != lastiter { -// board.board_hal.general_fault(iter % 5 == 0); -// for i in 0..PLANT_COUNT { -// let _ = board.board_hal.fault(i, iter == i); -// } -// lastiter = iter; -// } -// -// ota.write(to_write)?; -// if read == 0 { -// break; -// } -// } -// log::info!("wrote bytes ota {total_read}"); -// log::info!("finish ota"); -// let partition = ota.raw_partition(); -// log::info!("finalizing and changing boot partition to {partition:?}"); -// -// let mut finalizer = ota.finalize()?; -// log::info!("changing boot partition"); -// board.board_hal.get_esp().set_restart_to_conf(true); -// drop(board); -// finalizer.set_as_boot_partition()?; -// anyhow::Ok(None) -// } -// +pub(crate) async fn ota_operations( + conn: &mut Connection<'_, T, { N }>, + method: Method, +) -> Result, FatError> +where + T: Read + Write, +{ + Ok(match method { + Method::Options => { + conn.initiate_response( + 200, + Some("OK"), + &[ + ("Access-Control-Allow-Origin", "*"), + ("Access-Control-Allow-Headers", "*"), + ("Access-Control-Allow-Methods", "*"), + ], + ) + .await?; + Some(200) + } + Method::Post => { + let mut offset = 0_usize; + let mut chunk = 0; + loop { + let buf = read_up_to_bytes_from_request(conn, Some(4096)).await?; + if buf.is_empty() { + info!("file request for ota finished"); + let mut board = BOARD_ACCESS.get().await.lock().await; + board.board_hal.get_esp().finalize_ota().await?; + break; + } else { + let mut board = BOARD_ACCESS.get().await.lock().await; + board.board_hal.progress(chunk as u32).await; + // Erase next block if we are at a 4K boundary (including the first block at offset 0) + board + .board_hal + .get_esp() + .write_ota(offset as u32, &buf) + .await?; + } + offset += buf.len(); + chunk += 1; + } + BOARD_ACCESS + .get() + .await + .lock() + .await + .board_hal + .clear_progress() + .await; + conn.initiate_response( + 200, + Some("OK"), + &[ + ("Access-Control-Allow-Origin", "*"), + ("Access-Control-Allow-Headers", "*"), + ("Access-Control-Allow-Methods", "*"), + ], + ) + .await?; + Some(200) + } + _ => None, + }) +} struct HTTPRequestRouter { reboot_now: Arc, @@ -105,7 +126,12 @@ impl Handler for HTTPRequestRouter { let path = headers.path; let prefix = "/file?filename="; - let status = if path.starts_with(prefix) { + let status = if path == "/ota" { + ota_operations(conn, method).await.map_err(|e| { + error!("Error handling ota: {e}"); + e + })? + } else if path.starts_with(prefix) { file_operations(conn, method, &path, &prefix).await? } else { match method { From 08ee9018cff24e3bbc9a1979fe5d12785071015e Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 16:44:30 +0200 Subject: [PATCH 2/4] website add toast service for notifications --- rust/src_webpack/src/main.ts | 1 + rust/src_webpack/src/toast.ts | 94 +++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 rust/src_webpack/src/toast.ts diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index ba8c957..69845dc 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -31,6 +31,7 @@ import { FileList, SolarState, PumpTestResult } from "./api"; import {SolarView} from "./solarview"; +import {toast} from "./toast"; export class Controller { loadTankInfo(): Promise { diff --git a/rust/src_webpack/src/toast.ts b/rust/src_webpack/src/toast.ts new file mode 100644 index 0000000..bca6b71 --- /dev/null +++ b/rust/src_webpack/src/toast.ts @@ -0,0 +1,94 @@ +class ToastService { + private container: HTMLElement; + private stylesInjected = false; + + constructor() { + this.container = this.ensureContainer(); + this.injectStyles(); + } + + info(message: string, timeoutMs: number = 5000) { + const el = this.createToast(message, 'info'); + this.container.appendChild(el); + // Auto-dismiss after timeout + const timer = window.setTimeout(() => this.dismiss(el), timeoutMs); + // Dismiss on click immediately + el.addEventListener('click', () => { + window.clearTimeout(timer); + this.dismiss(el); + }); + } + + error(message: string) { + console.error(message); + const el = this.createToast(message, 'error'); + this.container.appendChild(el); + // Only dismiss on click + el.addEventListener('click', () => this.dismiss(el)); + } + + private dismiss(el: HTMLElement) { + if (!el.parentElement) return; + el.parentElement.removeChild(el); + } + + private createToast(message: string, type: 'info' | 'error'): HTMLElement { + const div = document.createElement('div'); + div.className = `toast ${type}`; + div.textContent = message; + div.setAttribute('role', 'status'); + div.setAttribute('aria-live', 'polite'); + return div; + } + + private ensureContainer(): HTMLElement { + let container = document.getElementById('toast-container'); + if (!container) { + container = document.createElement('div'); + container.id = 'toast-container'; + document.body.appendChild(container); + } + return container; + } + + private injectStyles() { + if (this.stylesInjected) return; + const style = document.createElement('style'); + style.textContent = ` +#toast-container { + position: fixed; + top: 12px; + right: 12px; + display: flex; + flex-direction: column; + gap: 8px; + z-index: 9999; +} +.toast { + max-width: 320px; + padding: 10px 12px; + border-radius: 6px; + box-shadow: 0 2px 6px rgba(0,0,0,0.2); + cursor: pointer; + user-select: none; + font-family: sans-serif; + font-size: 14px; + line-height: 1.3; +} +.toast.info { + background-color: #d4edda; /* green-ish */ + color: #155724; + border-left: 4px solid #28a745; +} +.toast.error { + background-color: #f8d7da; /* red-ish */ + color: #721c24; + border-left: 4px solid #dc3545; +} +`; + document.head.appendChild(style); + this.stylesInjected = true; + } +} + +export const toast = new ToastService(); From e3b7648a3f46b742c41be441786c355d97355c7c Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 16:45:06 +0200 Subject: [PATCH 3/4] website update version display and ota handler --- rust/src_webpack/src/api.ts | 10 ++++- rust/src_webpack/src/main.ts | 15 ++++++-- rust/src_webpack/src/ota.html | 70 ++++++++++++++++++++++++++--------- rust/src_webpack/src/ota.ts | 36 +++++++++++++++--- 4 files changed, 103 insertions(+), 28 deletions(-) diff --git a/rust/src_webpack/src/api.ts b/rust/src_webpack/src/api.ts index 3f13702..a234347 100644 --- a/rust/src_webpack/src/api.ts +++ b/rust/src_webpack/src/api.ts @@ -157,7 +157,13 @@ export interface Moistures { export interface VersionInfo { git_hash: string, build_time: string, - partition: string + current: string, + slot0_state: string, + slot1_state: string, + heap_total: number, + heap_used: number, + heap_free: number, + heap_max_used: number, } export interface BatteryState { @@ -189,4 +195,4 @@ export interface TankInfo { /// water temperature water_temp: number | null, temp_sensor_error: string | null -} \ No newline at end of file +} diff --git a/rust/src_webpack/src/main.ts b/rust/src_webpack/src/main.ts index 69845dc..28505aa 100644 --- a/rust/src_webpack/src/main.ts +++ b/rust/src_webpack/src/main.ts @@ -201,15 +201,22 @@ export class Controller { }, false); ajax.addEventListener("load", () => { controller.progressview.removeProgress("ota_upload") - controller.reboot(); + const status = ajax.status; + if (status >= 200 && status < 300) { + controller.reboot(); + } else { + const statusText = ajax.statusText || ""; + const body = ajax.responseText || ""; + toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`); + } }, false); ajax.addEventListener("error", () => { - alert("Error ota") controller.progressview.removeProgress("ota_upload") + toast.error("OTA upload failed due to a network error."); }, false); ajax.addEventListener("abort", () => { - alert("abort ota") controller.progressview.removeProgress("ota_upload") + toast.error("OTA upload was aborted."); }, false); ajax.open("POST", PUBLIC_URL + "/ota"); ajax.send(file); @@ -570,4 +577,4 @@ window.addEventListener("beforeunload", (event) => { event.returnValue = confirmationMessage; // This will trigger the browser's default dialog return confirmationMessage; } -}); \ No newline at end of file +}); diff --git a/rust/src_webpack/src/ota.html b/rust/src_webpack/src/ota.html index 8584893..ce6ed39 100644 --- a/rust/src_webpack/src/ota.html +++ b/rust/src_webpack/src/ota.html @@ -1,23 +1,27 @@
Current Firmware
+
Buildtime: @@ -28,14 +32,46 @@
- Partition: - + Partition: + +
+
+ State0: + +
+
+ State1: +

+
+
+ Heap Memory +
+
+
+
+ Free: + +
+
+ Used: + +
+
+ Total: + +
+
+ Peak used: + +
+ +
-
\ No newline at end of file + diff --git a/rust/src_webpack/src/ota.ts b/rust/src_webpack/src/ota.ts index f596ff1..dc0fee7 100644 --- a/rust/src_webpack/src/ota.ts +++ b/rust/src_webpack/src/ota.ts @@ -1,22 +1,38 @@ -import { Controller } from "./main"; +import {Controller} from "./main"; import {VersionInfo} from "./api"; +function fmtBytes(n: number): string { + return `${n} B (${(n / 1024).toFixed(1)} KiB)`; +} + export class OTAView { readonly file1Upload: HTMLInputElement; readonly firmware_buildtime: HTMLDivElement; readonly firmware_githash: HTMLDivElement; readonly firmware_partition: HTMLDivElement; + readonly firmware_state0: HTMLDivElement; + readonly firmware_state1: HTMLDivElement; + readonly heap_free: HTMLDivElement; + readonly heap_used: HTMLDivElement; + readonly heap_total: HTMLDivElement; + readonly heap_max_used: HTMLDivElement; constructor(controller: Controller) { (document.getElementById("firmwareview") as HTMLElement).innerHTML = require("./ota.html") - let test = document.getElementById("test") as HTMLButtonElement; + let test = document.getElementById("test") as HTMLButtonElement; + let refresh = document.getElementById("refresh_firmware_info") as HTMLButtonElement; this.firmware_buildtime = document.getElementById("firmware_buildtime") as HTMLDivElement; this.firmware_githash = document.getElementById("firmware_githash") as HTMLDivElement; this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement; + this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement; + this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement; + this.heap_free = document.getElementById("heap_free") as HTMLDivElement; + this.heap_used = document.getElementById("heap_used") as HTMLDivElement; + this.heap_total = document.getElementById("heap_total") as HTMLDivElement; + this.heap_max_used = document.getElementById("heap_max_used") as HTMLDivElement; - const file = document.getElementById("firmware_file") as HTMLInputElement; this.file1Upload = file this.file1Upload.onchange = () => { @@ -31,11 +47,21 @@ export class OTAView { test.onclick = () => { controller.selfTest(); } + + refresh.onclick = () => { + controller.version(); + } } setVersion(versionInfo: VersionInfo) { this.firmware_buildtime.innerText = versionInfo.build_time; this.firmware_githash.innerText = versionInfo.git_hash; - this.firmware_partition.innerText = versionInfo.partition; + this.firmware_partition.innerText = versionInfo.current; + this.firmware_state0.innerText = versionInfo.slot0_state; + this.firmware_state1.innerText = versionInfo.slot1_state; + this.heap_free.innerText = fmtBytes(versionInfo.heap_free); + this.heap_used.innerText = fmtBytes(versionInfo.heap_used); + this.heap_total.innerText = fmtBytes(versionInfo.heap_total); + this.heap_max_used.innerText = fmtBytes(versionInfo.heap_max_used); } -} \ No newline at end of file +} From 52049c456e395cd5c6b22c5d3df8cd10bdbcd186 Mon Sep 17 00:00:00 2001 From: ju6ge Date: Sun, 10 May 2026 17:22:45 +0200 Subject: [PATCH 4/4] update read_up_to_bytes_from_request with more robust implementation --- rust/src/webserver/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/rust/src/webserver/mod.rs b/rust/src/webserver/mod.rs index 7f128aa..e03ce19 100644 --- a/rust/src/webserver/mod.rs +++ b/rust/src/webserver/mod.rs @@ -221,12 +221,18 @@ where let mut data_store = Vec::new(); let mut total_read = 0; loop { + let left = max_read - total_read; let mut buf = [0_u8; 64]; - let read = request.read(&mut buf).await?; + let s_buf = if buf.len() <= left { + &mut buf + } else { + &mut buf[0..left] + }; + let read = request.read(s_buf).await?; if read == 0 { break; } - let actual_data = &buf[0..read]; + let actual_data = &s_buf[0..read]; total_read += read; if total_read > max_read { bail!("Request too large {total_read} > {max_read}");