Add one-way OTA target migration aliases

This commit is contained in:
mikecarper committed 2026-09-19 08:30:52 -07:00
1 parent fe9d48ea69
commit e48792e178
7 files changed
+102 -4

No files matched your search

+42 -1
View File
@@ -2411,6 +2411,33 @@ get_exact_identity_full_pio_env() {
fi
}
# A legacy observer FULL image can make one automatic OTA hop to the ordinary
# target whose exact-identity FULL image now replaces it. The old image keeps
# its own target ID until that hop, so deployed nodes remain discoverable and
# manual pulls stay target-specific. Only same-partition policy targets get an
# alias; partition-changing targets must continue through their existing path.
get_exact_identity_full_migration_target() {
local env_name=$1
local successor=""
case "${env_name,,}" in
*_observer_mqtt)
successor=${env_name%_observer_mqtt}
;;
*_observer_mqtt_)
successor=${env_name%_observer_mqtt_}
;;
*)
return 1
;;
esac
is_esp32_full_only_bulk_target "$env_name" || return 1
is_esp32_full_only_bulk_target "$successor" || return 1
[ "$(get_exact_identity_full_pio_env "$successor")" = "$env_name" ] || return 1
printf '%s\n' "$successor"
}
apply_esp32_full_shared_bridge_profile() {
local env_name=$1
@@ -4262,6 +4289,8 @@ build_firmware() {
local firmware_filename
local mota_target_id
local mota_target_flag=""
local mota_migration_target=""
local mota_migration_flag=""
local original_platformio_build_flags
local original_platformio_build_unflags
local original_platformio_build_src_filter
@@ -4352,6 +4381,18 @@ build_firmware() {
|| is_companion_radio_full_target "$env_name"; then
BUILD_PROFILE_FOR_TARGET="full"
fi
if [ "$ESP32_FULL_BUILD" = "1" ]; then
mota_migration_target=$(get_exact_identity_full_migration_target "$env_name") || mota_migration_target=""
if [ -n "$mota_migration_target" ]; then
local mota_migration_target_id
mota_migration_target_id=$(python3 -c "import hashlib,sys;print('0x%08x'%int.from_bytes(hashlib.sha256(sys.argv[1].encode()).digest()[:4],'little'))" "$mota_migration_target" 2>/dev/null || echo "")
if [ -n "$mota_migration_target_id" ]; then
mota_migration_flag=" -DMOTA_MIGRATION_TARGET_ID=${mota_migration_target_id}"
echo "Legacy FULL ${env_name} will accept successor mOTA target ${mota_migration_target}."
fi
fi
fi
configure_unified_full_infrastructure_output "$pio_env_name"
echo "Effective feature profile for ${env_name}: ${BUILD_PROFILE_FOR_TARGET}"
@@ -4431,7 +4472,7 @@ build_firmware() {
original_platformio_extra_scripts=""
fi
export PLATFORMIO_BUILD_FLAGS="${original_platformio_build_flags} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_BUILD_EPOCH=${firmware_build_epoch} -DFIRMWARE_VERSION='\"${embedded_version_string}\"' -DOTA_VARIANT='\"${env_name}\"'${mota_target_flag}"
export PLATFORMIO_BUILD_FLAGS="${original_platformio_build_flags} -DFIRMWARE_BUILD_DATE='\"${firmware_build_date}\"' -DFIRMWARE_BUILD_EPOCH=${firmware_build_epoch} -DFIRMWARE_VERSION='\"${embedded_version_string}\"' -DOTA_VARIANT='\"${env_name}\"'${mota_target_flag}${mota_migration_flag}"
disable_debug_flags "$env_name"
apply_debug_overrides "$env_name"
apply_mqtt_bridge_override "$env_name"
+6 -1
View File
@@ -712,6 +712,11 @@ transmission per hop.
- **`target_id` vs `hw_id`** - complementary, not redundant: `target_id` is the fetch-routing key
(hw + role + partition); `hw_id` is the human-readable brick-safety key (hardware only). Same board, two
roles => same `hw_id`, different `target_id`.
- **One-way target migration:** a release may compile `MOTA_MIGRATION_TARGET_ID` into a legacy image. While
that image is running, automatic fetch accepts only the declared successor target; manual pulls remain
unchanged. The staged manifest must still match the requested successor target, the signed `hw_id`, image
hash, codec, and partition/layout checks. After reboot the successor's EndF becomes the node's normal
identity, so later updates use the canonical target. The migration alias is not used for bootloader packages.
- **Naming a `target_id` locally:** only the 4-byte `target_id` ever travels on the wire. To show *which*
board/role a target is, a node (and `motatool`) reverse-looks-it-up in `src/helpers/ota/OtaTargets.h` -
a generated `target_id -> env-name` table covering every `ENABLE_OTA` env (`tools/mota/gen_targets.py`,
@@ -736,7 +741,7 @@ transmission per hop.
the app mints an authenticated one-reset media authorization for OTAFIX.
**Transfer needs no trust** - blocks are content-addressed against the manifest's merkle root.
- **Policies (persisted):** `autofetch` in {off, any, signed} (default off) gates automatic block fetching of
own-target adverts; `autoinstall` in {off, trusted} (default off) gates auto-apply of a COMPLETE signed +
own-target (or an explicitly compiled one-way successor) adverts; `autoinstall` in {off, trusted} (default off) gates auto-apply of a COMPLETE signed +
allowlisted fetch. Conservative defaults: a fresh node discovers + announces but never fetches/installs
without operator intent.
- **Supersession:** a newer version announced mid-download does not abort the in-progress transfer
+5
View File
@@ -647,6 +647,11 @@ struct OtaContext {
target_id = 0;
#endif
manager.begin(target_id, send, ctx);
#if defined(MOTA_MIGRATION_TARGET_ID)
// Legacy observer images keep their deployed target identity for serving
// and diagnostics, but automatically accept one canonical successor image.
manager.set_auto_migration_target((uint32_t)(MOTA_MIGRATION_TARGET_ID));
#endif
// Every OTA-capable application can receive ordinary raw RFC1951 transport blocks. OtaManager advertises
// that permission only because this exact, bounded decoder is registered; non-OTA builds do not compile
// OtaContext or link the decoder implementation.
+6 -2
View File
@@ -81,6 +81,7 @@ uint8_t* OtaManager::ensureScratch() {
void OtaManager::begin(uint32_t my_target_id, OtaSend send, void* ctx) {
_target = my_target_id; _send = send; _ctx = ctx;
_migration_target = 0;
_fstate = IDLE; _have = 0; _fbc = 0;
_paused_from = IDLE;
_fetch_error = FETCH_ERROR_NONE;
@@ -1099,7 +1100,8 @@ bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint32_t fw_versio
return memcmp(mid, _desired_mid, 4) == 0 && (_desired_target == 0 || target == _desired_target);
if (_desired_target) return target == _desired_target; // cross-target want (role switch)
if (_autofetch == AUTOFETCH_OFF) return false; // discover only
if (target != _target) return false; // auto-fetch = our own target
const uint32_t auto_target = _migration_target ? _migration_target : _target;
if (target != auto_target) return false; // own target or one-way successor
if (_autofetch == AUTOFETCH_SIGNED && !(flags & MFLAG_SIGNED)) return false; // signed-only policy
if (_enforce_auto_version &&
!ota_trusted_auto_version_allows(_running_fw_version, fw_version)) return false;
@@ -1608,7 +1610,9 @@ bool OtaManager::resumeStaged(const uint8_t* want_mid) {
return false;
}
const bool automatic_resume = want_mid == nullptr;
const uint32_t expected_target = automatic_resume ? _target : _fexpected_target;
const uint32_t expected_target = automatic_resume
? (_migration_target ? _migration_target : _target)
: _fexpected_target;
if (!_fetch->reopenFor(want_mid, expected_target)) return false;
uint32_t total = _fetch->staged_size();
uint8_t hdr[8];
+9
View File
@@ -455,6 +455,14 @@ public:
static const uint8_t AUTOFETCH_OFF = 0, AUTOFETCH_ANY = 1, AUTOFETCH_SIGNED = 2;
void set_autofetch(uint8_t p) { _autofetch = p; reDiscover(); }
uint8_t autofetch() const { return _autofetch; }
// One-way release migration for a legacy identity. Automatic OTA follows the
// successor target while the running firmware continues to report its own
// target until that successor is installed. Manual pulls are unaffected.
void set_auto_migration_target(uint32_t target_id) {
_migration_target = target_id != _target ? target_id : 0;
reDiscover();
}
uint32_t auto_migration_target() const { return _migration_target; }
void set_auto_version_floor(uint32_t running_version, bool enforce = true) {
_running_fw_version = running_version;
_enforce_auto_version = enforce;
@@ -728,6 +736,7 @@ private:
MerkleAccumulator _resume_merkle;
uint32_t _req_start = 0, _req_count = 0; // most recently requested block + active flight size
uint32_t _desired_target = 0; // manual cross-target override (0 = auto / own target)
uint32_t _migration_target = 0; // one-way automatic successor target for legacy releases
uint8_t _desired_mid[4] = {0,0,0,0}; // pull a specific manifest_id (see want_mid)
bool _have_desired_mid = false;
uint8_t _apply_codec = CODEC_DETOOLS_SEQUENTIAL; // platform's delta codec (OtaContext sets it)
@@ -71,6 +71,15 @@ done
[ "$(get_exact_identity_full_pio_env heltec_v4_r8_sensor)" \
= heltec_v4_r8_sensor ] \
|| fail "V4 R8 sensor should retain its own feature base"
[ "$(get_exact_identity_full_migration_target heltec_v4_r8_repeater_observer_mqtt)" \
= heltec_v4_r8_repeater ] \
|| fail "legacy V4 R8 observer lacks its canonical migration target"
if get_exact_identity_full_migration_target heltec_v4_r8_repeater >/dev/null; then
fail "ordinary V4 R8 target unexpectedly has a migration alias"
fi
if get_exact_identity_full_migration_target heltec_v4_r8_repeater_observer_mqtt_sim >/dev/null; then
fail "simulated observer target unexpectedly received a migration alias"
fi
# Portable images and exact reduced OTA identities must not be consolidated.
for target in heltec_v4_r8_repeater heltec_v4_r8_room_server \
+25
View File
@@ -2443,6 +2443,31 @@ TEST(OtaTransfer, TwoManagersFullTransfer) {
EXPECT_TRUE(mota_check_image_hash_full(m));
}
TEST(OtaTransfer, LegacyIdentityAutomaticallyFetchesItsSuccessor) {
g_q.clear();
OtaManager server, client;
OtaStoreRam<4096> store;
SendTo to_client{&client}, to_server{&server};
const uint32_t legacy_target = SIM_TARGET_ID ^ 0x13579BDFu;
server.begin(/*server's own target irrelevant for serving*/ 0, sim_send, &to_client);
client.begin(legacy_target, sim_send, &to_server);
client.set_auto_migration_target(SIM_TARGET_ID);
client.set_fetch_store(&store);
client.set_autofetch(OtaManager::AUTOFETCH_ANY);
ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN));
server.announce();
pump(client, &server);
ASSERT_EQ(client.fetchState(), OtaManager::COMPLETE);
MotaManifest migrated;
ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), migrated));
EXPECT_EQ(migrated.target_id, SIM_TARGET_ID);
EXPECT_EQ(client.target(), legacy_target); // identity changes only after reboot/apply
EXPECT_EQ(client.auto_migration_target(), SIM_TARGET_ID);
}
TEST(OtaTransfer, LegacyOneKilobyteV2CodecStillStagesOriginalContainer) {
g_q.clear();
MotaManifest manifest;