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:
agessaman committed 2026-10-03 15:47:01 -07:00
commit ebd562e2c8
8 files changed
+162 -8

No files matched your search

+59 -4
View File
@@ -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;
+54
View File
@@ -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;
}