OTA steps 3-5: Update Service, Probation and Rollback, Update from SD

- EcdsaVerifier (mbedTLS, embedded public key) and EspOtaSink (writes
  the inactive app slot, esp_ota_end validates the image, then sets
  the boot partition)
- UpdateService: listens on TCP 3232 (and mDNS roro9stack-<id>) while
  Wi-Fi is Connected; streams into UpdateParser; replies OK/ERR to the
  sender; remembers the pending version so a Rollback is reported
  after the reboot
- Probation (host-tested): confirm after the first frame + 30 s + Wi-Fi
  (if configured); roll back if configured Wi-Fi never connects in 3 min
- Main loop: full-screen progress while receiving; restart once
  installed, waiting up to 60 s for Text Entry to end
- Settings > Firmware: version, Probation status, push address and
  name, and the .ota files in /updates on the SD card to install
- StorageService.runJob() runs work on the storage task (SD installs)
- wifi status prints IP and running version; RORO_TEST_CRASH test hook

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-03 21:35:33 +02:00
co-authored by Claude Opus 5.5
parent 90cb6ef9b2
commit 21b3d9e422
19 changed files with 655 additions and 10 deletions
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <freertos/FreeRTOS.h>
#include <string>
#include "event_bus.h"
#include "key_value_store.h"
#include "service.h"
#include "services/storage_service.h"
#include "services/wifi_service.h"
namespace roro {
// Firmware Updates (see CONTEXT.md and docs/milestones/OTA.md): listens for signed Update Files on
// TCP 3232 while Wi-Fi is Connected, installs them from the SD card on request, keeps new firmware
// on Probation until it proves healthy, and reports a Rollback after the reboot.
class UpdateService : public Service {
public:
enum class Phase { Idle, Receiving, Installed, Failed };
static constexpr uint16_t kPort = 3232;
UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
EventBus& bus, const Settings& settings);
const char* name() const override { return "update"; }
uint32_t tickIntervalMs() const override { return 1000; }
void start() override;
void tick(uint32_t nowMs) override;
// For the progress screen and the Firmware page.
Phase phase() const { return phase_; }
int percent() const { return percent_; }
std::string incomingVersion() const;
bool onProbation() const { return probation_; }
const std::string& hostname() const { return hostname_; }
// Installs an Update File from the SD card (runs on the storage task).
void installFromSd(const std::string& path);
// The main loop calls this once it has drawn a frame (part of Probation).
void firstFrameDrawn() { firstFrame_ = true; }
// True when an installed update is waiting to reboot; the main loop reboots when it's safe.
bool rebootPending() const { return phase_ == Phase::Installed; }
private:
static void taskEntry(void* self);
void listen();
void install(class UpdateSource& source, const char* via);
void notify(const std::string& text, NotificationLevel level);
KeyValueStore& store_;
WifiService& wifi_;
SavedNetworks& saved_;
StorageService& storage_;
EventBus& bus_;
const Settings& settings_;
TaskHandle_t task_ = nullptr;
std::string hostname_;
volatile Phase phase_ = Phase::Idle;
volatile int percent_ = 0;
std::string incoming_;
bool probation_ = false;
volatile bool firstFrame_ = false;
};
} // namespace roro