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:
+61
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user