Public Access
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:
@@ -0,0 +1,196 @@
|
||||
#include "update_service.h"
|
||||
|
||||
#include <ESPmDNS.h>
|
||||
#include <SD.h>
|
||||
#include <WiFi.h>
|
||||
#include <esp_ota_ops.h>
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "platform/identity.h"
|
||||
#include "platform/ota_device.h"
|
||||
#include "probation.h"
|
||||
#include "update_parser.h"
|
||||
#include "version.h"
|
||||
|
||||
namespace roro {
|
||||
|
||||
// Where an Update File comes from: the network or a file on the SD card.
|
||||
class UpdateSource {
|
||||
public:
|
||||
virtual ~UpdateSource() = default;
|
||||
// Bytes read, 0 at the end of the file, -1 on an error or a stall.
|
||||
virtual int read(uint8_t* buf, size_t len) = 0;
|
||||
virtual void reply(const std::string& line) { (void)line; }
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kStallMs = 10000;
|
||||
constexpr size_t kChunk = 4096;
|
||||
|
||||
class NetSource : public UpdateSource {
|
||||
public:
|
||||
explicit NetSource(NetworkClient& c) : c_(c) {}
|
||||
int read(uint8_t* buf, size_t len) override {
|
||||
uint32_t since = millis();
|
||||
while (!c_.available()) {
|
||||
if (!c_.connected()) return 0; // the sender closed its side: end of file
|
||||
if (millis() - since > kStallMs) return -1;
|
||||
delay(5);
|
||||
}
|
||||
return c_.read(buf, len);
|
||||
}
|
||||
void reply(const std::string& line) override {
|
||||
c_.print((line + "\n").c_str());
|
||||
c_.flush();
|
||||
}
|
||||
|
||||
private:
|
||||
NetworkClient& c_;
|
||||
};
|
||||
|
||||
class FileSource : public UpdateSource {
|
||||
public:
|
||||
explicit FileSource(File& f) : f_(f) {}
|
||||
int read(uint8_t* buf, size_t len) override { return static_cast<int>(f_.read(buf, len)); }
|
||||
|
||||
private:
|
||||
File& f_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
UpdateService::UpdateService(KeyValueStore& store, WifiService& wifi, SavedNetworks& saved, StorageService& storage,
|
||||
EventBus& bus, const Settings& settings)
|
||||
: store_(store), wifi_(wifi), saved_(saved), storage_(storage), bus_(bus), settings_(settings) {
|
||||
hostname_ = "roro9stack-" + identity::defaultShortName();
|
||||
}
|
||||
|
||||
void UpdateService::notify(const std::string& text, NotificationLevel level) {
|
||||
bus_.publish(Event::withText(EventType::Notification, text.c_str(), static_cast<int32_t>(level)));
|
||||
}
|
||||
|
||||
std::string UpdateService::incomingVersion() const { return incoming_; }
|
||||
|
||||
void UpdateService::start() {
|
||||
// Probation: new firmware boots "pending verify" until it marks itself valid.
|
||||
esp_ota_img_states_t state;
|
||||
probation_ = esp_ota_get_state_partition(esp_ota_get_running_partition(), &state) == ESP_OK &&
|
||||
state == ESP_OTA_IMG_PENDING_VERIFY;
|
||||
|
||||
// A pending update that isn't the running version means the bootloader rolled it back.
|
||||
std::string pending, from;
|
||||
store_.getString("ota_pending", pending);
|
||||
store_.getString("ota_from", from);
|
||||
if (!pending.empty() && pending != versionString()) {
|
||||
notify("Update to " + pending + " failed, back on " + versionString(), NotificationLevel::Warning);
|
||||
store_.putString("ota_pending", "");
|
||||
}
|
||||
|
||||
if (!task_) xTaskCreate(taskEntry, "update", 8192, this, 1, &task_);
|
||||
}
|
||||
|
||||
void UpdateService::tick(uint32_t nowMs) {
|
||||
if (!probation_) return;
|
||||
bool wifiConfigured = settings_.getBool(Setting::WifiEnabled) && saved_.count() > 0;
|
||||
bool wifiUp = wifi_.state() == WifiController::State::Connected;
|
||||
switch (Probation::judge(nowMs, firstFrame_, wifiConfigured, wifiUp)) {
|
||||
case Probation::Verdict::Wait: break;
|
||||
case Probation::Verdict::Confirm:
|
||||
esp_ota_mark_app_valid_cancel_rollback();
|
||||
probation_ = false;
|
||||
store_.putString("ota_pending", "");
|
||||
notify(std::string("Updated to ") + versionString(), NotificationLevel::Info);
|
||||
break;
|
||||
case Probation::Verdict::RollBack:
|
||||
// ota_pending still names this version: the previous firmware will report the failure.
|
||||
delay(200);
|
||||
esp_ota_mark_app_invalid_rollback_and_reboot();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateService::install(UpdateSource& source, const char* via) {
|
||||
EcdsaVerifier verifier;
|
||||
EspOtaSink sink;
|
||||
UpdateParser parser(verifier, sink, sink.capacity(), versionString());
|
||||
std::unique_ptr<uint8_t[]> buf(new uint8_t[kChunk]);
|
||||
incoming_.clear();
|
||||
percent_ = 0;
|
||||
phase_ = Phase::Receiving;
|
||||
|
||||
bool ok = false;
|
||||
for (;;) {
|
||||
int n = source.read(buf.get(), kChunk);
|
||||
if (n < 0) break; // stalled or broken: aborted below
|
||||
if (n == 0) {
|
||||
ok = parser.end();
|
||||
break;
|
||||
}
|
||||
parser.feed(buf.get(), n);
|
||||
if (incoming_.empty() && parser.state() == UpdateParser::State::Image) incoming_ = parser.version();
|
||||
percent_ = parser.percent();
|
||||
if (parser.state() == UpdateParser::State::Failed) break;
|
||||
}
|
||||
if (!ok && parser.error().empty()) parser.end(); // e.g. a stall: abort the slot
|
||||
|
||||
if (ok) {
|
||||
store_.putString("ota_pending", parser.version());
|
||||
store_.putString("ota_from", versionString());
|
||||
source.reply("OK " + parser.version() + (parser.isDowngrade() ? " (older than the installed one)" : ""));
|
||||
notify("Update " + parser.version() + " installed (" + via + "), restarting", NotificationLevel::Info);
|
||||
phase_ = Phase::Installed;
|
||||
} else {
|
||||
std::string why = parser.error().empty() ? "transfer interrupted" : parser.error();
|
||||
source.reply("ERR " + why);
|
||||
notify("Update refused: " + why, NotificationLevel::Warning);
|
||||
phase_ = Phase::Idle;
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateService::installFromSd(const std::string& path) {
|
||||
if (phase_ != Phase::Idle) return;
|
||||
storage_.runJob([this, path]() {
|
||||
File f = SD.open(path.c_str());
|
||||
if (!f) {
|
||||
notify("Can't open " + path, NotificationLevel::Warning);
|
||||
return;
|
||||
}
|
||||
FileSource src(f);
|
||||
install(src, "SD card");
|
||||
f.close();
|
||||
});
|
||||
}
|
||||
|
||||
void UpdateService::taskEntry(void* self) { static_cast<UpdateService*>(self)->listen(); }
|
||||
|
||||
void UpdateService::listen() {
|
||||
NetworkServer server(kPort);
|
||||
bool listening = false;
|
||||
for (;;) {
|
||||
bool connected = wifi_.state() == WifiController::State::Connected;
|
||||
if (connected && !listening) {
|
||||
server.begin();
|
||||
if (MDNS.begin(hostname_.c_str())) MDNS.addService("roro9stack", "tcp", kPort);
|
||||
listening = true;
|
||||
} else if (!connected && listening) {
|
||||
server.end();
|
||||
MDNS.end();
|
||||
listening = false;
|
||||
}
|
||||
if (listening && phase_ == Phase::Idle) {
|
||||
NetworkClient client = server.accept();
|
||||
if (client) {
|
||||
client.setNoDelay(true);
|
||||
NetSource src(client);
|
||||
install(src, "Wi-Fi");
|
||||
delay(50);
|
||||
client.stop();
|
||||
}
|
||||
}
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace roro
|
||||
Reference in New Issue
Block a user