The host-side build/verify/inspect/serve CLI now lives as a standalone Rust
project at https://github.com/vk496/motatool. Remove the in-tree C++ copy
(tools/motatool/) and repoint the docs (ota_protocol.md, ota_user_guide.md,
tools/mota/README.md) at the standalone repo.
No cross-dependency either direction: MeshCore's firmware build never invoked
motatool (only tools/mota/ Python glue runs in the build), and motatool depends
only on the shared .mota wire spec in docs/ota_protocol.md. tools/mota/
(pio_endf.py EndF hook, motalib.py reference lib, gen_targets.py) is unchanged.
Integrates Mike Carper's transmission-reliability work (direct retries with
SNR-adaptive backoff and CR, flood retry controls, alt-path replies, flood
channel gates) plus its upstream/dev base (as of 2026-07-06) on top of the
observer stack. The flex branch is untouched; this branch is the experimental
integration line.
Conflict-resolution decisions, for future re-syncs (git rerere is enabled and
has recorded these):
- NodePrefs adopts keymind/upstream member order + retry/flood tail. Member
order is in-memory only: /com_prefs stays field-by-field with the same
canonical file order both sides already share through offset 294; keymind
appends the retry tail at 295+ (new canonical size 676). Flex fleet files
load unchanged; retry fields default via direct_retry_prefs_magic.
- loadPrefsInt keeps the fork's legacy MQTT-gap recovery but moves its
detection boundary from 'extra > 5' to 'extra > LEGACY_MQTT_GAP_3SLOT (864)'
so keymind-size tails take the normal read path. COM_PREFS_TAIL_BYTES
retired.
- Dispatcher/RadioLibWrappers: fork watchdog additions + keymind TX overrides
and CAD busy counter are unioned; RxReservePacketManager::queueOutbound
follows upstream's new bool return (false when shedding).
- CommonCLI: fork observer dispatch + versioned /mqtt_prefs machinery kept;
keymind CLI (retry/flood/radioat) taken; duplicate CAD/FEM handlers and
sanitise lines deduped (kept keymind placements to minimize future diff).
- MyMesh (repeater/room): fork alerter/OTA/observer wiring kept; keymind
scheduled-radio system replaces the old pending_* temp-radio members;
applySavedRadioParams() replaces raw setParams at startup.
- ESP32Board: fork manifest-OTA + keymind powerOff/deepSleep and stopOTAUpdate
unioned; startOTAUpdate keeps fork's STA-aware IP with keymind's idempotency
guard.
- build.sh taken wholesale from keymind (fleet builds use GH Actions).
Verified: Heltec_v3_repeater_observer_mqtt, Heltec_v3_repeater,
heltec_v4_repeater_observer_mqtt, Heltec_v3_room_server_observer_mqtt build;
host migration harness (13 MQTT + non-MQTT scenarios) passes with updated
expectations (676-byte /com_prefs; odd-size files normalize on save, not
load); native unit tests 13/13.
Spell out how the warm-start seed is actually applied (a user asked): the seed is the --seed <file> given to `motatool serve`, NOT a file dropped into the --dir destination (which starts empty). motatool stamps that one seed into the fresh .part on every `… folder validate` begin, so it's always the named file — no guessing. `validate` is the switch (a plain folder pull fetches from 0); a re-pull re-begins fresh (never resumes a stale partial); a mismatched/absent seed just falls back to fetching those blocks over the radio (correct result, only slower).
Add an admin-only `ota stats` reply: one dense line with the running firmware's merkle content-id (mid) AND its EndF body_hash (only body_hash was surfaced before), version, served-set count + digest, live fetch state/progress, and policy — snprintf-bounded to the 160-byte reply. The remote CLI path is already admin-gated, so it's admin-only over the mesh (send it from the app's repeater command screen, or the WiFi/serial OTA console). A new servedDigest() accessor exposes the beacon set-digest.
HW-verified on RAK4631: reports the fw identity + live fetch state (incl. during a warm-start capture).
Protocol: OTA_REQ and OTA_GET_MANIFEST now carry a want_mask bitmap, so a fetcher asks for specific fragments (all on the first request, only the still-missing holes on a retry) instead of a whole block/manifest window. The WANT_MANIFEST and FETCHING retry loops re-ask only on a no-progress tick, so a lost fragment costs one fragment to recover and a re-request can't collide with an in-flight multi-fragment burst on half-duplex radios.
Warm-start (motatool folder-capture only): new OTA_GET_LEAVES/OTA_LEAVES let `ota pull <#> folder validate` bulk-fetch the target's merkle leaves, authenticate them against the manifest root, diff a similar seed build already staged in the destination, and pull DATA over LoRa only for the blocks that differ. Leaves are bitmap-fragmented + no-progress retry-gated like the manifest, capped at OTA_DIFF_MAX_BLOCKS so the want_mask stays a fixed uint16, and the diff runs a bounded batch per loop tick so it never starves the mesh loop. motatool `serve --seed <build.mota>` injects the seed payload into the destination .part on OP_BEGIN.
Document `ota pull <#> <dest>` (flash|folder, destination mandatory), the folder
pull that captures a device's exact firmware to the host as <mid>.mota (for
delta-building), the paused/resume-on-reconnect behaviour, and that a `motatool
serve` link doubles as the pull-to-folder store. (protocol §10, user guide,
motatool README)
Bound OTA-over-LoRa duty cycle across repeaters with one runtime-tunable,
persisted limit (OtaManager::max_hops, `ota config hops <0..8>`, default 3):
- Accept-gate: a node ignores OTA that arrived from more than max_hops hops
away (neither processes nor relays it). 0 = direct only.
- Forward-cap: relay a flood only while still under max_hops, appending this
node's path-hash (hop count increments like the mesh flood routing).
- RAM guard: relay an OTA flood only while more than OTA_FWD_MIN_FREE packet-
pool slots stay free, so heavy OTA (best-effort, lowest-priority) can never
monopolise the shared pool and starve real traffic — a dropped relay is
re-requested by the source.
Persisted in NodePrefs (CommonCLI) and shown in `ota config`. Docs updated.
The discovery beacon previously re-announced at a random 3-10 min interval.
Replace that with a fixed, user-configurable cadence:
- OtaManager::advert_mins() — re-advertise every N minutes after the boot
burst; 0 disables periodic re-advertise (boot burst only). Default 24h.
- Persisted in NodePrefs (CommonCLI) and runtime-tunable: `ota config advert
<minutes>` (0..10080; 0 = off), and shown in `ota config`.
- When periodic advert is disabled, the scheduler still re-checks the config
on a slow timer, so a later `ota config advert <mins>` takes effect live.
Also advertise immediately whenever the served set changes — when a motatool
folder is attached to / detached from the ESP32 WiFi seeder — so peers learn
about newly-available firmware without waiting for the next interval (the
`ota folder` serial path already announced on attach).
Docs: protocol beacon-cadence note + user-guide `ota config advert`.
Extends the USB-serial folder relay to WiFi so an ESP32 companion can both
serve .mota and be operated headlessly:
- motatool `serve --tcp <host[:port]>`: a TcpTransport sibling of the serial
transport (default port 5001). SeederCore/Folder are reused unchanged — the
COUNT/DESCRIBE/READ protocol is transport-agnostic.
- ESP32 companion: a dedicated OTA seeder port (5001) for `serve --tcp`, plus
an OTA text console on 5002 (`nc <ip> 5002` -> `ota status|ls|announce|...`,
the same handle_ota_command CLI serial nodes have). Both run alongside the
phone-app port (5000); all three coexist.
- WiFi.setSleep(false): ESP32 STA mode's modem power-save periodically sleeps
the modem/CPU and stalls the SX1262 SPI+DIO servicing, leaving LoRa deaf
while WiFi is associated. Disabling it restores the radio (HW-validated:
a V3 WiFi companion is then discovered over LoRa and discovers its peers).
- docs: serving .mota over WiFi (protocol §10.2 + user guide).
Implemented new commands for configuring and diagnosing NTP server
settings in the MQTT bridge. Users can now set a custom NTP server
and probe connectivity to configured servers. This enhancement
improves time synchronization reliability for JWT authentication
and provides better diagnostics for NTP connectivity issues.