show recovery app in Version page

This commit is contained in:
proddy
2026-10-11 16:23:08 +02:00
parent 9934d05c5a
commit 956f7ab098
28 changed files with 761 additions and 272 deletions
+87 -26
View File
@@ -28,6 +28,7 @@
#include <esp_partition.h>
#include <esp_random.h>
#include <algorithm>
#include <vector>
#include "WWWData.h" // generated by `pnpm build-recovery` in ./interface
@@ -37,18 +38,18 @@
#define FACTORY_AP_CHANNEL 1
#endif
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024;
static constexpr uint32_t RESTART_DELAY_MS = 500;
static constexpr uint32_t ETH_WAIT_MS = 15000; // how long Ethernet gets to obtain an address before WiFi is tried
static constexpr size_t MIN_FIRMWARE_SIZE = 512 * 1024; // smallest .bin accepted by the upload, anything smaller is rejected
static constexpr uint32_t RESTART_DELAY_MS = 500; // delay before restarting, so the HTTP response is sent first
static constexpr uint32_t ETH_WAIT_MS = 15000; // how long Ethernet gets to obtain an address before WiFi is tried
static constexpr char NVS_NAMESPACE[] = "ems-esp";
static constexpr char NVS_FRESH_FW[] = "fresh_firmware"; // EMSESP_NVS_BOOT_NEW_FIRMWARE in emsesp.h
static constexpr char NVS_REASON[] = "rec_reason"; // EMSESP_NVS_RECOVERY_REASON in emsesp.h
#define LOG(...) \
do { \
Serial.print("[recovery] "); \
Serial.printf(__VA_ARGS__); \
Serial.println(); \
#define LOG(...) \
do { \
Serial.print("[recovery] "); \
Serial.printf(__VA_ARGS__); \
Serial.println(); \
} while (0)
struct AdminUser {
@@ -102,7 +103,7 @@ static std::vector<PartitionInfo> partitions;
static uint32_t restart_at = 0;
static String session_token;
static String ap_ssid;
static String recovery_reason; // set by EMS-ESP when it switched to the recovery
static String recovery_reason; // set by EMS-ESP when it switched to the recovery
static const char * eth_state = ""; // "", "disabled", "failed" or "started"
static uint32_t wifi_join_at = 0; // when to try WiFi if Ethernet has no address, 0 if not waiting
static bool wifi_started = false;
@@ -113,6 +114,8 @@ static bool ethConnected();
static int upload_error = 0;
static bool upload_done = false;
static String upload_target;
static size_t upload_size = 0; // content length, for the progress
static size_t upload_step = 0; // last progress step logged, in tens of percent
// ----------------------------------------------------------------------------
// settings (read-only)
@@ -256,6 +259,7 @@ static void loadRecoveryReason() {
if (nvs.isKey(NVS_REASON)) {
recovery_reason = nvs.getString(NVS_REASON);
nvs.remove(NVS_REASON);
LOG("EMS-ESP switched to the recovery: %s", recovery_reason.c_str());
}
nvs.end();
}
@@ -272,8 +276,8 @@ static void loadPartitions() {
const esp_partition_t * part = esp_partition_get(it);
PartitionInfo info;
info.label = part->label;
info.factory = part->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY;
info.label = part->label;
info.factory = part->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY;
info.address = part->address;
info.size = part->size;
info.image_size = 0;
@@ -293,6 +297,10 @@ static void loadPartitions() {
if (part == running) {
info.version = RECOVERY_VERSION;
// EMS-ESP stores the install date when it installs the recovery. Not set if it was flashed over serial
if (has_nvs) {
info.install_date = nvs.getULong(("d_" + info.label).c_str(), 0);
}
} else if (info.valid) {
// EMS-ESP stores the version and install date of each partition in NVS, keyed by the label
if (has_nvs && nvs.isKey(part->label)) {
@@ -305,6 +313,17 @@ static void loadPartitions() {
}
}
LOG("Partition %s: %s%s, %uKB at 0x%x%s%s%s%s",
info.label.c_str(),
info.factory ? "factory, " : "",
info.valid ? "valid" : "empty",
static_cast<unsigned>(info.size / 1024),
static_cast<unsigned>(info.address),
info.version.isEmpty() ? "" : ", v",
info.version.c_str(),
info.ota_state.isEmpty() ? "" : ", ota state ",
info.ota_state.c_str());
partitions.push_back(info);
}
esp_partition_iterator_release(it);
@@ -353,6 +372,7 @@ static const esp_partition_t * uploadTarget(AsyncWebServerRequest * request) {
static void markFreshFirmware(const String & label) {
Preferences nvs;
if (!openNvs(nvs, false)) {
LOG("Cannot open NVS, EMS-ESP won't know it's a new firmware");
return;
}
nvs.putBool(NVS_FRESH_FW, true);
@@ -381,10 +401,11 @@ static String generateToken() {
}
static bool isAuthorized(AsyncWebServerRequest * request) {
if (session_token.isEmpty() || !request->hasHeader("Authorization")) {
return false;
const bool ok = !session_token.isEmpty() && request->hasHeader("Authorization") && request->header("Authorization") == "Bearer " + session_token;
if (!ok) {
LOG("Unauthorized request to %s", request->url().c_str());
}
return request->header("Authorization") == "Bearer " + session_token;
return ok;
}
// ----------------------------------------------------------------------------
@@ -529,6 +550,7 @@ static void handleSetBoot(AsyncWebServerRequest * request, JsonVariant & json) {
const String label = json["partition"] | "";
const esp_partition_t * part = findAppPartition(label.c_str());
if (part == nullptr) {
LOG("Cannot boot from %s: no such partition", label.c_str());
request->send(404);
return;
}
@@ -569,6 +591,7 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
return;
}
LOG("Factory reset requested");
const bool ok = LittleFS.format(); // the only place the recovery writes to the filesystem
LOG("Factory reset %s", ok ? "done" : "failed");
if (!ok) {
@@ -580,12 +603,12 @@ static void handleFactoryReset(AsyncWebServerRequest * request) {
restart_at = millis() + RESTART_DELAY_MS;
}
static void uploadFail(AsyncWebServerRequest * request, int code) {
static void uploadFail(AsyncWebServerRequest * request, int code, const char * reason) {
if (upload_error) {
return;
}
upload_error = code;
LOG("Upload error %d", code);
LOG("Upload error %d: %s", code, reason);
request->send(code);
if (code == 406) {
request->client()->close();
@@ -596,10 +619,12 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
if (!index) {
upload_error = 0;
upload_done = false;
upload_size = 0;
upload_step = 0;
upload_target.clear();
if (!isAuthorized(request)) {
uploadFail(request, 403);
uploadFail(request, 403, "not signed in");
return;
}
@@ -611,41 +636,51 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
if (ota.setMd5(data, len)) {
LOG("MD5 digest received (%s)", ota.md5());
} else {
uploadFail(request, 406);
uploadFail(request, 406, "invalid MD5 digest");
}
return;
default:
// the recovery app only flashes firmware, it never touches the settings
ota.clearMd5();
LOG("Unsupported file %s (%u bytes)", filename.c_str(), static_cast<unsigned>(filesize));
uploadFail(request, 406);
uploadFail(request, 406, "unsupported file");
return;
}
if (!OtaUpdater::isCompatibleFirmware(data, len)) {
uploadFail(request, 503);
uploadFail(request, 503, "not a firmware image for this chip");
return;
}
if (OtaUpdater::isRecoveryFirmware(data, len)) {
uploadFail(request, 409);
uploadFail(request, 409, "the recovery firmware can't be installed from the recovery");
return;
}
const esp_partition_t * target = uploadTarget(request);
if (target == nullptr) {
uploadFail(request, 400);
if (request->hasParam("partition")) {
LOG("Invalid upload target %s", request->getParam("partition")->value().c_str());
}
uploadFail(request, 400, "no valid target partition");
return;
}
if (!ota.beginFirmware(filesize, target)) {
LOG("Cannot write to %s: %s", target->label, ota.errorString());
uploadFail(request, 507);
uploadFail(request, 507, "cannot start the update");
return;
}
upload_target = target->label;
LOG("Uploading %s (%uKB) to %s%s", filename.c_str(), static_cast<unsigned>(filesize / 1024), target->label, ota.md5Applied() ? ", MD5 check enabled" : "");
upload_size = filesize;
LOG("Uploading %s (%uKB) to %s (%s)%s",
filename.c_str(),
static_cast<unsigned>(filesize / 1024),
target->label,
request->hasParam("partition") ? "selected" : "default target",
ota.md5Applied() ? ", MD5 check enabled" : "");
request->onDisconnect([] {
if (!upload_done && !upload_target.isEmpty()) {
LOG("Upload aborted at %uKB, the client disconnected", static_cast<unsigned>(ota.progress() / 1024));
ota.abort();
}
});
@@ -658,17 +693,27 @@ static void handleUpload(AsyncWebServerRequest * request, const String & filenam
if (!ota.write(data, len)) {
LOG("Write failed at %u: %s", static_cast<unsigned>(ota.progress()), ota.errorString());
ota.abort();
uploadFail(request, 500);
uploadFail(request, 500, "write failed");
return;
}
// log the progress every 10%, the content length is a little more than the image so it may stop just short of 100%
if (upload_size > 0) {
const size_t step = std::min<size_t>(ota.progress() * 10 / upload_size, 10);
if (step > upload_step) {
upload_step = step;
LOG("Uploading to %s, %u%%", upload_target.c_str(), static_cast<unsigned>(step * 10));
}
}
if (final) {
// on success this also makes the new partition the boot partition
if (!ota.end()) {
LOG("Upload failed: %s", ota.errorString());
uploadFail(request, 500);
uploadFail(request, 500, "image check failed");
return;
}
LOG("Upload finished, the image is verified");
upload_done = true;
}
}
@@ -785,6 +830,17 @@ static bool ethConnected() {
}
static void startNetwork() {
// the reason is logged once per change, since a missing network disconnects again every few seconds
WiFi.onEvent(
[](arduino_event_id_t, arduino_event_info_t info) {
static uint8_t last_reason = 0;
if (info.wifi_sta_disconnected.reason != last_reason) {
last_reason = info.wifi_sta_disconnected.reason;
LOG("WiFi disconnected, reason %d", last_reason);
}
},
ARDUINO_EVENT_WIFI_STA_DISCONNECTED);
WiFi.persistent(false);
WiFi.setHostname(settings.hostname.c_str()); // also the default for the Ethernet interface
WiFi.mode(WIFI_AP);
@@ -826,6 +882,9 @@ void setup() {
Serial.begin(115200);
LOG("EMS-ESP Recovery v%s", RECOVERY_VERSION);
const esp_partition_t * boot = esp_ota_get_boot_partition();
LOG("Reset reason: %s, running %s, next boot %s", resetReasonName(esp_reset_reason()), esp_ota_get_running_partition()->label, boot ? boot->label : "unknown");
loadSettings();
loadRecoveryReason();
loadPartitions();
@@ -841,6 +900,8 @@ void loop() {
was_connected = WiFi.isConnected();
if (was_connected) {
LOG("WiFi connected, IP %s", WiFi.localIP().toString().c_str());
} else {
LOG("WiFi connection lost");
}
}