diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md index d4f867c8..cc5583fe 100644 --- a/docs/ota_protocol.md +++ b/docs/ota_protocol.md @@ -310,7 +310,10 @@ Message types: Because a node may serve **many** mOTAs (its own firmware plus an external folder — §10), discovery is split so the periodic beacon stays tiny regardless of catalog size: -**Tier 1 — `OTA_ADV` beacon** (10 bytes, constant, flooded periodically): +**Tier 1 — `OTA_ADV` beacon** (10 bytes, constant). Flooded as a short burst at boot, then every +`advert_mins` minutes (default 24h; runtime-tunable via `ota config advert`, `0` disables the periodic +re-advertise). It is also emitted immediately whenever the served set changes (e.g. a `motatool` folder is +attached/detached), so peers learn about newly-available firmware without waiting for the next interval: ``` seeder_id[4] advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md index bc0ba838..7d4434ee 100644 --- a/docs/ota_user_guide.md +++ b/docs/ota_user_guide.md @@ -129,6 +129,9 @@ ota config autofetch off # back to manual (default) ota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust ota config autoinstall off # never auto-install (default) +ota config advert 1440 # re-advertise this node every N minutes (default 1440 = 24h) +ota config advert 0 # disable periodic re-advertise (still advertises briefly at boot) + ota config # show the current settings ``` diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9a96b210..6844724b 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -130,6 +130,7 @@ void halt() { if (ota_seeder_client && ota_seeder_client.connected()) return; // still serving the current client if (ota_seeder_attached) { // previous client just disconnected mesh::ota::ota_ctx().detach_folder(); + mesh::ota::ota_ctx().manager.announce(); // served set shrank back to our own fw -> re-advertise ota_seeder_attached = false; WIFI_DEBUG_PRINTLN("OTA seeder: client disconnected, relay stopped"); } @@ -138,6 +139,7 @@ void halt() { ota_seeder_client = c; // rebind the persistent Stream to it if (mesh::ota::ota_ctx().manager.add_source(&ota_seeder_source)) { ota_seeder_attached = true; + mesh::ota::ota_ctx().manager.announce(); // new served set -> advertise the folder's fw to peers WIFI_DEBUG_PRINTLN("OTA seeder: client connected, relaying its folder"); } else { ota_seeder_client.stop(); // no free source slot diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 5423fa3d..167a314b 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -13,14 +13,12 @@ #ifndef OTA_ANNOUNCE_BURST_MS #define OTA_ANNOUNCE_BURST_MS 20000UL // spacing during the boot burst (~1 min total), then ... #endif -// ... then re-announce at a RANDOM interval in [MIN, MAX] so a long-running node stays discoverable -// (a fresh `ota ls` neighbour sees it within minutes, not at boot only) without all nodes beaconing in -// lockstep. The beacon is tiny + lowest-priority + duty-gated, so a few-minute cadence is cheap. -#ifndef OTA_ANNOUNCE_INTERVAL_MIN_MS -#define OTA_ANNOUNCE_INTERVAL_MIN_MS 180000UL // 3 min -#endif -#ifndef OTA_ANNOUNCE_INTERVAL_MAX_MS -#define OTA_ANNOUNCE_INTERVAL_MAX_MS 600000UL // 10 min +// ... then re-announce at a FIXED cadence so a long-running node stays discoverable (a fresh `ota ls` +// neighbour eventually sees it, not at boot only). The cadence is OtaManager::advert_mins() minutes (default +// 24h, runtime-tunable via `ota config advert` + persisted; 0 = disabled = boot burst only). The beacon is +// tiny + lowest-priority + duty-gated, so even a frequent cadence is cheap. +#ifndef OTA_ANNOUNCE_DISABLED_POLL_MS +#define OTA_ANNOUNCE_DISABLED_POLL_MS 600000UL // when periodic advert is off, re-check config every 10 min #endif #endif @@ -90,17 +88,21 @@ void Mesh::loop() { } if (millisHasNowPassed(_next_ota_announce)) { // auto-advertise so peers discover us (tiny beacon) ota::OtaContext& oc = ota::ota_ctx(); - // To be discoverable as a source of our OWN firmware, set up flash-backed self-serve once; then the - // beacon (announce) advertises our served set and peers can QUERY + fetch it. - if (!oc.serving) oc.serving = ota::ota_serve_self(oc, 0); - oc.manager.announce(); - // boot burst (a few closely-spaced adverts so a co-booting peer catches one), then a random - // few-minute cadence so a node stays discoverable long after boot (not just at boot). - uint32_t gap = (_ota_announce_count < OTA_ANNOUNCE_BURST) - ? OTA_ANNOUNCE_BURST_MS - : _rng->nextInt(OTA_ANNOUNCE_INTERVAL_MIN_MS, OTA_ANNOUNCE_INTERVAL_MAX_MS); + bool in_burst = _ota_announce_count < OTA_ANNOUNCE_BURST; + uint32_t mins = oc.manager.advert_mins(); // periodic cadence in minutes; 0 = disabled (boot burst only) + if (in_burst || mins != 0) { + // To be discoverable as a source of our OWN firmware, set up flash-backed self-serve once; then the + // beacon (announce) advertises our served set and peers can QUERY + fetch it. + if (!oc.serving) oc.serving = ota::ota_serve_self(oc, 0); + oc.manager.announce(); + if (_ota_announce_count < 250) _ota_announce_count++; + } + // Re-arm: tight spacing during the boot burst; afterwards the fixed cadence (default 24h). When periodic + // advert is disabled (0), re-check on a slow timer so a later `ota config advert ` takes effect live. + uint32_t gap = in_burst ? OTA_ANNOUNCE_BURST_MS + : (mins != 0) ? mins * 60000UL + : OTA_ANNOUNCE_DISABLED_POLL_MS; _next_ota_announce = futureMillis(gap); - if (_ota_announce_count < 250) _ota_announce_count++; } { // auto-install (once per COMPLETE fetch): only signed images, and apply_fetched enforces trust ota::OtaContext& oc = ota::ota_ctx(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index ca9d6264..163971bf 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -52,6 +52,7 @@ void CommonCLI::syncOtaConfigFromPrefs() { mesh::ota::OtaContext& c = mesh::ota::ota_ctx(); c.manager.set_autofetch(_prefs->ota_autofetch); c.manager.set_checkpoint_blocks(_prefs->ota_checkpoint_blocks); + c.manager.set_advert_mins(_prefs->ota_advert_interval); c.autoinstall = _prefs->ota_autoinstall; c.allow.clear(); for (uint8_t i = 0; i < _prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++) @@ -119,12 +120,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // defaults: a short file makes the reads below no-ops (read returns 0 bytes, values unchanged). _prefs->ota_autofetch = 0; _prefs->ota_autoinstall = 0; _prefs->ota_signer_count = 0; _prefs->ota_checkpoint_blocks = 4; // = OTA_CHECKPOINT_BLOCKS; older prefs lack it -> stays at default + _prefs->ota_advert_interval = 1440; // = OTA_ADVERT_INTERVAL_MINS (24h); older prefs lack it -> stays default file.read((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 295 file.read((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 296 file.read((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 297 file.read((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298 file.read((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426 - // next: 428 + file.read((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428 + // next: 430 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -159,6 +162,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->ota_autofetch = constrain(_prefs->ota_autofetch, 0, 2); _prefs->ota_autoinstall = constrain(_prefs->ota_autoinstall, 0, 1); if (_prefs->ota_checkpoint_blocks > 4096) _prefs->ota_checkpoint_blocks = 4; // 0=never; cap absurd + if (_prefs->ota_advert_interval > 10080) _prefs->ota_advert_interval = 1440; // 0=off; cap at 7 days if (_prefs->ota_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys file.close(); @@ -230,7 +234,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 297 file.write((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298 file.write((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426 - // next: 428 + file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428 + // next: 430 file.close(); } @@ -359,6 +364,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re mesh::ota::OtaContext& c = mesh::ota::ota_ctx(); _prefs->ota_autofetch = c.manager.autofetch(); _prefs->ota_checkpoint_blocks = c.manager.checkpoint_blocks(); + _prefs->ota_advert_interval = c.manager.advert_mins(); _prefs->ota_autoinstall = c.autoinstall; _prefs->ota_signer_count = c.allow.count(); for (uint8_t i = 0; i < c.allow.count() && i < MAX_OTA_SIGNERS; i++) diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8090f702..e0359fc1 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -71,6 +71,7 @@ struct NodePrefs { // persisted to file uint8_t ota_signer_count; // # of allowlisted signer pubkeys below uint8_t ota_signers[4][32]; // trusted Ed25519 signer pubkeys (== MAX_OTA_SIGNERS) uint16_t ota_checkpoint_blocks; // resume checkpoint cadence (blocks); 0=never. Default 4 (runtime-tunable) + uint16_t ota_advert_interval; // OTA beacon re-advertise cadence (mins); 0=off. Default 1440 (24h, tunable) }; class CommonCLICallbacks { diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index e35955b5..465f42ba 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -272,12 +272,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board if (n < 0 || n > 4096) { strcpy(reply, "ERR usage: ota config checkpoint <0..4096> (blocks; 0=never)"); return true; } c.manager.set_checkpoint_blocks((uint16_t)n); c.config_dirty = true; sprintf(reply, "OK checkpoint every %ld blocks (saved)%s", n, n == 0 ? " — periodic resume disabled" : ""); + } else if (strncmp(p, "advert ", 7) == 0) { // beacon re-advertise cadence (minutes; 0=disable) + long m = atol(p + 7); + if (m < 0 || m > 10080) { strcpy(reply, "ERR usage: ota config advert <0..10080> (minutes; 0=disable)"); return true; } + c.manager.set_advert_mins((uint16_t)m); c.config_dirty = true; + sprintf(reply, "OK re-advertise every %ld min (saved)%s", m, m == 0 ? " — periodic advert disabled" : ""); } else { // show current policy uint8_t af = c.manager.autofetch(); - sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u keys=%u (persisted)", + sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u advert=%umin keys=%u (persisted)", af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off", c.autoinstall == OtaContext::AUTOINSTALL_TRUSTED ? "trusted" : "off", - (unsigned)c.manager.checkpoint_blocks(), (unsigned)c.allow.count()); + (unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(), (unsigned)c.allow.count()); } // ---- trusted signer allowlist (security config; persisted): `ota key add|rm ` / `ota key` lists ---- diff --git a/src/helpers/ota/OtaManager.h b/src/helpers/ota/OtaManager.h index 3428b453..79f22ebb 100644 --- a/src/helpers/ota/OtaManager.h +++ b/src/helpers/ota/OtaManager.h @@ -37,6 +37,9 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) #ifndef OTA_CHECKPOINT_BLOCKS #define OTA_CHECKPOINT_BLOCKS 4 // persist progress (store.checkpoint) every N committed blocks (resume) #endif +#ifndef OTA_ADVERT_INTERVAL_MINS +#define OTA_ADVERT_INTERVAL_MINS 1440 // re-advertise our served set every N minutes after the boot burst (24h) +#endif #ifndef OTA_MF_FRAG #define OTA_MF_FRAG 176 // manifest bytes per OTA_MANIFEST fragment (<= MAX_PACKET_PAYLOAD - header) #endif @@ -204,6 +207,12 @@ public: // committed blocks. 0 = never (resume only from a finalized container). Default OTA_CHECKPOINT_BLOCKS. void set_checkpoint_blocks(uint16_t n) { _checkpoint_blocks = n; } uint16_t checkpoint_blocks() const { return _checkpoint_blocks; } + + // Beacon re-advertise cadence in MINUTES (runtime-tunable, persisted in NodePrefs): after the boot burst, + // re-send the discovery beacon every N minutes so a long-running node stays discoverable. 0 = disabled + // (boot burst only). Default OTA_ADVERT_INTERVAL_MINS (24h). + void set_advert_mins(uint16_t m) { _advert_mins = m; } + uint16_t advert_mins() const { return _advert_mins; } bool fetched_is_signed() const { return (_fflags & MFLAG_SIGNED) != 0; } // flags of the fetched manifest // This node's id (pubkey[0:4]), stamped into adverts we send so receivers can count distinct seeders. @@ -325,6 +334,7 @@ private: uint8_t _seeder_id[4] = {0,0,0,0}; // our node id (pubkey[0:4]) for advert seeder counting uint8_t _autofetch = AUTOFETCH_OFF; // auto-fetch policy (persisted in NodePrefs) uint16_t _checkpoint_blocks = OTA_CHECKPOINT_BLOCKS; // resume checkpoint cadence (persisted) + uint16_t _advert_mins = OTA_ADVERT_INTERVAL_MINS; // beacon re-advertise cadence, minutes; 0=off (persisted) uint8_t _fflags = 0; // flags of the manifest currently being fetched // multi-fragment reassembly of the current block (per-block 2-phase: fetch data, then its proof) uint32_t _reasm_block = NO_BLOCK; // block being reassembled / awaiting proof (none)