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).
The EndF post-build stamper read the version via _cppdef('FIRMWARE_VERSION'), which only sees -D build flags — but MeshCore authors FIRMWARE_VERSION as a header #define in each example, so the stamper found nothing and defaulted fw_version to 0. Every .mota / OTA advert then reported v0.0.0 while `ver` (which reads the header at runtime) showed the real version.
Read the header value as a fallback WITHOUT moving where MeshCore authors it: honor a -D override first (the header's #ifndef guard invites it, for release builds), else read the #define from the example this env builds (via build_src_filter), falling back to the repo-wide value when unambiguous. Purely additive to our own EndF tooling — no MeshCore source changed.
HW-verified: RAK4631 now reports `fw v1.17.0` in ota stats/ls (was v0.0.0), matching `ver`.
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.
Route both begin() and reopen() through a pure mota_nrf52_stage_plan() that bottom-aligns a received .mota below the filesystem region (trailer ending at FS_START, where the bootloader scans) and refuses any size that would overrun that ceiling or overlap the running image — the single place the FS/prefs-safe bounds are enforced. Add compile-time static_asserts pinning the flash-layout ordering (app < staging ceiling < bootloader; in-place workspace ends at/below the ceiling) so an inconsistent constant fails the build instead of silently corrupting user prefs. Cover the pure planner in test/test_ota/test_ota_flashplan.cpp.
Per review feedback from @liamcottle and @entr0p1, EXTERNAL_FLASH_DEVICES
is not used by MeshCore firmware — CustomLFS determines the flash chip
via JEDEC ID at runtime instead. Removing the definition entirely rather
than keeping the (now-confirmed-unnecessary) correct value.
Also removes EXTERNAL_FLASH_USE_QSPI, which was paired with the same
define.
variant.h defines EXTERNAL_FLASH_DEVICES twice:
- First (correct): ZD25WQ32CEIGR (32Mbit = 4MB, matches actual hardware)
- Second (incorrect): MX25R1635F (16Mbit = 2MB, T-Echo non-Lite chip)
Due to C preprocessor behavior, the second definition silently overrides
the first, causing firmware to initialize the wrong flash chip. This can
result in LittleFS operating incorrectly on T-Echo Lite hardware.
Remove the erroneous second definition.
Also fix missing newline at end of file.
The `recv_pkt_region` is set when processing a flood packet in `filterRecvFloodPacket`
but direct/non-flood packets would never pass through that function, so the pointer was
not cleared for them.
`sendFloodReply` would then later use it blindly, which meant that the response would
either inherit the region from the last flood packet, or refer to a non-initialised pointer
if no region floods had been received yet.
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)
If an `ota pull <#> folder` block-write fails mid-transfer (the motatool seeder
link dropped), the fetch enters a new PAUSED state instead of failing or falling
back to RAM/flash: progress stays on the host, the manager stops requesting, and
loop()/stall-detection leave it untouched (it waits indefinitely). On reconnect
the ESP32 seeder re-registers the folder destination and, if PAUSED, calls
resumeStaged(): OP_STAT re-attaches the host's partial, the leaves are re-read,
and only the missing blocks are re-requested (a brand-new/absent file restarts
from 0). `ota status` reports the paused state.
FolderMotaStore: an OtaStore that streams an in-transit .mota straight to the host
folder over the seeder link (OP_BEGIN/WRITE/SREAD/STAT/FIN) instead of RAM/flash —
the device holds no local staging for it. Wired as a selectable pull destination:
- OtaContext gains a folder_dest (registered by the app while a motatool `serve`
link is up) + its human id (e.g. "tcp 192.168.4.5").
- `ota pull <#> <dest>` now takes a MANDATORY destination; `ota pull <#>` with none
lists the choices (flash always; folder + its link id when connected).
- The ESP32 WiFi seeder registers/clears the folder destination on connect/close —
the same connection both serves the folder and accepts pulls into it.
Captures an exact copy of a device's firmware over the mesh to build a delta
against. Pause/resume on a mid-pull disconnect follows.
Add the device->host WRITE half of the mota-seeder link so a device can capture
a .mota it is fetching off-mesh into the host folder (the same --dir used for
serving), stored as <mid>.mota — e.g. to grab an exact copy of a device's
firmware to build a delta against firmware you don't otherwise have.
- MotaSeederProto: OP_STAT / OP_BEGIN / OP_WRITE / OP_SREAD / OP_FIN (keyed by
mid). Resume needs no host bookkeeping: BEGIN 0xFF-fills the file; on reconnect
the device SREADs the leaves and re-requests only missing blocks (same as flash
resume). Partial = <midhex>.mota.part, published to <midhex>.mota on FIN.
- motatool `serve --dir <folder>` now also handles the storage ops on the same
folder/connection (SeederCore gains the store dir; serve_loop frames the
variable-length WRITE). Host round-trip test added.
Firmware side (FolderMotaStore + `ota pull <#> <dest>` + pause/resume) follows.
hasSeen() was simultaneously a predicate and a mutator — it inserted the
packet hash on every miss, making five call sites that only wanted to mark
a packet as sent call it with the return value discarded.
Split into:
- wasSeen() — pure predicate, no side effects
- markSeen() — explicit insert
All query sites now call markSeen() immediately after wasSeen() returns
false, preserving identical runtime behaviour. The five mark-only send
sites (sendFlood, sendDirect, sendZeroHop x2) now call markSeen directly.
Also fixes three bridge sites (BridgeBase, ESPNowBridge, RS232Bridge)
that had the same query+implicit-insert pattern.
Tests: add test/test_mesh_tables/ covering wasSeen purity, markSeen,
dup stats, and clear. Update SHA256 mock to produce deterministic output
(previously finalize() was a no-op). Add Packet.cpp to native build filter.
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`.
- Added build flag "SX126X_USE_REGULATOR_LDO" (set to 1 to enable) to use LDO for the SX1262 radio instead of DC-DC when the DCDC pin isn't wired up (e.g. t-echo lite)
- Added additional debug mode diagnostic messages during SX1262 init to better highlight faults
The M6's L76K GPS streams NMEA at 9600 baud on its own, but the firmware
reported no GPS. Root cause: pin 29 (PIN_GPS_RESET / the module's REINIT
line) was driven HIGH, which holds the L76K silent so it never emits any
sentences and detection fails.
variant.cpp drove pin 29 HIGH at boot, and because PIN_GPS_RESET was
defined, MicroNMEALocationProvider also drove it HIGH in begin(). Bench
testing on a sealed M6 (passive NMEA capture, no logic analyzer) confirmed:
pin 29 driven HIGH = 0 bytes; pin 29 floating = full NMEA stream.
Define GPS_RESET (-1) so the location provider never touches pin 29, and
stop driving it in initVariant. This matches the Meshtastic M6 variant,
which leaves the same pin as a floating input.
Switch these existing variants from `nrf52_base` to `rak4631_hw` (which adds
ENABLE_OTA + the in-place flash store + the EndF post-build hook), so they get
OTA-over-LoRa. Scope is limited to variants already covered by the
Adafruit_nRF52_Bootloader_OTAFIX in-place apply — no new variants are added.
OtaTargets.h is regenerated to include their target ids.
Discovery was hard to use — a node only advertised at boot, so a peer that
ran `ota ls` minutes later saw "no neighbours".
- First self-advert ~8s after boot, then a short burst (~1 min), then
re-announce at a random 3-10 min interval so a long-running node stays
discoverable without all nodes beaconing in lockstep. The beacon is tiny,
lowest-priority and duty-gated, so a few-minute cadence is cheap.
- `ota ls` now shows the raw target id (`hw XXXXXXXX`) when the env name is
not in this build's OtaTargets.h table, instead of a blank "[other hw]".
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).