Add web dashboard OTA upload with live progress.

Share UART OTA logic between CLI and serve via POST /api/ota, WebSocket progress events, and a dashboard upload UI showing the running partition.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-19 00:45:54 +02:00
co-authored by Cursor
parent 59ca269407
commit 4bf43d8a5e
6 changed files with 463 additions and 198 deletions
+61 -1
View File
@@ -3,12 +3,15 @@ package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"powerpod/gotool/pb"
)
const otaMaxFirmwareSize = 2 * 1024 * 1024
type deadzoneAPIResponse struct {
Deadzone uint32 `json:"deadzone"`
ClientID uint32 `json:"client_id"`
@@ -37,7 +40,14 @@ type unicastAPIResponse struct {
Error string `json:"error,omitempty"`
}
func mountServeAPI(mux *http.ServeMux, link *managedSerial) {
type otaAPIResponse struct {
Success bool `json:"success"`
BytesWritten uint32 `json:"bytes_written,omitempty"`
TargetSlot uint32 `json:"target_slot,omitempty"`
Error string `json:"error,omitempty"`
}
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub) {
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
@@ -55,6 +65,56 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial) {
}
serveUnicastTest(w, r, link)
})
mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveOTAUpload(w, r, link, hub)
})
}
func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub) {
if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "invalid form"})
return
}
file, _, err := r.FormFile("firmware")
if err != nil {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "firmware file required"})
return
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, otaMaxFirmwareSize))
if err != nil {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: err.Error()})
return
}
if len(data) == 0 {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "empty firmware"})
return
}
var last OTAProgress
err = runOTAUpload(link, data, func(p OTAProgress) {
last = p
if hub != nil {
hub.broadcastRaw(p)
}
})
if err != nil {
if hub != nil {
hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()})
}
writeJSON(w, http.StatusServiceUnavailable, otaAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, otaAPIResponse{
Success: true,
BytesWritten: last.Bytes,
TargetSlot: last.Slot,
})
}
func serveDeadzoneGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {