Merge branch 'feat/ota-branch-switch' into merge/ota-branch-into-observer-firmware-dev
# Conflicts: # examples/simple_repeater/MyMesh.cpp
This commit is contained in:
commit
ebd562e2c8
8 files changed
+162
-8
No files matched your search
@@ -186,6 +186,9 @@ void ESP32Board::maintainOTAUpdate(uint32_t now_ms) {
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
#include <esp_partition.h>
|
||||
#include <esp_ota_ops.h>
|
||||
#include <esp_image_format.h>
|
||||
#include "OtaChannel.h"
|
||||
|
||||
// Embedded CA bundle (produced by board_build.embed_files). Weak so non-bundle
|
||||
// builds still link; we check for presence at runtime.
|
||||
@@ -231,6 +234,31 @@ static void ota_parseVersion(const char* ver, char* base_out, size_t base_sz, in
|
||||
}
|
||||
}
|
||||
|
||||
// Read the compat tag of the image just written to `part`, bounded by the image's own
|
||||
// length: bytes past it can be a stale, tagged image from an earlier flash.
|
||||
static bool ota_readImageCompat(const esp_partition_t* part, OtaCompat* out) {
|
||||
const esp_partition_pos_t pos = { part->address, part->size };
|
||||
esp_image_metadata_t meta;
|
||||
if (esp_image_verify(ESP_IMAGE_VERIFY_SILENT, &pos, &meta) != ESP_OK) return false;
|
||||
static const size_t kChunk = 2048, kTail = 64; // tail > tag + value, so a split tag is seen whole
|
||||
static uint8_t buf[kTail + kChunk];
|
||||
size_t have = 0;
|
||||
for (uint32_t off = 0; off < meta.image_len;) {
|
||||
size_t n = meta.image_len - off;
|
||||
if (n > kChunk) n = kChunk;
|
||||
if (esp_partition_read(part, off, buf + have, n) != ESP_OK) return false;
|
||||
have += n;
|
||||
off += n;
|
||||
const char* v = ota_compat_find(buf, have);
|
||||
if (v) return ota_compat_parse(v, out);
|
||||
if (have > kTail) {
|
||||
memmove(buf, buf + have - kTail, kTail);
|
||||
have = kTail;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Canonical signature of the FLASHED partition table — MUST match
|
||||
// scripts/partition_signature.py: each entry "type:subtype:offset:size" in
|
||||
// lowercase hex, sorted by offset, joined by ','. Lets `ota update` compare the
|
||||
@@ -447,13 +475,14 @@ bool ESP32Board::otaFromManifestImpl(const char* manifest_base, const char* curr
|
||||
bool same_base = (own_base[0] && avail_base[0] && strcmp(own_base, avail_base) == 0);
|
||||
bool have_builds = (own_build >= 0 && avail_build >= 0);
|
||||
bool diff_base = (own_base[0] && avail_base[0] && !same_base);
|
||||
// Build counters are per channel, so build numbers only compare within the native channel.
|
||||
// Another channel's image always differs (its native base does), even from the same
|
||||
// commit; build counters are per channel, so build numbers only compare natively.
|
||||
bool cross_channel = (strcmp(manifest_base, OTA_MANIFEST_BASE) != 0);
|
||||
|
||||
int behind = 0;
|
||||
bool up_to_date;
|
||||
if (cross_channel) {
|
||||
up_to_date = hash_equal;
|
||||
up_to_date = false;
|
||||
} else if (same_base && have_builds) {
|
||||
behind = avail_build - own_build;
|
||||
up_to_date = (behind <= 0);
|
||||
@@ -524,9 +553,35 @@ bool ESP32Board::otaFromManifestImpl(const char* manifest_base, const char* curr
|
||||
int d = (int)((int64_t)cur * 10 / total);
|
||||
if (d != ota_progress_decile) { ota_progress_decile = d; Serial.printf("OTA: %d%%\n", d * 10); }
|
||||
});
|
||||
httpUpdate.onEnd([]() { Serial.println("OTA: write complete, rebooting..."); });
|
||||
httpUpdate.rebootOnUpdate(true); // reboots into the new image on success
|
||||
httpUpdate.onEnd([]() { Serial.println("OTA: write complete"); });
|
||||
// A channel switch is checked before it may boot (below); native updates reboot here.
|
||||
httpUpdate.rebootOnUpdate(!cross_channel);
|
||||
t_httpUpdate_return ret = httpUpdate.update(uclient, file_url);
|
||||
if (ret == HTTP_UPDATE_OK && cross_channel) {
|
||||
const esp_partition_t* running = esp_ota_get_running_partition();
|
||||
const esp_partition_t* target = esp_ota_get_boot_partition();
|
||||
OtaCompat own = {0, 0}, img = {0, 0};
|
||||
const char* volatile own_tag = ota_compat_tag; // volatile: the tag must stay in the image
|
||||
ota_compat_parse(own_tag + sizeof(OTA_COMPAT_TAG) - 1, &own);
|
||||
bool tagged = target && target != running && ota_readImageCompat(target, &img);
|
||||
if (tagged && ota_compat_ok(own, img)) {
|
||||
Serial.println("OTA: channel switch compatible, rebooting...");
|
||||
delay(100);
|
||||
ESP.restart();
|
||||
}
|
||||
// Point boot back at the running image so the refused build never starts.
|
||||
bool reverted = (esp_ota_set_boot_partition(running) == ESP_OK);
|
||||
inhibit_sleep = false;
|
||||
if (!tagged) {
|
||||
snprintf(reply, 160, "ERR: channel switch refused: %s has no compat tag; cable flash%s",
|
||||
avail_disp, reverted ? "" : " [boot revert FAILED]");
|
||||
} else {
|
||||
snprintf(reply, 160, "ERR: channel switch refused: target compat %d/%x < this node %d/%x; cable flash%s",
|
||||
img.gen, img.caps, own.gen, own.caps, reverted ? "" : " [boot revert FAILED]");
|
||||
}
|
||||
Serial.print("OTA: "); Serial.println(reply);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only reached on failure (success reboots inside update()).
|
||||
inhibit_sleep = false;
|
||||
|
||||
@@ -76,3 +76,57 @@ static inline bool ota_parse_channel(const char* arg, uint8_t* out) {
|
||||
if (strcmp(arg, "default") == 0) { *out = OTA_CH_NATIVE; return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compatibility tag, read from a downloaded image before a channel switch boots it.
|
||||
// A switch can land on an older build, which would boot without state it cannot read
|
||||
// or without a transport this node depends on. Bump OTA_STATE_GEN when a build starts
|
||||
// storing state that older builds cannot read.
|
||||
// 1: /prefs.json + /mqtt_prefs
|
||||
#ifndef OTA_STATE_GEN
|
||||
#define OTA_STATE_GEN 1
|
||||
#endif
|
||||
#define OTA_CAP_ETH 0x01 // carries MQTT over Ethernet
|
||||
#if defined(NETWORK_PREFER_ETHERNET)
|
||||
#define OTA_CAPS_STR "+eth"
|
||||
#else
|
||||
#define OTA_CAPS_STR ""
|
||||
#endif
|
||||
#define OTA_STR_(x) #x
|
||||
#define OTA_STR(x) OTA_STR_(x)
|
||||
#define OTA_COMPAT_TAG "ota-compat:"
|
||||
static const char ota_compat_tag[] = OTA_COMPAT_TAG OTA_STR(OTA_STATE_GEN) OTA_CAPS_STR;
|
||||
|
||||
struct OtaCompat {
|
||||
int gen;
|
||||
uint8_t caps;
|
||||
};
|
||||
|
||||
// Parse the tag value "<gen>[+cap...]"; unknown caps are ignored.
|
||||
static inline bool ota_compat_parse(const char* s, OtaCompat* out) {
|
||||
if (*s < '0' || *s > '9') return false;
|
||||
out->gen = 0;
|
||||
out->caps = 0;
|
||||
while (*s >= '0' && *s <= '9') out->gen = out->gen * 10 + (*s++ - '0');
|
||||
while (*s == '+') {
|
||||
const char* cap = ++s;
|
||||
while (*s && *s != '+') s++;
|
||||
if ((size_t)(s - cap) == 3 && memcmp(cap, "eth", 3) == 0) out->caps |= OTA_CAP_ETH;
|
||||
}
|
||||
return *s == 0;
|
||||
}
|
||||
|
||||
// Find the tag in an image chunk; returns the NUL-terminated value text, or nullptr when
|
||||
// the chunk holds no complete tag.
|
||||
static inline const char* ota_compat_find(const uint8_t* buf, size_t len) {
|
||||
const size_t tag_len = sizeof(OTA_COMPAT_TAG) - 1;
|
||||
for (size_t i = 0; i + tag_len < len; i++) {
|
||||
if (memcmp(buf + i, OTA_COMPAT_TAG, tag_len) != 0) continue;
|
||||
if (memchr(buf + i + tag_len, 0, len - i - tag_len)) return (const char*)buf + i + tag_len;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// A target must read this node's state and keep every transport this node has.
|
||||
static inline bool ota_compat_ok(const OtaCompat& own, const OtaCompat& target) {
|
||||
return target.gen >= own.gen && (target.caps & own.caps) == own.caps;
|
||||
}
|
||||
Reference in new issue
Block a user