mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-10-28 11:14:58 +00:00
show recovery app in Version page
This commit is contained in:
+87
-26
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user