mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 13:22:22 +00:00
Add one-way OTA target migration aliases
This commit is contained in:
1 parent
86e8c413cd
commit
5fd9d039f5
7 files changed
+102
-4
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user