Files
MeshCore/tools/lora_ota
..

LoRa OTA tools

lora_ota.py is the existing Companion-based update runner. The separate mota_kiss_direct.py tool serves one verified .mota file through a full KISS modem on logical radio port 0 or 1. It does not use the Companion raw-packet command or the Ethernet-only KISS endpoint. See the mOTA tooling for container creation and the OTA protocol for the wire format.

KISS direct source

The default invocation is read-only probing; no RF transmission. Serving requires explicit --serve, an exact USB serial, a logical port, and an expected frequency. An optional --expect-public-key adds the modem's exact 64-hex-character public identity check. Use exclusive access to the modem; stop other serial users first.

The tool never tunes either radio, changes TX power, or clears temporary profiles. Configure both ends beforehand. Radio port 1 must already be RxTx with the MeshCore protocol, not Ethernet or Meshtext. Active temporary radio2 leases are rejected, including when their frequency matches the saved setting. Identity and frequency checks precede optional pacing changes; the selected profile is checked again before each RF frame. Unknown required getters fail closed. Production readback is not an atomic RF lock: prevent other controllers from changing the modem while serving.

From the repository root, install pyserial, cryptography, and the pinned build compressor dependencies (python -m pip install -r requirements-build.txt). For example, after configuring the modem for the lab profile:

python tools/lora_ota/mota_kiss_direct.py --port COM6 --expect-serial 0001 --logical-port 1 --expected-frequency 909.5 --require-lab-lock

To explicitly enable request-driven serving of one file for five minutes:

python tools/lora_ota/mota_kiss_direct.py --port COM6 --expect-serial 0001 --logical-port 1 --expected-frequency 909.5 --require-lab-lock --serve --mota firmware.mota --zero-pacing --duration 300

Replace the port, exact USB serial and file with the intended device and package. --zero-pacing explicitly sets KISS TXDELAY=0, PERSIST=255 and SLOTTIME=1 only after preflight succeeds. Omit it to retain existing modem pacing. Add --advertise only if one initial discovery advert is wanted; otherwise the source only replies to receiver requests. The source requires full KISS protocol 2+ and successful F8 TX_DONE notifications.

On the receiving node, open a matching temporary MeshCore radio window and set ota config hops 0 for direct negotiation. The source modem uses its configured KISS profile; it does not need a temporary lease. With automatic fetching off, run ota ls on the receiver while the source is serving to request its catalogue, then ota pull <eight-digit-ID> flash. Re-run ota status to confirm the verified download is ready before deliberately requesting installation. Keep ota config autoinstall off during tests. A seeder-only Companion cannot stage firmware to its own flash; use an OTA receiver build or its supported folder destination instead.

Using tempradio2 on the receiver

The KISS source may use its saved radio2 configuration on logical port 1, while the receiving repeater uses an actual tempradio2 lease. These are independent requirements: the source's temporary-lease rejection does not prohibit the receiver's temporary window.

For the V3-to-V4 lab setup, leave radio1 unchanged and configure the infrastructure receiver as follows before starting discovery:

ota config autofetch off
ota config autoinstall off
ota config hops 0
set tx.reply auto
set radio2.cross auto
set tempradio2 909.5,500,5,5,rxtx,30,256
get tempradio2
get radio2.status

Wait for the temporary profile and timing self-test to become active, then use ota ls and ota pull while the matching source is serving. tx.reply auto selects the temporary profile for ordinary discovery replies; infrastructure's explicit both policy can otherwise request a copy on radio1 too. Direct transfer requests remain bound to the negotiated physical profile.

The explicit 256-symbol preamble above is a tested lab value, not a universal minimum. When radio1 remains on slow USA settings, the receiver must spend time listening there; fast retuning alone does not make a 32-symbol radio2 preamble sufficient. Check both ends' timing/recommended-preamble readbacks. Lab images must retain the immutable 909.5-MHz TX guard so the primary USA profile stays receive-only. Do not change or end the temporary profile during a transfer; expiry/replacement fails the bound session closed rather than moving it to radio1.

Direct capacity and fallback

Negotiated, zero-hop direct OTA uses F2 00 followed by the OTA message: 255 RF bytes maximum, with up to 240 bytes of DATA after OTA framing and stream identity. KISS escaping exists only on the host cable. No Ethernet header, forwarding path or enlarged normal MeshCore packet is added. A raw 2 KiB block needs nine DATA frames instead of twelve normal v2 frames; a 1 KiB block needs five instead of six.

Discovery remains ordinary MeshCore OTA. A capable receiver negotiates direct framing; older or routed receivers keep normal framing (171-byte v2 or 160-byte legacy DATA). This source answers both; it never forwards the new direct frames. The receiver owns negotiation, timeout fallback and missing-fragment requests. An unanswered initial direct probe falls back immediately at its adaptive deadline. Once direct DATA has confirmed support, one entirely missed later flight gets a same-mode sparse retry before falling back. A locally rejected request does not consume that retry; valid direct DATA resets it.

Before opening the modem, the source verifies container structure, Merkle data, the full-image hash where applicable, and any embedded signature, then caches per-block raw DEFLATE using Google Zopfli, 1,000 iterations. Compression is used only when smaller than the raw block. Encoded stream identifiers and Merkle proofs retain block integrity; the receiver still enforces its normal target, approval and installation checks. A delta's reconstructed final-image hash must be checked by the receiver against its installed base; this tool does not apply the delta on the host. Container verification is not a replacement for the receiver's trusted-signer policy.

Completion and adaptive pacing

Only one DATA transmission is outstanding. The next RF frame waits for actual F8 TX_DONE, not a queue-acceptance ACK. TX_DONE confirms the local radio completed its transmission, not that the receiver accepted, verified or installed it. A timeout or failed TX ends this source attempt without hidden RF retries; receiver request masks provide resumable missing-fragment recovery.

Pace starts at 1.0. Sparse valid request masks reduce it to 0.5 and then, on loss in a distinct block, 0.25; duplicate requests do not compound the reduction. Sixteen forward clean blocks recover 0.1. --minimum-pace selects a floor of 0.25, 0.5 or 1. Only a pace below 1 adds airtime-scaled quiet gaps. Legacy proof replies also leave a request-turnaround gap; negotiated direct trains may send their proof immediately after the last completed DATA transmission. The final JSON reports actual RF bytes, completed versus attempted TX frames, retries, effective pace, elapsed time and explicitly estimated airtime.

Lab safety

All lab transmissions use 909.5 MHz, 500 kHz, SF5, CR5. Always pass --require-lab-lock during hardware tests: it requires the compiled 909.5 MHz TX lock and verifies that complete PHY tuple. Keep any primary USA profile RX-only; do not transmit tests on it. This flag requires the lab-only read-only guard getter; omit it only for deliberate production use with the explicitly selected legal frequency and matching radio configuration.

Host-only regression tests, without opening USB or transmitting:

python test/test_mota_kiss_direct.py -q

Measured qualification (2026-10-15)

Primary temporary radio (KISS port 0)

A Heltec V3 full KISS source sent a 64 KiB transport-only container to a Heltec V4 repeater on the MercerWoodMesh Pi, using 909.5 MHz / 500 kHz / SF5 / CR5 at 0 dBm. These runs used the receiving node's primary tempradio; radio1 was not also listening on USA. All 32 2-KiB blocks reached Merkle-verified completion in each run:

Transfer DATA frames Elapsed Verified payload rate
Normal v2, after direct-probe fallback 384 47.1 s 11.1 kbps
Direct KISS, raw 288 34.3 s 15.3 kbps
Direct KISS, Zopfli-1000 248 30.6 s 17.1 kbps

The raw direct run transmitted 256 full 255-byte RF frames and needed no DATA retransmissions. A separate compressed run deliberately omitted one DATA frame; the receiver requested the missing fragment, verified all blocks, and the sender reduced its pace to 0.5 before recovering to 0.6. The source also avoids a former 50-ms serial read-fill wait on every tiny modem response.

These are application goodput figures including host control/status polling, not PHY rates; the normal-mode run includes the initial fallback timeout. Compression gains depend on the image. The fixture contained the first 64 KiB of a real application, not a complete installable firmware: it was staged and verified, then cancelled, never installed. Final V4 Full repeater, RAK3401 reduced OTA and RAK4631 Full Companion builds passed their build/RAM guards; nRF52 was compiled and host-tested, not physically transferred in this qualification.

Secondary temporary radio (KISS port 1)

The same V3 source and V4 receiver also passed actual tempradio2 testing with both primary radios left on USA 910.525 MHz / 62.5 kHz / SF7 / CR5, receive-only under the immutable lab TX guard. Radio2 used 909.5 MHz / 500 kHz / SF5 / CR5, 0 dBm and preamble 256; the receiver used tx.reply auto and radio2.cross auto. Each run staged and Merkle-verified the same 64 KiB fixture, without installation:

Transfer Elapsed Verified payload rate Radio1 RX / TX during run
Direct raw, one entire later response flight deliberately omitted 78.3 s 6.7 kbps 21 / 0
Direct Zopfli-1000, no injected loss 72.6 s 7.2 kbps 33 / 0
Normal v2, initial direct probe deliberately ignored 114.2 s 4.6 kbps 40 / 0

The missed-flight test dropped exactly 18 DATA and two PROOF frames after five committed blocks. Before the recovery fix, the identical omission caused a 240-to-171 downgrade; after the fix, all 297 transmitted DATA frames stayed in direct geometry and all blocks verified. Ambient packet loss and pacing varied, so individual timings are observations, not a repeatable throughput guarantee. All tests recorded zero primary-profile TX increase. These figures include primary-channel listening, recovery, adaptive quiet gaps and status polling; they must not be substituted for the port-0-only figures above. The V4 Full repeater and RAK3401 reduced-sensor OTA rebuilds passed, as did all 163 native OTA core tests, including sanitizers, confirmed-empty retry admission, clock rollover and session expiry. This radio2 hardware qualification was ESP32-only; the nRF52 target was compiled, not physically transferred in this run.