diff --git a/.gitignore b/.gitignore index a0ad5f6ea9..4b13566454 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ compile_commands.json .venv/ venv/ platformio.local.ini + +__pycache__/ diff --git a/build.sh b/build.sh index 313c4c47a0..acda76c306 100755 --- a/build.sh +++ b/build.sh @@ -140,8 +140,15 @@ build_firmware() { # e.g: RAK_4631_Repeater-v1.0.0-SHA FIRMWARE_FILENAME="$1-${FIRMWARE_VERSION_STRING}" + # OTA target id = sha2-256:4(env_name) as a little-endian uint32 (matches tools/mota target_id_for_env + # and the device's MainBoard::getOtaTargetId()). Harmless when OTA is disabled. + MOTA_TARGET_ID=$(python3 -c "import hashlib,sys;print('0x%08x'%int.from_bytes(hashlib.sha256(sys.argv[1].encode()).digest()[:4],'little'))" "$1" 2>/dev/null || echo "") + # add firmware version info to end of existing platformio build flags in environment vars export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DFIRMWARE_BUILD_DATE='\"${FIRMWARE_BUILD_DATE}\"' -DFIRMWARE_VERSION='\"${FIRMWARE_VERSION_STRING}\"'" + if [ -n "$MOTA_TARGET_ID" ]; then + export PLATFORMIO_BUILD_FLAGS="${PLATFORMIO_BUILD_FLAGS} -DMOTA_TARGET_ID=${MOTA_TARGET_ID}" + fi # disable debug flags if requested disable_debug_flags diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md new file mode 100644 index 0000000000..4e7f5a28ea --- /dev/null +++ b/docs/ota_protocol.md @@ -0,0 +1,600 @@ +# MeshCore OTA — `.mota` container & LoRa protocol + +This is the **single source of truth** for MeshCore's over-the-air firmware update system ("mOTA"). It is +written for developers who want to implement an interoperable peer (server, fetcher, relay, or host tool) +in another codebase or project. Everything below is implemented and hardware-verified in this repository; +where a section names a source file, that file is the authoritative reference for byte-level details. + +> **Just want to update your node?** See the plain-language [OTA user guide](ota_user_guide.md) — this +> document is the technical/wire specification. + +**Design goals** + +- Distribute firmware over LoRa as a **self-verifying, resumable, BitTorrent-style block transfer** that + survives reboots and never auto-applies without explicit consent. +- **Trustless transport / relay:** any node may carry or relay any block; integrity is content-addressed + against a signed merkle root, so a relay need not be trusted and never needs the signing keys. +- **Lowest priority, always:** OTA traffic is enqueued behind all mesh traffic — "eventually upgradable". + A busy node delays OTA indefinitely rather than competing with real traffic. +- **Portable:** the engine (`src/helpers/ota/OtaManager`) is Arduino/radio/crypto-free and host-testable, + so the same logic drives a device, a simulation, or a third-party implementation. + +**Source map** (all under `src/helpers/ota/` unless noted) + +| Concern | File | +|---|---| +| Constants, enums, flags | `OtaFormat.h` | +| Container/manifest parse | `MotaContainer.{h,cpp}` | +| Merkle tree + proofs | `MerkleTree.{h,cpp}` | +| EndF self-identity | `FirmwareInfo.{h,cpp}` | +| Wire message codec | `OtaProtocol.{h,cpp}` | +| Session engine (serve+fetch+discovery) | `OtaManager.{h,cpp}` | +| Multi-mota / folder relay | `OtaSource.h`, `MotaSourceSerial.{h,cpp}`, `MotaSeederProto.h` | +| Staging stores | `OtaStore.h`, `OtaStoreFlashNrf52.*`, `OtaStoreFlashEsp32.*` | +| Apply | `OtaApply.*`, bootloader `Adafruit_nRF52_Bootloader_OTAFIX` | +| Device glue (CLI/context) | `OtaCli.cpp`, `OtaContext.h` | +| Host tooling | `tools/motatool/` (C++ CLI: build/verify/inspect/serve); `tools/mota/` (Python reference lib `motalib.py` + build/test glue) | + +--- + +## 1. Conventions + +- **Endianness:** all multi-byte integers are little-endian unless stated. +- **Hashes (multihash):** the hash family is declared once per manifest via `hash_algo` = + `0x12` = **SHA-256** (the [multihash](https://github.com/multiformats/multihash) code for sha2-256). + Truncations used: + - `sha2-256:4` — first 4 bytes of the SHA-256 digest. Merkle leaves, internal nodes, root, proofs, + `manifest_id`, and the discovery `set_digest`. + - `sha2-256:8` — first 8 bytes. Base-firmware identity (`base_hash`, `EndF.body_hash`). + - `sha2-256:32` — full digest. The image security anchor (`image_hash`). + Digests are stored **bare** (just the truncated bytes); the family is implied by `hash_algo`. +- **Signatures:** Ed25519 (RFC 8032), 64-byte detached signature, 32-byte public key. + +**Reference constants** (`OtaFormat.h`): + +| Name | Value | ASCII / note | +|---|---|---| +| Container `MAGIC` | `6D 4F 54 41` | `mOTA` | +| Container `TRAILER` | `76 6B 34 39 36` | `vk496` | +| `EndF` marker | `45 6E 64 46` | `EndF` | +| `hash_algo` (sha2-256) | `0x12` | multihash code | +| `format_ver` | `0x02` | this spec | +| `approval` = not approved | `FF FF FF FF` | erased NOR word | +| `approval` = approved | `41 50 52 56` | `APRV` | +| `MFLAG_FULL` | `0x01` | flags bit0 | +| `MFLAG_SIGNED` | `0x02` | flags bit1 | +| `CODEC_FULL` / `_SEQUENTIAL` / `_INPLACE` | `0` / `1` / `2` | §5 | +| `PAYLOAD_TYPE_OTA` | `0x0C` | MeshCore packet type (`src/Packet.h`) | +| `MAX_PACKET_PAYLOAD` | `184` | usable bytes per packet (`src/MeshCore.h`) | +| Default block size | `1024` | `block_size_log2 = 0x0A` | +| OTA TX priority | `250` | lowest (`OTA_TX_PRIORITY`, `src/Mesh.h`) | + +--- + +## 2. Firmware image & the `EndF` trailer + +Every OTA-capable build appends a fixed **56-byte** `EndF` trailer to its flashed image so a running node +can discover its own size **and self-describing identity** on any MCU (no linker symbols needed). Every +field is always present at a constant offset. Implemented by `FirmwareInfo.cpp`; appended at build time by +`tools/mota/pio_endf.py` (post-build hook). + +``` +flashed image = BODY (image bytes) || EndF trailer +EndF trailer (fixed 56 bytes): + off 0 4 "EndF" 45 6E 64 46 + off 4 4 body_len uint32 LE — length of BODY (excludes the whole trailer) + off 8 8 body_hash sha2-256:8 of BODY + off 16 4 fw_version uint32 LE, packed MAJOR<<24|MINOR<<16|PATCH<<8|pre (0 = unknown) + off 20 4 target_id uint32 LE — sha2-256:4(pio_env): hardware + role + partition (fetch routing) + off 24 32 hw_id NUL-padded ASCII hardware tag (brick-safety), e.g. "RAK4631" ("" = unknown) +``` + +- **Self-describing identity.** `pio_endf.py` computes `target_id` from the PlatformIO env name itself (so + it's correct even without `build.sh`'s `-D MOTA_TARGET_ID`), `hw_id` from `MOTA_HW_ID`, and `fw_version` + from `FIRMWARE_VERSION`. The device reads them back (`ota_self_firmware()`), so a node's advertised + identity is correct regardless of how it was built — and the packaging tool reads them straight from a raw + `.bin` (no `--target-env`/`--fw-version` flags, no reliance on filenames; §9, §13). A dev build with no + dotted version simply carries `fw_version = 0` / empty `hw_id` (= unknown) — still a full 56-byte trailer. +- **Size discovery:** scan flash from the partition top downward for the `EndF` marker; the byte before it + is the last BODY byte (the trailer is always 56 bytes). See `ota_self_firmware()`. +- **Delta base matching:** a node's `body_hash` is read directly from its own `EndF`; a delta's `base_hash` + (§5) must equal it. `body_hash` is over BODY only. +- **No circularity:** `EndF` hashes only the BODY, never itself. + +The "reconstructed image" referenced by the manifest is the full `BODY || EndF` (what gets flashed). + +> **Implementer note:** the bootloader (and any non-Arduino consumer) MUST locate the body extent by +> scanning for `EndF`, never by trusting a stored size — see the bootloader contract in §12. + +--- + +## 3. The `.mota` container + +The distributed form (host-built, wire-transferred). Parsed by `mota_parse()` in `MotaContainer.cpp`. + +``` +off size field +0 4 MAGIC = 6D 4F 54 41 +4 4 MOTA_TOTAL_SIZE uint32 LE — total container bytes (incl. manifest, leaves[], + payload, trailer). Lets a node pre-reserve staging and compute + write_start = staging_region_end − MOTA_TOTAL_SIZE. +8 M MANIFEST (§4; M = 197 fixed + leaves[], 4*BC; no length field — BC from payload_size) +8 + M P PAYLOAD (payload_size bytes; delta or full image) +8 + M + P 5 TRAILER = 76 6B 34 39 36 +``` + +`MOTA_TOTAL_SIZE = 4 + 4 + M + P + 5`. The manifest `M` **includes** `leaves[]`; the manifest-minus-leaves +prefix (`mfl`, sent over the wire as `OTA_MANIFEST`) is `[8, leaves_off)`. + +**Staged (in-flash) form.** Written bottom-aligned so `TRAILER` ends at `staging_region_end`. Identical +bytes, except the device mutates two regions in place (both NOR-safe, no re-erase): the `leaves[]` slots +(filled as blocks arrive — §7) and the 4-byte `approval` field (on owner consent — §4.2). Everything else +is immutable. + +--- + +## 4. The manifest + +**Fixed layout.** Every field sits at a constant offset and is always present — `base_hash`, +`signer_pubkey` and `signature` are zero-filled when not applicable (a full image / an unsigned container). +Only `leaves[]` is variable (one 4-byte hash per block). So the manifest-minus-leaves (`mfl`) is **always +197 bytes** and the parser is plain offset reads — no conditionals. Parsed by `mota_parse_manifest()`. + +``` +off size field notes +0 1 format_ver = 0x02 +1 1 flags bit0 FULL (0=delta/partial, 1=full image); bit1 SIGNED; bits2-7 reserved 0 +2 1 hash_algo 0x12 = sha2-256 +3 4 target_id device/arch/role discriminator (§9) +7 4 fw_version MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre (comparable uint32) +11 4 image_size size of the reconstructed image (BODY||EndF) +15 4 payload_size PAYLOAD bytes in this container +19 1 block_size_log2 e.g. 0x0A = 1024 +20 4 merkle_root sha2-256:4 over PAYLOAD blocks (§6) — also the manifest_id +24 32 image_hash sha2-256:32 of the reconstructed image — SECURITY anchor +56 1 codec_id 0=full/raw, 1=detools-sequential, 2=detools-in-place +57 32 hw_id NUL-padded ASCII hardware tag (e.g. "RAK4631"); same tag => bootable-compatible. + SIGNED. Applier refuses a mismatch (brick-safety); empty on either side = skip. +89 8 base_hash sha2-256:8 of the BASE image's BODY (== that build's EndF.body_hash). 0 if FULL. +97 32 signer_pubkey Ed25519 public key. 0 if not SIGNED. +129 64 signature Ed25519 over manifest[0, 129). 0 if not SIGNED. +193 4 approval FF FF FF FF = not approved; 41 50 52 56 ("APRV") = approved +--- end of manifest-minus-leaves: mfl = 197 (constant); leaves_off = 8 + 197 = 205 in the container --- +197 4*BC leaves[] BC = ceil(payload_size / 2^block_size_log2). sha2-256:4 each (the only variable field) +``` + +The signature always covers `manifest[0, 129)` (the head + `base_hash` + `signer_pubkey`). `approval` is +outside the signed region so it can be flipped in place on consent without breaking the signature. + +Manifest-minus-leaves size (`mfl`) is a constant **197 bytes** for every container (full or delta, signed +or unsigned). At 197 bytes the manifest exceeds one packet, so `OTA_MANIFEST` is always sent multi-fragment +(§8.4, 2 fragments) and reassembled by the fetcher. + +### 4.1 Signed region + +`signature` covers manifest bytes `[0, 129)` — the head + `base_hash` + `signer_pubkey`. It does **not** +cover `approval` or `leaves[]`: + +- `leaves[]` are verified against the signed `merkle_root` (§6), so they need no separate signature. +- `approval` is device-local consent (§4.2), deliberately outside the signature. + +### 4.2 The `approval` field + +- Distributed and **forced on ingest** to `FF FF FF FF` (a peer can never pre-approve). +- The local owner's `ota applydelta` writes `41 50 52 56` (`"APRV"`) — a single NOR-safe write (only clears + bits from the erased word). Any partial/other value reads as not-approved (fail-safe). +- Bound to this image (lives in this `.mota`'s manifest, re-erased when a new `.mota` is staged). +- A **consent** marker, not a security primitive. Authenticity = `signature` + `image_hash` + `hw_id`. + +--- + +## 5. Payload, codecs & delta base + +`PAYLOAD` is either the full reconstructed image (`FULL`) or a delta (`!FULL`). + +| `codec_id` | Meaning | Used by | +|---|---|---| +| 0 | full / raw | PAYLOAD = reconstructed image (`BODY‖EndF`). ESP32 A/B (and any board for a full image). | +| 1 | detools **sequential** | random read of base + sequential write of result → ESP32 A→B inactive slot. | +| 2 | detools **in-place** | bounded scratch; rewrites the app region in place → nRF52 single-slot. | + +For deltas, `base_hash` = the base build's `EndF.body_hash` (sha2-256:8 of its BODY). A node applies a +delta only if `base_hash` matches its own `EndF.body_hash`. After applying, the result MUST hash +(sha2-256:32) to `image_hash` before it is booted — the hard security gate. + +**A fetcher only requests firmware it can apply.** Each node declares the codec(s) it can apply +(`set_apply_codec`/`set_apply_codec2`): ESP32 accepts `full` + `sequential` (+ `in-place`), nRF52 accepts +`full` + `in-place`. `CODEC_FULL` is always acceptable. A `.mota` with an unsupported codec is rejected at +discovery time, before any blocks are requested. + +Compression is internal to the detools patch and must be supported by the applier. Patches are produced by +**detools 0.53.0** (`tools/mota` → `detools.create_patch`) and decoded on-device by detools' embeddable C +decoder, vendored verbatim at `src/helpers/ota/detools/` (see its `README.meshcore.txt`). That build +enables only the self-contained `NONE` + `CRLE` compressions (no malloc/liblzma/heatshrink), so MeshCore +deltas use `--compression crle`. **Do not reimplement the codec** — use the vendored decoder. + +--- + +## 6. Merkle tree (sha2-256:4) + +Verifies each PAYLOAD block against the signed `merkle_root` **before** the whole payload exists, so +corruption/forgery is localized to a block. Implemented in `MerkleTree.cpp`. + +- **Blocks:** PAYLOAD splits into `BC = ceil(payload_size / B)` blocks, `B = 2^block_size_log2` (default + 1024). The last block is its real length (**no zero padding**). +- **Leaf:** `leaves[i] = sha2-256:4( block_i_bytes )`. +- **Internal node:** `node = sha2-256:4( left ‖ right )` (4+4 input bytes). +- **Odd level:** an odd count promotes the **last node unchanged** to the next level (no duplication). +- **Root:** reduce until one node remains. `BC == 1` → root = `leaves[0]`. `BC == 0` is invalid. + +### 6.1 Proofs + +A proof for block `i` is the ordered list of sibling digests from leaf to root. Promoted levels contribute +**no** element. Verification (needs `BC` to know the tree shape): + +``` +h = leaf_i ; idx = i ; n = BC ; p = 0 +while n > 1: + if (n is odd) and (idx == n-1): # this node was promoted + pass + else: + sib, side = proof[p] ; p += 1 + h = sha2-256:4( sib ‖ h ) if side==left else sha2-256:4( h ‖ sib ) + idx //= 2 ; n = (n + 1) // 2 +accept iff h == merkle_root and p == len(proof) +``` + +Over LoRa, `leaves[]` are **omitted** from the manifest transfer; a serving node computes a block's proof +on demand from its stored `leaves[]` (`OTA_REQ_PROOF`/`OTA_PROOF`, §8.5), and the fetcher fills its own +`leaves[i]` as each verified block lands. + +--- + +## 7. Block availability, staging & resume + +There is no separate availability structure. **Block `i` is present ⟺ `leaves[i]` is non-erased** +(`!= FF FF FF FF`). Because `leaves[]` live in the staged flash region, availability **survives reboot**. + +**Commit order per block (crash-safe):** (1) verify proof, (2) write block payload to its offset, (3) write +`leaves[i]` **last**. A power loss before step 3 leaves the slot erased → the block is simply re-fetched +(idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning `leaves[]`. + +**Resume (`OtaManager::resumeStaged` + `OtaStore::checkpoint`/`reopen`):** an interrupted fetch resumes from +the staged container after a reboot — re-parse the stored manifest, recompute geometry, count present +blocks, continue fetching the holes (or jump straight to COMPLETE). The checkpoint cadence (persist progress +every N committed blocks) is runtime-tunable (`ota config checkpoint `, 0 = only finalized containers +resume). Stores keep `leaves[]` in RAM until flush and never auto-GC, preserving resumable progress. + +**Flash-store note (RX-safe writes):** a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa +RX, so the flash stores (`OtaStoreFlashNrf52`/`OtaStoreFlashEsp32`) **coalesce writes to the erase unit** +(4 KB page / sector) and commit each once off the per-packet path — RAM stays O(one page), not O(image). A +small delta that fits page 0 does zero flash I/O until COMPLETE. + +--- + +## 8. LoRa OTA protocol + +Carried in MeshCore packets with **`PAYLOAD_TYPE_OTA = 0x0C`**. Every OTA packet payload is: + +``` +[0] ota_msg_type (OtaMsgType, OtaFormat.h) +[1..] body (fixed per type; encode/decode in OtaProtocol.cpp) +``` + +Message types: + +| `ota_msg_type` | val | routing | purpose | +|---|---|---|---| +| `OTA_ADV` | 0x01 | flood | tiny per-node beacon (discovery tier 1) | +| `OTA_QUERY` | 0x02 | flood | ask a source for its catalog (discovery tier 2) | +| `OTA_HAVE` | 0x03 | flood | the catalog reply (fragmented, digest-tagged) | +| `OTA_GET_MANIFEST` | 0x04 | direct | request a manifest by `manifest_id` | +| `OTA_MANIFEST` | 0x05 | direct | the manifest-minus-leaves, fragmented | +| `OTA_REQ` | 0x06 | direct | request a window of blocks' DATA | +| `OTA_DATA` | 0x07 | direct | one self-describing fragment of a block's data | +| `OTA_REQ_PROOF` | 0x08 | direct | request the merkle proof for one block | +| `OTA_PROOF` | 0x09 | direct | the merkle proof for one block | + +- **`manifest_id`** = the manifest's `merkle_root` (4 bytes) — a compact content id present in every + transfer message, so a multi-mota server dispatches each request to the right image. +- **Priority:** all OTA packets enqueue at `OTA_TX_PRIORITY = 250` (lowest). OTA never competes with mesh + traffic; on a busy node it is delayed indefinitely. +- **Reliability is *eventual*:** the fetcher re-requests missing fragments/blocks after a timeout, possibly + from a different peer. No hard ACKs, no global ordering. +- **Relay:** replies are flooded, so transparent relay needs no per-requester addressing, and the transfer + is trustless (the fetcher verifies every block against the signed root). Any neighbor may serve any + fragment it has. +- **Hop limit + duty cycle:** OTA floods accumulate one path-hash per relay (the mesh's flood routing). A + node *accepts* a packet only if it arrived within `ota config hops` hops (default 3; `0` = direct only) + and *relays* it only while still under that limit, appending its own hash. Relays are lowest-priority and + are skipped when the packet pool runs low (the source retries), so heavy OTA can never monopolise a + repeater's RAM or starve real traffic. + +### 8.1 Two-tier discovery + +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 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 +n_motas uint8 — count of complete servable mOTAs (saturates at 255) +set_digest[4] sha2-256:4 over the SORTED set of served manifest_ids (see below) +``` + +`set_digest` is a **content hash of the offering**, not a counter: canonical across nodes, and it changes +iff the set of served mids changes. A peer that has already catalogued this `{seeder, set_digest}` ignores +the beacon (steady state is query-free). For a single served mota, `set_digest = sha2-256:4(mid)`. + +**Tier 2 — `OTA_QUERY` → `OTA_HAVE`** (on interest only): + +``` +OTA_QUERY (flood): seeder_id[4] set_digest[4] filter_target(uint32) # filter_target 0 = everything +OTA_HAVE (flood): seeder_id[4] set_digest[4] frag_idx(1) frag_total(1) n_rows(1) rows[] + HaveRow (16 bytes, OTA_HAVE_ROW_BYTES): mid[4] target_id(4) fw_version(4) codec_id(1) flags(1) have_count(2) +``` + +`have_count` is how many blocks the advertiser currently holds (`== block_count` for a full copy, less for a +partial/in-progress source). It lets a fetcher see, per mid, **how many peers have it and at what progress** +— so it knows the firmware is on multiple peers and can trust the swarm (§8.6) rather than depend on one. + +A node interested in a source's offering schedules a QUERY; the source replies with its full catalog as +`OTA_HAVE` rows (fragmented if they exceed one packet — up to 12 rows per fragment). The heavy manifest is +fetched per-mid only on commit (§8.3). + +### 8.2 Anti-storm (mandatory at mesh scale) + +If 50 neighbours all queried a new beacon at once, the mesh would collapse. Mitigations (gossip/mDNS +pattern), all in `OtaManager`: + +- **`OTA_HAVE` is flooded and digest-tagged.** EVERY node that overhears it caches the rows **passively** + (keyed by `{seeder, set_digest}`) — no query of its own needed. +- **Jittered query:** a peer needing a catalog schedules its `OTA_QUERY` after a random delay + `OTA_QUERY_MIN_MS (300) + rand(OTA_QUERY_SPREAD_MS (4000))`, derived from `id ⊕ digest ⊕ self`. +- **Overhear suppression:** during the jitter window, overhearing *another* QUERY **or** a HAVE for the same + `{seeder, set_digest}` CANCELS the pending query. + +Net effect: a digest change costs ~1 query + ~1 HAVE flood mesh-wide; a stable mesh is query-free. + +### 8.3 Fetch handshake + +``` +fetcher server (any node that has the mid) + OTA_GET_MANIFEST(mid) ───────► + ◄─────── OTA_MANIFEST(mid, frag_idx, frag_total, bytes) × frag_total + (reassemble manifest, verify, compute geometry: BC, block_size, payload_size) + for each missing block window: + OTA_REQ(mid, start_block, count) ► + ◄─────── OTA_DATA(mid, block_idx, frag_off, data) × (per block) + (reassemble block from frag_off slices) + OTA_REQ_PROOF(mid, block_idx) ────► + ◄─────── OTA_PROOF(mid, block_idx, n_proof, proof) + (verify proof vs merkle_root → write block → write leaves[i]) + when all blocks present: verify full merkle_root + image_hash → COMPLETE +``` + +### 8.4 Message bodies (transfer) + +All offsets after the 1-byte type. Encoders/decoders in `OtaProtocol.cpp`; constants in `OtaManager.h`. + +``` +OTA_GET_MANIFEST: manifest_id[4] +OTA_MANIFEST: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up to OTA_MF_FRAG=176 B/frag +OTA_REQ: manifest_id[4] start_block(uint16) count(1) +OTA_DATA: manifest_id[4] block_idx(uint16) frag_off(uint16) data[] # up to OTA_FRAG_DATA=160 B +OTA_REQ_PROOF: manifest_id[4] block_idx(uint16) +OTA_PROOF: manifest_id[4] block_idx(uint16) n_proof(1) proof[] # n_proof × 4 bytes +``` + +- **Block ⇆ fragments:** a 1 KB block is split into self-describing `OTA_DATA` fragments. `frag_off` is the + byte offset of `data` within the block, so the global position is `block_idx*block_size + frag_off` — + a fragment is self-placing and may be requested from **any** peer (BitTorrent-style). The fetcher tracks a + per-block slice bitmap and reassembles before requesting the proof. +- **Data and proof are separate phases.** `OTA_DATA` carries no proof; the proof is fetched once per block + via `OTA_REQ_PROOF`/`OTA_PROOF` after the block's data is complete. + +### 8.5 Sizing against `MAX_PACKET_PAYLOAD = 184` + +| message | fixed overhead | payload/packet | +|---|---|---| +| `OTA_DATA` | 9 B (type+mid4+idx2+off2) | `OTA_FRAG_DATA = 160` → 7 frags per 1 KB block | +| `OTA_MANIFEST` | 7 B | `OTA_MF_FRAG = 176` → signed manifest ≈ 2 frags | +| `OTA_HAVE` | 12 B | 12 rows × 14 B per fragment | +| `OTA_PROOF` | 8 B | up to ~44 sibling digests (≫ any real tree) | + +A served mota supports up to `OTA_MAX_BLOCK/4` leaves in the default 4 KB proof scratch (≤1024 blocks ≈ 1 MB +payload); larger self-images pass a bigger scratch buffer. + +### 8.6 Swarm load distribution (don't hammer one seeder) + +The discovery anti-storm (§8.2) stops 50 neighbours all *querying* one node. The same hazard exists for the +*transfer*: if one node has new firmware and 50 want it, naïve fetchers would all REQ the same blocks from +the same seeder. Because OTA is always lowest-priority (§8) the mesh won't collapse, but the transfer would +be needlessly slow and centralized. Mitigations (all in `OtaManager`, reusing the §8.2 jitter/suppress idea): + +- **Overhearing fills holes for free.** Every fetcher accepts any *broadcast* `OTA_DATA` for its mid, not + just data it requested. So within a broadcast neighbourhood, one peer's request serves everyone who hears it. +- **De-correlated requests.** A fetcher picks a **random** missing block (not lowest-first), so N fetchers + don't lockstep on the same block; collectively they pull different blocks and everyone overhears them all. + Each fetch also holds its first REQ a random `OTA_REQ_SPREAD_MS` so simultaneous starters don't burst together. +- **Request suppression.** Overhearing a peer's `OTA_REQ` for a block makes a fetcher spend its next REQ on a + *different* block (`OTA_REQ_SUPPRESS_MS`) — the broadcast DATA will fill the overheard one anyway. +- **Sources multiply (the key to "don't pull one node"):** + - **Re-seed after COMPLETE (epidemic).** A node that finishes a download advertises + serves it (it now has + all blocks *and* leaves, so it serves DATA and proofs). The origin seeds a few peers, they seed the next + ring, etc. — load on the origin drops from O(N) to ~O(log N). (Default `autoinstall=off` means a completed + node lingers as a seeder until the operator applies.) + - **Partial re-serve during the transfer.** A still-fetching node serves the **DATA** of blocks it already + holds (not proofs — it may lack sibling leaves), so peers can source bytes from it, not only the origin. +- **Serve de-dup.** A holder about to serve a block it just overheard *another* holder broadcast suppresses + its own send (`OTA_SERVE_SUPPRESS_MS`), so multiple sources of one mota don't duplicate-broadcast it. + +All serving stays reactive and lowest-priority, so seeding never competes with real traffic — the system is +"eventually upgradable": a busy node simply delays OTA until it has spare airtime. + +--- + +## 9. Identity, trust & versioning + +- **`target_id`** (4 B): `sha2-256:4(pio_env_name)` (little-endian uint32). The env name uniquely captures + hardware **and** role/partition, so a node auto-fetches only matching firmware (a companion image is not + fetched onto a repeater even though it shares `hw_id`). It is **self-described in the firmware's EndF** + (§2, written by `pio_endf.py`) and read via `ota_self_firmware()`, so it is correct on any build; + `-D MOTA_TARGET_ID` / `MainBoard::getOtaTargetId()` is the fallback when no EndF identity is present. + `tools/mota` reads it from the firmware's EndF (or `--target-env`). A manual `ota pull`/`want` can override + target (deliberate role switch); the `hw_id` brick-safety gate (§4) still applies at apply time. +- **`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`. +- **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`, + resolved from `pio project config`). So `ota ls` can render `[Heltec_v3_repeater]` for a neighbour's + beacon without the string being transmitted. Unknown ids show as `other hw` / `N/A`. +- **`fw_version`:** packed comparable uint32 (`MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre`); also self-described + in EndF. `ota ls` decodes it for display and flags each update `[yours]` / `[other hw]` / `[?]` by + comparing the advertised `target_id` to the node's own. +- **`hw_id`:** 32-byte NUL-padded ASCII hardware tag inside the signed head. The applier refuses a `.mota` + whose `hw_id` differs from the device's own tag (empty on either side = permissive). Brick-safety + independent of signature. +- **Signing & allowlist:** a node keeps a runtime allowlist of trusted Ed25519 signer pubkeys (none embedded + in firmware; `ota key add/list/rm`). A `.mota` is eligible for **auto-install** only if signed by an + allowlisted key, the signature verifies, and `image_hash` matches; otherwise it is manual-apply only with + explicit confirmation. **Transfer needs no trust** — blocks are content-addressed against the signed root. +- **Policies (persisted):** `autofetch` ∈ {off, any, signed} (default off) gates automatic block fetching of + own-target adverts; `autoinstall` ∈ {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 + (finish-current). + +--- + +## 10. Multi-mota serve & the external "folder" relay + +A node serves a **set** of mOTAs: its own firmware plus, optionally, an external folder of `.mota` files it +relays without holding them in flash. To peers it simply "has N mOTAs"; the relay is trustless (fetchers +verify everything). The serve side (`OtaManager`) keeps a lightweight registry of what it advertises and two +resident "views": `view0` (its own firmware) and one on-demand view loaded from a source when a request +targets an external mota. Every fetch message carries `manifest_id`, so dispatch is a registry lookup. + +### 10.1 The `MotaSource` abstraction (`OtaSource.h`) + +Transport-agnostic provider of one or more complete `.mota` as random-access bytes. The same serve code +drives USB-serial, BLE, a WiFi URL list, an NFS/samba mount, etc. — only `read()` differs. + +```cpp +struct MotaDesc { // catalog metadata + region offsets (no whole image in RAM) + uint8_t mid[4]; uint32_t target_id, fw_version; uint8_t codec_id, flags; + uint32_t total_size, leaves_off, block_count, payload_off, payload_size; +}; +class MotaSource { + virtual uint8_t count(); // # mOTAs offered + virtual bool describe(uint8_t idx, MotaDesc& out); // metadata + offsets + virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len); // random-access bytes +}; +``` + +To serve an external mota the node reads its manifest-minus-leaves + `leaves[]` into RAM (≤4 KB for ≤1024 +blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves. + +### 10.2 The `mota-seeder` transport (`MotaSeederProto.h`) + +A `MotaSource` is fed by a host that serves a folder over the device's **USB serial** (the same console the +CLI uses — no extra hardware) or, on an ESP32 WiFi companion, over **WiFi (TCP)**. The host is the +self-contained C++ tool `tools/motatool/` (`motatool serve --serial ` / `--tcp `, which +also builds + validates `.mota` and runs on small hardware). The device only emits request frames *while +actively serving a fetch*, and reads the reply synchronously, so over the shared USB console binary frames +coexist with the text CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed: + +``` +request (device → host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args) +response (host → device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior) + +OP_COUNT 0x01 args: - → payload: count(1) +OP_DESCRIBE 0x02 args: idx(1) → payload: MotaDesc wire (38 B) +OP_READ 0x03 args: idx(1) off(4) len(2) → payload: len bytes +MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1) + total_size(4) leaves_off(4) block_count(4) payload_off(4) payload_size(4) +status: 0 = OK, non-zero = error (out of range / past EOF). +``` + +Device CLI: `ota folder on` (attach + announce), `ota folder` (list), `ota folder off`. Build flag +`OTA_FOLDER_SERIAL` (default stream = console `Serial`; override `OTA_FOLDER_SERIAL_STREAM` + define +`OTA_FOLDER_SERIAL_BEGIN` for a dedicated UART). On an ESP32 WiFi companion the node also runs a second +`WiFiServer` on a **dedicated seeder port** (`OTA_SEEDER_TCP_PORT`, default `5001`), separate from the +companion app port (`TCP_PORT`, default `5000`) — so `motatool serve --tcp` can feed updates while a phone +app stays connected. The node auto-attaches the source when a seeder client connects and detaches when it +closes (no `ota folder on` needed over TCP). Verified on hardware: a RAK4631 relays a host folder to a +Heltec V3 over one USB cable, and a host feeds a Heltec V3 over WiFi (`:5001`) while the companion serves a +phone on `:5000` — every block merkle-checked. + +**Transport-agnostic by design.** The request/response *semantics* (`COUNT` / `DESCRIBE(idx)` / +`READ(idx, off, len)` over a folder catalog) are independent of the link. The 2-byte magic + XOR checksum + +resync framing above exists for the shared USB-UART (an unframed byte stream); it is harmless over a +reliable stream and the **WiFi (TCP)** transport reuses it as-is — both ends just treat the socket as a +byte stream (on-device, `SerialMotaSource` runs verbatim over an Arduino `Stream`-compatible `WiFiClient`; +`motatool`'s `TcpTransport` mirrors its `SerialTransport`). A future framed link such as **BLE GATT** (an +Android phone relaying a folder) could carry the same ops with no magic/checksum at all — a request +characteristic write delivers `op + args`, the reply notifies `status + payload`. `motatool` reflects this +split: a transport-free `SeederCore` (the catalog logic) under a swappable framing/transport layer. + +--- + +## 11. CLI surface (`OtaCli.cpp`) + +User-facing OTA data should travel via `CMD_OTA_*` companion binary frames; the text CLI below is +debug/operator oriented and replies are `snprintf`-bounded into a 160-byte buffer. + +Commands take intuitive aliases (matched by the first word; see `is_cmd` in `OtaCli.cpp`) so they're easy +to type and read — `status`/`neighbors`/`pull`/`drop`/`applydelta` are the canonical names, the aliases are +the recommended user-facing forms. Output is plain-language (a user-facing guide lives at +[ota_user_guide.md](ota_user_guide.md)). + +``` +ota help | ? list the commands +ota status | st (or bare `ota`) plain-language: running fw, the one fetch session (state/%/id), serving, keys +ota ls | neighbors | nbrs | updates | n discovered updates (queries sources; rows arrive async via OTA_HAVE) +ota get | pull | download <#|mid8> fetch a chosen mOTA (manual; works regardless of autofetch) +ota install | apply | applydelta verify + approve + (ESP32) apply / (nRF52) reboot-to-bootloader +ota cancel | drop | stop drop the current fetch session (frees the slot; stops re-seeding) +ota announce | adv serve self + send a beacon now +ota self | id print this firmware's EndF (body/image size, base_hash) +ota folder | fold [on|off] attach/detach an external .mota folder (host daemon) ; bare = list +ota config | cfg | set [autofetch|autoinstall|checkpoint] ... show/set persisted policy +ota key | keys [add|rm ] trusted signer allowlist ; bare = list +ota dev ... bring-up helpers (stage/recv/serve/verify) +``` + +--- + +## 12. Apply & bootloader contract + +- **ESP32 (A/B):** applied in-firmware via the detools decoder into the inactive OTA slot + (`OtaApply.cpp::ota_apply_detools_mota` + `OtaStoreFlashEsp32`), then set-boot + reboot (power-safe, + rollback-capable). No bootloader changes. Erase ranges must be sector-aligned (4096). +- **nRF52 (single-slot):** the running firmware **never** flashes the app. `ota applydelta` verifies fully + (`image_hash`, `base_hash`, signature/allowlist, `hw_id`), writes `approval = "APRV"`, then reboots into + the modified bootloader (`Adafruit_nRF52_Bootloader_OTAFIX`). The bootloader: + 1. **scans flash for `MAGIC`** to find the staged `.mota` (it must NOT trust any stored size), + 2. re-checks `TRAILER`, `image_hash`, `approval == "APRV"`, and that the delta's `base_hash` equals the + running firmware's `EndF.body_hash` (recomputed by scanning for `EndF` — never trust `bank_0_size`), + 3. applies the in-place codec over the app region and boots only if the result hashes to `image_hash`. + +The signature proves author authenticity; `approval` proves local owner consent — both required to apply. + +> **Bootloader testing note:** always test apply with a *real different* image (base ≠ target). A same-image +> (X→X) "delta" trivially reproduces the target and gives a false positive. + +--- + +## 13. Versioning of this spec + +`format_ver = 2`. A parser accepts exactly this value and rejects anything else — there is one container +format, fixed-layout, and no compatibility shims to carry. If the format ever needs to change, bump +`format_ver`; the multihash `hash_algo` separately allows swapping the digest family without a format +bump. Unknown `codec_id` / `ota_msg_type` values are ignored (a node simply won't fetch what it can't apply). diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md new file mode 100644 index 0000000000..0235be232f --- /dev/null +++ b/docs/ota_user_guide.md @@ -0,0 +1,241 @@ +# Updating your node over the air (OTA) — user guide + +This guide is for **node operators**: how to update your MeshCore device's firmware over the radio, in +plain language. No cables, no programmer — your node can download a new firmware from a neighbour and +install it. (For the technical wire format, see [the OTA protocol spec](ota_protocol.md).) + +> **Is my node supported?** OTA works on **ESP32** boards (e.g. Heltec V3) and on the **RAK4631** (nRF52, +> which needs the special MeshCore bootloader). Other boards build fine but can't self-update yet. + +--- + +## The important part first: it's safe + +- **Nothing installs by itself.** Your node can *discover* and *download* an update in the background, but + it only **installs** when you say so (unless you deliberately turn on auto-install — see below). +- **Bad downloads can't sneak in.** Every piece of the firmware is checked against a cryptographic + fingerprint as it arrives, and the whole image is verified again before install. A corrupt or tampered + download is rejected, not installed. +- **You choose who to trust.** Updates can be *signed* by their author. You can tell your node to only + auto-install firmware signed by keys you've added. +- **It won't disrupt your mesh.** OTA traffic is always the **lowest priority** — your node only spends + spare airtime on it. Messages and routing always come first; a busy node simply updates later. Think of + it as *"eventually upgradable."* +- **It can recover.** If an install ever fails, the node falls back to a safe recovery mode (you can + re-flash a known-good firmware over USB) — it won't be left bricked. + +--- + +## How to talk to your node + +Connect to your node's **console** — usually a USB serial terminal at **115200 baud** (or whatever tool +you already use to manage the node). You type `ota ...` commands and the node replies in plain words. + +The commands have short, friendly names (and most accept aliases, so you don't have to remember exact +spelling): type **`ota help`** any time to see the list, or just **`ota`** for a status summary. + +--- + +## Common tasks + +### 1. See what I'm running and whether anything is going on + +``` +ota status +``` + +Shows your current firmware version, your node's update "target" (its hardware/role id), and whether a +download is in progress. + +### 2. Find updates available near me + +``` +ota ls +``` + +Your node asks around and lists the firmware updates other nodes nearby are offering, in plain words — +each with a **number**, its version, whether it's a full image or a small delta, how many nodes have it, +and how recently it was seen. For example: + +``` +Updates nearby (2 src) — `ota get <#>` to download: + 1) v1.2.3 delta [yours] 3n 5s + 2) v1.2.0 full [other hw] 1n 12s [downloading] +``` + +Each row shows the version, full-vs-delta, **whether it fits your node**, how many nodes have it, and how +long ago it was seen. The fit marker: + +- **[yours]** — built for your exact hardware **and** role; safe to install. +- **[other hw]** — a different board or role (e.g. a companion image, or another board). Don't install it. +- **[?]** — can't tell (a build with no target id set, e.g. a bare IDE build rather than a release build). + +Run it again after a few seconds — discovery happens in the background, so the list fills in. Nothing is +downloaded yet; this is just looking around. (`ota neighbors` / `ota updates` also work.) + +### 3. Download an update + +Pick one from the list by its **number**: + +``` +ota get 1 +``` + +The node starts fetching it in the background, **at low priority**, a piece at a time — possibly from +several neighbours at once. Check progress any time with `ota status` (you'll see it climb, e.g. +`download: downloading 120/525 (23%)`). You can keep using your node normally meanwhile. + +To **stop** a download you no longer want: + +``` +ota cancel +``` + +### 4. Install a downloaded update + +Once `ota status` shows the download is **ready to install**: + +``` +ota install +``` + +The node verifies the firmware one last time, and if everything checks out it installs it and **reboots +into the new version**. If the check fails, it tells you why and does **not** install. (If you haven't +added the signer's key, an unsigned/untrusted image will only install with this explicit command — never +automatically.) + +After it reboots, run `ota status` to confirm the new version. + +### 5. If something goes wrong + +- A download that stalls or gets interrupted just **resumes** later, or you can `ota cancel` and try again. +- If an **install** fails, the node won't boot a broken image — it lands in **recovery mode**: + - **RAK4631 / nRF52:** it appears as a USB drive; drag a known-good firmware `.uf2` onto it to recover. + - **ESP32:** it keeps the previous firmware in the other slot and rolls back. +- When in doubt, you can always re-flash over USB the normal way. + +--- + +## Optional: let it update automatically + +By default your node only *discovers* updates — it won't download or install on its own. If you want more +automation (e.g. for a remote node you can't easily reach), you can opt in. These settings are saved. + +``` +ota config autofetch any # auto-DOWNLOAD any compatible update for this node (still won't install) +ota config autofetch signed # auto-download only signed updates +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 hops 3 # how far OTA travels: accept from / relay up to N repeater hops (default 3) +ota config hops 0 # only exchange OTA with directly-connected nodes (never relay) + +ota config # show the current settings +``` + +Recommended for most people: leave both **off** and update by hand. Use `autoinstall trusted` only once +you've added the signer's key (next section) and you trust them to push updates unattended. + +--- + +## Optional: only trust updates from specific people + +If you'll use auto-install, tell your node which signing keys to trust. The firmware author shares their +**public** key (a hex string); you add it: + +``` +ota key add # trust this signer +ota key list # show trusted signers +ota key rm # stop trusting one +``` + +Only updates signed by a trusted key are eligible for auto-install. Manual `ota install` still lets you +install anything yourself, on your own responsibility. + +--- + +## Sharing updates with others (advanced) + +### Relay a folder of firmware from a computer + +If your node is connected to a computer (e.g. a gateway on a Raspberry Pi), it can **hand out** a whole +folder of firmware files to the mesh — without storing them itself. Useful for seeding a new release to a +remote area. + +1. Put the firmware files (`.mota` files — see below) in a folder on the computer. +2. Build the helper tool once (`tools/motatool/`), then point it at your node and the folder — over the + node's **USB serial**, or over **WiFi** if it's an ESP32 companion on your network: + ``` + cmake -S tools/motatool -B tools/motatool/build && cmake --build tools/motatool/build + # over USB serial: + ./tools/motatool/build/motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v + # …or over WiFi (ESP32 companion): the seeder is on a DEDICATED port (5001), separate from the + # phone-app port (5000), so a phone can stay connected while you serve: + ./tools/motatool/build/motatool serve --dir ./my_firmware/ --tcp 192.168.1.50:5001 -v + ``` + It answers the node's requests; your node then advertises those updates to neighbours, who can + `ota get` them like any other. (A WiFi node prints its IP + seeder port to the serial log on connect. + Details: [tools/motatool/README.md](../tools/motatool/README.md).) + +To stop, just stop the daemon — over WiFi the node auto-detaches when the connection closes; over USB you +can also run `ota folder off` on the node. `ota folder` on its own lists what your node is offering. + +### Everyone helps share + +You don't have to be a gateway to help. Once **any** node finishes downloading an update, it automatically +offers it to *its* neighbours too. So a new firmware spreads outward node-to-node, instead of everyone +hammering the one node that had it first — and no node is ever overloaded, because all of this stays +lowest-priority. + +--- + +## Where firmware files come from + +OTA distributes **`.mota`** files — a packaged, verifiable firmware image (full image or a small "delta" +that only contains what changed). You get them by: + +- **Downloading a build.** This fork publishes a rolling **`dev-latest`** release on GitHub with the + current firmware for many boards, each accompanied by a `.full.mota` and a tiny `.delta.mota`. Grab the + one for your board to test. +- **Building your own** with the `mota` packaging tool — see [tools/mota/README.md](../tools/mota/README.md) + (this is for people distributing updates, not everyday operators). + +--- + +## Quick reference + +| I want to… | Command | +|---|---| +| List all commands | `ota help` | +| See my firmware + any download | `ota status` (or just `ota`) | +| Find updates nearby | `ota ls` | +| Download update #1 | `ota get 1` | +| Cancel a download | `ota cancel` | +| Install a finished download | `ota install` | +| Turn on auto-download | `ota config autofetch any` | +| Turn on auto-install (trusted only) | `ota config autoinstall trusted` | +| Trust a signer | `ota key add ` | +| Relay a folder (gateway) | `ota folder on` + the seeder daemon | +| List what I'm offering | `ota folder` | + +(Older names still work too: `neighbors`/`updates` = `ls`, `pull` = `get`, `applydelta`/`apply` = `install`, `drop`/`stop` = `cancel`.) + +--- + +## A few terms + +- **Firmware** — the software running your node. Updating it can add features or fix bugs. +- **`.mota`** — a packaged firmware update file, with built-in integrity checks. +- **Target** — your node's hardware + role identity. Your node only auto-fetches updates built for the + same target, so it won't grab firmware meant for a different board. +- **Delta** — a small update containing only the changes from your current firmware (faster to send than a + full image). Your node rebuilds the complete firmware from it and verifies the result before installing. +- **Signed** — the update carries the author's cryptographic signature, so you can verify who made it. + +For the full technical details (the file format and the radio protocol), see +[the OTA protocol spec](ota_protocol.md). diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index f4190f30ac..9539ad3db2 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -12,7 +12,7 @@ #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "v1.16.0" +#define FIRMWARE_VERSION "v1.17.0" #endif #if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM) diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ef9b6bfca4..6844724b6d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -111,6 +111,81 @@ void halt() { unsigned long last_wifi_reconnect_attempt = 0; #endif +/* WIFI OTA SEEDER — relay a host folder of .mota over WiFi (motatool `serve --tcp`), on a DEDICATED port + separate from the companion (TCP_PORT), so a phone app stays connected while motatool feeds updates. */ +#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA) + #include + #include + #ifndef OTA_SEEDER_TCP_PORT + #define OTA_SEEDER_TCP_PORT 5001 + #endif + static WiFiServer ota_seeder_server(OTA_SEEDER_TCP_PORT); + static WiFiClient ota_seeder_client; // the live seeder connection (reused) + static mesh::ota::SerialMotaSource ota_seeder_source(ota_seeder_client, 3000); + static bool ota_seeder_attached = false; + + // Accept one motatool connection at a time; while connected, register its folder as a serve source so + // the node advertises + relays it over LoRa. Drop the source the moment the connection closes. + static void ota_seeder_loop() { + 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"); + } + WiFiClient c = ota_seeder_server.available(); + if (c) { + 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 + } + } + } +#endif + +/* WIFI OTA CONSOLE — a tiny text CLI for OTA over WiFi. A WiFi companion has no serial text console, so + without this its OTA is only reachable through the phone app. Connect with e.g. `nc 5002` and type + `ota status` / `ota ls` / `ota announce` / ... — one client at a time, on a DEDICATED port separate from + the companion (5000) and the seeder (5001). */ +#if defined(ESP32) && defined(WIFI_SSID) && defined(ENABLE_OTA) + #include // mesh::ota::handle_ota_command(line, reply, board) + #ifndef OTA_CONSOLE_TCP_PORT + #define OTA_CONSOLE_TCP_PORT 5002 + #endif + static WiFiServer ota_console_server(OTA_CONSOLE_TCP_PORT); + static WiFiClient ota_console_client; + static char ota_console_line[128]; + static uint8_t ota_console_len = 0; + + static void ota_console_loop() { + if (!ota_console_client || !ota_console_client.connected()) { + WiFiClient c = ota_console_server.available(); + if (c) { ota_console_client = c; ota_console_len = 0; + ota_console_client.print("OTA console — type `ota ...` (e.g. ota status / ota ls / ota announce)\r\n> "); } + return; + } + while (ota_console_client.available()) { + char ch = (char)ota_console_client.read(); + if (ch == '\r' || ch == '\n') { + if (ota_console_len == 0) continue; // ignore blanks / the CRLF pair + ota_console_line[ota_console_len] = 0; + char reply[160]; reply[0] = 0; + if (!mesh::ota::handle_ota_command(ota_console_line, reply, board)) + strcpy(reply, "only `ota ...` commands are supported on this console"); + ota_console_client.print(" -> "); ota_console_client.print(reply); ota_console_client.print("\r\n> "); + ota_console_len = 0; + } else if (ota_console_len < sizeof(ota_console_line) - 1) { + ota_console_line[ota_console_len++] = ch; + } + } + } +#endif + void setup() { Serial.begin(115200); @@ -208,13 +283,24 @@ void setup() { WIFI_DEBUG_PRINTLN("WiFi disconnected. Flagging for reconnect..."); wifi_needs_reconnect = true; } else if (event == ARDUINO_EVENT_WIFI_STA_GOT_IP) { - WIFI_DEBUG_PRINTLN("WiFi connected successfully!"); + WIFI_DEBUG_PRINTLN("connected! IP %s (companion app on :%d)", + WiFi.localIP().toString().c_str(), TCP_PORT); wifi_needs_reconnect = false; } }); WiFi.begin(WIFI_SSID, WIFI_PWD); + // Disable WiFi modem power-save: its periodic modem/light-sleep stalls the SX1262 SPI+DIO servicing, + // which makes the LoRa radio go deaf (no TX/RX) while WiFi is associated. Required for LoRa+WiFi to + // coexist on this ESP32 — the small extra idle current is well worth a working radio. + WiFi.setSleep(false); serial_interface.begin(TCP_PORT); + #ifdef ENABLE_OTA + ota_seeder_server.begin(); // dedicated OTA seeder port for `motatool serve --tcp` (relay over LoRa) + WIFI_DEBUG_PRINTLN("OTA seeder listening on :%d (motatool serve --tcp)", OTA_SEEDER_TCP_PORT); + ota_console_server.begin(); // dedicated OTA text-console port (`nc 5002` -> `ota ...`) + WIFI_DEBUG_PRINTLN("OTA console listening on :%d (nc %d, type `ota ...`)", OTA_CONSOLE_TCP_PORT, OTA_CONSOLE_TCP_PORT); + #endif #elif defined(BLE_PIN_CODE) serial_interface.begin(BLE_NAME_PREFIX, the_mesh.getNodePrefs()->node_name, the_mesh.getBLEPin()); #elif defined(SERIAL_RX) @@ -257,6 +343,10 @@ void loop() { } #if defined(ESP32) && defined(WIFI_SSID) + #ifdef ENABLE_OTA + ota_seeder_loop(); // accept/drop a motatool `serve --tcp` connection on the dedicated seeder port + ota_console_loop(); // service the OTA text console (port 5002) + #endif // Safely attempt to reconnect every 10 seconds if flagged if (wifi_needs_reconnect && (millis() - last_wifi_reconnect_attempt > 10000)) { WIFI_DEBUG_PRINTLN("Attempting manual WiFi reconnect..."); diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index ca4cfad27d..17df619c5f 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -926,8 +926,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc memset(default_scope.key, 0, sizeof(default_scope.key)); } +// OTA mesh-integration (receive/begin/loop) is centralized in mesh::Mesh — no per-example wiring. + void MyMesh::begin(FILESYSTEM *fs) { - mesh::Mesh::begin(); + mesh::Mesh::begin(); // also starts OTA (ota_ctx().begin) for all roles _fs = fs; // load persisted prefs _cli.loadPrefs(_fs); @@ -1268,7 +1270,7 @@ void MyMesh::loop() { bridge.loop(); #endif - mesh::Mesh::loop(); + mesh::Mesh::loop(); // also drives the OTA fetch loop (centralized in mesh::Mesh) if (next_flood_advert && millisHasNowPassed(next_flood_advert)) { mesh::Packet *pkt = createSelfAdvert(); diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index fb091a4cf6..6a7aa66bd6 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -2,6 +2,9 @@ #include #include +#if defined(ENABLE_OTA) + #include +#endif #include #include @@ -73,7 +76,7 @@ struct NeighbourInfo { #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.16.0" + #define FIRMWARE_VERSION "v1.17.0" #endif #define FIRMWARE_ROLE "repeater" @@ -175,6 +178,7 @@ class MyMesh : public mesh::Mesh, public CommonCLICallbacks { void onPeerDataRecv(mesh::Packet* packet, uint8_t type, int sender_idx, const uint8_t* secret, uint8_t* data, size_t len) override; bool onPeerPathRecv(mesh::Packet* packet, int sender_idx, const uint8_t* secret, uint8_t* path, uint8_t path_len, uint8_t extra_type, uint8_t* extra, uint8_t extra_len) override; void onControlDataRecv(mesh::Packet* packet) override; + // OTA mesh-integration is centralized in mesh::Mesh (no per-example onOtaRecv / send adapter / tick). void sendFloodReply(mesh::Packet* packet, unsigned long delay_millis, uint8_t path_hash_size); diff --git a/examples/simple_room_server/MyMesh.h b/examples/simple_room_server/MyMesh.h index e9e53ec919..671c0df8d8 100644 --- a/examples/simple_room_server/MyMesh.h +++ b/examples/simple_room_server/MyMesh.h @@ -31,7 +31,7 @@ #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.16.0" + #define FIRMWARE_VERSION "v1.17.0" #endif #ifndef LORA_FREQ diff --git a/examples/simple_sensor/SensorMesh.h b/examples/simple_sensor/SensorMesh.h index 1d65b8772b..7cd53810ea 100644 --- a/examples/simple_sensor/SensorMesh.h +++ b/examples/simple_sensor/SensorMesh.h @@ -38,7 +38,7 @@ #endif #ifndef FIRMWARE_VERSION - #define FIRMWARE_VERSION "v1.16.0" + #define FIRMWARE_VERSION "v1.17.0" #endif #define FIRMWARE_ROLE "sensor" diff --git a/platformio.ini b/platformio.ini index e16f7b8304..5523ce7f3f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -58,11 +58,24 @@ build_src_filter = extends = arduino_base platform = platformio/espressif32@6.11.0 monitor_filters = esp32_exception_decoder +; OTA is available on every ESP32 variant (A/B via esp_ota; the detools-sequential apply decodes into the +; inactive slot). The received .mota is staged in the INACTIVE OTA slot (OtaStoreFlashEsp32), not RAM, so +; deltas/full images of any size fetch over the air (RX-safe, sector-coalesced). The build always includes +; it; for `applydelta` to actually run, the board must use a dual-app/OTA partition table +; (board_build.partitions) with two app slots + otadata (boards without one fetch-refuse cleanly). +; pio_endf appends the EndF self-identity trailer. (nRF52 single-slot OTA is enabled per-board, not here — +; it needs a custom bootloader; RP2040/STM32 have no A/B path yet.) extra_scripts = merge-bin.py + post:tools/mota/pio_endf.py build_flags = ${arduino_base.build_flags} -D ESP32_PLATFORM + -D ENABLE_OTA=1 + -D OTA_FLASH_STORE=1 + -D OTA_FOLDER_SERIAL ; `ota folder on` relays a host folder of .mota over the USB console (no extra HW) ; -D ESP32_CPU_FREQ=80 ; change it to your need build_src_filter = ${arduino_base.build_src_filter} + + + + [esp32_ota] lib_deps = @@ -93,6 +106,27 @@ build_flags = ${arduino_base.build_flags} lib_deps = ${arduino_base.lib_deps} https://github.com/oltaco/CustomLFS#0.2.2 + +; Shared OTA recipe for board=rak4631-hardware nRF52 variants. nRF52840 has no A/B slot, so the update is +; applied in place by the custom OTAFIX bootloader (in-place detools); the app only stages + verifies + +; approves it. Reuses the RAK4631 flash layout (src/helpers/ota/OtaFlashLayout_nrf52.h; FS_START 0xD4000 +; is the safe staging ceiling for every RAK4631 role/ldscript). The device must run the matching OTAFIX +; bootloader for `applydelta` to actually apply. (detools.c is intentionally NOT built into the app — +; only the bootloader decodes.) The [rak4631] base defines this same recipe inline because it carries +; board-specific extras (fix_bsec_lib.py, BSEC lib) and a fixed post-script order; the simpler RAK4631 +; variants below just extend this section. +[rak4631_hw] +extends = nrf52_base +extra_scripts = ${nrf52_base.extra_scripts} + post:tools/mota/pio_endf.py +build_flags = ${nrf52_base.build_flags} + -D ENABLE_OTA=1 + -D OTA_FLASH_STORE=1 + -D OTA_FOLDER_SERIAL ; `ota folder on` relays a host folder of .mota over the USB console (no extra HW) +build_src_filter = ${nrf52_base.build_src_filter} + + +lib_deps = ${nrf52_base.lib_deps} + ; ----------------- RP2040 --------------------- [rp2040_base] @@ -164,5 +198,11 @@ test_build_src = yes build_src_filter = -<*> +<../src/Utils.cpp> + +<../src/helpers/ota/MerkleTree.cpp> + +<../src/helpers/ota/MotaContainer.cpp> + +<../src/helpers/ota/FirmwareInfo.cpp> + +<../src/helpers/ota/OtaProtocol.cpp> + +<../src/helpers/ota/OtaManager.cpp> + +<../src/helpers/ota/detools/detools.c> lib_deps = google/googletest @ 1.17.0 diff --git a/src/Mesh.cpp b/src/Mesh.cpp index e9b92262ce..788c0f0cf5 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -1,14 +1,126 @@ #include "Mesh.h" //#include +#if defined(ENABLE_OTA) +#include "helpers/ota/OtaContext.h" // OTA mesh-integration is centralized here so every role gets it +#include "helpers/ota/OtaProtocol.h" // decode_adv -> the `ota neighbors` discovery table +#include "helpers/ota/OtaSelf.h" // ota_self_firmware -> auto-advertise our own image +#ifndef OTA_ANNOUNCE_BOOT_MS +#define OTA_ANNOUNCE_BOOT_MS 8000UL // first self-advert ~8 s after boot (settled, but quick to discover) +#endif +#ifndef OTA_ANNOUNCE_BURST +#define OTA_ANNOUNCE_BURST 4 // a few closely-spaced boot adverts so co-booting peers catch one +#endif +#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 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 namespace mesh { +#if defined(ENABLE_OTA) +// Adapter so the portable OtaManager can emit packets through the mesh (lowest priority, hop-capped). +void Mesh::otaSendAdapter(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) { + Mesh* m = (Mesh*)ctx; + Packet* p = m->createOtaPacket(msg, len); + if (p) m->sendOtaFlood(p); +} + +// Runtime OTA flood reach (`ota config hops`, persisted in NodePrefs): accept packets up to N hops away and +// relay those still under N hops. 0 = direct only. Overridable per-role by subclassing. +uint8_t Mesh::getOtaHopLimit() const { return ota::ota_ctx().manager.max_hops(); } +#endif + void Mesh::begin() { Dispatcher::begin(); +#if defined(ENABLE_OTA) + uint32_t my_tid = 0; + #ifdef MOTA_TARGET_ID + my_tid = (uint32_t)(MOTA_TARGET_ID); // sha2-256:4(env name), injected by build.sh + #endif + const char* my_hw = ""; + #ifdef MOTA_HW_ID + my_hw = MOTA_HW_ID; // human-readable hardware tag (per-variant), for the apply hw gate + #endif + ota::ota_ctx().begin(my_tid, Mesh::otaSendAdapter, this, my_hw); // also sets the platform apply codec + ota::ota_ctx().manager.set_seeder_id(self_id.pub_key); // node id (pubkey[0:4]) for advert seeder count + _next_ota_announce = futureMillis(OTA_ANNOUNCE_BOOT_MS); // advertise our own fw shortly after boot +#endif } void Mesh::loop() { Dispatcher::loop(); +#if defined(ENABLE_OTA) + // Deferred apply-reboot: a verified `ota applydelta` approves the update but does NOT reboot inline, + // so its "verified; applying" reply can be delivered first (over LoRa that reply is the operator's + // only confirmation the apply started). Reboot once that reply has actually been transmitted (the + // outbound queue drains) after a short grace to let it be queued, with a hard cap for a busy node + // whose queue never idles. + { + ota::OtaContext& oc = ota::ota_ctx(); + if (oc.apply_pending) { + if (oc.apply_at == 0) { + oc.apply_at = futureMillis(1500); + oc.apply_hard = futureMillis(15000); + } else if (millisHasNowPassed(oc.apply_at) && + (_mgr->getOutboundTotal() == 0 || millisHasNowPassed(oc.apply_hard))) { + ota::ota_reboot_to_apply(); // does not return + } + } + } + if (millisHasNowPassed(_next_ota_tick)) { + // one-shot on first tick: resume an interrupted fetch left staged in flash before a reboot. Only adopt + // a PARTIAL container (continue fetching the holes); a COMPLETE one is left for manual/auto-install, + // not re-adopted at boot. requestMissing() (inside resumeStaged) drives the rest via REQ/DATA. + if (!_ota_resumed) { + _ota_resumed = true; + ota::OtaContext& oc = ota::ota_ctx(); + if (oc.manager.fetchState() == ota::OtaManager::IDLE && oc.manager.resumeStaged(nullptr) + && oc.manager.fetchState() == ota::OtaManager::COMPLETE) { + oc.manager.reset_session(); // don't auto-adopt a complete staged container on boot + } + } + ota::ota_ctx().manager.set_clock(_ms->getMillis()); // for discovery jitter/ages + the pending-query timer + ota::ota_ctx().manager.loop(); // re-request still-missing OTA blocks + fire scheduled queries + _next_ota_tick = futureMillis(3000); + } + if (millisHasNowPassed(_next_ota_announce)) { // auto-advertise so peers discover us (tiny beacon) + ota::OtaContext& oc = ota::ota_ctx(); + 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); + } + { // auto-install (once per COMPLETE fetch): only signed images, and apply_fetched enforces trust + ota::OtaContext& oc = ota::ota_ctx(); + if (oc.manager.fetchState() != ota::OtaManager::COMPLETE) { + _ota_autoinstall_tried = false; + } else if (!_ota_autoinstall_tried && !oc.apply_pending + && oc.autoinstall == ota::OtaContext::AUTOINSTALL_TRUSTED + && oc.manager.fetched_is_signed()) { + _ota_autoinstall_tried = true; + char msg[100]; + oc.apply_fetched(msg); // arms + sets apply_pending only if signed & allowlisted; refused otherwise + } + } +#endif } bool Mesh::allowPacketForward(const mesh::Packet* packet) { @@ -313,6 +425,37 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } break; +#if defined(ENABLE_OTA) + case PAYLOAD_TYPE_OTA: { + uint8_t n = pkt->getPathHashCount(); // hops travelled to reach us (flood path-hash count) + // Accept-gate (duty-cycle horizon): ignore OTA from further than our hop limit — neither process nor + // relay it. 0 = only directly-received OTA. Runtime-tunable via `ota config hops`. + if (n > getOtaHopLimit()) break; + // ALWAYS process every accepted copy: OTA handlers are idempotent, and "eventually reliable" retries + // deliberately re-send IDENTICAL requests — if we gated processing on hasSeen(), the dedup would + // suppress those retries and the transfer could never recover from a lost reply. hasSeen() is used + // ONLY to avoid re-flooding the same packet more than once. + bool seen = _tables->hasSeen(pkt); + ota::ota_ctx().manager.set_clock(_ms->getMillis()); // discovery jitter/ages + ota::ota_ctx().manager.on_message(pkt->payload, pkt->payload_len); // central OTA receive (beacon/query/ + // have/manifest/data/proof; all roles) + ota::ota_ctx().track_session(ota::ota_ctx().manager.fetchState(), _ms->getMillis()); + onOtaRecv(pkt); // optional per-example hook + // Re-flood at the LOWEST priority and only while still under the hop limit, so OTA never competes with + // mesh traffic. The free-pool guard keeps heavy OTA from monopolising the shared packet pool — dropping + // a relay is safe (OTA is best-effort; the source retries). + if (!seen && pkt->isRouteFlood() && !pkt->isMarkedDoNotRetransmit() + && n < getOtaHopLimit() + && (n + 1) * pkt->getPathHashSize() <= MAX_PATH_SIZE + && _mgr->getFreeCount() > OTA_FWD_MIN_FREE + && allowPacketForward(pkt)) { + self_id.copyHashTo(&pkt->path[n * pkt->getPathHashSize()], pkt->getPathHashSize()); + pkt->setPathHashCount(n + 1); + action = ACTION_RETRANSMIT_DELAYED(OTA_TX_PRIORITY, getRetransmitDelay(pkt)); + } + break; + } +#endif default: MESH_DEBUG_PRINTLN("%s Mesh::onRecvPacket(): unknown payload type, header: %d", getLogDateTime(), (int) pkt->header); // Don't flood route unknown packet types! action = routeRecvPacket(pkt); @@ -623,6 +766,29 @@ Packet* Mesh::createControlData(const uint8_t* data, size_t len) { return packet; } +#if defined(ENABLE_OTA) +Packet* Mesh::createOtaPacket(const uint8_t* data, size_t len) { + if (len > sizeof(Packet::payload)) return NULL; + Packet* packet = obtainNewPacket(); + if (packet == NULL) { + MESH_DEBUG_PRINTLN("%s Mesh::createOtaPacket(): error, packet pool empty", getLogDateTime()); + return NULL; + } + packet->header = (PAYLOAD_TYPE_OTA << PH_TYPE_SHIFT); // ROUTE_TYPE_* set by sendOtaFlood + memcpy(packet->payload, data, len); + packet->payload_len = len; + return packet; +} + +void Mesh::sendOtaFlood(Packet* packet, uint32_t delay_millis) { + packet->header &= ~PH_ROUTE_MASK; + packet->header |= ROUTE_TYPE_FLOOD; + packet->setPathHashSizeAndCount(1, 0); + _tables->hasSeen(packet); // mark as sent, in case it floods back to us + sendPacket(packet, OTA_TX_PRIORITY, delay_millis); +} +#endif + void Mesh::sendFlood(Packet* packet, uint32_t delay_millis, uint8_t path_hash_size) { if (packet->getPayloadType() == PAYLOAD_TYPE_TRACE) { MESH_DEBUG_PRINTLN("%s Mesh::sendFlood(): TRACE type not suspported", getLogDateTime()); diff --git a/src/Mesh.h b/src/Mesh.h index 932541db55..04fbcfe111 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -2,6 +2,19 @@ #include +#if defined(ENABLE_OTA) + // OTA-over-LoRa: lowest TX priority (selected only after all real traffic). The hop limit is runtime- + // tunable (OtaManager::max_hops(), `ota config hops`); the default lives in OtaManager.h. + #ifndef OTA_TX_PRIORITY + #define OTA_TX_PRIORITY 250 + #endif + // An OTA flood is relayed only while this many pool slots stay free, so heavy OTA (best-effort, low- + // priority) can never monopolise the shared packet pool and starve real traffic. + #ifndef OTA_FWD_MIN_FREE + #define OTA_FWD_MIN_FREE 4 + #endif +#endif + namespace mesh { class GroupChannel { @@ -144,6 +157,26 @@ class Mesh : public Dispatcher { */ virtual void onRawDataRecv(Packet* packet) { } +#if defined(ENABLE_OTA) + /** + * \brief An OTA-over-LoRa packet (PAYLOAD_TYPE_OTA) has been received. Subclasses forward the + * payload bytes to their OtaManager. See docs/ota_protocol.md. + */ + virtual void onOtaRecv(Packet* packet) { } + + /** \returns max OTA flood reach in hops — accept up to N, relay while < N (0=direct). `ota config hops`. */ + virtual uint8_t getOtaHopLimit() const; + + // OTA mesh-integration is centralized in Mesh::begin()/loop()/dispatch, so every role (repeater, + // companion, room, sensor, ...) gets fetch/serve/apply without per-example wiring. + static void otaSendAdapter(void* ctx, const uint8_t* msg, uint16_t len, bool flood); + unsigned long _next_ota_tick = 0; + unsigned long _next_ota_announce = 0; // auto-advertise our own fw: boot burst + every OTA_ANNOUNCE_INTERVAL + uint8_t _ota_announce_count = 0; // adverts sent so far (boot burst before settling to daily) + bool _ota_resumed = false; // one-shot: resumed an interrupted fetch staged in flash on boot + bool _ota_autoinstall_tried = false; // attempted auto-install for the current COMPLETE fetch +#endif + /** * \brief Perform search of local DB of matching GroupChannels. * \param channels OUT - store matching channels in this array, up to max_matches @@ -192,6 +225,13 @@ class Mesh : public Dispatcher { Packet* createPathReturn(const uint8_t* dest_hash, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createPathReturn(const Identity& dest, const uint8_t* secret, const uint8_t* path, uint8_t path_len, uint8_t extra_type, const uint8_t*extra, size_t extra_len); Packet* createRawData(const uint8_t* data, size_t len); + +#if defined(ENABLE_OTA) + // Build a PAYLOAD_TYPE_OTA packet from raw OTA message bytes (route set by sendOtaFlood). + Packet* createOtaPacket(const uint8_t* data, size_t len); + // Flood-send at the lowest priority (so OTA never competes with mesh traffic). + void sendOtaFlood(Packet* packet, uint32_t delay_millis = 0); +#endif Packet* createTrace(uint32_t tag, uint32_t auth_code, uint8_t flags = 0); Packet* createControlData(const uint8_t* data, size_t len); diff --git a/src/MeshCore.h b/src/MeshCore.h index 89e60b1f7e..e8270053e6 100644 --- a/src/MeshCore.h +++ b/src/MeshCore.h @@ -67,6 +67,27 @@ class MainBoard { virtual bool setLoRaFemLnaEnabled(bool enable) { return false; } virtual bool canControlLoRaFemLna() const { return false; } virtual bool isLoRaFemLnaEnabled() const { return false; } +#if defined(ENABLE_OTA) + // 4-byte build-target discriminator for OTA-over-LoRa (docs/ota_protocol.md §9). Default is the + // MOTA_TARGET_ID build flag injected by build.sh; 0 when unset (e.g. a bare IDE build). + virtual uint32_t getOtaTargetId() const { + #ifdef MOTA_TARGET_ID + return (uint32_t)(MOTA_TARGET_ID); + #else + return 0; + #endif + } + // Human-readable hardware tag (<=32 ASCII chars, e.g. "RAK4631") naming the hardware this firmware can + // boot on. Same tag == bootable-compatible; the OTA applier refuses a `.mota` whose hw_id differs (brick- + // safety). Defined per-variant via the MOTA_HW_ID build flag; "" when unset (then the check is skipped). + virtual const char* getOtaHwId() const { + #ifdef MOTA_HW_ID + return MOTA_HW_ID; + #else + return ""; + #endif + } +#endif // Power management interface (boards with power management override these) virtual bool isExternalPowered() { return false; } diff --git a/src/Packet.h b/src/Packet.h index c19d9e9d8f..e872a05029 100644 --- a/src/Packet.h +++ b/src/Packet.h @@ -28,6 +28,7 @@ namespace mesh { #define PAYLOAD_TYPE_TRACE 0x09 // trace a path, collecting SNR for each hop #define PAYLOAD_TYPE_MULTIPART 0x0A // packet is one of a set of packets #define PAYLOAD_TYPE_CONTROL 0x0B // a control/discovery packet +#define PAYLOAD_TYPE_OTA 0x0C // OTA-over-LoRa firmware distribution (see docs/ota_protocol.md) //... #define PAYLOAD_TYPE_RAW_CUSTOM 0x0F // custom packet as raw bytes, for applications with custom encryption, payloads, etc diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index c95e3e34b0..56201e99ff 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -4,6 +4,10 @@ #include "AdvertDataHelpers.h" #include "TxtDataHelpers.h" #include +#if defined(ENABLE_OTA) + #include "ota/OtaCli.h" + #include "ota/OtaContext.h" // persist/sync OTA policy + signer allowlist with NodePrefs +#endif #ifndef BRIDGE_MAX_BAUD #define BRIDGE_MAX_BAUD 115200 @@ -28,15 +32,35 @@ static bool isValidName(const char *n) { } void CommonCLI::loadPrefs(FILESYSTEM* fs) { + bool loaded = false; if (fs->exists("/com_prefs")) { - loadPrefsInt(fs, "/com_prefs"); // new filename + loadPrefsInt(fs, "/com_prefs"); loaded = true; // new filename } else if (fs->exists("/node_prefs")) { loadPrefsInt(fs, "/node_prefs"); savePrefs(fs); // save to new filename fs->remove("/node_prefs"); // remove old + loaded = true; } +#if defined(ENABLE_OTA) + if (loaded) syncOtaConfigFromPrefs(); // persisted OTA policy/keys -> OtaContext (else keep safe defaults) +#endif } +#if defined(ENABLE_OTA) +// Push the persisted OTA policy + signer allowlist into the running OtaContext (called after load). +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.manager.set_max_hops(_prefs->ota_max_hops); + c.autoinstall = _prefs->ota_autoinstall; + c.allow.clear(); + for (uint8_t i = 0; i < _prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++) + c.allow.add(_prefs->ota_signers[i]); +} +#endif + void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { #if defined(RP2040_PLATFORM) File file = fs->open(filename, "r"); @@ -93,7 +117,20 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { file.read((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.read((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.read((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + // OTA config (295+). Default first so older prefs files (which lack these) keep conservative + // 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 + _prefs->ota_max_hops = 3; // = OTA_HOP_LIMIT_DEFAULT; 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 + file.read((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428 + file.read((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 430 + // next: 431 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -125,6 +162,12 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->rx_boosted_gain = constrain(_prefs->rx_boosted_gain, 0, 1); // boolean _prefs->radio_fem_rxgain = constrain(_prefs->radio_fem_rxgain, 0, 1); // boolean _prefs->cad_enabled = constrain(_prefs->cad_enabled, 0, 1); // boolean + _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_max_hops > 8) _prefs->ota_max_hops = 3; // 0=direct only; cap absurd reach + if (_prefs->ota_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys file.close(); } @@ -190,7 +233,14 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->flood_max_advert, sizeof(_prefs->flood_max_advert)); // 292 file.write((uint8_t *)&_prefs->radio_fem_rxgain, sizeof(_prefs->radio_fem_rxgain)); // 293 file.write((uint8_t *)&_prefs->cad_enabled, sizeof(_prefs->cad_enabled)); // 294 - // next: 295 + file.write((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 295 + file.write((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 296 + 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 + file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428 + file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 430 + // next: 431 file.close(); } @@ -312,6 +362,23 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re sprintf(reply, "%s (Build: %s)", _callbacks->getFirmwareVer(), _callbacks->getBuildDate()); } else if (memcmp(command, "board", 5) == 0) { sprintf(reply, "%s", _board->getManufacturerName()); +#if defined(ENABLE_OTA) + } else if (memcmp(command, "ota", 3) == 0 && (command[3] == 0 || command[3] == ' ')) { + mesh::ota::handle_ota_command(command, reply, *_board); + if (mesh::ota::ota_ctx().config_dirty) { // a policy/key changed via the CLI -> persist it + 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_max_hops = c.manager.max_hops(); + _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++) + memcpy(_prefs->ota_signers[i], c.allow.get(i), 32); + _callbacks->savePrefs(); + c.config_dirty = false; + } +#endif } else if (memcmp(command, "sensor get ", 11) == 0) { const char* key = command + 11; const char* val = _sensors->getSettingByKey(key); diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index f3abcf4772..bf9a1861fc 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -65,6 +65,14 @@ struct NodePrefs { // persisted to file uint8_t path_hash_mode; // which path mode to use when sending uint8_t loop_detect; uint8_t cad_enabled; // hardware Channel Activity Detection before TX (boolean) + // OTA config (persisted; synced to OtaContext on load, written on change). 0 = conservative defaults. + uint8_t ota_autofetch; // OtaManager AUTOFETCH_* (0=off, 1=any-compatible, 2=signed-only) + uint8_t ota_autoinstall; // OtaContext AUTOINSTALL_* (0=off, 1=trusted-only) + 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) + uint8_t ota_max_hops; // OTA flood reach in hops; 0=direct only. Default 3 (runtime-tunable) }; class CommonCLICallbacks { @@ -129,6 +137,9 @@ class CommonCLI { mesh::RTCClock* getRTCClock() { return _rtc; } void savePrefs(); void loadPrefsInt(FILESYSTEM* _fs, const char* filename); +#if defined(ENABLE_OTA) + void syncOtaConfigFromPrefs(); // persisted OTA policy + signer allowlist -> running OtaContext +#endif void handleRegionCmd(char* command, char* reply); void handleGetCmd(uint32_t sender_timestamp, char* command, char* reply); diff --git a/src/helpers/ota/BlockBitmap.h b/src/helpers/ota/BlockBitmap.h new file mode 100644 index 0000000000..c22835f5bf --- /dev/null +++ b/src/helpers/ota/BlockBitmap.h @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +// Block-availability helpers (docs/ota_protocol.md §7). +// +// Availability is *derived* from the staged manifest's leaves[]: block i is present iff its 4-byte +// leaf slot is non-erased (!= FF FF FF FF). No separate persistent structure. A compact bitmap +// (1 bit/block) is used on the wire (OTA_HAVE) and as an in-RAM cache. All ops are caller-buffer +// based; no allocation. + +namespace mesh { +namespace ota { + +inline bool leaf_present(const uint8_t* leaves, uint32_t i) { + const uint8_t* p = leaves + (size_t)i * 4; + return !(p[0] == 0xFF && p[1] == 0xFF && p[2] == 0xFF && p[3] == 0xFF); +} + +inline uint32_t bitmap_bytes(uint32_t block_count) { return (block_count + 7) / 8; } + +inline bool bitmap_get(const uint8_t* bm, uint32_t i) { + return (bm[i >> 3] >> (i & 7)) & 1; +} + +inline void bitmap_set(uint8_t* bm, uint32_t i, bool v) { + uint8_t mask = (uint8_t)(1u << (i & 7)); + if (v) bm[i >> 3] |= mask; else bm[i >> 3] &= (uint8_t)~mask; +} + +// Build a bitmap (caller buffer >= bitmap_bytes(count)) from leaves[]. +inline void leaves_to_bitmap(const uint8_t* leaves, uint32_t count, uint8_t* bm_out) { + memset(bm_out, 0, bitmap_bytes(count)); + for (uint32_t i = 0; i < count; i++) + if (leaf_present(leaves, i)) bitmap_set(bm_out, i, true); +} + +inline uint32_t count_present(const uint8_t* leaves, uint32_t count) { + uint32_t n = 0; + for (uint32_t i = 0; i < count; i++) if (leaf_present(leaves, i)) n++; + return n; +} + +inline bool all_present(const uint8_t* leaves, uint32_t count) { + for (uint32_t i = 0; i < count; i++) if (!leaf_present(leaves, i)) return false; + return true; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/FirmwareInfo.cpp b/src/helpers/ota/FirmwareInfo.cpp new file mode 100644 index 0000000000..c58cbfcc3c --- /dev/null +++ b/src/helpers/ota/FirmwareInfo.cpp @@ -0,0 +1,42 @@ +#include "FirmwareInfo.h" +#include "Multihash.h" +#include "OtaByteIO.h" +#include + +namespace mesh { +namespace ota { + +bool find_self_firmware(const uint8_t* region, uint32_t region_len, + SelfFwInfo& out, bool verify_body) { + out = SelfFwInfo(); + if (!region || region_len < ENDF_LEN) return false; + + for (uint32_t off = 0; off + ENDF_LEN <= region_len; off++) { + if (region[off] != ENDF_MAGIC[0]) continue; // cheap pre-filter ('E') + if (memcmp(region + off, ENDF_MAGIC, 4) != 0) continue; + uint32_t body_len = rd_u32le(region + off + 4); + if (body_len != off) continue; // trailer must sit right after the body + + if (verify_body) { + uint8_t h[8]; + mh8(h, region, body_len); + if (memcmp(h, region + off + 8, 8) != 0) continue; // coincidental marker — keep scanning + } + out.valid = true; + out.endf_offset = off; + out.body_len = body_len; + out.image_len = off + ENDF_LEN; + memcpy(out.body_hash, region + off + 8, 8); + // Fixed 56-byte trailer: the self-describing identity follows body_hash at constant offsets + // (fw_version@16, target_id@20, hw_id@24..56). Zero/"" means "unknown". + out.fw_version = rd_u32le(region + off + 16); + out.target_id = rd_u32le(region + off + 20); + memcpy(out.hw_id, region + off + 24, 32); + out.hw_id[32] = 0; + return true; + } + return false; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/FirmwareInfo.h b/src/helpers/ota/FirmwareInfo.h new file mode 100644 index 0000000000..a08bdc7578 --- /dev/null +++ b/src/helpers/ota/FirmwareInfo.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include "OtaFormat.h" + +// Locate the EndF trailer in a firmware image to learn the running firmware's size + identity +// (docs/ota_protocol.md §2). Portable: operates on a contiguous, readable region — on nRF52/ESP32 +// the application flash is memory-mapped, so the region pointer is just (const uint8_t*)APP_BASE. + +namespace mesh { +namespace ota { + +struct SelfFwInfo { + bool valid = false; + uint32_t body_len = 0; // firmware body length (excludes the EndF trailer) + uint32_t image_len = 0; // body_len + ENDF_LEN (what a delta base / full image hashes over) + uint32_t endf_offset = 0; // offset of the "EndF" marker within the region (== body_len) + uint8_t body_hash[8] = {0}; // sha2-256:8 of the body (read from EndF; == a delta's base_hash) + // Self-describing identity — always present in the fixed 56-byte trailer (zero/"" means "unknown", + // e.g. a dev build with no dotted version). + uint32_t fw_version = 0; // packed MAJOR<<24|MINOR<<16|PATCH<<8|pre + uint32_t target_id = 0; // sha2-256:4(env) as uint32 — hw+role+partition (fetch routing) + char hw_id[33] = {0}; // readable hardware tag (NUL-terminated), e.g. "RAK4631" +}; + +// Scan `region[0..region_len)` for the firmware's EndF trailer. The body starts at offset 0, so the +// trailer's offset must equal its stored body_len — this uniquely identifies the running firmware's +// EndF even if a staged `.mota` (which contains its own embedded EndF) sits higher in the region. +// If `verify_body` is true the body hash is recomputed and must match (rules out coincidental markers). +bool find_self_firmware(const uint8_t* region, uint32_t region_len, + SelfFwInfo& out, bool verify_body = false); + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MerkleTree.cpp b/src/helpers/ota/MerkleTree.cpp new file mode 100644 index 0000000000..8aa1099abe --- /dev/null +++ b/src/helpers/ota/MerkleTree.cpp @@ -0,0 +1,110 @@ +#include "MerkleTree.h" +#include "Multihash.h" +#include + +namespace mesh { +namespace ota { + +void merkle_leaf(uint8_t out[4], const uint8_t* block, uint32_t block_len) { + mh4(out, block, block_len); +} + +void merkle_combine(uint8_t out[4], const uint8_t* left, const uint8_t* right) { + sha256_trunc2(out, 4, left, 4, right, 4); +} + +// Root via binary-counter / Merkle-Mountain-Range with right-to-left bagging. +// Equivalent to the level-by-level "pair adjacent, promote lone last (left||right)" reduction +// (verified against the reference implementation across many counts in the native tests). +void merkle_root(uint8_t out[4], const uint8_t* leaves, uint32_t count) { + if (count == 0) { memset(out, 0, 4); return; } + if (count == 1) { memcpy(out, leaves, 4); return; } + + uint8_t peaks[32][4]; + bool valid[32] = { false }; + + for (uint32_t i = 0; i < count; i++) { + uint8_t cur[4]; + memcpy(cur, leaves + (size_t)i * 4, 4); + uint32_t level = 0; + while (valid[level]) { // carry: combine with the pending peak at this level + merkle_combine(cur, peaks[level], cur); // peak is earlier (left), cur is right + valid[level] = false; + level++; + } + memcpy(peaks[level], cur, 4); + valid[level] = true; + } + + // bag peaks right-to-left: acc starts at the lowest set level (rightmost peak) + int level = 0; + while (level < 32 && !valid[level]) level++; + uint8_t acc[4]; + memcpy(acc, peaks[level], 4); + for (int l = level + 1; l < 32; l++) { + if (valid[l]) merkle_combine(acc, peaks[l], acc); // higher peak is left, acc is right + } + memcpy(out, acc, 4); +} + +bool merkle_verify(const uint8_t* block, uint32_t block_len, uint32_t index, + const uint8_t* siblings, uint8_t n_siblings, + const uint8_t root[4], uint32_t count) { + uint8_t leaf[4]; + merkle_leaf(leaf, block, block_len); + return merkle_verify_from_leaf(leaf, index, siblings, n_siblings, root, count); +} + +bool merkle_verify_from_leaf(const uint8_t leaf[4], uint32_t index, + const uint8_t* siblings, uint8_t n_siblings, + const uint8_t root[4], uint32_t count) { + if (count == 0 || index >= count) return false; + uint8_t h[4]; + memcpy(h, leaf, 4); + + uint32_t idx = index; + uint32_t n = count; + uint8_t p = 0; + while (n > 1) { + bool is_last_odd = (n & 1u) && (idx == n - 1); + if (!is_last_odd) { + if (p >= n_siblings) return false; + const uint8_t* sib = siblings + (size_t)p * 4; + p++; + if (idx & 1u) merkle_combine(h, sib, h); // odd index -> sibling on the left + else merkle_combine(h, h, sib); // even index -> sibling on the right + } + idx >>= 1; + n = (n + 1) >> 1; + } + return (p == n_siblings) && (memcmp(h, root, 4) == 0); +} + +uint8_t merkle_gen_proof(const uint8_t* leaves, uint32_t count, uint32_t index, + uint8_t* scratch, uint8_t* out_siblings) { + if (count == 0 || index >= count) return 0; + memcpy(scratch, leaves, (size_t)count * 4); + uint32_t n = count, idx = index; + uint8_t p = 0; + while (n > 1) { + bool is_last_odd = (n & 1u) && (idx == n - 1); + if (!is_last_odd) { + uint32_t s = (idx & 1u) ? idx - 1 : idx + 1; + memcpy(out_siblings + (size_t)p * 4, scratch + (size_t)s * 4, 4); + p++; + } + // reduce one level in place (parent m written from children 2m,2m+1; m <= i so it's safe) + uint32_t m = 0; + for (uint32_t i = 0; i < n; i += 2) { + if (i + 1 < n) merkle_combine(scratch + (size_t)m * 4, scratch + (size_t)i * 4, scratch + (size_t)(i + 1) * 4); + else memmove(scratch + (size_t)m * 4, scratch + (size_t)i * 4, 4); + m++; + } + idx >>= 1; + n = (n + 1) >> 1; + } + return p; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MerkleTree.h b/src/helpers/ota/MerkleTree.h new file mode 100644 index 0000000000..15987f5b12 --- /dev/null +++ b/src/helpers/ota/MerkleTree.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include + +// Merkle tree over PAYLOAD blocks, sha2-256:4 (4-byte) leaves/nodes. See docs/ota_protocol.md §6. +// +// Scheme: leaf = H(block); node = H(left || right); on an odd level the last node is promoted +// unchanged (no duplication). Root = single remaining node. +// +// No dynamic allocation: the root is computed with an O(log count) "binary counter" of partial +// peaks (<= 32 levels => 128 bytes of stack). Proofs carry only sibling digests; the left/right +// direction is derived from the block index + total count (no direction bits on the wire). + +namespace mesh { +namespace ota { + +// leaf digest of one payload block +void merkle_leaf(uint8_t out[4], const uint8_t* block, uint32_t block_len); + +// parent of two 4-byte children +void merkle_combine(uint8_t out[4], const uint8_t* left, const uint8_t* right); + +// root over `count` contiguous 4-byte leaf digests (leaves[count*4]). count >= 1. +void merkle_root(uint8_t out[4], const uint8_t* leaves, uint32_t count); + +// Verify that `block` is block `index` of a `count`-block payload whose tree has the given `root`. +// `siblings` is n_siblings contiguous 4-byte digests, ordered leaf->root (promoted levels omitted; +// left/right direction derived from index + count). +bool merkle_verify(const uint8_t* block, uint32_t block_len, uint32_t index, + const uint8_t* siblings, uint8_t n_siblings, + const uint8_t root[4], uint32_t count); + +// Same, but starting from a precomputed 4-byte leaf digest (skips the H(block) step). +bool merkle_verify_from_leaf(const uint8_t leaf[4], uint32_t index, + const uint8_t* siblings, uint8_t n_siblings, + const uint8_t root[4], uint32_t count); + +// Generate the proof (ordered sibling digests) for block `index`, for a server holding leaves[]. +// `scratch` must be >= count*4 bytes (working buffer); `out_siblings` >= 32*4 bytes. +// Returns the number of 4-byte siblings written. Output matches the wire form merkle_verify expects. +uint8_t merkle_gen_proof(const uint8_t* leaves, uint32_t count, uint32_t index, + uint8_t* scratch, uint8_t* out_siblings); + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MotaContainer.cpp b/src/helpers/ota/MotaContainer.cpp new file mode 100644 index 0000000000..282d41759d --- /dev/null +++ b/src/helpers/ota/MotaContainer.cpp @@ -0,0 +1,83 @@ +#include "MotaContainer.h" +#include "MerkleTree.h" +#include "Multihash.h" +#include "OtaByteIO.h" +#include + +namespace mesh { +namespace ota { + +bool MotaManifest::is_approved() const { + return approval && memcmp(approval, APPROVAL_YES, 4) == 0; +} + +// Fixed-layout parse (docs/ota_protocol.md §4): every field sits at a constant offset — base_hash(8), +// signer_pubkey(32) and signature(64) are ALWAYS present (zero-filled when not applicable), so there are +// no conditionals. Only leaves[]/payload (after `approval`) is variable, read by the caller. The signature +// always covers manifest[0, MOTA_SIGNED_LEN). Returns false on any over-read or bad format_ver. +static bool parse_manifest_fields(ByteReader& r, MotaManifest& out) { + out.format_ver = r.u8(); + if (out.format_ver != MOTA_FORMAT_VER) return false; + out.flags = r.u8(); + out.hash_algo = r.u8(); + out.target_id = r.u32(); + out.fw_version = r.u32(); + out.image_size = r.u32(); + out.payload_size = r.u32(); + out.block_size_log2 = r.u8(); + out.merkle_root = r.take(4); + out.image_hash = r.take(32); + out.codec_id = r.u8(); + out.hw_id = r.take(32); // 32-byte NUL-padded hardware tag (signed) + out.base_hash = r.take(8); // always present (zero for a full image) + out.signer_pubkey = r.take(32); // always present (zero when unsigned) + out.signed_len = MOTA_SIGNED_LEN; // signature always covers manifest[0, 129) + out.signature = r.take(64); // always present (zero when unsigned) + out.approval = r.take(4); + if (!r.ok) return false; + if (out.block_size_log2 == 0 || out.block_size_log2 > 24 || out.payload_size == 0) return false; + out.block_count = (out.payload_size + out.block_size() - 1) / out.block_size(); + // block_idx is uint16 on the wire; capping here also keeps block_count*4 (leaves length) from overflowing. + return out.block_count != 0 && out.block_count <= 0xFFFFu; +} + +bool mota_parse(const uint8_t* buf, uint32_t len, MotaManifest& out) { + out = MotaManifest(); + if (len < 4 + 4 + 5) return false; + if (memcmp(buf, MOTA_MAGIC, 4) != 0) return false; + if (memcmp(buf + len - 5, MOTA_TRAILER, 5) != 0) return false; + if (rd_u32le(buf + 4) != len) return false; // MOTA_TOTAL_SIZE must equal the actual length + + ByteReader r(buf, len - 5); // everything up to (not incl.) the trailer + r.skip(4 + 4); // MAGIC + MOTA_TOTAL_SIZE (already validated) + out.manifest_start = buf + 8; + if (!parse_manifest_fields(r, out)) return false; + out.leaves = r.take(out.block_count * 4); + out.payload = r.take(out.payload_size); + if (!r.ok) return false; + return r.pos() == len - 5; // payload must end exactly at the trailer +} + +bool mota_parse_manifest(const uint8_t* mf, uint32_t len, MotaManifest& out) { + out = MotaManifest(); + out.manifest_start = mf; + ByteReader r(mf, len); // a standalone manifest = container bytes [8, leaves) + return parse_manifest_fields(r, out); +} + +bool mota_check_root(const MotaManifest& m) { + if (!m.leaves || m.block_count == 0) return false; + uint8_t root[4]; + merkle_root(root, m.leaves, m.block_count); + return memcmp(root, m.merkle_root, 4) == 0; +} + +bool mota_check_image_hash_full(const MotaManifest& m) { + if (!m.is_full() || !m.payload || !m.image_hash) return false; + uint8_t h[32]; + mh32(h, m.payload, m.payload_size); + return memcmp(h, m.image_hash, 32) == 0; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MotaContainer.h b/src/helpers/ota/MotaContainer.h new file mode 100644 index 0000000000..4425e62b6d --- /dev/null +++ b/src/helpers/ota/MotaContainer.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include "OtaFormat.h" + +// Parse/validate a `.mota` container that is fully present in a RAM buffer (docs/ota_protocol.md +// §3-§4). Variable-length parts are referenced by pointer into the caller's buffer — no copies, no +// allocation. (Device flash-backed staging gets a streaming variant in a later milestone; the field +// layout here is the single source of truth.) + +namespace mesh { +namespace ota { + +struct MotaManifest { + uint8_t format_ver = 0; + uint8_t flags = 0; + uint8_t hash_algo = 0; + uint32_t target_id = 0; + uint32_t fw_version = 0; + uint32_t image_size = 0; + uint32_t payload_size = 0; + uint8_t block_size_log2 = 0; + uint8_t codec_id = 0; + uint32_t block_count = 0; + + // Fixed layout (docs/ota_protocol.md §4): every field below sits at a constant offset and is ALWAYS + // present; base_hash/signer_pubkey/signature are zero-filled when not applicable (full / unsigned). + const uint8_t* merkle_root = nullptr; // 4 @20 + const uint8_t* image_hash = nullptr; // 32 @24 + const uint8_t* hw_id = nullptr; // 32 @57 (NUL-padded ASCII hardware tag; signed) + const uint8_t* base_hash = nullptr; // 8 @89 (zero for a full image) + const uint8_t* signer_pubkey = nullptr; // 32 @97 (zero when unsigned) + const uint8_t* signature = nullptr; // 64 @129 (zero when unsigned) + const uint8_t* approval = nullptr; // 4 @193 + const uint8_t* leaves = nullptr; // 4 * block_count (the only variable-length field) + const uint8_t* payload = nullptr; // payload_size + const uint8_t* manifest_start = nullptr;// first manifest byte (== start of the signed region) + uint32_t signed_len = 0; // = MOTA_SIGNED_LEN (129): bytes the signature covers + + bool is_full() const { return flags & MFLAG_FULL; } + bool is_signed() const { return flags & MFLAG_SIGNED; } + uint32_t block_size() const { return 1u << block_size_log2; } + bool is_approved() const; +}; + +// Parse a whole container in `buf[len]`. Returns true on success and fills `out` with pointers into +// `buf`. Validates MAGIC, TRAILER, MOTA_TOTAL_SIZE, format_ver, and internal length consistency. +bool mota_parse(const uint8_t* buf, uint32_t len, MotaManifest& out); + +// Parse a standalone manifest (the bytes [manifest_start, leaves) of a container, i.e. without the +// MAGIC/TOTAL_SIZE framing, leaves[] or payload). Used by the apply path, which receives the manifest +// separately from the image. Sets the fixed fields + signer/signature + signed_len; leaves/payload +// are left null. +bool mota_parse_manifest(const uint8_t* mf, uint32_t len, MotaManifest& out); + +// Recompute the merkle root from the manifest's leaves[] and compare to the merkle_root field. +bool mota_check_root(const MotaManifest& m); + +// For FULL images only: check sha2-256:32(payload) == image_hash. +bool mota_check_image_hash_full(const MotaManifest& m); + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MotaSeederProto.h b/src/helpers/ota/MotaSeederProto.h new file mode 100644 index 0000000000..51767c9cab --- /dev/null +++ b/src/helpers/ota/MotaSeederProto.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +// Wire contract for the "mota-seeder" link: a device (CLIENT) pulls `.mota` bytes on demand from a host +// daemon (SERVER) that owns a folder of `.mota` files. This is the FIRST concrete MotaSource transport +// (docs/ota_protocol.md §9) — the device speaks it over a dedicated Stream (a spare UART / USB-UART), so +// it never contends with the line-based text CLI on the main console. +// +// The device always initiates; every request gets exactly one response. Framing is resync-safe: the +// reader scans for the 2-byte magic, so line noise / a half-read frame just times out and is retried +// (OTA is lowest priority — eventually-upgradable). All multi-byte fields are little-endian. +// +// request (device -> host): 'M' 'S' op(1) args... xsum(1 = XOR of op+args) +// response (host -> device): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior) +// +// OP_COUNT 0x01 args: - resp payload: count(1) +// OP_DESCRIBE 0x02 args: idx(1) resp payload: MotaDesc wire (38 B, see below) [status OK] +// OP_READ 0x03 args: idx(1) off(4) len(2) resp payload: len bytes [status OK] +// +// MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1) total_size(4) +// leaves_off(4) block_count(4) payload_off(4) payload_size(4) +// +// status: 0 = OK, non-zero = error (idx out of range, read past EOF, ...). On error the response carries +// no payload (just magic+op+status+xsum). + +namespace mesh { +namespace ota { + +static const uint8_t MOTA_SEEDER_REQ_MAGIC0 = 'M'; +static const uint8_t MOTA_SEEDER_REQ_MAGIC1 = 'S'; +static const uint8_t MOTA_SEEDER_RSP_MAGIC0 = 'm'; +static const uint8_t MOTA_SEEDER_RSP_MAGIC1 = 's'; + +static const uint8_t MS_OP_COUNT = 0x01; +static const uint8_t MS_OP_DESCRIBE = 0x02; +static const uint8_t MS_OP_READ = 0x03; + +static const uint8_t MS_STATUS_OK = 0x00; + +static const uint16_t MOTA_DESC_WIRE = 38; // bytes of a MotaDesc on the wire (see layout above) + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MotaSourceSerial.cpp b/src/helpers/ota/MotaSourceSerial.cpp new file mode 100644 index 0000000000..9b3023292c --- /dev/null +++ b/src/helpers/ota/MotaSourceSerial.cpp @@ -0,0 +1,96 @@ +#include "MotaSourceSerial.h" +#include "MotaSeederProto.h" +#include "OtaByteIO.h" +#include + +namespace mesh { +namespace ota { + +bool SerialMotaSource::readByteT(uint8_t& b) { + uint32_t t0 = millis(); + while ((millis() - t0) < _to) { + int c = _io.read(); + if (c >= 0) { b = (uint8_t)c; return true; } + } + return false; +} + +bool SerialMotaSource::readExact(uint8_t* b, uint32_t n) { + for (uint32_t i = 0; i < n; i++) if (!readByteT(b[i])) return false; + return true; +} + +// One request/response transaction. Resync-safe: drains stale input, frames the request with an XOR +// checksum, then scans for the response magic and validates op+status+checksum before delivering payload. +bool SerialMotaSource::txn(uint8_t op, const uint8_t* args, uint8_t arglen, + uint8_t* payload, uint32_t payload_len) { + while (_io.read() >= 0) {} // drop any stale/partial bytes before a fresh request + uint8_t xs = op; + for (uint8_t i = 0; i < arglen; i++) xs ^= args[i]; + _io.write(MOTA_SEEDER_REQ_MAGIC0); _io.write(MOTA_SEEDER_REQ_MAGIC1); + _io.write(op); + if (arglen) _io.write(args, arglen); + _io.write(xs); + _io.flush(); + + // scan for response magic 'm' 's' (tolerate leading noise) + uint32_t t0 = millis(); bool got = false; + uint8_t prev = 0; + while ((millis() - t0) < _to) { + int c = _io.read(); + if (c < 0) continue; + if (prev == MOTA_SEEDER_RSP_MAGIC0 && (uint8_t)c == MOTA_SEEDER_RSP_MAGIC1) { got = true; break; } + prev = (uint8_t)c; + } + if (!got) return false; + + uint8_t hdr[2]; + if (!readExact(hdr, 2)) return false; // op, status + if (hdr[0] != op) return false; + uint8_t rxs = (uint8_t)(MOTA_SEEDER_RSP_MAGIC0 ^ MOTA_SEEDER_RSP_MAGIC1) ^ hdr[0] ^ hdr[1]; + bool ok = (hdr[1] == MS_STATUS_OK); + if (ok && payload_len) { + if (!readExact(payload, payload_len)) return false; + for (uint32_t i = 0; i < payload_len; i++) rxs ^= payload[i]; + } + uint8_t xsum; + if (!readByteT(xsum)) return false; + if (xsum != rxs) return false; // corrupt frame -> caller retries + return ok; +} + +uint8_t SerialMotaSource::count() { + uint8_t n = 0; + if (!txn(MS_OP_COUNT, nullptr, 0, &n, 1)) return 0; + return n; +} + +bool SerialMotaSource::describe(uint8_t idx, MotaDesc& out) { + uint8_t args[1] = { idx }; + uint8_t w[MOTA_DESC_WIRE]; + if (!txn(MS_OP_DESCRIBE, args, 1, w, MOTA_DESC_WIRE)) return false; + memcpy(out.mid, w, 4); + out.target_id = rd_u32le(w + 4); + out.fw_version = rd_u32le(w + 8); + out.codec_id = w[12]; + out.flags = w[13]; + out.total_size = rd_u32le(w + 14); + out.leaves_off = rd_u32le(w + 18); + out.block_count = rd_u32le(w + 22); + out.payload_off = rd_u32le(w + 26); + out.payload_size = rd_u32le(w + 30); + // bytes [34,38) reserved (zero) — kept for forward compat without changing MOTA_DESC_WIRE + return true; +} + +bool SerialMotaSource::read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) { + if (len > 0xFFFF) return false; // single transaction caps at 64 KB (a block is <=1 KB) + uint8_t args[7]; + args[0] = idx; + wr_u32le(args + 1, off); + args[5] = (uint8_t)(len & 0xFF); args[6] = (uint8_t)(len >> 8); + return txn(MS_OP_READ, args, 7, buf, len); +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/MotaSourceSerial.h b/src/helpers/ota/MotaSourceSerial.h new file mode 100644 index 0000000000..26b9e29863 --- /dev/null +++ b/src/helpers/ota/MotaSourceSerial.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "OtaSource.h" + +// A MotaSource backed by a host "mota-seeder" daemon over a dedicated Stream (a spare UART / USB-UART). +// The device pulls catalog + bytes on demand (MotaSeederProto.h); the folder image is never held on the +// device — it streams through. Use a stream that is NOT the text-CLI console so the binary framing never +// collides with command/log text. Reads block on the Stream up to `timeout_ms` (OTA is lowest priority, +// so a serial round-trip's latency is acceptable; keep the daemon on a fast link). + +namespace mesh { +namespace ota { + +class SerialMotaSource : public MotaSource { +public: + explicit SerialMotaSource(Stream& io, uint32_t timeout_ms = 400) : _io(io), _to(timeout_ms) {} + + uint8_t count() override; + bool describe(uint8_t idx, MotaDesc& out) override; + bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) override; + +private: + // Send a request (op+args) and read its response header; on OK, `payload` (if non-null) receives + // `payload_len` bytes. Returns true iff a well-formed OK response for `op` arrived in time. + bool txn(uint8_t op, const uint8_t* args, uint8_t arglen, uint8_t* payload, uint32_t payload_len); + bool readByteT(uint8_t& b); // one byte within the timeout + bool readExact(uint8_t* b, uint32_t n); + + Stream& _io; + uint32_t _to; +}; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/Multihash.h b/src/helpers/ota/Multihash.h new file mode 100644 index 0000000000..821641134e --- /dev/null +++ b/src/helpers/ota/Multihash.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include "Utils.h" // mesh::Utils::sha256 (real on device; real host SHA-256 via test/mocks) +#include "OtaFormat.h" + +// Thin multihash helpers: SHA-256 truncated to N bytes. No state, no allocation. + +namespace mesh { +namespace ota { + +inline void sha256_trunc(uint8_t* out, size_t out_len, const uint8_t* data, size_t len) { + mesh::Utils::sha256(out, out_len, data, (int)len); +} + +inline void sha256_trunc2(uint8_t* out, size_t out_len, + const uint8_t* a, size_t a_len, + const uint8_t* b, size_t b_len) { + mesh::Utils::sha256(out, out_len, a, (int)a_len, b, (int)b_len); +} + +inline void mh4(uint8_t out[4], const uint8_t* data, size_t len) { sha256_trunc(out, 4, data, len); } +inline void mh8(uint8_t out[8], const uint8_t* data, size_t len) { sha256_trunc(out, 8, data, len); } +inline void mh32(uint8_t out[32], const uint8_t* data, size_t len){ sha256_trunc(out, 32, data, len); } + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaApply.cpp b/src/helpers/ota/OtaApply.cpp new file mode 100644 index 0000000000..f63f4d6884 --- /dev/null +++ b/src/helpers/ota/OtaApply.cpp @@ -0,0 +1,509 @@ +#include "OtaApply.h" +#include "OtaFormat.h" +#include "MotaContainer.h" +#include "Identity.h" +#include + +#if defined(ESP32_PLATFORM) + #include // rweather streaming SHA-256 (for hashing the slot in chunks) + #include "esp_ota_ops.h" + #include "esp_partition.h" + #include "esp_system.h" + extern "C" { + #include "detools/detools.h" // vendored detools 0.53.0 embeddable decoder (CRLE-only build) + } + #if defined(OTA_FLASH_STORE) + #include "OtaStoreFlashEsp32.h" // flash-staged container (delta patch / full image in the slot) + #include "OtaSelf.h" // SelfFwInfo / ota_self_firmware (running-image base_hash gate) + #endif +#elif defined(NRF52_PLATFORM) + #include "OtaVerify.h" + #include "OtaSelf.h" + #include "OtaFlashLayout_nrf52.h" + #include "OtaBlInfo.h" // read the bootloader capability marker before arming an apply + #include "flash/flash_nrf5x.h" // Adafruit core internal-flash driver (has its own extern "C") + #include "nrf.h" + #include "nrf_soc.h" + #include "nrf_sdm.h" +#endif + +namespace mesh { +namespace ota { + +#if defined(ESP32_PLATFORM) + +bool ota_apply_slot_info(uint32_t* addr, uint32_t* size) { + const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr); + if (!p) return false; + if (addr) *addr = p->address; + if (size) *size = p->size; + return true; +} + +bool ota_apply_set_manifest(const uint8_t* mf, uint32_t len, const SignerAllowlist& allow, ApplyState& st) { + st = ApplyState(); + ota_apply_slot_info(&st.slot_addr, &st.slot_size); + MotaManifest m; + if (!mota_parse_manifest(mf, len, m)) return false; + if (!m.is_full()) return false; // A/B apply takes a full image (delta would need decode) + st.image_size = m.image_size; + memcpy(st.image_hash, m.image_hash, 32); + st.manifest_ok = true; + if (m.is_signed()) { + mesh::Identity signer(m.signer_pubkey); + st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len); + st.trusted = st.sig_ok && allow.contains(m.signer_pubkey); + } + return true; +} + +bool ota_apply_verify_slot(ApplyState& st) { + st.slot_ok = false; + if (!st.manifest_ok || st.image_size == 0 || st.image_size > st.slot_size) return false; + const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr); + if (!p) return false; + SHA256 sha; + uint8_t buf[512]; + uint32_t off = 0; + while (off < st.image_size) { + uint32_t n = st.image_size - off; if (n > sizeof(buf)) n = sizeof(buf); + if (esp_partition_read(p, off, buf, n) != ESP_OK) return false; + sha.update(buf, n); + off += n; + } + uint8_t h[32]; + sha.finalize(h, 32); + st.slot_ok = (memcmp(h, st.image_hash, 32) == 0); + return st.slot_ok; +} + +bool ota_apply_commit() { + const esp_partition_t* p = esp_ota_get_next_update_partition(nullptr); + if (!p) return false; + if (esp_ota_set_boot_partition(p) != ESP_OK) return false; + esp_restart(); // does not return + return true; +} + +// --- detools callback context ----------------------------------------------------------------- +// The delta base is the running OTA slot; the reconstructed image is streamed into the inactive slot +// via esp_ota_write (sequential, append-only -- matches detools' sequential output ordering) and +// hashed on the fly so we can check it against the signed manifest image_hash before arming. +struct DetoolsCtx { + const esp_partition_t* base; // delta base (running image), read at absolute `from_pos` + long from_pos; // absolute byte offset into `base` +#if defined(OTA_FLASH_STORE) + OtaStoreFlashEsp32* store; // staged container; patch = payload region [patch_base, +patch_len) + uint32_t patch_base; // container offset where the payload (patch) begins +#else + const uint8_t* patch; // .mota payload held wholly in RAM (RAM store; bring-up/host) +#endif + uint32_t patch_len; + uint32_t patch_pos; + esp_ota_handle_t out; // inactive slot write handle + SHA256* sha; // running hash of the reconstructed output + uint32_t out_pos; // #bytes written to the output slot + bool io_ok; +}; + +static int dt_from_read(void* arg, uint8_t* buf, size_t size) { + DetoolsCtx* c = (DetoolsCtx*)arg; + if (c->from_pos < 0 || (uint32_t)(c->from_pos) + size > c->base->size) return -DETOOLS_IO_FAILED; + if (esp_partition_read(c->base, (size_t)c->from_pos, buf, size) != ESP_OK) { c->io_ok = false; return -DETOOLS_IO_FAILED; } + c->from_pos += (long)size; + return DETOOLS_OK; +} +static int dt_from_seek(void* arg, int offset) { // detools uses relative seeks + DetoolsCtx* c = (DetoolsCtx*)arg; + c->from_pos += offset; + if (c->from_pos < 0 || (uint32_t)c->from_pos > c->base->size) return -DETOOLS_IO_FAILED; + return DETOOLS_OK; +} +static int dt_patch_read(void* arg, uint8_t* buf, size_t size) { + DetoolsCtx* c = (DetoolsCtx*)arg; + if (c->patch_pos + size > c->patch_len) return -DETOOLS_IO_FAILED; +#if defined(OTA_FLASH_STORE) + if (!c->store->read(c->patch_base + c->patch_pos, buf, size)) { c->io_ok = false; return -DETOOLS_IO_FAILED; } +#else + memcpy(buf, c->patch + c->patch_pos, size); +#endif + c->patch_pos += (uint32_t)size; + return DETOOLS_OK; +} +static int dt_to_write(void* arg, const uint8_t* buf, size_t size) { + DetoolsCtx* c = (DetoolsCtx*)arg; + if (esp_ota_write(c->out, buf, size) != ESP_OK) { c->io_ok = false; return -DETOOLS_IO_FAILED; } + c->sha->update(buf, size); + c->out_pos += (uint32_t)size; + return DETOOLS_OK; +} + +#if defined(OTA_FLASH_STORE) +// --- in-place delta on ESP32 (codec 2) ---------------------------------------------------------- +// A single in-place `.mota` can target BOTH nRF52 (bootloader applies it) and ESP32. On ESP32 the +// inactive slot is used as the in-place working memory: we copy the running image (the base) into the +// slot's bottom-staged-container-FREE region [0, write_start), then run detools' in-place decoder over +// that region (it reads the base, erases segments, writes the target back), reading the patch from the +// staged container's payload (which lives at/below write_start, disjoint from the working region). The +// decoded image is hashed against the signed image_hash BEFORE arming, so a bad decode never boots; the +// callbacks are bounded to [0, write_start) so they fail gracefully instead of touching the patch. +// (Sequential is still preferred on ESP32 — it streams straight to the slot with no base-copy; in-place +// exists only for single-artifact distribution. Requires the patch built with --inplace-segment 4096.) +struct InPlaceCtx { + const esp_partition_t* slot; // in-place working memory = slot[0, mem_max) + uint32_t mem_max; // = container write_start; accesses beyond this are refused + OtaStoreFlashEsp32* store; // staged container; patch = payload region [patch_base, +patch_len) + uint32_t patch_base, patch_len, patch_pos; + int step; // detools resume cursor (RAM; no cross-reboot resume of the apply) + bool io_ok; + const char* fail; // first failure point (diagnostic), nullptr until set + uint32_t fa, fn; int frc; // failing addr / len / esp_err +}; +static inline int ip_fail(InPlaceCtx* c, const char* w, uint32_t a, size_t n, int rc) { + if (!c->fail) { c->fail = w; c->fa = a; c->fn = (uint32_t)n; c->frc = rc; } + c->io_ok = false; return -DETOOLS_IO_FAILED; +} +static int ip_mem_read(void* a, void* dst, uintptr_t src, size_t n) { + InPlaceCtx* c = (InPlaceCtx*)a; + if ((uint32_t)src + n > c->mem_max) return ip_fail(c, "rd>max", (uint32_t)src, n, 0); + int rc = esp_partition_read(c->slot, (size_t)src, dst, n); + if (rc != ESP_OK) return ip_fail(c, "rd", (uint32_t)src, n, rc); + return DETOOLS_OK; +} +static int ip_mem_write(void* a, uintptr_t dst, void* src, size_t n) { + InPlaceCtx* c = (InPlaceCtx*)a; + if ((uint32_t)dst + n > c->mem_max) return ip_fail(c, "wr>max", (uint32_t)dst, n, 0); + int rc = esp_partition_write(c->slot, (size_t)dst, src, n); + if (rc != ESP_OK) return ip_fail(c, "wr", (uint32_t)dst, n, rc); + return DETOOLS_OK; +} +static int ip_mem_erase(void* a, uintptr_t addr, size_t n) { + InPlaceCtx* c = (InPlaceCtx*)a; + // esp_partition_erase_range requires a SECTOR-aligned size; detools' final in-place segment is partial + // (the image tail past the last full sector). addr is sector-aligned (== --inplace-segment), so round + // the length UP to a full sector. The over-erased bytes are scratch beyond image_size (never hashed), + // and — since detools processes high→low and erases-before-writing — they are never live patch data. + const uint32_t SEC = 4096; + if ((uint32_t)addr % SEC != 0) return ip_fail(c, "er!align", (uint32_t)addr, n, 0); + uint32_t len = ((uint32_t)n + SEC - 1) & ~(SEC - 1); + if ((uint32_t)addr + len > c->mem_max) return ip_fail(c, "er>max", (uint32_t)addr, len, 0); + int rc = esp_partition_erase_range(c->slot, (size_t)addr, len); + if (rc != ESP_OK) return ip_fail(c, "er", (uint32_t)addr, len, rc); + return DETOOLS_OK; +} +static int ip_step_set(void* a, int s) { ((InPlaceCtx*)a)->step = s; return DETOOLS_OK; } +static int ip_step_get(void* a, int* s) { *s = ((InPlaceCtx*)a)->step; return DETOOLS_OK; } +static int ip_patch_read(void* a, uint8_t* b, size_t n) { + InPlaceCtx* c = (InPlaceCtx*)a; + if (c->patch_pos + n > c->patch_len) return ip_fail(c, "patch>len", c->patch_pos, n, 0); + if (!c->store->read(c->patch_base + c->patch_pos, b, n)) return ip_fail(c, "patch_rd", c->patch_pos, n, 0); + c->patch_pos += (uint32_t)n; + return DETOOLS_OK; +} + +static bool esp32_inplace_apply(OtaStoreFlashEsp32& store, const MotaManifest& m, ApplyState& st, char* msg) { + const esp_partition_t* slot = store.partition(); + const esp_partition_t* base = esp_ota_get_running_partition(); + if (!slot || !base) { strcpy(msg, "no slot/base partition"); return false; } + SelfFwInfo fi; + if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; } + if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "not built for the running firmware (base mismatch)"); return false; } + uint32_t mem_max = store.write_start(); // working region [0, mem_max); the patch sits at/above it + if (mem_max == 0) { strcpy(msg, "in-place needs a bottom-staged container"); return false; } + if (fi.image_len > mem_max || m.image_size > mem_max) { strcpy(msg, "in-place region too small for base/image"); return false; } + + // load the base (running image) into the working region [0, base_len), sector by sector (erase + copy) + uint8_t buf[512]; + for (uint32_t off = 0; off < fi.image_len; ) { + uint32_t sec = off & ~(4096u - 1); + if (esp_partition_erase_range(slot, sec, 4096) != ESP_OK) { strcpy(msg, "base erase failed"); return false; } + uint32_t secend = sec + 4096; if (secend > fi.image_len) secend = fi.image_len; + for (uint32_t p = (off > sec ? off : sec); p < secend; ) { + uint32_t n = secend - p; if (n > sizeof(buf)) n = sizeof(buf); + if (esp_partition_read(base, p, buf, n) != ESP_OK || esp_partition_write(slot, p, buf, n) != ESP_OK) { + strcpy(msg, "base copy failed"); return false; } + p += n; + } + off = secend; + } + + // patch in place over the working region; patch streamed from the staged container payload + InPlaceCtx c; + c.slot = slot; c.mem_max = mem_max; c.store = &store; + c.patch_base = store.meta_bytes(); c.patch_len = m.payload_size; c.patch_pos = 0; c.step = 0; c.io_ok = true; + c.fail = nullptr; c.fa = c.fn = 0; c.frc = 0; + int r = detools_apply_patch_in_place_callbacks(ip_mem_read, ip_mem_write, ip_mem_erase, + ip_step_set, ip_step_get, ip_patch_read, + (size_t)m.payload_size, &c); + if (r < 0 || !c.io_ok) { + if (c.fail) sprintf(msg, "in-place decode err %d @%s a=%u n=%u rc=%d max=%u", r, c.fail, + (unsigned)c.fa, (unsigned)c.fn, c.frc, (unsigned)mem_max); + else sprintf(msg, "in-place decode err %d", r); + return false; + } + if ((uint32_t)r != m.image_size) { sprintf(msg, "in-place size %u!=%u", (unsigned)r, (unsigned)m.image_size); return false; } + + // verify the decoded slot image against the signed image_hash BEFORE arming (mismatch -> never boots) + SHA256 sha; + for (uint32_t off = 0; off < m.image_size; ) { + uint32_t n = m.image_size - off; if (n > sizeof(buf)) n = sizeof(buf); + if (esp_partition_read(slot, off, buf, n) != ESP_OK) { strcpy(msg, "slot read failed"); return false; } + sha.update(buf, n); off += n; + } + uint8_t hh[32]; sha.finalize(hh, 32); + st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0); + if (!st.slot_ok) { strcpy(msg, "image_hash MISMATCH after in-place decode"); return false; } + if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; } + sprintf(msg, "verified%s; in-place decoded %u B, image hash OK — armed, rebooting to apply", + m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size); + return true; +} + +// Apply the `.mota` staged in the inactive slot by OtaStoreFlashEsp32 (no contiguous RAM copy). +// FULL: the payload was streamed straight to slot offset 0 during the fetch -> hash the slot image +// and compare to the signed image_hash, then arm. No decode, no copy. +// DELTA (sequential): base = the running slot; the patch is read from the staged payload region (the +// slot's bottom); the reconstructed image is written to the inactive slot via esp_ota_write and +// hashed vs image_hash. esp_ota_begin only erases [0, image_size], which the fetch-time fit +// check kept below the bottom-staged container, so the patch survives while we decode over it. +// DELTA (in-place): copy the running image into the slot's working region then patch in place +// (esp32_inplace_apply); image_hash-gated before arming. Lets one in-place .mota target both +// ESP32 and nRF52. Sequential is still preferred on ESP32 (no base-copy). +// The result is verified (signature/trust up front, image_hash after) and the slot armed; the caller +// reboots once the confirmation reply has gone out. +bool ota_apply_detools_mota(OtaStoreFlashEsp32& store, const SignerAllowlist& allow, ApplyState& st, char* msg) { + st = ApplyState(); + const esp_partition_t* slot = store.partition(); + if (!slot || store.staged_size() < 16) { strcpy(msg, "no staged update"); return false; } + st.slot_addr = slot->address; st.slot_size = slot->size; + + // read + parse the manifest out of the staged container (header = MAGIC(4) + total(4)) + uint8_t hdr[8]; + if (!store.read(0, hdr, 8) || memcmp(hdr, MOTA_MAGIC, 4) != 0) { strcpy(msg, "bad container"); return false; } + uint8_t mfbuf[256]; + uint32_t mflen = store.meta_bytes() > 8 ? store.meta_bytes() - 8 : 0; // manifest+leaves; cap to mfbuf + if (mflen > sizeof(mfbuf)) mflen = sizeof(mfbuf); + MotaManifest m; + if (mflen < 57 || !store.read(8, mfbuf, mflen) || !mota_parse_manifest(mfbuf, mflen, m)) { + strcpy(msg, "manifest parse failed"); return false; } + st.image_size = m.image_size; memcpy(st.image_hash, m.image_hash, 32); st.manifest_ok = true; + if (m.image_size == 0 || m.image_size > slot->size) { strcpy(msg, "image > slot"); return false; } + + // signature / trust BEFORE arming an untrusted image (image_hash below is the target-firmware gate) + if (m.is_signed()) { + mesh::Identity signer(m.signer_pubkey); + st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len); + st.trusted = st.sig_ok && allow.contains(m.signer_pubkey); + if (!st.sig_ok) { strcpy(msg, "bad signature"); return false; } + if (!st.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; } + } + + // ---- FULL: payload already in slot[0]; verify hash + arm ---- + if (m.is_full()) { + SHA256 sha; uint8_t buf[512]; + for (uint32_t off = 0; off < m.image_size; ) { + uint32_t n = m.image_size - off; if (n > sizeof(buf)) n = sizeof(buf); + if (esp_partition_read(slot, off, buf, n) != ESP_OK) { strcpy(msg, "slot read failed"); return false; } + sha.update(buf, n); off += n; + } + uint8_t hh[32]; sha.finalize(hh, 32); + st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0); + if (!st.slot_ok) { strcpy(msg, "image_hash MISMATCH (slot)"); return false; } + if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; } + sprintf(msg, "verified%s full image %u B in slot — armed, rebooting to apply", + m.is_signed() ? " (trusted)" : " (unsigned)", (unsigned)m.image_size); + return true; + } + + // ---- DELTA ---- + if (m.codec_id == CODEC_DETOOLS_INPLACE) return esp32_inplace_apply(store, m, st, msg); // single-artifact codec + if (m.codec_id != CODEC_DETOOLS_SEQUENTIAL) { strcpy(msg, "unknown delta codec"); return false; } + + // delta must be built for the running firmware (cheap early gate; image_hash is the definitive check) + if (m.base_hash) { + SelfFwInfo fi; + if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; } + if (memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "delta not built for the running firmware (base mismatch)"); return false; } + } + + const esp_partition_t* base = esp_ota_get_running_partition(); + if (!base) { strcpy(msg, "no running partition"); return false; } + esp_ota_handle_t h; + if (esp_ota_begin(slot, m.image_size, &h) != ESP_OK) { strcpy(msg, "ota_begin failed"); return false; } + + SHA256 sha; + DetoolsCtx ctx; + ctx.base = base; ctx.from_pos = 0; + ctx.store = &store; ctx.patch_base = store.meta_bytes(); ctx.patch_len = m.payload_size; ctx.patch_pos = 0; + ctx.out = h; ctx.sha = &sha; ctx.out_pos = 0; ctx.io_ok = true; + + int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read, + (size_t)m.payload_size, dt_to_write, &ctx); + if (r < 0 || !ctx.io_ok) { esp_ota_abort(h); sprintf(msg, "detools err %d @%u/%u", + ctx.io_ok ? r : -DETOOLS_IO_FAILED, (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; } + if ((uint32_t)r != m.image_size || ctx.out_pos != m.image_size) { + esp_ota_abort(h); sprintf(msg, "size mismatch %u!=%u", (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; } + uint8_t hh[32]; sha.finalize(hh, 32); + st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0); + if (!st.slot_ok) { esp_ota_abort(h); strcpy(msg, "image_hash MISMATCH after decode"); return false; } + if (esp_ota_end(h) != ESP_OK) { strcpy(msg, "ota_end failed"); return false; } + if (esp_ota_set_boot_partition(slot) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; } + sprintf(msg, "verified%s; decoded %u B, image hash OK — armed, rebooting to apply", + m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size); + return true; +} + +#else // !OTA_FLASH_STORE: RAM-staged apply (whole .mota in a contiguous RAM buffer; bring-up/host) + +bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow, + ApplyState& st, char* msg) { + st = ApplyState(); + MotaManifest m; + if (!mota_parse(buf, len, m)) { strcpy(msg, "no valid .mota (parse failed)"); return false; } + if (m.is_full() || m.codec_id != CODEC_DETOOLS_SEQUENTIAL) { strcpy(msg, "not a detools-sequential delta"); return false; } + st.image_size = m.image_size; + memcpy(st.image_hash, m.image_hash, 32); + st.manifest_ok = true; + if (m.is_signed()) { + mesh::Identity signer(m.signer_pubkey); + st.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len); + st.trusted = st.sig_ok && allow.contains(m.signer_pubkey); + if (!st.sig_ok) { strcpy(msg, "bad signature"); return false; } + if (!st.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; } + } + const esp_partition_t* base = esp_ota_get_running_partition(); + const esp_partition_t* out = esp_ota_get_next_update_partition(nullptr); + if (!base || !out) { strcpy(msg, "no A/B slot"); return false; } + st.slot_addr = out->address; st.slot_size = out->size; + if (m.image_size > out->size) { strcpy(msg, "image > slot"); return false; } + esp_ota_handle_t h; + if (esp_ota_begin(out, m.image_size, &h) != ESP_OK) { strcpy(msg, "ota_begin failed"); return false; } + SHA256 sha; + DetoolsCtx ctx; + ctx.base = base; ctx.from_pos = 0; + ctx.patch = m.payload; ctx.patch_len = m.payload_size; ctx.patch_pos = 0; + ctx.out = h; ctx.sha = &sha; ctx.out_pos = 0; ctx.io_ok = true; + int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read, + (size_t)m.payload_size, dt_to_write, &ctx); + if (r < 0 || !ctx.io_ok) { esp_ota_abort(h); sprintf(msg, "detools err %d @%u/%u", + ctx.io_ok ? r : -DETOOLS_IO_FAILED, (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; } + if ((uint32_t)r != m.image_size || ctx.out_pos != m.image_size) { + esp_ota_abort(h); sprintf(msg, "size mismatch %u!=%u", (unsigned)ctx.out_pos, (unsigned)m.image_size); return false; } + uint8_t hh[32]; sha.finalize(hh, 32); + st.slot_ok = (memcmp(hh, m.image_hash, 32) == 0); + if (!st.slot_ok) { esp_ota_abort(h); strcpy(msg, "image_hash MISMATCH after decode"); return false; } + if (esp_ota_end(h) != ESP_OK) { strcpy(msg, "ota_end failed"); return false; } + if (esp_ota_set_boot_partition(out) != ESP_OK) { strcpy(msg, "set_boot failed"); return false; } + sprintf(msg, "verified%s; decoded %u B, image hash OK — armed, rebooting to apply", + m.is_signed() ? " (signer trusted)" : " (unsigned)", (unsigned)m.image_size); + return true; +} +#endif // OTA_FLASH_STORE + +bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { + st = ApplyState(); strcpy(msg, "nRF52-only (ESP32 uses ota_apply_detools_mota)"); return false; +} + +void ota_reboot_to_apply() { esp_restart(); } // boots the slot armed by ota_apply_detools_mota; no return + +#elif defined(NRF52_PLATFORM) // single-slot: verify + mark APPROVED + hand off to the bootloader + +// ESP32 A/B-only entry points are unsupported on nRF52. +bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; } +bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; } +bool ota_apply_verify_slot(ApplyState&) { return false; } +bool ota_apply_commit() { return false; } +bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "use ota_apply_mota_nrf52"); return false; } + +void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return) + uint8_t sd_en = 0; + sd_softdevice_is_enabled(&sd_en); + if (sd_en) { // POWER is SD-restricted while the SoftDevice runs + sd_power_gpregret_clr(0, 0xFFFFFFFF); + sd_power_gpregret_set(0, GPREGRET_OTA_APPLY); + } else { + NRF_POWER->GPREGRET = GPREGRET_OTA_APPLY; + } + NVIC_SystemReset(); // does not return +} + +uint8_t ota_bootloader_last_rc() { // bootloader's last in-place-apply code, stashed in GPREGRET2 + uint32_t v = 0; + uint8_t en = 0; sd_softdevice_is_enabled(&en); + if (en) sd_power_gpregret_get(1, &v); else v = NRF_POWER->GPREGRET2; + return (uint8_t)v; +} + +bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow, + ApplyState& st, char* msg) { + st = ApplyState(); + MotaManifest m; + if (!mota_parse(buf, len, m)) { strcpy(msg, "parse failed"); return false; } + if (m.is_full() || m.codec_id != CODEC_DETOOLS_INPLACE) { strcpy(msg, "not an in-place delta"); return false; } + st.image_size = m.image_size; + memcpy(st.image_hash, m.image_hash, 32); + st.manifest_ok = true; + + // 0) THIS device's bootloader must be able to apply this .mota — otherwise staging + approving + rebooting + // just bounces back unchanged (a legacy/stock/older-OTAFIX bootloader). Refuse here, before any reboot. + { + OtaBlCaps bl = ota_bootloader_caps(); + if (!bl.present) { strcpy(msg, "this bootloader has no OTA-apply support — update the bootloader first"); return false; } + if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) { + snprintf(msg, 159, "bootloader too old to apply this update (bl abi=%u codecs=0x%x; need fmt>=%u codec=%u) — update the bootloader", + bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id); + return false; + } + } + + // Gated verification, in order, returning the FIRST failing reason (the bootloader re-checks integrity + // again before booting, so authenticity is gated here and re-validated there): + VerifyResult vr = ota_verify(buf, len, allow); + st.sig_ok = vr.sig_ok; st.trusted = vr.trusted; + + // 1) downloaded payload: the fetched blocks must match the manifest's merkle root (intact + complete) + if (!vr.root_ok || !vr.image_ok) { strcpy(msg, "payload hash mismatch (incomplete or corrupt .mota)"); return false; } + + // 2) target firmware: the delta must be built against THIS running image (base_hash == our EndF body + // hash). The resulting image_hash is re-checked by the bootloader after the in-place decode -- a + // single-slot device cannot produce the target image to hash it before applying. + SelfFwInfo fi; + if (!ota_self_firmware(fi) || !fi.valid) { strcpy(msg, "cannot read running firmware (no EndF)"); return false; } + if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8) != 0) { strcpy(msg, "not built for the running firmware (base mismatch)"); return false; } + st.slot_ok = true; + + // 3) signature (only if the .mota is signed): valid Ed25519 AND signer in this device's allowlist + if (vr.is_signed) { + if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; } + if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; } + } + + // mark the staged manifest APPROVED in flash (buf is the memory-mapped staging region, so + // m.approval is a real flash address). NOR-clear over the erased 0xFFFFFFFF -> "APRV". + uint32_t approval_addr = (uint32_t)(uintptr_t)m.approval; + if (flash_nrf5x_write(approval_addr, APPROVAL_YES, 4) < 0) { strcpy(msg, "approval write failed"); return false; } + flash_nrf5x_flush(); + if (memcmp((const void*)(uintptr_t)approval_addr, APPROVAL_YES, 4) != 0) { strcpy(msg, "approval not set"); return false; } + + // Approved. Do NOT reset here — return so the caller can deliver `msg` to the operator first; the + // deferred ota_reboot_to_apply() (after the reply is sent) does the actual handoff to the bootloader. + sprintf(msg, "verified%s; applying — rebooting into bootloader once this reply is sent", + vr.is_signed ? " (signer trusted)" : " (unsigned)"); + return true; +} + +#else // native / other platforms + +bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; } +bool ota_apply_set_manifest(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st) { st = ApplyState(); return false; } +bool ota_apply_verify_slot(ApplyState&) { return false; } +bool ota_apply_commit() { return false; } +bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; } +bool ota_apply_mota_nrf52(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "unsupported"); return false; } +void ota_reboot_to_apply() {} +uint8_t ota_bootloader_last_rc() { return 0; } + +#endif + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaApply.h b/src/helpers/ota/OtaApply.h new file mode 100644 index 0000000000..42e2f7b189 --- /dev/null +++ b/src/helpers/ota/OtaApply.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include "SignerAllowlist.h" + +// P6 apply (full-image, ESP32 A/B). The new image is delivered into the inactive OTA slot; the device +// then verifies that slot against the signed manifest's image_hash (+ Ed25519/allowlist), and commits +// by setting it as the boot partition and rebooting. Safe + rollback-capable (the bootloader validates +// the image; a bad image rolls back). nRF52 apply is the bootloader-handoff path (separate). Functions +// return false on platforms without an A/B OTA layout. + +namespace mesh { +namespace ota { + +struct ApplyState { + bool manifest_ok = false; + bool sig_ok = false; + bool trusted = false; // signer in allowlist + bool slot_ok = false; // inactive slot image hashes to manifest.image_hash + uint32_t slot_addr = 0, slot_size = 0; + uint32_t image_size = 0; + uint8_t image_hash[32] = {0}; +}; + +bool ota_apply_slot_info(uint32_t* addr, uint32_t* size); // the inactive A/B slot +bool ota_apply_set_manifest(const uint8_t* mf, uint32_t len, + const SignerAllowlist& allow, ApplyState& st); // parse + verify signature +bool ota_apply_verify_slot(ApplyState& st); // hash the slot vs image_hash +bool ota_apply_commit(); // set-boot + reboot (no return) + +// Apply an ESP32 A/B `.mota` (CODEC_DETOOLS_SEQUENTIAL delta or CODEC_FULL image) and arm the inactive +// slot; the caller reboots after the confirmation reply. `msg` (>=80 bytes) receives a human-readable +// result. With OTA_FLASH_STORE the container is staged in the inactive slot (no contiguous RAM copy): a +// full payload is already in the slot (just verified), a sequential delta is decoded from the running +// slot over the staged patch. Without OTA_FLASH_STORE (bring-up) the whole container is a RAM buffer. +#if defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) +class OtaStoreFlashEsp32; +bool ota_apply_detools_mota(OtaStoreFlashEsp32& store, + const SignerAllowlist& allow, ApplyState& st, char* msg); +#else +bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len, + const SignerAllowlist& allow, ApplyState& st, char* msg); +#endif + +// nRF52 (RAK4631) single-slot apply. The running app can't rewrite itself, so it does NOT decode: it +// runs the gated verification chain (payload hash -> built-for-this-firmware -> signature/trust) and, +// only if all pass, marks the staged manifest APPROVED in flash. It does NOT reboot — so the caller can +// first send the result back to the operator — the actual handoff is ota_reboot_to_apply() below. +// Returns true (msg = "verified...") when approved, false (msg = the first failing gate) otherwise. +bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len, + const SignerAllowlist& allow, ApplyState& st, char* msg); + +// Commit the (already approved/armed) update and reboot into it — does NOT return. Call this only after +// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows +// the apply started (over LoRa the device then goes silent while the bootloader applies). nRF52: set +// the GPREGRET apply magic + reset (the bootloader does the in-place decode + verify). ESP32: reboot +// into the slot already armed by ota_apply_detools_mota. +void ota_reboot_to_apply(); + +// DIAGNOSTIC (nRF52): the bootloader stashes its last in-place-apply bail/progress code in GPREGRET2 (see +// ota_delta.c). Read it back so `ota status` can show why an apply didn't take. 0 / other platforms = n/a. +uint8_t ota_bootloader_last_rc(); + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaBlInfo.h b/src/helpers/ota/OtaBlInfo.h new file mode 100644 index 0000000000..d22468fa79 --- /dev/null +++ b/src/helpers/ota/OtaBlInfo.h @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +// Read the bootloader's OTA capability marker so the app can decide, BEFORE staging+approving+rebooting, +// whether THIS device's bootloader can actually apply a `.mota`. Without this the app would reboot into a +// bootloader that silently can't apply (stock Adafruit, or an OLDER OTAFIX predating a `.mota` format +// change) and the device would just come back up unchanged. +// +// Mirror of Adafruit_nRF52_Bootloader_OTAFIX/src/ota_bl_info.h — keep byte-identical. +// nRF52 only (the bootloader flash is memory-mapped + readable by the app); a no-op elsewhere. + +#if defined(NRF52_PLATFORM) + #include "OtaFlashLayout_nrf52.h" +#endif + +namespace mesh { +namespace ota { + +// 16-byte marker: magic[8] "MOTABLDR" + apply_abi(2) + codec_mask(2) + reserved(4). +static const uint8_t OTA_BL_MAGIC[8] = { 'M','O','T','A','B','L','D','R' }; + +struct OtaBlCaps { + bool present = false; + uint16_t apply_abi = 0; // max .mota format_ver the bootloader can apply + uint16_t codec_mask = 0; // bit i set => can apply codec_id i (in-place delta = bit 2) +}; + +// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52. +inline OtaBlCaps ota_bootloader_caps() { + OtaBlCaps c; +#if defined(NRF52_PLATFORM) + const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START; + const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END; + for (const uint8_t* p = lo; p + 16 <= hi; p++) { + if (p[0] != OTA_BL_MAGIC[0] || memcmp(p, OTA_BL_MAGIC, 8) != 0) continue; + c.present = true; + c.apply_abi = (uint16_t)(p[8] | ((uint16_t)p[9] << 8)); + c.codec_mask = (uint16_t)(p[10] | ((uint16_t)p[11] << 8)); + break; + } +#endif + return c; +} + +// True if this device's bootloader can apply a .mota of the given format_ver + codec_id. +inline bool ota_bootloader_can_apply(uint8_t format_ver, uint8_t codec_id) { + OtaBlCaps c = ota_bootloader_caps(); + return c.present && c.apply_abi >= format_ver && (c.codec_mask & (1u << codec_id)) != 0; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaByteIO.h b/src/helpers/ota/OtaByteIO.h new file mode 100644 index 0000000000..d4834cd8b6 --- /dev/null +++ b/src/helpers/ota/OtaByteIO.h @@ -0,0 +1,58 @@ +#pragma once + +#include +#include + +// A tiny bounds-checked little-endian cursor for reading the `.mota` container (docs/ota_protocol.md §3-§4) +// in a self-documenting way: each field is read by name in order, instead of hand-computed byte offsets +// (`p[0]`, `rd_u32(p+3)`, `p += 89`, `NEED(n)` ...). Any over-read flips `ok` false and yields zero/null, so +// callers parse the whole struct then check `r.ok` once. 32-bit offsets (a container can be >64 KB; the +// 16-byte LoRa wire messages keep their own uint16 cursor in OtaProtocol.cpp). No allocation; `take()` +// returns a pointer INTO the caller's buffer (zero-copy), matching the manifest's by-pointer fields. + +namespace mesh { +namespace ota { + +// Little-endian scalar read/write for RANDOM-access fields (a specific offset into a buffer, e.g. a wire +// row or a fixed manifest slot). Sequential parsing should prefer the ByteReader cursor below. These +// replace the per-file `rd_u32`/`wr_u32` helpers that were copy-pasted across the OTA sources. +inline uint16_t rd_u16le(const uint8_t* p) { return (uint16_t)p[0] | ((uint16_t)p[1] << 8); } +inline uint32_t rd_u32le(const uint8_t* p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} +inline void wr_u16le(uint8_t* p, uint16_t v) { p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); } +inline void wr_u32le(uint8_t* p, uint32_t v) { + p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); +} + +// Round to a multiple of `unit` (a power of two — a flash sector/page size). Names the `& ~(unit-1)` +// idiom so flash-geometry math in the stores reads as intent (align down / align up). +inline uint32_t align_down(uint32_t x, uint32_t unit) { return x & ~(unit - 1); } +inline uint32_t align_up(uint32_t x, uint32_t unit) { return (x + unit - 1) & ~(unit - 1); } + +struct ByteReader { + const uint8_t* p; + uint32_t len; + uint32_t n = 0; + bool ok = true; + + ByteReader(const uint8_t* buf, uint32_t length) : p(buf), len(length) {} + + uint32_t pos() const { return n; } + bool fits(uint32_t k) const { return ok && (uint64_t)n + k <= len; } + + uint8_t u8() { if (!fits(1)) { ok = false; return 0; } return p[n++]; } + uint32_t u32() { // little-endian + if (!fits(4)) { ok = false; return 0; } + uint32_t v = rd_u32le(p + n); n += 4; return v; + } + // Borrow `k` bytes at the cursor (e.g. merkle_root[4], leaves[4*BC]) and advance; null on overflow. + const uint8_t* take(uint32_t k) { + if (!fits(k)) { ok = false; return nullptr; } + const uint8_t* r = p + n; n += k; return r; + } + void skip(uint32_t k) { if (!fits(k)) { ok = false; return; } n += k; } +}; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp new file mode 100644 index 0000000000..f514dea219 --- /dev/null +++ b/src/helpers/ota/OtaCli.cpp @@ -0,0 +1,410 @@ +#include "OtaCli.h" +#include "OtaContext.h" +#include "OtaVerify.h" +#include "OtaSelf.h" +#include "OtaTargets.h" // ota_target_env_name(): human-readable name for a target_id (no string on the wire) +#if defined(NRF52_PLATFORM) + #include "OtaBlInfo.h" // ota_bootloader_caps(): can this device's bootloader apply a .mota? +#endif +#include "Utils.h" +#include +#include +#include +#include // millis() for session-age display (device-only command surface) + +namespace mesh { +namespace ota { + +static uint32_t parse_u32(const char* s) { + uint32_t n = 0; + while (*s == ' ') s++; + while (*s >= '0' && *s <= '9') n = n * 10 + (uint32_t)(*s++ - '0'); + return n; +} + +static char fstate_char(OtaManager::FetchState s) { + switch (s) { + case OtaManager::IDLE: return 'I'; + case OtaManager::WANT_MANIFEST: return 'W'; + case OtaManager::FETCHING: return 'F'; + case OtaManager::COMPLETE: return 'C'; + default: return 'X'; + } +} + +// For users, the only distinction that matters is full image vs. delta (which delta codec is internal). +static const char* codec_kind(uint8_t c) { return c == CODEC_FULL ? "full" : "delta"; } + +// A plain-language word for the fetch state (shown in `ota status`). +static const char* state_word(OtaManager::FetchState s) { + switch (s) { + case OtaManager::IDLE: return "idle"; + case OtaManager::WANT_MANIFEST: return "starting"; + case OtaManager::FETCHING: return "downloading"; + case OtaManager::COMPLETE: return "ready to install"; + case OtaManager::FAILED: return "failed"; + default: return "?"; + } +} + +// Render the packed fw_version as "v1.2.3" (or "v1.2.3.4" when a prerelease byte is set). +static void ver_str(char* out, size_t cap, uint32_t v) { + FwVersion fw = FwVersion::unpack(v); + if (fw.prerelease) snprintf(out, cap, "v%u.%u.%u.%u", fw.major, fw.minor, fw.patch, fw.prerelease); + else snprintf(out, cap, "v%u.%u.%u", fw.major, fw.minor, fw.patch); +} + +// Match the first word of `a` against any of the '|'-separated names (so commands have intuitive aliases +// and short forms); on a match, point `*rest` at the argument text. Keeps the dispatch table readable. +static bool is_cmd(const char* a, const char* names, const char** rest) { + size_t tlen = 0; while (a[tlen] && a[tlen] != ' ') tlen++; + for (const char* s = names; *s; ) { + const char* d = s; while (*d && *d != '|') d++; + if ((size_t)(d - s) == tlen && tlen && strncmp(a, s, tlen) == 0) { + const char* r = a + tlen; while (*r == ' ') r++; + if (rest) *rest = r; + return true; + } + s = (*d == '|') ? d + 1 : d; + } + return false; +} + +// The everyday OTA surface is BitTorrent-shaped: `ota` shows what you're holding (your running firmware +// as a full mOTA + your one fetch session), `ota neighbors` shows the mOTAs heard around you, `ota pull` +// starts fetching one, `ota drop` frees the session. The raw primitives (manual content load, low-level +// apply steps) live under `ota dev ...` so they don't clutter the everyday surface. Every reply fits one +// packet so it works as remote-admin over LoRa. +static bool handle_dev(const char* d, char* reply, OtaContext& c); + +bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board) { + const char* a = command + 3; + if (*a != 0 && *a != ' ') return false; + while (*a == ' ') a++; + OtaContext& c = ota_ctx(); + const char* rest = a; + + // ---- raw / internal primitives, tucked under `ota dev ...` ---- + if (is_cmd(a, "dev", &rest)) { + return handle_dev(rest, reply, c); + } + + // ---- help: list the commands in plain words (aliases in parentheses) ---- + if (is_cmd(a, "help|?|h", &rest)) { + snprintf(reply, 160, + "OTA: status | ls=find updates | get <#>=download | install | cancel | announce | self | " + "folder | config | key. Try `ota ls`."); + + // ---- inventory dashboard: running fw (self), the one fetch session, serving state ---- + } else if (*a == 0 || is_cmd(a, "status|st", &rest)) { + SelfFwInfo fi; bool s = ota_self_firmware(fi); + char selfhx[9]; if (s && fi.valid) mesh::Utils::toHex(selfhx, fi.body_hash, 4); else strcpy(selfhx, "?"); + OtaManager::FetchState fs = c.manager.fetchState(); + char dl[80]; + if (fs == OtaManager::IDLE) { + strcpy(dl, "no download"); + } else { + char midhx[9]; mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4); + unsigned have = (unsigned)c.manager.blocksHave(), tot = (unsigned)c.manager.blocksTotal(); + unsigned pct = tot ? (unsigned)((uint64_t)have * 100 / tot) : 0; + unsigned age = c.session_started_ms ? (unsigned)((millis() - c.session_started_ms) / 1000) : 0; + snprintf(dl, sizeof dl, "download: %s %u/%u (%u%%) id=%s %us", state_word(fs), have, tot, pct, midhx, age); + } + const char* hw = (c.hw_id[0]) ? c.hw_id : "?"; + const char* tenv = ota_target_env_name(c.manager.target()); // env name, or "?" if not in the table + int n = snprintf(reply, 160, "OTA | this fw %s (%uK) hw=%s | %s | serving:%s (%u) | keys:%u | target:%08X (%s)", + selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw, dl, + c.serving ? "on" : "off", (unsigned)c.manager.servedCount(), + (unsigned)c.allow.count(), (unsigned)c.manager.target(), tenv ? tenv : "?"); +#if defined(NRF52_PLATFORM) + // nRF52 applies via the bootloader — show (cached) whether it can, so `ota get`/`install` won't surprise. + // blrc = the bootloader's last in-place-apply code (diagnostic; 0xB8=success, see ota_delta.c). + if (n < 146) n += snprintf(reply + n, 160 - n, " | bl:%s blrc:%02X", + c.bootloaderCaps().present ? "apply" : "NONE", ota_bootloader_last_rc()); +#endif + + // ---- what's available around me (catalogued from beacons + OTA_HAVE), best/most-recent first ---- + } else if (is_cmd(a, "neighbors|nbrs|updates|ls|n", &rest)) { + // Kick a fresh round of catalog queries (async — rows arrive over the next seconds); render what we + // have now in plain words. The reply buffer is 160 B (serial / one LoRa packet for remote-admin), so + // writes are bounded and extra rows collapse to "+N more". + c.manager.queryAll(); + const int CAP = 160; + int n = snprintf(reply, CAP, "Updates nearby (%u src) — `ota get <#>` to download:", + (unsigned)c.manager.sourceCount()); + const uint8_t* cur = (c.manager.fetchState() != OtaManager::IDLE) ? c.manager.fetchManifestId() : nullptr; + uint32_t myt = c.manager.target(); // effective target (EndF identity if present, else build flag) + uint32_t now = millis(); int shown = 0, more = 0; + for (uint8_t i = 0; i < c.manager.catalogCount(); i++) { + const OtaManager::CatRow* h = c.manager.catalogRow(i); + if (CAP - n < 48) { more++; continue; } + bool on = cur && memcmp(cur, h->mid, 4) == 0; + uint32_t age = (now - h->last_ms) / 1000; if (age > 99999) age = 99999; + char ver[20]; ver_str(ver, sizeof ver, h->fw_version); + // What is this update for? "yours" if same target (hw+role) as us; else the target's env name when we + // know it (named locally from its 4-byte target_id — no string travels on the wire); else the raw + // target_id hex (an env this build's OtaTargets.h table doesn't know) or '?' for an unset target. + char hwbuf[16]; + const char* fit; + const char* env = ota_target_env_name(h->target_id); + if (myt && h->target_id == myt) fit = "yours"; + else if (env) fit = env; + else if (h->target_id == 0) fit = "?"; + else { snprintf(hwbuf, sizeof hwbuf, "hw %08X", (unsigned)h->target_id); fit = hwbuf; } + n += snprintf(reply + n, CAP - n, "\n %d) %s %s [%s] %un %us%s", shown + 1, ver, + codec_kind(h->codec), fit, (unsigned)h->n_seeders, (unsigned)age, + on ? " [downloading]" : ""); + shown++; + } + if (more && n < CAP) snprintf(reply + n, CAP - n, "\n +%d more", more); + if (shown == 0) strcpy(reply, "No updates seen yet — re-run `ota ls` in a few seconds (just asked around)."); + + // ---- start fetching a specific catalogued mOTA (by list index or manifest_id) ---- + } else if (is_cmd(a, "pull|get|download", &rest)) { + const char* p = rest; + if (*p == 0) { strcpy(reply, "usage: ota get <#> (see the numbers in `ota ls`)"); return true; } + const OtaManager::CatRow* sel = nullptr; uint8_t mid[4]; + if (*p == '#' || (p[0] >= '1' && p[0] <= '9' && (p[1] == 0 || p[1] == ' '))) { // index among catalogue + int idx = atoi(*p == '#' ? p + 1 : p); + if (idx >= 1 && idx <= c.manager.catalogCount()) sel = c.manager.catalogRow((uint8_t)(idx - 1)); + } else if (mesh::Utils::fromHex(mid, 4, p)) { // explicit manifest_id + for (uint8_t i = 0; i < c.manager.catalogCount(); i++) + if (memcmp(c.manager.catalogRow(i)->mid, mid, 4) == 0) { sel = c.manager.catalogRow(i); break; } + } + if (!sel) { strcpy(reply, "ERR no such update (see the numbers in `ota ls`)"); return true; } + if (c.apply_pending) { strcpy(reply, "ERR busy applying"); return true; } + uint8_t selmid[4]; uint32_t seltgt = sel->target_id; memcpy(selmid, sel->mid, 4); // sel may move on reset + c.manager.reset_session(); c.fetch_store.clear(); + c.manager.pull(selmid, seltgt); // sets want + begins the manifest fetch now + char midhx[9]; mesh::Utils::toHex(midhx, selmid, 4); + sprintf(reply, "OK pulling mid=%s target=%08X (low priority)", midhx, (unsigned)seltgt); + + // ---- discard the current session (e.g. a stalled old fetch) to free the slot ---- + } else if (is_cmd(a, "drop|cancel|stop", &rest)) { + OtaManager::FetchState fs = c.manager.fetchState(); + char midhx[9]; strcpy(midhx, "-"); + if (fs != OtaManager::IDLE) mesh::Utils::toHex(midhx, c.manager.fetchManifestId(), 4); + c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr); + c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0; + sprintf(reply, "OK dropped session (was %c mid=%s); slot free for a new pull", fstate_char(fs), midhx); + + // ---- broadcast our tiny beacon so peers discover us. If not already serving, set up flash-backed + // self-serve first (so we're a real, fetchable source of our own running firmware). ---- + } else if (is_cmd(a, "announce|adv", &rest)) { + if (!c.serving) c.serving = ota_serve_self(c, 0); + c.manager.announce(); + sprintf(reply, "OK beacon sent (serving=%s)", c.serving ? "self fw" : "nothing"); + + // ---- running firmware identity (compare against a delta's base_hash) ---- + } else if (is_cmd(a, "self|id", &rest)) { + SelfFwInfo fi; + if (!ota_self_firmware(fi) || !fi.valid) { strcpy(reply, "ERR no EndF (firmware lacks the trailer?)"); return true; } + char hx[17]; mesh::Utils::toHex(hx, fi.body_hash, 8); + int n = snprintf(reply, 160, "self body=%u image=%u base_hash=%s", (unsigned)fi.body_len, (unsigned)fi.image_len, hx); +#if defined(NRF52_PLATFORM) + // nRF52 applies via the bootloader, so surface whether THIS device's bootloader can (delta install gate) + const OtaBlCaps& bl = c.bootloaderCaps(); // cached (flash scanned once) + if (bl.present) snprintf(reply + n, 160 - n, " | bootloader: apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask); + else snprintf(reply + n, 160 - n, " | bootloader: NO mota-apply support (delta install will refuse)"); +#endif + + } else if (is_cmd(a, "install|apply|applydelta", &rest)) { + // Apply the fetched update. Destructive (reflashes + reboots) and GATED, not interactive (no "type + // yes" round-trip — unreliable over LoRa): refuse unless the fetch is COMPLETE, then the apply path + // validates in order (payload hash -> built-for-this-firmware -> signature/trust) and returns the + // FIRST failing gate, so the operator knows exactly why it refused; it proceeds only if all pass. + if (c.manager.fetchState() != OtaManager::COMPLETE || c.fetch_store.staged_size() == 0) { + sprintf(reply, "ERR no complete update fetched (fetch=%c %u/%u)", + fstate_char(c.manager.fetchState()), (unsigned)c.manager.blocksHave(), + (unsigned)c.manager.blocksTotal()); + return true; + } + // On success the slot is armed but NOT yet rebooted — defer so this reply reaches the operator first; + // the mesh loop reboots once it has been transmitted (same path used by auto-install). + char m2[100]; + bool ok = c.apply_fetched(m2); + sprintf(reply, "%s | %s", ok ? "OK" : "ERR", m2); + + // ---- external folder relay: advertise + serve `.mota` from a host daemon over the seeder UART, so the + // node hosts MANY images (any architecture) it doesn't hold in flash. Trustless (fetchers verify). -- + } else if (is_cmd(a, "folder|fold", &rest)) { + const char* p = rest; + if (strncmp(p, "on", 2) == 0) { +#if defined(OTA_FOLDER_SERIAL) + if (!c.serving) c.serving = ota_serve_self(c, 0); // keep serving our own fw alongside the folder + char m2[120]; c.attach_folder(m2, sizeof(m2)); c.manager.announce(); + strncpy(reply, m2, 159); reply[159] = 0; +#else + strcpy(reply, "ERR not built with OTA_FOLDER_SERIAL (set the seeder UART in platformio.ini)"); +#endif + } else if (strncmp(p, "off", 3) == 0) { + c.detach_folder(); c.manager.announce(); + strcpy(reply, "OK folder detached (still serving own fw)"); + } else { // status + list served entries (* = our own fw) + int n = snprintf(reply, 159, "folder=%s serving=%u:", c.folder_active ? "on" : "off", + (unsigned)c.manager.servedCount()); + for (uint8_t i = 0; i < c.manager.servedCount() && n < 148; i++) { + const OtaManager::ServeEntry* e = c.manager.servedEntry(i); + if (!e) break; + char midhx[9]; mesh::Utils::toHex(midhx, e->mid, 4); + n += snprintf(reply + n, 159 - n, " %s%s/%08X", e->is_self ? "*" : "", midhx, (unsigned)e->target_id); + } + } + + // ---- policy config (persisted via NodePrefs). conservative defaults: autofetch/autoinstall off ---- + } else if (is_cmd(a, "config|cfg|set", &rest)) { + const char* p = rest; + if (strncmp(p, "autofetch ", 10) == 0) { + const char* v = p + 10; + uint8_t pol = strncmp(v, "any", 3) == 0 ? OtaManager::AUTOFETCH_ANY + : strncmp(v, "signed", 6) == 0 ? OtaManager::AUTOFETCH_SIGNED + : strncmp(v, "off", 3) == 0 ? OtaManager::AUTOFETCH_OFF : 0xFF; + if (pol == 0xFF) { strcpy(reply, "ERR usage: ota config autofetch "); return true; } + c.manager.set_autofetch(pol); c.config_dirty = true; strcpy(reply, "OK autofetch updated (saved)"); + } else if (strncmp(p, "autoinstall ", 12) == 0) { + const char* v = p + 12; + uint8_t pol = strncmp(v, "trusted", 7) == 0 ? OtaContext::AUTOINSTALL_TRUSTED + : strncmp(v, "off", 3) == 0 ? OtaContext::AUTOINSTALL_OFF : 0xFF; + if (pol == 0xFF) { strcpy(reply, "ERR usage: ota config autoinstall "); return true; } + c.autoinstall = pol; c.config_dirty = true; strcpy(reply, "OK autoinstall updated (saved)"); + } else if (strncmp(p, "checkpoint ", 11) == 0) { // resume checkpoint cadence (blocks; 0=never) + long n = atol(p + 11); + 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 if (strncmp(p, "hops ", 5) == 0) { // OTA flood reach in hops (0 = direct only) + long h = atol(p + 5); + if (h < 0 || h > 8) { strcpy(reply, "ERR usage: ota config hops <0..8> (hops; 0 = direct only)"); return true; } + c.manager.set_max_hops((uint8_t)h); c.config_dirty = true; + sprintf(reply, "OK OTA reach = %ld hop%s (saved)%s", h, h == 1 ? "" : "s", h == 0 ? " — direct only" : ""); + } else { // show current policy + uint8_t af = c.manager.autofetch(); + sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u advert=%umin hops=%u 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.manager.advert_mins(), + (unsigned)c.manager.max_hops(), (unsigned)c.allow.count()); + } + + // ---- trusted signer allowlist (security config; persisted): `ota key add|rm ` / `ota key` lists ---- + } else if (is_cmd(a, "key|keys", &rest)) { + const char* p = rest; + if (strncmp(p, "add ", 4) == 0) { + uint8_t pub[32]; + if (mesh::Utils::fromHex(pub, 32, p + 4) && c.allow.add(pub)) { c.config_dirty = true; strcpy(reply, "OK key added (saved)"); } + else strcpy(reply, "ERR key"); + } else if (strncmp(p, "rm ", 3) == 0 || strncmp(p, "remove ", 7) == 0) { + uint8_t pub[32]; const char* h = p + (p[0] == 'r' && p[1] == 'm' ? 3 : 7); + if (mesh::Utils::fromHex(pub, 32, h) && c.allow.remove(pub)) { c.config_dirty = true; strcpy(reply, "OK removed (saved)"); } + else strcpy(reply, "ERR"); + } else { // bare `ota key` (or `key list`) -> show them + int n = snprintf(reply, 160, "trusted signer keys (%u):", (unsigned)c.allow.count()); + for (uint8_t i = 0; i < c.allow.count() && n < 140; i++) { + char hx[17]; mesh::Utils::toHex(hx, c.allow.get(i), 8); + n += snprintf(reply + n, 160 - n, " %s", hx); + } + if (c.allow.count() == 0) strcpy(reply, "no trusted signer keys yet (add one with `ota key add `)"); + } + + } else { + strcpy(reply, "Unknown OTA command. Type `ota help`."); + } + return true; +} + +// Raw / internal primitives (manual content load + low-level apply steps), under `ota dev ...`. +static bool handle_dev(const char* d, char* reply, OtaContext& c) { + if (strncmp(d, "stage ", 6) == 0) { + uint32_t sz = parse_u32(d + 6); + if (sz == 0 || sz > OTA_SERVE_BUF_SIZE) { sprintf(reply, "ERR size 1..%u", OTA_SERVE_BUF_SIZE); } + else { memset(c.serve_buf, 0xFF, sz); c.serve_expected = sz; c.serving = false; + sprintf(reply, "OK stage %u bytes", (unsigned)sz); } + + } else if (strncmp(d, "recv ", 5) == 0) { + const char* p = d + 5; uint32_t off = parse_u32(p); + const char* hex = strchr(p, ' '); + if (!hex) { strcpy(reply, "ERR usage: ota dev recv "); return true; } + hex++; + int blen = (int)strlen(hex) / 2; + uint8_t tmp[80]; + if (blen <= 0 || blen > (int)sizeof(tmp) || !mesh::Utils::fromHex(tmp, blen, hex)) strcpy(reply, "ERR hex"); + else if (off + blen > c.serve_expected) strcpy(reply, "ERR off>size (stage first)"); + else { memcpy(c.serve_buf + off, tmp, blen); sprintf(reply, "OK %d@%u", blen, (unsigned)off); } + + } else if (strncmp(d, "serve self", 10) == 0) { // host our own running firmware, served from flash + if (ota_serve_self(c, 0)) { + c.serving = true; + char midhx[9]; mesh::Utils::toHex(midhx, c.serve_self_manifest + 20, 4); + uint32_t img = (uint32_t)c.serve_self_manifest[11] | ((uint32_t)c.serve_self_manifest[12] << 8) + | ((uint32_t)c.serve_self_manifest[13] << 16) | ((uint32_t)c.serve_self_manifest[14] << 24); + sprintf(reply, "OK serving self fw mid=%s (%u B, flash-backed) — peers can pull it", midhx, (unsigned)img); + } else strcpy(reply, "ERR serve self (no EndF / image too big / OOM)"); + } else if (strncmp(d, "serve", 5) == 0) { + c.serving = c.manager.serve(c.serve_buf, c.serve_expected); + if (!c.serving) { strcpy(reply, "ERR serve (bad .mota)"); return true; } + VerifyResult r = ota_verify(c.serve_buf, c.serve_expected, c.allow); + sprintf(reply, "OK serving | root=%d img=%d sig=%d trust=%d", r.root_ok, r.image_ok, r.sig_ok, r.trusted); + + } else if (strncmp(d, "resume", 6) == 0) { // re-adopt a container already staged in flash (test/debug) + bool ok = c.manager.resumeStaged(nullptr); + sprintf(reply, "%s resume: sess=%c %u/%u", ok ? "OK" : "ERR", fstate_char(c.manager.fetchState()), + (unsigned)c.manager.blocksHave(), (unsigned)c.manager.blocksTotal()); + + } else if (strncmp(d, "announce", 8) == 0) { + if (!c.serving) { strcpy(reply, "ERR not serving (ota dev serve first)"); return true; } + c.manager.announce(); + strcpy(reply, "OK announced"); + + } else if (strncmp(d, "verify", 6) == 0) { + const uint8_t* buf; uint32_t len; + if (c.manager.fetchState() == OtaManager::COMPLETE) { buf = c.fetch_store.data(); len = c.fetch_store.staged_size(); } + else { buf = c.serve_buf; len = c.serve_expected; } + if (len == 0 || !buf) { strcpy(reply, "ERR nothing to verify (flash-staged: applydelta verifies)"); return true; } + VerifyResult r = ota_verify(buf, len, c.allow); + sprintf(reply, "verify parsed=%d root=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d", + r.parsed, r.root_ok, r.image_ok, r.is_signed, r.sig_ok, r.trusted, r.integrity_ok(), r.auto_appliable()); + + } else if (strncmp(d, "want ", 5) == 0) { + const char* p = d + 5; while (*p == ' ') p++; + if (strncmp(p, "auto", 4) == 0) { c.manager.want(0); c.manager.want_mid(nullptr); strcpy(reply, "OK auto (own target only)"); } + else { uint32_t t = (uint32_t)strtoul(p, nullptr, 16); c.manager.want(t); c.manager.want_mid(nullptr); + sprintf(reply, "OK cross-target: will fetch %08X (you ensure HW compatible)", (unsigned)t); } + + } else if (strncmp(d, "apply", 5) == 0) { + const char* sub = d + 5; while (*sub == ' ') sub++; + if (strncmp(sub, "slot", 4) == 0) { + uint32_t addr = 0, size = 0; + if (ota_apply_slot_info(&addr, &size)) sprintf(reply, "inactive slot addr=0x%X size=%u", (unsigned)addr, (unsigned)size); + else strcpy(reply, "ERR no A/B slot (apply unsupported on this build)"); + } else if (strncmp(sub, "manifest", 8) == 0) { + if (ota_apply_set_manifest(c.serve_buf, c.serve_expected, c.allow, c.apply_st)) + sprintf(reply, "manifest ok img=%u sig=%d trust=%d", (unsigned)c.apply_st.image_size, c.apply_st.sig_ok, c.apply_st.trusted); + else strcpy(reply, "ERR manifest parse / not full-image / unsupported"); + } else if (strncmp(sub, "verify", 6) == 0) { + bool ok = ota_apply_verify_slot(c.apply_st); + sprintf(reply, "slot image_hash %s (size=%u)", ok ? "MATCH" : "MISMATCH", (unsigned)c.apply_st.image_size); + } else if (strncmp(sub, "commit", 6) == 0) { + if (!c.apply_st.slot_ok) { strcpy(reply, "ERR run 'ota dev apply verify' first (slot must match)"); return true; } + ota_apply_commit(); // set boot partition + reboot; no return + strcpy(reply, "ERR commit failed (no A/B slot?)"); + } else { + strcpy(reply, "ERR ota dev apply (slot|manifest|verify|commit)"); + } + + } else if (strncmp(d, "clear", 5) == 0) { + c.serve_expected = 0; c.serving = false; c.fetch_store.clear(); c.manager.reset_session(); + strcpy(reply, "OK cleared"); + + } else { + strcpy(reply, "ota dev: stage|recv|serve|announce|verify|want|apply slot|manifest|verify|commit|clear"); + } + return true; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaCli.h b/src/helpers/ota/OtaCli.h new file mode 100644 index 0000000000..987eebb516 --- /dev/null +++ b/src/helpers/ota/OtaCli.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +// Text-CLI surface for OTA (P3/P5). Wired from CommonCLI (and reachable over LoRa remote-admin). +// Kept out of CommonCLI.cpp itself so the OTA state (allowlist, staging store) lives in the OTA module. + +namespace mesh { +namespace ota { + +// Handle an "ota ..." command. `command` is the full line (starts with "ota"). Fills `reply` +// (<= ~160 bytes, as per the CLI buffer). Returns true if it was an OTA command. +bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board); + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaContext.cpp b/src/helpers/ota/OtaContext.cpp new file mode 100644 index 0000000000..10f2bba4b7 --- /dev/null +++ b/src/helpers/ota/OtaContext.cpp @@ -0,0 +1,12 @@ +#include "OtaContext.h" + +namespace mesh { +namespace ota { + +OtaContext& ota_ctx() { + static OtaContext ctx; + return ctx; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaContext.h b/src/helpers/ota/OtaContext.h new file mode 100644 index 0000000000..2a67c23804 --- /dev/null +++ b/src/helpers/ota/OtaContext.h @@ -0,0 +1,191 @@ +#pragma once + +#include // snprintf (hw_id mismatch message) +#include // strncmp/strncpy (hw_id) +#include "OtaManager.h" +#include "OtaStore.h" +#include "SignerAllowlist.h" +#include "OtaApply.h" +#include "OtaFormat.h" +#include "OtaSelf.h" // ota_self_firmware() — prefer self-describing EndF identity at begin() +#include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read +#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) + #include "OtaStoreFlashNrf52.h" +#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) + #include "OtaStoreFlashEsp32.h" +#endif +#if defined(OTA_FOLDER_SERIAL) + #include "MotaSourceSerial.h" // relay an external folder served by a host daemon over the USB serial + #ifndef OTA_FOLDER_SERIAL_STREAM + #define OTA_FOLDER_SERIAL_STREAM Serial // default: the same USB console the CLI uses (no extra HW) + #endif + #ifndef OTA_FOLDER_SERIAL_BAUD + #define OTA_FOLDER_SERIAL_BAUD 115200 + #endif + // The console Serial is already begun by the example; a DEDICATED UART (override the stream) needs init, + // so define OTA_FOLDER_SERIAL_BEGIN to have attach_folder() call .begin(baud) on it. +#endif + +// Per-device OTA singleton shared by the CLI (OtaCli) and the mesh adapter (the example's MyMesh). +// Holds the session engine, a staging store (fetch), a RAM serve buffer, and the signer allowlist. +// nRF52 stages into FLASH (OtaStoreFlashNrf52): a delta can be 100 KB+, too big to hold in RAM, and the +// COMPLETE container must persist so the bootloader can apply it after reboot. A flash page-erase halts +// the CPU (~85 ms) and starves the LoRa RX, so the store COALESCES writes to the 4 KB page (the erase +// unit) and commits each page once, off the per-packet path (see OtaManager.h) — RAM stays O(one page). +// (v1 has no mid-transfer resume; an interrupted fetch simply restarts.) ESP32/native use the RAM store. + +namespace mesh { +namespace ota { + +#ifndef OTA_SERVE_BUF_SIZE +#define OTA_SERVE_BUF_SIZE 16384 +#endif +#ifndef OTA_FETCH_BUF_SIZE +#define OTA_FETCH_BUF_SIZE 16384 +#endif + +struct OtaContext { + OtaManager manager; +#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) + OtaStoreFlashNrf52 fetch_store; // persistent flash staging (survives reboot; large deltas) +#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) + OtaStoreFlashEsp32 fetch_store; // stages in the inactive A/B slot (delta + full, RX-safe) +#else + OtaStoreRam fetch_store; +#endif + SignerAllowlist allow; + uint8_t serve_buf[OTA_SERVE_BUF_SIZE]; + uint32_t serve_expected = 0; // size declared by `ota stage` + bool serving = false; // manager.serve() succeeded + // flash-backed self-serve: cached merkle leaves (heap, freed on re-serve) + assembled manifest of our + // own running firmware. The payload is read from flash per block; only the metadata is held in RAM. + // serve_self_proof is the proof-gen working buffer (>= block_count*4) — sized to OUR image's block + // count (the manager's fixed 4 KB scratch only covers <=1024 blocks; a >1 MB image needs more). + uint8_t* serve_self_leaves = nullptr; + uint8_t* serve_self_proof = nullptr; + uint8_t serve_self_manifest[MOTA_MFL]; // fixed-layout full+unsigned manifest-minus-leaves (197 B) + ApplyState apply_st; // pending apply (P6) + + // OTA policy (persisted via NodePrefs; autofetch lives in the manager). Conservative defaults: a fresh + // node discovers + announces but never fetches/installs without operator intent. + static const uint8_t AUTOINSTALL_OFF = 0, AUTOINSTALL_TRUSTED = 1; + uint8_t autoinstall = AUTOINSTALL_OFF; // 1 = auto-apply a COMPLETE fetch IF signed + allowlisted + bool config_dirty = false; // CLI set a policy/key -> CommonCLI persists + clears + char hw_id[33] = {0}; // this device's hardware tag (from board.getOtaHwId(), set in begin) + + // True if the staged .mota's hw_id is compatible with this device: equal tags, or either side empty + // ("unknown" -> can't enforce -> permissive). Brick-safety gate for apply (esp. manual cross-target). + bool hwMatches(const uint8_t* mhw /*32B, may be null*/) const { + if (!hw_id[0] || !mhw) return true; + bool declared = false; for (int i = 0; i < 32; i++) if (mhw[i]) { declared = true; break; } + if (!declared) return true; + return strncmp((const char*)mhw, hw_id, 32) == 0; + } + + // Apply the COMPLETE fetched .mota (platform dispatch) and arm the slot; sets apply_pending on success + // so the deferred-reboot path (mesh loop) takes over. Caller ensures the fetch is COMPLETE. Shared by + // manual `ota applydelta` and the auto-install path. + bool apply_fetched(char* msg) { + // hardware-compatibility gate (brick-safety) — refuse a .mota whose hw_id is for different hardware, + // independent of signature; covers a manual cross-target `ota dev want` onto an incompatible board. + { + uint8_t hdr[8], mb[256]; + uint32_t total = fetch_store.staged_size(); + if (total >= 13 && fetch_store.read(0, hdr, 8) && memcmp(hdr, MOTA_MAGIC, 4) == 0) { + uint32_t mr = total - 8; if (mr > sizeof(mb)) mr = sizeof(mb); + MotaManifest mm; + if (fetch_store.read(8, mb, mr) && mota_parse_manifest(mb, mr, mm) && !hwMatches(mm.hw_id)) { + char want[33] = {0}; memcpy(want, mm.hw_id, 32); + snprintf(msg, 96, "refused: .mota hw_id '%.32s' != this device '%s' (incompatible hardware)", want, hw_id); + return false; + } + } + } + bool ok; +#if defined(NRF52_PLATFORM) + ok = ota_apply_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg); +#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) + ok = ota_apply_detools_mota(fetch_store, allow, apply_st, msg); +#else + ok = ota_apply_detools_mota(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg); +#endif + if (ok) apply_pending = true; + return ok; + } + + // Deferred apply-reboot: a verified `ota applydelta` approves the update but does NOT reboot inline, + // so the CLI can first deliver the "verified; applying" reply (over LoRa it's the only way the + // operator learns the apply started). The mesh loop then calls ota_reboot_to_apply() once that reply + // has actually been transmitted. apply_at/apply_hard are mesh-clock deadlines the loop fills in. + bool apply_pending = false; + uint32_t apply_at = 0; // earliest reboot time (lets the reply get queued + start sending) + uint32_t apply_hard = 0; // hard cap, in case the TX queue never idles on a busy node + + // Bootloader OTA-apply capability (nRF52): can THIS device's bootloader apply a .mota? Read from flash + // ONCE (the scan is ~40 KB) and cached in RAM. On other platforms present=false (apply is in-app). + OtaBlCaps _bl_caps; + bool _bl_caps_read = false; + const OtaBlCaps& bootloaderCaps() { + if (!_bl_caps_read) { _bl_caps = ota_bootloader_caps(); _bl_caps_read = true; } + return _bl_caps; + } + + // --- discovery: the "what mOTAs are available around me" view ---------------------------------- + // The catalog (heard mOTAs) + the heard-sources table now live in OtaManager (built from beacons + + // OTA_HAVE catalog replies, the two-tier discovery). `ota neighbors` renders manager.catalogRow(); + // `ota pull` acts on a mid. Here we only keep the fetch-session age stamp. + uint32_t session_started_ms = 0; // when the fetch session last left IDLE (for the age display) + uint8_t prev_fstate = OtaManager::IDLE; + bool folder_active = false; // an external `.mota` folder is attached + being relayed + + // Attach/detach an external folder of `.mota` served by a host daemon over the seeder UART (the node + // then advertises + relays them alongside its own fw). Only built when OTA_FOLDER_SERIAL is configured. +#if defined(OTA_FOLDER_SERIAL) + bool attach_folder(char* msg, size_t cap) { + static SerialMotaSource src(OTA_FOLDER_SERIAL_STREAM, 600); +#ifdef OTA_FOLDER_SERIAL_BEGIN + OTA_FOLDER_SERIAL_STREAM.begin(OTA_FOLDER_SERIAL_BAUD); // dedicated UART; console is already up +#endif + manager.clear_sources(); // idempotent re-attach + if (!manager.add_source(&src)) { strncpy(msg, "ERR no free source slot", cap); return false; } + folder_active = true; + snprintf(msg, cap, "OK folder attached (serial) — serving %u mOTA total (own fw + folder)", + (unsigned)manager.servedCount()); + return true; + } +#endif + void detach_folder() { manager.clear_sources(); folder_active = false; } + + void track_session(uint8_t fstate, uint32_t now) { // stamp the session start (age display) + if (fstate != prev_fstate) { + if (prev_fstate == OtaManager::IDLE && fstate != OtaManager::IDLE) session_started_ms = now; + prev_fstate = fstate; + } + } + + void begin(uint32_t target_id, OtaSend send, void* ctx, const char* hw = nullptr) { + // Prefer the firmware's SELF-DESCRIBING EndF identity (docs §2) over the build-flag values the caller + // passed — it's correct on any build (build.sh injection, bare IDE build, ...), so `ota ls`/`status` + // and fetch-routing show the right hardware/role instead of 0 / "". + SelfFwInfo _fi; + if (ota_self_firmware(_fi) && _fi.valid) { + if (_fi.target_id) target_id = _fi.target_id; + if (_fi.hw_id[0]) hw = _fi.hw_id; + } + manager.begin(target_id, send, ctx); + if (hw) { strncpy(hw_id, hw, sizeof(hw_id) - 1); hw_id[sizeof(hw_id) - 1] = 0; } + // a node only fetches firmware it can apply: ESP32 A/B -> sequential, nRF52 single-slot -> in-place +#if defined(NRF52_PLATFORM) + manager.set_apply_codec(CODEC_DETOOLS_INPLACE); +#elif defined(ESP32_PLATFORM) + manager.set_apply_codec(CODEC_DETOOLS_SEQUENTIAL); // preferred (streams straight to the slot) + manager.set_apply_codec2(CODEC_DETOOLS_INPLACE); // also accepted -> a single in-place .mota fits both +#endif + manager.set_fetch_store(&fetch_store); + } +}; + +OtaContext& ota_ctx(); // process-wide singleton + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaDebug.h b/src/helpers/ota/OtaDebug.h new file mode 100644 index 0000000000..a126cdec5c --- /dev/null +++ b/src/helpers/ota/OtaDebug.h @@ -0,0 +1,11 @@ +#pragma once + +// Opt-in OTA tracing over Serial: build with -D OTA_DEBUG to watch the fetch (ADV/REQ/block/page-flush) +// during bring-up. Compiles to nothing otherwise, and on the native host (no Arduino), so it never +// touches a non-debug or test build. +#if defined(OTA_DEBUG) && defined(ARDUINO) + #include + #define OTA_DBG(...) do { Serial.printf(__VA_ARGS__); } while (0) +#else + #define OTA_DBG(...) do {} while (0) +#endif diff --git a/src/helpers/ota/OtaFlashLayout_nrf52.h b/src/helpers/ota/OtaFlashLayout_nrf52.h new file mode 100644 index 0000000000..289348fdf5 --- /dev/null +++ b/src/helpers/ota/OtaFlashLayout_nrf52.h @@ -0,0 +1,39 @@ +#pragma once + +// Shared OTA flash-layout constants for the nRF52840 (RAK4631) single-slot delta-apply path. +// SINGLE SOURCE OF TRUTH — keep byte-identical with the bootloader's src/ota_layout.h. +// +// The running app occupies [APP_BASE, app_end]; the primary LittleFS (InternalFS) starts at FS_START. +// MeshCore stages a verified+approved `.mota` in the free flash below FS_START (bottom-aligned), then +// sets GPREGRET_OTA_APPLY and resets; the bootloader scans [APP_BASE, FS_START) for it and applies it +// in place. These must match the bootloader and the running SoftDevice's app base. + +#include + +namespace mesh { +namespace ota { + +static const uint32_t MOTA_NRF52_APP_BASE = 0x00026000u; // S140 end (== CODE_REGION_1_START) +// Staging ceiling: the lowest filesystem region above the app. RAK4631 companion builds use the +// extrafs ldscript with ExtraFS at 0xD4000..0xED000 (and InternalFS at 0xED000), while the repeater +// uses the default ldscript (InternalFS at 0xED000, 0xD4000..0xED000 free). 0xD4000 is the safe +// universal ceiling for ALL RAK4631 roles: staging below it never touches ExtraFS or InternalFS, and +// the app (~520 KB) sits well below 0xD4000 either way. +static const uint32_t MOTA_NRF52_FS_START = 0x000D4000u; // ExtraFS start (universal staging ceiling) +static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u; +static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8 + +// In-place patches are built with --inplace-memory = this (the apply workspace, from APP_BASE up). +// It must hold the new image (~520 KB) yet leave the staged mota room below FS_START: workspace ends +// at APP_BASE+this = 0xBE000, leaving 0xBE000..0xD4000 (~88 KB) for the staged delta. The bootloader +// also bounds writes to < the (scanned) mota start, so a mis-sized memory still fails safe. +static const uint32_t MOTA_NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB (APP_BASE .. 0xBE000) + +// Bootloader flash region (nRF52840: 39 KB ending just below the CF2/MBR-params pages). The app scans +// this for the bootloader capability marker (OtaBlInfo.h) to know whether THIS device's bootloader can +// actually apply a .mota before staging+approving+rebooting. +static const uint32_t MOTA_NRF52_BL_START = 0x000F4000u; +static const uint32_t MOTA_NRF52_BL_END = 0x000FE000u; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaFormat.h b/src/helpers/ota/OtaFormat.h new file mode 100644 index 0000000000..6a8da2e2d6 --- /dev/null +++ b/src/helpers/ota/OtaFormat.h @@ -0,0 +1,92 @@ +#pragma once + +#include +#include + +// On-the-wire constants for the MeshCore OTA `.mota` container and protocol. +// Normative definition: docs/ota_protocol.md (v1). Mirrors tools/mota/motalib.py. +// +// Portable: no Arduino / RadioLib includes. Compiles on the native host (unit tests) and on device. + +namespace mesh { +namespace ota { + +// ---- container framing ---------------------------------------------------- +static const uint8_t MOTA_MAGIC[4] = { 'm', 'O', 'T', 'A' }; // 6D 4F 54 41 +static const uint8_t MOTA_TRAILER[5] = { 'v', 'k', '4', '9', '6' }; // 76 6B 34 39 36 +static const uint8_t ENDF_MAGIC[4] = { 'E', 'n', 'd', 'F' }; // 45 6E 64 46 +// Fixed 56-byte trailer (docs/ota_protocol.md §2): marker(4) body_len(4) body_hash8(8) + a self-describing +// identity block fw_version(4) target_id(4) hw_id(32). No optional/variable parts. +static const uint32_t ENDF_LEN = 56; + +// ---- manifest ------------------------------------------------------------- +static const uint8_t MOTA_FORMAT_VER = 2; // fixed-layout manifest (see offsets below) +static const uint8_t HASH_ALGO_SHA256 = 0x12; // multihash code + +// Fixed manifest layout (manifest-minus-leaves) — every field is present at a constant offset, so the +// parser is plain offset reads (docs/ota_protocol.md §4). base_hash/signer_pubkey/signature are always +// present (zero-filled when not applicable); only leaves[] (after `approval`) is variable. +static const uint32_t MOTA_OFF_BASE_HASH = 89; // 8 (zero for a full image) +static const uint32_t MOTA_OFF_SIGNER = 97; // 32 (zero when unsigned) +static const uint32_t MOTA_OFF_SIGNATURE = 129; // 64 (zero when unsigned) — covers manifest[0,129) +static const uint32_t MOTA_OFF_APPROVAL = 193; // 4 +static const uint32_t MOTA_MFL = 197; // manifest-minus-leaves length (constant) +static const uint32_t MOTA_SIGNED_LEN = 129; // bytes the signature covers (manifest[0, signer_end)) + +// hw_id: a fixed 32-byte, NUL-padded ASCII string naming the hardware a firmware can boot on (e.g. +// "RAK4631", "Heltec_v3"). Same hw_id == bootable-compatible (a role switch on the same board keeps it; +// different MCU/board differs). It sits in the SIGNED region of the manifest, so it can't be tampered. +// The applier refuses a `.mota` whose hw_id differs from the device's own (brick-safety, esp. for a manual +// cross-target `ota dev want`). An empty hw_id on either side = "unknown", and the check is skipped. +static const uint8_t MOTA_HW_ID_LEN = 32; + +static const uint8_t MFLAG_FULL = 0x01; // 0 = delta/partial, 1 = full image +static const uint8_t MFLAG_SIGNED = 0x02; + +static const uint8_t CODEC_FULL = 0; +static const uint8_t CODEC_DETOOLS_SEQUENTIAL = 1; +static const uint8_t CODEC_DETOOLS_INPLACE = 2; + +// ---- firmware version ------------------------------------------------------ +// The comparable uint32 carried in the manifest + EndF identity: MAJOR<<24 | MINOR<<16 | PATCH<<8 | PRE. +// 0 == "unknown" (e.g. a dev build with no dotted version). Defined once here so the pack/unpack layout +// isn't re-derived with raw shifts at each call site (OtaSelf builds it, OtaCli renders it). +struct FwVersion { + uint8_t major, minor, patch, prerelease; + static FwVersion unpack(uint32_t v) { + return { (uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v }; + } + uint32_t pack() const { + return ((uint32_t)major << 24) | ((uint32_t)minor << 16) | ((uint32_t)patch << 8) | prerelease; + } +}; + +// ---- hash truncations ----------------------------------------------------- +static const uint8_t MH4 = 4; // sha2-256:4 (merkle leaves/nodes/root/proofs) +static const uint8_t MH8 = 8; // sha2-256:8 (base/EndF body hash) +static const uint8_t MH32 = 32; // sha2-256:32 (image security anchor) + +// ---- approval marker (manifest field, after the signature) ---------------- +static const uint8_t APPROVAL_NOT[4] = { 0xFF, 0xFF, 0xFF, 0xFF }; +static const uint8_t APPROVAL_YES[4] = { 'A', 'P', 'R', 'V' }; // 41 50 52 56 + +// ---- LoRa protocol -------------------------------------------------------- +// The packet payload type is PAYLOAD_TYPE_OTA (0x0C), defined in src/Packet.h for the core dispatch. + +enum OtaMsgType : uint8_t { + OTA_ADV = 0x01, + OTA_QUERY = 0x02, + OTA_HAVE = 0x03, + OTA_GET_MANIFEST = 0x04, + OTA_MANIFEST = 0x05, + OTA_REQ = 0x06, // request a window of blocks' DATA fragments + OTA_DATA = 0x07, // one fragment of a block's data (self-describing by frag_off; no proof) + OTA_REQ_PROOF = 0x08, // request the merkle proof for one block (data + proof are fetched separately) + OTA_PROOF = 0x09, // the merkle proof for one block +}; + +static const uint16_t OTA_DEFAULT_BLOCK_SIZE = 1024; +static const uint8_t OTA_DEFAULT_HOP_LIMIT = 3; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaManager.cpp b/src/helpers/ota/OtaManager.cpp new file mode 100644 index 0000000000..e204a05795 --- /dev/null +++ b/src/helpers/ota/OtaManager.cpp @@ -0,0 +1,780 @@ +#include "OtaManager.h" +#include "OtaProtocol.h" +#include "MerkleTree.h" +#include "Multihash.h" +#include "OtaByteIO.h" +#include "OtaDebug.h" +#include + +namespace mesh { +namespace ota { + +void OtaManager::begin(uint32_t my_target_id, OtaSend send, void* ctx) { + _target = my_target_id; _send = send; _ctx = ctx; + _fstate = IDLE; _have = 0; _fbc = 0; + _n_serve = 0; _n_src_obj = 0; _view0.valid = false; _srcv.valid = false; _fetch_served = false; + for (uint8_t i = 0; i < 8; i++) _recent_blk[i] = NO_BLOCK; // empty slot (never a real block index) +} + +// ---------------- serve (multi-mota registry) ---------------- +// +// A node offers a SET of mOTAs: its own firmware (view0) plus any external "folder" sources (OtaSource). +// Every fetch message carries the manifest_id, so a request dispatches to the matching ServeView via +// resolve() — view0 is resident; an external mota is (re)loaded on demand into _srcv. The catalog (what +// we advertise / answer OTA_QUERY with) is the lightweight _serve[] registry. + +bool OtaManager::serve(const uint8_t* mota, uint32_t len) { + if (!mota_parse(mota, len, _view0.m)) return false; + _view0.mfl = (uint16_t)(_view0.m.leaves - _view0.m.manifest_start); // contiguous container + _view0.read = nullptr; _view0.read_ctx = nullptr; // payload is contiguous _view0.m.payload + _view0.scratch = _scratch; _view0.scratch_sz = sizeof(_scratch); // <=1024 blocks (RAM .mota is small) + _view0.valid = true; + registerSelfEntry(); + return true; +} + +bool OtaManager::serve_self(const uint8_t* manifest, uint16_t mfl, const uint8_t* leaves, + uint32_t block_count, uint8_t* proof_scratch, uint32_t proof_scratch_sz, + ServeReadFn read, void* ctx) { + if (proof_scratch_sz < (uint64_t)block_count * 4) return false; // proof-gen needs count*4 working bytes + if (!mota_parse_manifest(manifest, mfl, _view0.m)) return false; // fixed fields: root, image_hash, sizes + _view0.m.manifest_start = manifest; + _view0.m.leaves = leaves; // pre-computed, caller-owned (heap) + _view0.m.payload = nullptr; // read on demand via `read` + _view0.m.block_count = block_count; + _view0.mfl = mfl; _view0.read = read; _view0.read_ctx = ctx; + _view0.scratch = proof_scratch; _view0.scratch_sz = proof_scratch_sz; // sized for our (large) image + _view0.valid = true; + registerSelfEntry(); + return true; +} + +// (Re)build registry slot 0 from view0 (our own fw / RAM mota). Keeps any source entries in [1..]. +void OtaManager::registerSelfEntry() { + if (!_view0.valid) return; + ServeEntry& e = _serve[0]; + memcpy(e.mid, _view0.m.merkle_root, 4); + e.target_id = _view0.m.target_id; e.fw_version = _view0.m.fw_version; + e.codec_id = _view0.m.codec_id; e.flags = _view0.m.flags; e.have_count = _view0.m.block_count; + e.is_self = true; e.is_fetch = false; e.src = nullptr; e.src_idx = 0; + if (_n_serve == 0) _n_serve = 1; +} + +bool OtaManager::add_source(MotaSource* src) { + if (!src || _n_src_obj >= OTA_MAX_SOURCE_OBJ) return false; + _src_list[_n_src_obj++] = src; + refresh_sources(); + return true; +} + +void OtaManager::refresh_sources() { + uint8_t base = _view0.valid ? 1 : 0; // entry 0 stays our own fw + if (_view0.valid) registerSelfEntry(); + _n_serve = base; + for (uint8_t s = 0; s < _n_src_obj; s++) { + MotaSource* src = _src_list[s]; + if (!src) continue; + uint8_t cnt = src->count(); + for (uint8_t i = 0; i < cnt && _n_serve < OTA_MAX_SERVE; i++) { + MotaDesc d; + if (!src->describe(i, d)) continue; + if (serveEntryIndex(d.mid) >= 0) continue; // already offered (e.g. our own fw in the folder) + ServeEntry& e = _serve[_n_serve++]; + memcpy(e.mid, d.mid, 4); + e.target_id = d.target_id; e.fw_version = d.fw_version; + e.codec_id = d.codec_id; e.flags = d.flags; e.have_count = d.block_count; // a folder mota is fully held + e.is_self = false; e.is_fetch = false; e.src = src; e.src_idx = i; e.desc = d; + } + } + // re-seed a completed download (epidemic spread) as one more served mota, backed by the fetch store + if (_fetch_served && _n_serve < OTA_MAX_SERVE && serveEntryIndex(_fetch_desc.mid) < 0) { + ServeEntry& e = _serve[_n_serve++]; + memcpy(e.mid, _fetch_desc.mid, 4); + e.target_id = _fetch_desc.target_id; e.fw_version = _fetch_desc.fw_version; + e.codec_id = _fetch_desc.codec_id; e.flags = _fetch_desc.flags; e.have_count = _fetch_desc.block_count; + e.is_self = false; e.is_fetch = true; e.src = nullptr; e.src_idx = 0; e.desc = _fetch_desc; + } + _srcv.valid = false; // a loaded source view may now be stale; reloads on demand +} + +void OtaManager::clear_sources() { + _n_src_obj = 0; _srcv.valid = false; + _n_serve = _view0.valid ? 1 : 0; + if (_view0.valid) registerSelfEntry(); +} + +int OtaManager::serveEntryIndex(const uint8_t* mid) const { + for (uint8_t i = 0; i < _n_serve; i++) + if (memcmp(_serve[i].mid, mid, 4) == 0) return i; + return -1; +} + +OtaManager::ServeView* OtaManager::resolve(const uint8_t* mid) { + if (_view0.valid && memcmp(mid, _view0.m.merkle_root, 4) == 0) return &_view0; + if (_srcv.valid && memcmp(mid, _srcv_mid, 4) == 0) return &_srcv; + int i = serveEntryIndex(mid); + if (i < 0) return nullptr; + if (_serve[i].is_self) return _view0.valid ? &_view0 : nullptr; + return loadSource(_serve[i]) ? &_srcv : nullptr; +} + +// Load an external mota into the on-demand _srcv: read its manifest-minus-leaves + leaves[] from the +// source into RAM, parse, and wire a payload reader that streams blocks from the source on REQ. (The +// payload itself is NOT held in RAM — only the small head + the leaves, <=4 KB for <=1024 blocks.) +bool OtaManager::loadSource(const ServeEntry& e) { + const MotaDesc& d = e.desc; + if (d.leaves_off < 8) return false; + if (e.is_fetch ? (_fetch == nullptr) : (e.src == nullptr)) return false; + uint16_t mfl = (uint16_t)(d.leaves_off - 8); + if (mfl == 0 || mfl > sizeof(_src_manifest)) return false; + if (d.block_count == 0 || (uint64_t)d.block_count * 4 > sizeof(_src_leaves)) return false; + // read the manifest-minus-leaves + leaves[] from the backing — an external folder MotaSource, or (for a + // completed download we re-seed) our own fetch store. Container offsets are absolute, so a store read + // at the same offsets works identically. + bool ok = e.is_fetch ? _fetch->read(8, _src_manifest, mfl) + : e.src->read(e.src_idx, 8, _src_manifest, mfl); + if (!ok || !mota_parse_manifest(_src_manifest, mfl, _srcv.m)) return false; + if (memcmp(_srcv.m.merkle_root, d.mid, 4) != 0) return false; // descriptor/bytes disagree + if (_srcv.m.block_count != d.block_count) return false; + ok = e.is_fetch ? _fetch->read(d.leaves_off, _src_leaves, d.block_count * 4) + : e.src->read(e.src_idx, d.leaves_off, _src_leaves, d.block_count * 4); + if (!ok) return false; + _srcv.m.manifest_start = _src_manifest; + _srcv.m.leaves = _src_leaves; + _srcv.m.payload = nullptr; + _srcv.mfl = mfl; + _srcv_rdctx.src = e.is_fetch ? nullptr : e.src; _srcv_rdctx.idx = e.src_idx; + _srcv_rdctx.payload_off = d.payload_off; _srcv_rdctx.store = e.is_fetch ? _fetch : nullptr; + _srcv.read = srcReadTramp; _srcv.read_ctx = &_srcv_rdctx; + _srcv.scratch = _scratch; _srcv.scratch_sz = sizeof(_scratch); + memcpy(_srcv_mid, d.mid, 4); + _srcv.valid = true; + return true; +} + +// ServeReadFn trampoline: payload-relative offset -> absolute read of the backing (external source or fetch store). +bool OtaManager::srcReadTramp(void* c, uint32_t off, uint8_t* buf, uint32_t len) { + SrcReadCtx* x = (SrcReadCtx*)c; + if (x->store) return x->store->read(x->payload_off + off, buf, len); + return x->src->read(x->idx, x->payload_off + off, buf, len); +} + +// After a download COMPLETEs, advertise + serve the staged container so this node re-seeds it to peers +// (epidemic spread: the origin seeds a few, they seed the next ring -> load on the origin is O(log N), not +// O(N)). The completed container has ALL blocks + leaves, so it serves DATA *and* proofs correctly. Re-uses +// the on-demand source view; serving is reactive + lowest-priority, so it never competes with real traffic. +void OtaManager::serveFetched() { + if (!_fetch || _fstate != COMPLETE || _fbc == 0 || _floff < 8) return; + uint16_t mfl = (uint16_t)(_floff - 8); + if (mfl == 0 || mfl > sizeof(_src_manifest)) return; + if ((uint64_t)_fbc * 4 > sizeof(_src_leaves)) return; // proof-gen scratch caps re-seed at <=1024 blocks + uint8_t head[OTA_SRC_MANIFEST_MAX]; + if (!_fetch->read(8, head, mfl)) return; + MotaManifest m; + if (!mota_parse_manifest(head, mfl, m)) return; + MotaDesc& d = _fetch_desc; + memcpy(d.mid, _fid, 4); + d.target_id = m.target_id; d.fw_version = m.fw_version; d.codec_id = m.codec_id; d.flags = m.flags; + d.total_size = _ftotal; d.leaves_off = _floff; d.block_count = _fbc; + d.payload_off = _fpoff; d.payload_size = _fpsize; + _fetch_served = true; + refresh_sources(); // add the fetch entry to the catalog; the set-digest change makes the next beacon advertise it +} + +void OtaManager::unserveFetched() { + if (!_fetch_served) return; + _fetch_served = false; + _srcv.valid = false; // the loaded source view may be the fetch we're dropping + refresh_sources(); +} + +// sha2-256:4 over the SORTED set of mids we serve — peers use it to tell if our offering changed. Sorting +// makes it canonical across nodes regardless of insert order; for a single mota it is mh4(mid) (unchanged). +void OtaManager::setDigest(uint8_t out[4]) const { + if (_n_serve == 0) { memset(out, 0, 4); return; } + uint8_t order[OTA_MAX_SERVE]; + for (uint8_t i = 0; i < _n_serve; i++) order[i] = i; + for (uint8_t i = 1; i < _n_serve; i++) { // insertion sort by mid (n <= 12) + uint8_t v = order[i]; int j = (int)i - 1; + while (j >= 0 && memcmp(_serve[order[j]].mid, _serve[v].mid, 4) > 0) { order[j+1] = order[j]; j--; } + order[j+1] = v; + } + uint8_t cat[OTA_MAX_SERVE * 4]; + for (uint8_t i = 0; i < _n_serve; i++) memcpy(cat + (uint32_t)i * 4, _serve[order[i]].mid, 4); + mh4(out, cat, (size_t)_n_serve * 4); +} + +void OtaManager::announce() { // tiny per-node beacon (constant size, independent of how many mOTAs) + AdvMsg a; + memcpy(a.seeder_id, _seeder_id, 4); + a.n_motas = _n_serve; + setDigest(a.set_digest); + uint8_t b[16]; + emit(b, encode_adv(b, sizeof(b), a), true); +} + +// OTA_QUERY: two roles. (1) OVERHEAR-SUPPRESSION — any node that has a pending query for the same +// {source,digest} cancels it (someone else already asked; the broadcast HAVE is coming). (2) If the query +// is addressed to US, reply with our catalog (broadcast, tagged with our digest so every overhearer caches +// it). All served mOTAs matching filter_target are returned, fragmented if they exceed one packet. +void OtaManager::handleQuery(const uint8_t* m, uint16_t n) { + QueryMsg q; + if (!decode_query(m, n, q)) return; + if (_pq_active && memcmp(_pq_seeder, q.seeder_id, 4) == 0 && memcmp(_pq_digest, q.set_digest, 4) == 0) + _pq_active = false; // (1) suppress our own pending query + if (_n_serve == 0 || memcmp(q.seeder_id, _seeder_id, 4) != 0) return; // (2) only WE answer queries to us + uint8_t dg[4]; setDigest(dg); + uint8_t rowbuf[OTA_MAX_SERVE * OTA_HAVE_ROW_BYTES]; + uint8_t nm = 0; + for (uint8_t i = 0; i < _n_serve; i++) { + const ServeEntry& e = _serve[i]; + if (q.filter_target != 0 && q.filter_target != e.target_id) continue; + uint8_t* row = rowbuf + (uint32_t)nm * OTA_HAVE_ROW_BYTES; + memcpy(row, e.mid, 4); + wr_u32le(row + 4, e.target_id); wr_u32le(row + 8, e.fw_version); + row[12] = e.codec_id; row[13] = e.flags; + uint32_t hc = e.have_count > 0xFFFFu ? 0xFFFFu : e.have_count; // blocks we hold (awareness for fetchers) + row[14] = (uint8_t)(hc & 0xFF); row[15] = (uint8_t)(hc >> 8); + nm++; + } + const uint8_t per = (uint8_t)((MAX_PACKET_PAYLOAD - 12) / OTA_HAVE_ROW_BYTES); // rows per HAVE fragment + uint8_t ftotal = (uint8_t)((nm + per - 1) / per); if (ftotal == 0) ftotal = 1; + for (uint8_t fi = 0; fi < ftotal; fi++) { + uint8_t bse = (uint8_t)(fi * per); + uint8_t cnt = (uint8_t)((nm - bse > per) ? per : (nm - bse)); + HaveMsg hv; memcpy(hv.seeder_id, _seeder_id, 4); memcpy(hv.set_digest, dg, 4); + hv.frag_idx = fi; hv.frag_total = ftotal; hv.n_rows = cnt; + hv.rows = cnt ? (rowbuf + (uint32_t)bse * OTA_HAVE_ROW_BYTES) : nullptr; + uint8_t b[MAX_PACKET_PAYLOAD]; + emit(b, encode_have(b, sizeof(b), hv), true); // broadcast: all neighbours cache it + } +} + +void OtaManager::handleGetManifest(const uint8_t* m, uint16_t n) { + GetManifestMsg gm; + if (!decode_get_manifest(m, n, gm)) return; + ServeView* v = resolve(gm.manifest_id); + if (!v) return; + // A signed v2 manifest (with hw_id[32]) exceeds one LoRa packet, so send it as fragments. Each carries + // up to OTA_MF_FRAG manifest bytes; the client reassembles by frag_idx. (Re-sent in full on a retry.) + uint32_t mfl = v->mfl; + const uint8_t* src = v->m.manifest_start; + uint8_t ftotal = (uint8_t)((mfl + OTA_MF_FRAG - 1) / OTA_MF_FRAG); if (ftotal == 0) ftotal = 1; + for (uint8_t fi = 0; fi < ftotal; fi++) { + uint32_t off = (uint32_t)fi * OTA_MF_FRAG; + uint32_t fl = mfl - off; if (fl > OTA_MF_FRAG) fl = OTA_MF_FRAG; + ManifestMsg mm; + memcpy(mm.manifest_id, v->m.merkle_root, 4); + mm.frag_idx = fi; mm.frag_total = ftotal; + mm.bytes = src + off; mm.len = (uint16_t)fl; + uint8_t b[MAX_PACKET_PAYLOAD]; + emit(b, encode_manifest(b, sizeof(b), mm), false); + } +} + +// Emit one block's data as self-describing DATA fragments (frag_off); the proof is fetched separately. +void OtaManager::emitBlockData(const uint8_t* mid, uint32_t idx, const uint8_t* data, uint32_t blen) { + for (uint32_t fo = 0; fo < blen; fo += OTA_FRAG_DATA) { + uint32_t fl = (fo + OTA_FRAG_DATA <= blen) ? OTA_FRAG_DATA : (blen - fo); + DataMsg dm; + memcpy(dm.manifest_id, mid, 4); + dm.block_idx = (uint16_t)idx; dm.frag_off = (uint16_t)fo; + dm.data = data + fo; dm.data_len = (uint16_t)fl; + uint8_t b[MAX_PACKET_PAYLOAD]; + emit(b, encode_data(b, sizeof(b), dm), false); + } +} + +// True if we recently overheard ANOTHER holder broadcast this block's DATA — so we should not re-serve it +// (avoids N sources duplicate-broadcasting one block; keeps OTA airtime minimal). See noteOverheardData(). +bool OtaManager::recentlyServed(uint32_t blk) const { + for (uint8_t i = 0; i < 8; i++) + if (_recent_blk[i] == blk && (uint32_t)(_now_ms - _recent_at[i]) < OTA_SERVE_SUPPRESS_MS) return true; + return false; +} + +void OtaManager::noteOverheardData(const uint8_t* m, uint16_t n) { + DataMsg dm; + if (!decode_data(m, n, dm)) return; + _recent_blk[_recent_i] = dm.block_idx; _recent_at[_recent_i] = _now_ms; + _recent_i = (uint8_t)((_recent_i + 1) & 7); +} + +void OtaManager::handleReq(const uint8_t* m, uint16_t n) { + ReqMsg rq; + if (!decode_req(m, n, rq)) return; + ServeView* v = resolve(rq.manifest_id); + if (v) { // serve a fully-held mota (own fw / folder / completed fetch) + uint32_t bs = v->m.block_size(); + for (uint32_t k = 0; k < rq.count; k++) { + uint32_t idx = rq.start_block + k; + if (idx >= v->m.block_count) break; + if (recentlyServed(idx)) continue; // another holder just broadcast it — don't duplicate + uint32_t off = idx * bs; + uint32_t blen = (off + bs <= v->m.payload_size) ? bs : (v->m.payload_size - off); + uint8_t blk[OTA_MAX_BLOCK]; + const uint8_t* data; + if (v->read) { if (!v->read(v->read_ctx, off, blk, blen)) break; data = blk; } + else { data = v->m.payload + off; } + emitBlockData(v->m.merkle_root, idx, data, blen); + } + return; + } + // Partial re-serve (swarm DURING the transfer): we're fetching this mid and already hold some of these + // blocks — serve their DATA (not proofs; we may lack sibling leaves) from our staging store, so peers can + // source from us, not only the origin. Reactive + lowest-priority, so real traffic is never impacted. + if (_fetch && _fstate == FETCHING && memcmp(rq.manifest_id, _fid, 4) == 0) { + for (uint32_t k = 0; k < rq.count; k++) { + uint32_t idx = rq.start_block + k; + if (idx >= _fbc) break; + if (!blockPresent(idx) || recentlyServed(idx)) continue; + uint8_t blk[OTA_MAX_BLOCK]; + uint32_t blen = blockLen(idx); + if (!_fetch->read(_fpoff + idx * _fbs, blk, blen)) continue; + emitBlockData(_fid, idx, blk, blen); + } + } +} + +void OtaManager::handleReqProof(const uint8_t* m, uint16_t n) { + ReqProofMsg rp; + if (!decode_req_proof(m, n, rp)) return; + ServeView* v = resolve(rp.manifest_id); + if (!v) return; + if (rp.block_idx >= v->m.block_count) return; + if ((uint64_t)v->m.block_count * 4 > v->scratch_sz) return; // proof-gen needs block_count*4 scratch + uint8_t proof[32 * 4]; + uint8_t np = merkle_gen_proof(v->m.leaves, v->m.block_count, rp.block_idx, v->scratch, proof); + ProofMsg pm; + memcpy(pm.manifest_id, v->m.merkle_root, 4); + pm.block_idx = rp.block_idx; pm.n_proof = np; pm.proof = proof; + uint8_t b[MAX_PACKET_PAYLOAD]; + emit(b, encode_proof(b, sizeof(b), pm), false); +} + +// ---------------- fetch ---------------- + +// A tiny per-node BEACON: record the source; ask it for its catalog (OTA_QUERY) only when we're +// interested AND its set-digest is one we haven't catalogued yet (so a stable mesh is query-free). +void OtaManager::handleAdv(const uint8_t* m, uint16_t n) { + AdvMsg a; + if (!decode_adv(m, n, a)) return; + bool have_sid = (_seeder_id[0] | _seeder_id[1] | _seeder_id[2] | _seeder_id[3]) != 0; + if (have_sid && memcmp(a.seeder_id, _seeder_id, 4) == 0) return; // our own beacon, re-flooded + if (a.n_motas == 0) return; // source offers nothing + + int slot = -1, lru = 0; // find/insert the source (LRU evict) + for (int i = 0; i < _n_src; i++) { + if (memcmp(_sources[i].seeder, a.seeder_id, 4) == 0) { slot = i; break; } + if (_sources[i].last_ms < _sources[lru].last_ms) lru = i; + } + bool fresh = (slot < 0); + if (fresh) { slot = (_n_src < OTA_MAX_SOURCES) ? _n_src++ : lru; _sources[slot] = Source{}; } + Source& s = _sources[slot]; + bool changed = fresh || memcmp(s.digest, a.set_digest, 4) != 0; + memcpy(s.seeder, a.seeder_id, 4); memcpy(s.digest, a.set_digest, 4); + s.n_motas = a.n_motas; s.last_ms = _now_ms; + if (changed) s.have_catalog = false; + + // interested = auto-fetch enabled, or a manual pull/want is pending. (Browsing queries via queryAll().) + bool interested = (_autofetch != AUTOFETCH_OFF) || _have_desired_mid || _desired_target; + if (interested && !s.have_catalog) scheduleQuery(a.seeder_id, a.set_digest); // jittered + suppressible +} + +// Schedule a catalog query after a random jitter (id ⊕ digest, so neighbours pick different delays). The +// node with the shortest jitter sends; the rest overhear that QUERY (or the broadcast HAVE) and suppress. +void OtaManager::scheduleQuery(const uint8_t* seeder, const uint8_t* digest) { + if (_pq_active && memcmp(_pq_seeder, seeder, 4) == 0 && memcmp(_pq_digest, digest, 4) == 0) return; // already pending + memcpy(_pq_seeder, seeder, 4); memcpy(_pq_digest, digest, 4); + uint32_t j = (rd_u32le(seeder) ^ rd_u32le(digest) ^ rd_u32le(_seeder_id)) % OTA_QUERY_SPREAD_MS; + _pq_at = _now_ms + OTA_QUERY_MIN_MS + j; + _pq_active = true; +} + +void OtaManager::sendQuery(const uint8_t* seeder, const uint8_t* digest, uint32_t filter_target) { + QueryMsg q; memcpy(q.seeder_id, seeder, 4); memcpy(q.set_digest, digest, 4); q.filter_target = filter_target; + uint8_t b[16]; + emit(b, encode_query(b, sizeof(b), q), true); // FLOODED so neighbours overhear it and suppress +} + +// User-initiated browse (`ota neighbors`): ask every known source now (no jitter — infrequent + explicit). +void OtaManager::queryAll() { for (uint8_t i = 0; i < _n_src; i++) sendQuery(_sources[i].seeder, _sources[i].digest, 0); } + +// A catalog reply: record each mOTA (deduped by mid; distinct-source count for the UI), and if a row +// matches our fetch interest (auto-fetch own-target, or a pending pull/want), begin fetching it. +void OtaManager::handleHave(const uint8_t* m, uint16_t n) { + HaveMsg hv; + if (!decode_have(m, n, hv)) return; + bool have_sid = (_seeder_id[0] | _seeder_id[1] | _seeder_id[2] | _seeder_id[3]) != 0; + if (have_sid && memcmp(hv.seeder_id, _seeder_id, 4) == 0) return; // our own catalog + // PASSIVE: any overheard HAVE marks its source catalogued + cancels a pending query for it (storm + // suppression) — every node caches the rows below, even one that never queried. + for (uint8_t i = 0; i < _n_src; i++) + if (memcmp(_sources[i].seeder, hv.seeder_id, 4) == 0 && memcmp(_sources[i].digest, hv.set_digest, 4) == 0) + _sources[i].have_catalog = true; + if (_pq_active && memcmp(_pq_seeder, hv.seeder_id, 4) == 0 && memcmp(_pq_digest, hv.set_digest, 4) == 0) + _pq_active = false; + for (uint8_t r = 0; r < hv.n_rows && hv.rows; r++) { + const uint8_t* row = hv.rows + (uint32_t)r * OTA_HAVE_ROW_BYTES; + const uint8_t* mid = row; + uint32_t target = rd_u32le(row + 4), fwver = rd_u32le(row + 8); + uint8_t codec = row[12], flags = row[13]; + uint32_t have_count = rd_u16le(row + 14); // this source's progress + int slot = -1, lru = 0; // upsert into the catalog (dedup by mid) + for (int i = 0; i < _n_cat; i++) { + if (memcmp(_catalog[i].mid, mid, 4) == 0) { slot = i; break; } + if (_catalog[i].last_ms < _catalog[lru].last_ms) lru = i; + } + if (slot < 0) { + slot = (_n_cat < OTA_MAX_CATALOG) ? _n_cat++ : lru; + _catalog[slot] = CatRow{}; + memcpy(_catalog[slot].mid, mid, 4); + memcpy(_catalog[slot].seeders[0], hv.seeder_id, 4); + _catalog[slot].n_seeders = 1; + } else { + CatRow& cc = _catalog[slot]; // count DISTINCT sources (no double-count) + bool known = false; + for (uint8_t k = 0; k < cc.n_seeders; k++) + if (memcmp(cc.seeders[k], hv.seeder_id, 4) == 0) { known = true; break; } + if (!known && cc.n_seeders < OTA_CAT_SEEDERS) memcpy(cc.seeders[cc.n_seeders++], hv.seeder_id, 4); + } + CatRow& c = _catalog[slot]; + c.target_id = target; c.fw_version = fwver; c.codec = codec; c.flags = flags; c.last_ms = _now_ms; + if (have_count > c.have_max) c.have_max = have_count; // best-known progress among sources + if (wantRow(mid, target, codec, flags)) startFetch(mid, target); + } +} + +bool OtaManager::wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uint8_t flags) const { + if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST) return false; // busy with a session + if (_fstate == COMPLETE && memcmp(mid, _fid, 4) == 0) return false; // already have it + if (!codecOk(codec)) return false; // can't apply this codec + if (_have_desired_mid) // manual pull of a specific mid + 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 + if (_autofetch == AUTOFETCH_SIGNED && !(flags & MFLAG_SIGNED)) return false; // signed-only policy + return true; +} + +// Forget the block currently being reassembled / awaited (back to NO_BLOCK). Safe to call between blocks: +// the next DATA fragment re-derives the slice mask for whatever block it belongs to. +void OtaManager::clearReassembly() { + _reasm_block = NO_BLOCK; _reasm_mask = 0; _reasm_need = 0; _awaiting_proof = false; +} + +// De-sync the first REQ across the swarm: hold it a random fraction of OTA_REQ_SPREAD_MS so N nodes that +// just discovered the same mid don't burst-request block 0 in lockstep (loop() fires it once the hold +// elapses). Assumes the per-node RNG is already seeded; also forgets any peer-REQ note from a prior session. +void OtaManager::armFirstReqHold() { + _req_hold_at = _now_ms + (rngNext() % OTA_REQ_SPREAD_MS); + _peer_req_block = NO_BLOCK; _peer_req_at = 0; +} + +// Begin (or resume) fetching a chosen mid: try a staged-partial resume first, else request the manifest. +void OtaManager::startFetch(const uint8_t* mid, uint32_t target) { + (void)target; + if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST) return; + if (resumeStaged(mid)) return; // resume a partial container left in flash + memcpy(_fid, mid, 4); + seedBlockRng(); // per-node block-pick/jitter sequence (distinct per node) + _fstate = WANT_MANIFEST; + _mf_total = 0; _mf_mask = 0; _mf_len = 0; _mf_retries = 0; // fresh manifest reassembly + GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4); + uint8_t b[16]; + emit(b, encode_get_manifest(b, sizeof(b), gm), false); +} + +void OtaManager::handleManifest(const uint8_t* m, uint16_t n) { + ManifestMsg mm; + if (!decode_manifest(m, n, mm) || !_fetch) return; + if (_fstate != WANT_MANIFEST || memcmp(mm.manifest_id, _fid, 4) != 0) return; + if (mm.frag_total == 0 || mm.frag_total > OTA_MF_MAXFRAG || mm.frag_idx >= mm.frag_total) return; + + // reassemble the (possibly multi-fragment) manifest into _mf_buf; place fragment frag_idx at its offset + uint32_t foff = (uint32_t)mm.frag_idx * OTA_MF_FRAG; + if (foff + mm.len > sizeof(_mf_buf)) return; + if (mm.frag_total != _mf_total) { _mf_total = mm.frag_total; _mf_mask = 0; _mf_len = 0; } // (re)start + memcpy(_mf_buf + foff, mm.bytes, mm.len); + _mf_mask |= (uint16_t)(1u << mm.frag_idx); + if (mm.frag_idx == mm.frag_total - 1) _mf_len = foff + mm.len; // last fragment fixes the length + uint16_t full = (mm.frag_total >= 16) ? 0xFFFF : (uint16_t)((1u << mm.frag_total) - 1); + if (_mf_mask != full || _mf_len == 0) return; // wait until every fragment is in + + const uint8_t* mf = _mf_buf; // fully reassembled manifest-minus-leaves + uint32_t mfl = _mf_len; + if (mfl != MOTA_MFL) { _fstate = FAILED; return; } // manifest-minus-leaves is a fixed 197 bytes + if (!codecOk(mf[56])) { _fstate = IDLE; return; } // codec we can't apply (lying/stale ADV) — abort + uint32_t payload_size = rd_u32le(mf + 15); + uint8_t bsl = mf[19]; + uint32_t bs = 1u << bsl; + // a block must fit our reassembly buffer (and be non-empty) — reject an oversized block_size up front + if (bs == 0 || bs > OTA_MAX_BLOCK || payload_size == 0) { _fstate = FAILED; return; } + uint32_t bc = (payload_size + bs - 1) / bs; + if (bc > 0xFFFFu) { _fstate = FAILED; return; } // block_idx is uint16 on the wire — can't address more + memcpy(_froot, mf + 20, 4); + + uint32_t leaves_off = 8 + mfl; + uint32_t payload_off = leaves_off + bc * 4; + uint32_t total = payload_off + payload_size + 5; + + // Hand the store the parsed layout BEFORE begin(), so a partition-backed store (ESP32) can choose + // placement and refuse an unfittable fetch up front: a FULL payload streams to the inactive slot, + // a delta's whole container is staged together. (image_size at mf+11, is_full from flags at mf+1.) + bool is_full = (mf[1] & MFLAG_FULL) != 0; + if (!_fetch->plan_layout(is_full, rd_u32le(mf + 11), payload_off, payload_size)) { _fstate = FAILED; return; } + unserveFetched(); // the store is about to be overwritten by this new fetch — stop re-seeding the old one + if (!_fetch->begin(total)) { _fstate = FAILED; return; } + // declare the metadata extent so a flash store can pin it (leaves are written all transfer long) + if (!_fetch->set_meta_size(payload_off)) { _fstate = FAILED; return; } + uint8_t hdr[8]; + memcpy(hdr, MOTA_MAGIC, 4); + wr_u32le(hdr + 4, total); + if (!_fetch->write(0, hdr, 8) || + !_fetch->write(8, mf, mfl) || + !_fetch->write(total - 5, MOTA_TRAILER, 5)) { _fstate = FAILED; return; } + + _fflags = mf[1]; // manifest flags (FULL/SIGNED) of the fetch in progress (auto-install gate) + _fpoff = payload_off; _floff = leaves_off; _fpsize = payload_size; _fbc = bc; _fbs = bs; + _ftotal = total; _have = 0; _fstate = FETCHING; + clearReassembly(); // fresh transfer: drop any prior per-block state + _loop_last_have = 0; _loop_last_mask = 0; + if (_rng == 0) seedBlockRng(); + armFirstReqHold(); + OTA_DBG("OTA: FETCHING bc=%u bs=%u total=%u\n", (unsigned)bc, (unsigned)bs, (unsigned)total); +} + +bool OtaManager::resumeStaged(const uint8_t* want_mid) { + if (!_fetch || _fstate == FETCHING || _fstate == WANT_MANIFEST) return false; + if (!_fetch->reopen()) return false; // nothing persisted in the store + uint32_t total = _fetch->staged_size(); + uint8_t hdr[8]; + if (total < 13 || !_fetch->read(0, hdr, 8) || memcmp(hdr, MOTA_MAGIC, 4) != 0) return false; + // read + parse the stored manifest (everything before leaves[]) to recompute the geometry + uint8_t mbuf[256]; + uint32_t mread = total - 8; if (mread > sizeof(mbuf)) mread = sizeof(mbuf); + MotaManifest m; + if (!_fetch->read(8, mbuf, mread) || !mota_parse_manifest(mbuf, mread, m)) return false; + if (want_mid && memcmp(m.merkle_root, want_mid, 4) != 0) return false; // a different fw is staged + if (!codecOk(m.codec_id)) return false; + uint32_t mfl = (uint32_t)(m.approval - m.manifest_start) + 4; // manifest-minus-leaves length + uint32_t bs = m.block_size(); + if (bs == 0 || bs > OTA_MAX_BLOCK) return false; + uint32_t bc = m.block_count; + uint32_t leaves_off = 8 + mfl; + uint32_t payload_off = leaves_off + bc * 4; + if ((uint64_t)payload_off + m.payload_size + 5 != total) return false; // geometry must match the header + + memcpy(_fid, m.merkle_root, 4); + memcpy(_froot, m.merkle_root, 4); + _fflags = m.flags; + _fpoff = payload_off; _floff = leaves_off; _fpsize = m.payload_size; _fbc = bc; _fbs = bs; + _ftotal = total; + _have = 0; + for (uint32_t i = 0; i < bc; i++) if (blockPresent(i)) _have++; // count blocks whose leaf survived + clearReassembly(); + _loop_last_have = 0; _loop_last_mask = 0; + OTA_DBG("OTA: RESUME have=%u/%u total=%u\n", (unsigned)_have, (unsigned)bc, (unsigned)total); + + if (_have >= bc) { // already complete -> verify root + finalize + if (bc * 4 <= sizeof(_scratch) && _fetch->read(_floff, _scratch, bc * 4)) { + uint8_t root[4]; merkle_root(root, _scratch, bc); + _fstate = (memcmp(root, _froot, 4) == 0) ? COMPLETE : FAILED; + } else { + _fstate = COMPLETE; + } + if (_fstate == COMPLETE) { _fetch->finalize(); serveFetched(); } + return true; + } + _fstate = FETCHING; // resume fetching the holes + // De-sync the first REQ exactly like a fresh fetch, so a coordinated reboot (whole-site power-cycle) + // doesn't make every resuming node REQ in lockstep. + seedBlockRng(); + armFirstReqHold(); + _loop_last_have = _have; _loop_last_mask = _reasm_mask; // "no progress yet" -> loop will request after the hold + return true; +} + +uint32_t OtaManager::blockLen(uint32_t i) const { + uint32_t off = i * _fbs; + return (off + _fbs <= _fpsize) ? _fbs : (_fpsize - off); +} + +bool OtaManager::blockPresent(uint32_t i) const { + uint8_t leaf[4]; + if (!_fetch->read(_floff + i * 4, leaf, 4)) return false; + return !(leaf[0]==0xFF && leaf[1]==0xFF && leaf[2]==0xFF && leaf[3]==0xFF); +} + +void OtaManager::handleData(const uint8_t* m, uint16_t n) { + DataMsg dm; + if (!decode_data(m, n, dm) || !_fetch) return; + if (_fstate != FETCHING || memcmp(dm.manifest_id, _fid, 4) != 0) return; + if (dm.block_idx >= _fbc) return; + if (blockPresent(dm.block_idx)) return; // already stored + verified + uint32_t blen = blockLen(dm.block_idx); + if (dm.frag_off % OTA_FRAG_DATA != 0) return; // canonical FRAG_DATA-aligned slices only + if ((uint32_t)dm.frag_off + dm.data_len > blen) return; // slice out of the block + if (dm.block_idx != _reasm_block) { // (re)start reassembly for this block + _reasm_block = dm.block_idx; _reasm_mask = 0; _awaiting_proof = false; + uint32_t nf = (blen + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA; + _reasm_need = (nf >= 16) ? 0xFFFF : (uint16_t)((1u << nf) - 1); + } + uint32_t kf = dm.frag_off / OTA_FRAG_DATA; + if (kf >= 16) return; + memcpy(_reasm_buf + dm.frag_off, dm.data, dm.data_len); + _reasm_mask |= (uint16_t)(1u << kf); + if (_reasm_mask != _reasm_need || _awaiting_proof) return; // wait for all slices (or proof already asked) + // block fully reassembled -> request its proof (data + proof are fetched separately) + _awaiting_proof = true; + ReqProofMsg rp; memcpy(rp.manifest_id, _fid, 4); rp.block_idx = (uint16_t)_reasm_block; + uint8_t b[16]; emit(b, encode_req_proof(b, sizeof(b), rp), false); +} + +void OtaManager::handleProof(const uint8_t* m, uint16_t n) { + ProofMsg pm; + if (!decode_proof(m, n, pm) || !_fetch) return; + if (_fstate != FETCHING || memcmp(pm.manifest_id, _fid, 4) != 0) return; + if (!_awaiting_proof || pm.block_idx != _reasm_block) return; // not the block we're verifying + uint32_t blen = blockLen(_reasm_block); + if (!merkle_verify(_reasm_buf, blen, _reasm_block, pm.proof, pm.n_proof, _froot, _fbc)) { + clearReassembly(); // bad -> drop, re-fetch the block + return; + } + // verified -> commit the payload block, then its leaf (the present marker) + if (!_fetch->write(_fpoff + (uint32_t)_reasm_block * _fbs, _reasm_buf, blen)) return; + uint8_t leaf[4]; merkle_leaf(leaf, _reasm_buf, blen); + if (!_fetch->write(_floff + (uint32_t)_reasm_block * 4, leaf, 4)) return; + _have++; + OTA_DBG("OTA: block %u OK have=%u/%u\n", (unsigned)_reasm_block, (unsigned)_have, (unsigned)_fbc); + clearReassembly(); + // periodically persist progress (meta/leaf page + open payload) so a reboot can resume (no-op for RAM); + // cadence is runtime-tunable via `ota config checkpoint ` (0 = never) + if (_checkpoint_blocks && _have % _checkpoint_blocks == 0) _fetch->checkpoint(); + if (_have < _fbc) { requestMissing(); return; } // next block + // all blocks present -> final root cross-check + finalize + if (_fbc * 4 <= sizeof(_scratch) && _fetch->read(_floff, _scratch, _fbc * 4)) { + uint8_t root[4]; merkle_root(root, _scratch, _fbc); + _fstate = (memcmp(root, _froot, 4) == 0) ? COMPLETE : FAILED; + } else { + _fstate = COMPLETE; // per-block proofs already guaranteed integrity vs the root + } + if (_fstate == COMPLETE) { _fetch->finalize(); serveFetched(); } // commit + re-seed (epidemic spread) + OTA_DBG("OTA: transfer %s\n", _fstate == COMPLETE ? "COMPLETE" : "FAILED(root)"); +} + +void OtaManager::requestMissing() { + if (_fstate != FETCHING) return; + // Per-block serial flow (split data/proof). If the current block's data is fully reassembled and we + // are waiting on its proof, (re)send the proof request rather than re-fetching the data — this also + // recovers from a lost PROOF reply. + if (_awaiting_proof && _reasm_block != NO_BLOCK) { + ReqProofMsg rp; memcpy(rp.manifest_id, _fid, 4); rp.block_idx = (uint16_t)_reasm_block; + uint8_t b[16]; emit(b, encode_req_proof(b, sizeof(b), rp), false); + OTA_DBG("OTA: REQ_PROOF block=%u (have=%u/%u)\n", + (unsigned)_reasm_block, (unsigned)_have, (unsigned)_fbc); + return; + } + // Otherwise request the DATA fragments of the next missing block. One block at a time keeps the + // server's TX queue tiny so OTA never floods the mesh (docs/ota_protocol.md §8); a block's fragments + // are self-describing (frag_off) so they may be served by ANY peer, BitTorrent-style. + uint32_t start = pickMissingBlock(); + if (start >= _fbc) return; + _req_start = start; _req_count = 1; + ReqMsg rq; memcpy(rq.manifest_id, _fid, 4); + rq.start_block = (uint16_t)start; rq.count = 1; + uint8_t b[16]; + OTA_DBG("OTA: REQ block=%u (have=%u/%u mask=%04x)\n", + (unsigned)start, (unsigned)_have, (unsigned)_fbc, (unsigned)_reasm_mask); + emit(b, encode_req(b, sizeof(b), rq), false); +} + +// Choose which block to request next. Swarm-aware so N fetchers of the same mid spread their load instead +// of marching in lockstep on the same block: +// - finish an in-flight partially-reassembled block first (don't waste received fragments); +// - otherwise pick a RANDOM missing block (de-correlates fetchers -> they collectively pull different +// blocks, and every broadcast DATA fills everyone's hole); +// - skip a block a peer just REQ'd (its DATA is already coming over the air) unless it's all that's left. +// Returns _fbc if nothing to request. +uint32_t OtaManager::pickMissingBlock() { + if (_fbc == 0) return _fbc; + // (1) keep finishing a block we've already started reassembling (recover its lost fragments) + if (_reasm_block < _fbc && !blockPresent(_reasm_block) && _reasm_mask != 0) return _reasm_block; + // (2) count missing blocks + uint32_t miss = 0; + for (uint32_t i = 0; i < _fbc; i++) if (!blockPresent(i)) miss++; + if (miss == 0) return _fbc; + bool suppress = (_peer_req_block < _fbc) && ((uint32_t)(_now_ms - _peer_req_at) < OTA_REQ_SUPPRESS_MS); + // (3) pick the k-th missing block (k from the per-node RNG), optionally skipping the peer-REQ'd one + uint32_t k = rngNext() % miss; + uint32_t seen = 0, chosen = _fbc, firstAny = _fbc; + for (uint32_t i = 0; i < _fbc; i++) { + if (blockPresent(i)) continue; + if (firstAny == _fbc) firstAny = i; + if (seen == k) { chosen = i; } + seen++; + } + if (suppress && chosen == _peer_req_block) { // pick a different missing block than the one in flight elsewhere + for (uint32_t i = 0; i < _fbc; i++) { + uint32_t j = (chosen + 1 + i) % _fbc; + if (!blockPresent(j) && j != _peer_req_block) { chosen = j; break; } + } + // if the suppressed block is the ONLY one left, chosen stays == it (we still need it eventually) + } + return (chosen < _fbc) ? chosen : firstAny; +} + +// Observe a peer's OTA_REQ for the mid we're fetching: its block's DATA is broadcast, so it will fill our +// hole too — note it so pickMissingBlock() spends our next REQ on a DIFFERENT block (swarm de-dup). +void OtaManager::noteOverheardReq(const uint8_t* m, uint16_t n) { + if (_fstate != FETCHING) return; + ReqMsg rq; + if (!decode_req(m, n, rq) || memcmp(rq.manifest_id, _fid, 4) != 0) return; + _peer_req_block = rq.start_block; + _peer_req_at = _now_ms; +} + +void OtaManager::loop() { + // fire a scheduled catalog query once its jitter has elapsed (unless overhearing already suppressed it) + if (_pq_active && (int32_t)(_now_ms - _pq_at) >= 0) { + _pq_active = false; + sendQuery(_pq_seeder, _pq_digest, 0); // unfiltered: one broadcast HAVE serves everyone + } + if (_fstate == WANT_MANIFEST) { + // the MANIFEST reply may have been lost on a marginal link — retry GET_MANIFEST, but give up after a + // cap so an unreachable mid doesn't pin the single fetch slot (or emit) forever. + if (++_mf_retries > OTA_MANIFEST_MAX_RETRY) { _fstate = FAILED; return; } + GetManifestMsg gm; memcpy(gm.manifest_id, _fid, 4); + uint8_t b[16]; + emit(b, encode_get_manifest(b, sizeof(b), gm), false); + return; + } + if (_fstate != FETCHING) return; + if ((int32_t)(_now_ms - _req_hold_at) < 0) return; // swarm: initial random hold (de-sync N fetchers) + // retry only when a whole tick passed with NO progress — neither a committed block nor a new fragment + // of the in-flight block. This avoids re-request spam while a block's fragments are still streaming in. + if (_have == _loop_last_have && _reasm_mask == _loop_last_mask) requestMissing(); + _loop_last_have = _have; + _loop_last_mask = _reasm_mask; +} + +// ---------------- dispatch ---------------- + +void OtaManager::on_message(const uint8_t* msg, uint16_t len) { + switch (ota_msg_type(msg, len)) { + case OTA_ADV: handleAdv(msg, len); break; + case OTA_QUERY: handleQuery(msg, len); break; + case OTA_HAVE: handleHave(msg, len); break; + case OTA_GET_MANIFEST: handleGetManifest(msg, len); break; + case OTA_MANIFEST: handleManifest(msg, len); break; + case OTA_REQ: noteOverheardReq(msg, len); handleReq(msg, len); break; + case OTA_DATA: handleData(msg, len); noteOverheardData(msg, len); break; + case OTA_REQ_PROOF: handleReqProof(msg, len); break; + case OTA_PROOF: handleProof(msg, len); break; + default: break; + } +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaManager.h b/src/helpers/ota/OtaManager.h new file mode 100644 index 0000000000..a0c8cf0419 --- /dev/null +++ b/src/helpers/ota/OtaManager.h @@ -0,0 +1,378 @@ +#pragma once + +#include +#include +#include "OtaFormat.h" +#include "OtaByteIO.h" +#include "OtaStore.h" +#include "MotaContainer.h" +#include "OtaSource.h" + +// Transport-agnostic OTA session engine (docs/ota_protocol.md §5/§8). It SERVES a complete `.mota` +// (answering GET_MANIFEST / REQ) and/or FETCHES one into an OtaStore (verifying every block against +// the signed merkle root via proofs). It is portable (no Arduino / radio / Ed25519) so it can be +// driven by a host simulation; a thin Mesh adapter wires it to PAYLOAD_TYPE_OTA on device. +// +// Transfer is per-block and 2-phase: a 1 KB logical block is fetched as self-describing DATA fragments +// (frag_off, so any peer can serve any fragment — BitTorrent-style), reassembled, then its merkle PROOF +// is requested separately and verified against the signed root before the block is committed. + +namespace mesh { +namespace ota { + +// Emit an OTA message (one packet payload). `flood`=true for announce/query, false for direct replies. +typedef void (*OtaSend)(void* ctx, const uint8_t* msg, uint16_t len, bool flood); + +// Read `len` payload bytes at offset `off` from the serve source (flash-backed self-serve); false on +// error. nullptr means the payload is a contiguous RAM buffer (the staged `.mota`). +typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len); + +#ifndef OTA_PROOFGEN_SCRATCH +#define OTA_PROOFGEN_SCRATCH 4096 // server proof-gen working buffer (supports up to 1024 blocks) +#endif + +#ifndef OTA_MAX_BLOCK +#define OTA_MAX_BLOCK 1024 // largest logical block (merkle leaf unit) = reassembly buffer size +#endif +#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_HOP_LIMIT_DEFAULT +#define OTA_HOP_LIMIT_DEFAULT 3 // default OTA flood reach in hops: accept <= N, relay while < N (0=direct) +#endif +#ifndef OTA_MF_FRAG +#define OTA_MF_FRAG 176 // manifest bytes per OTA_MANIFEST fragment (<= MAX_PACKET_PAYLOAD - header) +#endif +#ifndef OTA_MF_MAXFRAG +#define OTA_MF_MAXFRAG 4 // max manifest fragments (the fixed 197 B manifest is always 2) +#endif +#ifndef OTA_MANIFEST_MAX_RETRY +#define OTA_MANIFEST_MAX_RETRY 20 // give up (FAILED) after this many GET_MANIFEST retries — frees the slot +#endif +#ifndef OTA_MAX_SOURCES +#define OTA_MAX_SOURCES 12 // heard OTA sources (beacon senders) tracked (LRU); ~12 B each +#endif +#ifndef OTA_MAX_SERVE +#define OTA_MAX_SERVE 12 // mOTAs THIS node offers (own fw + external folder); == one HAVE fragment +#endif +#ifndef OTA_MAX_SOURCE_OBJ +#define OTA_MAX_SOURCE_OBJ 4 // external MotaSource objects (folders/transports) attached at once +#endif +#ifndef OTA_SRC_MANIFEST_MAX +#define OTA_SRC_MANIFEST_MAX 256 // manifest-minus-leaves buffer for the loaded source mota (head+sig+approval) +#endif +#ifndef OTA_MAX_CATALOG +#define OTA_MAX_CATALOG 12 // distinct mOTAs catalogued from OTA_HAVE replies (LRU) +#endif +#ifndef OTA_QUERY_MIN_MS +#define OTA_QUERY_MIN_MS 300 // min delay before sending a catalog query (overhear-suppression window) +#endif +#ifndef OTA_QUERY_SPREAD_MS +#define OTA_QUERY_SPREAD_MS 4000 // random jitter span so 50 neighbours don't all query at once (storm) +#endif +#ifndef OTA_REQ_SPREAD_MS +#define OTA_REQ_SPREAD_MS 3000 // initial random hold before a fetch's first REQ (de-sync N fetchers) +#endif +#ifndef OTA_REQ_SUPPRESS_MS +#define OTA_REQ_SUPPRESS_MS 2500 // after overhearing a peer's REQ for a block, don't also request it — +#endif // its DATA is broadcast and will fill our hole too (swarm de-dup) +#ifndef OTA_SERVE_SUPPRESS_MS +#define OTA_SERVE_SUPPRESS_MS 1500 // don't re-serve a block whose DATA we just overheard another holder send +#endif // (so multiple sources of the same mota don't duplicate-broadcast it) +#ifndef OTA_FRAG_DATA +#define OTA_FRAG_DATA 160 // data bytes per DATA fragment (<= MAX_PACKET_PAYLOAD - 9-byte header) +#endif +// nRF52 note: a flash page-erase halts the CPU (~85 ms, code runs from flash) and starves the LoRa RX, +// so writing to flash on every received packet drops in-flight DATA and the transfer stalls. The SD-safe +// driver (Adafruit flash_nrf5x) always erases on flush, so there is no erase-free write; instead +// OtaStoreFlashNrf52 COALESCES to the 4 KB page (the erase unit) and writes each page once — RAM stays +// O(one page), never O(mota). It pins flash page 0 (header+manifest+merkle leaves, which update all +// transfer long) in RAM and streams the payload through one sliding page buffer, flushing page 0 and the +// last page at finalize() (radio idle). Flash is then touched ~once per 4 KB (≈1 per 4 blocks), not per +// packet; a small delta that fits page 0 does ZERO flash I/O until COMPLETE. (Pacing alone is not enough.) + +class OtaManager { +public: + enum FetchState : uint8_t { IDLE, WANT_MANIFEST, FETCHING, COMPLETE, FAILED }; + + // Sentinel for "no block" in the reassembly / peer-REQ / recently-served slots (a real block index is + // a small uint16, so 0xFFFFFFFF is never valid). + static const uint32_t NO_BLOCK = 0xFFFFFFFFu; + + // --- multi-mota serve --- A ServeView is everything a serve handler needs for ONE mota. Two can be + // resident: view0 = our own fw / a RAM `.mota` (always loaded), plus one on-demand source view that is + // (re)loaded from a MotaSource when a request targets a different external mota. Requests dispatch by + // manifest_id (carried in every fetch message) -> the matching ServeView via resolve(). + struct ServeView { + bool valid = false; + MotaManifest m; // parsed manifest (fields/pointers into the backing buffers) + uint16_t mfl = 0; // manifest-minus-leaves length (the OTA_MANIFEST payload) + ServeReadFn read = nullptr; // payload reader (nullptr => m.payload is contiguous in RAM) + void* read_ctx = nullptr; + uint8_t* scratch = nullptr; // proof-gen working buffer (>= block_count*4) + uint32_t scratch_sz = 0; + }; + // A lightweight catalog entry: what we advertise per mota + how to load its ServeView on demand. + struct ServeEntry { + uint8_t mid[4]; + uint32_t target_id, fw_version; + uint8_t codec_id, flags; + uint32_t have_count; // blocks we currently hold (== block_count when complete) + bool is_self; // true => entry is view0 (our own fw / RAM mota) + bool is_fetch; // true => load from our own fetch store (a completed download we re-seed) + MotaSource* src; // else: load from this external source ... + uint8_t src_idx; // ... at this index + MotaDesc desc; // cached region offsets (source / fetch entries) + }; + // Context for the source-payload reader trampoline (maps a payload-relative offset to a backing read: + // an external MotaSource, or — when `store` is set — our own fetch store, for re-seeding a completed mota). + struct SrcReadCtx { MotaSource* src; uint8_t idx; uint32_t payload_off; OtaStore* store; }; + + void begin(uint32_t my_target_id, OtaSend send, void* ctx); + + // --- serve --- Provide a complete, contiguous `.mota` to serve (caller keeps it alive). + bool serve(const uint8_t* mota, uint32_t len); + // Serve from a non-contiguous source (e.g. our own firmware in flash): a pre-assembled manifest + // (manifest-minus-leaves, `mfl` bytes), the pre-computed merkle `leaves` (kept alive by caller), and a + // `read` callback for payload blocks. Lets a node host its own image without holding it in RAM. + bool serve_self(const uint8_t* manifest, uint16_t mfl, const uint8_t* leaves, uint32_t block_count, + uint8_t* proof_scratch, uint32_t proof_scratch_sz, ServeReadFn read, void* ctx); + // Attach an external "folder" of `.mota` images (USB-serial daemon, BLE, WiFi URLs, NFS/samba, ...). + // The node then advertises + RELAYS them transparently alongside its own fw — peers just see more mOTAs. + // Re-enumerates the source into the serve registry. Returns false if no slots remain. (Trustless: the + // fetcher verifies merkle+signature, so the source is never trusted — see OtaSource.h.) + bool add_source(MotaSource* src); + // Re-read every attached source's catalog (call when the folder's contents change). Rebuilds entries + // [1..] from the sources; entry 0 (our own fw) is preserved. + void refresh_sources(); + // Drop all external sources (keep serving our own fw). + void clear_sources(); + uint8_t servedCount() const { return _n_serve; } // total mOTAs we offer (own fw + folder) + // Read-only view of one served entry (for `ota serve` listing): mid/target/fwver/codec/flags + is_self. + const ServeEntry* servedEntry(uint8_t i) const { return i < _n_serve ? &_serve[i] : nullptr; } + + // Broadcast the tiny per-node BEACON (OTA_ADV): seeder_id + count + set-digest of everything we serve. + // Constant size regardless of how many mOTAs — peers ask for the catalog via OTA_QUERY only on interest. + void announce(); + + // --- fetch --- Provide the staging store; fetching starts on a matching OTA_ADV. + void set_fetch_store(OtaStore* s) { _fetch = s; } + + // Resume a fetch from a container already persisted in the store (after a reboot). want_mid=nullptr + // accepts whatever is staged; otherwise only resumes if the staged manifest_id matches. Re-parses the + // stored manifest, recomputes geometry, counts present blocks, and continues FETCHING the holes (or goes + // straight to COMPLETE if all blocks are present). Returns true if it adopted a staged container. + bool resumeStaged(const uint8_t* want_mid); + + // Manual cross-target override (decision: deliberate role switch, e.g. companion -> repeater on the + // same hardware). Normally a node only auto-fetches its OWN target_id; `want(T)` makes it accept an + // ADV for target T instead (T=0 restores auto). The user takes responsibility for HW compatibility; + // a hw_id brick-safety check is the planned safety layer (see docs/ota_protocol.md / plan). + void want(uint32_t target_id) { _desired_target = target_id; reDiscover(); } + uint32_t wanted() const { return _desired_target; } + uint32_t target() const { return _target; } // this node's own OTA target_id (set in begin) + + // Pull a SPECIFIC advertised mOTA by manifest_id (e.g. `ota pull <#>` picks the one more peers have), + // not just any firmware for the target. mid=nullptr clears the filter (accept any mid for the target). + void want_mid(const uint8_t* mid) { + if (mid) { for (int i = 0; i < 4; i++) _desired_mid[i] = mid[i]; _have_desired_mid = true; } + else _have_desired_mid = false; + reDiscover(); + } + + // Begin fetching a chosen mid now (sets want + starts the manifest fetch / resume). Used by `ota pull` + // once the user picks a catalogued mOTA (the source is reached via the flooded GET_MANIFEST). + void pull(const uint8_t* mid, uint32_t target) { want(target); want_mid(mid); startFetch(mid, target); } + // Ask every known source for its catalog (populates `ota neighbors`). Async — rows arrive via OTA_HAVE. + void queryAll(); + // Coarse clock for source/catalog ages + LRU (the Mesh adapter feeds millis; 0 in host tests is fine). + void set_clock(uint32_t ms) { _now_ms = ms; } + + // Codec compatibility: a node only fetches/accepts fw it can actually apply. CODEC_FULL is always + // acceptable; the platform's single delta codec is set here (ESP32 A/B -> sequential, nRF52 single- + // slot -> in-place). A mismatching `.mota` is rejected at OTA_ADV time, before fetching anything. + void set_apply_codec(uint8_t c) { _apply_codec = c; } + // A platform may apply MORE than one delta codec (ESP32 does both sequential AND in-place, so a single + // in-place `.mota` can target both ESP32 and nRF52). 0xFF = unset. + void set_apply_codec2(uint8_t c) { _apply_codec2 = c; } + bool codecOk(uint8_t c) const { return c == CODEC_FULL || c == _apply_codec || c == _apply_codec2; } + + // Auto-fetch policy (manual `ota pull` always works regardless): 0=off (discover only), 1=any + // compatible own-target advert, 2=only signed adverts. Conservative default = off. + 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; } + + // Resume checkpoint cadence (runtime-tunable, persisted in NodePrefs): persist progress every N + // 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; } + + // Max OTA flood reach in HOPS (runtime-tunable, persisted): a node accepts OTA from up to N hops away and + // relays packets still under N hops; 0 = direct only (accept path_count 0, never relay). Bounds duty-cycle + // when crossing repeaters. Default OTA_HOP_LIMIT_DEFAULT. Set via `ota config hops`. + void set_max_hops(uint8_t h) { _max_hops = h; } + uint8_t max_hops() const { return _max_hops; } + 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. + void set_seeder_id(const uint8_t* id4) { if (id4) for (int i = 0; i < 4; i++) _seeder_id[i] = id4[i]; } + + void on_message(const uint8_t* msg, uint16_t len); // feed one received OTA message + void loop(); // drive fetch (re-request missing blocks) + + // Drop the current fetch session back to IDLE (so a fresh `ota pull` / advert starts a new one). Also + // stops re-seeding a previously-completed download — callers clear the staging store right after, so the + // re-seed view would otherwise advertise a mota we can no longer serve. + void reset_session() { + _fstate = IDLE; _have = 0; _req_count = 0; _mf_retries = 0; + clearReassembly(); + _loop_last_have = 0; _loop_last_mask = 0; + _mf_total = 0; _mf_mask = 0; _mf_len = 0; + unserveFetched(); + } + + FetchState fetchState() const { return _fstate; } + uint32_t blocksHave() const { return _have; } + uint32_t blocksTotal() const { return _fbc; } + const uint8_t* fetchManifestId() const { return _fid; } + + // --- discovery catalog (for `ota neighbors`): mOTAs heard around us via OTA_HAVE, deduped by mid --- + static const uint8_t OTA_CAT_SEEDERS = 4; // distinct sources tracked per catalog row (for "N nodes have it") + struct CatRow { + uint8_t mid[4]; + uint32_t target_id, fw_version; + uint8_t codec, flags; + uint8_t seeders[OTA_CAT_SEEDERS][4]; // distinct sources advertising this mid (deduped; capped) + uint8_t n_seeders; // count of the above (capped at OTA_CAT_SEEDERS) — "N+ nodes have it" + uint32_t have_max; // best block-count any source reported (== total when a full copy exists) + uint32_t last_ms; + }; + uint8_t catalogCount() const { return _n_cat; } + const CatRow* catalogRow(uint8_t i) const { return i < _n_cat ? &_catalog[i] : nullptr; } + uint8_t sourceCount() const { return _n_src; } // distinct OTA sources (beacon senders) heard + +private: + void emit(const uint8_t* b, uint16_t n, bool flood) { if (_send && n) _send(_ctx, b, n, flood); } + void handleAdv(const uint8_t* m, uint16_t n); // beacon -> sources table (+ query if interested) + void handleQuery(const uint8_t* m, uint16_t n); // serve: reply OTA_HAVE catalog + void handleHave(const uint8_t* m, uint16_t n); // peer: catalog rows (+ startFetch if a row matches) + void handleGetManifest(const uint8_t* m, uint16_t n); + void handleManifest(const uint8_t* m, uint16_t n); + void handleReq(const uint8_t* m, uint16_t n); + void handleData(const uint8_t* m, uint16_t n); + void handleReqProof(const uint8_t* m, uint16_t n); + void handleProof(const uint8_t* m, uint16_t n); + void startFetch(const uint8_t* mid, uint32_t target); // begin/resume a fetch of a chosen mid + bool wantRow(const uint8_t* mid, uint32_t target, uint8_t codec, uint8_t flags) const; // fetch this row? + void noteOverheardReq(const uint8_t* m, uint16_t n); // observe a peer's OTA_REQ (swarm de-dup) + uint32_t rngNext() { _rng = _rng * 1664525u + 1013904223u; return _rng; } // per-node LCG (block pick/jitter) + void seedBlockRng() { _rng = (rd_u32le(_seeder_id) ^ rd_u32le(_fid)) | 1u; } // distinct per node (id^mid) + void clearReassembly(); // forget the in-flight block (reset to NO_BLOCK) + void armFirstReqHold(); // de-sync the first REQ across swarm peers (jitter) + uint32_t pickMissingBlock(); // choose the next block to request (swarm-aware) + int serveEntryIndex(const uint8_t* mid) const; // registry slot serving this mid (-1 if none) + ServeView* resolve(const uint8_t* mid); // pick/load the ServeView for this mid (nullptr) + bool loadSource(const ServeEntry& e); // load an external mota into _srcv (head+leaves) + void registerSelfEntry(); // (re)build entry[0] from view0 + static bool srcReadTramp(void* c, uint32_t off, uint8_t* buf, uint32_t len); // source payload reader + void serveFetched(); // after COMPLETE: re-seed the staged mota (epidemic) + void unserveFetched(); // stop re-seeding (store about to be overwritten) + void emitBlockData(const uint8_t* mid, uint32_t idx, const uint8_t* data, uint32_t blen); // DATA fragments + bool recentlyServed(uint32_t blk) const; // a peer just broadcast this block's DATA? + void noteOverheardData(const uint8_t* m, uint16_t n); // remember overheard DATA (serve de-dup) + void sendQuery(const uint8_t* seeder, const uint8_t* digest, uint32_t filter_target); // ask a source for its catalog + void scheduleQuery(const uint8_t* seeder, const uint8_t* digest); // jittered + suppressible + void reDiscover() { for (uint8_t i = 0; i < _n_src; i++) _sources[i].have_catalog = false; _pq_active = false; } + void setDigest(uint8_t out[4]) const; // sha2-256:4 over our served mids + bool blockPresent(uint32_t i) const; + void requestMissing(); + uint32_t blockLen(uint32_t i) const; + + uint32_t _target = 0; + OtaSend _send = nullptr; + void* _ctx = nullptr; + + // serve (multi-mota): view0 = our own fw / a RAM `.mota`; _srcv = the currently-loaded external mota. + ServeView _view0; + ServeView _srcv; + uint8_t _srcv_mid[4] = {0}; + SrcReadCtx _srcv_rdctx = {nullptr, 0, 0}; + ServeEntry _serve[OTA_MAX_SERVE]; // catalog (what we advertise) — entry 0 is view0 + uint8_t _n_serve = 0; + MotaSource* _src_list[OTA_MAX_SOURCE_OBJ] = {nullptr}; + uint8_t _n_src_obj = 0; + bool _fetch_served = false; // we re-seed our last completed download (epidemic spread) + MotaDesc _fetch_desc; // its catalog descriptor (mid + region offsets) + uint8_t _src_manifest[OTA_SRC_MANIFEST_MAX]; // manifest-minus-leaves of the loaded source mota + uint8_t _src_leaves[OTA_PROOFGEN_SCRATCH]; // leaves[] of the loaded source mota (<=1024 blocks) + uint8_t _scratch[OTA_PROOFGEN_SCRATCH]; // proof-gen / fetch root-check working buffer + + // fetch + OtaStore* _fetch = nullptr; + FetchState _fstate = IDLE; + uint8_t _fid[4] = {0}; + uint8_t _froot[4] = {0}; + uint32_t _ftotal = 0, _fpoff = 0, _floff = 0, _fpsize = 0, _fbc = 0, _fbs = 0; + uint32_t _have = 0; + uint32_t _req_start = 0, _req_count = 0; // last block requested (per-block serial flow; telemetry) + uint32_t _loop_last_have = 0; // for stall detection in loop() + // swarm load-spreading (so 50 fetchers don't all hammer the seeder for the same block in lockstep) + uint32_t _rng = 0; // per-node LCG state (seeded from seeder_id^fid) + uint32_t _req_hold_at = 0; // _now_ms before which we hold the first REQ (startup jitter) + uint32_t _peer_req_block = NO_BLOCK; // a block a peer just REQ'd (its broadcast DATA will fill us) + uint32_t _peer_req_at = 0; // when we overheard it (suppression window) + // serve-side de-dup: blocks whose DATA we recently overheard ANOTHER holder broadcast (don't re-serve) + uint32_t _recent_blk[8]; + uint32_t _recent_at[8] = {0}; + uint8_t _recent_i = 0; + uint32_t _desired_target = 0; // manual cross-target override (0 = auto / own target) + 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) + uint8_t _apply_codec2 = 0xFF; // optional 2nd accepted delta codec (ESP32: in-place) + 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 _max_hops = OTA_HOP_LIMIT_DEFAULT; // OTA flood reach in hops; 0=direct only (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) + uint16_t _reasm_mask = 0; // received FRAG_DATA-slice bitmap (bit k = slice @ k*FRAG_DATA) + uint16_t _reasm_need = 0; // full mask once all slices of the current block are in + bool _awaiting_proof = false; // data complete; REQ_PROOF sent, verify on PROOF + uint16_t _loop_last_mask = 0; // fragment-level stall detection in loop() + uint8_t _reasm_buf[OTA_MAX_BLOCK]; + // multi-fragment manifest reassembly (a signed v2 manifest exceeds one packet) + uint8_t _mf_buf[OTA_MF_MAXFRAG * OTA_MF_FRAG]; // sized to the fragment cap so no valid manifest is silently dropped + uint16_t _mf_retries = 0; // GET_MANIFEST retries while WANT_MANIFEST (give up after a cap) + uint8_t _mf_total = 0; // frag_total of the manifest being reassembled (0 = none) + uint16_t _mf_mask = 0; // received manifest-fragment bitmap + uint32_t _mf_len = 0; // assembled manifest length (set by the last fragment) + + // discovery: heard sources (beacon senders) + the catalog assembled from their OTA_HAVE replies + struct Source { uint8_t seeder[4]; uint8_t digest[4]; uint8_t n_motas; uint32_t last_ms; bool have_catalog; }; + Source _sources[OTA_MAX_SOURCES]; + uint8_t _n_src = 0; + CatRow _catalog[OTA_MAX_CATALOG]; + uint8_t _n_cat = 0; + uint32_t _now_ms = 0; // coarse clock (fed by set_clock; for ages/LRU/jitter) + // pending catalog query (jittered + suppressed on overhearing a matching QUERY/HAVE — anti-storm) + bool _pq_active = false; + uint8_t _pq_seeder[4] = {0}; + uint8_t _pq_digest[4] = {0}; + uint32_t _pq_at = 0; // _now_ms deadline to actually send the query +}; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaProtocol.cpp b/src/helpers/ota/OtaProtocol.cpp new file mode 100644 index 0000000000..50e6147668 --- /dev/null +++ b/src/helpers/ota/OtaProtocol.cpp @@ -0,0 +1,137 @@ +#include "OtaProtocol.h" +#include + +namespace mesh { +namespace ota { + +// Little-endian cursor helpers. +namespace { +struct W { + uint8_t* p; uint16_t cap; uint16_t n; bool ok; + W(uint8_t* b, uint16_t c) : p(b), cap(c), n(0), ok(true) {} + void u8(uint8_t v) { if (n + 1 > cap) { ok = false; return; } p[n++] = v; } + void u16(uint16_t v){ u8(v & 0xFF); u8(v >> 8); } + void u32(uint32_t v){ u8(v); u8(v >> 8); u8(v >> 16); u8(v >> 24); } + void raw(const uint8_t* d, uint16_t l) { if (n + l > cap) { ok = false; return; } memcpy(p + n, d, l); n += l; } +}; +struct R { + const uint8_t* p; uint16_t len; uint16_t n; bool ok; + R(const uint8_t* b, uint16_t l) : p(b), len(l), n(0), ok(true) {} + uint8_t u8() { if (n + 1 > len) { ok = false; return 0; } return p[n++]; } + uint16_t u16() { uint16_t a = u8(); return a | ((uint16_t)u8() << 8); } + uint32_t u32() { uint32_t a = u8(); a |= (uint32_t)u8() << 8; a |= (uint32_t)u8() << 16; a |= (uint32_t)u8() << 24; return a; } + const uint8_t* raw(uint16_t l) { if (n + l > len) { ok = false; return nullptr; } const uint8_t* r = p + n; n += l; return r; } + uint16_t remaining() const { return len - n; } +}; +} // namespace + +uint16_t encode_adv(uint8_t* buf, uint16_t cap, const AdvMsg& m) { // tiny per-node beacon + W w(buf, cap); w.u8(OTA_ADV); w.raw(m.seeder_id, 4); w.u8(m.n_motas); w.raw(m.set_digest, 4); + return w.ok ? w.n : 0; +} +bool decode_adv(const uint8_t* buf, uint16_t len, AdvMsg& m) { + R r(buf, len); if (r.u8() != OTA_ADV) return false; + const uint8_t* sid = r.raw(4); if (sid) memcpy(m.seeder_id, sid, 4); + m.n_motas = r.u8(); + const uint8_t* d = r.raw(4); if (d) memcpy(m.set_digest, d, 4); + return r.ok; +} + +uint16_t encode_query(uint8_t* buf, uint16_t cap, const QueryMsg& m) { + W w(buf, cap); w.u8(OTA_QUERY); w.raw(m.seeder_id, 4); w.raw(m.set_digest, 4); w.u32(m.filter_target); + return w.ok ? w.n : 0; +} +bool decode_query(const uint8_t* buf, uint16_t len, QueryMsg& m) { + R r(buf, len); if (r.u8() != OTA_QUERY) return false; + const uint8_t* sid = r.raw(4); if (sid) memcpy(m.seeder_id, sid, 4); + const uint8_t* dg = r.raw(4); if (dg) memcpy(m.set_digest, dg, 4); + m.filter_target = r.u32(); + return r.ok; +} + +uint16_t encode_have(uint8_t* buf, uint16_t cap, const HaveMsg& m) { + W w(buf, cap); w.u8(OTA_HAVE); w.raw(m.seeder_id, 4); w.raw(m.set_digest, 4); + w.u8(m.frag_idx); w.u8(m.frag_total); w.u8(m.n_rows); w.raw(m.rows, (uint16_t)m.n_rows * OTA_HAVE_ROW_BYTES); + return w.ok ? w.n : 0; +} +bool decode_have(const uint8_t* buf, uint16_t len, HaveMsg& m) { + R r(buf, len); if (r.u8() != OTA_HAVE) return false; + const uint8_t* sid = r.raw(4); if (sid) memcpy(m.seeder_id, sid, 4); + const uint8_t* dg = r.raw(4); if (dg) memcpy(m.set_digest, dg, 4); + m.frag_idx = r.u8(); m.frag_total = r.u8(); m.n_rows = r.u8(); + m.rows = r.raw((uint16_t)m.n_rows * OTA_HAVE_ROW_BYTES); + return r.ok; +} + +uint16_t encode_get_manifest(uint8_t* buf, uint16_t cap, const GetManifestMsg& m) { + W w(buf, cap); w.u8(OTA_GET_MANIFEST); w.raw(m.manifest_id, 4); + return w.ok ? w.n : 0; +} +bool decode_get_manifest(const uint8_t* buf, uint16_t len, GetManifestMsg& m) { + R r(buf, len); if (r.u8() != OTA_GET_MANIFEST) return false; + const uint8_t* id = r.raw(4); if (id) memcpy(m.manifest_id, id, 4); + return r.ok; +} + +uint16_t encode_manifest(uint8_t* buf, uint16_t cap, const ManifestMsg& m) { + W w(buf, cap); w.u8(OTA_MANIFEST); w.raw(m.manifest_id, 4); w.u8(m.frag_idx); w.u8(m.frag_total); + w.raw(m.bytes, m.len); + return w.ok ? w.n : 0; +} +bool decode_manifest(const uint8_t* buf, uint16_t len, ManifestMsg& m) { + R r(buf, len); if (r.u8() != OTA_MANIFEST) return false; + const uint8_t* id = r.raw(4); if (id) memcpy(m.manifest_id, id, 4); + m.frag_idx = r.u8(); m.frag_total = r.u8(); + m.len = r.remaining(); m.bytes = r.raw(m.len); + return r.ok; +} + +uint16_t encode_req(uint8_t* buf, uint16_t cap, const ReqMsg& m) { + W w(buf, cap); w.u8(OTA_REQ); w.raw(m.manifest_id, 4); w.u16(m.start_block); w.u8(m.count); + return w.ok ? w.n : 0; +} +bool decode_req(const uint8_t* buf, uint16_t len, ReqMsg& m) { + R r(buf, len); if (r.u8() != OTA_REQ) return false; + const uint8_t* id = r.raw(4); if (id) memcpy(m.manifest_id, id, 4); + m.start_block = r.u16(); m.count = r.u8(); + return r.ok; +} + +uint16_t encode_data(uint8_t* buf, uint16_t cap, const DataMsg& m) { + W w(buf, cap); w.u8(OTA_DATA); w.raw(m.manifest_id, 4); w.u16(m.block_idx); w.u16(m.frag_off); + w.raw(m.data, m.data_len); + return w.ok ? w.n : 0; +} +bool decode_data(const uint8_t* buf, uint16_t len, DataMsg& m) { + R r(buf, len); if (r.u8() != OTA_DATA) return false; + const uint8_t* id = r.raw(4); if (id) memcpy(m.manifest_id, id, 4); + m.block_idx = r.u16(); m.frag_off = r.u16(); + m.data_len = r.remaining(); m.data = r.raw(m.data_len); + return r.ok; +} + +uint16_t encode_req_proof(uint8_t* buf, uint16_t cap, const ReqProofMsg& m) { + W w(buf, cap); w.u8(OTA_REQ_PROOF); w.raw(m.manifest_id, 4); w.u16(m.block_idx); + return w.ok ? w.n : 0; +} +bool decode_req_proof(const uint8_t* buf, uint16_t len, ReqProofMsg& m) { + R r(buf, len); if (r.u8() != OTA_REQ_PROOF) return false; + const uint8_t* id = r.raw(4); if (id) memcpy(m.manifest_id, id, 4); + m.block_idx = r.u16(); + return r.ok; +} + +uint16_t encode_proof(uint8_t* buf, uint16_t cap, const ProofMsg& m) { + W w(buf, cap); w.u8(OTA_PROOF); w.raw(m.manifest_id, 4); w.u16(m.block_idx); + w.u8(m.n_proof); w.raw(m.proof, (uint16_t)m.n_proof * 4); + return w.ok ? w.n : 0; +} +bool decode_proof(const uint8_t* buf, uint16_t len, ProofMsg& m) { + R r(buf, len); if (r.u8() != OTA_PROOF) return false; + const uint8_t* id = r.raw(4); if (id) memcpy(m.manifest_id, id, 4); + m.block_idx = r.u16(); m.n_proof = r.u8(); m.proof = r.raw((uint16_t)m.n_proof * 4); + return r.ok; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaProtocol.h b/src/helpers/ota/OtaProtocol.h new file mode 100644 index 0000000000..d8385353a1 --- /dev/null +++ b/src/helpers/ota/OtaProtocol.h @@ -0,0 +1,112 @@ +#pragma once + +#include +#include +#include "OtaFormat.h" + +// Encode/decode for the OTA LoRa messages (docs/ota_protocol.md §8). Each message is a packet payload: +// [0]=ota_msg_type, then a fixed body. Portable + allocation-free; unit-tested on the host. +// +// manifest_id == the manifest's merkle_root (4 bytes), a compact content id. + +namespace mesh { +namespace ota { + +// ---- OTA_ADV: tiny per-NODE beacon (flood, periodic). CONSTANT size regardless of how many mOTAs a +// node serves — it just says "I'm a source, here's how many + a digest of my set". A peer that's +// interested asks for the catalog via OTA_QUERY. (Replaces the old per-mOTA advert so a folder node with +// N images costs one 10-byte beacon, not N adverts.) ---- +struct AdvMsg { + uint8_t seeder_id[4]; // advertiser's node id = pubkey[0:4]; the QUERY address + distinct-source id + uint8_t n_motas; // # of complete, servable mOTAs (saturates at 255) + uint8_t set_digest[4]; // sha2-256:4 over the sorted set of served mids; "did my offering change?" +}; + +// ---- OTA_QUERY: "list what you serve" — addressed to a source by seeder_id, FLOODED so neighbours +// overhear it (storm suppression). set_digest identifies the offering being asked about (so an overhearer +// can suppress its own pending query for the same {source,digest}). filter_target=0 = everything. ---- +struct QueryMsg { + uint8_t seeder_id[4]; // which source this query is for (the source matches its own id) + uint8_t set_digest[4]; // the offering digest we're asking about (for overhear-suppression) + uint32_t filter_target; // 0 = all (the scalable default); else only mOTAs for this target_id +}; + +// ---- OTA_HAVE: the compact catalog (source -> mesh), FLOODED + tagged with set_digest so EVERY node +// that overhears it caches the rows (passive, no query needed). Fragmented. ---- +// body: seeder_id(4) set_digest(4) frag_idx(1) frag_total(1) n_rows(1) rows[ mid(4) target(4) fwver(4) codec(1) flags(1) ] +struct HaveRow { uint8_t mid[4]; uint32_t target_id; uint32_t fw_version; uint8_t codec_id; uint8_t flags; + uint16_t have_count; }; // blocks the advertiser holds (== block_count if complete; less => partial source) +struct HaveMsg { + uint8_t seeder_id[4]; + uint8_t set_digest[4]; // the offering this catalog describes (overhearers cache by it) + uint8_t frag_idx, frag_total; + uint8_t n_rows; // rows in THIS fragment + const uint8_t* rows; // points into buf: n_rows * OTA_HAVE_ROW_BYTES +}; +static const uint8_t OTA_HAVE_ROW_BYTES = 16; // mid4 + target4 + fwver4 + codec1 + flags1 + have_count2 + +// ---- OTA_GET_MANIFEST: request the manifest for a content id (direct) ---- +struct GetManifestMsg { uint8_t manifest_id[4]; }; + +// ---- OTA_MANIFEST: the manifest-minus-leaves[], fragmented (direct) ---- +// body: manifest_id(4) frag_idx(1) frag_total(1) bytes[] +struct ManifestMsg { + uint8_t manifest_id[4]; + uint8_t frag_idx, frag_total; + const uint8_t* bytes; uint16_t len; +}; + +// ---- OTA_REQ: request a window of blocks (direct) ---- +struct ReqMsg { uint8_t manifest_id[4]; uint16_t start_block; uint8_t count; }; + +// ---- OTA_DATA: one self-describing fragment of a block's data (proof is fetched separately) ---- +// body: manifest_id(4) block_idx(2) frag_off(2) data[] +// `frag_off` is the byte offset of `data` within block `block_idx` (global position = block_idx*block_size +// + frag_off), so a fragment is self-placing and can be requested from ANY peer (BitTorrent-style). +struct DataMsg { + uint8_t manifest_id[4]; + uint16_t block_idx; + uint16_t frag_off; + const uint8_t* data; uint16_t data_len; +}; + +// ---- OTA_REQ_PROOF: request the merkle proof for one (reassembled) block (direct) ---- +struct ReqProofMsg { uint8_t manifest_id[4]; uint16_t block_idx; }; + +// ---- OTA_PROOF: the merkle proof (ordered sibling digests) for one block (direct) ---- +struct ProofMsg { uint8_t manifest_id[4]; uint16_t block_idx; uint8_t n_proof; const uint8_t* proof; }; + +// Each encode_* returns the total payload length (incl. the leading msg-type byte), 0 on overflow. +// Each decode_* returns true on success (and points struct fields into `buf`). + +uint16_t encode_adv(uint8_t* buf, uint16_t cap, const AdvMsg& m); +bool decode_adv(const uint8_t* buf, uint16_t len, AdvMsg& m); + +uint16_t encode_query(uint8_t* buf, uint16_t cap, const QueryMsg& m); +bool decode_query(const uint8_t* buf, uint16_t len, QueryMsg& m); + +uint16_t encode_have(uint8_t* buf, uint16_t cap, const HaveMsg& m); +bool decode_have(const uint8_t* buf, uint16_t len, HaveMsg& m); + +uint16_t encode_get_manifest(uint8_t* buf, uint16_t cap, const GetManifestMsg& m); +bool decode_get_manifest(const uint8_t* buf, uint16_t len, GetManifestMsg& m); + +uint16_t encode_manifest(uint8_t* buf, uint16_t cap, const ManifestMsg& m); +bool decode_manifest(const uint8_t* buf, uint16_t len, ManifestMsg& m); + +uint16_t encode_req(uint8_t* buf, uint16_t cap, const ReqMsg& m); +bool decode_req(const uint8_t* buf, uint16_t len, ReqMsg& m); + +uint16_t encode_data(uint8_t* buf, uint16_t cap, const DataMsg& m); +bool decode_data(const uint8_t* buf, uint16_t len, DataMsg& m); + +uint16_t encode_req_proof(uint8_t* buf, uint16_t cap, const ReqProofMsg& m); +bool decode_req_proof(const uint8_t* buf, uint16_t len, ReqProofMsg& m); + +uint16_t encode_proof(uint8_t* buf, uint16_t cap, const ProofMsg& m); +bool decode_proof(const uint8_t* buf, uint16_t len, ProofMsg& m); + +inline uint8_t ota_msg_type(const uint8_t* buf, uint16_t len) { return len ? buf[0] : 0xFF; } + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaSelf.cpp b/src/helpers/ota/OtaSelf.cpp new file mode 100644 index 0000000000..f1f1902d4e --- /dev/null +++ b/src/helpers/ota/OtaSelf.cpp @@ -0,0 +1,186 @@ +#include "OtaSelf.h" +#include "FirmwareInfo.h" +#include "OtaByteIO.h" +#include + +#if defined(ESP32_PLATFORM) + #include "esp_ota_ops.h" + #include "esp_partition.h" +#elif defined(NRF52_PLATFORM) + #include "OtaFlashLayout_nrf52.h" +#endif + +#if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM) + #include "OtaContext.h" // serve our own fw from flash (cache leaves, read payload on demand) + #include "MerkleTree.h" + #include + #include + #ifndef OTA_SELF_LEAVES_MAX + #define OTA_SELF_LEAVES_MAX 65536u // cap heap for cached leaves (~16k blocks @1 KB = up to ~16 MB image) + #endif +#endif + +namespace mesh { +namespace ota { + +#if defined(ESP32_PLATFORM) +// Scan the running app partition for the firmware's EndF trailer using esp_partition_read (stable +// across IDF versions — no mmap). Same rule as find_self_firmware(): the marker's absolute offset +// must equal its stored body_len, which uniquely identifies the running firmware's own trailer. +bool ota_self_firmware(SelfFwInfo& out) { + out = SelfFwInfo(); + const esp_partition_t* p = esp_ota_get_running_partition(); + if (!p) return false; + + const uint32_t CH = 512; + uint8_t buf[CH + ENDF_LEN]; // overlap so a marker spanning a chunk edge is still seen + for (uint32_t base = 0; base + ENDF_LEN <= p->size; base += CH) { + uint32_t want = CH + ENDF_LEN; + if (base + want > p->size) want = p->size - base; + if (esp_partition_read(p, base, buf, want) != ESP_OK) return false; + for (uint32_t i = 0; i + ENDF_LEN <= want; i++) { + if (buf[i] != ENDF_MAGIC[0]) continue; + if (memcmp(buf + i, ENDF_MAGIC, 4) != 0) continue; + uint32_t body_len = rd_u32le(buf + i + 4); + if (body_len != base + i) continue; // must sit immediately after a body of that length + out.valid = true; + out.endf_offset = body_len; + out.body_len = body_len; + out.image_len = body_len + ENDF_LEN; + memcpy(out.body_hash, buf + i + 8, 8); + // Fixed 56-byte trailer: re-read it whole at the marker (it may straddle the chunk window, so the + // identity fields aren't reliably in `buf`) and pull identity from constant offsets (docs §2). + uint8_t tr[ENDF_LEN]; + if (body_len + ENDF_LEN <= p->size && + esp_partition_read(p, body_len, tr, ENDF_LEN) == ESP_OK) { + out.fw_version = (uint32_t)tr[16] | ((uint32_t)tr[17]<<8) | ((uint32_t)tr[18]<<16) | ((uint32_t)tr[19]<<24); + out.target_id = (uint32_t)tr[20] | ((uint32_t)tr[21]<<8) | ((uint32_t)tr[22]<<16) | ((uint32_t)tr[23]<<24); + memcpy(out.hw_id, tr + 24, 32); out.hw_id[32] = 0; + } + return true; + } + } + return false; +} +#elif defined(NRF52_PLATFORM) +// nRF52 internal flash is memory-mapped, so the running app is directly scannable. The body starts at +// APP_BASE; find_self_firmware() picks the EndF whose stored body_len equals its offset (the running +// firmware's own trailer), ignoring any staged `.mota` (which carries its own embedded EndF) higher up. +bool ota_self_firmware(SelfFwInfo& out) { + const uint8_t* region = (const uint8_t*)(uintptr_t)MOTA_NRF52_APP_BASE; + uint32_t region_len = MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE; + return find_self_firmware(region, region_len, out, /*verify_body=*/true); +} +#else +bool ota_self_firmware(SelfFwInfo& out) { + // STM32/RP2040: app-region access lands with their apply path. + out = SelfFwInfo(); + return false; +} +#endif + +#if defined(ESP32_PLATFORM) +bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) { + const esp_partition_t* p = esp_ota_get_running_partition(); + return p && esp_partition_read(p, off, buf, len) == ESP_OK; +} +#elif defined(NRF52_PLATFORM) +bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len) { + if ((uint64_t)MOTA_NRF52_APP_BASE + off + len > MOTA_NRF52_FS_START) return false; + memcpy(buf, (const uint8_t*)(uintptr_t)(MOTA_NRF52_APP_BASE + off), len); + return true; +} +#else +bool ota_self_read(uint32_t, uint8_t*, uint32_t) { return false; } +#endif + +#if defined(ESP32_PLATFORM) || defined(NRF52_PLATFORM) +static bool self_read_cb(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) { + (void)ctx; return ota_self_read(off, buf, len); +} +// Build (once) the full-image manifest + merkle leaves for the running firmware, cache them in `c`, and +// hand the manager a flash-read callback for the payload. The image is read ONCE here to compute the +// leaves + image_hash; thereafter a block REQ reads only that block (proof comes from the cached leaves). +// Pack the first "MAJOR.MINOR.PATCH" found in `s` into the comparable uint32 the manifest uses +// (MAJOR<<24 | MINOR<<16 | PATCH<<8). Returns 0 if there's no dotted number (e.g. a "dev-" build). +static uint32_t parse_fw_version(const char* s) { + if (!s) return 0; + for (; *s; s++) { // find the start of a "d.d" run + if (*s < '0' || *s > '9') continue; + const char* p = s; uint32_t a = 0, b = 0, d = 0; int dots = 0; + uint32_t* cur = &a; + for (; *p; p++) { + if (*p >= '0' && *p <= '9') { *cur = *cur * 10 + (uint32_t)(*p - '0'); } + else if (*p == '.' && dots < 2) { dots++; cur = (dots == 1) ? &b : &d; } + else break; + } + if (dots >= 1) return FwVersion{ (uint8_t)a, (uint8_t)b, (uint8_t)d, 0 }.pack(); + s = p - 1; // a bare number, no dots — keep scanning + } + return 0; +} + +bool ota_serve_self(OtaContext& c, uint32_t fw_version) { + // Derive our version from the build string when the caller didn't supply one, so the mOTA we advertise + // carries a real version (was hard-coded 0 -> peers saw "v0.0.0"). A dev build with no dotted number + // still reads 0 — the self-describing EndF identity (docs) is the durable fix for that. +#ifdef FIRMWARE_VERSION + if (fw_version == 0) fw_version = parse_fw_version(FIRMWARE_VERSION); +#endif + SelfFwInfo fi; + if (!ota_self_firmware(fi) || !fi.valid) return false; + // 1 KB logical blocks (delivered as multiple LoRa fragments): 8x fewer merkle leaves than 128 B, so a + // ~530 KB image is ~518 blocks (proof-gen scratch ~2 KB) instead of ~4150 (which overflowed the scratch). + const uint32_t image_size = fi.image_len, BS = OTA_DEFAULT_BLOCK_SIZE; + const uint32_t bc = (image_size + BS - 1) / BS; + if ((uint64_t)bc * 4 > OTA_SELF_LEAVES_MAX) return false; + + free(c.serve_self_leaves); free(c.serve_self_proof); + c.serve_self_leaves = (uint8_t*)malloc((size_t)bc * 4); + c.serve_self_proof = (uint8_t*)malloc((size_t)bc * 4); // proof-gen working buffer (sized to OUR image) + if (!c.serve_self_leaves || !c.serve_self_proof) { + free(c.serve_self_leaves); free(c.serve_self_proof); + c.serve_self_leaves = c.serve_self_proof = nullptr; + return false; + } + + SHA256 sha; uint8_t blk[BS]; + for (uint32_t i = 0, off = 0; i < bc; i++, off += BS) { + uint32_t blen = (off + BS <= image_size) ? BS : (image_size - off); + if (!ota_self_read(off, blk, blen)) { + free(c.serve_self_leaves); free(c.serve_self_proof); + c.serve_self_leaves = c.serve_self_proof = nullptr; + return false; + } + merkle_leaf(c.serve_self_leaves + (size_t)i * 4, blk, blen); + sha.update(blk, blen); + } + uint8_t image_hash[32]; sha.finalize(image_hash, 32); + uint8_t root[4]; merkle_root(root, c.serve_self_leaves, bc); + + // Prefer the SELF-DESCRIBING identity embedded in our own EndF (docs §2) over build flags / the param — + // it's correct regardless of how the firmware was built (build.sh injection, IDE, etc.). + uint32_t out_target = fi.target_id ? fi.target_id : c.manager.target(); + uint32_t out_ver = fi.fw_version ? fi.fw_version : fw_version; + const char* out_hw = fi.hw_id[0] ? fi.hw_id : c.hw_id; + + // Assemble the fixed-layout manifest-minus-leaves (full, unsigned) = MOTA_MFL bytes. base_hash(89), + // signer_pubkey(97) and signature(129) stay zero-filled (full + unsigned); only `approval` is set. + uint8_t* m = c.serve_self_manifest; + memset(m, 0, MOTA_MFL); + m[0] = MOTA_FORMAT_VER; m[1] = MFLAG_FULL; m[2] = HASH_ALGO_SHA256; + wr_u32le(m + 3, out_target); wr_u32le(m + 7, out_ver); + wr_u32le(m + 11, image_size); wr_u32le(m + 15, image_size); // full: payload == image + m[19] = 10; // block_size_log2 = 10 (1024 B logical block) + memcpy(m + 20, root, 4); + memcpy(m + 24, image_hash, 32); + m[56] = CODEC_FULL; + memcpy(m + 57, out_hw, strlen(out_hw) < 32 ? strlen(out_hw) : 32); // hw_id[32] (NUL-padded by memset) + memcpy(m + MOTA_OFF_APPROVAL, APPROVAL_NOT, 4); // approval (fetching device's apply-gate handles it) + return c.manager.serve_self(m, MOTA_MFL, c.serve_self_leaves, bc, + c.serve_self_proof, (size_t)bc * 4, self_read_cb, nullptr); +} +#endif + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaSelf.h b/src/helpers/ota/OtaSelf.h new file mode 100644 index 0000000000..df855c6f43 --- /dev/null +++ b/src/helpers/ota/OtaSelf.h @@ -0,0 +1,28 @@ +#pragma once + +#include "FirmwareInfo.h" + +// Device-side accessor for the running firmware's own image (to read its EndF trailer). +// Per-platform: ESP32 memory-maps the running app partition; other platforms TBD (nRF52 uses the +// bootloader-apply path, so its app-region wiring lands with that work). Not compiled on the native +// host — the portable scan logic in FirmwareInfo.{h,cpp} is what gets unit-tested there. + +namespace mesh { +namespace ota { + +// Locate this firmware's EndF trailer in its own flash image. Returns false if unsupported on this +// platform or no valid EndF is present (e.g. firmware built without the EndF build hook). +bool ota_self_firmware(SelfFwInfo& out); + +// Read `len` bytes of the running firmware image at offset `off` (ESP32: running partition via +// esp_partition_read; nRF52: memory-mapped app region). false on unsupported platforms. +bool ota_self_read(uint32_t off, uint8_t* buf, uint32_t len); + +// Compute (once) + cache our running firmware's manifest + merkle leaves in `c`, then serve it from +// flash as a full `.mota` (payload read on demand per block; only metadata held in RAM). Returns false +// if no EndF / image too big / OOM. Device platforms only. +struct OtaContext; +bool ota_serve_self(OtaContext& c, uint32_t fw_version); // target = this node's own (c.manager.target()) + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaSource.h b/src/helpers/ota/OtaSource.h new file mode 100644 index 0000000000..64c8fce694 --- /dev/null +++ b/src/helpers/ota/OtaSource.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include "OtaFormat.h" + +// Transport-agnostic "folder of firmware" abstraction (docs/ota_protocol.md §9). A node can RELAY +// `.mota` images it does not hold in flash — a user drops several `.mota` (different architectures) into +// some external store and the node advertises + serves them as if it held them. Peers just see "this node +// has N mOTAs"; the node knows they are external. The store is reached through a MotaSource, so the SAME +// serve code drives a USB-serial host daemon, BLE, a WiFi URL list, an NFS/samba mount, ... — only the +// `read()` plumbing differs per transport. +// +// The relay is TRUSTLESS: the fetcher verifies every block against the signed merkle root, so a source is +// never trusted. A wrong descriptor or wrong bytes simply makes the fetch fail its merkle/signature check +// — a malicious or buggy source cannot forge firmware, only deny it. + +namespace mesh { +namespace ota { + +// A parsed top-level descriptor of one `.mota` a source provides: enough to advertise it in the catalog +// AND to locate every region for serving, WITHOUT holding the whole image in RAM. Offsets are absolute +// byte positions within the `.mota` container (which always begins MAGIC(4) total(4) manifest...). +struct MotaDesc { + uint8_t mid[4] = {0}; // merkle_root (the content id peers fetch by) + uint32_t target_id = 0; + uint32_t fw_version = 0; + uint8_t codec_id = 0; + uint8_t flags = 0; + uint32_t total_size = 0; // full `.mota` length (bytes) + uint32_t leaves_off = 0; // byte offset of the merkle leaves[] (manifest-minus-leaves = [8, leaves_off)) + uint32_t block_count = 0; // == number of leaves (== number of payload blocks) + uint32_t payload_off = 0; // byte offset of the payload + uint32_t payload_size = 0; +}; + +// One or more complete `.mota` images, reachable as random-access bytes. Implementations are device-side +// and transport-specific (the engine in OtaManager is portable and never includes this directly for I/O; +// it only calls through the interface). +class MotaSource { +public: + virtual ~MotaSource() {} + // Number of complete, servable mOTAs this source currently offers (may change as the folder changes; + // the manager re-enumerates on add_source / refresh). + virtual uint8_t count() = 0; + // Cheap metadata + region offsets for mota `idx`. False if idx is out of range or unparsable. + virtual bool describe(uint8_t idx, MotaDesc& out) = 0; + // Random-access read of `len` bytes at absolute offset `off` of mota `idx` into `buf`. Returns true iff + // exactly `len` bytes were produced. May block on the transport (serial round-trip); OTA is lowest + // priority so latency here is acceptable. + virtual bool read(uint8_t idx, uint32_t off, uint8_t* buf, uint32_t len) = 0; +}; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaStore.h b/src/helpers/ota/OtaStore.h new file mode 100644 index 0000000000..27f2fb294a --- /dev/null +++ b/src/helpers/ota/OtaStore.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include +#include +#include "OtaFormat.h" // MOTA_MAGIC (resume: detect a persisted partial container) + +// Staging backend for an in-transit `.mota` (docs/ota_protocol.md §7). Blocks may arrive out of order +// and progress must survive reboots, so the store is random-access. The transfer/verify logic is +// written against this interface; concrete impls are per-platform (RAM for tests/bring-up; persistent +// flash — ESP32 OTA slot / nRF52 raw region — for production, dropped in behind the same interface). + +namespace mesh { +namespace ota { + +class OtaStore { +public: + virtual ~OtaStore() {} + // Prepare staging for a container of `total_size` bytes (erases/clears). false if it won't fit. + virtual bool begin(uint32_t total_size) = 0; + virtual bool write(uint32_t offset, const uint8_t* data, uint32_t len) = 0; + virtual bool read(uint32_t offset, uint8_t* buf, uint32_t len) const = 0; + virtual uint32_t capacity() const = 0; + virtual uint32_t staged_size() const = 0; // total_size from begin(), 0 if none + virtual void clear() = 0; + + // Optional: declare the size of the leading metadata (header + manifest + merkle leaves, i.e. + // everything before the payload). A flash-backed store keeps that region — which is updated + // throughout the transfer (a leaf is committed per block) — pinned in one RAM page, so it can + // flush the bulk payload page-by-page without re-erasing the leaves' page on every block. + // Returns false if the metadata won't fit the store's pinned region (transfer is then refused). + virtual bool set_meta_size(uint32_t meta_bytes) { (void)meta_bytes; return true; } + + // Optional: commit any RAM-buffered data to persistent storage. Called once when the transfer + // reaches COMPLETE (radio idle), so a flash store does its page writes off the RX critical path. + // After this returns, a flash store's data() view is coherent. No-op for purely in-RAM stores. + virtual void finalize() {} + + // Optional: persist in-progress state (the metadata/leaf-progress page + any open payload buffer) so a + // reboot mid-transfer can resume. Called by OtaManager every OTA_CHECKPOINT_BLOCKS committed blocks. + // A flash store flushes its pinned meta page + the open payload page (consistency: payload-before-leaves) + // so every block whose leaf is persisted also has its payload in flash. No-op for RAM stores. Infrequent + // (at LoRa block rates, ~once per many minutes) so the extra erases don't matter. + virtual void checkpoint() {} + + // Optional: re-attach to a container ALREADY persisted in the backing store (after a reboot), WITHOUT + // erasing. Returns true if a syntactically valid container (MOTA_MAGIC header + plausible total) is + // present and the store is now set up to read/continue-writing it; false if none (caller starts fresh). + // OtaManager then reads + parses the stored manifest to recompute geometry and resume the fetch. + virtual bool reopen() { return false; } + + // Optional: declare the container's logical layout once the manifest is parsed, BEFORE begin(), so a + // store backed by a single spare A/B partition (ESP32) can choose placement and reject an unfittable + // fetch up front. A FULL image's payload IS the final firmware (no decode), so it can stream straight + // to the inactive slot's offset 0 while the small meta/leaves/trailer persist elsewhere; a delta's + // whole container is staged together (the decoder reads the patch from it at apply). image_size is the + // reconstructed image; [payload_off, payload_off+payload_size) is the payload region in the container. + // Return false if it cannot fit the backing store (the transfer is then refused before any block). + virtual bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) { + (void)is_full; (void)image_size; (void)payload_off; (void)payload_size; return true; + } +}; + +// Fixed-capacity RAM store — for native tests and device bring-up of the transfer/verify path. +// (Does NOT survive reboot; a persistent flash store replaces it for production — see D1.) +template +class OtaStoreRam : public OtaStore { + uint8_t _buf[CAP] = {}; // zero-init so a never-written store's reopen() finds no MOTA_MAGIC + uint32_t _total = 0; +public: + bool begin(uint32_t total_size) override { + if (total_size > CAP) return false; + _total = total_size; + memset(_buf, 0xFF, total_size); // mimic erased flash (so unfilled leaf slots read as 'missing') + return true; + } + bool write(uint32_t off, const uint8_t* d, uint32_t len) override { + if ((uint64_t)off + len > _total) return false; + memcpy(_buf + off, d, len); + return true; + } + bool read(uint32_t off, uint8_t* b, uint32_t len) const override { + if ((uint64_t)off + len > _total) return false; + memcpy(b, _buf + off, len); + return true; + } + uint32_t capacity() const override { return CAP; } + uint32_t staged_size() const override { return _total; } + void clear() override { _total = 0; } + // RAM doesn't survive a real reboot, but the buffer persists within a process — enough to exercise the + // manager's resume path in native tests. Recover `total` from the stored header so read() bounds work. + bool reopen() override { + if (memcmp(_buf, MOTA_MAGIC, 4) != 0) return false; + uint32_t t = (uint32_t)_buf[4] | ((uint32_t)_buf[5] << 8) | ((uint32_t)_buf[6] << 16) | ((uint32_t)_buf[7] << 24); + if (t < 13 || t > CAP) return false; + _total = t; + return true; + } + const uint8_t* data() const { return _buf; } // contiguous view (RAM store only) +}; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaStoreFlashEsp32.cpp b/src/helpers/ota/OtaStoreFlashEsp32.cpp new file mode 100644 index 0000000000..9afcb80315 --- /dev/null +++ b/src/helpers/ota/OtaStoreFlashEsp32.cpp @@ -0,0 +1,243 @@ +#include "OtaStoreFlashEsp32.h" + +#if defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) + +#include "OtaDebug.h" +#include "OtaByteIO.h" // align_up / align_down (flash-sector geometry) +#include "MotaContainer.h" // mota_parse_manifest (reopen: rebuild geometry from the staged manifest) +#include +#include // malloc/free (the meta buffer is sized per fetch) +#include "esp_ota_ops.h" // esp_ota_get_next_update_partition (the inactive A/B slot) + +namespace mesh { +namespace ota { + +OtaStoreFlashEsp32::~OtaStoreFlashEsp32() { free(_meta); } + +bool OtaStoreFlashEsp32::acquire() { + if (!_part) { + _part = esp_ota_get_next_update_partition(nullptr); + _psize = _part ? _part->size : 0; + } + return _part != nullptr; +} + +// Compute the slot placement from _full/_image_size/_meta_bytes/_pay_size (already set). Returns false if +// it won't fit. Shared by plan_layout (fresh fetch) and reopen (resume) so both derive identical geometry. +bool OtaStoreFlashEsp32::layout() { + uint32_t total = _meta_bytes + _pay_size + 5; + if (_full) { + // payload streams to slot offset 0 (it IS the image); header+manifest+leaves+trailer persist at the + // bottom so the container survives a reboot (resume / re-serve). + _meta_span = _meta_bytes; // routing boundary: [0,meta) -> RAM meta buffer + _meta_flush = align_up(_meta_bytes + 5, SEC); // meta + 5-byte trailer, whole sectors + if (_meta_flush > OTA_ESP32_META_CAP) return false; + uint32_t bottom = align_down(_psize - _meta_flush, SEC); + _meta_part = bottom; + _pay_log0 = _meta_bytes; _pay_part0 = 0; + _write_start = 0; + if (_image_size > bottom) return false; // image would overrun the bottom meta region + } else { + // whole container staged bottom-aligned; the decoded image fills the slot from offset 0. + _meta_span = align_up(_meta_bytes, SEC); // pin whole sectors covering meta (+ spillover payload) + _meta_flush = _meta_span; + if (_meta_flush > OTA_ESP32_META_CAP) return false; + if (total > _psize) return false; + _write_start = align_down(_psize - total, SEC); + _meta_part = _write_start; + _pay_log0 = _meta_span; _pay_part0 = _write_start + _meta_span; + if (_image_size > _write_start) return false; // decoded output would overlap the staged container + } + _total = total; + return true; +} + +// Choose placement from the parsed manifest and refuse anything that won't fit, BEFORE any block is +// staged. (See the header for the delta-vs-full layout rationale.) +bool OtaStoreFlashEsp32::plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) { + if (!acquire()) return false; + _full = is_full; _image_size = image_size; _meta_bytes = payload_off; _pay_size = payload_size; + bool ok = layout(); + OTA_DBG("OTA esp32: plan %s total=%u image=%u meta=%u write_start=%u meta_part=%u ok=%d\n", + is_full ? "FULL" : "DELTA", (unsigned)_total, (unsigned)image_size, (unsigned)_meta_bytes, + (unsigned)_write_start, (unsigned)_meta_part, (int)ok); + return ok; +} + +bool OtaStoreFlashEsp32::begin(uint32_t total_size) { + if (!_part || _total == 0 || total_size != _total) return false; // plan_layout must have run + agreed + free(_meta); + _meta = (uint8_t*)malloc(_meta_flush); // header+manifest+leaves(+full trailer) + if (!_meta) { _total = 0; return false; } + memset(_meta, 0xFF, _meta_flush); + memset(_trailer, 0xFF, sizeof(_trailer)); + _pay_open = false; _pay_sec = 0; _pay_max_sec = 0; _flushed = false; + _io_ok = true; + return true; +} + +void OtaStoreFlashEsp32::clear() { + free(_meta); _meta = nullptr; + _total = 0; _pay_open = false; _flushed = false; // _part kept (re-acquire is fine) +} + +uint8_t* OtaStoreFlashEsp32::meta_slot(uint32_t L) { + if (in_trailer(L)) return _full ? (_meta + _meta_bytes + (L - (_total - 5))) + : (_trailer + (L - (_total - 5))); + if (L < _meta_span) return _meta + L; + return nullptr; // payload -> sliding sector / flash +} +const uint8_t* OtaStoreFlashEsp32::meta_slot_c(uint32_t L) const { + if (in_trailer(L)) return _full ? (_meta + _meta_bytes + (L - (_total - 5))) + : (_trailer + (L - (_total - 5))); + if (L < _meta_span) return _meta + L; + return nullptr; +} + +// Bytes from `pos` that stay in one region, and (for payload) one flash sector. +uint32_t OtaStoreFlashEsp32::run(uint32_t pos, uint32_t remain) const { + if (pos >= _total - 5) return remain; // trailer (<=5, one buffer) + if (pos < _meta_span) { uint32_t c = _meta_span - pos; return remain < c ? remain : c; } + uint32_t poff = pay_part(pos); + uint32_t to_sec = SEC - (poff % SEC); + uint32_t to_end = (_total - 5) - pos; // don't cross into the trailer + uint32_t c = remain; + if (to_sec < c) c = to_sec; + if (to_end < c) c = to_end; + return c; +} + +void OtaStoreFlashEsp32::flush_sector(uint32_t slot_off, const uint8_t* buf, uint32_t n) { + if (!_io_ok) return; + if (esp_partition_erase_range(_part, slot_off, n) != ESP_OK) { _io_ok = false; return; } + if (esp_partition_write(_part, slot_off, buf, n) != ESP_OK) { _io_ok = false; return; } + OTA_DBG("OTA esp32: flushed %u B @ slot+%u\n", (unsigned)n, (unsigned)slot_off); +} + +void OtaStoreFlashEsp32::flush_pay() { + if (_pay_open) { flush_sector((uint32_t)_pay_sec * SEC, _pay, SEC); _pay_open = false; } +} + +void OtaStoreFlashEsp32::open_pay(uint32_t sec) { + if (_pay_open) flush_pay(); + if (sec < _pay_max_sec) { + // revisiting an already-flushed sector (out-of-order block) -> read it back so the gaps we don't + // touch are preserved (they were programmed as 0xFF or earlier block data); we erase+reprogram on flush. + if (esp_partition_read(_part, (size_t)sec * SEC, _pay, SEC) != ESP_OK) _io_ok = false; + } else { + memset(_pay, 0xFF, SEC); // fresh sector + _pay_max_sec = sec; + } + _pay_sec = sec; _pay_open = true; +} + +bool OtaStoreFlashEsp32::write(uint32_t offset, const uint8_t* d, uint32_t len) { + if ((uint64_t)offset + len > _total || !_io_ok) return false; + for (uint32_t pos = offset, end = offset + len; pos < end; ) { + uint32_t n = run(pos, end - pos); + if (uint8_t* dst = meta_slot(pos)) { // meta / leaves / trailer -> pinned RAM + memcpy(dst, d, n); + } else { // payload -> sliding sector + uint32_t poff = pay_part(pos); + uint32_t sec = poff / SEC; + if (!_pay_open || sec != _pay_sec) open_pay(sec); + memcpy(_pay + (poff % SEC), d, n); + } + pos += n; d += n; + if (!_io_ok) return false; + } + return true; +} + +bool OtaStoreFlashEsp32::read(uint32_t offset, uint8_t* buf, uint32_t len) const { + if ((uint64_t)offset + len > _total) return false; + for (uint32_t pos = offset, end = offset + len; pos < end; ) { + uint32_t n = run(pos, end - pos); + if (const uint8_t* src = meta_slot_c(pos)) { + memcpy(buf, src, n); + } else { + uint32_t poff = pay_part(pos); + if (_pay_open && poff / SEC == _pay_sec) memcpy(buf, _pay + (poff % SEC), n); // still in RAM + else if (esp_partition_read(_part, poff, buf, n) != ESP_OK) return false; // flushed -> flash + } + pos += n; buf += n; + } + return true; +} + +void OtaStoreFlashEsp32::finalize() { + if (_flushed || _total == 0) return; + if (!_full) { + // delta: drop the 5-byte trailer into the sliding payload sector(s) so it flushes with them (the + // trailer sits right after the payload; this also covers the rare case it spills into a fresh sector). + uint32_t tpoff = _write_start + (_total - 5); + for (uint32_t off = 0; off < 5; ) { + uint32_t sec = (tpoff + off) / SEC; + if (!_pay_open || sec != _pay_sec) open_pay(sec); + uint32_t in = SEC - ((tpoff + off) % SEC); if (in > 5 - off) in = 5 - off; + memcpy(_pay + ((tpoff + off) % SEC), _trailer + off, in); + off += in; + } + } + flush_pay(); // last payload sector(s) (+ delta trailer) + flush_sector(_meta_part, _meta, _meta_flush); // meta (+ trailer for full) + _flushed = true; + OTA_DBG("OTA esp32: finalize %s io_ok=%d\n", _full ? "FULL" : "DELTA", (int)_io_ok); +} + +// Persist mid-transfer progress so a reboot can resume. Flush the open payload sector (KEEP it buffered so +// continued in-order writes aren't lost), then the meta region (leaves). Payload-before-leaves keeps it +// consistent: every block whose leaf is now in flash also has its payload in flash. Infrequent. +void OtaStoreFlashEsp32::checkpoint() { + if (_total == 0 || _flushed || !_io_ok) return; + if (_pay_open) flush_sector((uint32_t)_pay_sec * SEC, _pay, SEC); // flush but leave _pay/_pay_open intact + flush_sector(_meta_part, _meta, _meta_flush); // header + manifest + leaves (+full trailer) +} + +// Re-attach to a container already staged in the slot (after a reboot), WITHOUT erasing. The header +// (MOTA_MAGIC + total) sits at a sector boundary (meta_part for full, write_start for delta — both +// sector-aligned), so scan sector starts from the bottom up. A candidate is accepted only if: magic + +// plausible total, the manifest parses, the container geometry is self-consistent with the total, AND the +// recomputed placement lands the meta exactly where we found the magic. Any miss -> false (fetch fresh), +// so a stray/stale match can only cost a restart, never a corrupt adopt. +bool OtaStoreFlashEsp32::reopen() { + if (!acquire() || _psize < SEC) return false; + uint8_t hb[8]; + // the meta/container is staged at the BOTTOM of the slot, so scan upward from there; cap the scan (a + // miss just means "fetch fresh"). 128 sectors (512 KB) covers any full image's meta + typical deltas. + uint32_t scanned = 0; + for (uint32_t o = align_down(_psize - SEC, SEC); ; o -= SEC) { + if (esp_partition_read(_part, o, hb, 8) == ESP_OK && memcmp(hb, MOTA_MAGIC, 4) == 0) { + uint32_t total = rd_u32le(hb + 4); + if (total >= 13 && total <= _psize) { + uint8_t mbuf[256]; uint32_t mread = total - 8; if (mread > sizeof(mbuf)) mread = sizeof(mbuf); + MotaManifest m; + if (esp_partition_read(_part, o + 8, mbuf, mread) == ESP_OK && mota_parse_manifest(mbuf, mread, m)) { + uint32_t mfl = (uint32_t)(m.approval - m.manifest_start) + 4; + uint32_t payload_off = 8 + mfl + m.block_count * 4; + if ((uint64_t)payload_off + m.payload_size + 5 == total) { + _full = m.is_full(); _image_size = m.image_size; _meta_bytes = payload_off; _pay_size = m.payload_size; + if (layout() && _meta_part == o) { // geometry agrees AND magic is where we'd place meta + free(_meta); _meta = (uint8_t*)malloc(_meta_flush); + if (!_meta) { _total = 0; return false; } + if (esp_partition_read(_part, _meta_part, _meta, _meta_flush) != ESP_OK) { + free(_meta); _meta = nullptr; _total = 0; return false; } + memset(_trailer, 0xFF, sizeof(_trailer)); // delta trailer (re-written at finalize); full reads it from _meta + _pay_open = false; _pay_sec = 0; _flushed = false; _io_ok = true; + _pay_max_sec = (_pay_part0 + _pay_size + SEC) / SEC; // treat all payload sectors as seen -> RMW preserves committed blocks + OTA_DBG("OTA esp32: reopen %s total=%u meta_part=%u\n", _full ? "FULL" : "DELTA", (unsigned)total, (unsigned)o); + return true; + } + } + } + } + } + if (o == 0 || ++scanned >= 128) break; + } + return false; +} + +} // namespace ota +} // namespace mesh + +#endif diff --git a/src/helpers/ota/OtaStoreFlashEsp32.h b/src/helpers/ota/OtaStoreFlashEsp32.h new file mode 100644 index 0000000000..f0eee558ff --- /dev/null +++ b/src/helpers/ota/OtaStoreFlashEsp32.h @@ -0,0 +1,116 @@ +#pragma once + +#if defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE) + +#include "OtaStore.h" +#include "esp_partition.h" + +// Persistent flash-backed OtaStore for ESP32 (A/B). Stages the received `.mota` in the INACTIVE OTA +// slot, so a delta/full of any size is bounded to O(one sector) of RAM, never O(mota) -- lifting the old +// RAM store's ~16 KB ceiling (a full 1 MB+ image now fetches over the air). Placement is chosen from the +// parsed manifest (plan_layout), keyed only on full-vs-delta: +// +// - DELTA (codec sequential/in-place): the whole container is staged bottom-aligned in the slot. At +// apply the decoder reads the patch from it -- sequential: base from the running slot -> output to +// the inactive slot from offset 0; in-place: copy running->slot then patch in place. The decoded +// image fills the slot from offset 0, so plan_layout refuses unless `image_size + container` fits +// (the output must never reach the bottom-staged container; the fit makes them disjoint). +// +// - FULL (codec full): the payload IS the final image (no decode), so it streams straight to slot +// offset 0 while the small header+manifest+merkle-leaves and the trailer persist at the bottom of the +// slot (so the whole container survives a reboot -- for fetch-resume and for re-serving to other +// nodes). One default for every A/B full payload; plan_layout refuses unless `image_size + meta` +// fits, and if that doesn't fit nothing would. +// +// RX-safety: an ESP32 flash erase+write disables the XIP instruction cache and stalls code running from +// flash (the dispatcher loop), exactly like the nRF52 page erase that starved the LoRa RX. So writes are +// COALESCED to the 4 KB sector and each sector is programmed once. The meta region (header+manifest+ +// leaves -- written all transfer long as blocks/leaves arrive, often out of order) is PINNED in RAM and +// flushed at finalize() with the radio idle; the bulk payload streams through ONE sliding sector buffer, +// flushing the sector it leaves behind (~1 flush per 4 KB, off the per-packet path). OTA is also the +// lowest-priority TX, so a brief stall yields to real traffic. A small delta whose container fits the +// pinned meta region does ZERO flash I/O until COMPLETE. + +namespace mesh { +namespace ota { + +#ifndef OTA_ESP32_META_CAP +#define OTA_ESP32_META_CAP 65536 // max heap for header+manifest+merkle leaves (4 B/block) -> ~16k blocks +#endif // (a 1.27 MB full image at 128 B LoRa blocks ~= 40 KB of leaves) + +class OtaStoreFlashEsp32 : public OtaStore { + static const uint32_t SEC = 4096; // ESP32 NOR flash erase unit + + const esp_partition_t* _part = nullptr; // inactive OTA slot (acquired in plan_layout/begin) + uint32_t _psize = 0; // slot size + + // container geometry (from plan_layout / handleManifest) + uint32_t _total = 0; // container size (0 = none staged) + uint32_t _meta_bytes = 0; // header+manifest+leaves (== payload offset in the container) + uint32_t _pay_size = 0; // payload bytes + uint32_t _image_size = 0; // reconstructed image (delta) / == _pay_size (full) + bool _full = false; + + // logical->partition placement (see header doc). For delta everything is contiguous at _write_start; + // for full the payload lands at slot 0 and meta+trailer at the bottom. + uint32_t _write_start = 0; // delta: container offset 0 in the slot (sector-aligned) + uint32_t _meta_span = 0; // container bytes held in the RAM meta buffer (whole sectors) + uint32_t _meta_part = 0; // slot offset the meta buffer flushes to + uint32_t _pay_log0 = 0; // first container offset that streams to the payload region + uint32_t _pay_part0 = 0; // slot offset of that first payload byte + uint32_t _trailer_part = 0; // slot offset of the 5-byte trailer + + // RX-safe staging buffers + uint8_t* _meta = nullptr; // heap, sized per fetch: header+manifest+leaves(+full trailer) + uint8_t _pay[SEC]; // one sliding payload sector (slot-sector aligned) + uint32_t _pay_sec = 0; // slot sector index currently in _pay (0 = none open) + uint8_t _trailer[5]; + uint32_t _meta_flush = 0; // whole-sector byte count to program for the meta buffer + uint32_t _pay_max_sec = 0; // highest payload slot-sector opened (out-of-order detection) + bool _pay_open = false; // a sliding sector is currently buffered in _pay + bool _flushed = false; + bool _io_ok = true; // cleared if any flash erase/write/read fails + + bool acquire(); // resolve the inactive slot (idempotent) + bool layout(); // compute placement from _full/_image_size/_meta_bytes/_pay_size + uint32_t pay_part(uint32_t L) const { return _pay_part0 + (L - _pay_log0); } // payload slot offset + bool in_trailer(uint32_t L) const { return L >= _total - 5; } + uint32_t run(uint32_t pos, uint32_t remain) const; // bytes from `pos` that stay in one region+sector + uint8_t* meta_slot(uint32_t L); // RAM home of a meta/trailer byte (nullptr if it's payload) + const uint8_t* meta_slot_c(uint32_t L) const; + void open_pay(uint32_t sec); // make `sec` the buffered sliding sector (RMW if revisited) + void flush_pay(); // erase+write the open sliding sector + void flush_sector(uint32_t slot_off, const uint8_t* buf, uint32_t n); // erase+program sector(s) + +public: + ~OtaStoreFlashEsp32() override; + bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) override; + bool begin(uint32_t total_size) override; + bool write(uint32_t offset, const uint8_t* data, uint32_t len) override; + bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override; + uint32_t capacity() const override { return _psize; } // loose bound; plan_layout does the real check + uint32_t staged_size() const override { return _total; } + void clear() override; + bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes < OTA_ESP32_META_CAP; } + void finalize() override; + void checkpoint() override; // persist meta(leaves) + open payload sector so a reboot can resume + bool reopen() override; // re-attach to a container already staged in the slot (scan + rebuild geometry) + + // No contiguous RAM/mmap view: the staged container lives in the slot (split for FULL). The ESP32 + // apply path reads what it needs via read()/esp_partition_read instead of a data() pointer, so this + // returns nullptr (kept for interface parity with the nRF52 store, whose flash is memory-mapped). + const uint8_t* data() const { return nullptr; } + + // Apply-path accessors: where the staged container physically lives in the inactive slot. + const esp_partition_t* partition() const { return _part; } + uint32_t write_start() const { return _write_start; } // delta: container offset 0 in the slot + bool is_full() const { return _full; } + uint32_t image_size() const { return _image_size; } + uint32_t meta_bytes() const { return _meta_bytes; } + uint32_t payload_slot_off(uint32_t k) const { return _pay_part0 + k; } // slot off of payload byte k +}; + +} // namespace ota +} // namespace mesh + +#endif diff --git a/src/helpers/ota/OtaStoreFlashNrf52.cpp b/src/helpers/ota/OtaStoreFlashNrf52.cpp new file mode 100644 index 0000000000..51979e2a60 --- /dev/null +++ b/src/helpers/ota/OtaStoreFlashNrf52.cpp @@ -0,0 +1,159 @@ +#include "OtaStoreFlashNrf52.h" + +#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) + +#include "OtaSelf.h" +#include "OtaDebug.h" +#include "OtaByteIO.h" // align_down / rd_u32le (flash-page geometry + header read) +#include +#include "flash/flash_nrf5x.h" // Adafruit core internal-flash driver (SoftDevice-safe; LittleFS path) + +namespace mesh { +namespace ota { + +// Write one whole 4 KB page from `buf` to flash (erase + program, ~85 ms). `buf` is PG bytes, 0xFF-padded +// past the container, so the program is clean. The last container page ends exactly at FS_START (both +// FS_START and _write_start are page-aligned and the container ends <= FS_START), so a full-page write +// never reaches into ExtraFS. +void OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) { + uint32_t addr = _write_start + page_idx * PG; + if (addr + PG > MOTA_NRF52_FS_START) return; // defensive: never cross the staging ceiling + OTA_DBG("OTA flash: write page %u @ %08x\n", (unsigned)page_idx, (unsigned)addr); + flash_nrf5x_write(addr, buf, PG); + flash_nrf5x_flush(); +} + +void OtaStoreFlashNrf52::flush_pay() { + if (_pay_idx != 0) flush_page(_pay_idx, _pay_page); // _pay_idx 0 == no payload page open +} + +uint32_t OtaStoreFlashNrf52::run(uint32_t pos, uint32_t remain) const { + uint32_t trailer = _total - 5; // container is always >= 13 bytes (begin checks) + if (pos >= trailer) return remain; // tail: caller bounds remain to <= 5 already + uint32_t end = pos + remain; + uint32_t page_end = (pos / PG + 1) * PG; + if (end > page_end) end = page_end; // a run stays within one flash page, + if (end > trailer) end = trailer; // and never crosses into the trailer tail + return end - pos; +} + +const uint8_t* OtaStoreFlashNrf52::read_slot(uint32_t pos) const { + if (pos >= _total - 5) return _trailer + (pos - (_total - 5)); // trailer tail (RAM until finalize) + uint32_t page = pos / PG; + if (page == 0) return _meta_page + pos; // pinned page 0 (incl. leaves) + if (page == _pay_idx) return _pay_page + (pos - page * PG); // current sliding payload page + return (const uint8_t*)(uintptr_t)(_write_start + pos); // already flushed -> memory-mapped +} + +uint8_t* OtaStoreFlashNrf52::write_slot(uint32_t pos) { + if (pos >= _total - 5) return _trailer + (pos - (_total - 5)); + uint32_t page = pos / PG; + if (page == 0) return _meta_page + pos; + if (page > _pay_idx) { flush_pay(); _pay_idx = page; memset(_pay_page, 0xFF, PG); } // advance, fresh page + if (page == _pay_idx) return _pay_page + (pos - page * PG); + return nullptr; // page < _pay_idx: already flushed +} + +bool OtaStoreFlashNrf52::begin(uint32_t total_size) { + clear(); + if (total_size < 13 || total_size > capacity()) return false; // 13 = header(8) + trailer(5) + + // bottom-align against FS_START so the trailer ends exactly at FS_START (bootloader scans for it) + uint32_t start = align_down(MOTA_NRF52_FS_START - total_size, PG); + + // never collide with the running application image (its extent comes from its EndF trailer) + uint32_t app_end = MOTA_NRF52_APP_BASE; + SelfFwInfo fi; + if (ota_self_firmware(fi) && fi.valid) app_end = MOTA_NRF52_APP_BASE + fi.image_len; + if (start < app_end) return false; + + _write_start = start; + _total = total_size; + memset(_meta_page, 0xFF, PG); // assemble page 0 in RAM; 0xFF = erased sentinel (unfilled leaf slots) + memset(_trailer, 0xFF, sizeof(_trailer)); + _pay_idx = 0; + _flushed = false; + OTA_DBG("OTA flash: begin total=%u start=%08x app_end=%08x\n", + (unsigned)total_size, (unsigned)start, (unsigned)app_end); + return true; // no pre-erase: each page is erased by its own (single) flush +} + +bool OtaStoreFlashNrf52::write(uint32_t offset, const uint8_t* d, uint32_t len) { + if ((uint64_t)offset + len > _total) return false; + for (uint32_t pos = offset, end = offset + len; pos < end; ) { + uint32_t n = run(pos, end - pos); + if (uint8_t* dst = write_slot(pos)) { + memcpy(dst, d, n); + } else { + // out-of-order write to an already-flushed page: read-modify-write straight to flash. Safe -- the + // driver erases the page before programming, so re-touching it never breaks writes-per-word. + OTA_DBG("OTA flash: RMW page %u (out-of-order) @ off %u\n", (unsigned)(pos / PG), (unsigned)pos); + if (flash_nrf5x_write(_write_start + pos, d, n) < 0) return false; + flash_nrf5x_flush(); + } + pos += n; d += n; + } + return true; +} + +bool OtaStoreFlashNrf52::read(uint32_t offset, uint8_t* buf, uint32_t len) const { + if ((uint64_t)offset + len > _total) return false; + for (uint32_t pos = offset, end = offset + len; pos < end; ) { + uint32_t n = run(pos, end - pos); + memcpy(buf, read_slot(pos), n); + pos += n; buf += n; + } + return true; +} + +void OtaStoreFlashNrf52::finalize() { + if (_flushed || _total == 0) return; + OTA_DBG("OTA flash: finalize total=%u\n", (unsigned)_total); + flush_pay(); // the last (highest) payload page, if one is open + flush_page(0, _meta_page); // page 0: header + manifest + leaves + first payload bytes + flash_nrf5x_write(_write_start + _total - 5, _trailer, 5); // trailer tail (radio idle at COMPLETE) + flash_nrf5x_flush(); + _flushed = true; +} + +// Persist mid-transfer progress so a reboot can resume. Order matters for consistency: flush the open +// payload page FIRST, then page 0 (the leaf-progress markers) -- so every block whose leaf is now in flash +// also has its payload in flash. Infrequent (every OTA_CHECKPOINT_BLOCKS blocks), so the 2 extra page +// erases don't matter; at LoRa block rates it's roughly once per many minutes. +void OtaStoreFlashNrf52::checkpoint() { + if (_total == 0 || _flushed) return; + flush_pay(); // keep _pay_idx open (it may still receive writes); just re-flush its bytes + flush_page(0, _meta_page); // header + manifest + leaves accumulated so far +} + +// Re-attach to a container already staged in flash (after a reboot), without erasing. The container is +// bottom-aligned (begin: start = (FS_START - total) & ~(PG-1)) and flash is memory-mapped, so scan page +// starts from just below FS_START down to the app end for MOTA_MAGIC with a self-consistent total; adopt +// the first match (highest address = most recent for the common single-container case). The manager then +// parses the loaded manifest and validates geometry/root, so a stale leftover is rejected there. +bool OtaStoreFlashNrf52::reopen() { + uint32_t app_end = MOTA_NRF52_APP_BASE; + SelfFwInfo fi; + if (ota_self_firmware(fi) && fi.valid) app_end = MOTA_NRF52_APP_BASE + fi.image_len; + for (uint32_t start = align_down(MOTA_NRF52_FS_START - PG, PG); start >= app_end; start -= PG) { + const uint8_t* p = (const uint8_t*)(uintptr_t)start; + if (memcmp(p, MOTA_MAGIC, 4) != 0) continue; + uint32_t total = rd_u32le(p + 4); + if (total < 13 || total > capacity()) continue; + if (align_down(MOTA_NRF52_FS_START - total, PG) != start) continue; // must match begin()'s placement + _write_start = start; + _total = total; + memcpy(_meta_page, p, PG); // load page 0 (header+manifest+leaves) into RAM to continue + memcpy(_trailer, p + (total - 5), 5); // recover the trailer tail (flushed at last finalize, if any) + _pay_idx = 0; + _flushed = false; + OTA_DBG("OTA flash: reopen total=%u start=%08x\n", (unsigned)total, (unsigned)start); + return true; + } + return false; +} + +} // namespace ota +} // namespace mesh + +#endif diff --git a/src/helpers/ota/OtaStoreFlashNrf52.h b/src/helpers/ota/OtaStoreFlashNrf52.h new file mode 100644 index 0000000000..37d64ceff3 --- /dev/null +++ b/src/helpers/ota/OtaStoreFlashNrf52.h @@ -0,0 +1,77 @@ +#pragma once + +#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) + +#include "OtaStore.h" +#include "OtaFlashLayout_nrf52.h" + +// Persistent flash-backed OtaStore for nRF52 (RAK4631). Stages the received `.mota` in the free flash +// below the primary LittleFS (FS_START), bottom-aligned so its trailer ends at FS_START and the +// bootloader can scan for it. Survives reboot — the whole point — so the bootloader can apply the +// staged delta on the next boot. +// +// RAM is bounded to O(one flash page), NEVER O(mota): a 100 KB+ delta must not live in RAM. +// - On nRF52 the flash *erase* unit is one 4 KB page and the only SoftDevice-safe writer +// (Adafruit `flash_nrf5x`) erases the whole page on every flush (~85 ms, CPU stalled → LoRa RX +// starved). Writing to flash per received packet therefore drops in-flight DATA and the transfer +// stalls. The fix: coalesce to the *page*, the hardware-natural unit, and write each page once. +// - `_meta_page` pins flash page 0 (header + manifest + the merkle-leaf progress markers, which are +// written one-per-block all transfer long). Keeping it in RAM means streaming the payload never +// re-erases the leaves' page. Flushed once at finalize(). Requires metadata <= one page +// (set_meta_size enforces it; true for <= ~979 blocks, i.e. any realistic MeshCore image). +// - `_pay_page` is a single sliding buffer for one payload page (index >= 1). It advances +// monotonically with the (mostly in-order) block stream and flushes the page it leaves behind. +// Rare out-of-order writes to an already-flushed page go straight to flash as a safe read-modify- +// write (flash_nrf5x erases before programming, so re-touching a page never violates the +// writes-per-word limit — it just costs one extra erase). +// - The 5-byte trailer is buffered and written at finalize(). +// Net: flash is touched ~once per 4 KB page (≈ 1 per 4 blocks at 1 KB), off the per-packet path; page +// 0 and the last page are written at finalize() with the radio idle. For a small delta (whole .mota +// in page 0) there is ZERO flash I/O during the transfer. + +namespace mesh { +namespace ota { + +class OtaStoreFlashNrf52 : public OtaStore { + static const uint32_t PG = MOTA_NRF52_FLASH_PAGE; // 4096 + + uint32_t _write_start = 0; // flash address of container offset 0 (page-aligned) + uint32_t _total = 0; // staged container size (0 = none) + bool _flushed = false; // finalize() committed everything to flash + + uint8_t _meta_page[PG]; // pinned flash page 0 (header + manifest + leaves + 1st payload) + uint8_t _pay_page[PG]; // sliding buffer for one payload page (index _pay_idx) + uint32_t _pay_idx = 0; // page index currently held in _pay_page (0 = none open; pages >= 1) + uint8_t _trailer[5]; // last 5 container bytes (kept in RAM, written at finalize) + + // Bytes from `pos` that stay in one store region (a single flash page, or the trailer tail). + uint32_t run(uint32_t pos, uint32_t remain) const; + // RAM home of byte `pos`: read_slot always resolves (flushed pages → memory-mapped flash); write_slot + // opens/advances the sliding payload page and returns nullptr if `pos` is in an already-flushed page. + const uint8_t* read_slot(uint32_t pos) const; + uint8_t* write_slot(uint32_t pos); + void flush_pay(); // commit _pay_page to flash (erase + program, one page) + void flush_page(uint32_t page_idx, const uint8_t* buf); // write a full page to flash + +public: + bool begin(uint32_t total_size) override; + bool write(uint32_t offset, const uint8_t* data, uint32_t len) override; + bool read(uint32_t offset, uint8_t* buf, uint32_t len) const override; + uint32_t capacity() const override { return MOTA_NRF52_FS_START - MOTA_NRF52_APP_BASE; } + uint32_t staged_size() const override { return _total; } + void clear() override { _total = 0; _pay_idx = 0; _flushed = false; } + bool set_meta_size(uint32_t meta_bytes) override { return meta_bytes <= PG; } // leaves must fit page 0 + void finalize() override; + void checkpoint() override; // persist page 0 (leaves) + the open payload page so a reboot can resume + bool reopen() override; // re-attach to a container already staged in flash (scan for it) + + // Contiguous view (flash is memory-mapped). VALID ONLY AFTER finalize() — before that, page 0 and the + // tail are still in RAM. OtaManager/OtaCli/verify use this only once the transfer is COMPLETE. + const uint8_t* data() const { return (const uint8_t*)(uintptr_t)_write_start; } + uint32_t write_start() const { return _write_start; } +}; + +} // namespace ota +} // namespace mesh + +#endif diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h new file mode 100644 index 0000000000..9fded575fe --- /dev/null +++ b/src/helpers/ota/OtaTargets.h @@ -0,0 +1,438 @@ +#pragma once +#include + +// AUTO-GENERATED by tools/mota/gen_targets.py — do not edit by hand. +// 418 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32) +// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT +// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes. + +namespace mesh { namespace ota { + +// target_id -> env name, or nullptr if unknown. Linear scan (table is small; lookups are rare). +inline const char* ota_target_env_name(uint32_t target_id) { + static const struct { uint32_t id; const char* env; } T[] = { + { 0x149bcb23, "Ebyte_EoRa-S3_companion_radio_ble" }, + { 0x30d4e442, "Ebyte_EoRa-S3_companion_radio_usb" }, + { 0x263a8d80, "Ebyte_EoRa-S3_kiss_modem" }, + { 0x9070ae2c, "Ebyte_EoRa-S3_Repeater" }, + { 0xf190d834, "Ebyte_EoRa-S3_room_server" }, + { 0x32890982, "Ebyte_EoRa-S3_terminal_chat" }, + { 0xacb599d5, "GAT562_30S_Mesh_Kit_companion_radio_ble" }, + { 0x82d3b273, "GAT562_30S_Mesh_Kit_companion_radio_usb" }, + { 0xe96a6af8, "GAT562_30S_Mesh_Kit_kiss_modem" }, + { 0x70d8478d, "GAT562_30S_Mesh_Kit_repeater" }, + { 0xf9986af5, "GAT562_30S_Mesh_Kit_room_server" }, + { 0x98519a1d, "GAT562_Mesh_EVB_Pro_kiss_modem" }, + { 0x06b71719, "GAT562_Mesh_EVB_Pro_repeater" }, + { 0x4c0461c8, "GAT562_Mesh_EVB_Pro_room_server" }, + { 0x4fa88992, "GAT562_Mesh_Tracker_Pro_companion_radio_ble" }, + { 0xcd6604d0, "GAT562_Mesh_Tracker_Pro_companion_radio_usb" }, + { 0xc54ea258, "GAT562_Mesh_Tracker_Pro_kiss_modem" }, + { 0x0b3985a8, "GAT562_Mesh_Tracker_Pro_repeater" }, + { 0x03d9704a, "GAT562_Mesh_Tracker_Pro_room_server" }, + { 0x8456f753, "GAT562_Mesh_Watch13_companion_radio_ble" }, + { 0xc8104197, "GAT562_Mesh_Watch13_kiss_modem" }, + { 0xf5eb61ec, "Generic_E22_kiss_modem" }, + { 0x70d2d590, "Generic_E22_sx1262_repeater" }, + { 0xe1a7ffdf, "Generic_E22_sx1262_repeater_bridge_espnow" }, + { 0xd5e04824, "Generic_E22_sx1268_repeater" }, + { 0x61a7b2dd, "Generic_E22_sx1268_repeater_bridge_espnow" }, + { 0xa416bb20, "Generic_ESPNOW_comp_radio_usb" }, + { 0x875f3744, "Generic_ESPNOW_repeatr" }, + { 0xdaf349be, "Generic_ESPNOW_room_svr" }, + { 0x93b93199, "Generic_ESPNOW_terminal_chat" }, + { 0x5293da13, "Heltec_ct62_companion_radio_ble" }, + { 0xf2eec98c, "Heltec_ct62_companion_radio_usb" }, + { 0x48a986f1, "Heltec_ct62_kiss_modem" }, + { 0xd2cf978c, "Heltec_ct62_repeater" }, + { 0x53626334, "Heltec_ct62_repeater_bridge_espnow" }, + { 0xefe4f19f, "Heltec_ct62_sensor" }, + { 0xfcaf3730, "Heltec_E213_companion_radio_ble" }, + { 0xd90deef7, "Heltec_E213_companion_radio_usb" }, + { 0x2d2a4089, "Heltec_E213_kiss_modem" }, + { 0x89ea5c4e, "Heltec_E213_repeater" }, + { 0xc0eb712a, "Heltec_E213_repeater_bridge_espnow" }, + { 0x67293f43, "Heltec_E213_room_server" }, + { 0x3f364910, "Heltec_E290_companion_ble" }, + { 0xac4b4eb5, "Heltec_E290_companion_usb" }, + { 0xe077fb8d, "Heltec_E290_kiss_modem" }, + { 0x2bbe7fd8, "Heltec_E290_repeater" }, + { 0x9e40157c, "Heltec_E290_repeater_bridge_espnow" }, + { 0xb12e6e01, "Heltec_E290_room_server" }, + { 0x32bcdd14, "Heltec_t114_companion_radio_ble" }, + { 0xfe937f14, "Heltec_t114_companion_radio_usb" }, + { 0x5b9b1933, "Heltec_t114_kiss_modem" }, + { 0x191f3545, "Heltec_t114_repeater" }, + { 0x1b4399de, "Heltec_t114_repeater_bridge_rs232" }, + { 0xb1153422, "Heltec_t114_room_server" }, + { 0x075ca3f9, "Heltec_t114_without_display_companion_radio_ble" }, + { 0x2f9f8e7d, "Heltec_t114_without_display_companion_radio_usb" }, + { 0xc9dc5d8c, "Heltec_t114_without_display_repeater" }, + { 0x34b0e5f3, "Heltec_t114_without_display_repeater_bridge_rs232" }, + { 0xcfa0e233, "Heltec_t114_without_display_room_server" }, + { 0x17d5e1ff, "Heltec_T190_companion_radio_ble_" }, + { 0xc99313c6, "Heltec_T190_companion_radio_usb_" }, + { 0x21a73973, "Heltec_T190_kiss_modem" }, + { 0x3067f656, "Heltec_T190_repeater_" }, + { 0xfdb25057, "Heltec_T190_repeater_bridge_espnow_" }, + { 0x0f1767cd, "Heltec_T190_room_server_" }, + { 0x73387372, "heltec_tracker_v2_companion_radio_ble" }, + { 0xe6eeffe1, "heltec_tracker_v2_companion_radio_usb" }, + { 0x35f4e7ac, "heltec_tracker_v2_companion_radio_wifi" }, + { 0xb62a1e75, "heltec_tracker_v2_kiss_modem" }, + { 0x9e64845d, "heltec_tracker_v2_repeater" }, + { 0x19232e8b, "heltec_tracker_v2_repeater_bridge_espnow" }, + { 0x38b948b4, "heltec_tracker_v2_room_server" }, + { 0xed21ca4f, "heltec_tracker_v2_sensor" }, + { 0x9f5f39a7, "heltec_tracker_v2_terminal_chat" }, + { 0x081d2219, "Heltec_v2_companion_radio_ble" }, + { 0xc283ad60, "Heltec_v2_companion_radio_usb" }, + { 0x4fbdc1e1, "Heltec_v2_companion_radio_wifi" }, + { 0xc797fd73, "Heltec_v2_kiss_modem" }, + { 0xab9871c8, "Heltec_v2_repeater" }, + { 0x5caaf9bf, "Heltec_v2_repeater_bridge_espnow" }, + { 0x2075687c, "Heltec_v2_room_server" }, + { 0x5d726067, "Heltec_v2_terminal_chat" }, + { 0x9969bbc9, "Heltec_v3_companion_radio_ble" }, + { 0x22f900de, "Heltec_v3_companion_radio_usb" }, + { 0xaca0e153, "Heltec_v3_companion_radio_wifi" }, + { 0xcb1c2e65, "Heltec_v3_kiss_modem" }, + { 0x9f2a6b84, "Heltec_v3_ota_test" }, + { 0xd1b29b18, "Heltec_v3_repeater" }, + { 0x644bb68d, "Heltec_v3_repeater_bridge_espnow" }, + { 0xd19a9759, "Heltec_v3_repeater_bridge_rs232" }, + { 0xc59d294e, "Heltec_v3_room_server" }, + { 0x21519537, "Heltec_v3_sensor" }, + { 0x382bb181, "Heltec_v3_terminal_chat" }, + { 0x01d8f124, "heltec_v4_companion_radio_ble" }, + { 0x1a4d0096, "heltec_v4_companion_radio_usb" }, + { 0x34240e36, "heltec_v4_companion_radio_wifi" }, + { 0xd522a14f, "heltec_v4_expansionkit_repeater" }, + { 0xf75feb3e, "heltec_v4_kiss_modem" }, + { 0xe792a051, "heltec_v4_repeater" }, + { 0x2d5ea842, "heltec_v4_repeater_bridge_espnow" }, + { 0xeed78ce4, "heltec_v4_room_server" }, + { 0x0b6847ed, "heltec_v4_sensor" }, + { 0xab3e1ad3, "heltec_v4_terminal_chat" }, + { 0x60fca2ae, "heltec_v4_tft_companion_radio_ble" }, + { 0xf6b40343, "heltec_v4_tft_companion_radio_usb" }, + { 0x76640bcc, "heltec_v4_tft_companion_radio_wifi" }, + { 0x87513d56, "heltec_v4_tft_repeater" }, + { 0xca812b41, "heltec_v4_tft_repeater_bridge_espnow" }, + { 0x18594231, "heltec_v4_tft_room_server" }, + { 0x188f3ac1, "heltec_v4_tft_sensor" }, + { 0x402055ec, "heltec_v4_tft_terminal_chat" }, + { 0x12393f1f, "Heltec_Wireless_Paper_companion_radio_ble" }, + { 0xed3ef5b8, "Heltec_Wireless_Paper_companion_radio_usb" }, + { 0x8ea86381, "Heltec_Wireless_Paper_kiss_modem" }, + { 0x09f57289, "Heltec_Wireless_Paper_repeater" }, + { 0x148aeaab, "Heltec_Wireless_Paper_repeater_bridge_espnow" }, + { 0x6e9f7fec, "Heltec_Wireless_Paper_room_server" }, + { 0x6a25d024, "Heltec_Wireless_Tracker_companion_radio_ble" }, + { 0xc4a11710, "Heltec_Wireless_Tracker_companion_radio_usb" }, + { 0x753947a4, "Heltec_Wireless_Tracker_kiss_modem" }, + { 0xe6fa98b4, "Heltec_Wireless_Tracker_repeater" }, + { 0xf8c27cf4, "Heltec_Wireless_Tracker_repeater_bridge_espnow" }, + { 0x90e227d9, "Heltec_Wireless_Tracker_room_server" }, + { 0x812f5e52, "Heltec_WSL3_companion_radio_ble" }, + { 0x1ddd15c8, "Heltec_WSL3_companion_radio_usb" }, + { 0x49d67d0d, "Heltec_WSL3_companion_radio_wifi" }, + { 0xbc003322, "Heltec_WSL3_repeater" }, + { 0x835b70c0, "Heltec_WSL3_repeater_bridge_espnow" }, + { 0xd7460575, "Heltec_WSL3_repeater_bridge_rs232" }, + { 0x33e2c171, "Heltec_WSL3_room_server" }, + { 0xa165ae99, "Heltec_WSL3_sensor" }, + { 0x04c5c2b7, "ikoka_handheld_nrf_e22_30dbm_096_companion_radio_ble" }, + { 0x6c1ca659, "ikoka_handheld_nrf_e22_30dbm_096_companion_radio_usb" }, + { 0xa295fb04, "ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_ble" }, + { 0x61c405ca, "ikoka_handheld_nrf_e22_30dbm_096_rotated_companion_radio_usb" }, + { 0xe893fe91, "ikoka_handheld_nrf_e22_30dbm_repeater" }, + { 0x2b599d31, "ikoka_handheld_nrf_e22_30dbm_room_server" }, + { 0xf1442d25, "ikoka_handheld_nrf_kiss_modem" }, + { 0x15082ede, "ikoka_nano_nrf_22dbm_companion_radio_ble" }, + { 0x6ff350d1, "ikoka_nano_nrf_22dbm_companion_radio_usb" }, + { 0xe03d0cd9, "ikoka_nano_nrf_22dbm_kiss_modem" }, + { 0x82e1eb2c, "ikoka_nano_nrf_22dbm_repeater" }, + { 0x0ee19b12, "ikoka_nano_nrf_22dbm_room_server" }, + { 0x6c67ed89, "ikoka_nano_nrf_30dbm_companion_radio_ble" }, + { 0xd1c05d0d, "ikoka_nano_nrf_30dbm_companion_radio_usb" }, + { 0xb90150b0, "ikoka_nano_nrf_30dbm_kiss_modem" }, + { 0x7285bdb3, "ikoka_nano_nrf_30dbm_repeater" }, + { 0x77cadc99, "ikoka_nano_nrf_30dbm_room_server" }, + { 0xc177e6cd, "ikoka_nano_nrf_33dbm_companion_radio_ble" }, + { 0xf7a38993, "ikoka_nano_nrf_33dbm_companion_radio_usb" }, + { 0x83762f33, "ikoka_nano_nrf_33dbm_kiss_modem" }, + { 0x86cadda9, "ikoka_nano_nrf_33dbm_repeater" }, + { 0x7e49da49, "ikoka_nano_nrf_33dbm_room_server" }, + { 0x305d6f92, "ikoka_stick_nrf_22dbm_companion_radio_ble" }, + { 0xbc6b870b, "ikoka_stick_nrf_22dbm_companion_radio_usb" }, + { 0x78845fdd, "ikoka_stick_nrf_22dbm_kiss_modem" }, + { 0x6d35f434, "ikoka_stick_nrf_22dbm_repeater" }, + { 0xa52d76a8, "ikoka_stick_nrf_22dbm_room_server" }, + { 0xd8de9639, "ikoka_stick_nrf_30dbm_companion_radio_ble" }, + { 0x8ab5c51b, "ikoka_stick_nrf_30dbm_companion_radio_usb" }, + { 0x80097676, "ikoka_stick_nrf_30dbm_kiss_modem" }, + { 0xffe70df5, "ikoka_stick_nrf_30dbm_repeater" }, + { 0x3da80417, "ikoka_stick_nrf_30dbm_room_server" }, + { 0x47ca3c13, "ikoka_stick_nrf_33dbm_companion_radio_ble" }, + { 0x28b499c7, "ikoka_stick_nrf_33dbm_companion_radio_usb" }, + { 0xf1ab4c28, "ikoka_stick_nrf_33dbm_kiss_modem" }, + { 0xe5331f02, "ikoka_stick_nrf_33dbm_repeater" }, + { 0xfba7177b, "ikoka_stick_nrf_33dbm_room_server" }, + { 0x69df7765, "LilyGo_T-Echo-Lite_companion_radio_ble" }, + { 0xe9fbc9be, "LilyGo_T-Echo-Lite_kiss_modem" }, + { 0x40818863, "LilyGo_T-Echo-Lite_non_shell_companion_radio_ble" }, + { 0x65deac33, "LilyGo_T-Echo-Lite_non_shell_companion_radio_usb" }, + { 0xd86cf67a, "LilyGo_T-Echo-Lite_repeater" }, + { 0xed73ac03, "LilyGo_T-Echo-Lite_room_server" }, + { 0xab0d550a, "LilyGo_T-Echo_Card_companion_radio_ble" }, + { 0x1bd2a266, "LilyGo_T-Echo_Card_companion_radio_usb" }, + { 0x9a5ae8fc, "LilyGo_T-Echo_Card_repeater" }, + { 0x1e9eb97a, "LilyGo_T-Echo_Card_room_server" }, + { 0x139536f4, "LilyGo_T-Echo_companion_radio_ble" }, + { 0x0716c724, "LilyGo_T-Echo_companion_radio_usb" }, + { 0x8de23fa7, "LilyGo_T-Echo_kiss_modem" }, + { 0xb92ec96b, "LilyGo_T-Echo_repeater" }, + { 0x420ebc63, "LilyGo_T-Echo_room_server" }, + { 0x967ccaee, "LilyGo_T3S3_sx1262_companion_radio_ble" }, + { 0x5f7c7688, "LilyGo_T3S3_sx1262_companion_radio_usb" }, + { 0xbd04e6e7, "LilyGo_T3S3_sx1262_kiss_modem" }, + { 0x1aeac884, "LilyGo_T3S3_sx1262_repeater" }, + { 0x07849209, "LilyGo_T3S3_sx1262_repeater_bridge_espnow" }, + { 0xbfb60e7c, "LilyGo_T3S3_sx1262_room_server" }, + { 0x96036a68, "LilyGo_T3S3_sx1262_terminal_chat" }, + { 0xb76a745c, "LilyGo_T3S3_sx1276_companion_radio_ble" }, + { 0x43023193, "LilyGo_T3S3_sx1276_companion_radio_usb" }, + { 0xa7d97db7, "LilyGo_T3S3_sx1276_kiss_modem" }, + { 0x468a133e, "LilyGo_T3S3_sx1276_repeater" }, + { 0xb59ea2ed, "LilyGo_T3S3_sx1276_repeater_bridge_espnow" }, + { 0xbe372c6d, "LilyGo_T3S3_sx1276_room_server" }, + { 0x6215e9eb, "LilyGo_T3S3_sx1276_terminal_chat" }, + { 0x3898ea56, "LilyGo_TBeam_1W_companion_radio_ble" }, + { 0x4f722e70, "LilyGo_TBeam_1W_companion_radio_usb" }, + { 0x694eed25, "LilyGo_TBeam_1W_companion_radio_wifi" }, + { 0x85b8b9d7, "LilyGo_TBeam_1W_kiss_modem" }, + { 0xe8421e36, "LilyGo_TBeam_1W_repeater" }, + { 0x82eaaf9e, "LilyGo_TBeam_1W_repeater_bridge_espnow" }, + { 0xe4a2de74, "LilyGo_TBeam_1W_room_server" }, + { 0xaeb637f1, "LilyGo_TDeck_companion_radio_ble" }, + { 0x86813271, "LilyGo_TDeck_companion_radio_usb" }, + { 0x67b2ff84, "LilyGo_TDeck_kiss_modem" }, + { 0xd0e2e616, "LilyGo_TDeck_repeater" }, + { 0x3f9edfe3, "LilyGo_TETH_Elite_sx1262_companion_radio_ble" }, + { 0xc23f8082, "LilyGo_TETH_Elite_sx1262_companion_radio_usb" }, + { 0x8f256e56, "LilyGo_TETH_Elite_sx1262_repeater" }, + { 0xf1407f39, "LilyGo_TETH_Elite_sx1262_room_server" }, + { 0xe90a3181, "LilyGo_Tlora_C6_companion_radio_ble_" }, + { 0x3b1b4237, "LilyGo_Tlora_C6_kiss_modem" }, + { 0x21c6d08d, "LilyGo_Tlora_C6_repeater_" }, + { 0x34302c14, "LilyGo_Tlora_C6_room_server_" }, + { 0x268d6fc1, "LilyGo_TLora_V2_1_1_6_companion_radio_ble" }, + { 0x3c172d27, "LilyGo_TLora_V2_1_1_6_companion_radio_usb" }, + { 0xf7c5d584, "LilyGo_TLora_V2_1_1_6_companion_radio_wifi" }, + { 0x81960cc0, "LilyGo_TLora_V2_1_1_6_kiss_modem" }, + { 0x504020ea, "LilyGo_TLora_V2_1_1_6_repeater" }, + { 0x8e5c0c88, "LilyGo_TLora_V2_1_1_6_repeater_bridge_espnow" }, + { 0x4e42e0ad, "LilyGo_TLora_V2_1_1_6_repeater_bridge_rs232" }, + { 0xa2ce002f, "LilyGo_TLora_V2_1_1_6_room_server" }, + { 0x6f16479e, "LilyGo_TLora_V2_1_1_6_terminal_chat" }, + { 0x1018e5d1, "M5Stack_Unit_C6L_companion_radio_ble" }, + { 0xc0ab5040, "M5Stack_Unit_C6L_companion_radio_usb" }, + { 0xa2bf90bd, "M5Stack_Unit_C6L_kiss_modem" }, + { 0xe6bff8f9, "M5Stack_Unit_C6L_repeater" }, + { 0xb9cd26cb, "M5Stack_Unit_C6L_room_server" }, + { 0xdbfe2675, "Meshadventurer_sx1262_companion_radio_ble" }, + { 0xb00241d0, "Meshadventurer_sx1262_companion_radio_usb" }, + { 0x88e77fee, "Meshadventurer_sx1262_kiss_modem" }, + { 0xae2d9caa, "Meshadventurer_sx1262_repeater" }, + { 0xecea445b, "Meshadventurer_sx1262_repeater_bridge_espnow" }, + { 0xa0043e00, "Meshadventurer_sx1262_room_server" }, + { 0x26bbf4ed, "Meshadventurer_sx1262_terminal_chat" }, + { 0xdcc0f158, "Meshadventurer_sx1268_companion_radio_ble" }, + { 0xe3320463, "Meshadventurer_sx1268_companion_radio_usb" }, + { 0x852b928a, "Meshadventurer_sx1268_kiss_modem" }, + { 0x0ec870e9, "Meshadventurer_sx1268_repeater" }, + { 0xc845072e, "Meshadventurer_sx1268_repeater_bridge_espnow" }, + { 0x8f00c1f2, "Meshadventurer_sx1268_room_server" }, + { 0xcea95e20, "Meshadventurer_sx1268_terminal_chat" }, + { 0xd45277ff, "Meshimi_companion_radio_ble_" }, + { 0x2cad9a9d, "Meshimi_repeater_" }, + { 0x063f1f00, "nibble_screen_connect_companion_radio_ble_" }, + { 0xa8a79da5, "nibble_screen_connect_companion_radio_usb_" }, + { 0xc0d3815b, "nibble_screen_connect_companion_radio_wifi_" }, + { 0x966ac88e, "nibble_screen_connect_kiss_modem_" }, + { 0x104b3f8b, "nibble_screen_connect_repeater_" }, + { 0x23b49348, "nibble_screen_connect_repeater_bridge_espnow_" }, + { 0xc42cc90d, "nibble_screen_connect_room_server_" }, + { 0xc24791c3, "nibble_screen_connect_terminal_chat_" }, + { 0x15e11067, "nibble_zero_connect_companion_radio_ble_" }, + { 0x90799c32, "nibble_zero_connect_companion_radio_usb_" }, + { 0x881a6a48, "nibble_zero_connect_companion_radio_wifi_" }, + { 0xf26af51c, "nibble_zero_connect_repeater_" }, + { 0x8174cbdc, "nibble_zero_connect_repeater_bridge_espnow_" }, + { 0x0bf0f061, "nibble_zero_connect_room_server_" }, + { 0x2509dd80, "nibble_zero_connect_terminal_chat_" }, + { 0x56293b24, "ProMicro_companion_radio_ble" }, + { 0x8c81078c, "ProMicro_companion_radio_usb" }, + { 0xbd6a1c97, "ProMicro_kiss_modem" }, + { 0xf4d98c12, "ProMicro_repeater" }, + { 0xccac4283, "ProMicro_repeater_bridge_rs232_serial1" }, + { 0x5f606274, "ProMicro_room_server" }, + { 0x84923735, "ProMicro_sensor" }, + { 0x2c0b8b05, "ProMicro_terminal_chat" }, + { 0xdba69401, "R1Neo_companion_radio_ble" }, + { 0xcb6555ee, "R1Neo_companion_radio_usb" }, + { 0xd60daf25, "R1Neo_kiss_modem" }, + { 0x410bd800, "R1Neo_repeater" }, + { 0xd8641710, "R1Neo_room_server" }, + { 0x4d0b1601, "R1Neo_sensor" }, + { 0xab2ac09e, "R1Neo_terminal_chat" }, + { 0xa17fc4f7, "RAK_3112_companion_radio_ble" }, + { 0x90c4eb3f, "RAK_3112_companion_radio_usb" }, + { 0x19d9ce91, "RAK_3112_companion_radio_wifi" }, + { 0x121cc72f, "RAK_3112_kiss_modem" }, + { 0xfa8510fa, "RAK_3112_repeater" }, + { 0xd34d8461, "RAK_3112_repeater_bridge_espnow" }, + { 0x98077550, "RAK_3112_repeater_bridge_rs232" }, + { 0x2ed959aa, "RAK_3112_room_server" }, + { 0xced7e127, "RAK_3112_sensor" }, + { 0x6925aaf0, "RAK_3112_terminal_chat" }, + { 0x9f4d58ac, "RAK_4631_companion_radio_ble" }, + { 0x48d3b5d4, "RAK_4631_companion_radio_usb" }, + { 0x14e6965d, "RAK_4631_kiss_modem" }, + { 0x04d413fd, "RAK_4631_repeater" }, + { 0x22ea8497, "RAK_4631_repeater_bridge_rs232_serial1" }, + { 0x21bae522, "RAK_4631_repeater_bridge_rs232_serial2" }, + { 0x626d80ed, "RAK_4631_room_server" }, + { 0x8b1d6850, "RAK_4631_sensor" }, + { 0xf1d3c5a8, "RAK_4631_terminal_chat" }, + { 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" }, + { 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" }, + { 0x8a7cb79b, "RAK_WisMesh_Tag_kiss_modem" }, + { 0x4fa4ae55, "RAK_WisMesh_Tag_repeater" }, + { 0x9cb0b232, "RAK_WisMesh_Tag_room_server" }, + { 0x51fa1a49, "RAK_WisMesh_Tag_sensor" }, + { 0xd904c2d5, "SenseCapIndicator-ESPNow_comp_radio_usb" }, + { 0xd0df8303, "Station_G2_companion_radio_ble" }, + { 0x5c1a54f8, "Station_G2_companion_radio_usb" }, + { 0x79cc029e, "Station_G2_companion_radio_wifi" }, + { 0x41197b92, "Station_G2_kiss_modem" }, + { 0x05747eb6, "Station_G2_logging_repeater" }, + { 0x75587f24, "Station_G2_logging_repeater_bridge_espnow" }, + { 0xf2128c81, "Station_G2_repeater" }, + { 0x1b34e7b4, "Station_G2_repeater_bridge_espnow" }, + { 0x73c4a019, "Station_G2_room_server" }, + { 0x8e549407, "Station_G3_ESP32_companion_radio_ble" }, + { 0x1deac038, "Station_G3_ESP32_companion_radio_usb" }, + { 0x2a2f303b, "Station_G3_ESP32_companion_radio_wifi" }, + { 0x0ba4453d, "Station_G3_ESP32_kiss_modem" }, + { 0xf8d1958f, "Station_G3_ESP32_logging_repeater" }, + { 0x3c49caf8, "Station_G3_ESP32_repeater" }, + { 0x91878dd8, "Station_G3_ESP32_room_server" }, + { 0x4dbf035e, "t1000e_companion_radio_ble" }, + { 0xff4102d6, "t1000e_companion_radio_usb" }, + { 0x2f9a9185, "t1000e_kiss_modem" }, + { 0x000fc519, "t1000e_repeater" }, + { 0x3e55e34d, "t1000e_room_server" }, + { 0x16482630, "T_Beam_S3_Supreme_SX1262_companion_radio_ble" }, + { 0x01c43d49, "T_Beam_S3_Supreme_SX1262_companion_radio_wifi" }, + { 0x51fe3801, "T_Beam_S3_Supreme_SX1262_kiss_modem" }, + { 0x857149e5, "T_Beam_S3_Supreme_SX1262_repeater" }, + { 0xd9d64938, "T_Beam_S3_Supreme_SX1262_repeater_bridge_espnow" }, + { 0x9952fbe3, "T_Beam_S3_Supreme_SX1262_room_server" }, + { 0xdc27c37c, "Tbeam_SX1262_companion_radio_ble" }, + { 0xb27514a3, "Tbeam_SX1262_kiss_modem" }, + { 0xe9ce038c, "Tbeam_SX1262_repeater" }, + { 0x06406dd4, "Tbeam_SX1262_repeater_bridge_espnow" }, + { 0x189660b3, "Tbeam_SX1262_room_server" }, + { 0x2082078d, "Tbeam_SX1276_companion_radio_ble" }, + { 0x3750e80d, "Tbeam_SX1276_kiss_modem" }, + { 0xe4b49973, "Tbeam_SX1276_repeater" }, + { 0x0c0c4605, "Tbeam_SX1276_repeater_bridge_espnow" }, + { 0x5a79bac1, "Tbeam_SX1276_room_server" }, + { 0x8eaa289e, "Tenstar_C3_sx1262_kiss_modem" }, + { 0x70e91bd9, "Tenstar_C3_sx1262_repeater" }, + { 0x5a7e9c2d, "Tenstar_C3_sx1262_repeater_bridge_espnow" }, + { 0x1211b151, "Tenstar_C3_sx1268_kiss_modem" }, + { 0x0a32044b, "Tenstar_C3_sx1268_repeater" }, + { 0xa13fe853, "Tenstar_C3_sx1268_repeater_bridge_espnow" }, + { 0x79b9ff01, "ThinkNode_M1_companion_radio_ble" }, + { 0xc3decea2, "ThinkNode_M1_companion_radio_usb" }, + { 0x3c012018, "ThinkNode_M1_kiss_modem" }, + { 0x6afd6f75, "ThinkNode_M1_repeater" }, + { 0x7517d400, "ThinkNode_M1_room_server" }, + { 0xd7598668, "ThinkNode_M2_companion_radio_ble" }, + { 0x413287d8, "ThinkNode_M2_companion_radio_serial" }, + { 0xe8cd2377, "ThinkNode_M2_companion_radio_usb" }, + { 0x3309bdaf, "ThinkNode_M2_companion_radio_wifi" }, + { 0x7330afbb, "ThinkNode_M2_kiss_modem" }, + { 0x9df48ce6, "ThinkNode_M2_Repeater" }, + { 0xa5cb783a, "ThinkNode_M2_Repeater_bridge_espnow" }, + { 0x4b9f8283, "ThinkNode_M2_room_server" }, + { 0x311846c9, "ThinkNode_M2_terminal_chat" }, + { 0x8b6b29b7, "ThinkNode_M3_companion_radio_ble" }, + { 0x37928233, "ThinkNode_M3_companion_radio_usb" }, + { 0x88ac45da, "ThinkNode_M3_kiss_modem" }, + { 0x087f99c0, "ThinkNode_M3_repeater" }, + { 0x2afe6c63, "ThinkNode_M3_room_server" }, + { 0xf2d61a1c, "ThinkNode_M5_companion_radio_ble" }, + { 0x1da30414, "ThinkNode_M5_companion_radio_serial" }, + { 0xae8fe1c9, "ThinkNode_M5_companion_radio_usb" }, + { 0x67acd73f, "ThinkNode_M5_companion_radio_wifi" }, + { 0xfb118ed8, "ThinkNode_M5_kiss_modem" }, + { 0xe4213729, "ThinkNode_M5_Repeater" }, + { 0x1298038d, "ThinkNode_M5_Repeater_bridge_espnow" }, + { 0x2c47acff, "ThinkNode_M5_room_server" }, + { 0x7d641646, "ThinkNode_M5_terminal_chat" }, + { 0x51efdbfe, "ThinkNode_M6_companion_radio_ble" }, + { 0xf2aada5b, "ThinkNode_M6_companion_radio_usb" }, + { 0xe59c9373, "ThinkNode_M6_kiss_modem" }, + { 0x236f605c, "ThinkNode_M6_repeater" }, + { 0x93994fed, "ThinkNode_M6_room_server" }, + { 0x37443a5b, "WHY2025_badge_companion_radio_ble_" }, + { 0x603384a7, "WHY2025_badge_repeater_" }, + { 0x475a2a6a, "WioTrackerL1_companion_radio_ble" }, + { 0xf4175d2c, "WioTrackerL1_companion_radio_usb" }, + { 0x3d831006, "WioTrackerL1_kiss_modem" }, + { 0x556eb1bc, "WioTrackerL1_repeater" }, + { 0x0c689b90, "WioTrackerL1_room_server" }, + { 0x91eac424, "WioTrackerL1Eink_companion_radio_ble" }, + { 0x7a84a8c4, "WioTrackerL1Eink_kiss_modem" }, + { 0x67843a5f, "Xiao_C3_companion_radio_ble" }, + { 0xfad357ab, "Xiao_C3_companion_radio_usb" }, + { 0x16af5c67, "Xiao_C3_companion_radio_wifi" }, + { 0x8c105104, "Xiao_C3_kiss_modem" }, + { 0xa80042a0, "Xiao_C3_repeater" }, + { 0x4bff0748, "Xiao_C3_room_server" }, + { 0x77c8f2b5, "Xiao_C6_companion_radio_ble_" }, + { 0x71ee0e15, "Xiao_C6_kiss_modem" }, + { 0x8824cd0c, "Xiao_C6_repeater_" }, + { 0xe5439198, "Xiao_nrf52_companion_radio_ble" }, + { 0xe90a6e74, "Xiao_nrf52_companion_radio_usb" }, + { 0x833e5cdc, "Xiao_nrf52_kiss_modem" }, + { 0x93145fbb, "Xiao_nrf52_repeater" }, + { 0x33a154ff, "Xiao_nrf52_room_server" }, + { 0x6658b418, "Xiao_S3_companion_radio_ble" }, + { 0x964f4f5c, "Xiao_S3_companion_radio_usb" }, + { 0x0a5dc760, "Xiao_S3_kiss_modem" }, + { 0xd08496ce, "Xiao_S3_repeater" }, + { 0x293ac2c8, "Xiao_S3_repeater_bridge_espnow" }, + { 0x60ebfa87, "Xiao_S3_room_server" }, + { 0x60f5fdc2, "Xiao_S3_sensor" }, + { 0xe8f79d22, "Xiao_S3_WIO_companion_radio_ble" }, + { 0x9f27be15, "Xiao_S3_WIO_companion_radio_serial" }, + { 0x396fecc7, "Xiao_S3_WIO_companion_radio_usb" }, + { 0xf94e3407, "Xiao_S3_WIO_companion_radio_wifi" }, + { 0xe1930103, "Xiao_S3_WIO_kiss_modem" }, + { 0x9f19bcd1, "Xiao_S3_WIO_repeater" }, + { 0xfcebff61, "Xiao_S3_WIO_repeater_bridge_espnow" }, + { 0xccb734e2, "Xiao_S3_WIO_room_server" }, + { 0xf967a300, "Xiao_S3_WIO_sensor" }, + { 0xa0cc1c36, "Xiao_S3_WIO_terminal_chat" }, + }; + for (unsigned i = 0; i < sizeof(T) / sizeof(T[0]); i++) + if (T[i].id == target_id) return T[i].env; + return nullptr; +} + +} } // namespace mesh::ota diff --git a/src/helpers/ota/OtaVerify.cpp b/src/helpers/ota/OtaVerify.cpp new file mode 100644 index 0000000000..42b6d53fe8 --- /dev/null +++ b/src/helpers/ota/OtaVerify.cpp @@ -0,0 +1,27 @@ +#include "OtaVerify.h" +#include "MerkleTree.h" +#include "Multihash.h" +#include "Identity.h" + +namespace mesh { +namespace ota { + +VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow) { + VerifyResult r; + MotaManifest m; + if (!mota_parse(buf, len, m)) return r; + r.parsed = true; + r.root_ok = mota_check_root(m); + r.image_ok = m.is_full() ? mota_check_image_hash_full(m) + : true; // delta image_hash needs the base; verified at apply time + r.is_signed = m.is_signed(); + if (r.is_signed) { + mesh::Identity signer(m.signer_pubkey); + r.sig_ok = signer.verify(m.signature, m.manifest_start, (int)m.signed_len); + r.trusted = r.sig_ok && allow.contains(m.signer_pubkey); + } + return r; +} + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/OtaVerify.h b/src/helpers/ota/OtaVerify.h new file mode 100644 index 0000000000..85fb968346 --- /dev/null +++ b/src/helpers/ota/OtaVerify.h @@ -0,0 +1,29 @@ +#pragma once + +#include "MotaContainer.h" +#include "SignerAllowlist.h" + +// Full verification of a staged `.mota` (device-side: uses Ed25519 via mesh::Identity, so NOT compiled +// on the native host — the portable integrity checks live in MotaContainer and are unit-tested there). + +namespace mesh { +namespace ota { + +struct VerifyResult { + bool parsed = false; // container + manifest parsed + bool root_ok = false; // merkle_root recomputed from leaves[] matches + bool image_ok = false; // full: sha2-256(payload)==image_hash; delta: deferred to apply (set true) + bool is_signed = false; + bool sig_ok = false; // Ed25519 signature valid for signer_pubkey + bool trusted = false; // signer_pubkey is in the allowlist + + // Integrity holds (safe to keep/serve). For a signed image, the signature must also verify. + bool integrity_ok() const { return parsed && root_ok && image_ok && (!is_signed || sig_ok); } + // Eligible for AUTO-apply: integrity + signed by an allowlisted key (decision D2). + bool auto_appliable() const { return integrity_ok() && is_signed && sig_ok && trusted; } +}; + +VerifyResult ota_verify(const uint8_t* buf, uint32_t len, const SignerAllowlist& allow); + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/SignerAllowlist.h b/src/helpers/ota/SignerAllowlist.h new file mode 100644 index 0000000000..726644fd5a --- /dev/null +++ b/src/helpers/ota/SignerAllowlist.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include + +// Runtime-managed allowlist of trusted Ed25519 firmware-signer public keys (docs/ota_protocol.md §9, +// decision D2 + Q1: no key embedded in firmware; only allowlist-signed firmware may auto-apply). +// Portable + fixed-capacity (no dynamic allocation). Persistence (load/save) is layered on per-platform. + +namespace mesh { +namespace ota { + +#ifndef MAX_OTA_SIGNERS +#define MAX_OTA_SIGNERS 4 +#endif + +class SignerAllowlist { + uint8_t _keys[MAX_OTA_SIGNERS][32]; + uint8_t _count = 0; + +public: + void clear() { _count = 0; } + uint8_t count() const { return _count; } + const uint8_t* get(uint8_t i) const { return (i < _count) ? _keys[i] : nullptr; } + + bool contains(const uint8_t* pub) const { + for (uint8_t i = 0; i < _count; i++) + if (memcmp(_keys[i], pub, 32) == 0) return true; + return false; + } + + // Add a key (idempotent). Returns false if the list is full. + bool add(const uint8_t* pub) { + if (contains(pub)) return true; + if (_count >= MAX_OTA_SIGNERS) return false; + memcpy(_keys[_count++], pub, 32); + return true; + } + + bool remove(const uint8_t* pub) { + for (uint8_t i = 0; i < _count; i++) { + if (memcmp(_keys[i], pub, 32) == 0) { + memmove(_keys[i], _keys[i + 1], (size_t)(_count - i - 1) * 32); + _count--; + return true; + } + } + return false; + } + + // Serialize as: count(1) || key0(32) || key1(32) ... Returns bytes written. + uint32_t serialize(uint8_t* out, uint32_t max_len) const { + uint32_t need = 1 + (uint32_t)_count * 32; + if (max_len < need) return 0; + out[0] = _count; + memcpy(out + 1, _keys, (size_t)_count * 32); + return need; + } + + bool deserialize(const uint8_t* in, uint32_t len) { + if (len < 1) return false; + uint8_t n = in[0]; + if (n > MAX_OTA_SIGNERS || (uint32_t)1 + n * 32 > len) return false; + _count = n; + memcpy(_keys, in + 1, (size_t)n * 32); + return true; + } +}; + +} // namespace ota +} // namespace mesh diff --git a/src/helpers/ota/detools/README.meshcore.txt b/src/helpers/ota/detools/README.meshcore.txt new file mode 100644 index 0000000000..770733aadb --- /dev/null +++ b/src/helpers/ota/detools/README.meshcore.txt @@ -0,0 +1,42 @@ +Vendored detools embeddable C decoder +===================================== + +Source : https://github.com/eerimoq/detools (tag 0.53.0, c/ directory) +Files : detools.c, detools.h +License : BSD 2-Clause (Erik Moqvist; original bsdiff (c) Colin Percival). + sais.c (host packager only, not vendored) is MIT. + Compatible with MeshCore's MIT license; notices retained in-file. + +Why vendored +------------ +The detools PyPI package (used by tools/mota to *create* patches with +detools.create_patch) ships only the Python library + patch-creation C +extensions -- NOT the embeddable decoder. The on-device delta applier needs +detools' own decoder, so we vendor c/detools.{c,h} verbatim. This is detools' +official C implementation; MeshCore does not reimplement the delta codec. + +Local modifications +------------------- +Only the config defaults at the top of detools.h were changed (upstream = 1): + DETOOLS_CONFIG_FILE_IO -> 0 (no file IO on device) + DETOOLS_CONFIG_COMPRESSION_LZMA -> 0 (would need liblzma) + DETOOLS_CONFIG_COMPRESSION_HEATSHRINK -> 0 (would need malloc + heatshrink/) + DETOOLS_CONFIG_COMPRESSION_NONE = 1 (kept) + DETOOLS_CONFIG_COMPRESSION_CRLE = 1 (kept) +detools.c is byte-for-byte upstream. + +With this config the decoder is self-contained (no malloc, no liblzma, no +heatshrink/, no file IO) and applies `--codec sequential --compression crle` +patches, which is what tools/mota produces for MeshCore .mota deltas. + +Usage on device +--------------- +src/helpers/ota/OtaApply.cpp wraps detools_apply_patch_callbacks(): + from_read/from_seek -> running OTA slot (the delta base, via esp_partition_read) + patch_read -> the .mota payload held in RAM (fetched over LoRa) + to_write -> inactive OTA slot (via esp_ota_write) + running SHA-256 +The decoded image is verified against the signed manifest image_hash before the +slot is armed as boot partition. + +To update: re-copy c/detools.{c,h} from the pinned detools tag and re-apply the +three config-default edits above. diff --git a/src/helpers/ota/detools/detools.c b/src/helpers/ota/detools/detools.c new file mode 100644 index 0000000000..5d25489576 --- /dev/null +++ b/src/helpers/ota/detools/detools.c @@ -0,0 +1,2783 @@ +/** + * BSD 2-Clause License + * + * Copyright (c) 2019-2020, Erik Moqvist + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include +#include "detools.h" + +/* Patch types. */ +#define PATCH_TYPE_SEQUENTIAL 0 +#define PATCH_TYPE_IN_PLACE 1 + +/* Compressions. */ +#define COMPRESSION_NONE 0 +#define COMPRESSION_LZMA 1 +#define COMPRESSION_CRLE 2 +#define COMPRESSION_HEATSHRINK 4 + +#define MIN(x, y) (((x) < (y)) ? (x) : (y)) +#define MAX(x, y) (((x) > (y)) ? (x) : (y)) +#define DIV_CEIL(n, d) (((n) + (d) - 1) / (d)) + +/* + * Utility functions. + */ + +static size_t chunk_left(struct detools_apply_patch_chunk_t *self_p) +{ + return (self_p->size - self_p->offset); +} + +static bool chunk_available(struct detools_apply_patch_chunk_t *self_p) +{ + return (chunk_left(self_p) > 0); +} + +static uint8_t chunk_get_no_check(struct detools_apply_patch_chunk_t *self_p) +{ + uint8_t data; + + data = self_p->buf_p[self_p->offset]; + self_p->offset++; + + return (data); +} + +static int chunk_get(struct detools_apply_patch_chunk_t *self_p, + uint8_t *data_p) +{ + if (!chunk_available(self_p)) { + return (1); + } + + *data_p = chunk_get_no_check(self_p); + + return (0); +} + +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 \ + || DETOOLS_CONFIG_COMPRESSION_CRLE == 1 \ + || DETOOLS_CONFIG_COMPRESSION_LZMA == 1 + +static void chunk_read_all_no_check(struct detools_apply_patch_chunk_t *self_p, + uint8_t *buf_p, + size_t size) +{ + memcpy(buf_p, &self_p->buf_p[self_p->offset], size); + self_p->offset += size; +} + +#endif + +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 \ + || DETOOLS_CONFIG_COMPRESSION_CRLE == 1 + +static int chunk_read(struct detools_apply_patch_chunk_t *self_p, + uint8_t *buf_p, + size_t *size_p) +{ + if (!chunk_available(self_p)) { + return (1); + } + + *size_p = MIN(*size_p, chunk_left(self_p)); + chunk_read_all_no_check(self_p, buf_p, *size_p); + + return (0); +} + +#endif + +static bool is_overflow(int value) +{ + return ((value + 7) > (int)(8 * sizeof(int))); +} + +static int chunk_unpack_header_size(struct detools_apply_patch_chunk_t *self_p, + struct detools_apply_patch_size_t *size_state_p, + int *size_p) +{ + int res; + uint8_t byte; + + do { + switch (size_state_p->state) { + + case detools_unpack_usize_state_first_t: + res = chunk_get(self_p, &byte); + + if (res != 0) { + return (res); + } + + size_state_p->value = (byte & 0x3f); + size_state_p->offset = 6; + size_state_p->state = detools_unpack_usize_state_consecutive_t; + break; + + case detools_unpack_usize_state_consecutive_t: + res = chunk_get(self_p, &byte); + + if (res != 0) { + return (res); + } + + if (is_overflow(size_state_p->offset)) { + return (-DETOOLS_CORRUPT_PATCH_OVERFLOW); + } + + size_state_p->value |= ((byte & 0x7f) << size_state_p->offset); + size_state_p->offset += 7; + break; + + default: + return (-DETOOLS_INTERNAL_ERROR); + } + } while ((byte & 0x80) != 0); + + /* Done, fix sign. */ + size_state_p->state = detools_unpack_usize_state_first_t; + + *size_p = size_state_p->value; + + return (res); +} + +/* + * None patch reader. + */ + +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 + +static int patch_reader_none_decompress( + struct detools_apply_patch_patch_reader_t *self_p, + uint8_t *buf_p, + size_t *size_p) +{ + int res; + struct detools_apply_patch_patch_reader_none_t *none_p; + + none_p = &self_p->compression.none; + + if (none_p->patch_offset + *size_p > none_p->patch_size) { + return (-DETOOLS_CORRUPT_PATCH); + } + + res = chunk_read(self_p->patch_chunk_p, + buf_p, + size_p); + + if (res != 0) { + return (res); + } + + none_p->patch_offset += *size_p; + + return (0); +} + +static int patch_reader_none_destroy( + struct detools_apply_patch_patch_reader_t *self_p) +{ + struct detools_apply_patch_patch_reader_none_t *none_p; + + none_p = &self_p->compression.none; + + if (none_p->patch_offset == none_p->patch_size) { + return (0); + } else { + return (-DETOOLS_CORRUPT_PATCH); + } +} + +static int patch_reader_none_init(struct detools_apply_patch_patch_reader_t *self_p, + size_t patch_size) +{ + struct detools_apply_patch_patch_reader_none_t *none_p; + + none_p = &self_p->compression.none; + none_p->patch_size = patch_size; + none_p->patch_offset = 0; + self_p->destroy = patch_reader_none_destroy; + self_p->decompress = patch_reader_none_decompress; + + return (0); +} + +#endif + +/* + * Heatshrink patch reader. + */ + +#if DETOOLS_CONFIG_COMPRESSION_HEATSHRINK == 1 + +static void unpack_heatshrink_header(uint8_t byte, + int8_t *window_sz2_p, + int8_t *lookahead_sz2_p) +{ + *window_sz2_p = (((byte >> 4) & 0xf) + 4); + *lookahead_sz2_p = ((byte & 0xf) + 3); +} + +static int patch_reader_heatshrink_decompress( + struct detools_apply_patch_patch_reader_t *self_p, + uint8_t *buf_p, + size_t *size_p) +{ + int res; + struct detools_apply_patch_patch_reader_heatshrink_t *heatshrink_p; + size_t size; + size_t left; + HSD_poll_res pres; + HSD_sink_res sres; + uint8_t byte; + + heatshrink_p = &self_p->compression.heatshrink; + left = *size_p; + + if (heatshrink_p->window_sz2 == -1) { + res = chunk_get(self_p->patch_chunk_p, &byte); + + if (res != 0) { + return (1); + } + + unpack_heatshrink_header(byte, + &heatshrink_p->window_sz2, + &heatshrink_p->lookahead_sz2); + +#if HEATSHRINK_DYNAMIC_ALLOC == 1 + heatshrink_p->decoder_p = heatshrink_decoder_alloc( + 256, + heatshrink_p->window_sz2, + heatshrink_p->lookahead_sz2); + + if (heatshrink_p->decoder_p == NULL) { + return (-DETOOLS_HEATSHRINK_HEADER); + } +#else + if ((heatshrink_p->window_sz2 != HEATSHRINK_STATIC_WINDOW_BITS) + || (heatshrink_p->lookahead_sz2 != HEATSHRINK_STATIC_LOOKAHEAD_BITS)) { + return (-DETOOLS_HEATSHRINK_HEADER); + } + + heatshrink_p->decoder_p = &heatshrink_p->decoder; + heatshrink_decoder_reset(heatshrink_p->decoder_p); +#endif + + } + + while (1) { + /* Get available data. */ + pres = heatshrink_decoder_poll(heatshrink_p->decoder_p, + buf_p, + left, + &size); + + if (pres < 0) { + return (-DETOOLS_HEATSHRINK_POLL); + } + + buf_p += size; + left -= size; + + if (left == 0) { + return (0); + } + + /* Input (sink) more data if available. */ + res = chunk_get(self_p->patch_chunk_p, &byte); + + if (res == 0) { + sres = heatshrink_decoder_sink(heatshrink_p->decoder_p, + &byte, + sizeof(byte), + &size); + + if ((sres < 0) || (size != sizeof(byte))) { + return (-DETOOLS_HEATSHRINK_SINK); + } + } else { + if (left != *size_p) { + *size_p -= left; + + return (0); + } else { + return (1); + } + } + } + + return (res); +} + +static int patch_reader_heatshrink_destroy( + struct detools_apply_patch_patch_reader_t *self_p) +{ + struct detools_apply_patch_patch_reader_heatshrink_t *heatshrink_p; + HSD_finish_res fres; + + heatshrink_p = &self_p->compression.heatshrink; + + if (heatshrink_p->decoder_p == NULL) { + return (0); + } + + fres = heatshrink_decoder_finish(heatshrink_p->decoder_p); + +#if HEATSHRINK_DYNAMIC_ALLOC == 1 + heatshrink_decoder_free(heatshrink_p->decoder_p); +#endif + + if (fres == HSDR_FINISH_DONE) { + return (0); + } else { + return (-DETOOLS_CORRUPT_PATCH); + } +} + +static int patch_reader_heatshrink_init( + struct detools_apply_patch_patch_reader_t *self_p) +{ + struct detools_apply_patch_patch_reader_heatshrink_t *heatshrink_p; + + heatshrink_p = &self_p->compression.heatshrink; + heatshrink_p->window_sz2 = -1; + heatshrink_p->lookahead_sz2 = -1; + heatshrink_p->decoder_p = NULL; + self_p->destroy = patch_reader_heatshrink_destroy; + self_p->decompress = patch_reader_heatshrink_decompress; + + return (0); +} + +#endif + +/* + * LZMA patch reader. + */ + +#if DETOOLS_CONFIG_COMPRESSION_LZMA == 1 + +static int get_decompressed_data( + struct detools_apply_patch_patch_reader_lzma_t *lzma_p, + uint8_t *buf_p, + size_t size) +{ + int res; + + if (lzma_p->output_size >= size) { + memcpy(buf_p, lzma_p->output_p, size); + memmove(lzma_p->output_p, + &lzma_p->output_p[size], + lzma_p->output_size - size); + lzma_p->output_size -= size; + res = 0; + } else { + res = 1; + } + + return (res); +} + +static int prepare_input_buffer(struct detools_apply_patch_patch_reader_t *self_p) +{ + struct detools_apply_patch_patch_reader_lzma_t *lzma_p; + uint8_t *next_p; + size_t left; + + lzma_p = &self_p->compression.lzma; + left = chunk_left(self_p->patch_chunk_p); + + if (left == 0) { + return (1); + } + + next_p = malloc(lzma_p->stream.avail_in + left); + + if (next_p == NULL) { + return (-DETOOLS_OUT_OF_MEMORY); + } + + if (lzma_p->stream.next_in != NULL) { + memcpy(next_p, lzma_p->stream.next_in, lzma_p->stream.avail_in); + free(lzma_p->input_p); + } + + lzma_p->input_p = next_p; + chunk_read_all_no_check(self_p->patch_chunk_p, + &lzma_p->input_p[lzma_p->stream.avail_in], + left); + lzma_p->stream.next_in = next_p; + lzma_p->stream.avail_in += left; + + return (0); +} + +static int prepare_output_buffer(struct detools_apply_patch_patch_reader_t *self_p, + size_t size) +{ + struct detools_apply_patch_patch_reader_lzma_t *lzma_p; + uint8_t *output_p; + + lzma_p = &self_p->compression.lzma; + + output_p = malloc(size); + + if (output_p == NULL) { + return (-DETOOLS_OUT_OF_MEMORY); + } + + if (lzma_p->output_p != NULL) { + memcpy(output_p, lzma_p->output_p, lzma_p->output_size); + free(lzma_p->output_p); + } + + lzma_p->output_p = output_p; + lzma_p->stream.next_out = (output_p + lzma_p->output_size); + lzma_p->stream.avail_out = (size - lzma_p->output_size); + + return (0); +} + +static int patch_reader_lzma_decompress( + struct detools_apply_patch_patch_reader_t *self_p, + uint8_t *buf_p, + size_t *size_p) +{ + int res; + struct detools_apply_patch_patch_reader_lzma_t *lzma_p; + lzma_ret ret; + + lzma_p = &self_p->compression.lzma; + + /* Check if enough decompressed data is available. */ + res = get_decompressed_data(lzma_p, buf_p, *size_p); + + if (res == 0) { + return (res); + } + + while (1) { + /* Try to decompress requested data. */ + if (lzma_p->stream.avail_in > 0) { + res = prepare_output_buffer(self_p, *size_p); + + if (res != 0) { + return (res); + } + + ret = lzma_code(&lzma_p->stream, LZMA_RUN); + + switch (ret) { + + case LZMA_OK: + case LZMA_STREAM_END: + break; + + default: + return (-DETOOLS_LZMA_DECODE); + } + + lzma_p->output_size = (size_t)(lzma_p->stream.next_out - lzma_p->output_p); + } + + /* Check if enough decompressed data is available. */ + res = get_decompressed_data(lzma_p, buf_p, *size_p); + + if (res == 0) { + return (res); + } + + /* Get more data to decompress. */ + res = prepare_input_buffer(self_p); + + if (res != 0) { + return (res); + } + } +} + +static int patch_reader_lzma_destroy( + struct detools_apply_patch_patch_reader_t *self_p) +{ + struct detools_apply_patch_patch_reader_lzma_t *lzma_p; + + lzma_p = &self_p->compression.lzma; + + if (lzma_p->input_p != NULL) { + free(lzma_p->input_p); + } + + if (lzma_p->output_p != NULL) { + free(lzma_p->output_p); + } + + lzma_end(&lzma_p->stream); + + if ((lzma_p->stream.avail_in == 0) && (lzma_p->output_size == 0)) { + return (0); + } else { + return (-DETOOLS_CORRUPT_PATCH); + } +} + +static int patch_reader_lzma_init(struct detools_apply_patch_patch_reader_t *self_p) +{ + lzma_ret ret; + struct detools_apply_patch_patch_reader_lzma_t *lzma_p; + + lzma_p = &self_p->compression.lzma; + memset(&lzma_p->stream, 0, sizeof(lzma_p->stream)); + + ret = lzma_alone_decoder(&lzma_p->stream, UINT64_MAX); + + if (ret != LZMA_OK) { + return (-DETOOLS_LZMA_INIT); + } + + lzma_p->input_p = NULL; + lzma_p->output_p = NULL; + lzma_p->output_size = 0; + self_p->destroy = patch_reader_lzma_destroy; + self_p->decompress = patch_reader_lzma_decompress; + + return (0); +} + +#endif + +/* + * CRLE patch reader. + */ + +#if DETOOLS_CONFIG_COMPRESSION_CRLE == 1 + +static void unpack_usize_init(struct detools_unpack_usize_t *self_p) +{ + self_p->state = detools_unpack_usize_state_first_t; + self_p->value = 0; + self_p->offset = 0; +} + +static int unpack_usize(struct detools_unpack_usize_t *self_p, + struct detools_apply_patch_chunk_t *patch_chunk_p, + int *size_p) +{ + int res; + uint8_t byte; + + switch (self_p->state) { + + case detools_unpack_usize_state_first_t: + self_p->value = 0; + self_p->offset = 0; + self_p->state = detools_unpack_usize_state_consecutive_t; + break; + + case detools_unpack_usize_state_consecutive_t: + break; + + default: + return (-DETOOLS_INTERNAL_ERROR); + } + + do { + res = chunk_get(patch_chunk_p, &byte); + + if (res != 0) { + return (res); + } + + if (is_overflow(self_p->offset)) { + return (-DETOOLS_CORRUPT_PATCH_OVERFLOW); + } + + self_p->value |= ((byte & 0x7f) << self_p->offset); + self_p->offset += 7; + } while ((byte & 0x80) != 0); + + *size_p = self_p->value; + + return (0); +} + +static int patch_reader_crle_decompress_idle( + struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_patch_reader_crle_t *crle_p) +{ + int res; + uint8_t kind; + + res = chunk_get(self_p->patch_chunk_p, &kind); + + if (res != 0) { + return (res); + } + + res = 2; + + switch (kind) { + + case 0: + crle_p->state = detools_crle_state_scattered_size_t; + unpack_usize_init(&crle_p->kind.scattered.size); + break; + + case 1: + crle_p->state = detools_crle_state_repeated_repetitions_t; + unpack_usize_init(&crle_p->kind.repeated.size); + break; + + default: + res = -DETOOLS_CORRUPT_PATCH_CRLE_KIND; + break; + } + + return (res); +} + +static int patch_reader_crle_decompress_scattered_size( + struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_patch_reader_crle_t *crle_p) +{ + int res; + int size; + + res = unpack_usize(&crle_p->kind.scattered.size, + self_p->patch_chunk_p, + &size); + + if (res != 0) { + return (res); + } + + crle_p->state = detools_crle_state_scattered_data_t; + crle_p->kind.scattered.number_of_bytes_left = (size_t)size; + + return (2); +} + +static int patch_reader_crle_decompress_scattered_data( + struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_patch_reader_crle_t *crle_p, + uint8_t *buf_p, + size_t *size_p) +{ + int res; + + *size_p = MIN(*size_p, crle_p->kind.scattered.number_of_bytes_left); + res = chunk_read(self_p->patch_chunk_p, buf_p, size_p); + + if (res != 0) { + return (res); + } + + crle_p->kind.scattered.number_of_bytes_left -= *size_p; + + if (crle_p->kind.scattered.number_of_bytes_left == 0) { + crle_p->state = detools_crle_state_idle_t; + } + + return (0); +} + +static int patch_reader_crle_decompress_repeated_repetitions( + struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_patch_reader_crle_t *crle_p) +{ + int res; + int repetitions; + + res = unpack_usize(&crle_p->kind.repeated.size, + self_p->patch_chunk_p, + &repetitions); + + if (res != 0) { + return (res); + } + + crle_p->state = detools_crle_state_repeated_data_t; + crle_p->kind.repeated.number_of_bytes_left = (size_t)repetitions; + + return (2); +} + +static int patch_reader_crle_decompress_repeated_data( + struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_patch_reader_crle_t *crle_p) +{ + int res; + + res = chunk_get(self_p->patch_chunk_p, + &crle_p->kind.repeated.value); + + if (res != 0) { + return (res); + } + + crle_p->state = detools_crle_state_repeated_data_read_t; + + return (2); +} + +static int patch_reader_crle_decompress_repeated_data_read( + struct detools_apply_patch_patch_reader_crle_t *crle_p, + uint8_t *buf_p, + size_t *size_p) +{ + size_t size; + size_t i; + + size = MIN(*size_p, crle_p->kind.repeated.number_of_bytes_left); + + for (i = 0; i < size; i++) { + buf_p[i] = crle_p->kind.repeated.value; + } + + *size_p = size; + crle_p->kind.repeated.number_of_bytes_left -= size; + + if (crle_p->kind.repeated.number_of_bytes_left == 0) { + crle_p->state = detools_crle_state_idle_t; + } + + return (0); +} + +static int patch_reader_crle_decompress( + struct detools_apply_patch_patch_reader_t *self_p, + uint8_t *buf_p, + size_t *size_p) +{ + int res; + struct detools_apply_patch_patch_reader_crle_t *crle_p; + + crle_p = &self_p->compression.crle; + + do { + switch (crle_p->state) { + + case detools_crle_state_idle_t: + res = patch_reader_crle_decompress_idle(self_p, crle_p); + break; + + case detools_crle_state_scattered_size_t: + res = patch_reader_crle_decompress_scattered_size(self_p, crle_p); + break; + + case detools_crle_state_scattered_data_t: + res = patch_reader_crle_decompress_scattered_data(self_p, + crle_p, + buf_p, + size_p); + break; + + case detools_crle_state_repeated_repetitions_t: + res = patch_reader_crle_decompress_repeated_repetitions(self_p, + crle_p); + break; + + case detools_crle_state_repeated_data_t: + res = patch_reader_crle_decompress_repeated_data(self_p, crle_p); + break; + + case detools_crle_state_repeated_data_read_t: + res = patch_reader_crle_decompress_repeated_data_read(crle_p, + buf_p, + size_p); + break; + + default: + res = -DETOOLS_INTERNAL_ERROR; + break; + } + } while (res == 2); + + return (res); +} + +static int patch_reader_crle_destroy( + struct detools_apply_patch_patch_reader_t *self_p) +{ + (void)self_p; + + return (0); +} + +static int patch_reader_crle_init(struct detools_apply_patch_patch_reader_t *self_p) +{ + + struct detools_apply_patch_patch_reader_crle_t *crle_p; + + crle_p = &self_p->compression.crle; + crle_p->state = detools_crle_state_idle_t; + self_p->destroy = patch_reader_crle_destroy; + self_p->decompress = patch_reader_crle_decompress; + + return (0); +} + +#endif + +/* + * Patch reader. + */ + +/** + * Initialize given patch reader. + */ +static int patch_reader_init(struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_chunk_t *patch_chunk_p, + size_t patch_size, + int compression) +{ + int res; + +#if DETOOLS_CONFIG_COMPRESSION_NONE != 1 + (void)patch_size; +#endif + + self_p->patch_chunk_p = patch_chunk_p; + self_p->size.state = detools_unpack_usize_state_first_t; + + switch (compression) { + +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 + case COMPRESSION_NONE: + res = patch_reader_none_init(self_p, patch_size); + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_LZMA == 1 + case COMPRESSION_LZMA: + res = patch_reader_lzma_init(self_p); + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_CRLE == 1 + case COMPRESSION_CRLE: + res = patch_reader_crle_init(self_p); + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_HEATSHRINK == 1 + case COMPRESSION_HEATSHRINK: + res = patch_reader_heatshrink_init(self_p); + break; +#endif + + default: + res = -DETOOLS_BAD_COMPRESSION; + break; + } + + return (res); +} + +static int patch_reader_dump(struct detools_apply_patch_patch_reader_t *self_p, + int compression, + detools_state_write_t state_write) +{ + (void)self_p; + (void)state_write; + + int res; + + res = 0; + + switch (compression) { + +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 + case COMPRESSION_NONE: + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_CRLE == 1 + case COMPRESSION_CRLE: + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_HEATSHRINK == 1 +# if HEATSHRINK_DYNAMIC_ALLOC == 0 + case COMPRESSION_HEATSHRINK: + break; +# endif +#endif + + default: + res = -DETOOLS_NOT_IMPLEMENTED; + break; + } + + return (res); +} + +static int patch_reader_restore(struct detools_apply_patch_patch_reader_t *self_p, + struct detools_apply_patch_patch_reader_t *dumped_p, + struct detools_apply_patch_chunk_t *patch_chunk_p, + int compression, + detools_state_read_t state_read) +{ + (void)state_read; + + int res; + + res = 0; + *self_p = *dumped_p; + self_p->patch_chunk_p = patch_chunk_p; + + switch (compression) { + +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 + case COMPRESSION_NONE: + self_p->destroy = patch_reader_none_destroy; + self_p->decompress = patch_reader_none_decompress; + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_CRLE == 1 + case COMPRESSION_CRLE: + self_p->destroy = patch_reader_crle_destroy; + self_p->decompress = patch_reader_crle_decompress; + break; +#endif + +#if DETOOLS_CONFIG_COMPRESSION_HEATSHRINK == 1 +# if HEATSHRINK_DYNAMIC_ALLOC == 0 + case COMPRESSION_HEATSHRINK: + self_p->compression.heatshrink.decoder_p = + &self_p->compression.heatshrink.decoder; + self_p->destroy = patch_reader_heatshrink_destroy; + self_p->decompress = patch_reader_heatshrink_decompress; + break; +# endif +#endif + + default: + res = -DETOOLS_NOT_IMPLEMENTED; + break; + } + + return (res); +} + +/** + * Try to decompress given number of bytes. + * + * @return zero(0) if at least one byte was decompressed, one(1) if + * zero bytes were decompressed and more input is needed, or + * negative error code. + */ +static int patch_reader_decompress( + struct detools_apply_patch_patch_reader_t *self_p, + uint8_t *buf_p, + size_t *size_p) +{ + return (self_p->decompress(self_p, buf_p, size_p)); +} + +/** + * Unpack a size value. + */ +static int patch_reader_unpack_size( + struct detools_apply_patch_patch_reader_t *self_p, + int *size_p) +{ + int res; + uint8_t byte; + size_t size; + + size = 1; + + do { + switch (self_p->size.state) { + + case detools_unpack_usize_state_first_t: + res = patch_reader_decompress(self_p, &byte, &size); + + if (res != 0) { + return (res); + } + + self_p->size.is_signed = ((byte & 0x40) == 0x40); + self_p->size.value = (byte & 0x3f); + self_p->size.offset = 6; + self_p->size.state = detools_unpack_usize_state_consecutive_t; + break; + + case detools_unpack_usize_state_consecutive_t: + res = patch_reader_decompress(self_p, &byte, &size); + + if (res != 0) { + return (res); + } + + if (is_overflow(self_p->size.offset)) { + return (-DETOOLS_CORRUPT_PATCH_OVERFLOW); + } + + self_p->size.value |= ((byte & 0x7f) << self_p->size.offset); + self_p->size.offset += 7; + break; + + default: + return (-DETOOLS_INTERNAL_ERROR); + } + } while ((byte & 0x80) != 0); + + /* Done, fix sign. */ + self_p->size.state = detools_unpack_usize_state_first_t; + + if (self_p->size.is_signed) { + self_p->size.value *= -1; + } + + *size_p = self_p->size.value; + + return (res); +} + +static int common_process_size( + struct detools_apply_patch_patch_reader_t *patch_reader_p, + size_t to_pos, + size_t to_size, + int *size_p) +{ + int res; + + res = patch_reader_unpack_size(patch_reader_p, size_p); + + if (res != 0) { + return (res); + } + + if (to_pos + (size_t)*size_p > to_size) { + return (-DETOOLS_CORRUPT_PATCH); + } + + return (res); +} + +/* + * Low level sequential patch type functionality. + */ + +static int process_init_fixed_header(struct detools_apply_patch_t *self_p) +{ + int patch_type; + uint8_t byte; + + if (chunk_get(&self_p->chunk, &byte) != 0) { + return (-DETOOLS_SHORT_HEADER); + } + + patch_type = ((byte >> 4) & 0x7); + self_p->compression = (byte & 0xf); + + if (patch_type != PATCH_TYPE_SEQUENTIAL) { + return (-DETOOLS_BAD_PATCH_TYPE); + } + + self_p->init_state = detools_apply_patch_init_state_to_size_t; + self_p->size.state = detools_unpack_usize_state_first_t; + + return (0); +} + +static int process_init_to_size(struct detools_apply_patch_t *self_p) +{ + int res; + int to_size; + + res = chunk_unpack_header_size(&self_p->chunk, &self_p->size, &to_size); + + if (res != 0) { + return (res); + } + + res = patch_reader_init(&self_p->patch_reader, + &self_p->chunk, + self_p->patch_size - self_p->chunk.offset, + self_p->compression); + + if (res != 0) { + return (res); + } + + if (to_size < 0) { + return (-DETOOLS_CORRUPT_PATCH); + } + + self_p->to_size = (size_t)to_size; + + if (to_size > 0) { + self_p->state = detools_apply_patch_state_dfpatch_size_t; + } else { + self_p->state = detools_apply_patch_state_done_t; + } + + return (res); +} + +static int process_init(struct detools_apply_patch_t *self_p) +{ + int res; + + switch (self_p->init_state) { + + case detools_apply_patch_init_state_fixed_header_t: + res = process_init_fixed_header(self_p); + break; + + case detools_apply_patch_init_state_to_size_t: + res = process_init_to_size(self_p); + break; + + default: + res = -DETOOLS_INTERNAL_ERROR; + break; + } + + return (res); +} + +static int process_dfpatch_size(struct detools_apply_patch_t *self_p) +{ + int res; + int size; + + res = patch_reader_unpack_size(&self_p->patch_reader, &size); + + if (res != 0) { + return (res); + } + + if (size > 0) { + return (-DETOOLS_NOT_IMPLEMENTED); + } + + self_p->state = detools_apply_patch_state_diff_size_t; + + return (0); +} + +static int process_size(struct detools_apply_patch_t *self_p, + enum detools_apply_patch_state_t next_state) +{ + int res; + int size; + + res = common_process_size(&self_p->patch_reader, + self_p->to_offset, + self_p->to_size, + &size); + + if (res != 0) { + return (res); + } + + self_p->state = next_state; + self_p->chunk_size = (size_t)size; + + return (res); +} + +static int process_data(struct detools_apply_patch_t *self_p, + enum detools_apply_patch_state_t next_state) +{ + int res; + size_t i; + uint8_t to[128]; + size_t to_size; + uint8_t from[128]; + + to_size = MIN(sizeof(to), self_p->chunk_size); + + if (to_size == 0) { + self_p->state = next_state; + + return (0); + } + + res = patch_reader_decompress(&self_p->patch_reader, + &to[0], + &to_size); + + if (res != 0) { + return (res); + } + + if (next_state == detools_apply_patch_state_extra_size_t) { + res = self_p->from_read(self_p->arg_p, &from[0], to_size); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + self_p->from_offset += to_size; + + for (i = 0; i < to_size; i++) { + to[i] = (uint8_t)(to[i] + from[i]); + } + } + + self_p->to_offset += to_size; + self_p->chunk_size -= to_size; + + res = self_p->to_write(self_p->arg_p, &to[0], to_size); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + return (res); +} + +static int process_diff_size(struct detools_apply_patch_t *self_p) +{ + return (process_size(self_p, detools_apply_patch_state_diff_data_t)); +} + +static int process_diff_data(struct detools_apply_patch_t *self_p) +{ + return (process_data(self_p, detools_apply_patch_state_extra_size_t)); +} + +static int process_extra_size(struct detools_apply_patch_t *self_p) +{ + return (process_size(self_p, detools_apply_patch_state_extra_data_t)); +} + +static int process_extra_data(struct detools_apply_patch_t *self_p) +{ + return (process_data(self_p, detools_apply_patch_state_adjustment_t)); +} + +static int process_adjustment(struct detools_apply_patch_t *self_p) +{ + int res; + int offset; + + res = patch_reader_unpack_size(&self_p->patch_reader, &offset); + + if (res != 0) { + return (res); + } + + res = self_p->from_seek(self_p->arg_p, offset); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + self_p->from_offset += offset; + + if (self_p->to_offset == self_p->to_size) { + self_p->state = detools_apply_patch_state_done_t; + } else { + self_p->state = detools_apply_patch_state_diff_size_t; + } + + return (res); +} + +static int apply_patch_process_once(struct detools_apply_patch_t *self_p) +{ + int res; + + switch (self_p->state) { + + case detools_apply_patch_state_init_t: + res = process_init(self_p); + break; + + case detools_apply_patch_state_dfpatch_size_t: + res = process_dfpatch_size(self_p); + break; + + case detools_apply_patch_state_diff_size_t: + res = process_diff_size(self_p); + break; + + case detools_apply_patch_state_diff_data_t: + res = process_diff_data(self_p); + break; + + case detools_apply_patch_state_extra_size_t: + res = process_extra_size(self_p); + break; + + case detools_apply_patch_state_extra_data_t: + res = process_extra_data(self_p); + break; + + case detools_apply_patch_state_adjustment_t: + res = process_adjustment(self_p); + break; + + case detools_apply_patch_state_done_t: + return (-DETOOLS_ALREADY_DONE); + + case detools_apply_patch_state_failed_t: + res = -DETOOLS_ALREADY_FAILED; + break; + + default: + res = -DETOOLS_INTERNAL_ERROR; + break; + } + + if (res < 0) { + self_p->state = detools_apply_patch_state_failed_t; + } + + return (res); +} + +static int apply_patch_common_finalize( + int res, + struct detools_apply_patch_patch_reader_t *patch_reader_p, + size_t to_size) +{ + if (res == 1) { + res = -DETOOLS_NOT_ENOUGH_PATCH_DATA; + } + + if (res == -DETOOLS_ALREADY_DONE) { + res = 0; + } + + if (patch_reader_p->destroy != NULL) { + if (res == 0) { + res = patch_reader_p->destroy(patch_reader_p); + } else { + (void)patch_reader_p->destroy(patch_reader_p); + } + } + + if (res == 0) { + res = (int)to_size; + } + + return (res); +} + +int detools_apply_patch_init(struct detools_apply_patch_t *self_p, + detools_read_t from_read, + detools_seek_t from_seek, + size_t patch_size, + detools_write_t to_write, + void *arg_p) +{ + self_p->from_read = from_read; + self_p->from_seek = from_seek; + self_p->patch_size = patch_size; + self_p->patch_offset = 0; + self_p->to_offset = 0; + self_p->to_write = to_write; + self_p->from_offset = 0; + self_p->arg_p = arg_p; + self_p->state = detools_apply_patch_state_init_t; + self_p->init_state = detools_apply_patch_init_state_fixed_header_t; + self_p->patch_reader.destroy = NULL; + + return (0); +} + +int detools_apply_patch_dump(struct detools_apply_patch_t *self_p, + detools_state_write_t state_write) +{ + int res; + + res = state_write(self_p->arg_p, self_p, sizeof(*self_p)); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + if (self_p->state == detools_apply_patch_state_init_t) { + return (0); + } + + return (patch_reader_dump(&self_p->patch_reader, + self_p->compression, + state_write)); +} + +int detools_apply_patch_restore(struct detools_apply_patch_t *self_p, + detools_state_read_t state_read) +{ + int res; + struct detools_apply_patch_t dumped; + + res = state_read(self_p->arg_p, &dumped, sizeof(dumped)); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + self_p->state = dumped.state; + self_p->patch_size = dumped.patch_size; + + if (self_p->state == detools_apply_patch_state_init_t) { + return (0); + } + + self_p->compression = dumped.compression; + self_p->patch_offset = dumped.patch_offset; + self_p->to_offset = dumped.to_offset; + self_p->to_size = dumped.to_size; + self_p->from_offset = dumped.from_offset; + self_p->chunk_size = dumped.chunk_size; + + res = self_p->from_seek(self_p->arg_p, self_p->from_offset); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + return (patch_reader_restore(&self_p->patch_reader, + &dumped.patch_reader, + &self_p->chunk, + self_p->compression, + state_read)); +} + +size_t detools_apply_patch_get_patch_offset(struct detools_apply_patch_t *self_p) +{ + return (self_p->patch_offset); +} + +size_t detools_apply_patch_get_to_offset(struct detools_apply_patch_t *self_p) +{ + return (self_p->to_offset); +} + +int detools_apply_patch_process(struct detools_apply_patch_t *self_p, + const uint8_t *patch_p, + size_t size) +{ + int res; + + res = 0; + self_p->patch_offset += size; + self_p->chunk.buf_p = patch_p; + self_p->chunk.size = size; + self_p->chunk.offset = 0; + + while (chunk_available(&self_p->chunk) && (res >= 0)) { + res = apply_patch_process_once(self_p); + } + + if ((res == 1) || (res == -DETOOLS_ALREADY_DONE)) { + res = 0; + } + + return (res); +} + +int detools_apply_patch_finalize(struct detools_apply_patch_t *self_p) +{ + int res; + + self_p->chunk.size = 0; + self_p->chunk.offset = 0; + + do { + res = apply_patch_process_once(self_p); + } while (res == 0); + + return (apply_patch_common_finalize(res, + &self_p->patch_reader, + self_p->to_size)); +} + +/* + * Low level in-place patch type functionality. + */ + +static int in_place_all_steps_completed(struct detools_apply_patch_in_place_t *self_p) +{ + int res; + + res = 0; + + if (self_p->step_set != NULL) { + res = self_p->step_set(self_p->arg_p, 0); + + if (res != 0) { + res = -DETOOLS_STEP_SET_FAILED; + } + } + + return (res); +} + +static int in_place_is_step_completed(struct detools_apply_patch_in_place_t *self_p, + bool *res_p) +{ + int res; + int completed_step; + + if (self_p->step_get != NULL) { + res = self_p->step_get(self_p->arg_p, &completed_step); + + if (res != 0) { + return (-DETOOLS_STEP_GET_FAILED); + } + + *res_p = (self_p->ongoing_step <= completed_step); + } else { + *res_p = false; + } + + return (0); +} + +static int in_place_next_step(struct detools_apply_patch_in_place_t *self_p) +{ + int res; + bool is_step_completed; + + res = 0; + + if (self_p->step_set != NULL) { + res = in_place_is_step_completed(self_p, &is_step_completed); + + if (res != 0) { + return (res); + } + + if (!is_step_completed) { + res = self_p->step_set(self_p->arg_p, self_p->ongoing_step); + + if (res != 0) { + res = -DETOOLS_STEP_SET_FAILED; + } + } + } + + self_p->ongoing_step++; + + return (res); +} + +static int in_place_mem_read(struct detools_apply_patch_in_place_t *self_p, + void *dst_p, + uintptr_t src, + size_t size) +{ + int res; + bool is_step_completed; + + res = in_place_is_step_completed(self_p, &is_step_completed); + + if (res != 0) { + return (res); + } + + if (!is_step_completed) { + return (self_p->mem_read(self_p->arg_p, dst_p, src, size)); + } else { + memset(dst_p, 0, size); + + return (0); + } +} + +static int in_place_mem_write(struct detools_apply_patch_in_place_t *self_p, + uintptr_t dst, + void *src_p, + size_t size) +{ + int res; + bool is_step_completed; + + res = in_place_is_step_completed(self_p, &is_step_completed); + + if (res != 0) { + return (res); + } + + if (!is_step_completed) { + return (self_p->mem_write(self_p->arg_p, dst, src_p, size)); + } else { + return (0); + } +} + +static int in_place_mem_erase(struct detools_apply_patch_in_place_t *self_p, + uintptr_t addr, + size_t size) +{ + int res; + bool is_step_completed; + + res = in_place_is_step_completed(self_p, &is_step_completed); + + if (res != 0) { + return (res); + } + + if (!is_step_completed) { + return (self_p->mem_erase(self_p->arg_p, addr, size)); + } else { + return (0); + } +} + +static int in_place_shift_memory(struct detools_apply_patch_in_place_t *self_p, + size_t memory_size, + size_t from_size) +{ + size_t i; + size_t number_of_segments; + int res; + size_t read_address; + size_t write_address; + uint8_t buf[128]; + size_t offset; + size_t size; + + number_of_segments = DIV_CEIL(MIN(from_size, memory_size - self_p->shift_size), + self_p->segment_size); + read_address = ((number_of_segments - 1) * self_p->segment_size); + write_address = (read_address + self_p->shift_size); + + for (i = 0; i < number_of_segments; i++) { + /* Erase segment to write to. */ + res = in_place_mem_erase(self_p, + write_address, + self_p->segment_size); + + if (res != 0) { + return (res); + } + + /* Copy data to erased segment. */ + offset = 0; + + while (offset < self_p->segment_size) { + size = MIN(sizeof(buf), self_p->segment_size - offset); + res = in_place_mem_read(self_p, + &buf[0], + read_address + offset, + size); + + if (res != 0) { + return (res); + } + + res = in_place_mem_write(self_p, + write_address + offset, + &buf[0], + size); + + if (res != 0) { + return (res); + } + + offset += size; + } + + res = in_place_next_step(self_p); + + if (res != 0) { + return (res); + } + + write_address -= self_p->segment_size; + read_address -= self_p->segment_size; + } + + return (0); +} + +static int in_place_process_init_fixed_header( + struct detools_apply_patch_in_place_t *self_p) +{ + int patch_type; + uint8_t byte; + + if (chunk_get(&self_p->chunk, &byte) != 0) { + return (-DETOOLS_SHORT_HEADER); + } + + patch_type = ((byte >> 4) & 0x7); + self_p->compression = (byte & 0xf); + + if (patch_type != PATCH_TYPE_IN_PLACE) { + return (-DETOOLS_BAD_PATCH_TYPE); + } + + self_p->init_state = detools_apply_patch_in_place_init_state_memory_size_t; + self_p->size.state = detools_unpack_usize_state_first_t; + + return (0); +} + +static int in_place_process_init_memory_size( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int memory_size; + + res = chunk_unpack_header_size(&self_p->chunk, &self_p->size, &memory_size); + + if (res != 0) { + return (res); + } + + self_p->memory_size = (size_t)memory_size; + self_p->init_state = detools_apply_patch_in_place_init_state_segment_size_t; + self_p->size.state = detools_unpack_usize_state_first_t; + + return (0); +} + +static int in_place_process_init_segment_size( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int segment_size; + + res = chunk_unpack_header_size(&self_p->chunk, &self_p->size, &segment_size); + + if (res != 0) { + return (res); + } + + self_p->segment_size = (size_t)segment_size; + self_p->init_state = detools_apply_patch_in_place_init_state_shift_size_t; + self_p->size.state = detools_unpack_usize_state_first_t; + + return (0); +} + +static int in_place_process_init_shift_size( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int shift_size; + + res = chunk_unpack_header_size(&self_p->chunk, &self_p->size, &shift_size); + + if (res != 0) { + return (res); + } + + self_p->shift_size = (size_t)shift_size; + self_p->init_state = detools_apply_patch_in_place_init_state_from_size_t; + self_p->size.state = detools_unpack_usize_state_first_t; + + return (0); +} + +static int in_place_process_init_from_size( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int from_size; + + res = chunk_unpack_header_size(&self_p->chunk, &self_p->size, &from_size); + + if (res != 0) { + return (res); + } + + self_p->from_size = (size_t)from_size; + self_p->init_state = detools_apply_patch_in_place_init_state_to_size_t; + self_p->size.state = detools_unpack_usize_state_first_t; + + return (0); +} + +static int in_place_process_init_to_size( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int to_size; + + res = chunk_unpack_header_size(&self_p->chunk, &self_p->size, &to_size); + + if (res != 0) { + return (res); + } + + res = patch_reader_init(&self_p->patch_reader, + &self_p->chunk, + self_p->patch_size - self_p->chunk.offset, + self_p->compression); + + if (res != 0) { + return (res); + } + + if (to_size < 0) { + return (-DETOOLS_CORRUPT_PATCH); + } + + self_p->to_pos = 0; + self_p->to_size = (size_t)to_size; + self_p->segment.index = 0; + + if (to_size > 0) { + res = in_place_shift_memory(self_p, + self_p->memory_size, + self_p->from_size); + + if (res != 0) { + return (res); + } + + self_p->state = detools_apply_patch_state_dfpatch_size_t; + } else { + self_p->state = detools_apply_patch_state_done_t; + } + + return (res); +} + +static int in_place_process_init(struct detools_apply_patch_in_place_t *self_p) +{ + int res; + + switch (self_p->init_state) { + + case detools_apply_patch_in_place_init_state_fixed_header_t: + res = in_place_process_init_fixed_header(self_p); + break; + + case detools_apply_patch_in_place_init_state_memory_size_t: + res = in_place_process_init_memory_size(self_p); + break; + + case detools_apply_patch_in_place_init_state_segment_size_t: + res = in_place_process_init_segment_size(self_p); + break; + + case detools_apply_patch_in_place_init_state_shift_size_t: + res = in_place_process_init_shift_size(self_p); + break; + + case detools_apply_patch_in_place_init_state_from_size_t: + res = in_place_process_init_from_size(self_p); + break; + + case detools_apply_patch_in_place_init_state_to_size_t: + res = in_place_process_init_to_size(self_p); + break; + + default: + res = -DETOOLS_INTERNAL_ERROR; + break; + } + + return (res); +} + +static int in_place_process_dfpatch_size( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int size; + + res = patch_reader_unpack_size(&self_p->patch_reader, &size); + + if (res != 0) { + return (res); + } + + if (size > 0) { + return (-DETOOLS_NOT_IMPLEMENTED); + } + + self_p->state = detools_apply_patch_state_diff_size_t; + self_p->segment.from_offset = + (int)MAX(self_p->segment_size * (self_p->segment.index + 1), + self_p->shift_size); + self_p->segment.to_offset = (self_p->segment.index * self_p->segment_size); + self_p->segment.to_size = MIN(self_p->segment_size, + self_p->to_size - self_p->segment.to_offset); + self_p->segment.to_pos = 0; + self_p->segment.index++; + + return (in_place_mem_erase(self_p, + self_p->segment.to_offset, + self_p->segment.to_size)); +} + +static int in_place_process_size(struct detools_apply_patch_in_place_t *self_p, + enum detools_apply_patch_state_t next_state) +{ + int res; + int size; + + res = common_process_size(&self_p->patch_reader, + self_p->to_pos, + self_p->to_size, + &size); + + if (res != 0) { + return (res); + } + + self_p->state = next_state; + self_p->chunk_size = (size_t)size; + + return (0); +} + +static int in_place_process_data(struct detools_apply_patch_in_place_t *self_p, + enum detools_apply_patch_state_t next_state) +{ + int res; + size_t i; + uint8_t to[128]; + size_t to_size; + uint8_t from[128]; + + to_size = MIN(sizeof(to), self_p->chunk_size); + + if (to_size == 0) { + self_p->state = next_state; + + return (0); + } + + res = patch_reader_decompress(&self_p->patch_reader, + &to[0], + &to_size); + + if (res != 0) { + return (res); + } + + if (next_state == detools_apply_patch_state_extra_size_t) { + res = in_place_mem_read(self_p, + &from[0], + (size_t)self_p->segment.from_offset, + to_size); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + self_p->segment.from_offset += (int)to_size; + + for (i = 0; i < to_size; i++) { + to[i] = (uint8_t)(to[i] + from[i]); + } + } + + res = in_place_mem_write(self_p, + self_p->segment.to_pos + self_p->segment.to_offset, + &to[0], + to_size); + + if (res != 0) { + return (-DETOOLS_IO_FAILED); + } + + self_p->to_pos += to_size; + self_p->segment.to_pos += to_size; + self_p->chunk_size -= to_size; + + return (res); +} + +static int in_place_process_diff_size(struct detools_apply_patch_in_place_t *self_p) +{ + return (in_place_process_size(self_p, detools_apply_patch_state_diff_data_t)); +} + +static int in_place_process_diff_data(struct detools_apply_patch_in_place_t *self_p) +{ + return (in_place_process_data(self_p, detools_apply_patch_state_extra_size_t)); +} + +static int in_place_process_extra_size(struct detools_apply_patch_in_place_t *self_p) +{ + return (in_place_process_size(self_p, detools_apply_patch_state_extra_data_t)); +} + +static int in_place_process_extra_data(struct detools_apply_patch_in_place_t *self_p) +{ + return (in_place_process_data(self_p, detools_apply_patch_state_adjustment_t)); +} + +static int in_place_process_adjustment(struct detools_apply_patch_in_place_t *self_p) +{ + int res; + int offset; + + res = patch_reader_unpack_size(&self_p->patch_reader, &offset); + + if (res != 0) { + return (res); + } + + if (self_p->to_pos == self_p->to_size) { + res = in_place_all_steps_completed(self_p); + self_p->state = detools_apply_patch_state_done_t; + } else if (self_p->segment.to_pos == self_p->segment.to_size) { + res = in_place_next_step(self_p); + self_p->state = detools_apply_patch_state_dfpatch_size_t; + } else { + self_p->segment.from_offset += offset; + self_p->state = detools_apply_patch_state_diff_size_t; + } + + return (res); +} + +static int apply_patch_in_place_process_once( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + + switch (self_p->state) { + + case detools_apply_patch_state_init_t: + res = in_place_process_init(self_p); + break; + + case detools_apply_patch_state_dfpatch_size_t: + res = in_place_process_dfpatch_size(self_p); + break; + + case detools_apply_patch_state_diff_size_t: + res = in_place_process_diff_size(self_p); + break; + + case detools_apply_patch_state_diff_data_t: + res = in_place_process_diff_data(self_p); + break; + + case detools_apply_patch_state_extra_size_t: + res = in_place_process_extra_size(self_p); + break; + + case detools_apply_patch_state_extra_data_t: + res = in_place_process_extra_data(self_p); + break; + + case detools_apply_patch_state_adjustment_t: + res = in_place_process_adjustment(self_p); + break; + + case detools_apply_patch_state_done_t: + return (-DETOOLS_ALREADY_DONE); + + case detools_apply_patch_state_failed_t: + res = -DETOOLS_ALREADY_FAILED; + break; + + default: + res = -DETOOLS_INTERNAL_ERROR; + break; + } + + if (res < 0) { + self_p->state = detools_apply_patch_state_failed_t; + } + + return (res); +} + +int detools_apply_patch_in_place_init( + struct detools_apply_patch_in_place_t *self_p, + detools_mem_read_t mem_read, + detools_mem_write_t mem_write, + detools_mem_erase_t mem_erase, + detools_step_set_t step_set, + detools_step_get_t step_get, + size_t patch_size, + void *arg_p) +{ + self_p->mem_read = mem_read; + self_p->mem_write = mem_write; + self_p->mem_erase = mem_erase; + self_p->step_set = step_set; + self_p->step_get = step_get; + self_p->patch_size = patch_size; + self_p->arg_p = arg_p; + self_p->state = detools_apply_patch_state_init_t; + self_p->ongoing_step = 1; + self_p->init_state = detools_apply_patch_in_place_init_state_fixed_header_t; + self_p->patch_reader.destroy = NULL; + + return (0); +} + +int detools_apply_patch_in_place_process( + struct detools_apply_patch_in_place_t *self_p, + const uint8_t *patch_p, + size_t size) +{ + int res; + + res = 0; + self_p->chunk.buf_p = patch_p; + self_p->chunk.size = size; + self_p->chunk.offset = 0; + + while (chunk_available(&self_p->chunk) && (res >= 0)) { + res = apply_patch_in_place_process_once(self_p); + } + + if ((res == 1) || (res == -DETOOLS_ALREADY_DONE)) { + res = 0; + } + + return (res); +} + +int detools_apply_patch_in_place_finalize( + struct detools_apply_patch_in_place_t *self_p) +{ + int res; + + self_p->chunk.size = 0; + self_p->chunk.offset = 0; + + do { + res = apply_patch_in_place_process_once(self_p); + } while (res == 0); + + return (apply_patch_common_finalize(res, + &self_p->patch_reader, + self_p->to_size)); +} + +/* + * Callback functionality. + */ + +static int callbacks_process(struct detools_apply_patch_t *apply_patch_p, + detools_read_t patch_read, + size_t patch_size, + void *arg_p) +{ + int res; + size_t patch_offset; + size_t chunk_size; + uint8_t chunk[512]; + + res = 0; + patch_offset = 0; + + while ((patch_offset < patch_size) && (res == 0)) { + chunk_size = MIN(patch_size - patch_offset, 512); + res = patch_read(arg_p, &chunk[0], chunk_size); + + if (res == 0) { + res = detools_apply_patch_process(apply_patch_p, + &chunk[0], + chunk_size); + patch_offset += chunk_size; + } else { + res = -DETOOLS_IO_FAILED; + } + } + + if (res == 0) { + res = detools_apply_patch_finalize(apply_patch_p); + } else { + (void)detools_apply_patch_finalize(apply_patch_p); + } + + return (res); +} + +int detools_apply_patch_callbacks(detools_read_t from_read, + detools_seek_t from_seek, + detools_read_t patch_read, + size_t patch_size, + detools_write_t to_write, + void *arg_p) +{ + int res; + struct detools_apply_patch_t apply_patch; + + res = detools_apply_patch_init(&apply_patch, + from_read, + from_seek, + patch_size, + to_write, + arg_p); + + if (res != 0) { + return (res); + } + + return (callbacks_process(&apply_patch, patch_read, patch_size, arg_p)); +} + +static int in_place_callbacks_process( + struct detools_apply_patch_in_place_t *apply_patch_p, + detools_read_t patch_read, + size_t patch_size, + void *arg_p) +{ + int res; + size_t patch_offset; + size_t chunk_size; + uint8_t chunk[512]; + + res = 0; + patch_offset = 0; + + while ((patch_offset < patch_size) && (res == 0)) { + chunk_size = MIN(patch_size - patch_offset, 512); + res = patch_read(arg_p, &chunk[0], chunk_size); + + if (res == 0) { + res = detools_apply_patch_in_place_process(apply_patch_p, + &chunk[0], + chunk_size); + patch_offset += chunk_size; + } else { + res = -DETOOLS_IO_FAILED; + } + } + + if (res == 0) { + res = detools_apply_patch_in_place_finalize(apply_patch_p); + } else { + (void)detools_apply_patch_in_place_finalize(apply_patch_p); + } + + return (res); +} + +int detools_apply_patch_in_place_callbacks(detools_mem_read_t mem_read, + detools_mem_write_t mem_write, + detools_mem_erase_t mem_erase, + detools_step_set_t step_set, + detools_step_get_t step_get, + detools_read_t patch_read, + size_t patch_size, + void *arg_p) +{ + int res; + struct detools_apply_patch_in_place_t apply_patch; + + res = detools_apply_patch_in_place_init(&apply_patch, + mem_read, + mem_write, + mem_erase, + step_set, + step_get, + patch_size, + arg_p); + + if (res != 0) { + return (res); + } + + return (in_place_callbacks_process(&apply_patch, + patch_read, + patch_size, + arg_p)); +} + +/* + * File io functionality. + */ + +#if DETOOLS_CONFIG_FILE_IO == 1 + +struct file_io_t { + FILE *ffrom_p; + FILE *fpatch_p; + FILE *fto_p; +}; + +static int file_size(FILE *file_p, size_t *size_p) +{ + int res; + long size; + + res = fseek(file_p, 0, SEEK_END); + + if (res != 0) { + return (-DETOOLS_FILE_SEEK_FAILED); + } + + size = ftell(file_p); + + if (size <= 0) { + return (-DETOOLS_FILE_TELL_FAILED); + } + + *size_p = (size_t)size; + + res = fseek(file_p, 0, SEEK_SET); + + if (res != 0) { + return (-DETOOLS_FILE_SEEK_FAILED); + } + + return (res); +} + +static int file_io_init(struct file_io_t *self_p, + const char *from_p, + const char *patch_p, + const char *to_p, + size_t *patch_size_p) +{ + int res; + FILE *file_p; + + res = -DETOOLS_FILE_OPEN_FAILED; + + /* From. */ + file_p = fopen(from_p, "rb"); + + if (file_p == NULL) { + return (res); + } + + self_p->ffrom_p = file_p; + + /* To. */ + file_p = fopen(to_p, "wb"); + + if (file_p == NULL) { + goto err1; + } + + self_p->fto_p = file_p; + + /* Patch. */ + file_p = fopen(patch_p, "rb"); + + if (file_p == NULL) { + goto err2; + } + + self_p->fpatch_p = file_p; + res = file_size(self_p->fpatch_p, patch_size_p); + + if (res != 0) { + goto err3; + } + + return (res); + + err3: + fclose(self_p->fpatch_p); + + err2: + fclose(self_p->fto_p); + + err1: + fclose(self_p->ffrom_p); + + return (res); +} + +static int file_io_cleanup(struct file_io_t *self_p) +{ + int res; + int res2; + int res3; + + res = fclose(self_p->ffrom_p); + res2 = fclose(self_p->fto_p); + res3 = fclose(self_p->fpatch_p); + + if ((res != 0) || (res2 != 0) || (res3 != 0)) { + res = -DETOOLS_FILE_CLOSE_FAILED; + } + + return (res); +} + +static int file_io_read(FILE *file_p, uint8_t *buf_p, size_t size) +{ + int res; + + res = 0; + + if (size > 0) { + if (fread(buf_p, size, 1, file_p) != 1) { + res = -DETOOLS_FILE_READ_FAILED; + } + } + + return (res); +} + +static int file_io_from_read(void *arg_p, uint8_t *buf_p, size_t size) +{ + struct file_io_t *self_p; + + self_p = (struct file_io_t *)arg_p; + + return (file_io_read(self_p->ffrom_p, buf_p, size)); +} + +static int file_io_from_seek(void *arg_p, int offset) +{ + struct file_io_t *self_p; + + self_p = (struct file_io_t *)arg_p; + + return (fseek(self_p->ffrom_p, offset, SEEK_CUR)); +} + +static int file_io_patch_read(void *arg_p, uint8_t *buf_p, size_t size) +{ + struct file_io_t *self_p; + + self_p = (struct file_io_t *)arg_p; + + return (file_io_read(self_p->fpatch_p, buf_p, size)); +} + +static int file_io_to_write(void *arg_p, const uint8_t *buf_p, size_t size) +{ + int res; + struct file_io_t *self_p; + + self_p = (struct file_io_t *)arg_p; + res = 0; + + if (size > 0) { + if (fwrite(buf_p, size, 1, self_p->fto_p) != 1) { + res = -DETOOLS_FILE_WRITE_FAILED; + } + } + + return (res); +} + +int detools_apply_patch_filenames(const char *from_p, + const char *patch_p, + const char *to_p) +{ + int res; + struct file_io_t file_io; + size_t patch_size; + + res = file_io_init(&file_io, + from_p, + patch_p, + to_p, + &patch_size); + + if (res != 0) { + return (res); + } + + res = detools_apply_patch_callbacks(file_io_from_read, + file_io_from_seek, + file_io_patch_read, + patch_size, + file_io_to_write, + &file_io); + + if (res != 0) { + goto err1; + } + + return (file_io_cleanup(&file_io)); + + err1: + (void)file_io_cleanup(&file_io); + + return (res); +} + +struct in_place_file_io_t { + FILE *fmemory_p; + FILE *fpatch_p; +}; + +static int in_place_file_io_init(struct in_place_file_io_t *self_p, + const char *memory_p, + const char *patch_p, + size_t *patch_size_p) +{ + int res; + FILE *file_p; + + res = -DETOOLS_FILE_OPEN_FAILED; + + /* Memory. */ + file_p = fopen(memory_p, "r+b"); + + if (file_p == NULL) { + return (res); + } + + self_p->fmemory_p = file_p; + + /* Patch. */ + file_p = fopen(patch_p, "rb"); + + if (file_p == NULL) { + goto err1; + } + + self_p->fpatch_p = file_p; + res = file_size(self_p->fpatch_p, patch_size_p); + + if (res != 0) { + goto err2; + } + + return (res); + + err2: + fclose(self_p->fpatch_p); + + err1: + fclose(self_p->fmemory_p); + + return (res); +} + +static int in_place_file_io_mem_read(void *arg_p, + void *dst_p, + uintptr_t src, + size_t size) +{ + int res; + struct in_place_file_io_t *self_p; + + self_p = (struct in_place_file_io_t *)arg_p; + res = 0; + + if (size > 0) { + res = fseek(self_p->fmemory_p, (int)src, SEEK_SET); + + if (res != 0) { + return (-DETOOLS_FILE_SEEK_FAILED); + } + + if (fread(dst_p, size, 1, self_p->fmemory_p) != 1) { + res = -DETOOLS_FILE_READ_FAILED; + } + } + + return (res); +} + +static int in_place_file_io_mem_write(void *arg_p, + uintptr_t dst, + void *src_p, + size_t size) +{ + int res; + struct in_place_file_io_t *self_p; + + self_p = (struct in_place_file_io_t *)arg_p; + res = 0; + + if (size > 0) { + res = fseek(self_p->fmemory_p, (int)dst, SEEK_SET); + + if (res != 0) { + return (-DETOOLS_FILE_SEEK_FAILED); + } + + if (fwrite(src_p, size, 1, self_p->fmemory_p) != 1) { + res = -DETOOLS_FILE_WRITE_FAILED; + } + } + + return (res); +} + +static int in_place_file_io_mem_erase(void *arg_p, uintptr_t addr, size_t size) +{ + (void)arg_p; + (void)addr; + (void)size; + + return (0); +} + +static int in_place_file_io_cleanup(struct in_place_file_io_t *self_p) +{ + int res; + int res2; + + res = fclose(self_p->fmemory_p); + res2 = fclose(self_p->fpatch_p); + + if ((res != 0) || (res2 != 0)) { + res = -DETOOLS_FILE_CLOSE_FAILED; + } + + return (res); +} + +int detools_apply_patch_in_place_filenames(const char *memory_p, + const char *patch_p, + detools_step_set_t step_set, + detools_step_get_t step_get) +{ + int res; + struct in_place_file_io_t file_io; + size_t patch_size; + + res = in_place_file_io_init(&file_io, + memory_p, + patch_p, + &patch_size); + + if (res != 0) { + return (res); + } + + res = detools_apply_patch_in_place_callbacks(in_place_file_io_mem_read, + in_place_file_io_mem_write, + in_place_file_io_mem_erase, + step_set, + step_get, + file_io_patch_read, + patch_size, + &file_io); + + if (res != 0) { + goto err1; + } + + return (in_place_file_io_cleanup(&file_io)); + + err1: + (void)in_place_file_io_cleanup(&file_io); + + return (res); +} + +#endif + +const char *detools_error_as_string(int error) +{ + if (error < 0) { + error *= -1; + } + + switch (error) { + + case DETOOLS_NOT_IMPLEMENTED: + return "Function not implemented."; + + case DETOOLS_NOT_DONE: + return "Not done."; + + case DETOOLS_BAD_PATCH_TYPE: + return "Bad patch type."; + + case DETOOLS_BAD_COMPRESSION: + return "Bad compression."; + + case DETOOLS_INTERNAL_ERROR: + return "Internal error."; + + case DETOOLS_LZMA_INIT: + return "LZMA init."; + + case DETOOLS_LZMA_DECODE: + return "LZMA decode."; + + case DETOOLS_OUT_OF_MEMORY: + return "Out of memory."; + + case DETOOLS_CORRUPT_PATCH: + return "Corrupt patch."; + + case DETOOLS_IO_FAILED: + return "Input/output failed."; + + case DETOOLS_ALREADY_DONE: + return "Already done."; + + case DETOOLS_FILE_OPEN_FAILED: + return "File open failed."; + + case DETOOLS_FILE_CLOSE_FAILED: + return "File close failed."; + + case DETOOLS_FILE_READ_FAILED: + return "File read failed."; + + case DETOOLS_FILE_WRITE_FAILED: + return "File write failed."; + + case DETOOLS_FILE_SEEK_FAILED: + return "File seek failed."; + + case DETOOLS_FILE_TELL_FAILED: + return "File tell failed."; + + case DETOOLS_SHORT_HEADER: + return "Short header."; + + case DETOOLS_NOT_ENOUGH_PATCH_DATA: + return "Not enough patch data."; + + case DETOOLS_HEATSHRINK_SINK: + return "Heatshrink sink."; + + case DETOOLS_HEATSHRINK_POLL: + return "Heatshrink poll."; + + case DETOOLS_STEP_SET_FAILED: + return "Step set failed."; + + case DETOOLS_STEP_GET_FAILED: + return "Step get failed."; + + case DETOOLS_ALREADY_FAILED: + return "Already failed."; + + case DETOOLS_CORRUPT_PATCH_OVERFLOW: + return "Corrupt patch, overflow."; + + case DETOOLS_CORRUPT_PATCH_CRLE_KIND: + return "Corrupt patch, CRLE kind."; + + case DETOOLS_HEATSHRINK_HEADER: + return "Heatshrink header."; + + default: + return "Unknown error."; + } +} diff --git a/src/helpers/ota/detools/detools.h b/src/helpers/ota/detools/detools.h new file mode 100644 index 0000000000..c24d9677c7 --- /dev/null +++ b/src/helpers/ota/detools/detools.h @@ -0,0 +1,639 @@ +/** + * BSD 2-Clause License + * + * Copyright (c) 2019-2020, Erik Moqvist + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef DETOOLS_H +#define DETOOLS_H + +/* + * Configuration. + * + * Define any of the defines below to 0 to disable given feature. + * + * MeshCore note: upstream defaults are all 1. We flip FILE_IO, LZMA and + * HEATSHRINK off here so this vendored copy is safe-by-default on bare-metal + * targets (no liblzma, no malloc/heatshrink, no file IO) regardless of + * build flags. MeshCore .mota deltas use --codec sequential --compression crle, + * which is fully self-contained in detools.c (NONE + CRLE only). Re-enable a + * feature by passing -DDETOOLS_CONFIG_..=1. See detools/README.meshcore.txt. + */ + +#ifndef DETOOLS_CONFIG_FILE_IO +# define DETOOLS_CONFIG_FILE_IO 0 +#endif + +#ifndef DETOOLS_CONFIG_COMPRESSION_NONE +# define DETOOLS_CONFIG_COMPRESSION_NONE 1 +#endif + +#ifndef DETOOLS_CONFIG_COMPRESSION_LZMA +# define DETOOLS_CONFIG_COMPRESSION_LZMA 0 +#endif + +#ifndef DETOOLS_CONFIG_COMPRESSION_CRLE +# define DETOOLS_CONFIG_COMPRESSION_CRLE 1 +#endif + +#ifndef DETOOLS_CONFIG_COMPRESSION_HEATSHRINK +# define DETOOLS_CONFIG_COMPRESSION_HEATSHRINK 0 +#endif + +#include +#include +#include +#include + +#define DETOOLS_VERSION "0.53.0" + +/* Error codes. */ +#define DETOOLS_OK 0 +#define DETOOLS_NOT_IMPLEMENTED 1 +#define DETOOLS_NOT_DONE 2 +#define DETOOLS_BAD_PATCH_TYPE 3 +#define DETOOLS_BAD_COMPRESSION 4 +#define DETOOLS_INTERNAL_ERROR 5 +#define DETOOLS_LZMA_INIT 6 +#define DETOOLS_LZMA_DECODE 7 +#define DETOOLS_OUT_OF_MEMORY 8 +#define DETOOLS_CORRUPT_PATCH 9 +#define DETOOLS_IO_FAILED 10 +#define DETOOLS_ALREADY_DONE 11 +#define DETOOLS_FILE_OPEN_FAILED 12 +#define DETOOLS_FILE_CLOSE_FAILED 13 +#define DETOOLS_FILE_READ_FAILED 14 +#define DETOOLS_FILE_WRITE_FAILED 15 +#define DETOOLS_FILE_SEEK_FAILED 16 +#define DETOOLS_FILE_TELL_FAILED 17 +#define DETOOLS_SHORT_HEADER 18 +#define DETOOLS_NOT_ENOUGH_PATCH_DATA 19 +#define DETOOLS_HEATSHRINK_SINK 20 +#define DETOOLS_HEATSHRINK_POLL 21 +#define DETOOLS_STEP_SET_FAILED 22 +#define DETOOLS_STEP_GET_FAILED 23 +#define DETOOLS_ALREADY_FAILED 24 +#define DETOOLS_CORRUPT_PATCH_OVERFLOW 25 +#define DETOOLS_CORRUPT_PATCH_CRLE_KIND 26 +#define DETOOLS_HEATSHRINK_HEADER 27 + +/** + * Read callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[out] buf_p Buffer to read into. + * @param[in] size Number of bytes to read. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_read_t)(void *arg_p, uint8_t *buf_p, size_t size); + +/** + * Write callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[in] buf_p Buffer to write. + * @param[in] size Number of bytes to write. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_write_t)(void *arg_p, const uint8_t *buf_p, size_t size); + +/** + * Seek from current position callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[in] offset Offset to seek to from current position. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_seek_t)(void *arg_p, int offset); + +/** + * Memory read callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[out] dst_p Buffer to read into. + * @param[in] src Address to read from. + * @param[in] size Number of bytes to read. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_mem_read_t)(void *arg_p, + void *dst_p, + uintptr_t src, + size_t size); + +/** + * Memory write callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[in] dst Address to write to. + * @param[in] addr src_p Buffer to write from. + * @param[in] size Number of bytes to write. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_mem_write_t)(void *arg_p, + uintptr_t dst, + void *src_p, + size_t size); + +/** + * Memory erase callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[in] addr Address to erase from. + * @param[in] size Number of bytes to erase. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_mem_erase_t)(void *arg_p, uintptr_t addr, size_t size); + +/** + * State read callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[out] buf_p Buffer to read into. + * @param[in] size Number of bytes to read. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_state_read_t)(void *arg_p, void *buf_p, size_t size); + +/** + * State write callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[in] buf_p Buffer to write. + * @param[in] size Number of bytes to write. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_state_write_t)(void *arg_p, const void *buf_p, size_t size); + +/** + * Step set callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[in] step Step to set. Later read by the step get callback. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_step_set_t)(void *arg_p, int step); + +/** + * Step get callback. + * + * @param[in] arg_p User data passed to detools_apply_patch_init(). + * @param[out] step_p Outputs the most recently set step by the set + * callback, or zero(0) if not yet set. + * + * @return zero(0) or negative error code. + */ +typedef int (*detools_step_get_t)(void *arg_p, int *step_p); + +struct detools_apply_patch_size_t { + int state; + int value; + int offset; + bool is_signed; +}; + +struct detools_apply_patch_patch_reader_none_t { + size_t patch_size; + size_t patch_offset; +}; + +#if DETOOLS_CONFIG_COMPRESSION_LZMA == 1 + +#include + +struct detools_apply_patch_patch_reader_lzma_t { + lzma_stream stream; + uint8_t *input_p; + uint8_t *output_p; + size_t output_size; +}; + +#endif + +#if DETOOLS_CONFIG_COMPRESSION_HEATSHRINK == 1 + +#include "heatshrink_decoder.h" + +struct detools_apply_patch_patch_reader_heatshrink_t { + int8_t window_sz2; + int8_t lookahead_sz2; + heatshrink_decoder *decoder_p; +#if HEATSHRINK_DYNAMIC_ALLOC == 0 + heatshrink_decoder decoder; +#endif +}; + +#endif + +enum detools_unpack_usize_state_t { + detools_unpack_usize_state_first_t = 0, + detools_unpack_usize_state_consecutive_t +}; + +struct detools_unpack_usize_t { + enum detools_unpack_usize_state_t state; + int value; + int offset; +}; + +enum detools_crle_state_t { + detools_crle_state_idle_t = 0, + detools_crle_state_scattered_size_t, + detools_crle_state_scattered_data_t, + detools_crle_state_repeated_repetitions_t, + detools_crle_state_repeated_data_t, + detools_crle_state_repeated_data_read_t +}; + +struct detools_apply_patch_patch_reader_crle_t { + enum detools_crle_state_t state; + union { + struct { + size_t number_of_bytes_left; + struct detools_unpack_usize_t size; + } scattered; + struct { + uint8_t value; + size_t number_of_bytes_left; + struct detools_unpack_usize_t size; + } repeated; + } kind; +}; + +struct detools_apply_patch_patch_reader_t { + struct detools_apply_patch_chunk_t *patch_chunk_p; + struct detools_apply_patch_size_t size; + union { +#if DETOOLS_CONFIG_COMPRESSION_NONE == 1 + struct detools_apply_patch_patch_reader_none_t none; +#endif +#if DETOOLS_CONFIG_COMPRESSION_LZMA == 1 + struct detools_apply_patch_patch_reader_lzma_t lzma; +#endif +#if DETOOLS_CONFIG_COMPRESSION_CRLE == 1 + struct detools_apply_patch_patch_reader_crle_t crle; +#endif +#if DETOOLS_CONFIG_COMPRESSION_HEATSHRINK == 1 + struct detools_apply_patch_patch_reader_heatshrink_t heatshrink; +#endif + } compression; + int (*destroy)(struct detools_apply_patch_patch_reader_t *self_p); + int (*decompress)(struct detools_apply_patch_patch_reader_t *self_p, + uint8_t *buf_p, + size_t *size_p); +}; + +struct detools_apply_patch_chunk_t { + const uint8_t *buf_p; + size_t size; + size_t offset; +}; + +enum detools_apply_patch_state_t { + detools_apply_patch_state_init_t = 0, + detools_apply_patch_state_dfpatch_size_t, + detools_apply_patch_state_diff_size_t, + detools_apply_patch_state_diff_data_t, + detools_apply_patch_state_extra_size_t, + detools_apply_patch_state_extra_data_t, + detools_apply_patch_state_adjustment_t, + detools_apply_patch_state_done_t, + detools_apply_patch_state_failed_t +}; + +enum detools_apply_patch_init_state_t { + detools_apply_patch_init_state_fixed_header_t = 0, + detools_apply_patch_init_state_to_size_t +}; + +/** + * The apply patch data structure. + */ +struct detools_apply_patch_t { + detools_read_t from_read; + detools_seek_t from_seek; + size_t patch_size; + detools_write_t to_write; + void *arg_p; + enum detools_apply_patch_state_t state; + enum detools_apply_patch_init_state_t init_state; + int compression; + size_t patch_offset; + size_t to_offset; + size_t to_size; + int from_offset; + size_t chunk_size; + struct detools_apply_patch_patch_reader_t patch_reader; + struct detools_apply_patch_chunk_t chunk; + struct detools_apply_patch_size_t size; +}; + +enum detools_apply_patch_in_place_init_state_t { + detools_apply_patch_in_place_init_state_fixed_header_t = 0, + detools_apply_patch_in_place_init_state_memory_size_t, + detools_apply_patch_in_place_init_state_segment_size_t, + detools_apply_patch_in_place_init_state_shift_size_t, + detools_apply_patch_in_place_init_state_from_size_t, + detools_apply_patch_in_place_init_state_to_size_t +}; + +/** + * The in-place apply patch data structure. + */ +struct detools_apply_patch_in_place_t { + detools_mem_read_t mem_read; + detools_mem_write_t mem_write; + detools_mem_erase_t mem_erase; + detools_step_set_t step_set; + detools_step_get_t step_get; + size_t patch_size; + void *arg_p; + enum detools_apply_patch_state_t state; + enum detools_apply_patch_in_place_init_state_t init_state; + int compression; + int ongoing_step; + size_t to_pos; + size_t to_size; + size_t from_size; + size_t memory_size; + size_t segment_size; + size_t shift_size; + size_t chunk_size; + struct { + size_t index; + int from_offset; + size_t to_offset; + size_t to_size; + size_t to_pos; + } segment; + struct detools_apply_patch_patch_reader_t patch_reader; + struct detools_apply_patch_chunk_t chunk; + struct detools_apply_patch_size_t size; +}; + +/** + * Initialize given apply patch object. + * + * @param[out] self_p Apply patch object to initialize. + * @param[in] from_read Callback to read from-data. + * @param[in] from_seek Callback to seek from current position in from-data. + * @param[in] patch_size Patch size in bytes. Not used if + * `detools_apply_patch_restore()` is called + * immediately after this function. + * @param[in] to_write Destination callback. + * @param[in] arg_p Argument passed to the callbacks. + * + * @return zero(0) or negative error code. + */ +int detools_apply_patch_init(struct detools_apply_patch_t *self_p, + detools_read_t from_read, + detools_seek_t from_seek, + size_t patch_size, + detools_write_t to_write, + void *arg_p); + +/** + * Dump given apply patch object state. Call + * `detools_apply_patch_restore()` to restore an apply patch object to + * the dumped state. + * + * @param[in] self_p Apply patch object to dump. + * @param[in] write Write callback. + * + * @return zero(0) or negative error code. + */ +int detools_apply_patch_dump(struct detools_apply_patch_t *self_p, + detools_state_write_t state_write); + +/** + * Restore given apply patch object to given dumped + * state. + * + * `detools_apply_patch_get_to_offset()` and + * `detools_apply_patch_get_patch_offset()` are often called after + * this function to restore the to and patch streams. + * + * @param[in,out] self_p Initialized apply patch object to restore. + * @param[in] read Callback to read the dumped state. + * + * @return zero(0) or negative error code. + */ +int detools_apply_patch_restore(struct detools_apply_patch_t *self_p, + detools_state_read_t state_read); + +/** + * Get the current to stream offset. Often used to restore the to + * stream after restore. + * + * @param[in] self_p Apply patch object. + * + * @return The current to stream offset. + */ +size_t detools_apply_patch_get_to_offset(struct detools_apply_patch_t *self_p); + +/** + * Get the current patch stream offset. Often used to restore the + * patch stream after restore. + * + * @param[in] self_p Apply patch object. + * + * @return The current patch stream offset. + */ +size_t detools_apply_patch_get_patch_offset(struct detools_apply_patch_t *self_p); + +/** + * Call this function repeatedly until all patch data has been + * processed or an error occurres. Call detools_apply_patch_finalize() + * to finalize the patching, even if an error occurred. + * + * @param[in,out] self_p Initialized apply patch object. + * @param[in] patch_p Next chunk of the patch. + * @param[in] size Patch buffer size. + * + * @return zero(0) or negative error code. + */ +int detools_apply_patch_process(struct detools_apply_patch_t *self_p, + const uint8_t *patch_p, + size_t size); + +/** + * Call once after all data has been processed to finalize the + * patching. The value returned from this function should be ignored + * if an error occurred in detools_apply_patch_process(). + * + * @param[in,out] self_p Initialized apply patch object. + * + * @return Size of to-data in bytes if the patch was applied + * successfully, or negative error code. + */ +int detools_apply_patch_finalize(struct detools_apply_patch_t *self_p); + +/** + * Initialize given in-place apply patch object. + * + * @param[out] self_p In-place apply patch object to initialize. + * @param[in] mem_read Callback to read data. + * @param[in] mem_write Callback to write data. + * @param[in] mem_erase Callback to erase data. + * @param[in] step_set Callback to set the step. + * @param[in] step_get Callback to get the step. + * @param[in] patch_size Patch size in bytes. + * @param[in] arg_p Argument passed to the callbacks. + * + * @return zero(0) or negative error code. + */ +int detools_apply_patch_in_place_init( + struct detools_apply_patch_in_place_t *self_p, + detools_mem_read_t mem_read, + detools_mem_write_t mem_write, + detools_mem_erase_t mem_erase, + detools_step_set_t step_set, + detools_step_get_t step_get, + size_t patch_size, + void *arg_p); + +/** + * Call this function repeatedly until all patch data has been + * processed or an error occurres. Call + * detools_apply_patch_in_place_finalize() to finalize the patching, + * even if an error occurred. + * + * @param[in,out] self_p Initialized apply patch object. + * @param[in] patch_p Next chunk of the patch. + * @param[in] size Patch buffer size. + * + * @return zero(0) or negative error code. + */ +int detools_apply_patch_in_place_process( + struct detools_apply_patch_in_place_t *self_p, + const uint8_t *patch_p, + size_t size); + +/** + * Call once after all data has been processed to finalize the + * patching. The value returned from this function should be ignored + * if an error occurred in detools_apply_patch_in_place_process(). + * + * @param[in,out] self_p Initialized apply patch object. + * + * @return Size of to-data in bytes if the patch was applied + * successfully, or negative error code. + */ +int detools_apply_patch_in_place_finalize( + struct detools_apply_patch_in_place_t *self_p); + +/** + * Apply given patch using read, write and seek callbacks. + * + * @param[in] from_read Source read callback. + * @param[in] from_seek Source seek callback. + * @param[in] patch_read Patch read callback. + * @param[in] patch_size Patch size in bytes. + * @param[in] to_write Destination write callback. + * @param[in] arg_p Argument passed to all callbacks. + * + * @return Size of to-data in bytes or negative error code. + */ +int detools_apply_patch_callbacks(detools_read_t from_read, + detools_seek_t from_seek, + detools_read_t patch_read, + size_t patch_size, + detools_write_t to_write, + void *arg_p); + +/** + * Apply given in-place patch using read, write and erase callbacks. + * + * @param[in] mem_read Callback to read data. + * @param[in] mem_write Callback to write data. + * @param[in] mem_erase Callback to erase data. + * @param[in] step_set Callback to set the step. + * @param[in] step_get Callback to get the step. + * @param[in] patch_read Patch read callback. + * @param[in] patch_size Patch size in bytes. + * @param[in] arg_p Argument passed to the callbacks. + * + * @return Size of to-data in bytes or negative error code. + */ +int detools_apply_patch_in_place_callbacks(detools_mem_read_t mem_read, + detools_mem_write_t mem_write, + detools_mem_erase_t mem_erase, + detools_step_set_t step_set, + detools_step_get_t step_get, + detools_read_t patch_read, + size_t patch_size, + void *arg_p); + +#if DETOOLS_CONFIG_FILE_IO == 1 + +/** + * Apply given patch file to given from file and write the output to + * given to file. + * + * @param[in] from_p Source file name. + * @param[in] patch_p Patch file name. + * @param[in] to_p Destination file name. + * + * @return Size of to-data in bytes or negative error code. + */ +int detools_apply_patch_filenames(const char *from_p, + const char *patch_p, + const char *to_p); + +/** + * Apply given patch file to given memory file. + * + * @param[in] memory_p Memory file name. + * @param[in] patch_p Patch file name. + * @param[in] step_set Callback to set the step. + * @param[in] step_get Callback to get the step. + * + * @return Size of to-data in bytes or negative error code. + */ +int detools_apply_patch_in_place_filenames(const char *memory_p, + const char *patch_p, + detools_step_set_t step_set, + detools_step_get_t step_get); + +#endif + +/** + * Get the error string for given error code. + * + * @param[in] Error code. + * + * @return Error string. + */ +const char *detools_error_as_string(int error); + +#endif diff --git a/test/mocks/SHA256.h b/test/mocks/SHA256.h index b6e551a077..56d214b193 100644 --- a/test/mocks/SHA256.h +++ b/test/mocks/SHA256.h @@ -2,13 +2,102 @@ #include #include +#include -// Mock SHA256 class for testing -// Provides minimal interface to allow Utils.cpp to compile +// Real SHA-256 for native/host tests. Mirrors the rweather/Crypto streaming API used by +// src/Utils.cpp (update / finalize-with-truncation, plus resetHMAC / finalizeHMAC), so that +// Utils::sha256(...) produces correct digests on the host and OTA merkle tests are meaningful. +// (On-device the real rweather is used instead of this mock.) class SHA256 { + uint32_t h[8]; + uint8_t buf[64]; + uint32_t buf_len; + uint64_t total_len; + uint8_t hmac_key[64]; + + static uint32_t ror(uint32_t x, uint32_t n) { return (x >> n) | (x << (32 - n)); } + + void process(const uint8_t* p) { + static const uint32_t K[64] = { + 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, + 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, + 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, + 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, + 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, + 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, + 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, + 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2}; + uint32_t w[64]; + for (int i = 0; i < 16; i++) + w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) | ((uint32_t)p[i*4+2] << 8) | p[i*4+3]; + for (int i = 16; i < 64; i++) { + uint32_t s0 = ror(w[i-15],7) ^ ror(w[i-15],18) ^ (w[i-15] >> 3); + uint32_t s1 = ror(w[i-2],17) ^ ror(w[i-2],19) ^ (w[i-2] >> 10); + w[i] = w[i-16] + s0 + w[i-7] + s1; + } + uint32_t a=h[0],b=h[1],c=h[2],d=h[3],e=h[4],f=h[5],g=h[6],hh=h[7]; + for (int i = 0; i < 64; i++) { + uint32_t S1 = ror(e,6) ^ ror(e,11) ^ ror(e,25); + uint32_t ch = (e & f) ^ ((~e) & g); + uint32_t t1 = hh + S1 + ch + K[i] + w[i]; + uint32_t S0 = ror(a,2) ^ ror(a,13) ^ ror(a,22); + uint32_t maj = (a & b) ^ (a & c) ^ (b & c); + uint32_t t2 = S0 + maj; + hh=g; g=f; f=e; e=d+t1; d=c; c=b; b=a; a=t1+t2; + } + h[0]+=a; h[1]+=b; h[2]+=c; h[3]+=d; h[4]+=e; h[5]+=f; h[6]+=g; h[7]+=hh; + } + public: - void update(const uint8_t* data, size_t len) {} - void finalize(uint8_t* hash, size_t hashLen) {} - void resetHMAC(const uint8_t* key, size_t keyLen) {} - void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* hash, size_t hashLen) {} + SHA256() { reset(); } + + void reset() { + h[0]=0x6a09e667; h[1]=0xbb67ae85; h[2]=0x3c6ef372; h[3]=0xa54ff53a; + h[4]=0x510e527f; h[5]=0x9b05688c; h[6]=0x1f83d9ab; h[7]=0x5be0cd19; + buf_len = 0; total_len = 0; + } + + void update(const uint8_t* data, size_t n) { + total_len += n; + while (n) { + size_t take = 64 - buf_len; if (take > n) take = n; + memcpy(buf + buf_len, data, take); buf_len += (uint32_t)take; data += take; n -= take; + if (buf_len == 64) { process(buf); buf_len = 0; } + } + } + + void finalize(uint8_t* out, size_t out_len) { + uint64_t bits = total_len * 8; + buf[buf_len++] = 0x80; + if (buf_len > 56) { while (buf_len < 64) buf[buf_len++] = 0; process(buf); buf_len = 0; } + while (buf_len < 56) buf[buf_len++] = 0; + for (int i = 0; i < 8; i++) buf[56 + i] = (uint8_t)(bits >> (56 - 8*i)); + process(buf); buf_len = 0; + uint8_t full[32]; + for (int i = 0; i < 8; i++) { + full[i*4] = (uint8_t)(h[i] >> 24); full[i*4+1] = (uint8_t)(h[i] >> 16); + full[i*4+2] = (uint8_t)(h[i] >> 8); full[i*4+3] = (uint8_t)(h[i]); + } + if (out_len > 32) out_len = 32; + memcpy(out, full, out_len); + } + + // Standard HMAC-SHA256 (kept correct for API parity; OTA tests don't exercise it). + void resetHMAC(const uint8_t* key, size_t keyLen) { + memset(hmac_key, 0, 64); + if (keyLen > 64) { SHA256 t; t.update(key, keyLen); t.finalize(hmac_key, 32); } + else memcpy(hmac_key, key, keyLen); + reset(); + uint8_t ipad[64]; + for (int i = 0; i < 64; i++) ipad[i] = hmac_key[i] ^ 0x36; + update(ipad, 64); + } + + void finalizeHMAC(const uint8_t* key, size_t keyLen, uint8_t* out, size_t out_len) { + (void)key; (void)keyLen; + uint8_t inner[32]; finalize(inner, 32); + uint8_t opad[64]; + for (int i = 0; i < 64; i++) opad[i] = hmac_key[i] ^ 0x5c; + reset(); update(opad, 64); update(inner, 32); finalize(out, out_len); + } }; diff --git a/test/test_ota/mota_vectors.h b/test/test_ota/mota_vectors.h new file mode 100644 index 0000000000..48a1e205d5 --- /dev/null +++ b/test/test_ota/mota_vectors.h @@ -0,0 +1,122 @@ +// AUTO-GENERATED by tools/mota/gen_vectors.py — do not edit by hand. +// Cross-check vectors: a real .mota from the reference packager (motalib.py). +#pragma once +#include + +static const uint8_t MOTA_VEC[5547] = {109,79,84,65,171,21,0,0,2,1,18,68,51,34,17,0,0,16,1,193,20,0,0,193,20,0,0,10,242,21,180,56,232,121,143,217,6,12,12,64,164,54,242,61,17,64,7,235,26,78,104,183,219,50,235,76,151,230,187,56,8,129,160,136,0,84,69,83,84,72,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,136,178,44,23,110,140,98,143,139,154,17,11,47,111,103,108,222,151,146,129,106,86,0,67,163,28,6,189,70,62,57,35,188,26,173,189,228,139,22,151,108,8,7,23,55,59,129,154,6,143,50,183,166,179,139,107,56,114,150,71,207,222,1,194,206,40,178,108,87,71,39,55,245,195,86,26,23,97,24,91,216,88,154,67,206,11,186,117,137,31,249,236,96,20,141,75,212,160,158,226,220,92,147,49,180,17,11,169,58,197,74,252,20,218,59,221,25,97,71,116,162,213,93,41,94,90,53,171,68,179,239,174,165,18,155,162,43,136,186,62,41,118,97,69,253,236,163,176,142,56,175,83,215,196,198,14,58,210,8,206,80,102,68,16,54,233,241,145,224,183,80,54,167,127,101,226,234,164,117,36,67,35,63,190,143,137,67,191,149,109,229,149,102,92,56,255,255,35,130,126,23,193,12,220,28,39,160,40,202,174,108,152,16,98,97,152,255,119,135,64,248,141,220,241,2,174,184,29,174,226,137,192,68,196,164,87,28,75,111,40,116,0,244,184,224,184,67,248,128,195,45,129,233,27,222,160,76,215,163,129,155,50,39,95,195,41,138,244,199,236,135,235,0,153,82,125,4,28,237,92,224,252,212,206,78,61,14,61,224,145,242,20,21,187,124,208,17,250,194,136,196,32,32,168,121,242,140,42,67,135,223,155,108,246,54,237,138,193,186,176,51,182,79,102,254,171,166,95,112,230,132,115,30,63,57,16,86,5,150,141,58,150,56,1,18,181,161,15,58,17,231,8,220,84,18,131,60,71,171,124,54,138,33,185,239,225,146,147,121,62,200,121,206,104,48,24,24,168,110,90,108,105,119,221,186,13,172,167,251,165,25,15,103,186,86,204,220,27,63,49,48,137,114,35,108,46,71,118,63,223,236,19,113,206,220,219,140,25,12,166,255,138,214,3,248,23,237,192,217,60,42,104,124,123,54,221,102,231,15,42,97,0,252,99,67,237,200,200,116,73,108,178,245,187,254,200,142,169,183,124,39,48,75,55,247,14,148,188,138,15,191,80,14,12,149,122,128,235,218,135,40,14,245,130,20,217,47,17,152,17,172,220,60,103,30,241,227,145,63,148,152,10,158,20,107,168,149,144,133,80,239,66,52,171,183,80,61,67,101,33,171,165,76,117,80,237,192,239,18,2,117,159,255,144,255,25,18,137,54,129,67,33,238,89,225,17,225,62,94,72,40,112,213,139,180,77,156,251,252,206,167,135,2,170,209,141,76,238,169,26,240,224,34,67,29,227,27,190,141,39,69,72,154,53,183,87,52,175,162,218,67,129,125,64,231,232,216,13,23,162,108,212,70,11,0,85,197,33,163,250,67,41,189,113,141,180,109,143,2,28,19,241,226,176,231,38,139,9,213,94,149,141,37,110,32,10,78,93,230,238,203,248,220,10,230,91,53,174,63,170,26,90,199,143,226,223,104,249,158,191,39,236,238,60,221,41,249,204,207,45,225,105,6,45,188,236,85,200,238,105,205,171,221,188,207,63,68,40,201,179,27,97,223,9,219,120,56,51,209,235,117,89,78,210,203,223,58,57,6,168,49,102,84,71,221,17,247,197,71,89,164,130,102,173,251,215,137,84,240,7,29,224,248,66,45,148,246,251,67,9,27,152,111,88,186,201,80,111,155,251,130,29,98,230,147,48,65,11,181,111,0,133,236,206,137,175,184,240,189,188,171,50,93,110,17,242,170,235,84,159,80,169,217,31,184,230,76,129,79,170,104,83,103,178,75,141,32,49,107,170,240,97,173,191,231,44,157,145,77,103,140,213,0,77,73,53,110,201,148,155,167,82,119,113,113,172,54,130,121,203,230,245,203,188,43,168,21,72,131,169,162,158,85,23,209,243,192,60,172,79,57,206,50,37,6,11,62,251,121,156,217,196,18,116,106,226,161,147,49,183,178,98,126,102,62,37,167,176,1,228,192,220,197,226,27,199,108,56,45,205,245,178,132,118,12,142,63,234,217,31,116,34,205,118,170,135,252,143,152,81,243,193,228,113,156,208,184,228,129,109,212,232,140,114,229,40,190,220,121,115,66,192,63,215,163,70,196,199,133,124,160,61,70,112,19,182,73,60,69,85,81,228,138,20,35,38,59,98,177,39,180,54,16,106,104,84,138,119,106,15,52,213,107,99,231,197,149,242,178,5,219,225,195,147,97,122,1,241,90,76,192,99,218,228,244,213,107,137,191,188,139,204,154,229,56,124,56,69,111,124,7,99,86,171,173,204,103,185,42,215,119,235,32,251,159,136,6,232,100,151,144,169,6,21,164,109,34,221,118,46,12,66,97,83,54,116,83,86,194,225,97,71,192,243,212,107,64,213,20,120,4,191,138,13,255,243,89,57,166,17,199,245,166,10,193,7,243,63,51,214,5,159,39,61,32,121,171,29,144,242,55,119,179,65,196,94,42,155,155,246,191,183,29,199,209,41,246,79,27,148,6,237,79,147,173,232,245,96,101,241,183,50,19,151,176,212,160,62,26,178,197,77,217,175,153,206,30,203,251,144,200,10,88,136,109,169,94,17,129,165,87,3,217,107,210,125,27,110,245,92,162,228,212,117,181,39,111,45,187,133,247,166,69,157,206,235,137,198,123,119,111,211,187,151,68,82,218,62,212,239,22,71,225,115,62,192,118,145,156,171,97,86,7,126,217,83,46,124,54,90,204,66,87,71,225,152,179,225,70,142,2,132,242,48,21,61,184,104,125,142,194,61,176,121,165,182,125,114,202,4,23,75,56,103,177,62,78,169,148,94,121,141,135,88,108,255,190,140,84,90,179,116,69,78,64,59,30,184,49,80,30,190,137,243,195,176,47,49,55,189,123,70,185,150,250,194,134,152,72,251,25,213,49,75,58,92,45,77,3,181,136,32,70,11,249,13,141,74,178,241,32,163,222,192,125,26,223,3,146,72,120,122,112,87,47,247,13,64,240,220,122,29,210,16,102,125,18,147,161,175,13,38,38,207,144,242,77,21,254,63,30,142,195,106,155,152,202,158,57,198,133,97,115,232,113,76,220,150,253,109,78,145,158,15,156,245,189,25,242,195,53,160,54,67,169,20,40,61,44,141,19,40,0,104,115,176,152,120,74,8,59,73,180,72,179,220,116,18,175,59,236,67,201,202,160,150,169,205,239,50,108,29,139,57,165,38,232,68,211,36,18,15,42,202,78,152,191,211,145,235,73,112,31,119,176,77,179,103,241,69,128,138,126,112,20,153,10,227,110,188,82,154,64,6,23,58,246,172,214,220,147,150,243,5,255,195,172,210,68,147,10,195,193,44,120,132,166,113,234,71,46,255,149,111,162,208,125,248,23,120,89,104,85,82,171,26,219,41,84,105,177,126,73,169,241,102,208,194,140,9,116,22,80,64,82,29,248,197,103,221,131,211,252,0,168,222,138,118,105,13,48,132,92,159,193,127,160,113,194,13,52,68,140,33,237,73,112,225,178,124,31,7,249,161,155,204,61,181,40,79,141,3,141,104,23,57,254,215,233,29,118,242,30,165,213,39,127,238,183,74,130,180,69,106,213,123,250,120,62,116,141,37,98,48,235,153,130,191,225,34,221,17,70,197,202,218,106,87,239,201,129,68,210,0,72,185,76,214,150,148,255,168,125,221,38,114,137,123,88,85,141,195,139,96,116,238,82,222,48,251,178,61,146,98,59,219,198,105,11,81,190,121,180,233,207,97,98,253,169,202,210,166,251,38,126,246,9,32,128,247,151,84,222,25,223,216,112,25,134,233,116,3,184,36,104,222,167,248,39,19,120,200,248,67,86,159,177,101,166,20,218,84,218,172,219,136,97,244,81,160,183,227,194,124,223,138,9,158,17,60,161,175,235,73,255,58,191,23,111,250,25,194,162,180,223,25,113,42,177,76,231,7,11,83,203,14,75,91,95,110,37,62,135,105,144,174,202,46,43,44,20,156,222,97,158,174,61,127,233,149,36,59,118,163,65,117,65,170,2,230,205,119,230,73,173,139,40,18,113,241,88,252,150,76,163,246,108,176,64,116,216,77,50,255,98,218,123,27,60,97,146,91,147,75,254,179,75,5,250,212,168,101,70,2,144,221,175,199,190,249,12,233,155,190,127,213,231,231,73,198,204,58,155,205,90,56,162,48,158,64,173,193,184,196,168,174,214,35,160,24,231,160,165,10,79,201,112,8,148,93,187,33,23,232,75,83,191,106,44,51,33,201,138,224,248,93,135,128,233,69,212,42,65,233,211,241,123,247,206,75,191,222,86,205,29,119,246,19,36,193,247,57,220,173,185,172,250,101,247,216,205,142,93,23,202,101,3,67,137,31,116,94,172,191,172,67,149,97,210,163,240,95,27,172,59,120,6,158,226,241,143,83,234,156,56,165,16,162,210,118,232,179,77,166,104,29,35,11,242,9,77,254,126,29,24,60,227,137,34,99,116,94,171,243,190,178,242,138,107,150,190,186,39,226,106,167,25,213,125,157,104,240,243,71,8,176,94,55,113,113,243,60,218,92,25,251,175,94,139,230,250,165,91,15,101,70,48,247,31,242,217,210,116,23,169,54,164,163,152,248,5,12,201,85,62,253,32,201,144,52,17,212,195,141,53,150,55,208,222,59,84,198,37,201,230,152,0,70,219,251,37,252,33,138,64,204,44,28,169,221,6,33,3,91,202,201,60,150,82,4,44,67,13,32,189,107,134,29,190,16,121,114,199,92,131,151,27,115,128,56,242,157,11,186,200,232,221,168,133,77,117,164,246,7,15,255,122,216,102,109,175,27,125,182,232,113,18,230,20,82,155,37,16,32,70,159,162,149,140,182,83,97,254,152,135,75,116,129,154,110,25,203,179,29,218,167,166,224,196,141,184,221,55,110,115,227,58,105,86,211,116,102,106,186,24,80,109,80,170,65,95,244,39,175,236,121,17,23,212,21,23,110,24,190,189,95,207,33,142,15,150,244,143,143,84,171,31,105,90,223,170,240,192,108,222,234,184,13,247,73,153,79,90,26,147,129,54,39,168,123,57,216,27,89,216,142,94,29,195,71,146,57,206,109,216,143,249,196,209,159,157,172,164,142,6,155,237,168,212,177,68,7,46,69,179,195,79,235,86,89,1,46,222,36,144,168,102,17,36,189,162,248,7,23,191,135,55,96,107,116,87,40,94,79,184,83,198,241,145,152,21,226,13,39,40,193,158,12,172,20,69,113,169,108,124,155,113,106,69,55,193,131,29,88,110,28,72,173,173,151,124,134,170,78,11,56,101,252,153,14,1,52,77,242,54,196,35,195,65,74,83,30,1,127,191,110,44,33,97,136,180,58,128,143,213,171,206,90,18,101,220,189,10,111,4,117,235,19,220,80,147,109,146,103,181,163,106,74,29,103,5,247,83,43,205,242,158,117,212,176,235,92,22,111,216,27,62,111,150,102,134,20,101,222,79,190,86,56,85,199,43,19,130,162,29,135,130,49,231,198,89,89,186,245,209,165,208,37,60,26,37,65,50,44,154,39,194,194,167,19,45,243,197,160,126,118,193,144,194,148,114,174,236,225,144,164,162,252,159,82,221,248,160,80,38,112,17,120,113,161,77,203,70,151,14,90,129,18,79,118,115,9,14,94,212,73,19,165,221,250,218,23,157,152,129,98,118,148,141,244,202,189,229,10,115,232,207,146,166,48,82,154,121,128,38,245,15,115,26,207,230,214,87,251,182,21,129,165,44,10,63,181,112,253,112,134,133,156,40,93,95,234,72,99,104,198,86,173,153,13,202,161,165,85,16,84,24,142,173,98,72,64,185,218,168,246,232,154,223,38,85,20,149,169,36,234,89,79,247,167,178,169,100,33,152,181,240,21,79,143,96,164,202,84,208,32,171,179,212,242,189,255,175,233,134,23,165,171,108,130,92,4,92,79,46,243,54,87,242,196,124,49,57,255,35,39,19,75,216,201,25,129,197,138,213,189,226,134,9,169,86,224,196,158,33,152,96,39,41,46,212,177,197,159,207,231,42,184,112,11,105,93,173,184,60,248,113,156,72,192,191,200,114,59,136,61,79,247,207,200,120,231,213,49,94,173,242,146,252,112,118,196,72,199,97,128,135,107,247,41,209,51,205,154,35,223,64,13,164,123,223,95,141,239,26,182,216,132,217,31,72,21,195,41,69,115,231,131,37,212,111,23,242,233,56,209,115,226,89,238,6,106,13,101,128,95,60,98,254,20,95,57,7,81,238,25,214,182,166,85,202,37,35,9,73,234,212,120,178,212,35,194,180,120,114,157,1,231,20,4,65,55,213,38,140,240,186,155,135,108,28,198,73,60,77,31,12,61,107,163,203,159,117,16,28,214,231,127,152,137,4,161,131,147,61,183,36,74,109,0,157,90,61,146,106,47,170,171,21,134,249,92,17,244,134,139,129,201,253,129,141,5,99,223,120,11,162,99,251,95,64,191,4,91,201,17,88,61,187,168,160,26,197,148,188,193,85,34,11,90,139,86,208,164,44,212,199,175,118,251,178,122,161,46,207,34,16,183,198,241,117,9,75,51,11,202,51,226,10,80,238,79,131,101,253,208,139,121,64,9,192,165,48,73,91,220,199,12,221,167,84,69,31,204,94,111,227,102,190,112,229,244,98,86,249,47,127,177,127,94,236,204,132,68,205,21,186,108,20,110,154,254,210,46,139,75,82,26,20,83,169,75,78,114,154,183,109,42,176,113,89,114,10,186,222,233,90,157,255,111,70,163,250,202,242,14,19,171,163,103,93,131,205,191,173,40,243,7,36,217,155,173,200,112,8,32,17,60,199,165,93,92,98,243,145,8,154,39,173,115,242,94,95,113,195,19,146,35,135,93,101,80,166,71,63,245,29,6,188,47,127,132,99,233,143,30,67,198,66,180,114,54,255,156,73,177,234,255,125,51,31,34,218,18,115,44,230,182,113,255,22,207,174,247,216,252,81,170,88,181,16,140,138,74,228,76,217,40,182,181,237,179,163,44,203,92,130,57,31,252,51,202,35,60,202,126,6,92,141,146,94,119,205,251,141,33,156,226,22,16,79,101,255,183,184,122,134,105,196,104,210,147,18,32,248,81,164,18,115,119,174,132,88,32,224,212,199,141,163,150,46,196,247,33,110,128,233,222,14,212,31,132,39,77,42,41,82,239,181,57,88,242,240,132,229,72,216,20,64,50,162,244,141,70,32,160,77,157,136,23,128,164,43,151,241,148,39,43,168,159,184,230,154,86,215,236,144,10,211,221,7,20,11,242,164,197,147,67,166,53,196,146,106,158,163,7,127,227,160,139,74,164,244,77,123,62,206,206,175,103,76,116,18,176,15,40,112,106,123,118,52,87,155,36,80,220,183,81,187,252,220,88,249,102,33,194,94,131,143,27,81,61,119,31,68,115,63,36,24,12,74,242,98,221,157,107,63,246,221,230,40,208,83,239,147,184,80,48,195,40,127,254,131,119,127,225,78,127,5,23,241,100,129,117,247,61,55,149,90,12,12,72,126,152,225,215,167,172,120,73,137,2,216,27,110,34,225,67,186,93,195,103,93,11,102,13,145,143,49,92,141,73,18,98,129,115,195,140,71,211,253,159,174,156,30,32,249,24,100,95,203,252,86,142,240,93,193,36,50,154,130,102,128,10,11,9,35,182,85,205,121,132,116,38,155,228,131,35,83,238,156,81,41,100,253,157,189,215,76,151,86,129,212,130,136,125,181,144,76,121,208,4,94,84,172,28,250,106,149,78,203,230,185,223,176,161,6,152,121,67,247,167,200,248,198,148,147,58,184,13,149,122,43,134,161,184,158,198,215,97,37,210,174,62,8,146,242,179,28,48,4,112,80,107,38,105,176,52,105,128,198,156,235,120,223,217,188,186,15,180,35,132,53,143,83,255,169,122,134,96,80,244,44,117,233,136,87,139,90,173,197,222,184,174,164,205,177,67,156,123,49,245,63,71,142,76,57,241,249,251,76,197,73,180,53,176,180,125,81,122,89,143,239,239,203,184,70,73,31,146,173,139,97,229,250,101,209,88,244,197,205,37,74,10,73,244,182,20,88,236,113,167,65,191,122,54,51,211,137,69,238,143,178,69,35,27,157,189,150,61,62,12,171,231,135,57,163,59,13,25,105,84,183,120,25,174,197,35,1,246,140,250,237,40,104,167,239,225,224,121,122,166,51,193,246,73,82,73,165,15,232,196,22,166,146,59,136,189,185,217,239,9,233,235,44,106,225,214,45,239,235,9,255,214,101,201,126,47,239,191,246,223,237,74,224,9,2,76,145,154,27,237,251,85,72,116,253,164,139,134,126,227,240,34,217,129,119,69,49,207,28,84,41,187,117,165,65,183,47,3,188,86,202,75,145,172,193,49,44,156,219,163,229,103,211,109,131,83,22,102,171,24,47,251,35,122,82,239,63,1,66,98,60,114,192,68,244,84,77,149,185,146,2,66,167,92,177,60,15,170,30,119,78,40,103,175,128,236,229,227,180,197,79,176,30,163,234,240,75,94,157,56,56,245,34,122,39,116,191,253,155,95,106,179,140,233,120,193,137,205,170,211,55,195,63,174,193,152,223,201,20,134,114,135,180,92,19,234,144,28,15,212,140,231,129,51,146,137,38,42,83,218,133,113,29,174,52,183,149,125,23,230,130,114,207,14,116,33,131,106,116,144,14,143,118,172,206,78,185,5,101,65,209,0,190,55,148,18,11,108,88,179,16,138,254,15,239,228,17,252,239,120,8,73,104,46,196,34,196,164,250,186,165,246,107,95,254,228,97,114,222,234,232,96,96,20,174,246,169,223,138,34,167,220,89,30,45,254,137,100,135,32,186,250,57,213,0,193,5,250,76,118,172,184,139,108,136,97,210,58,63,117,88,39,70,48,239,224,185,195,28,8,207,169,107,157,196,239,226,227,4,61,52,17,25,152,8,114,153,172,180,223,12,62,189,11,102,112,59,138,55,193,221,198,14,35,128,254,74,59,208,234,187,147,81,147,153,197,172,209,82,60,77,224,36,252,169,133,56,105,76,70,15,142,242,151,225,188,233,44,160,173,109,142,126,12,248,88,241,164,171,97,201,134,81,178,106,104,38,76,96,47,193,137,121,61,217,57,76,219,181,36,206,118,234,14,143,105,247,106,142,135,34,99,62,65,52,84,165,20,236,115,216,94,23,137,185,212,48,13,68,96,172,154,154,10,223,18,48,205,194,150,185,171,143,55,122,53,222,232,85,77,244,232,3,54,239,48,246,189,30,191,255,193,122,234,62,178,154,180,52,101,234,61,141,82,198,72,97,119,136,166,91,78,66,92,131,225,127,119,25,205,251,184,120,194,214,81,234,52,94,80,105,11,144,221,56,189,37,4,66,141,239,149,148,184,106,75,39,50,84,58,97,145,213,62,127,140,167,241,175,86,65,195,210,125,247,185,164,189,125,117,43,187,203,90,43,35,184,139,125,47,234,227,138,253,164,245,15,134,8,214,216,19,241,209,171,12,195,1,105,35,215,161,59,17,181,38,2,55,129,116,95,15,158,163,170,239,157,233,123,168,124,4,1,136,141,105,3,4,135,184,70,137,250,73,4,128,208,178,172,110,206,240,232,45,27,235,24,134,38,61,49,158,134,64,208,90,68,203,101,20,95,245,103,117,144,62,253,178,57,76,175,211,217,20,167,253,219,166,194,8,23,103,97,96,141,121,14,163,2,179,43,21,127,216,111,165,200,84,144,250,219,249,24,229,135,235,10,58,54,230,222,177,227,145,121,69,11,236,19,175,236,71,230,139,144,168,8,45,237,217,80,4,246,53,150,36,192,210,182,210,101,237,19,76,41,144,61,145,213,217,99,173,229,138,84,98,193,189,35,202,253,176,185,20,128,190,249,88,13,25,111,59,214,19,87,154,196,157,244,152,101,248,198,83,7,162,69,200,254,115,125,58,91,141,240,96,110,47,174,149,169,97,21,197,158,75,204,63,182,18,21,68,39,97,182,200,162,39,189,99,81,92,27,23,1,241,78,113,92,194,69,26,33,22,47,110,114,142,142,131,104,26,6,22,90,141,23,152,153,200,83,221,98,3,74,105,99,199,21,185,230,143,231,254,250,62,146,133,43,175,97,43,35,68,77,68,126,37,16,42,111,70,107,76,123,200,19,92,64,241,63,184,160,126,152,157,50,117,27,34,77,1,242,101,85,215,158,97,205,220,84,112,85,110,208,210,220,166,249,152,34,76,82,154,242,177,51,122,80,45,246,101,247,81,74,188,177,162,125,247,147,200,62,83,96,71,209,201,100,93,29,238,144,51,151,255,139,46,174,196,140,6,243,186,118,241,181,53,112,204,74,212,177,17,209,217,203,203,104,172,127,35,162,77,61,64,168,39,183,108,202,96,18,114,253,153,122,149,102,136,129,236,235,222,177,107,139,9,202,247,92,179,229,207,137,152,163,234,21,27,196,63,168,170,90,42,165,156,11,144,251,165,173,165,102,247,192,84,247,203,110,27,2,25,66,56,130,191,132,142,148,176,147,56,114,95,100,118,197,173,150,176,128,38,88,255,6,123,26,75,106,235,246,21,29,212,240,186,36,89,79,87,116,200,52,133,123,89,121,24,112,184,178,115,81,17,76,11,207,181,29,5,217,87,165,27,173,204,42,238,251,189,62,132,44,141,40,84,143,109,245,118,59,204,103,161,47,47,163,168,110,101,7,188,157,226,50,115,151,109,99,1,180,54,52,71,192,180,207,203,16,147,26,204,223,137,47,93,83,50,117,29,67,171,220,125,135,247,163,80,152,99,156,100,150,29,89,90,216,117,158,44,208,172,180,204,76,235,157,151,21,172,34,80,30,61,78,29,46,95,177,36,130,99,107,152,35,147,98,108,47,124,162,137,177,235,165,238,255,44,142,42,125,73,35,47,80,215,115,158,13,221,91,243,2,124,34,49,208,98,246,143,129,167,126,104,175,125,106,181,215,113,125,42,21,144,7,203,194,56,74,8,70,57,137,73,43,199,117,144,190,197,196,126,140,130,28,146,29,68,198,139,210,253,93,138,210,193,10,194,184,112,139,55,253,108,26,188,212,167,192,63,76,223,255,8,115,67,89,220,209,22,112,221,254,30,198,207,60,53,207,188,150,176,89,220,181,156,161,109,42,157,35,200,52,208,52,206,15,145,89,136,71,152,137,43,82,250,180,74,74,146,68,243,219,131,252,230,173,208,24,34,246,192,201,105,235,15,254,70,221,167,32,179,221,33,63,37,182,82,212,63,194,215,173,100,125,36,147,161,68,160,106,96,115,19,162,203,225,196,23,103,131,191,71,177,239,224,94,116,243,124,83,148,0,222,198,216,187,23,187,248,117,162,170,178,91,217,16,203,136,101,55,247,109,211,54,126,68,82,212,72,86,140,147,33,144,218,124,201,87,228,174,195,12,11,25,160,200,214,117,4,31,237,219,40,112,116,0,253,245,109,51,254,176,232,33,225,166,77,40,223,238,224,70,23,167,92,64,21,95,170,231,166,42,13,101,160,78,185,179,193,59,109,167,23,180,24,0,54,122,19,33,151,57,132,174,113,2,2,178,87,209,30,252,220,108,177,33,122,18,58,98,22,187,206,26,26,80,94,225,76,35,97,198,208,192,223,34,164,254,173,36,17,135,144,2,156,166,42,112,89,184,54,160,191,38,235,105,157,175,113,220,55,22,229,25,35,192,31,150,186,98,89,246,109,80,202,35,63,70,165,21,63,141,153,153,184,155,72,199,240,176,6,217,216,168,224,77,52,132,155,130,48,191,100,75,165,13,200,229,203,244,61,126,98,28,61,127,163,152,18,135,227,250,3,252,92,239,69,110,100,70,137,20,0,0,122,7,213,179,185,90,134,189,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118,107,52,57,54}; + +static const uint32_t MOTA_VEC_LEN = 5547; +static const uint32_t EXP_TARGET_ID = 0x11223344u; +static const uint32_t EXP_FW_VERSION = 0x01100000u; +static const uint32_t EXP_IMAGE_SIZE = 5313u; +static const uint32_t EXP_PAYLOAD_SIZE = 5313u; +static const uint32_t EXP_BLOCK_COUNT = 6u; +static const uint8_t EXP_BLOCK_SIZE_LOG2 = 10; +static const uint8_t EXP_CODEC_ID = 0; +static const uint8_t EXP_MERKLE_ROOT[4] = {242,21,180,56}; + +static const uint8_t EXP_IMAGE_HASH[32] = {232,121,143,217,6,12,12,64,164,54,242,61,17,64,7,235,26,78,104,183,219,50,235,76,151,230,187,56,8,129,160,136}; + +static const uint8_t EXP_HW_ID[32] = {84,69,83,84,72,87,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + +static const uint32_t PROOF_INDEX = 2u; +static const uint8_t PROOF_NSIB = 3; +static const uint8_t PROOF_SIBLINGS[12] = {47,111,103,108,64,78,181,132,54,247,159,221}; + +static const uint32_t T0_COUNT = 5u; +static const uint8_t T0_LEAVES[20] = {123,202,230,102,137,105,224,241,146,163,254,76,91,140,169,45,125,31,146,228}; + +static const uint8_t T0_ROOT[4] = {37,197,150,102}; + +static const uint16_t T0_POFF[5] = {0,12,24,36,48}; +static const uint8_t T0_PNSIB[5] = {3,3,3,3,1}; +static const uint8_t T0_PBLOB[52] = {137,105,224,241,76,132,127,136,125,31,146,228,123,202,230,102,76,132,127,136,125,31,146,228,91,140,169,45,113,104,86,244,125,31,146,228,146,163,254,76,113,104,86,244,125,31,146,228,32,57,180,151}; + +static const uint32_t T1_COUNT = 7u; +static const uint8_t T1_LEAVES[28] = {219,8,204,214,152,44,227,79,189,183,207,45,57,133,249,14,162,50,142,13,114,148,72,187,230,159,8,8}; + +static const uint8_t T1_ROOT[4] = {124,128,11,251}; + +static const uint16_t T1_POFF[7] = {0,12,24,36,48,60,72}; +static const uint8_t T1_PNSIB[7] = {3,3,3,3,3,3,2}; +static const uint8_t T1_PBLOB[80] = {152,44,227,79,6,192,47,1,91,61,26,208,219,8,204,214,6,192,47,1,91,61,26,208,57,133,249,14,208,126,61,254,91,61,26,208,189,183,207,45,208,126,61,254,91,61,26,208,114,148,72,187,230,159,8,8,21,58,53,95,162,50,142,13,230,159,8,8,21,58,53,95,50,200,22,34,21,58,53,95}; + +static const uint32_t T2_COUNT = 8u; +static const uint8_t T2_LEAVES[32] = {133,246,245,113,87,20,181,196,236,26,12,243,169,182,211,36,223,172,151,252,28,151,88,171,2,138,202,222,83,66,56,124}; + +static const uint8_t T2_ROOT[4] = {230,153,143,58}; + +static const uint16_t T2_POFF[8] = {0,12,24,36,48,60,72,84}; +static const uint8_t T2_PNSIB[8] = {3,3,3,3,3,3,3,3}; +static const uint8_t T2_PBLOB[96] = {87,20,181,196,185,61,131,58,188,95,217,235,133,246,245,113,185,61,131,58,188,95,217,235,169,182,211,36,9,144,114,130,188,95,217,235,236,26,12,243,9,144,114,130,188,95,217,235,28,151,88,171,182,122,229,190,175,255,207,112,223,172,151,252,182,122,229,190,175,255,207,112,83,66,56,124,132,32,7,1,175,255,207,112,2,138,202,222,132,32,7,1,175,255,207,112}; + +static const uint32_t T3_COUNT = 65u; +static const uint8_t T3_LEAVES[260] = {228,177,31,222,213,237,94,231,50,158,16,45,240,127,150,228,208,46,120,34,47,80,173,66,32,115,193,214,60,40,73,37,201,143,198,211,67,231,14,145,23,231,206,70,151,82,247,207,115,74,95,131,213,33,121,147,248,20,31,29,209,72,70,23,241,87,234,199,6,202,12,131,134,233,213,230,82,81,214,46,254,186,73,15,59,134,85,179,59,184,245,117,217,237,95,37,157,89,225,249,112,43,154,184,72,149,130,37,183,19,100,29,203,32,180,97,196,69,203,119,144,197,108,45,1,133,113,168,36,18,78,210,75,94,150,40,116,19,98,94,169,42,12,32,192,70,6,142,125,129,126,57,25,67,158,181,8,30,134,82,88,158,230,175,192,176,37,16,195,177,239,43,191,233,217,203,1,13,34,120,83,7,204,235,200,216,114,244,220,22,118,100,210,158,109,213,107,142,73,151,244,88,103,219,166,50,243,79,183,246,47,87,18,1,5,200,72,44,140,231,168,129,59,227,6,227,103,55,54,204,4,229,59,253,227,61,20,147,26,54,186,174,38,57,191,247,231,13,192,44,230,40,192,252,173,250,93,54,156,198}; + +static const uint8_t T3_ROOT[4] = {143,88,46,110}; + +static const uint16_t T3_POFF[65] = {0,28,56,84,112,140,168,196,224,252,280,308,336,364,392,420,448,476,504,532,560,588,616,644,672,700,728,756,784,812,840,868,896,924,952,980,1008,1036,1064,1092,1120,1148,1176,1204,1232,1260,1288,1316,1344,1372,1400,1428,1456,1484,1512,1540,1568,1596,1624,1652,1680,1708,1736,1764,1792}; +static const uint8_t T3_PNSIB[65] = {7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,1}; +static const uint8_t T3_PBLOB[1796] = {213,237,94,231,226,151,15,25,186,191,115,46,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,228,177,31,222,226,151,15,25,186,191,115,46,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,240,127,150,228,171,21,9,181,186,191,115,46,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,50,158,16,45,171,21,9,181,186,191,115,46,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,47,80,173,66,142,196,71,64,14,98,188,211,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,208,46,120,34,142,196,71,64,14,98,188,211,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,60,40,73,37,50,228,24,222,14,98,188,211,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,32,115,193,214,50,228,24,222,14,98,188,211,87,73,67,144,23,152,173,217,71,3,57,148,93,54,156,198,67,231,14,145,71,231,4,206,205,182,67,188,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,201,143,198,211,71,231,4,206,205,182,67,188,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,151,82,247,207,124,211,70,56,205,182,67,188,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,23,231,206,70,124,211,70,56,205,182,67,188,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,213,33,121,147,47,185,242,240,12,185,30,58,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,115,74,95,131,47,185,242,240,12,185,30,58,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,209,72,70,23,198,222,217,47,12,185,30,58,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,248,20,31,29,198,222,217,47,12,185,30,58,47,60,59,137,23,152,173,217,71,3,57,148,93,54,156,198,6,202,12,131,37,124,190,231,133,59,19,106,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,241,87,234,199,37,124,190,231,133,59,19,106,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,82,81,214,46,25,35,102,192,133,59,19,106,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,134,233,213,230,25,35,102,192,133,59,19,106,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,59,134,85,179,180,250,218,148,37,137,24,30,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,254,186,73,15,180,250,218,148,37,137,24,30,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,217,237,95,37,163,134,12,115,37,137,24,30,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,59,184,245,117,163,134,12,115,37,137,24,30,100,204,178,18,126,106,252,248,71,3,57,148,93,54,156,198,112,43,154,184,171,186,157,117,95,173,243,202,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,157,89,225,249,171,186,157,117,95,173,243,202,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,183,19,100,29,117,33,228,235,95,173,243,202,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,72,149,130,37,117,33,228,235,95,173,243,202,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,196,69,203,119,109,168,123,61,165,127,162,224,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,203,32,180,97,109,168,123,61,165,127,162,224,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,1,133,113,168,19,170,184,98,165,127,162,224,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,144,197,108,45,19,170,184,98,165,127,162,224,15,216,163,10,126,106,252,248,71,3,57,148,93,54,156,198,75,94,150,40,133,173,206,145,50,201,231,4,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,36,18,78,210,133,173,206,145,50,201,231,4,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,169,42,12,32,206,234,112,47,50,201,231,4,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,116,19,98,94,206,234,112,47,50,201,231,4,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,125,129,126,57,170,111,85,90,106,61,62,225,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,192,70,6,142,170,111,85,90,106,61,62,225,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,8,30,134,82,84,11,55,210,106,61,62,225,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,25,67,158,181,84,11,55,210,106,61,62,225,30,94,224,127,107,72,112,56,161,178,183,96,93,54,156,198,192,176,37,16,73,215,44,124,75,85,11,61,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,88,158,230,175,73,215,44,124,75,85,11,61,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,191,233,217,203,104,27,130,168,75,85,11,61,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,195,177,239,43,104,27,130,168,75,85,11,61,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,83,7,204,235,77,162,227,126,184,101,244,22,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,1,13,34,120,77,162,227,126,184,101,244,22,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,220,22,118,100,132,206,108,94,184,101,244,22,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,200,216,114,244,132,206,108,94,184,101,244,22,81,85,123,40,107,72,112,56,161,178,183,96,93,54,156,198,107,142,73,151,74,78,179,108,244,44,244,141,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,210,158,109,213,74,78,179,108,244,44,244,141,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,166,50,243,79,186,100,96,255,244,44,244,141,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,244,88,103,219,186,100,96,255,244,44,244,141,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,18,1,5,200,112,90,214,108,23,138,61,93,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,183,246,47,87,112,90,214,108,23,138,61,93,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,168,129,59,227,68,36,234,87,23,138,61,93,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,72,44,140,231,68,36,234,87,23,138,61,93,152,179,15,151,5,193,169,14,161,178,183,96,93,54,156,198,54,204,4,229,190,153,32,138,33,227,4,234,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,6,227,103,55,190,153,32,138,33,227,4,234,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,20,147,26,54,96,205,181,224,33,227,4,234,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,59,253,227,61,96,205,181,224,33,227,4,234,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,191,247,231,13,229,50,248,97,89,252,12,65,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,186,174,38,57,229,50,248,97,89,252,12,65,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,192,252,173,250,142,15,70,253,89,252,12,65,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,192,44,230,40,142,15,70,253,89,252,12,65,139,152,195,249,5,193,169,14,161,178,183,96,93,54,156,198,149,210,13,187}; + +static const uint32_t T4_COUNT = 100u; +static const uint8_t T4_LEAVES[400] = {250,6,181,225,56,207,131,195,200,254,34,10,238,105,33,132,48,50,156,13,143,158,201,222,178,194,103,10,124,149,60,141,9,94,243,101,75,219,122,125,213,33,64,40,32,228,121,213,226,253,231,144,75,135,234,131,152,214,212,106,51,145,3,183,128,183,173,26,69,67,252,15,42,219,249,19,124,72,186,210,66,55,64,53,27,111,249,47,219,212,89,93,243,63,146,136,20,157,216,177,50,200,32,211,137,241,63,21,222,236,109,140,89,124,160,1,24,252,27,231,105,182,145,139,59,254,133,183,194,117,230,133,86,39,227,203,37,170,103,5,12,76,12,151,82,192,112,121,156,72,138,44,13,141,142,83,126,173,15,67,193,11,13,252,218,226,147,150,252,127,155,49,168,56,120,149,138,173,127,54,5,91,4,203,100,103,34,42,224,120,111,134,236,40,86,53,123,141,164,71,178,229,86,75,83,180,208,56,78,46,180,59,154,244,17,9,205,174,70,75,97,138,191,59,188,95,40,182,254,210,175,49,38,68,197,58,155,82,244,130,58,135,66,250,162,37,231,120,15,232,85,246,64,60,232,16,250,85,139,178,226,21,172,135,128,162,227,198,90,175,208,53,161,165,158,250,207,38,249,195,125,26,112,252,188,142,55,227,7,88,106,227,22,155,231,103,250,222,43,181,121,239,180,119,204,205,9,203,29,142,3,40,113,191,12,53,22,240,105,201,205,146,175,25,133,29,26,65,72,143,138,248,90,133,22,232,188,178,200,127,202,39,235,197,94,167,110,82,62,235,228,215,17,17,117,54,102,46,37,22,164,40,183,159,25,55,80,182,241,239,12,185,12,200,147,241,76,90,125,48,23,173,235,75,97,85,135,96,255,21,97,103,85,13,180,129,176,168,148,246}; + +static const uint8_t T4_ROOT[4] = {169,167,185,243}; + +static const uint16_t T4_POFF[100] = {0,28,56,84,112,140,168,196,224,252,280,308,336,364,392,420,448,476,504,532,560,588,616,644,672,700,728,756,784,812,840,868,896,924,952,980,1008,1036,1064,1092,1120,1148,1176,1204,1232,1260,1288,1316,1344,1372,1400,1428,1456,1484,1512,1540,1568,1596,1624,1652,1680,1708,1736,1764,1792,1820,1848,1876,1904,1932,1960,1988,2016,2044,2072,2100,2128,2156,2184,2212,2240,2268,2296,2324,2352,2380,2408,2436,2464,2492,2520,2548,2576,2604,2632,2660,2688,2704,2720,2736}; +static const uint8_t T4_PNSIB[100] = {7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,4,4,4,4}; +static const uint8_t T4_PBLOB[2752] = {56,207,131,195,144,4,34,70,211,98,27,29,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,250,6,181,225,144,4,34,70,211,98,27,29,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,238,105,33,132,21,3,59,222,211,98,27,29,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,200,254,34,10,21,3,59,222,211,98,27,29,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,143,158,201,222,96,188,135,52,159,141,76,84,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,48,50,156,13,96,188,135,52,159,141,76,84,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,124,149,60,141,98,199,71,87,159,141,76,84,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,178,194,103,10,98,199,71,87,159,141,76,84,229,111,37,122,45,29,32,241,1,82,253,243,18,149,46,121,75,219,122,125,169,14,251,128,243,113,185,185,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,9,94,243,101,169,14,251,128,243,113,185,185,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,32,228,121,213,35,96,203,98,243,113,185,185,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,213,33,64,40,35,96,203,98,243,113,185,185,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,75,135,234,131,212,196,76,34,41,226,170,218,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,226,253,231,144,212,196,76,34,41,226,170,218,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,51,145,3,183,204,217,111,117,41,226,170,218,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,152,214,212,106,204,217,111,117,41,226,170,218,54,115,204,16,45,29,32,241,1,82,253,243,18,149,46,121,69,67,252,15,57,132,159,140,174,142,2,180,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,128,183,173,26,57,132,159,140,174,142,2,180,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,124,72,186,210,8,162,138,25,174,142,2,180,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,42,219,249,19,8,162,138,25,174,142,2,180,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,27,111,249,47,19,49,135,113,74,129,59,240,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,66,55,64,53,19,49,135,113,74,129,59,240,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,243,63,146,136,75,253,36,162,74,129,59,240,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,219,212,89,93,75,253,36,162,74,129,59,240,177,111,134,244,146,34,242,217,1,82,253,243,18,149,46,121,50,200,32,211,196,6,205,75,139,127,232,79,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,20,157,216,177,196,6,205,75,139,127,232,79,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,222,236,109,140,155,133,32,81,139,127,232,79,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,137,241,63,21,155,133,32,81,139,127,232,79,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,24,252,27,231,16,192,89,45,184,230,20,55,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,89,124,160,1,16,192,89,45,184,230,20,55,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,59,254,133,183,58,26,222,255,184,230,20,55,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,105,182,145,139,58,26,222,255,184,230,20,55,21,35,67,161,146,34,242,217,1,82,253,243,18,149,46,121,86,39,227,203,37,93,129,116,201,76,71,155,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,194,117,230,133,37,93,129,116,201,76,71,155,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,12,76,12,151,171,23,55,158,201,76,71,155,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,37,170,103,5,171,23,55,158,201,76,71,155,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,156,72,138,44,178,196,184,248,236,104,3,167,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,82,192,112,121,178,196,184,248,236,104,3,167,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,126,173,15,67,148,162,128,54,236,104,3,167,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,13,141,142,83,148,162,128,54,236,104,3,167,177,116,125,30,199,197,197,5,236,20,11,118,18,149,46,121,218,226,147,150,0,21,170,193,76,191,87,153,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,193,11,13,252,0,21,170,193,76,191,87,153,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,168,56,120,149,17,155,65,75,76,191,87,153,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,252,127,155,49,17,155,65,75,76,191,87,153,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,5,91,4,203,81,193,61,17,63,43,120,95,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,138,173,127,54,81,193,61,17,63,43,120,95,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,224,120,111,134,197,230,31,135,63,43,120,95,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,100,103,34,42,197,230,31,135,63,43,120,95,39,66,60,32,199,197,197,5,236,20,11,118,18,149,46,121,123,141,164,71,92,60,129,206,78,17,61,245,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,236,40,86,53,92,60,129,206,78,17,61,245,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,83,180,208,56,113,215,5,130,78,17,61,245,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,178,229,86,75,113,215,5,130,78,17,61,245,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,154,244,17,9,241,185,75,36,120,5,10,203,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,78,46,180,59,241,185,75,36,120,5,10,203,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,97,138,191,59,36,148,140,82,120,5,10,203,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,205,174,70,75,36,148,140,82,120,5,10,203,190,40,237,164,62,10,40,12,236,20,11,118,18,149,46,121,254,210,175,49,58,244,150,107,254,225,141,150,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,188,95,40,182,58,244,150,107,254,225,141,150,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,155,82,244,130,3,23,222,243,254,225,141,150,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,38,68,197,58,3,23,222,243,254,225,141,150,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,162,37,231,120,111,3,141,159,193,86,116,188,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,58,135,66,250,111,3,141,159,193,86,116,188,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,64,60,232,16,135,211,9,178,193,86,116,188,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,15,232,85,246,135,211,9,178,193,86,116,188,117,212,105,39,62,10,40,12,236,20,11,118,18,149,46,121,226,21,172,135,111,71,248,116,217,80,60,219,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,250,85,139,178,111,71,248,116,217,80,60,219,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,90,175,208,53,118,128,133,179,217,80,60,219,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,128,162,227,198,118,128,133,179,217,80,60,219,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,207,38,249,195,37,131,44,47,77,82,94,73,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,161,165,158,250,37,131,44,47,77,82,94,73,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,188,142,55,227,180,0,66,204,77,82,94,73,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,125,26,112,252,180,0,66,204,77,82,94,73,188,142,11,51,245,200,136,13,137,103,31,19,18,72,110,254,22,155,231,103,9,180,25,20,71,242,120,146,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,7,88,106,227,9,180,25,20,71,242,120,146,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,121,239,180,119,106,219,202,18,71,242,120,146,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,250,222,43,181,106,219,202,18,71,242,120,146,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,29,142,3,40,29,164,246,98,55,204,76,233,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,204,205,9,203,29,164,246,98,55,204,76,233,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,22,240,105,201,197,130,110,101,55,204,76,233,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,113,191,12,53,197,130,110,101,55,204,76,233,238,91,144,22,245,200,136,13,137,103,31,19,18,72,110,254,133,29,26,65,5,22,180,187,234,247,123,20,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,205,146,175,25,5,22,180,187,234,247,123,20,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,90,133,22,232,144,41,193,104,234,247,123,20,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,72,143,138,248,144,41,193,104,234,247,123,20,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,202,39,235,197,208,130,10,34,23,254,125,179,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,188,178,200,127,208,130,10,34,23,254,125,179,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,62,235,228,215,5,218,223,93,23,254,125,179,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,94,167,110,82,5,218,223,93,23,254,125,179,249,90,67,149,254,114,155,109,137,103,31,19,18,72,110,254,102,46,37,22,127,3,61,253,120,198,248,114,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,17,17,117,54,127,3,61,253,120,198,248,114,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,25,55,80,182,112,68,206,98,120,198,248,114,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,164,40,183,159,112,68,206,98,120,198,248,114,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,12,200,147,241,240,129,157,149,214,175,107,55,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,241,239,12,185,240,129,157,149,214,175,107,55,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,23,173,235,75,247,5,120,126,214,175,107,55,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,76,90,125,48,247,5,120,126,214,175,107,55,124,220,117,120,254,114,155,109,137,103,31,19,18,72,110,254,255,21,97,103,4,191,4,227,166,66,253,139,18,72,110,254,97,85,135,96,4,191,4,227,166,66,253,139,18,72,110,254,176,168,148,246,118,100,253,16,166,66,253,139,18,72,110,254,85,13,180,129,118,100,253,16,166,66,253,139,18,72,110,254}; + +static const uint32_t T5_COUNT = 255u; +static const uint8_t T5_LEAVES[1020] = {99,92,33,97,215,30,198,101,110,35,237,220,30,114,79,78,74,29,101,121,222,43,226,84,17,236,91,25,47,133,95,23,43,24,222,170,190,97,229,65,76,57,46,5,125,175,81,95,129,156,155,63,139,113,160,112,9,231,52,203,252,7,66,123,48,252,216,98,69,222,198,113,178,127,158,162,81,195,118,245,12,104,14,245,195,109,80,118,156,90,70,58,116,110,104,111,98,218,154,9,0,246,201,54,21,252,233,206,122,167,217,33,27,229,83,158,113,117,40,232,181,76,250,165,202,26,5,178,24,172,180,91,248,103,94,4,159,132,24,153,192,212,194,197,91,144,28,217,220,138,69,32,88,62,37,53,222,242,192,136,108,15,206,62,175,37,53,189,122,8,1,151,251,79,38,134,218,146,209,8,209,69,107,224,101,240,227,215,64,22,211,222,200,168,86,206,166,14,152,208,136,237,24,102,21,75,241,208,229,172,0,114,82,123,56,244,139,243,123,44,152,92,225,42,18,138,48,214,248,103,199,37,85,246,102,71,235,63,48,78,118,250,138,255,183,97,23,93,190,117,84,232,123,112,151,77,179,219,237,53,190,42,18,93,107,191,176,137,124,122,74,20,142,110,105,47,54,175,181,107,222,49,152,50,17,253,130,64,26,56,103,115,55,250,154,108,68,73,225,98,107,219,25,215,125,6,206,178,170,145,250,144,206,100,175,127,158,19,247,69,153,200,212,164,188,45,2,3,199,126,46,239,154,24,194,145,219,53,73,113,240,97,62,59,45,184,154,234,46,208,65,46,153,97,185,189,80,96,1,217,222,177,59,129,129,235,150,104,135,177,190,75,226,233,82,45,254,83,235,151,179,166,244,74,106,234,100,179,159,56,71,63,37,219,60,174,72,210,203,202,117,29,49,129,193,83,137,16,54,223,126,253,200,222,108,14,42,75,153,9,237,246,91,48,20,82,162,31,204,182,70,123,170,142,148,194,92,104,120,163,114,162,123,143,133,175,134,57,34,19,231,207,204,211,133,58,121,174,231,52,136,250,88,177,238,169,212,146,91,241,161,135,170,251,202,84,228,237,231,18,165,178,89,96,218,135,59,199,248,202,177,222,234,229,141,108,120,41,139,215,36,65,173,224,128,97,225,251,106,100,234,202,236,222,20,34,145,135,162,162,186,253,245,44,87,151,161,224,84,113,162,206,100,154,175,183,109,12,155,201,93,121,19,131,120,4,60,73,19,222,160,237,167,13,195,180,108,243,228,36,28,72,216,0,231,211,112,6,34,247,115,50,7,25,79,120,224,173,218,66,44,6,104,153,101,176,42,94,72,63,198,240,243,34,3,7,51,230,53,223,243,118,3,198,52,105,9,172,174,40,34,202,177,224,62,9,73,147,168,22,37,254,205,200,135,62,227,125,114,160,12,107,0,103,126,218,5,89,61,218,9,212,71,125,200,61,131,83,235,30,236,207,185,131,237,198,225,61,208,112,102,57,123,99,71,28,42,28,25,186,35,229,158,90,56,92,133,16,82,153,52,230,163,184,45,20,64,214,172,131,94,26,249,52,201,130,242,21,132,147,240,84,64,243,51,52,187,146,246,205,26,211,105,175,61,221,209,21,230,137,88,0,176,105,144,150,137,124,228,223,2,180,169,190,150,90,170,98,205,172,40,91,71,138,242,213,140,102,41,178,241,149,113,112,82,60,194,159,187,87,136,172,169,188,177,199,28,202,9,199,205,7,162,249,44,225,132,229,226,98,158,22,105,45,129,185,103,208,200,45,189,25,202,91,179,42,213,54,2,126,142,224,63,170,137,186,245,105,74,26,77,175,88,126,216,140,18,235,136,40,156,192,38,255,28,233,255,161,39,109,33,227,82,143,178,188,149,99,100,231,41,39,133,234,183,230,240,243,133,198,56,49,198,172,6,104,36,29,157,93,82,16,201,114,111,225,187,176,227,62,90,123,100,22,147,98,176,102,96,95,228,116,217,35,147,129,183,208,131,237,195,100,89,243,185,43,17,209,44,159,179,151,84,159,167,187,80,191,118,89,121,54,37,82,106,170,109,95,140,81,224,108,165,161,252,226,92,204,207,129,169,31,4,27,80,174,208,127,241,68,233,224,38,93,233,162,74,54,164,245,145,211,22,15,219,10,211,93,159,62,71,14,45,19,225,239,69,93,176,167,66,95,75,158,79,2,221,52,115,89,137,108,87,155,209,12,100,54,71,201,170,17,132,15,135,37,31,139,240,132,99,151,51,84,187,224,195,234,66,91,87,129,171,22,20,242,205,129,26,178}; + +static const uint8_t T5_ROOT[4] = {125,131,30,129}; + +static const uint16_t T5_POFF[255] = {0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,480,512,544,576,608,640,672,704,736,768,800,832,864,896,928,960,992,1024,1056,1088,1120,1152,1184,1216,1248,1280,1312,1344,1376,1408,1440,1472,1504,1536,1568,1600,1632,1664,1696,1728,1760,1792,1824,1856,1888,1920,1952,1984,2016,2048,2080,2112,2144,2176,2208,2240,2272,2304,2336,2368,2400,2432,2464,2496,2528,2560,2592,2624,2656,2688,2720,2752,2784,2816,2848,2880,2912,2944,2976,3008,3040,3072,3104,3136,3168,3200,3232,3264,3296,3328,3360,3392,3424,3456,3488,3520,3552,3584,3616,3648,3680,3712,3744,3776,3808,3840,3872,3904,3936,3968,4000,4032,4064,4096,4128,4160,4192,4224,4256,4288,4320,4352,4384,4416,4448,4480,4512,4544,4576,4608,4640,4672,4704,4736,4768,4800,4832,4864,4896,4928,4960,4992,5024,5056,5088,5120,5152,5184,5216,5248,5280,5312,5344,5376,5408,5440,5472,5504,5536,5568,5600,5632,5664,5696,5728,5760,5792,5824,5856,5888,5920,5952,5984,6016,6048,6080,6112,6144,6176,6208,6240,6272,6304,6336,6368,6400,6432,6464,6496,6528,6560,6592,6624,6656,6688,6720,6752,6784,6816,6848,6880,6912,6944,6976,7008,7040,7072,7104,7136,7168,7200,7232,7264,7296,7328,7360,7392,7424,7456,7488,7520,7552,7584,7616,7648,7680,7712,7744,7776,7808,7840,7872,7904,7936,7968,8000,8032,8064,8096,8128}; +static const uint8_t T5_PNSIB[255] = {8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,7}; +static const uint8_t T5_PBLOB[8156] = {215,30,198,101,190,120,224,191,253,188,254,181,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,99,92,33,97,190,120,224,191,253,188,254,181,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,30,114,79,78,121,1,165,22,253,188,254,181,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,110,35,237,220,121,1,165,22,253,188,254,181,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,222,43,226,84,141,8,229,42,34,203,204,102,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,74,29,101,121,141,8,229,42,34,203,204,102,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,47,133,95,23,206,109,234,200,34,203,204,102,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,17,236,91,25,206,109,234,200,34,203,204,102,170,224,185,20,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,190,97,229,65,204,162,212,253,158,221,103,119,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,43,24,222,170,204,162,212,253,158,221,103,119,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,125,175,81,95,207,138,11,26,158,221,103,119,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,76,57,46,5,207,138,11,26,158,221,103,119,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,139,113,160,112,70,90,148,231,170,148,193,238,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,129,156,155,63,70,90,148,231,170,148,193,238,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,252,7,66,123,221,141,84,219,170,148,193,238,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,9,231,52,203,221,141,84,219,170,148,193,238,94,138,252,177,232,113,80,94,246,33,129,111,238,68,253,253,233,186,40,163,69,222,198,113,5,81,192,127,46,0,223,26,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,48,252,216,98,5,81,192,127,46,0,223,26,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,81,195,118,245,162,175,255,73,46,0,223,26,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,178,127,158,162,162,175,255,73,46,0,223,26,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,195,109,80,118,127,36,23,8,63,65,173,128,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,12,104,14,245,127,36,23,8,63,65,173,128,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,116,110,104,111,201,228,150,142,63,65,173,128,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,156,90,70,58,201,228,150,142,63,65,173,128,254,26,86,132,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,0,246,201,54,128,178,99,183,233,161,204,65,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,98,218,154,9,128,178,99,183,233,161,204,65,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,122,167,217,33,47,15,253,93,233,161,204,65,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,21,252,233,206,47,15,253,93,233,161,204,65,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,113,117,40,232,216,234,112,92,191,252,51,40,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,27,229,83,158,216,234,112,92,191,252,51,40,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,202,26,5,178,170,64,23,82,191,252,51,40,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,181,76,250,165,170,64,23,82,191,252,51,40,5,250,190,144,76,178,240,104,246,33,129,111,238,68,253,253,233,186,40,163,248,103,94,4,94,81,198,48,105,236,187,128,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,24,172,180,91,94,81,198,48,105,236,187,128,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,192,212,194,197,113,91,163,95,105,236,187,128,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,159,132,24,153,113,91,163,95,105,236,187,128,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,220,138,69,32,74,123,206,252,86,172,110,251,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,91,144,28,217,74,123,206,252,86,172,110,251,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,222,242,192,136,14,210,218,85,86,172,110,251,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,88,62,37,53,14,210,218,85,86,172,110,251,230,97,25,120,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,175,37,53,189,128,163,232,24,76,245,254,84,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,108,15,206,62,128,163,232,24,76,245,254,84,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,251,79,38,134,1,113,206,77,76,245,254,84,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,122,8,1,151,1,113,206,77,76,245,254,84,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,209,69,107,224,183,59,60,223,198,229,132,188,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,218,146,209,8,183,59,60,223,198,229,132,188,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,64,22,211,222,205,108,8,132,198,229,132,188,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,101,240,227,215,205,108,8,132,198,229,132,188,247,196,73,244,232,125,159,24,124,247,176,24,238,68,253,253,233,186,40,163,166,14,152,208,48,91,11,92,204,141,206,246,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,200,168,86,206,48,91,11,92,204,141,206,246,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,21,75,241,208,63,55,43,68,204,141,206,246,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,136,237,24,102,63,55,43,68,204,141,206,246,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,82,123,56,244,254,79,71,92,116,172,104,147,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,229,172,0,114,254,79,71,92,116,172,104,147,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,152,92,225,42,240,171,219,118,116,172,104,147,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,139,243,123,44,240,171,219,118,116,172,104,147,20,174,189,54,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,248,103,199,37,16,8,159,27,191,1,63,178,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,18,138,48,214,16,8,159,27,191,1,63,178,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,235,63,48,78,217,233,2,26,191,1,63,178,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,85,246,102,71,217,233,2,26,191,1,63,178,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,183,97,23,93,130,244,157,66,116,89,239,252,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,118,250,138,255,130,244,157,66,116,89,239,252,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,123,112,151,77,120,114,184,129,116,89,239,252,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,190,117,84,232,120,114,184,129,116,89,239,252,89,185,204,8,137,23,246,178,124,247,176,24,238,68,253,253,233,186,40,163,190,42,18,93,115,230,103,83,250,214,185,120,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,179,219,237,53,115,230,103,83,250,214,185,120,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,124,122,74,20,97,4,88,76,250,214,185,120,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,107,191,176,137,97,4,88,76,250,214,185,120,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,54,175,181,107,59,91,192,30,154,31,201,64,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,142,110,105,47,59,91,192,30,154,31,201,64,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,17,253,130,64,228,124,158,215,154,31,201,64,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,222,49,152,50,228,124,158,215,154,31,201,64,6,177,124,63,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,55,250,154,108,155,128,129,53,134,36,253,166,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,26,56,103,115,155,128,129,53,134,36,253,166,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,107,219,25,215,247,173,148,218,134,36,253,166,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,68,73,225,98,247,173,148,218,134,36,253,166,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,170,145,250,144,247,90,52,20,72,129,76,253,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,125,6,206,178,247,90,52,20,72,129,76,253,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,158,19,247,69,247,95,164,102,72,129,76,253,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,206,100,175,127,247,95,164,102,72,129,76,253,16,136,239,89,141,126,172,97,186,114,88,100,190,95,10,234,233,186,40,163,188,45,2,3,212,87,54,4,182,96,47,198,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,153,200,212,164,212,87,54,4,182,96,47,198,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,154,24,194,145,102,246,218,123,182,96,47,198,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,199,126,46,239,102,246,218,123,182,96,47,198,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,240,97,62,59,132,77,109,128,30,168,105,2,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,219,53,73,113,132,77,109,128,30,168,105,2,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,46,208,65,46,219,116,26,133,30,168,105,2,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,45,184,154,234,219,116,26,133,30,168,105,2,19,88,25,99,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,80,96,1,217,107,212,205,186,49,177,100,105,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,153,97,185,189,107,212,205,186,49,177,100,105,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,129,235,150,104,41,140,2,32,49,177,100,105,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,222,177,59,129,41,140,2,32,49,177,100,105,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,226,233,82,45,227,209,206,178,239,122,214,94,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,135,177,190,75,227,209,206,178,239,122,214,94,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,179,166,244,74,27,220,156,60,239,122,214,94,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,254,83,235,151,27,220,156,60,239,122,214,94,12,28,250,33,179,154,15,154,186,114,88,100,190,95,10,234,233,186,40,163,159,56,71,63,104,144,222,36,82,146,142,92,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,106,234,100,179,104,144,222,36,82,146,142,92,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,72,210,203,202,139,83,253,41,82,146,142,92,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,37,219,60,174,139,83,253,41,82,146,142,92,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,193,83,137,16,92,24,45,75,67,119,114,64,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,117,29,49,129,92,24,45,75,67,119,114,64,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,200,222,108,14,198,219,28,222,67,119,114,64,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,54,223,126,253,198,219,28,222,67,119,114,64,101,134,175,232,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,237,246,91,48,198,140,193,108,237,129,4,58,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,42,75,153,9,198,140,193,108,237,129,4,58,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,204,182,70,123,33,124,19,168,237,129,4,58,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,20,82,162,31,33,124,19,168,237,129,4,58,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,92,104,120,163,233,43,141,148,84,198,210,114,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,170,142,148,194,233,43,141,148,84,198,210,114,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,133,175,134,57,188,213,87,64,84,198,210,114,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,114,162,123,143,188,213,87,64,84,198,210,114,185,251,175,87,4,224,138,85,173,101,8,17,190,95,10,234,233,186,40,163,204,211,133,58,172,141,153,40,4,89,108,160,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,34,19,231,207,172,141,153,40,4,89,108,160,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,136,250,88,177,28,20,32,234,4,89,108,160,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,121,174,231,52,28,20,32,234,4,89,108,160,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,91,241,161,135,1,39,136,34,213,47,116,54,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,238,169,212,146,1,39,136,34,213,47,116,54,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,228,237,231,18,80,230,21,186,213,47,116,54,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,170,251,202,84,80,230,21,186,213,47,116,54,216,249,136,47,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,218,135,59,199,10,168,54,220,255,52,36,199,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,165,178,89,96,10,168,54,220,255,52,36,199,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,234,229,141,108,8,71,93,60,255,52,36,199,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,248,202,177,222,8,71,93,60,255,52,36,199,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,36,65,173,224,188,66,117,38,117,207,98,22,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,120,41,139,215,188,66,117,38,117,207,98,22,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,106,100,234,202,132,221,207,206,117,207,98,22,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,128,97,225,251,132,221,207,206,117,207,98,22,124,230,161,37,136,111,137,202,173,101,8,17,190,95,10,234,233,186,40,163,145,135,162,162,128,226,107,222,27,245,169,183,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,236,222,20,34,128,226,107,222,27,245,169,183,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,87,151,161,224,245,250,197,106,27,245,169,183,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,186,253,245,44,245,250,197,106,27,245,169,183,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,100,154,175,183,44,49,136,235,120,234,96,37,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,84,113,162,206,44,49,136,235,120,234,96,37,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,93,121,19,131,91,181,125,107,120,234,96,37,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,109,12,155,201,91,181,125,107,120,234,96,37,60,125,13,147,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,19,222,160,237,67,77,22,132,180,72,145,62,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,120,4,60,73,67,77,22,132,180,72,145,62,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,108,243,228,36,222,239,160,173,180,72,145,62,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,167,13,195,180,222,239,160,173,180,72,145,62,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,231,211,112,6,114,90,52,194,139,74,217,121,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,28,72,216,0,114,90,52,194,139,74,217,121,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,7,25,79,120,146,163,201,62,139,74,217,121,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,34,247,115,50,146,163,201,62,139,74,217,121,24,52,46,163,237,58,98,217,159,169,75,34,212,98,161,137,210,194,150,8,44,6,104,153,56,87,29,135,154,198,105,207,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,224,173,218,66,56,87,29,135,154,198,105,207,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,72,63,198,240,65,166,63,192,154,198,105,207,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,101,176,42,94,65,166,63,192,154,198,105,207,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,51,230,53,223,28,211,208,130,48,96,188,16,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,243,34,3,7,28,211,208,130,48,96,188,16,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,52,105,9,172,54,211,161,218,48,96,188,16,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,243,118,3,198,54,211,161,218,48,96,188,16,151,164,231,88,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,177,224,62,9,59,205,5,114,107,174,125,168,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,174,40,34,202,59,205,5,114,107,174,125,168,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,37,254,205,200,44,209,46,252,107,174,125,168,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,73,147,168,22,44,209,46,252,107,174,125,168,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,114,160,12,107,235,25,189,36,233,112,219,6,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,135,62,227,125,235,25,189,36,233,112,219,6,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,5,89,61,218,90,114,53,227,233,112,219,6,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,0,103,126,218,90,114,53,227,233,112,219,6,56,155,1,160,37,0,202,214,159,169,75,34,212,98,161,137,210,194,150,8,200,61,131,83,40,49,219,14,254,162,186,146,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,9,212,71,125,40,49,219,14,254,162,186,146,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,185,131,237,198,15,92,26,171,254,162,186,146,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,235,30,236,207,15,92,26,171,254,162,186,146,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,102,57,123,99,144,130,249,100,143,201,181,210,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,225,61,208,112,144,130,249,100,143,201,181,210,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,25,186,35,229,160,164,55,211,143,201,181,210,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,71,28,42,28,160,164,55,211,143,201,181,210,250,26,34,183,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,133,16,82,153,87,90,43,2,165,219,251,178,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,158,90,56,92,87,90,43,2,165,219,251,178,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,45,20,64,214,230,188,131,193,165,219,251,178,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,52,230,163,184,230,188,131,193,165,219,251,178,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,249,52,201,130,77,104,95,217,211,245,148,89,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,172,131,94,26,77,104,95,217,211,245,148,89,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,240,84,64,243,170,132,164,23,211,245,148,89,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,242,21,132,147,170,132,164,23,211,245,148,89,104,250,180,234,72,63,60,250,171,254,193,32,212,98,161,137,210,194,150,8,246,205,26,211,15,174,31,185,55,209,133,81,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,51,52,187,146,15,174,31,185,55,209,133,81,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,209,21,230,137,138,228,144,223,55,209,133,81,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,105,175,61,221,138,228,144,223,55,209,133,81,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,144,150,137,124,174,229,97,187,146,63,122,179,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,88,0,176,105,174,229,97,187,146,63,122,179,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,169,190,150,90,126,224,170,11,146,63,122,179,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,228,223,2,180,126,224,170,11,146,63,122,179,85,74,56,148,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,40,91,71,138,119,136,244,54,130,225,71,203,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,170,98,205,172,119,136,244,54,130,225,71,203,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,41,178,241,149,28,208,76,222,130,225,71,203,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,242,213,140,102,28,208,76,222,130,225,71,203,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,194,159,187,87,57,72,39,59,255,196,49,66,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,113,112,82,60,57,72,39,59,255,196,49,66,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,177,199,28,202,169,232,218,239,255,196,49,66,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,136,172,169,188,169,232,218,239,255,196,49,66,216,9,61,7,53,81,124,127,171,254,193,32,212,98,161,137,210,194,150,8,162,249,44,225,249,165,132,202,234,33,63,146,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,9,199,205,7,249,165,132,202,234,33,63,146,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,158,22,105,45,34,213,230,145,234,33,63,146,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,132,229,226,98,34,213,230,145,234,33,63,146,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,200,45,189,25,171,54,42,114,248,169,247,187,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,129,185,103,208,171,54,42,114,248,169,247,187,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,213,54,2,126,251,67,226,96,248,169,247,187,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,202,91,179,42,251,67,226,96,248,169,247,187,60,224,153,145,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,137,186,245,105,144,131,210,251,112,70,110,179,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,142,224,63,170,144,131,210,251,112,70,110,179,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,88,126,216,140,30,32,172,94,112,70,110,179,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,74,26,77,175,30,32,172,94,112,70,110,179,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,156,192,38,255,33,149,144,107,38,82,1,94,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,18,235,136,40,33,149,144,107,38,82,1,94,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,39,109,33,227,5,87,237,247,38,82,1,94,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,28,233,255,161,5,87,237,247,38,82,1,94,241,127,216,216,218,21,199,202,42,98,34,4,214,10,216,155,210,194,150,8,149,99,100,231,97,200,185,0,114,180,59,25,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,82,143,178,188,97,200,185,0,114,180,59,25,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,183,230,240,243,50,21,131,25,114,180,59,25,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,41,39,133,234,50,21,131,25,114,180,59,25,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,198,172,6,104,123,204,219,26,132,238,138,31,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,133,198,56,49,123,204,219,26,132,238,138,31,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,82,16,201,114,49,236,17,124,132,238,138,31,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,36,29,157,93,49,236,17,124,132,238,138,31,239,193,60,24,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,227,62,90,123,221,253,182,84,222,147,38,189,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,111,225,187,176,221,253,182,84,222,147,38,189,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,176,102,96,95,99,160,74,245,222,147,38,189,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,100,22,147,98,99,160,74,245,222,147,38,189,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,147,129,183,208,5,201,11,116,231,37,94,179,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,228,116,217,35,5,201,11,116,231,37,94,179,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,89,243,185,43,160,179,19,124,231,37,94,179,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,131,237,195,100,160,179,19,124,231,37,94,179,212,95,114,28,220,94,32,174,42,98,34,4,214,10,216,155,210,194,150,8,179,151,84,159,236,194,218,247,95,184,14,182,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,17,209,44,159,236,194,218,247,95,184,14,182,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,118,89,121,54,244,73,190,177,95,184,14,182,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,167,187,80,191,244,73,190,177,95,184,14,182,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,109,95,140,81,42,92,129,234,198,169,156,133,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,37,82,106,170,42,92,129,234,198,169,156,133,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,252,226,92,204,108,153,157,227,198,169,156,133,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,224,108,165,161,108,153,157,227,198,169,156,133,203,139,195,106,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,4,27,80,174,163,50,171,76,217,92,112,26,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,207,129,169,31,163,50,171,76,217,92,112,26,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,233,224,38,93,241,7,162,27,217,92,112,26,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,208,127,241,68,241,7,162,27,217,92,112,26,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,164,245,145,211,206,131,153,180,248,164,93,124,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,233,162,74,54,206,131,153,180,248,164,93,124,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,211,93,159,62,43,141,136,237,248,164,93,124,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,22,15,219,10,43,141,136,237,248,164,93,124,68,158,170,93,178,241,150,166,105,50,201,221,214,10,216,155,210,194,150,8,225,239,69,93,58,174,252,226,253,62,176,21,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,71,14,45,19,58,174,252,226,253,62,176,21,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,75,158,79,2,212,250,126,95,253,62,176,21,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,176,167,66,95,212,250,126,95,253,62,176,21,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,137,108,87,155,235,72,9,223,219,78,116,24,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,221,52,115,89,235,72,9,223,219,78,116,24,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,71,201,170,17,200,40,165,177,219,78,116,24,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,209,12,100,54,200,40,165,177,219,78,116,24,120,148,23,214,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,31,139,240,132,128,92,178,133,95,12,184,178,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,132,15,135,37,128,92,178,133,95,12,184,178,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,187,224,195,234,23,150,210,86,95,12,184,178,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,99,151,51,84,23,150,210,86,95,12,184,178,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,171,22,20,242,205,129,26,178,175,20,10,249,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,66,91,87,129,205,129,26,178,175,20,10,249,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8,58,200,104,48,175,20,10,249,84,135,163,76,214,159,130,226,105,50,201,221,214,10,216,155,210,194,150,8}; + +static const uint32_t T6_COUNT = 256u; +static const uint8_t T6_LEAVES[1024] = {140,30,28,139,112,181,234,109,189,248,70,203,91,147,16,79,160,214,180,247,227,25,226,179,212,216,74,183,29,218,24,245,174,86,29,161,246,98,88,70,57,109,6,63,2,118,183,123,179,170,29,164,240,42,222,210,44,221,150,34,140,58,40,230,34,40,243,221,70,200,235,154,222,245,150,142,26,73,239,10,149,160,70,187,237,164,108,108,255,97,11,186,38,207,198,61,150,120,191,174,140,118,171,135,98,134,144,226,134,125,154,211,144,180,156,190,239,161,238,1,12,58,108,180,205,156,159,243,234,143,100,122,48,2,67,107,16,94,165,53,11,230,64,17,156,108,188,173,215,106,87,57,40,134,87,49,84,165,105,235,101,114,152,214,254,223,71,12,185,117,113,106,204,140,153,139,17,99,242,43,219,162,40,74,235,167,12,139,193,244,177,243,168,241,35,162,3,244,187,19,8,31,0,50,125,10,54,219,128,111,71,120,98,85,123,253,109,114,78,90,28,74,181,193,78,119,202,25,52,9,11,168,104,102,167,195,163,38,76,46,244,20,10,239,73,42,118,156,35,123,44,47,169,77,215,85,5,158,206,234,117,78,236,245,199,90,190,233,27,94,66,124,125,216,144,206,176,49,232,25,240,96,71,166,1,188,162,37,52,170,95,214,202,140,244,166,202,197,31,175,200,200,184,216,89,16,76,83,0,214,101,183,191,58,24,205,152,185,199,196,158,173,204,151,167,47,174,166,166,219,25,142,153,242,69,204,213,20,122,92,72,139,123,35,174,151,61,167,16,59,48,232,207,11,215,131,29,237,20,185,230,248,162,116,73,249,8,193,17,122,162,2,30,200,86,10,211,163,22,176,91,129,245,134,24,251,148,174,99,121,51,181,210,28,91,5,71,206,178,16,152,119,134,224,1,52,173,144,205,28,218,5,29,48,99,59,245,188,174,66,8,246,1,98,79,31,195,126,195,166,105,161,123,146,111,78,186,171,239,170,88,104,171,67,123,130,25,202,36,167,154,238,225,39,215,152,97,177,82,216,38,134,48,83,21,132,181,183,97,164,142,64,211,142,129,160,76,126,37,4,25,250,177,60,206,78,107,67,60,48,109,132,22,42,14,35,31,107,190,9,174,168,77,194,252,126,158,68,230,216,74,121,1,20,130,34,13,230,124,118,63,95,167,113,32,221,48,116,23,12,240,245,169,207,209,44,166,94,127,63,210,175,232,112,46,53,192,226,14,93,152,208,26,123,5,72,249,48,209,230,232,165,48,43,179,126,101,6,6,32,139,240,34,165,130,235,221,88,169,2,237,233,7,132,223,53,21,123,154,157,80,249,223,99,236,125,24,201,152,48,250,227,240,143,9,96,140,175,122,0,252,131,89,231,69,134,174,100,160,121,169,149,133,58,128,154,161,213,253,115,9,78,17,162,36,204,144,39,254,0,202,10,231,163,145,210,170,33,99,34,51,113,149,65,26,52,97,21,41,130,29,16,14,2,224,227,56,163,187,59,211,67,23,244,16,132,64,72,173,170,144,118,245,143,49,245,173,189,32,108,165,42,182,244,204,65,246,192,37,126,173,189,141,102,42,108,224,229,243,79,2,122,50,173,7,109,74,193,250,241,62,119,175,121,191,252,45,227,1,178,83,242,47,51,38,223,124,72,20,198,43,190,11,27,198,20,180,133,14,88,191,54,150,173,116,181,86,153,4,30,100,151,106,29,221,6,67,158,161,230,124,15,88,83,106,181,58,198,103,66,16,18,120,121,81,131,20,21,57,196,42,236,254,91,123,120,206,207,152,201,32,88,58,158,182,125,143,86,40,251,141,75,187,57,85,90,76,77,62,226,116,232,18,4,109,141,210,161,216,65,104,210,138,24,186,238,238,226,2,92,49,35,250,235,8,158,91,229,28,150,74,93,54,112,81,75,223,55,136,55,155,189,6,233,116,184,247,71,111,154,180,28,143,63,60,255,75,249,199,13,97,69,87,36,175,246,44,216,61,168,118,195,204,131,86,83,83,32,90,221,132,118,242,32,60,215,255,231,227,69,26,211,0,238,219,13,201,242,27,183,76,251,117,59,5,13,224,73,11,245,144,125,225,219,27,34,56,188,153,118,49,119,131,237,26,122,26,117,25,247,250,36,237,13,251,160,154,108,50,14,128,55,252,79,241,90,51,108,77,162,87,54,209,217,193,197,84,169,7,120,34,68,123,53,198,185,26,150,54,8,118,114,189,199,0,181,43,2,251,13,79,53,110,102,8,8,189,31,22,138,15,158,160,16,43,19,128,242,89,33,244,11,70,181}; + +static const uint8_t T6_ROOT[4] = {10,218,78,212}; + +static const uint16_t T6_POFF[256] = {0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,480,512,544,576,608,640,672,704,736,768,800,832,864,896,928,960,992,1024,1056,1088,1120,1152,1184,1216,1248,1280,1312,1344,1376,1408,1440,1472,1504,1536,1568,1600,1632,1664,1696,1728,1760,1792,1824,1856,1888,1920,1952,1984,2016,2048,2080,2112,2144,2176,2208,2240,2272,2304,2336,2368,2400,2432,2464,2496,2528,2560,2592,2624,2656,2688,2720,2752,2784,2816,2848,2880,2912,2944,2976,3008,3040,3072,3104,3136,3168,3200,3232,3264,3296,3328,3360,3392,3424,3456,3488,3520,3552,3584,3616,3648,3680,3712,3744,3776,3808,3840,3872,3904,3936,3968,4000,4032,4064,4096,4128,4160,4192,4224,4256,4288,4320,4352,4384,4416,4448,4480,4512,4544,4576,4608,4640,4672,4704,4736,4768,4800,4832,4864,4896,4928,4960,4992,5024,5056,5088,5120,5152,5184,5216,5248,5280,5312,5344,5376,5408,5440,5472,5504,5536,5568,5600,5632,5664,5696,5728,5760,5792,5824,5856,5888,5920,5952,5984,6016,6048,6080,6112,6144,6176,6208,6240,6272,6304,6336,6368,6400,6432,6464,6496,6528,6560,6592,6624,6656,6688,6720,6752,6784,6816,6848,6880,6912,6944,6976,7008,7040,7072,7104,7136,7168,7200,7232,7264,7296,7328,7360,7392,7424,7456,7488,7520,7552,7584,7616,7648,7680,7712,7744,7776,7808,7840,7872,7904,7936,7968,8000,8032,8064,8096,8128,8160}; +static const uint8_t T6_PNSIB[256] = {8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8,8}; +static const uint8_t T6_PBLOB[8192] = {112,181,234,109,61,1,12,254,33,187,164,242,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,140,30,28,139,61,1,12,254,33,187,164,242,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,91,147,16,79,201,152,239,203,33,187,164,242,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,189,248,70,203,201,152,239,203,33,187,164,242,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,227,25,226,179,173,126,189,138,120,56,19,180,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,160,214,180,247,173,126,189,138,120,56,19,180,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,29,218,24,245,135,255,44,188,120,56,19,180,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,212,216,74,183,135,255,44,188,120,56,19,180,39,169,252,55,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,246,98,88,70,236,139,173,115,56,194,247,25,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,174,86,29,161,236,139,173,115,56,194,247,25,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,2,118,183,123,101,83,76,9,56,194,247,25,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,57,109,6,63,101,83,76,9,56,194,247,25,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,240,42,222,210,128,122,161,242,120,133,88,237,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,179,170,29,164,128,122,161,242,120,133,88,237,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,140,58,40,230,12,204,61,48,120,133,88,237,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,44,221,150,34,12,204,61,48,120,133,88,237,44,119,165,187,151,142,28,255,20,91,128,192,110,91,64,66,11,50,110,46,70,200,235,154,155,192,177,60,14,228,38,70,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,34,40,243,221,155,192,177,60,14,228,38,70,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,26,73,239,10,164,82,17,53,14,228,38,70,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,222,245,150,142,164,82,17,53,14,228,38,70,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,237,164,108,108,158,68,74,89,73,205,248,16,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,149,160,70,187,158,68,74,89,73,205,248,16,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,38,207,198,61,89,73,17,146,73,205,248,16,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,255,97,11,186,89,73,17,146,73,205,248,16,8,205,156,178,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,140,118,171,135,145,169,118,133,95,35,5,66,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,150,120,191,174,145,169,118,133,95,35,5,66,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,134,125,154,211,163,128,131,192,95,35,5,66,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,98,134,144,226,163,128,131,192,95,35,5,66,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,239,161,238,1,158,10,68,7,182,80,56,25,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,144,180,156,190,158,10,68,7,182,80,56,25,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,205,156,159,243,136,66,47,18,182,80,56,25,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,12,58,108,180,136,66,47,18,182,80,56,25,212,234,181,1,175,42,80,15,20,91,128,192,110,91,64,66,11,50,110,46,48,2,67,107,248,10,239,42,204,110,154,236,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,234,143,100,122,248,10,239,42,204,110,154,236,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,11,230,64,17,246,211,40,32,204,110,154,236,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,16,94,165,53,246,211,40,32,204,110,154,236,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,215,106,87,57,152,82,220,235,137,94,251,72,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,156,108,188,173,152,82,220,235,137,94,251,72,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,84,165,105,235,140,26,155,3,137,94,251,72,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,40,134,87,49,140,26,155,3,137,94,251,72,208,111,42,78,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,254,223,71,12,132,90,115,110,234,251,207,116,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,101,114,152,214,132,90,115,110,234,251,207,116,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,204,140,153,139,137,178,153,49,234,251,207,116,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,185,117,113,106,137,178,153,49,234,251,207,116,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,219,162,40,74,69,228,101,168,223,175,27,248,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,17,99,242,43,69,228,101,168,223,175,27,248,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,193,244,177,243,125,249,212,200,223,175,27,248,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,235,167,12,139,125,249,212,200,223,175,27,248,249,181,216,28,192,126,161,111,46,33,156,185,110,91,64,66,11,50,110,46,3,244,187,19,94,151,244,19,107,129,211,179,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,168,241,35,162,94,151,244,19,107,129,211,179,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,125,10,54,219,202,36,212,89,107,129,211,179,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,8,31,0,50,202,36,212,89,107,129,211,179,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,98,85,123,253,26,197,238,204,90,36,254,87,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,128,111,71,120,26,197,238,204,90,36,254,87,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,28,74,181,193,127,14,113,74,90,36,254,87,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,109,114,78,90,127,14,113,74,90,36,254,87,113,136,247,118,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,52,9,11,168,169,44,254,214,66,244,207,42,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,78,119,202,25,169,44,254,214,66,244,207,42,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,163,38,76,46,97,244,180,89,66,244,207,42,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,104,102,167,195,97,244,180,89,66,244,207,42,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,73,42,118,156,79,101,8,131,93,71,48,66,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,244,20,10,239,79,101,8,131,93,71,48,66,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,169,77,215,85,169,129,185,199,93,71,48,66,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,35,123,44,47,169,129,185,199,93,71,48,66,59,29,246,54,185,81,249,212,46,33,156,185,110,91,64,66,11,50,110,46,117,78,236,245,235,154,192,111,240,18,230,218,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,5,158,206,234,235,154,192,111,240,18,230,218,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,27,94,66,124,233,56,213,248,240,18,230,218,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,199,90,190,233,233,56,213,248,240,18,230,218,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,176,49,232,25,220,128,15,34,230,236,117,104,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,125,216,144,206,220,128,15,34,230,236,117,104,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,1,188,162,37,203,208,234,75,230,236,117,104,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,240,96,71,166,203,208,234,75,230,236,117,104,49,34,73,109,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,202,140,244,166,94,202,124,71,119,27,207,13,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,52,170,95,214,94,202,124,71,119,27,207,13,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,200,200,184,216,10,151,94,242,119,27,207,13,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,202,197,31,175,10,151,94,242,119,27,207,13,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,0,214,101,183,3,71,78,156,125,228,213,164,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,89,16,76,83,3,71,78,156,125,228,213,164,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,152,185,199,196,153,28,104,1,125,228,213,164,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,191,58,24,205,153,28,104,1,125,228,213,164,109,180,97,35,181,47,173,43,69,90,182,188,13,119,131,192,11,50,110,46,167,47,174,166,133,151,46,226,42,176,96,146,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,158,173,204,151,133,151,46,226,42,176,96,146,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,153,242,69,204,33,228,244,127,42,176,96,146,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,166,219,25,142,33,228,244,127,42,176,96,146,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,72,139,123,35,150,96,253,48,201,147,77,187,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,213,20,122,92,150,96,253,48,201,147,77,187,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,16,59,48,232,68,185,0,27,201,147,77,187,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,174,151,61,167,68,185,0,27,201,147,77,187,69,205,119,196,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,29,237,20,185,229,214,255,73,23,131,74,221,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,207,11,215,131,229,214,255,73,23,131,74,221,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,73,249,8,193,142,154,168,78,23,131,74,221,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,230,248,162,116,142,154,168,78,23,131,74,221,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,30,200,86,10,109,134,40,163,227,231,120,235,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,17,122,162,2,109,134,40,163,227,231,120,235,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,91,129,245,134,86,43,234,76,227,231,120,235,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,211,163,22,176,86,43,234,76,227,231,120,235,194,223,146,194,160,113,89,231,69,90,182,188,13,119,131,192,11,50,110,46,99,121,51,181,87,196,85,145,88,254,58,219,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,24,251,148,174,87,196,85,145,88,254,58,219,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,71,206,178,16,95,6,2,213,88,254,58,219,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,210,28,91,5,95,6,2,213,88,254,58,219,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,1,52,173,144,237,118,80,254,72,165,67,184,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,152,119,134,224,237,118,80,254,72,165,67,184,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,29,48,99,59,197,94,52,174,72,165,67,184,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,205,28,218,5,197,94,52,174,72,165,67,184,68,225,163,175,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,8,246,1,98,170,186,235,80,84,146,181,196,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,245,188,174,66,170,186,235,80,84,146,181,196,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,195,166,105,161,202,194,96,248,84,146,181,196,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,79,31,195,126,202,194,96,248,84,146,181,196,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,186,171,239,170,244,54,50,8,202,28,238,117,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,123,146,111,78,244,54,50,8,202,28,238,117,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,123,130,25,202,234,2,144,51,202,28,238,117,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,88,104,171,67,234,2,144,51,202,28,238,117,215,183,224,153,55,106,44,53,36,70,252,155,13,119,131,192,11,50,110,46,225,39,215,152,207,242,127,202,42,74,199,130,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,36,167,154,238,207,242,127,202,42,74,199,130,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,38,134,48,83,104,123,243,232,42,74,199,130,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,97,177,82,216,104,123,243,232,42,74,199,130,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,97,164,142,64,126,233,6,137,167,204,43,160,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,21,132,181,183,126,233,6,137,167,204,43,160,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,76,126,37,4,200,38,101,97,167,204,43,160,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,211,142,129,160,200,38,101,97,167,204,43,160,90,255,186,65,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,206,78,107,67,79,168,90,42,1,107,110,249,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,25,250,177,60,79,168,90,42,1,107,110,249,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,22,42,14,35,225,182,62,86,1,107,110,249,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,60,48,109,132,225,182,62,86,1,107,110,249,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,174,168,77,194,114,252,130,194,92,96,157,227,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,31,107,190,9,114,252,130,194,92,96,157,227,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,230,216,74,121,36,64,151,125,92,96,157,227,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,252,126,158,68,36,64,151,125,92,96,157,227,76,250,12,7,253,241,16,85,36,70,252,155,13,119,131,192,11,50,110,46,13,230,124,118,17,156,215,202,183,49,105,235,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,1,20,130,34,17,156,215,202,183,49,105,235,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,32,221,48,116,36,250,74,15,183,49,105,235,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,63,95,167,113,36,250,74,15,183,49,105,235,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,169,207,209,44,18,101,254,55,135,197,25,81,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,23,12,240,245,18,101,254,55,135,197,25,81,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,210,175,232,112,231,137,163,84,135,197,25,81,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,166,94,127,63,231,137,163,84,135,197,25,81,215,51,80,145,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,14,93,152,208,42,201,125,57,85,173,43,21,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,46,53,192,226,42,201,125,57,85,173,43,21,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,249,48,209,230,27,246,163,220,85,173,43,21,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,26,123,5,72,27,246,163,220,85,173,43,21,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,179,126,101,6,84,20,12,42,18,232,75,10,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,232,165,48,43,84,20,12,42,18,232,75,10,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,34,165,130,235,198,44,224,37,18,232,75,10,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,6,32,139,240,198,44,224,37,18,232,75,10,129,232,137,147,139,170,98,148,168,231,5,161,169,49,25,41,205,145,153,193,237,233,7,132,34,148,45,218,205,140,108,59,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,221,88,169,2,34,148,45,218,205,140,108,59,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,154,157,80,249,138,243,53,229,205,140,108,59,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,223,53,21,123,138,243,53,229,205,140,108,59,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,24,201,152,48,77,11,238,45,47,105,248,72,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,223,99,236,125,77,11,238,45,47,105,248,72,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,9,96,140,175,11,32,118,46,47,105,248,72,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,250,227,240,143,11,32,118,46,47,105,248,72,178,172,196,88,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,89,231,69,134,170,55,205,13,225,0,77,244,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,122,0,252,131,170,55,205,13,225,0,77,244,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,169,149,133,58,8,52,141,54,225,0,77,244,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,174,100,160,121,8,52,141,54,225,0,77,244,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,253,115,9,78,253,6,237,133,242,199,92,176,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,128,154,161,213,253,6,237,133,242,199,92,176,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,144,39,254,0,138,111,241,58,242,199,92,176,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,17,162,36,204,138,111,241,58,242,199,92,176,145,124,231,103,124,98,133,186,168,231,5,161,169,49,25,41,205,145,153,193,145,210,170,33,178,23,38,4,249,81,224,211,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,202,10,231,163,178,23,38,4,249,81,224,211,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,149,65,26,52,53,143,130,185,249,81,224,211,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,99,34,51,113,53,143,130,185,249,81,224,211,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,29,16,14,2,184,254,73,5,136,27,21,135,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,97,21,41,130,184,254,73,5,136,27,21,135,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,187,59,211,67,136,22,241,209,136,27,21,135,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,224,227,56,163,136,22,241,209,136,27,21,135,160,149,210,156,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,64,72,173,170,66,192,129,16,61,45,220,54,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,23,244,16,132,66,192,129,16,61,45,220,54,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,49,245,173,189,156,87,248,186,61,45,220,54,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,144,118,245,143,156,87,248,186,61,45,220,54,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,182,244,204,65,238,158,136,206,182,187,182,151,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,32,108,165,42,238,158,136,206,182,187,182,151,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,173,189,141,102,147,196,149,176,182,187,182,151,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,246,192,37,126,147,196,149,176,182,187,182,151,214,237,112,183,151,119,3,93,184,170,201,21,169,49,25,41,205,145,153,193,243,79,2,122,92,204,93,90,204,88,124,201,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,42,108,224,229,92,204,93,90,204,88,124,201,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,74,193,250,241,54,84,165,173,204,88,124,201,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,50,173,7,109,54,84,165,173,204,88,124,201,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,191,252,45,227,225,175,9,37,8,140,232,231,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,62,119,175,121,225,175,9,37,8,140,232,231,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,47,51,38,223,133,178,119,244,8,140,232,231,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,1,178,83,242,133,178,119,244,8,140,232,231,60,130,205,15,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,43,190,11,27,44,226,61,236,100,101,157,254,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,124,72,20,198,44,226,61,236,100,101,157,254,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,14,88,191,54,173,170,78,64,100,101,157,254,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,198,20,180,133,173,170,78,64,100,101,157,254,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,86,153,4,30,192,111,140,96,80,35,205,94,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,150,173,116,181,192,111,140,96,80,35,205,94,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,221,6,67,158,137,248,10,193,80,35,205,94,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,100,151,106,29,137,248,10,193,80,35,205,94,86,254,208,218,127,121,251,11,184,170,201,21,169,49,25,41,205,145,153,193,88,83,106,181,176,46,58,108,158,69,241,23,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,161,230,124,15,176,46,58,108,158,69,241,23,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,16,18,120,121,20,157,197,130,158,69,241,23,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,58,198,103,66,20,157,197,130,158,69,241,23,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,57,196,42,236,188,201,76,45,180,210,216,181,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,81,131,20,21,188,201,76,45,180,210,216,181,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,206,207,152,201,155,240,76,139,180,210,216,181,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,254,91,123,120,155,240,76,139,180,210,216,181,131,185,71,239,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,182,125,143,86,188,11,230,127,58,74,252,219,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,32,88,58,158,188,11,230,127,58,74,252,219,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,187,57,85,90,11,131,61,239,58,74,252,219,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,40,251,141,75,11,131,61,239,58,74,252,219,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,116,232,18,4,37,43,67,225,144,156,101,124,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,76,77,62,226,37,43,67,225,144,156,101,124,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,216,65,104,210,141,5,173,175,144,156,101,124,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,109,141,210,161,141,5,173,175,144,156,101,124,243,220,247,115,249,90,208,219,50,56,128,16,39,36,114,140,205,145,153,193,238,226,2,92,42,239,5,81,77,101,160,124,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,138,24,186,238,42,239,5,81,77,101,160,124,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,8,158,91,229,207,11,159,198,77,101,160,124,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,49,35,250,235,207,11,159,198,77,101,160,124,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,54,112,81,75,32,156,170,201,175,73,239,231,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,28,150,74,93,32,156,170,201,175,73,239,231,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,155,189,6,233,10,239,214,233,175,73,239,231,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,223,55,136,55,10,239,214,233,175,73,239,231,120,171,179,254,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,111,154,180,28,25,229,39,173,135,72,150,61,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,116,184,247,71,25,229,39,173,135,72,150,61,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,75,249,199,13,48,182,150,175,135,72,150,61,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,143,63,60,255,48,182,150,175,135,72,150,61,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,175,246,44,216,102,52,237,10,146,220,104,237,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,97,69,87,36,102,52,237,10,146,220,104,237,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,204,131,86,83,35,22,177,96,146,220,104,237,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,61,168,118,195,35,22,177,96,146,220,104,237,147,214,199,152,113,48,110,147,50,56,128,16,39,36,114,140,205,145,153,193,132,118,242,32,140,185,38,31,244,28,217,183,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,83,32,90,221,140,185,38,31,244,28,217,183,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,227,69,26,211,68,62,78,226,244,28,217,183,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,60,215,255,231,68,62,78,226,244,28,217,183,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,201,242,27,183,96,251,95,202,200,19,188,50,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,0,238,219,13,96,251,95,202,200,19,188,50,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,5,13,224,73,145,158,120,63,200,19,188,50,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,76,251,117,59,145,158,120,63,200,19,188,50,43,201,19,159,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,225,219,27,34,167,21,105,44,50,101,172,28,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,11,245,144,125,167,21,105,44,50,101,172,28,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,49,119,131,237,24,221,50,135,50,101,172,28,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,56,188,153,118,24,221,50,135,50,101,172,28,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,25,247,250,36,202,15,38,98,231,5,85,193,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,26,122,26,117,202,15,38,98,231,5,85,193,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,154,108,50,14,83,247,163,3,231,5,85,193,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,237,13,251,160,83,247,163,3,231,5,85,193,137,231,135,203,142,40,224,45,187,74,156,253,39,36,114,140,205,145,153,193,241,90,51,108,160,86,8,3,35,42,57,2,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,128,55,252,79,160,86,8,3,35,42,57,2,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,209,217,193,197,18,124,187,233,35,42,57,2,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,77,162,87,54,18,124,187,233,35,42,57,2,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,34,68,123,53,182,252,59,195,146,136,61,177,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,84,169,7,120,182,252,59,195,146,136,61,177,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,54,8,118,114,58,164,90,159,146,136,61,177,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,198,185,26,150,58,164,90,159,146,136,61,177,24,12,220,46,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,43,2,251,13,99,37,63,226,66,59,122,13,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,189,199,0,181,99,37,63,226,66,59,122,13,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,8,8,189,31,184,65,4,129,66,59,122,13,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,79,53,110,102,184,65,4,129,66,59,122,13,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,160,16,43,19,103,77,131,14,242,113,165,227,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,22,138,15,158,103,77,131,14,242,113,165,227,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,244,11,70,181,109,53,81,138,242,113,165,227,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193,128,242,89,33,109,53,81,138,242,113,165,227,69,38,40,64,6,151,150,245,187,74,156,253,39,36,114,140,205,145,153,193}; + +struct ProofCase { uint32_t count; const uint8_t* leaves; const uint8_t* root; const uint16_t* poff; const uint8_t* pnsib; const uint8_t* pblob; }; +static const ProofCase PROOF_CASES[] = {{T0_COUNT,T0_LEAVES,T0_ROOT,T0_POFF,T0_PNSIB,T0_PBLOB},{T1_COUNT,T1_LEAVES,T1_ROOT,T1_POFF,T1_PNSIB,T1_PBLOB},{T2_COUNT,T2_LEAVES,T2_ROOT,T2_POFF,T2_PNSIB,T2_PBLOB},{T3_COUNT,T3_LEAVES,T3_ROOT,T3_POFF,T3_PNSIB,T3_PBLOB},{T4_COUNT,T4_LEAVES,T4_ROOT,T4_POFF,T4_PNSIB,T4_PBLOB},{T5_COUNT,T5_LEAVES,T5_ROOT,T5_POFF,T5_PNSIB,T5_PBLOB},{T6_COUNT,T6_LEAVES,T6_ROOT,T6_POFF,T6_PNSIB,T6_PBLOB}}; +static const int N_PROOF_CASES = 7; +// small-block signed .mota for the OtaManager host transfer simulation +static const uint8_t SIM_MOTA[2334] = {109,79,84,65,30,9,0,0,2,3,18,190,186,254,202,0,0,0,3,8,8,0,0,8,8,0,0,7,97,190,149,151,182,60,80,58,200,234,98,44,193,140,71,76,243,130,97,209,39,43,50,65,54,148,88,72,127,67,165,233,114,102,17,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,161,7,191,243,206,16,190,29,112,221,24,231,75,192,153,103,228,214,48,155,165,13,95,29,220,134,100,18,85,49,184,38,98,213,118,187,206,236,168,51,174,162,73,89,30,161,211,77,42,34,60,159,243,83,241,213,243,188,72,94,109,154,234,68,70,87,185,215,226,224,96,146,252,13,183,118,91,107,134,241,252,255,206,215,103,126,185,188,114,49,17,62,148,194,5,255,255,255,255,95,197,98,90,98,17,7,154,48,52,162,55,106,72,59,84,136,235,135,81,243,8,241,172,4,174,251,120,121,213,89,254,16,213,235,145,157,150,188,132,125,7,184,29,126,203,177,170,175,71,66,193,125,16,43,58,126,48,74,90,30,57,43,99,175,85,112,245,103,97,51,153,45,58,63,34,194,22,64,186,98,135,175,179,137,22,240,159,125,51,107,184,156,150,55,202,230,201,95,210,99,220,174,54,38,118,169,45,217,145,86,21,242,183,135,173,198,22,120,80,103,10,68,142,1,108,52,92,253,238,136,20,118,132,230,50,155,118,250,13,91,33,55,243,235,183,41,191,202,113,152,236,251,45,241,128,169,45,9,28,5,43,121,175,209,107,20,239,108,161,22,36,79,158,1,97,84,34,133,103,51,230,201,249,14,163,54,113,65,139,103,211,195,32,90,240,177,121,236,14,33,189,221,183,153,199,191,38,155,240,104,31,24,30,36,75,44,253,45,104,37,236,53,225,143,81,197,127,204,59,216,65,30,219,210,92,47,173,18,128,73,2,149,213,12,86,55,96,102,86,210,207,102,98,106,208,94,136,23,46,225,9,236,95,184,219,102,197,113,58,79,218,167,57,167,184,79,27,223,176,184,80,55,195,207,182,172,182,63,98,75,112,192,245,20,225,112,107,246,232,65,17,128,75,232,175,223,82,201,122,136,32,238,229,65,110,225,74,104,247,168,211,156,40,72,97,173,173,154,220,252,158,170,182,112,49,97,86,227,50,88,1,170,90,177,75,246,217,49,228,170,170,43,202,163,66,168,106,190,192,221,180,59,104,220,220,88,168,102,0,28,128,136,51,0,133,79,243,172,250,191,253,42,130,200,206,113,238,52,67,191,217,189,106,119,143,135,160,158,196,161,212,195,155,196,73,51,189,5,255,26,206,78,19,148,8,246,14,106,76,94,248,142,160,242,181,150,147,75,49,193,25,253,221,203,101,102,202,44,67,117,84,32,224,33,230,114,222,217,177,179,99,107,143,244,183,45,92,64,53,70,118,146,216,227,179,97,47,211,233,239,255,122,146,228,160,227,234,121,170,43,250,19,136,61,255,117,133,3,29,84,203,126,18,85,167,187,1,145,174,136,70,162,3,149,81,105,66,42,34,150,235,12,101,9,12,87,130,58,142,193,74,40,214,112,14,178,198,106,58,206,48,29,124,43,69,70,168,93,18,143,235,50,198,143,44,179,90,167,245,208,136,20,190,173,29,185,32,211,35,38,139,22,8,194,131,165,97,42,90,222,200,183,59,157,155,194,247,156,41,87,197,13,228,87,207,160,111,48,78,45,189,28,37,127,164,247,133,187,238,3,59,96,234,174,203,24,200,203,253,60,145,113,187,202,154,223,227,20,224,209,41,224,227,214,42,198,113,95,64,80,150,11,149,101,5,76,242,40,33,241,2,225,73,207,142,75,74,31,126,109,205,18,10,90,41,155,174,81,216,85,169,83,206,212,247,8,96,208,69,156,58,140,38,177,172,4,195,220,240,73,189,102,107,131,183,230,198,58,44,134,104,253,13,54,61,165,66,41,215,145,24,111,1,144,241,7,214,51,33,230,244,46,161,76,15,253,225,65,46,148,195,110,38,0,238,164,9,128,101,239,253,250,114,18,42,150,59,87,111,33,122,234,156,23,94,128,40,59,187,146,124,53,183,218,25,111,156,51,231,204,44,34,121,188,229,63,44,215,175,65,42,166,132,172,204,129,234,222,217,139,19,60,61,134,58,175,180,107,101,134,143,18,61,64,82,196,144,85,126,48,24,203,160,7,241,9,111,15,80,48,236,12,169,74,47,68,230,236,171,65,2,147,135,75,50,185,93,29,40,88,99,36,178,78,135,66,50,115,14,241,245,6,90,59,36,155,20,213,220,97,4,91,240,81,227,96,62,137,39,199,18,5,7,45,54,56,87,158,65,194,241,18,165,0,111,29,168,111,20,177,216,249,186,94,127,108,142,224,96,196,143,231,165,67,83,182,88,206,29,69,135,62,16,238,8,56,68,83,158,26,69,224,78,159,159,62,54,113,170,113,133,4,154,190,67,19,198,98,31,188,4,72,126,195,241,230,206,180,113,109,203,237,193,82,130,255,60,154,177,136,236,223,221,27,234,73,101,198,53,17,151,236,71,116,190,125,86,255,152,21,178,5,12,144,21,162,33,220,104,49,108,226,169,168,150,148,4,5,31,204,82,55,52,170,163,254,27,3,255,110,212,240,224,73,46,214,34,209,198,176,73,255,45,180,106,26,79,215,47,192,107,98,161,24,19,68,242,134,77,64,217,252,186,40,192,70,186,31,8,191,154,212,17,64,121,100,32,227,74,153,123,55,216,255,220,197,128,78,212,114,64,128,64,162,47,117,72,38,48,84,130,32,205,128,58,157,90,154,132,18,58,142,111,200,37,146,0,86,185,201,187,102,148,203,21,2,108,22,108,117,27,21,1,49,222,238,22,198,61,76,49,29,37,187,66,181,79,44,211,142,173,164,48,22,157,28,82,6,247,214,55,76,103,85,129,10,113,166,247,64,122,138,157,89,55,89,33,11,179,83,178,233,141,241,172,233,59,217,53,207,204,160,133,154,108,183,121,245,41,154,88,142,45,186,132,216,204,136,78,5,4,155,151,88,39,32,111,11,17,136,81,244,172,142,74,213,92,44,141,84,177,244,197,176,25,186,140,75,8,170,232,87,108,50,162,40,10,44,160,23,121,100,109,222,139,139,97,95,151,164,213,236,96,66,26,235,52,251,199,90,34,16,67,174,25,235,1,94,99,80,151,60,151,31,154,170,250,74,254,238,70,104,229,33,34,170,227,251,215,33,73,248,179,169,200,196,219,242,6,140,130,136,67,36,63,105,134,141,32,67,77,111,190,98,23,214,246,88,7,229,125,58,122,166,16,127,225,77,61,199,129,253,9,32,116,207,202,17,236,90,143,244,33,247,164,237,253,90,198,189,137,250,10,16,3,212,55,185,188,238,3,115,51,164,134,195,117,127,162,183,0,55,186,11,155,56,39,252,163,110,47,227,32,248,220,234,206,140,117,73,213,155,5,197,212,184,208,8,77,14,160,8,175,63,145,100,217,95,240,194,127,195,34,72,146,89,178,228,38,176,235,8,201,84,147,210,95,137,215,51,170,106,244,173,67,45,131,65,122,215,20,99,181,105,184,253,66,245,155,77,122,0,142,44,197,249,7,179,64,59,44,231,172,224,244,182,215,4,40,135,220,56,76,65,82,100,91,102,41,226,67,95,200,42,1,39,140,118,118,71,188,214,123,161,81,187,161,45,83,145,36,176,157,12,128,102,142,171,150,39,203,245,119,54,128,30,203,176,101,252,84,197,108,136,80,84,250,55,180,16,22,57,249,160,194,63,203,253,98,40,248,62,182,104,245,244,58,87,20,47,15,185,14,22,161,120,97,55,186,249,67,112,86,129,1,244,46,167,142,254,163,109,152,223,164,111,81,70,188,245,43,159,236,36,123,145,95,176,215,34,244,123,253,113,118,46,116,175,132,124,152,152,76,137,114,157,118,199,25,123,8,247,235,230,172,116,49,31,88,125,2,97,111,227,242,80,212,15,186,50,32,224,200,31,238,232,140,109,243,138,238,193,253,222,0,28,32,150,190,48,34,244,56,197,196,64,5,88,180,249,15,245,11,196,51,9,49,56,240,154,254,52,54,132,192,1,47,118,67,121,90,212,93,88,234,230,86,62,74,4,165,115,26,42,148,244,10,210,223,239,225,54,204,223,102,234,204,72,197,208,214,94,87,47,181,173,120,85,167,77,201,165,122,165,126,3,120,7,65,137,154,23,84,27,251,239,7,243,108,182,237,236,95,176,233,237,180,0,44,95,91,93,150,86,172,69,245,81,234,190,228,243,239,236,67,99,42,154,211,59,170,79,213,233,246,137,20,22,171,123,18,201,20,174,51,28,88,99,220,82,250,56,219,62,13,214,33,141,64,205,143,101,42,215,166,1,222,87,80,48,96,68,127,188,203,45,43,224,181,148,200,224,171,5,168,15,39,8,78,252,90,120,37,15,35,73,166,90,148,66,63,80,237,61,15,63,64,110,238,28,211,153,21,206,143,45,0,163,151,175,49,81,125,98,92,35,255,157,214,250,223,198,59,54,238,109,208,130,116,71,106,246,221,224,50,114,241,58,227,192,72,132,9,183,55,223,87,248,119,200,188,52,25,141,180,183,9,44,8,253,50,69,220,217,111,3,18,22,43,4,63,22,31,19,17,20,4,185,220,235,68,204,223,209,95,199,95,44,113,98,153,12,109,46,120,50,131,118,82,236,19,90,44,176,79,168,210,78,153,10,163,55,171,51,11,131,141,80,191,166,60,156,119,28,69,220,141,106,190,54,31,8,41,123,205,30,202,192,46,51,33,43,160,109,215,19,150,232,243,219,67,20,200,61,53,213,132,196,237,137,4,173,157,112,173,155,116,158,69,190,147,248,64,132,112,33,143,197,191,249,118,137,88,239,250,192,122,192,62,202,131,155,222,27,224,34,17,222,231,164,17,43,154,60,226,223,135,32,134,149,107,194,168,74,204,4,72,98,47,171,92,224,231,153,13,18,153,50,192,130,191,64,92,33,243,39,161,69,110,100,70,208,7,0,0,250,86,224,170,102,212,176,242,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118,107,52,57,54}; + +static const uint32_t SIM_MOTA_LEN = 2334; +static const uint32_t SIM_TARGET_ID = 0xcafebabeu; +// 1 KB-block signed .mota (multi-fragment per block) for the reassembly transfer test +static const uint8_t SIM_MOTA_1K[2278] = {109,79,84,65,230,8,0,0,2,3,18,190,186,254,202,0,0,0,3,8,8,0,0,8,8,0,0,10,164,51,202,103,182,60,80,58,200,234,98,44,193,140,71,76,243,130,97,209,39,43,50,65,54,148,88,72,127,67,165,233,114,102,17,129,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,161,7,191,243,206,16,190,29,112,221,24,231,75,192,153,103,228,214,48,155,165,13,95,29,220,134,100,18,85,49,184,171,129,180,182,69,0,212,122,174,47,219,250,34,204,198,63,40,244,119,210,21,11,30,188,119,170,189,46,247,90,166,169,6,102,11,226,54,97,112,202,185,10,171,131,170,250,246,20,84,173,222,226,146,215,83,51,164,107,48,67,2,52,40,1,255,255,255,255,226,102,124,83,43,137,26,96,175,85,112,245,103,97,51,153,45,58,63,34,194,22,64,186,98,135,175,179,137,22,240,159,125,51,107,184,156,150,55,202,230,201,95,210,99,220,174,54,38,118,169,45,217,145,86,21,242,183,135,173,198,22,120,80,103,10,68,142,1,108,52,92,253,238,136,20,118,132,230,50,155,118,250,13,91,33,55,243,235,183,41,191,202,113,152,236,251,45,241,128,169,45,9,28,5,43,121,175,209,107,20,239,108,161,22,36,79,158,1,97,84,34,133,103,51,230,201,249,14,163,54,113,65,139,103,211,195,32,90,240,177,121,236,14,33,189,221,183,153,199,191,38,155,240,104,31,24,30,36,75,44,253,45,104,37,236,53,225,143,81,197,127,204,59,216,65,30,219,210,92,47,173,18,128,73,2,149,213,12,86,55,96,102,86,210,207,102,98,106,208,94,136,23,46,225,9,236,95,184,219,102,197,113,58,79,218,167,57,167,184,79,27,223,176,184,80,55,195,207,182,172,182,63,98,75,112,192,245,20,225,112,107,246,232,65,17,128,75,232,175,223,82,201,122,136,32,238,229,65,110,225,74,104,247,168,211,156,40,72,97,173,173,154,220,252,158,170,182,112,49,97,86,227,50,88,1,170,90,177,75,246,217,49,228,170,170,43,202,163,66,168,106,190,192,221,180,59,104,220,220,88,168,102,0,28,128,136,51,0,133,79,243,172,250,191,253,42,130,200,206,113,238,52,67,191,217,189,106,119,143,135,160,158,196,161,212,195,155,196,73,51,189,5,255,26,206,78,19,148,8,246,14,106,76,94,248,142,160,242,181,150,147,75,49,193,25,253,221,203,101,102,202,44,67,117,84,32,224,33,230,114,222,217,177,179,99,107,143,244,183,45,92,64,53,70,118,146,216,227,179,97,47,211,233,239,255,122,146,228,160,227,234,121,170,43,250,19,136,61,255,117,133,3,29,84,203,126,18,85,167,187,1,145,174,136,70,162,3,149,81,105,66,42,34,150,235,12,101,9,12,87,130,58,142,193,74,40,214,112,14,178,198,106,58,206,48,29,124,43,69,70,168,93,18,143,235,50,198,143,44,179,90,167,245,208,136,20,190,173,29,185,32,211,35,38,139,22,8,194,131,165,97,42,90,222,200,183,59,157,155,194,247,156,41,87,197,13,228,87,207,160,111,48,78,45,189,28,37,127,164,247,133,187,238,3,59,96,234,174,203,24,200,203,253,60,145,113,187,202,154,223,227,20,224,209,41,224,227,214,42,198,113,95,64,80,150,11,149,101,5,76,242,40,33,241,2,225,73,207,142,75,74,31,126,109,205,18,10,90,41,155,174,81,216,85,169,83,206,212,247,8,96,208,69,156,58,140,38,177,172,4,195,220,240,73,189,102,107,131,183,230,198,58,44,134,104,253,13,54,61,165,66,41,215,145,24,111,1,144,241,7,214,51,33,230,244,46,161,76,15,253,225,65,46,148,195,110,38,0,238,164,9,128,101,239,253,250,114,18,42,150,59,87,111,33,122,234,156,23,94,128,40,59,187,146,124,53,183,218,25,111,156,51,231,204,44,34,121,188,229,63,44,215,175,65,42,166,132,172,204,129,234,222,217,139,19,60,61,134,58,175,180,107,101,134,143,18,61,64,82,196,144,85,126,48,24,203,160,7,241,9,111,15,80,48,236,12,169,74,47,68,230,236,171,65,2,147,135,75,50,185,93,29,40,88,99,36,178,78,135,66,50,115,14,241,245,6,90,59,36,155,20,213,220,97,4,91,240,81,227,96,62,137,39,199,18,5,7,45,54,56,87,158,65,194,241,18,165,0,111,29,168,111,20,177,216,249,186,94,127,108,142,224,96,196,143,231,165,67,83,182,88,206,29,69,135,62,16,238,8,56,68,83,158,26,69,224,78,159,159,62,54,113,170,113,133,4,154,190,67,19,198,98,31,188,4,72,126,195,241,230,206,180,113,109,203,237,193,82,130,255,60,154,177,136,236,223,221,27,234,73,101,198,53,17,151,236,71,116,190,125,86,255,152,21,178,5,12,144,21,162,33,220,104,49,108,226,169,168,150,148,4,5,31,204,82,55,52,170,163,254,27,3,255,110,212,240,224,73,46,214,34,209,198,176,73,255,45,180,106,26,79,215,47,192,107,98,161,24,19,68,242,134,77,64,217,252,186,40,192,70,186,31,8,191,154,212,17,64,121,100,32,227,74,153,123,55,216,255,220,197,128,78,212,114,64,128,64,162,47,117,72,38,48,84,130,32,205,128,58,157,90,154,132,18,58,142,111,200,37,146,0,86,185,201,187,102,148,203,21,2,108,22,108,117,27,21,1,49,222,238,22,198,61,76,49,29,37,187,66,181,79,44,211,142,173,164,48,22,157,28,82,6,247,214,55,76,103,85,129,10,113,166,247,64,122,138,157,89,55,89,33,11,179,83,178,233,141,241,172,233,59,217,53,207,204,160,133,154,108,183,121,245,41,154,88,142,45,186,132,216,204,136,78,5,4,155,151,88,39,32,111,11,17,136,81,244,172,142,74,213,92,44,141,84,177,244,197,176,25,186,140,75,8,170,232,87,108,50,162,40,10,44,160,23,121,100,109,222,139,139,97,95,151,164,213,236,96,66,26,235,52,251,199,90,34,16,67,174,25,235,1,94,99,80,151,60,151,31,154,170,250,74,254,238,70,104,229,33,34,170,227,251,215,33,73,248,179,169,200,196,219,242,6,140,130,136,67,36,63,105,134,141,32,67,77,111,190,98,23,214,246,88,7,229,125,58,122,166,16,127,225,77,61,199,129,253,9,32,116,207,202,17,236,90,143,244,33,247,164,237,253,90,198,189,137,250,10,16,3,212,55,185,188,238,3,115,51,164,134,195,117,127,162,183,0,55,186,11,155,56,39,252,163,110,47,227,32,248,220,234,206,140,117,73,213,155,5,197,212,184,208,8,77,14,160,8,175,63,145,100,217,95,240,194,127,195,34,72,146,89,178,228,38,176,235,8,201,84,147,210,95,137,215,51,170,106,244,173,67,45,131,65,122,215,20,99,181,105,184,253,66,245,155,77,122,0,142,44,197,249,7,179,64,59,44,231,172,224,244,182,215,4,40,135,220,56,76,65,82,100,91,102,41,226,67,95,200,42,1,39,140,118,118,71,188,214,123,161,81,187,161,45,83,145,36,176,157,12,128,102,142,171,150,39,203,245,119,54,128,30,203,176,101,252,84,197,108,136,80,84,250,55,180,16,22,57,249,160,194,63,203,253,98,40,248,62,182,104,245,244,58,87,20,47,15,185,14,22,161,120,97,55,186,249,67,112,86,129,1,244,46,167,142,254,163,109,152,223,164,111,81,70,188,245,43,159,236,36,123,145,95,176,215,34,244,123,253,113,118,46,116,175,132,124,152,152,76,137,114,157,118,199,25,123,8,247,235,230,172,116,49,31,88,125,2,97,111,227,242,80,212,15,186,50,32,224,200,31,238,232,140,109,243,138,238,193,253,222,0,28,32,150,190,48,34,244,56,197,196,64,5,88,180,249,15,245,11,196,51,9,49,56,240,154,254,52,54,132,192,1,47,118,67,121,90,212,93,88,234,230,86,62,74,4,165,115,26,42,148,244,10,210,223,239,225,54,204,223,102,234,204,72,197,208,214,94,87,47,181,173,120,85,167,77,201,165,122,165,126,3,120,7,65,137,154,23,84,27,251,239,7,243,108,182,237,236,95,176,233,237,180,0,44,95,91,93,150,86,172,69,245,81,234,190,228,243,239,236,67,99,42,154,211,59,170,79,213,233,246,137,20,22,171,123,18,201,20,174,51,28,88,99,220,82,250,56,219,62,13,214,33,141,64,205,143,101,42,215,166,1,222,87,80,48,96,68,127,188,203,45,43,224,181,148,200,224,171,5,168,15,39,8,78,252,90,120,37,15,35,73,166,90,148,66,63,80,237,61,15,63,64,110,238,28,211,153,21,206,143,45,0,163,151,175,49,81,125,98,92,35,255,157,214,250,223,198,59,54,238,109,208,130,116,71,106,246,221,224,50,114,241,58,227,192,72,132,9,183,55,223,87,248,119,200,188,52,25,141,180,183,9,44,8,253,50,69,220,217,111,3,18,22,43,4,63,22,31,19,17,20,4,185,220,235,68,204,223,209,95,199,95,44,113,98,153,12,109,46,120,50,131,118,82,236,19,90,44,176,79,168,210,78,153,10,163,55,171,51,11,131,141,80,191,166,60,156,119,28,69,220,141,106,190,54,31,8,41,123,205,30,202,192,46,51,33,43,160,109,215,19,150,232,243,219,67,20,200,61,53,213,132,196,237,137,4,173,157,112,173,155,116,158,69,190,147,248,64,132,112,33,143,197,191,249,118,137,88,239,250,192,122,192,62,202,131,155,222,27,224,34,17,222,231,164,17,43,154,60,226,223,135,32,134,149,107,194,168,74,204,4,72,98,47,171,92,224,231,153,13,18,153,50,192,130,191,64,92,33,243,39,161,69,110,100,70,208,7,0,0,250,86,224,170,102,212,176,242,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,118,107,52,57,54}; + +static const uint32_t SIM_MOTA_1K_LEN = 2278; +static const uint32_t SIM_MOTA_1K_BLOCKS = 3; +// detools sequential+crle delta: apply DT_PATCH to DT_BASE -> DT_TARGET +static const uint8_t DT_BASE[3056] = {120,46,186,148,77,51,227,185,104,193,183,194,67,136,62,162,208,188,127,90,106,134,186,157,246,55,79,139,180,84,132,19,187,198,255,221,52,176,192,186,119,236,181,212,223,167,37,136,54,222,105,250,14,197,89,160,106,119,31,185,190,35,195,83,99,84,88,203,51,83,109,106,81,145,54,231,222,104,58,52,10,191,57,195,4,248,221,66,216,129,81,197,245,145,205,180,107,157,28,84,217,167,155,199,59,60,254,118,93,34,51,94,126,152,214,160,36,67,99,159,86,85,240,181,255,182,119,220,43,175,178,196,220,33,84,236,52,148,175,16,25,240,215,44,1,230,38,112,180,60,89,58,20,50,205,72,61,177,118,154,67,123,134,225,111,169,248,106,51,215,18,77,77,71,34,144,169,187,64,134,25,126,55,228,50,200,99,45,131,217,57,81,90,192,179,228,204,11,148,230,238,173,139,96,239,189,184,243,162,18,30,58,14,132,32,241,212,53,232,162,157,236,22,242,129,44,60,124,149,204,187,42,41,22,32,158,26,207,241,152,143,207,254,154,161,7,152,27,143,46,139,178,80,0,31,71,7,46,15,26,162,219,159,172,158,187,53,148,53,165,48,118,47,121,80,69,187,116,162,112,213,183,206,210,55,102,150,221,114,221,107,152,179,34,225,53,41,79,101,50,192,45,91,116,175,3,30,85,172,0,197,57,192,129,107,168,249,9,54,155,118,141,127,140,206,12,110,85,2,130,87,141,249,230,240,224,65,170,187,40,57,154,248,27,211,203,216,110,17,245,225,34,44,6,220,86,78,242,234,64,126,41,92,213,119,237,111,241,125,157,83,40,9,197,31,30,93,108,162,244,46,129,180,24,13,168,111,228,6,207,233,227,240,69,59,30,81,139,222,145,35,54,145,149,27,110,26,241,153,180,178,68,111,143,40,195,59,240,0,131,31,50,96,137,146,104,114,146,201,44,212,165,236,63,141,235,195,74,230,208,70,151,91,115,163,28,103,101,194,72,81,128,133,58,60,210,199,206,91,58,107,201,119,146,79,73,245,172,175,236,49,119,165,138,13,64,97,211,166,53,67,105,132,34,167,80,72,176,137,206,241,34,195,23,129,56,118,155,71,75,63,165,132,99,189,72,244,47,246,228,233,247,122,206,93,247,7,152,165,96,177,16,193,185,231,34,25,108,156,82,48,255,196,244,244,19,194,148,65,8,206,163,198,66,171,217,133,48,241,218,204,111,42,49,182,120,211,68,17,118,31,25,151,68,33,191,98,200,250,150,212,165,25,57,193,149,60,42,75,166,229,35,209,177,238,206,98,175,27,249,33,86,105,84,161,179,85,140,210,192,60,5,154,71,97,35,145,68,43,129,204,230,65,55,225,230,140,33,179,109,189,138,48,32,208,33,172,177,59,64,6,163,157,173,28,251,108,135,107,8,121,39,70,182,92,118,88,74,123,227,173,148,114,232,141,165,8,174,233,72,47,98,166,229,126,163,92,128,124,93,240,40,16,129,188,248,249,141,68,50,46,112,119,83,159,1,36,188,29,108,12,72,193,168,191,20,181,224,21,171,122,118,241,53,134,128,172,191,216,59,171,163,169,80,167,131,162,77,124,75,148,14,173,54,172,186,138,121,182,62,79,166,255,80,64,84,10,35,159,137,202,138,66,88,88,38,13,89,3,158,80,192,80,50,47,239,72,206,116,135,12,251,40,25,201,123,163,67,100,13,105,51,174,34,34,93,3,115,92,192,106,231,63,31,87,218,156,115,83,1,188,172,152,212,206,133,105,183,160,198,113,51,32,213,29,130,254,173,239,245,104,126,255,101,127,115,186,56,236,11,217,178,248,114,215,168,163,134,209,19,244,68,229,206,194,29,94,160,221,137,217,92,17,83,227,202,123,155,151,121,220,191,199,202,241,205,234,70,164,104,185,211,96,128,163,65,230,28,4,63,219,134,230,237,183,181,229,109,41,192,38,48,3,205,3,69,15,27,85,41,249,58,60,229,132,70,199,19,39,247,213,215,10,64,234,230,209,250,91,21,134,94,143,43,27,92,64,200,3,206,207,147,130,194,137,110,114,158,192,62,200,9,173,197,36,195,222,90,68,139,0,117,232,231,62,93,112,14,37,53,86,17,150,199,31,117,20,200,185,21,58,0,193,20,229,210,41,161,119,53,225,179,50,59,120,212,104,243,159,148,20,197,167,106,194,135,192,98,133,68,210,210,227,227,161,183,180,36,62,92,189,50,39,122,5,177,246,255,185,32,70,105,173,233,213,61,174,111,102,212,174,40,98,96,113,237,146,163,30,186,69,139,67,157,37,83,30,161,170,92,20,88,126,31,148,49,32,133,35,206,117,26,155,154,102,119,184,196,144,146,104,188,112,247,229,124,82,237,146,81,215,157,134,60,215,44,249,56,157,202,243,236,105,6,130,117,190,131,93,11,78,190,251,41,176,233,229,147,70,59,56,145,165,174,147,111,242,160,241,201,203,118,88,132,232,178,22,117,168,133,225,76,86,240,4,246,158,51,191,121,245,52,164,72,112,109,131,246,21,37,123,166,143,107,76,69,128,184,87,200,23,39,48,21,138,248,161,140,8,151,174,127,172,55,217,205,164,191,41,40,21,65,102,38,231,245,193,137,82,79,200,219,185,83,168,181,87,125,247,47,112,61,195,99,115,20,131,186,193,11,9,106,72,43,175,52,216,11,212,247,27,151,35,162,53,237,20,208,201,47,218,30,26,86,4,185,26,142,255,250,59,219,212,222,152,155,131,117,49,122,51,2,78,187,225,215,90,99,196,255,71,7,19,218,205,190,132,213,178,193,114,94,81,104,244,138,51,254,116,38,23,73,190,214,169,31,220,41,172,172,205,57,159,93,237,199,135,219,63,101,42,222,90,192,55,33,98,171,182,18,47,63,214,206,6,76,251,28,178,150,174,154,63,127,87,170,172,107,230,55,12,253,165,15,70,55,191,80,114,158,203,222,55,160,212,2,147,59,21,136,61,126,64,193,123,132,221,255,44,162,133,94,13,143,204,9,115,165,105,81,111,100,16,20,44,80,204,209,171,181,144,67,195,225,253,33,209,171,193,155,183,25,142,150,232,170,231,94,127,241,58,4,97,162,46,174,96,95,35,15,98,225,46,228,222,216,179,230,92,153,238,220,253,247,20,57,60,168,242,236,186,70,34,78,184,1,123,95,111,107,91,156,13,3,71,10,125,232,250,215,249,13,223,48,132,48,156,61,129,121,46,148,222,129,228,3,189,217,19,25,6,29,125,180,13,205,152,222,79,198,73,205,97,155,156,229,15,195,235,128,254,9,137,30,63,146,187,223,104,18,14,16,87,252,21,24,8,109,65,199,160,20,169,27,205,213,146,211,166,109,129,203,224,196,235,198,154,251,215,11,228,122,121,254,118,28,97,98,132,36,48,194,97,45,127,162,252,193,144,38,51,235,54,12,75,128,236,76,100,184,5,223,113,92,189,7,229,144,154,126,214,210,14,217,255,3,240,73,227,232,82,220,33,93,55,112,7,32,76,178,247,134,245,50,157,101,68,55,237,191,114,53,220,235,56,124,190,163,8,192,216,1,26,101,153,181,14,174,1,213,255,111,61,46,65,109,153,124,203,111,65,13,206,176,115,10,245,43,152,144,152,119,211,90,150,27,127,253,111,51,198,47,247,9,224,245,233,188,100,81,18,83,249,180,142,84,249,109,215,178,75,102,203,163,250,154,185,204,231,27,239,194,79,230,120,16,249,205,89,189,108,124,23,74,196,171,118,88,103,14,22,9,250,115,195,156,95,122,201,54,2,16,21,23,8,89,182,241,168,220,85,132,206,169,166,61,26,8,208,0,235,19,187,18,27,197,95,7,147,13,48,151,71,243,218,70,3,189,109,122,228,228,24,168,225,138,111,120,81,184,200,93,61,118,227,105,174,242,24,113,22,201,161,12,120,202,28,112,5,43,46,172,168,46,226,229,44,48,20,91,10,59,228,230,236,190,97,0,217,185,27,113,192,7,100,14,89,93,107,66,95,179,55,124,165,105,168,237,38,3,204,188,169,189,35,154,233,228,249,76,132,68,255,95,118,181,182,132,234,105,157,178,176,132,195,183,64,62,170,40,20,135,99,6,238,75,241,135,101,245,19,28,239,79,117,251,11,161,103,148,230,156,97,82,245,248,36,29,77,232,24,61,115,206,10,85,185,234,141,140,183,158,86,215,114,111,36,207,92,252,210,111,241,157,99,97,115,199,170,90,232,213,30,185,191,109,54,216,177,170,225,232,90,48,170,58,215,86,94,202,54,39,191,150,47,189,120,234,161,59,91,219,95,38,160,190,181,2,118,54,254,2,89,50,63,108,80,84,9,115,125,140,243,178,117,100,5,38,237,219,200,62,254,182,2,127,93,146,47,79,13,150,88,221,178,75,232,71,43,46,81,92,163,53,180,207,202,243,40,229,120,166,221,150,6,20,155,26,22,24,92,163,85,117,214,210,191,112,30,227,92,77,38,111,245,111,152,252,10,35,129,158,153,153,20,150,170,209,86,177,142,81,169,6,55,32,170,218,25,23,47,109,30,135,107,196,169,200,79,178,229,12,27,72,239,20,61,227,166,234,174,86,177,184,197,72,161,19,162,9,20,149,29,178,171,11,75,218,23,33,14,131,126,156,122,48,129,25,221,92,199,2,61,248,38,79,17,27,76,70,170,222,166,233,192,96,154,178,148,64,21,136,4,86,222,208,52,174,19,163,74,215,130,27,118,18,129,120,192,188,202,75,171,253,246,36,163,136,10,247,100,10,119,218,67,158,103,253,48,145,245,89,110,244,154,197,9,238,221,133,47,78,160,159,204,225,4,169,209,223,136,127,179,234,176,178,32,244,109,158,123,117,143,142,122,200,37,130,29,87,226,67,209,10,52,142,29,49,199,189,107,161,95,142,101,182,230,0,3,181,31,212,100,211,194,68,88,239,122,38,199,231,62,143,195,76,198,217,49,152,198,98,169,6,111,161,208,229,37,157,26,175,58,235,194,12,130,208,47,58,96,40,177,162,78,24,167,121,80,70,17,255,86,54,223,60,120,243,107,155,52,51,131,168,140,230,94,207,167,67,162,146,199,15,19,23,147,144,161,222,118,19,234,109,214,155,142,65,191,43,26,239,28,102,167,172,222,237,26,148,36,49,105,211,242,73,60,84,80,26,221,208,83,138,48,87,250,120,37,33,218,171,246,97,171,6,247,86,59,64,65,8,160,226,204,127,68,59,77,12,12,224,140,114,52,166,11,25,44,233,41,5,3,182,252,41,105,251,215,25,153,44,189,144,150,73,1,142,202,175,232,240,68,218,90,200,93,130,163,19,144,168,137,22,244,71,172,30,111,151,230,127,28,105,156,154,140,249,220,201,203,136,93,28,117,47,165,141,138,122,248,160,217,119,113,167,209,245,182,1,252,150,25,54,36,62,194,197,136,4,190,12,126,29,230,234,246,252,254,36,99,6,237,175,42,95,94,164,0,220,157,137,123,47,38,152,42,140,245,31,54,38,211,14,178,164,2,211,234,250,121,117,69,98,190,78,20,238,181,194,96,144,209,138,137,232,226,205,225,115,24,57,129,221,89,191,35,96,149,25,151,73,139,213,145,7,162,175,53,142,12,248,115,235,28,27,222,249,253,184,233,157,176,24,138,48,219,6,132,62,41,213,69,73,110,37,72,178,91,57,202,151,2,180,18,216,169,25,181,105,47,144,143,2,187,101,193,163,107,54,42,18,254,14,108,234,166,29,33,219,251,212,119,76,247,204,242,159,49,224,204,189,75,30,135,183,182,147,252,14,187,185,86,6,228,160,111,45,122,79,195,199,30,106,201,237,247,201,72,29,186,226,152,54,181,133,122,118,173,45,197,146,66,247,83,25,176,166,117,207,96,149,25,198,175,41,154,59,109,225,184,107,236,150,25,34,202,13,43,92,96,249,85,73,54,22,172,123,6,174,149,76,59,19,63,195,115,254,12,143,253,116,104,204,9,75,118,200,248,23,178,80,193,61,48,128,232,47,156,18,18,158,148,238,118,11,167,70,41,112,70,48,178,1,174,82,110,22,16,245,110,154,81,216,72,211,37,132,72,185,141,157,102,134,78,205,218,30,109,59,236,102,25,38,221,165,122,246,131,120,66,137,225,50,184,190,74,195,26,32,155,162,132,142,156,141,32,2,209,213,139,198,59,168,211,168,199,46,189,74,203,219,127,108,221,43,124,185,228,187,250,239,195,8,221,31,84,161,24,80,133,244,168,50,199,166,173,233,92,225,38,1,54,145,7,160,122,105,0,32,234,2,89,146,61,78,132,101,85,31,164,226,151,123,53,197,167,56,212,124,226,220,252,7,161,98,26,46,140,40,136,42,158,224,188,172,96,245,228,231,224,19,99,127,123,162,38,111,213,4,169,9,38,253,178,208,83,237,62,224,131,82,90,104,235,13,88,191,215,199,182,210,225,25,64,216,125,102,252,136,181,125,116,52,11,149,169,37,139,183,73,178,160,210,184,143,164,202,70,104,52,141,189,138,2,105,15,214,66,15,180,136,205,72,233,248,103,141,34,179,176,190,125,226,194,107,143,147,164,226,90,12,17,135,137,31,15,146,15,78,151,177,88,61,19,157,195,159,16,1,144,109,100,25,169,117,64,189,36,176,62,18,199,120,137,164,82,192,209,137,235,200,110,117,175,96,198,31,20,251,69,110,100,70,184,11,0,0,206,99,32,159,72,64,138,233,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + +static const uint32_t DT_BASE_LEN = 3056; +static const uint8_t DT_PATCH[256] = {2,184,50,0,3,0,183,46,1,137,1,0,0,3,58,70,58,1,208,10,0,0,1,90,1,218,11,0,0,219,1,129,4,161,65,21,222,118,227,83,116,130,47,46,29,77,101,152,18,250,150,181,146,187,69,168,41,231,203,39,100,60,10,171,117,166,25,196,223,139,175,135,160,165,105,80,177,67,238,254,178,160,71,28,52,19,182,119,243,6,138,208,97,27,67,219,113,216,204,202,205,248,201,87,118,153,109,114,73,143,254,2,242,154,38,46,102,225,149,144,52,199,233,250,158,219,149,126,61,148,56,5,192,133,20,207,187,123,188,13,103,205,254,252,168,177,233,227,201,226,244,110,112,157,146,184,209,213,57,222,245,174,112,135,212,136,219,201,165,19,83,42,252,113,219,24,136,72,17,243,212,100,222,153,200,61,81,236,19,226,122,18,18,186,90,31,101,103,96,250,243,52,123,4,252,143,86,100,29,235,253,116,35,103,161,211,45,146,97,128,89,98,232,29,89,4,210,177,153,51,113,37,130,194,69,110,100,70,128,12,0,0,252,28,232,94,154,58,84,206,1,40,0,0,1,56}; + +static const uint32_t DT_PATCH_LEN = 256; +static const uint8_t DT_TARGET[3256] = {120,46,186,148,77,51,227,185,104,193,183,194,67,136,62,162,208,188,127,90,106,134,186,157,246,55,79,139,180,84,132,19,187,198,255,221,52,176,192,186,119,236,181,212,223,167,37,136,54,222,105,250,14,197,89,160,106,119,31,185,190,35,195,83,99,84,88,203,51,83,109,106,81,145,54,231,222,104,58,52,10,191,57,195,4,248,221,66,216,129,81,197,245,145,205,180,107,157,28,84,217,167,155,199,59,60,254,118,93,34,51,94,126,152,214,160,36,67,99,159,86,85,240,181,255,182,119,220,43,175,178,196,220,33,84,236,52,206,245,74,25,240,215,44,1,230,38,112,180,60,89,58,20,50,205,72,61,177,118,154,67,123,134,225,111,169,248,106,51,215,18,77,77,71,34,144,169,187,64,134,25,126,55,228,50,200,99,45,131,217,57,81,90,192,179,228,204,11,148,230,238,173,139,96,239,189,184,243,162,18,30,58,14,132,32,241,212,53,232,162,157,236,22,242,129,44,60,124,149,204,187,42,41,22,32,158,26,207,241,152,143,207,254,154,161,7,152,27,143,46,139,178,80,0,31,71,7,46,15,26,162,219,159,172,158,187,53,148,53,165,48,118,47,121,80,69,187,116,162,112,213,183,206,210,55,102,150,221,114,221,107,152,179,34,225,53,41,79,101,50,192,45,91,116,175,3,30,85,172,0,197,57,192,129,107,168,249,9,54,155,118,141,127,140,206,12,110,85,2,130,87,141,249,230,240,224,65,170,187,40,57,154,248,27,211,203,216,110,17,245,225,34,44,6,220,86,78,242,234,64,126,41,92,213,119,237,111,241,125,157,83,40,9,197,31,30,93,108,162,244,46,129,180,24,13,168,111,228,6,207,233,227,240,69,59,30,81,139,222,145,35,54,145,149,27,110,26,241,153,180,178,68,111,143,40,195,59,240,0,131,31,50,96,137,146,104,114,146,201,44,212,165,236,63,141,235,195,74,230,208,70,151,91,115,163,28,103,101,194,72,81,128,133,58,60,210,199,206,91,58,107,201,119,146,79,73,245,172,175,236,49,119,165,138,13,64,97,211,166,53,67,105,132,34,167,80,72,176,137,206,241,34,195,23,129,56,118,155,71,75,63,165,132,99,189,72,244,47,246,228,233,247,122,206,93,247,7,152,165,96,177,16,193,185,231,34,25,108,156,82,48,255,196,244,244,19,194,148,65,8,206,163,198,66,171,217,133,48,241,218,204,111,42,49,182,120,211,68,17,118,31,25,151,68,33,191,98,200,250,150,212,165,25,57,193,149,60,42,75,166,229,35,209,177,238,206,98,175,27,249,33,86,105,84,161,179,85,140,210,192,60,5,154,71,97,35,145,68,43,129,204,230,65,55,225,230,140,33,179,109,189,138,48,32,208,33,172,177,59,64,6,163,157,173,28,251,108,135,107,8,121,39,70,182,92,118,88,74,123,227,173,148,114,232,141,165,8,174,233,72,47,98,166,229,126,163,92,128,124,93,240,40,16,129,188,248,249,141,68,50,46,112,119,83,159,1,36,188,29,108,12,72,193,168,191,20,181,224,21,171,122,118,241,53,134,128,172,191,216,59,171,163,169,80,167,131,162,77,124,75,148,14,173,54,172,186,138,121,182,62,79,166,255,80,64,84,10,35,159,137,202,138,66,88,88,38,13,89,3,158,80,192,80,50,47,239,72,206,116,135,12,251,40,25,201,123,163,67,100,13,105,51,174,34,34,93,3,115,92,192,106,231,63,31,87,218,156,115,83,1,188,172,152,212,206,133,105,183,160,198,113,51,32,213,29,130,254,173,239,245,104,126,255,101,127,115,186,56,236,11,217,178,248,114,215,168,163,134,209,19,244,68,229,206,194,29,94,160,221,137,217,92,17,83,227,202,123,155,151,121,220,191,199,202,241,205,234,70,164,104,185,211,96,128,163,65,230,28,4,63,219,134,230,237,183,181,229,109,41,192,38,48,3,205,3,69,15,27,85,41,249,58,60,229,132,70,199,19,39,247,213,215,10,64,234,230,209,250,91,21,134,94,143,43,27,92,64,200,3,206,207,147,130,194,137,110,114,158,192,62,200,9,173,197,36,195,222,90,68,139,0,117,232,231,62,93,112,14,37,53,86,17,150,199,31,117,20,200,185,21,58,0,193,20,229,210,41,161,119,53,225,179,50,59,120,212,104,243,159,148,20,197,167,106,194,135,192,98,133,68,210,210,227,227,161,183,180,36,62,92,189,50,39,122,5,177,246,255,185,32,70,105,173,233,213,61,174,111,102,212,174,40,98,96,113,237,146,163,30,186,69,139,67,157,37,83,30,161,170,92,20,88,126,31,148,49,32,133,35,206,117,26,155,154,102,119,184,196,144,146,104,188,112,247,229,124,82,237,146,81,215,157,134,60,215,44,249,56,157,202,243,236,105,6,130,117,190,131,93,11,78,190,251,41,176,233,229,147,70,59,56,145,165,174,147,111,242,160,241,201,203,118,88,132,232,178,22,117,168,133,225,76,86,240,4,246,158,51,191,121,245,52,164,72,112,109,131,246,21,37,123,166,143,107,76,69,128,184,87,200,23,39,48,21,138,248,161,140,8,151,174,127,172,55,217,205,164,191,41,40,21,65,102,38,231,245,193,137,82,79,200,219,185,83,168,181,87,125,247,47,112,61,195,99,115,20,131,186,193,11,9,106,72,43,175,52,216,11,212,247,27,151,35,162,53,237,20,208,201,47,218,30,26,86,4,185,26,142,255,250,59,219,212,222,152,155,131,117,49,122,51,2,78,187,225,215,90,99,196,255,71,7,19,218,205,190,132,213,178,193,114,94,81,104,244,138,51,254,116,38,23,73,190,214,169,31,220,41,172,172,205,57,159,93,237,199,135,219,63,101,42,222,90,192,55,33,98,171,182,18,47,63,214,206,6,76,251,28,178,150,174,154,63,127,87,170,172,107,230,55,12,253,165,15,70,55,191,80,114,158,203,222,55,160,212,2,147,59,21,136,61,126,64,193,123,132,221,255,44,162,133,94,13,143,204,9,115,165,105,81,111,100,16,20,44,80,204,209,171,181,144,67,195,225,253,33,209,171,193,155,183,25,142,150,232,170,231,94,127,241,58,4,97,162,46,174,96,95,35,15,98,225,46,228,222,216,179,230,92,153,238,220,253,247,20,57,60,168,242,236,186,70,34,78,184,1,123,95,111,107,91,156,13,3,71,10,125,232,250,215,249,13,223,48,132,48,156,61,129,121,46,148,222,129,228,3,189,217,19,25,6,29,125,180,13,205,152,222,79,198,73,205,97,155,156,229,15,195,235,128,254,9,137,30,63,146,187,223,104,18,14,16,87,252,21,24,8,109,65,199,250,20,169,27,205,213,146,211,166,109,129,203,224,196,235,198,154,251,215,11,228,122,121,254,118,28,97,98,132,36,48,194,97,45,127,162,252,193,144,38,51,235,54,12,75,128,236,76,100,184,5,223,113,92,189,7,229,144,154,126,214,210,14,217,255,3,240,73,227,232,82,220,33,93,55,112,7,32,76,178,247,134,245,50,157,101,68,55,237,191,114,53,220,235,56,124,190,163,8,192,216,1,26,101,153,181,14,174,1,213,255,111,61,46,65,109,153,124,203,111,65,13,206,176,115,10,245,43,152,144,152,119,211,90,150,27,127,253,111,51,198,47,247,9,224,245,233,188,100,81,18,83,249,180,142,84,249,109,215,178,75,102,203,163,250,154,185,204,231,27,239,194,79,230,120,16,249,205,89,189,108,124,23,74,196,171,118,88,103,14,22,9,250,115,195,156,95,122,201,54,2,16,21,23,8,89,182,241,168,220,85,132,206,169,166,61,26,8,208,0,235,19,187,18,27,197,95,7,147,13,48,151,71,243,218,70,3,189,109,122,228,228,24,168,225,138,111,120,81,184,200,93,61,118,227,105,174,242,24,113,22,201,161,12,120,202,28,112,5,43,46,172,168,46,226,229,44,48,20,91,10,59,228,230,236,190,97,0,217,185,27,113,192,7,100,14,89,93,107,66,95,179,55,124,165,105,168,237,38,3,204,188,169,189,35,154,233,228,249,76,132,68,255,95,118,181,182,132,234,105,157,178,176,132,195,183,64,62,170,40,20,135,99,6,238,75,241,135,101,245,19,28,239,79,117,251,11,161,103,148,230,156,97,82,245,248,36,29,77,232,24,61,115,206,10,85,185,234,141,140,183,158,86,215,114,111,36,207,92,252,210,111,241,157,99,97,115,199,170,90,232,213,30,185,191,109,54,216,177,170,225,232,90,48,170,58,215,86,94,202,54,39,191,150,47,189,120,234,161,59,91,219,95,38,160,190,181,2,118,54,254,2,89,50,63,108,80,84,9,115,125,140,243,178,117,100,5,38,237,219,200,62,254,182,2,127,93,146,47,79,13,150,88,221,178,75,232,71,43,46,81,92,163,53,180,207,202,243,40,229,120,166,221,150,6,20,155,26,22,24,92,163,85,117,214,210,191,112,30,227,92,77,38,111,245,111,152,252,10,35,129,158,153,153,20,150,170,209,86,177,142,81,169,6,55,32,170,218,25,23,47,109,30,135,107,196,169,200,79,178,229,12,27,72,239,20,61,227,166,234,174,86,177,184,197,72,161,19,162,9,20,149,29,178,171,11,75,218,23,33,14,131,126,156,122,48,129,25,221,92,199,2,61,248,38,79,17,27,76,70,170,222,166,233,192,96,154,178,148,64,21,136,4,86,222,208,52,174,19,163,74,215,130,27,118,18,129,120,192,188,202,75,171,253,246,36,163,136,10,247,100,10,119,218,67,158,103,253,48,145,245,89,110,244,154,197,9,238,221,133,47,78,160,159,204,225,4,169,209,223,136,127,179,234,176,178,32,244,109,158,123,117,143,142,122,200,37,130,29,87,226,67,209,10,52,142,29,49,199,189,107,161,95,142,101,182,230,0,3,181,31,212,100,211,194,68,88,239,122,38,199,231,62,143,195,76,198,217,49,152,198,98,169,6,111,161,208,229,37,157,26,175,58,235,194,12,130,208,47,58,96,40,177,162,78,24,167,121,80,70,17,255,86,54,223,60,120,243,107,155,52,51,131,168,140,230,94,207,167,67,162,146,199,15,19,23,147,144,161,222,118,19,234,109,214,155,142,65,191,43,26,239,28,102,167,172,222,237,26,148,36,49,105,211,242,73,60,84,80,26,221,208,83,138,48,87,250,120,37,33,218,171,246,97,171,6,247,86,59,64,65,8,160,226,204,127,68,59,77,12,12,224,140,114,52,166,11,25,44,233,41,5,3,182,252,41,105,251,215,25,153,44,189,144,150,73,1,142,202,175,232,240,68,218,90,200,93,130,163,19,144,168,137,22,244,71,172,30,111,151,230,127,28,105,156,154,140,249,220,201,203,136,93,28,117,47,165,141,138,122,248,160,217,119,113,167,209,245,182,1,252,150,25,54,36,62,194,197,136,4,190,12,126,29,230,234,246,252,254,36,99,6,237,175,42,95,94,164,0,220,157,137,123,47,38,152,42,140,245,31,54,38,211,14,178,164,2,211,234,250,121,117,69,98,190,78,20,238,181,194,96,144,209,138,137,232,226,205,225,115,24,57,129,221,89,191,35,96,149,25,151,73,139,213,145,7,162,175,53,142,12,248,115,235,28,27,222,249,253,184,233,157,176,24,138,48,219,6,132,62,41,213,69,73,110,37,72,178,91,57,202,151,2,180,18,216,169,25,181,105,47,144,143,2,187,101,193,163,107,54,42,18,254,14,108,234,166,29,33,219,251,212,119,76,247,204,242,159,49,224,204,189,75,30,135,183,182,147,252,14,187,185,86,6,228,160,111,45,122,79,195,199,30,106,201,237,247,201,72,29,186,226,152,54,181,133,122,118,173,45,197,146,66,247,83,25,176,166,117,207,96,149,25,198,175,41,154,59,109,225,184,107,236,150,25,34,202,13,43,92,96,249,85,73,54,22,172,123,6,174,149,76,59,19,63,195,115,254,12,143,253,116,104,204,9,75,118,200,248,23,178,80,193,61,48,128,232,47,156,18,18,158,148,238,118,11,167,70,41,112,70,48,178,1,174,82,110,22,16,245,110,154,81,216,72,211,37,132,72,185,141,157,102,134,78,205,218,30,109,59,236,102,25,38,221,165,122,246,131,120,66,137,225,50,184,190,74,195,26,32,155,162,132,142,156,141,32,2,209,213,139,198,59,168,211,168,199,46,189,74,203,219,127,108,221,43,124,185,228,187,250,239,195,8,221,31,84,161,24,80,133,244,168,50,199,166,173,233,92,225,38,1,54,145,7,160,122,105,0,32,234,2,89,146,61,78,132,101,85,31,164,226,151,123,53,197,167,56,212,124,226,220,252,7,161,98,26,46,140,40,136,42,158,224,188,172,96,245,228,231,224,19,99,127,123,162,38,111,213,4,169,9,38,253,178,208,83,237,62,224,131,82,90,104,235,13,88,191,215,199,182,210,225,25,64,216,125,102,252,136,181,125,116,52,11,149,169,37,139,183,73,178,160,210,184,143,164,202,70,104,52,141,189,138,2,105,15,214,66,15,180,136,205,72,233,248,103,141,34,179,176,190,125,226,194,107,143,147,164,226,90,12,17,135,137,31,15,146,15,78,151,177,88,61,19,157,195,159,16,1,144,109,100,25,169,117,64,189,36,176,62,18,199,120,137,164,82,192,209,137,235,200,110,117,175,96,198,31,20,161,65,21,222,118,227,83,116,130,47,46,29,77,101,152,18,250,150,181,146,187,69,168,41,231,203,39,100,60,10,171,117,166,25,196,223,139,175,135,160,165,105,80,177,67,238,254,178,160,71,28,52,19,182,119,243,6,138,208,97,27,67,219,113,216,204,202,205,248,201,87,118,153,109,114,73,143,254,2,242,154,38,46,102,225,149,144,52,199,233,250,158,219,149,126,61,148,56,5,192,133,20,207,187,123,188,13,103,205,254,252,168,177,233,227,201,226,244,110,112,157,146,184,209,213,57,222,245,174,112,135,212,136,219,201,165,19,83,42,252,113,219,24,136,72,17,243,212,100,222,153,200,61,81,236,19,226,122,18,18,186,90,31,101,103,96,250,243,52,123,4,252,143,86,100,29,235,253,116,35,103,161,211,45,146,97,128,89,98,232,29,89,4,210,177,153,51,113,37,130,194,69,110,100,70,128,12,0,0,252,28,232,94,154,58,84,206,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + +static const uint32_t DT_TARGET_LEN = 3256; +// detools in-place+crle delta: apply DT_IP_PATCH over a DT_IP_MEM buffer holding DT_IP_BASE +static const uint8_t DT_IP_BASE[3056] = {120,46,186,148,77,51,227,185,104,193,183,194,67,136,62,162,208,188,127,90,106,134,186,157,246,55,79,139,180,84,132,19,187,198,255,221,52,176,192,186,119,236,181,212,223,167,37,136,54,222,105,250,14,197,89,160,106,119,31,185,190,35,195,83,99,84,88,203,51,83,109,106,81,145,54,231,222,104,58,52,10,191,57,195,4,248,221,66,216,129,81,197,245,145,205,180,107,157,28,84,217,167,155,199,59,60,254,118,93,34,51,94,126,152,214,160,36,67,99,159,86,85,240,181,255,182,119,220,43,175,178,196,220,33,84,236,52,148,175,16,25,240,215,44,1,230,38,112,180,60,89,58,20,50,205,72,61,177,118,154,67,123,134,225,111,169,248,106,51,215,18,77,77,71,34,144,169,187,64,134,25,126,55,228,50,200,99,45,131,217,57,81,90,192,179,228,204,11,148,230,238,173,139,96,239,189,184,243,162,18,30,58,14,132,32,241,212,53,232,162,157,236,22,242,129,44,60,124,149,204,187,42,41,22,32,158,26,207,241,152,143,207,254,154,161,7,152,27,143,46,139,178,80,0,31,71,7,46,15,26,162,219,159,172,158,187,53,148,53,165,48,118,47,121,80,69,187,116,162,112,213,183,206,210,55,102,150,221,114,221,107,152,179,34,225,53,41,79,101,50,192,45,91,116,175,3,30,85,172,0,197,57,192,129,107,168,249,9,54,155,118,141,127,140,206,12,110,85,2,130,87,141,249,230,240,224,65,170,187,40,57,154,248,27,211,203,216,110,17,245,225,34,44,6,220,86,78,242,234,64,126,41,92,213,119,237,111,241,125,157,83,40,9,197,31,30,93,108,162,244,46,129,180,24,13,168,111,228,6,207,233,227,240,69,59,30,81,139,222,145,35,54,145,149,27,110,26,241,153,180,178,68,111,143,40,195,59,240,0,131,31,50,96,137,146,104,114,146,201,44,212,165,236,63,141,235,195,74,230,208,70,151,91,115,163,28,103,101,194,72,81,128,133,58,60,210,199,206,91,58,107,201,119,146,79,73,245,172,175,236,49,119,165,138,13,64,97,211,166,53,67,105,132,34,167,80,72,176,137,206,241,34,195,23,129,56,118,155,71,75,63,165,132,99,189,72,244,47,246,228,233,247,122,206,93,247,7,152,165,96,177,16,193,185,231,34,25,108,156,82,48,255,196,244,244,19,194,148,65,8,206,163,198,66,171,217,133,48,241,218,204,111,42,49,182,120,211,68,17,118,31,25,151,68,33,191,98,200,250,150,212,165,25,57,193,149,60,42,75,166,229,35,209,177,238,206,98,175,27,249,33,86,105,84,161,179,85,140,210,192,60,5,154,71,97,35,145,68,43,129,204,230,65,55,225,230,140,33,179,109,189,138,48,32,208,33,172,177,59,64,6,163,157,173,28,251,108,135,107,8,121,39,70,182,92,118,88,74,123,227,173,148,114,232,141,165,8,174,233,72,47,98,166,229,126,163,92,128,124,93,240,40,16,129,188,248,249,141,68,50,46,112,119,83,159,1,36,188,29,108,12,72,193,168,191,20,181,224,21,171,122,118,241,53,134,128,172,191,216,59,171,163,169,80,167,131,162,77,124,75,148,14,173,54,172,186,138,121,182,62,79,166,255,80,64,84,10,35,159,137,202,138,66,88,88,38,13,89,3,158,80,192,80,50,47,239,72,206,116,135,12,251,40,25,201,123,163,67,100,13,105,51,174,34,34,93,3,115,92,192,106,231,63,31,87,218,156,115,83,1,188,172,152,212,206,133,105,183,160,198,113,51,32,213,29,130,254,173,239,245,104,126,255,101,127,115,186,56,236,11,217,178,248,114,215,168,163,134,209,19,244,68,229,206,194,29,94,160,221,137,217,92,17,83,227,202,123,155,151,121,220,191,199,202,241,205,234,70,164,104,185,211,96,128,163,65,230,28,4,63,219,134,230,237,183,181,229,109,41,192,38,48,3,205,3,69,15,27,85,41,249,58,60,229,132,70,199,19,39,247,213,215,10,64,234,230,209,250,91,21,134,94,143,43,27,92,64,200,3,206,207,147,130,194,137,110,114,158,192,62,200,9,173,197,36,195,222,90,68,139,0,117,232,231,62,93,112,14,37,53,86,17,150,199,31,117,20,200,185,21,58,0,193,20,229,210,41,161,119,53,225,179,50,59,120,212,104,243,159,148,20,197,167,106,194,135,192,98,133,68,210,210,227,227,161,183,180,36,62,92,189,50,39,122,5,177,246,255,185,32,70,105,173,233,213,61,174,111,102,212,174,40,98,96,113,237,146,163,30,186,69,139,67,157,37,83,30,161,170,92,20,88,126,31,148,49,32,133,35,206,117,26,155,154,102,119,184,196,144,146,104,188,112,247,229,124,82,237,146,81,215,157,134,60,215,44,249,56,157,202,243,236,105,6,130,117,190,131,93,11,78,190,251,41,176,233,229,147,70,59,56,145,165,174,147,111,242,160,241,201,203,118,88,132,232,178,22,117,168,133,225,76,86,240,4,246,158,51,191,121,245,52,164,72,112,109,131,246,21,37,123,166,143,107,76,69,128,184,87,200,23,39,48,21,138,248,161,140,8,151,174,127,172,55,217,205,164,191,41,40,21,65,102,38,231,245,193,137,82,79,200,219,185,83,168,181,87,125,247,47,112,61,195,99,115,20,131,186,193,11,9,106,72,43,175,52,216,11,212,247,27,151,35,162,53,237,20,208,201,47,218,30,26,86,4,185,26,142,255,250,59,219,212,222,152,155,131,117,49,122,51,2,78,187,225,215,90,99,196,255,71,7,19,218,205,190,132,213,178,193,114,94,81,104,244,138,51,254,116,38,23,73,190,214,169,31,220,41,172,172,205,57,159,93,237,199,135,219,63,101,42,222,90,192,55,33,98,171,182,18,47,63,214,206,6,76,251,28,178,150,174,154,63,127,87,170,172,107,230,55,12,253,165,15,70,55,191,80,114,158,203,222,55,160,212,2,147,59,21,136,61,126,64,193,123,132,221,255,44,162,133,94,13,143,204,9,115,165,105,81,111,100,16,20,44,80,204,209,171,181,144,67,195,225,253,33,209,171,193,155,183,25,142,150,232,170,231,94,127,241,58,4,97,162,46,174,96,95,35,15,98,225,46,228,222,216,179,230,92,153,238,220,253,247,20,57,60,168,242,236,186,70,34,78,184,1,123,95,111,107,91,156,13,3,71,10,125,232,250,215,249,13,223,48,132,48,156,61,129,121,46,148,222,129,228,3,189,217,19,25,6,29,125,180,13,205,152,222,79,198,73,205,97,155,156,229,15,195,235,128,254,9,137,30,63,146,187,223,104,18,14,16,87,252,21,24,8,109,65,199,160,20,169,27,205,213,146,211,166,109,129,203,224,196,235,198,154,251,215,11,228,122,121,254,118,28,97,98,132,36,48,194,97,45,127,162,252,193,144,38,51,235,54,12,75,128,236,76,100,184,5,223,113,92,189,7,229,144,154,126,214,210,14,217,255,3,240,73,227,232,82,220,33,93,55,112,7,32,76,178,247,134,245,50,157,101,68,55,237,191,114,53,220,235,56,124,190,163,8,192,216,1,26,101,153,181,14,174,1,213,255,111,61,46,65,109,153,124,203,111,65,13,206,176,115,10,245,43,152,144,152,119,211,90,150,27,127,253,111,51,198,47,247,9,224,245,233,188,100,81,18,83,249,180,142,84,249,109,215,178,75,102,203,163,250,154,185,204,231,27,239,194,79,230,120,16,249,205,89,189,108,124,23,74,196,171,118,88,103,14,22,9,250,115,195,156,95,122,201,54,2,16,21,23,8,89,182,241,168,220,85,132,206,169,166,61,26,8,208,0,235,19,187,18,27,197,95,7,147,13,48,151,71,243,218,70,3,189,109,122,228,228,24,168,225,138,111,120,81,184,200,93,61,118,227,105,174,242,24,113,22,201,161,12,120,202,28,112,5,43,46,172,168,46,226,229,44,48,20,91,10,59,228,230,236,190,97,0,217,185,27,113,192,7,100,14,89,93,107,66,95,179,55,124,165,105,168,237,38,3,204,188,169,189,35,154,233,228,249,76,132,68,255,95,118,181,182,132,234,105,157,178,176,132,195,183,64,62,170,40,20,135,99,6,238,75,241,135,101,245,19,28,239,79,117,251,11,161,103,148,230,156,97,82,245,248,36,29,77,232,24,61,115,206,10,85,185,234,141,140,183,158,86,215,114,111,36,207,92,252,210,111,241,157,99,97,115,199,170,90,232,213,30,185,191,109,54,216,177,170,225,232,90,48,170,58,215,86,94,202,54,39,191,150,47,189,120,234,161,59,91,219,95,38,160,190,181,2,118,54,254,2,89,50,63,108,80,84,9,115,125,140,243,178,117,100,5,38,237,219,200,62,254,182,2,127,93,146,47,79,13,150,88,221,178,75,232,71,43,46,81,92,163,53,180,207,202,243,40,229,120,166,221,150,6,20,155,26,22,24,92,163,85,117,214,210,191,112,30,227,92,77,38,111,245,111,152,252,10,35,129,158,153,153,20,150,170,209,86,177,142,81,169,6,55,32,170,218,25,23,47,109,30,135,107,196,169,200,79,178,229,12,27,72,239,20,61,227,166,234,174,86,177,184,197,72,161,19,162,9,20,149,29,178,171,11,75,218,23,33,14,131,126,156,122,48,129,25,221,92,199,2,61,248,38,79,17,27,76,70,170,222,166,233,192,96,154,178,148,64,21,136,4,86,222,208,52,174,19,163,74,215,130,27,118,18,129,120,192,188,202,75,171,253,246,36,163,136,10,247,100,10,119,218,67,158,103,253,48,145,245,89,110,244,154,197,9,238,221,133,47,78,160,159,204,225,4,169,209,223,136,127,179,234,176,178,32,244,109,158,123,117,143,142,122,200,37,130,29,87,226,67,209,10,52,142,29,49,199,189,107,161,95,142,101,182,230,0,3,181,31,212,100,211,194,68,88,239,122,38,199,231,62,143,195,76,198,217,49,152,198,98,169,6,111,161,208,229,37,157,26,175,58,235,194,12,130,208,47,58,96,40,177,162,78,24,167,121,80,70,17,255,86,54,223,60,120,243,107,155,52,51,131,168,140,230,94,207,167,67,162,146,199,15,19,23,147,144,161,222,118,19,234,109,214,155,142,65,191,43,26,239,28,102,167,172,222,237,26,148,36,49,105,211,242,73,60,84,80,26,221,208,83,138,48,87,250,120,37,33,218,171,246,97,171,6,247,86,59,64,65,8,160,226,204,127,68,59,77,12,12,224,140,114,52,166,11,25,44,233,41,5,3,182,252,41,105,251,215,25,153,44,189,144,150,73,1,142,202,175,232,240,68,218,90,200,93,130,163,19,144,168,137,22,244,71,172,30,111,151,230,127,28,105,156,154,140,249,220,201,203,136,93,28,117,47,165,141,138,122,248,160,217,119,113,167,209,245,182,1,252,150,25,54,36,62,194,197,136,4,190,12,126,29,230,234,246,252,254,36,99,6,237,175,42,95,94,164,0,220,157,137,123,47,38,152,42,140,245,31,54,38,211,14,178,164,2,211,234,250,121,117,69,98,190,78,20,238,181,194,96,144,209,138,137,232,226,205,225,115,24,57,129,221,89,191,35,96,149,25,151,73,139,213,145,7,162,175,53,142,12,248,115,235,28,27,222,249,253,184,233,157,176,24,138,48,219,6,132,62,41,213,69,73,110,37,72,178,91,57,202,151,2,180,18,216,169,25,181,105,47,144,143,2,187,101,193,163,107,54,42,18,254,14,108,234,166,29,33,219,251,212,119,76,247,204,242,159,49,224,204,189,75,30,135,183,182,147,252,14,187,185,86,6,228,160,111,45,122,79,195,199,30,106,201,237,247,201,72,29,186,226,152,54,181,133,122,118,173,45,197,146,66,247,83,25,176,166,117,207,96,149,25,198,175,41,154,59,109,225,184,107,236,150,25,34,202,13,43,92,96,249,85,73,54,22,172,123,6,174,149,76,59,19,63,195,115,254,12,143,253,116,104,204,9,75,118,200,248,23,178,80,193,61,48,128,232,47,156,18,18,158,148,238,118,11,167,70,41,112,70,48,178,1,174,82,110,22,16,245,110,154,81,216,72,211,37,132,72,185,141,157,102,134,78,205,218,30,109,59,236,102,25,38,221,165,122,246,131,120,66,137,225,50,184,190,74,195,26,32,155,162,132,142,156,141,32,2,209,213,139,198,59,168,211,168,199,46,189,74,203,219,127,108,221,43,124,185,228,187,250,239,195,8,221,31,84,161,24,80,133,244,168,50,199,166,173,233,92,225,38,1,54,145,7,160,122,105,0,32,234,2,89,146,61,78,132,101,85,31,164,226,151,123,53,197,167,56,212,124,226,220,252,7,161,98,26,46,140,40,136,42,158,224,188,172,96,245,228,231,224,19,99,127,123,162,38,111,213,4,169,9,38,253,178,208,83,237,62,224,131,82,90,104,235,13,88,191,215,199,182,210,225,25,64,216,125,102,252,136,181,125,116,52,11,149,169,37,139,183,73,178,160,210,184,143,164,202,70,104,52,141,189,138,2,105,15,214,66,15,180,136,205,72,233,248,103,141,34,179,176,190,125,226,194,107,143,147,164,226,90,12,17,135,137,31,15,146,15,78,151,177,88,61,19,157,195,159,16,1,144,109,100,25,169,117,64,189,36,176,62,18,199,120,137,164,82,192,209,137,235,200,110,117,175,96,198,31,20,251,69,110,100,70,184,11,0,0,206,99,32,159,72,64,138,233,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + +static const uint32_t DT_IP_BASE_LEN = 3056; +static const uint8_t DT_IP_PATCH[266] = {18,128,128,4,128,64,128,192,3,176,47,184,50,0,3,0,183,46,1,137,1,0,0,3,58,70,58,1,208,10,0,0,1,90,1,218,11,0,0,219,1,129,4,161,65,21,222,118,227,83,116,130,47,46,29,77,101,152,18,250,150,181,146,187,69,168,41,231,203,39,100,60,10,171,117,166,25,196,223,139,175,135,160,165,105,80,177,67,238,254,178,160,71,28,52,19,182,119,243,6,138,208,97,27,67,219,113,216,204,202,205,248,201,87,118,153,109,114,73,143,254,2,242,154,38,46,102,225,149,144,52,199,233,250,158,219,149,126,61,148,56,5,192,133,20,207,187,123,188,13,103,205,254,252,168,177,233,227,201,226,244,110,112,157,146,184,209,213,57,222,245,174,112,135,212,136,219,201,165,19,83,42,252,113,219,24,136,72,17,243,212,100,222,153,200,61,81,236,19,226,122,18,18,186,90,31,101,103,96,250,243,52,123,4,252,143,86,100,29,235,253,116,35,103,161,211,45,146,97,128,89,98,232,29,89,4,210,177,153,51,113,37,130,194,69,110,100,70,128,12,0,0,252,28,232,94,154,58,84,206,1,40,0,0,1,56}; + +static const uint32_t DT_IP_PATCH_LEN = 266; +static const uint8_t DT_IP_TARGET[3256] = {120,46,186,148,77,51,227,185,104,193,183,194,67,136,62,162,208,188,127,90,106,134,186,157,246,55,79,139,180,84,132,19,187,198,255,221,52,176,192,186,119,236,181,212,223,167,37,136,54,222,105,250,14,197,89,160,106,119,31,185,190,35,195,83,99,84,88,203,51,83,109,106,81,145,54,231,222,104,58,52,10,191,57,195,4,248,221,66,216,129,81,197,245,145,205,180,107,157,28,84,217,167,155,199,59,60,254,118,93,34,51,94,126,152,214,160,36,67,99,159,86,85,240,181,255,182,119,220,43,175,178,196,220,33,84,236,52,206,245,74,25,240,215,44,1,230,38,112,180,60,89,58,20,50,205,72,61,177,118,154,67,123,134,225,111,169,248,106,51,215,18,77,77,71,34,144,169,187,64,134,25,126,55,228,50,200,99,45,131,217,57,81,90,192,179,228,204,11,148,230,238,173,139,96,239,189,184,243,162,18,30,58,14,132,32,241,212,53,232,162,157,236,22,242,129,44,60,124,149,204,187,42,41,22,32,158,26,207,241,152,143,207,254,154,161,7,152,27,143,46,139,178,80,0,31,71,7,46,15,26,162,219,159,172,158,187,53,148,53,165,48,118,47,121,80,69,187,116,162,112,213,183,206,210,55,102,150,221,114,221,107,152,179,34,225,53,41,79,101,50,192,45,91,116,175,3,30,85,172,0,197,57,192,129,107,168,249,9,54,155,118,141,127,140,206,12,110,85,2,130,87,141,249,230,240,224,65,170,187,40,57,154,248,27,211,203,216,110,17,245,225,34,44,6,220,86,78,242,234,64,126,41,92,213,119,237,111,241,125,157,83,40,9,197,31,30,93,108,162,244,46,129,180,24,13,168,111,228,6,207,233,227,240,69,59,30,81,139,222,145,35,54,145,149,27,110,26,241,153,180,178,68,111,143,40,195,59,240,0,131,31,50,96,137,146,104,114,146,201,44,212,165,236,63,141,235,195,74,230,208,70,151,91,115,163,28,103,101,194,72,81,128,133,58,60,210,199,206,91,58,107,201,119,146,79,73,245,172,175,236,49,119,165,138,13,64,97,211,166,53,67,105,132,34,167,80,72,176,137,206,241,34,195,23,129,56,118,155,71,75,63,165,132,99,189,72,244,47,246,228,233,247,122,206,93,247,7,152,165,96,177,16,193,185,231,34,25,108,156,82,48,255,196,244,244,19,194,148,65,8,206,163,198,66,171,217,133,48,241,218,204,111,42,49,182,120,211,68,17,118,31,25,151,68,33,191,98,200,250,150,212,165,25,57,193,149,60,42,75,166,229,35,209,177,238,206,98,175,27,249,33,86,105,84,161,179,85,140,210,192,60,5,154,71,97,35,145,68,43,129,204,230,65,55,225,230,140,33,179,109,189,138,48,32,208,33,172,177,59,64,6,163,157,173,28,251,108,135,107,8,121,39,70,182,92,118,88,74,123,227,173,148,114,232,141,165,8,174,233,72,47,98,166,229,126,163,92,128,124,93,240,40,16,129,188,248,249,141,68,50,46,112,119,83,159,1,36,188,29,108,12,72,193,168,191,20,181,224,21,171,122,118,241,53,134,128,172,191,216,59,171,163,169,80,167,131,162,77,124,75,148,14,173,54,172,186,138,121,182,62,79,166,255,80,64,84,10,35,159,137,202,138,66,88,88,38,13,89,3,158,80,192,80,50,47,239,72,206,116,135,12,251,40,25,201,123,163,67,100,13,105,51,174,34,34,93,3,115,92,192,106,231,63,31,87,218,156,115,83,1,188,172,152,212,206,133,105,183,160,198,113,51,32,213,29,130,254,173,239,245,104,126,255,101,127,115,186,56,236,11,217,178,248,114,215,168,163,134,209,19,244,68,229,206,194,29,94,160,221,137,217,92,17,83,227,202,123,155,151,121,220,191,199,202,241,205,234,70,164,104,185,211,96,128,163,65,230,28,4,63,219,134,230,237,183,181,229,109,41,192,38,48,3,205,3,69,15,27,85,41,249,58,60,229,132,70,199,19,39,247,213,215,10,64,234,230,209,250,91,21,134,94,143,43,27,92,64,200,3,206,207,147,130,194,137,110,114,158,192,62,200,9,173,197,36,195,222,90,68,139,0,117,232,231,62,93,112,14,37,53,86,17,150,199,31,117,20,200,185,21,58,0,193,20,229,210,41,161,119,53,225,179,50,59,120,212,104,243,159,148,20,197,167,106,194,135,192,98,133,68,210,210,227,227,161,183,180,36,62,92,189,50,39,122,5,177,246,255,185,32,70,105,173,233,213,61,174,111,102,212,174,40,98,96,113,237,146,163,30,186,69,139,67,157,37,83,30,161,170,92,20,88,126,31,148,49,32,133,35,206,117,26,155,154,102,119,184,196,144,146,104,188,112,247,229,124,82,237,146,81,215,157,134,60,215,44,249,56,157,202,243,236,105,6,130,117,190,131,93,11,78,190,251,41,176,233,229,147,70,59,56,145,165,174,147,111,242,160,241,201,203,118,88,132,232,178,22,117,168,133,225,76,86,240,4,246,158,51,191,121,245,52,164,72,112,109,131,246,21,37,123,166,143,107,76,69,128,184,87,200,23,39,48,21,138,248,161,140,8,151,174,127,172,55,217,205,164,191,41,40,21,65,102,38,231,245,193,137,82,79,200,219,185,83,168,181,87,125,247,47,112,61,195,99,115,20,131,186,193,11,9,106,72,43,175,52,216,11,212,247,27,151,35,162,53,237,20,208,201,47,218,30,26,86,4,185,26,142,255,250,59,219,212,222,152,155,131,117,49,122,51,2,78,187,225,215,90,99,196,255,71,7,19,218,205,190,132,213,178,193,114,94,81,104,244,138,51,254,116,38,23,73,190,214,169,31,220,41,172,172,205,57,159,93,237,199,135,219,63,101,42,222,90,192,55,33,98,171,182,18,47,63,214,206,6,76,251,28,178,150,174,154,63,127,87,170,172,107,230,55,12,253,165,15,70,55,191,80,114,158,203,222,55,160,212,2,147,59,21,136,61,126,64,193,123,132,221,255,44,162,133,94,13,143,204,9,115,165,105,81,111,100,16,20,44,80,204,209,171,181,144,67,195,225,253,33,209,171,193,155,183,25,142,150,232,170,231,94,127,241,58,4,97,162,46,174,96,95,35,15,98,225,46,228,222,216,179,230,92,153,238,220,253,247,20,57,60,168,242,236,186,70,34,78,184,1,123,95,111,107,91,156,13,3,71,10,125,232,250,215,249,13,223,48,132,48,156,61,129,121,46,148,222,129,228,3,189,217,19,25,6,29,125,180,13,205,152,222,79,198,73,205,97,155,156,229,15,195,235,128,254,9,137,30,63,146,187,223,104,18,14,16,87,252,21,24,8,109,65,199,250,20,169,27,205,213,146,211,166,109,129,203,224,196,235,198,154,251,215,11,228,122,121,254,118,28,97,98,132,36,48,194,97,45,127,162,252,193,144,38,51,235,54,12,75,128,236,76,100,184,5,223,113,92,189,7,229,144,154,126,214,210,14,217,255,3,240,73,227,232,82,220,33,93,55,112,7,32,76,178,247,134,245,50,157,101,68,55,237,191,114,53,220,235,56,124,190,163,8,192,216,1,26,101,153,181,14,174,1,213,255,111,61,46,65,109,153,124,203,111,65,13,206,176,115,10,245,43,152,144,152,119,211,90,150,27,127,253,111,51,198,47,247,9,224,245,233,188,100,81,18,83,249,180,142,84,249,109,215,178,75,102,203,163,250,154,185,204,231,27,239,194,79,230,120,16,249,205,89,189,108,124,23,74,196,171,118,88,103,14,22,9,250,115,195,156,95,122,201,54,2,16,21,23,8,89,182,241,168,220,85,132,206,169,166,61,26,8,208,0,235,19,187,18,27,197,95,7,147,13,48,151,71,243,218,70,3,189,109,122,228,228,24,168,225,138,111,120,81,184,200,93,61,118,227,105,174,242,24,113,22,201,161,12,120,202,28,112,5,43,46,172,168,46,226,229,44,48,20,91,10,59,228,230,236,190,97,0,217,185,27,113,192,7,100,14,89,93,107,66,95,179,55,124,165,105,168,237,38,3,204,188,169,189,35,154,233,228,249,76,132,68,255,95,118,181,182,132,234,105,157,178,176,132,195,183,64,62,170,40,20,135,99,6,238,75,241,135,101,245,19,28,239,79,117,251,11,161,103,148,230,156,97,82,245,248,36,29,77,232,24,61,115,206,10,85,185,234,141,140,183,158,86,215,114,111,36,207,92,252,210,111,241,157,99,97,115,199,170,90,232,213,30,185,191,109,54,216,177,170,225,232,90,48,170,58,215,86,94,202,54,39,191,150,47,189,120,234,161,59,91,219,95,38,160,190,181,2,118,54,254,2,89,50,63,108,80,84,9,115,125,140,243,178,117,100,5,38,237,219,200,62,254,182,2,127,93,146,47,79,13,150,88,221,178,75,232,71,43,46,81,92,163,53,180,207,202,243,40,229,120,166,221,150,6,20,155,26,22,24,92,163,85,117,214,210,191,112,30,227,92,77,38,111,245,111,152,252,10,35,129,158,153,153,20,150,170,209,86,177,142,81,169,6,55,32,170,218,25,23,47,109,30,135,107,196,169,200,79,178,229,12,27,72,239,20,61,227,166,234,174,86,177,184,197,72,161,19,162,9,20,149,29,178,171,11,75,218,23,33,14,131,126,156,122,48,129,25,221,92,199,2,61,248,38,79,17,27,76,70,170,222,166,233,192,96,154,178,148,64,21,136,4,86,222,208,52,174,19,163,74,215,130,27,118,18,129,120,192,188,202,75,171,253,246,36,163,136,10,247,100,10,119,218,67,158,103,253,48,145,245,89,110,244,154,197,9,238,221,133,47,78,160,159,204,225,4,169,209,223,136,127,179,234,176,178,32,244,109,158,123,117,143,142,122,200,37,130,29,87,226,67,209,10,52,142,29,49,199,189,107,161,95,142,101,182,230,0,3,181,31,212,100,211,194,68,88,239,122,38,199,231,62,143,195,76,198,217,49,152,198,98,169,6,111,161,208,229,37,157,26,175,58,235,194,12,130,208,47,58,96,40,177,162,78,24,167,121,80,70,17,255,86,54,223,60,120,243,107,155,52,51,131,168,140,230,94,207,167,67,162,146,199,15,19,23,147,144,161,222,118,19,234,109,214,155,142,65,191,43,26,239,28,102,167,172,222,237,26,148,36,49,105,211,242,73,60,84,80,26,221,208,83,138,48,87,250,120,37,33,218,171,246,97,171,6,247,86,59,64,65,8,160,226,204,127,68,59,77,12,12,224,140,114,52,166,11,25,44,233,41,5,3,182,252,41,105,251,215,25,153,44,189,144,150,73,1,142,202,175,232,240,68,218,90,200,93,130,163,19,144,168,137,22,244,71,172,30,111,151,230,127,28,105,156,154,140,249,220,201,203,136,93,28,117,47,165,141,138,122,248,160,217,119,113,167,209,245,182,1,252,150,25,54,36,62,194,197,136,4,190,12,126,29,230,234,246,252,254,36,99,6,237,175,42,95,94,164,0,220,157,137,123,47,38,152,42,140,245,31,54,38,211,14,178,164,2,211,234,250,121,117,69,98,190,78,20,238,181,194,96,144,209,138,137,232,226,205,225,115,24,57,129,221,89,191,35,96,149,25,151,73,139,213,145,7,162,175,53,142,12,248,115,235,28,27,222,249,253,184,233,157,176,24,138,48,219,6,132,62,41,213,69,73,110,37,72,178,91,57,202,151,2,180,18,216,169,25,181,105,47,144,143,2,187,101,193,163,107,54,42,18,254,14,108,234,166,29,33,219,251,212,119,76,247,204,242,159,49,224,204,189,75,30,135,183,182,147,252,14,187,185,86,6,228,160,111,45,122,79,195,199,30,106,201,237,247,201,72,29,186,226,152,54,181,133,122,118,173,45,197,146,66,247,83,25,176,166,117,207,96,149,25,198,175,41,154,59,109,225,184,107,236,150,25,34,202,13,43,92,96,249,85,73,54,22,172,123,6,174,149,76,59,19,63,195,115,254,12,143,253,116,104,204,9,75,118,200,248,23,178,80,193,61,48,128,232,47,156,18,18,158,148,238,118,11,167,70,41,112,70,48,178,1,174,82,110,22,16,245,110,154,81,216,72,211,37,132,72,185,141,157,102,134,78,205,218,30,109,59,236,102,25,38,221,165,122,246,131,120,66,137,225,50,184,190,74,195,26,32,155,162,132,142,156,141,32,2,209,213,139,198,59,168,211,168,199,46,189,74,203,219,127,108,221,43,124,185,228,187,250,239,195,8,221,31,84,161,24,80,133,244,168,50,199,166,173,233,92,225,38,1,54,145,7,160,122,105,0,32,234,2,89,146,61,78,132,101,85,31,164,226,151,123,53,197,167,56,212,124,226,220,252,7,161,98,26,46,140,40,136,42,158,224,188,172,96,245,228,231,224,19,99,127,123,162,38,111,213,4,169,9,38,253,178,208,83,237,62,224,131,82,90,104,235,13,88,191,215,199,182,210,225,25,64,216,125,102,252,136,181,125,116,52,11,149,169,37,139,183,73,178,160,210,184,143,164,202,70,104,52,141,189,138,2,105,15,214,66,15,180,136,205,72,233,248,103,141,34,179,176,190,125,226,194,107,143,147,164,226,90,12,17,135,137,31,15,146,15,78,151,177,88,61,19,157,195,159,16,1,144,109,100,25,169,117,64,189,36,176,62,18,199,120,137,164,82,192,209,137,235,200,110,117,175,96,198,31,20,161,65,21,222,118,227,83,116,130,47,46,29,77,101,152,18,250,150,181,146,187,69,168,41,231,203,39,100,60,10,171,117,166,25,196,223,139,175,135,160,165,105,80,177,67,238,254,178,160,71,28,52,19,182,119,243,6,138,208,97,27,67,219,113,216,204,202,205,248,201,87,118,153,109,114,73,143,254,2,242,154,38,46,102,225,149,144,52,199,233,250,158,219,149,126,61,148,56,5,192,133,20,207,187,123,188,13,103,205,254,252,168,177,233,227,201,226,244,110,112,157,146,184,209,213,57,222,245,174,112,135,212,136,219,201,165,19,83,42,252,113,219,24,136,72,17,243,212,100,222,153,200,61,81,236,19,226,122,18,18,186,90,31,101,103,96,250,243,52,123,4,252,143,86,100,29,235,253,116,35,103,161,211,45,146,97,128,89,98,232,29,89,4,210,177,153,51,113,37,130,194,69,110,100,70,128,12,0,0,252,28,232,94,154,58,84,206,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; + +static const uint32_t DT_IP_TARGET_LEN = 3256; +static const uint32_t DT_IP_MEM = 32768u; diff --git a/test/test_ota/test_ota_core.cpp b/test/test_ota/test_ota_core.cpp new file mode 100644 index 0000000000..bd3b8c83bd --- /dev/null +++ b/test/test_ota/test_ota_core.cpp @@ -0,0 +1,972 @@ +#include +#include +#include +#include + +#include "helpers/ota/MotaContainer.h" +#include "helpers/ota/MerkleTree.h" +#include "helpers/ota/BlockBitmap.h" +#include "helpers/ota/Multihash.h" +#include "helpers/ota/FirmwareInfo.h" +#include "helpers/ota/SignerAllowlist.h" +#include "helpers/ota/OtaStore.h" +#include "helpers/ota/OtaProtocol.h" +#include "helpers/ota/OtaManager.h" +#include "mota_vectors.h" // auto-generated by tools/mota/gen_vectors.py + +extern "C" { + #include "helpers/ota/detools/detools.h" // vendored detools 0.53.0 embeddable decoder +} + +using namespace mesh::ota; + +// Build a flashed-image layout (body || fixed 56-byte EndF) the way the host packager / build hook do: +// marker(4) body_len(4) body_hash8(8) fw_version(4) target_id(4) hw_id(32). Identity is always present +// (zero/"" = unknown). +static std::vector make_image_id(const std::vector& body, uint32_t fw_version, + uint32_t target_id, const char* hw_id) { + std::vector img = body; + img.insert(img.end(), ENDF_MAGIC, ENDF_MAGIC + 4); + uint32_t n = (uint32_t)body.size(); + for (int i = 0; i < 4; i++) img.push_back((uint8_t)(n >> (8 * i))); + uint8_t h[8]; mh8(h, body.data(), body.size()); + img.insert(img.end(), h, h + 8); + for (int i = 0; i < 4; i++) img.push_back((uint8_t)(fw_version >> (8 * i))); + for (int i = 0; i < 4; i++) img.push_back((uint8_t)(target_id >> (8 * i))); + uint8_t hw[32] = {0}; size_t k = hw_id ? strlen(hw_id) : 0; if (k > 32) k = 32; if (k) memcpy(hw, hw_id, k); + img.insert(img.end(), hw, hw + 32); // -> fixed 56-byte trailer + return img; +} +static std::vector make_image(const std::vector& body) { + return make_image_id(body, 0, 0, ""); // zero identity (still a full 56-byte trailer) +} + +// --- cross-check the C++ parser/merkle against the Python reference vectors ---------------- + +TEST(OtaParse, ParsesReferenceContainer) { + MotaManifest m; + ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m)); + EXPECT_EQ(m.format_ver, MOTA_FORMAT_VER); + EXPECT_TRUE(m.is_full()); + EXPECT_FALSE(m.is_signed()); + EXPECT_EQ(m.target_id, EXP_TARGET_ID); + EXPECT_EQ(m.fw_version, EXP_FW_VERSION); + EXPECT_EQ(m.image_size, EXP_IMAGE_SIZE); + EXPECT_EQ(m.payload_size, EXP_PAYLOAD_SIZE); + EXPECT_EQ(m.block_count, EXP_BLOCK_COUNT); + EXPECT_EQ(m.block_size_log2, EXP_BLOCK_SIZE_LOG2); + EXPECT_EQ(m.codec_id, EXP_CODEC_ID); + EXPECT_EQ(0, memcmp(m.merkle_root, EXP_MERKLE_ROOT, 4)); + EXPECT_EQ(0, memcmp(m.image_hash, EXP_IMAGE_HASH, 32)); + ASSERT_NE(m.hw_id, nullptr); + EXPECT_EQ(0, memcmp(m.hw_id, EXP_HW_ID, 32)); // v2 hardware tag ("TESTHW" NUL-padded) + EXPECT_EQ(0, memcmp(m.approval, APPROVAL_NOT, 4)); // distributed = not approved + EXPECT_FALSE(m.is_approved()); +} + +TEST(OtaParse, RejectsTampering) { + MotaManifest m; + // bad magic + std::vector b(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN); + b[0] ^= 0xFF; + EXPECT_FALSE(mota_parse(b.data(), b.size(), m)); + // bad trailer + b.assign(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN); + b[b.size() - 1] ^= 0xFF; + EXPECT_FALSE(mota_parse(b.data(), b.size(), m)); + // wrong total-size field + b.assign(MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN); + b[4] ^= 0x01; + EXPECT_FALSE(mota_parse(b.data(), b.size(), m)); +} + +// block_idx is a uint16 on the wire, so a manifest needing > 65535 blocks can't be addressed and must be +// rejected at parse (this also keeps block_count*4 from overflowing the leaves-length computation). +TEST(OtaParse, RejectsTooManyBlocks) { + auto manifest = [](uint32_t payload_size, uint8_t bsl) { // fixed-layout unsigned-full manifest + std::vector m(MOTA_MFL, 0); + m[0] = MOTA_FORMAT_VER; m[1] = MFLAG_FULL; m[2] = 0x12; + m[15] = payload_size; m[16] = payload_size >> 8; m[17] = payload_size >> 16; m[18] = payload_size >> 24; + m[19] = bsl; + return m; + }; + MotaManifest mm; + auto over = manifest(65536u * 1024u, 10); // 65536 blocks -> rejected + EXPECT_FALSE(mota_parse_manifest(over.data(), over.size(), mm)); + auto ok = manifest(65535u * 1024u, 10); // 65535 blocks -> allowed + EXPECT_TRUE(mota_parse_manifest(ok.data(), ok.size(), mm)); + EXPECT_EQ(mm.block_count, 65535u); +} + +TEST(OtaMerkle, RootMatchesVectorAndLeaves) { + MotaManifest m; + ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m)); + // root recomputed from stored leaves[] == merkle_root field == Python's EXP_MERKLE_ROOT + uint8_t root[4]; + merkle_root(root, m.leaves, m.block_count); + EXPECT_EQ(0, memcmp(root, EXP_MERKLE_ROOT, 4)); + EXPECT_TRUE(mota_check_root(m)); + // recompute each leaf from the payload block and compare to the stored leaf + uint32_t bs = m.block_size(); + for (uint32_t i = 0; i < m.block_count; i++) { + uint32_t off = i * bs; + uint32_t len = (off + bs <= m.payload_size) ? bs : (m.payload_size - off); + uint8_t leaf[4]; + merkle_leaf(leaf, m.payload + off, len); + EXPECT_EQ(0, memcmp(leaf, m.leaves + i * 4, 4)) << "leaf " << i; + } +} + +TEST(OtaMerkle, FullImageHashMatches) { + MotaManifest m; + ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m)); + EXPECT_TRUE(mota_check_image_hash_full(m)); +} + +TEST(OtaMerkle, ProofFromReferenceVerifies) { + MotaManifest m; + ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m)); + uint32_t bs = m.block_size(); + uint32_t off = PROOF_INDEX * bs; + uint32_t len = (off + bs <= m.payload_size) ? bs : (m.payload_size - off); + + EXPECT_TRUE(merkle_verify(m.payload + off, len, PROOF_INDEX, + PROOF_SIBLINGS, PROOF_NSIB, EXP_MERKLE_ROOT, m.block_count)); + + // tampered block -> fails + std::vector blk(m.payload + off, m.payload + off + len); + blk[0] ^= 0xFF; + EXPECT_FALSE(merkle_verify(blk.data(), len, PROOF_INDEX, + PROOF_SIBLINGS, PROOF_NSIB, EXP_MERKLE_ROOT, m.block_count)); + + // wrong index with the same proof -> fails + EXPECT_FALSE(merkle_verify(m.payload + off, len, PROOF_INDEX + 1, + PROOF_SIBLINGS, PROOF_NSIB, EXP_MERKLE_ROOT, m.block_count)); +} + +// --- validate the O(log n) binary-counter root vs a plain level-by-level reference ---------- + +static void ref_root(uint8_t out[4], std::vector> level) { + while (level.size() > 1) { + std::vector> nxt; + for (size_t i = 0; i < level.size(); i += 2) { + if (i + 1 < level.size()) { + std::array p; + merkle_combine(p.data(), level[i].data(), level[i + 1].data()); + nxt.push_back(p); + } else { + nxt.push_back(level[i]); // promote lone last node + } + } + level.swap(nxt); + } + std::memcpy(out, level[0].data(), 4); +} + +TEST(OtaMerkle, BinaryCounterMatchesLevelByLevel) { + uint32_t state = 0x12345678; + auto rnd = [&]() { state = state * 1103515245u + 12345u; return (uint8_t)(state >> 16); }; + // O(log n) root must equal the plain level-by-level root for every count + for (uint32_t count = 1; count <= 600; count++) { + std::vector leaves(count * 4); + std::vector> ref(count); + for (uint32_t i = 0; i < count; i++) + for (int j = 0; j < 4; j++) { uint8_t v = rnd(); leaves[i * 4 + j] = v; ref[i][j] = v; } + uint8_t a[4], b[4]; + merkle_root(a, leaves.data(), count); + ref_root(b, ref); + ASSERT_EQ(0, std::memcmp(a, b, 4)) << "root mismatch count=" << count; + } +} + +// Verify every block's proof for several tricky counts, using proofs generated by the Python +// reference (the oracle) — covers deep promotion chains (100, 255, 256, ...). +TEST(OtaMerkle, ReferenceProofsAllIndices) { + for (int c = 0; c < N_PROOF_CASES; c++) { + const ProofCase& pc = PROOF_CASES[c]; + uint8_t root[4]; + merkle_root(root, pc.leaves, pc.count); + EXPECT_EQ(0, std::memcmp(root, pc.root, 4)) << "root mismatch count=" << pc.count; + for (uint32_t i = 0; i < pc.count; i++) { + EXPECT_TRUE(merkle_verify_from_leaf(pc.leaves + i * 4, i, + pc.pblob + pc.poff[i], pc.pnsib[i], pc.root, pc.count)) + << "count=" << pc.count << " idx=" << i; + } + // a wrong sibling for index 0 must fail + if (pc.pnsib[0] > 0) { + std::vector bad(pc.pblob + pc.poff[0], pc.pblob + pc.poff[0] + pc.pnsib[0] * 4); + bad[0] ^= 0xFF; + EXPECT_FALSE(merkle_verify_from_leaf(pc.leaves, 0, bad.data(), pc.pnsib[0], pc.root, pc.count)); + } + } +} + +// --- availability bitmap (derived from leaves[]) ------------------------------------------- + +TEST(OtaBitmap, AllPresentForCompleteContainer) { + MotaManifest m; + ASSERT_TRUE(mota_parse(MOTA_VEC, MOTA_VEC_LEN, m)); + EXPECT_TRUE(all_present(m.leaves, m.block_count)); + EXPECT_EQ(count_present(m.leaves, m.block_count), m.block_count); + + // a leaf slot of all-FF (erased) means "missing"; bitmap round-trips + std::vector leaves(m.leaves, m.leaves + m.block_count * 4); + std::memset(&leaves[4], 0xFF, 4); // mark block 1 missing + EXPECT_FALSE(leaf_present(leaves.data(), 1)); + EXPECT_FALSE(all_present(leaves.data(), m.block_count)); + EXPECT_EQ(count_present(leaves.data(), m.block_count), m.block_count - 1); + + std::vector bm(bitmap_bytes(m.block_count)); + leaves_to_bitmap(leaves.data(), m.block_count, bm.data()); + EXPECT_FALSE(bitmap_get(bm.data(), 1)); + EXPECT_TRUE(bitmap_get(bm.data(), 0)); +} + +// --- EndF self-firmware scan (P2) ----------------------------------------------------------- + +TEST(OtaFirmwareInfo, FindsEndFInImage) { + std::vector body(4321); + for (size_t i = 0; i < body.size(); i++) body[i] = (uint8_t)(i * 37 + 11); + std::vector img = make_image(body); + + // simulate a flash region: image, then erased 0xFF up to the partition end + std::vector region = img; + region.resize(img.size() + 4096, 0xFF); + + SelfFwInfo fi; + ASSERT_TRUE(find_self_firmware(region.data(), (uint32_t)region.size(), fi, /*verify_body=*/true)); + EXPECT_EQ(fi.body_len, body.size()); + EXPECT_EQ(fi.image_len, img.size()); + EXPECT_EQ(fi.endf_offset, body.size()); + uint8_t h[8]; mh8(h, body.data(), body.size()); + EXPECT_EQ(0, std::memcmp(fi.body_hash, h, 8)); +} + +// The self-describing identity lives at fixed offsets in the 56-byte EndF and is always parsed; a +// zero-identity trailer reports zero/"" (unknown), still at the fixed 56-byte size. +TEST(OtaFirmwareInfo, ParsesIdentity) { + std::vector body(2000); + for (size_t i = 0; i < body.size(); i++) body[i] = (uint8_t)(i * 13 + 5); + + auto img = make_image_id(body, 0x01100000u, 0x04d413fdu, "RAK4631"); + std::vector region = img; region.resize(img.size() + 4096, 0xFF); + SelfFwInfo fi; + ASSERT_TRUE(find_self_firmware(region.data(), (uint32_t)region.size(), fi, /*verify_body=*/true)); + EXPECT_EQ(fi.body_len, body.size()); + EXPECT_EQ(fi.image_len, body.size() + 56); // fixed trailer length + EXPECT_EQ(fi.fw_version, 0x01100000u); + EXPECT_EQ(fi.target_id, 0x04d413fdu); + EXPECT_STREQ(fi.hw_id, "RAK4631"); + + auto img1 = make_image(body); // zero-identity trailer (still 56 bytes) + std::vector r1 = img1; r1.resize(img1.size() + 64, 0xFF); + SelfFwInfo fi1; + ASSERT_TRUE(find_self_firmware(r1.data(), (uint32_t)r1.size(), fi1, true)); + EXPECT_EQ(fi1.fw_version, 0u); + EXPECT_EQ(fi1.target_id, 0u); + EXPECT_STREQ(fi1.hw_id, ""); + EXPECT_EQ(fi1.image_len, body.size() + 56); +} + +TEST(OtaFirmwareInfo, IgnoresStagedMotaHigherInRegion) { + // The firmware's own EndF must win even when a staged .mota (which embeds its own EndF) sits + // above it in the same region — the body_len == offset check disambiguates. + std::vector body(2000); + for (size_t i = 0; i < body.size(); i++) body[i] = (uint8_t)(i ^ 0x5A); + std::vector img = make_image(body); + + std::vector region = img; + region.resize(8192, 0xFF); // gap + // drop the reference .mota (which contains an embedded EndF in its payload) higher up + region.insert(region.end(), MOTA_VEC, MOTA_VEC + MOTA_VEC_LEN); + + SelfFwInfo fi; + ASSERT_TRUE(find_self_firmware(region.data(), (uint32_t)region.size(), fi, true)); + EXPECT_EQ(fi.endf_offset, body.size()); // found OUR firmware, not the .mota's + EXPECT_EQ(fi.body_len, body.size()); +} + +TEST(OtaFirmwareInfo, NoMarkerReturnsFalse) { + std::vector region(1000, 0xAB); + SelfFwInfo fi; + EXPECT_FALSE(find_self_firmware(region.data(), (uint32_t)region.size(), fi)); +} + +// --- signer allowlist (P3) ------------------------------------------------------------------ + +TEST(OtaAllowlist, AddContainsRemoveSerialize) { + SignerAllowlist a; + uint8_t k1[32], k2[32], k3[32]; + memset(k1, 0x11, 32); memset(k2, 0x22, 32); memset(k3, 0x33, 32); + EXPECT_FALSE(a.contains(k1)); + EXPECT_TRUE(a.add(k1)); + EXPECT_TRUE(a.add(k2)); + EXPECT_TRUE(a.add(k1)); // idempotent + EXPECT_EQ(a.count(), 2); + EXPECT_TRUE(a.contains(k1)); + EXPECT_FALSE(a.contains(k3)); + + uint8_t buf[1 + MAX_OTA_SIGNERS * 32]; + uint32_t n = a.serialize(buf, sizeof(buf)); + EXPECT_EQ(n, 1u + 2 * 32); + SignerAllowlist b; + EXPECT_TRUE(b.deserialize(buf, n)); + EXPECT_EQ(b.count(), 2); + EXPECT_TRUE(b.contains(k1) && b.contains(k2)); + + EXPECT_TRUE(a.remove(k1)); + EXPECT_EQ(a.count(), 1); + EXPECT_FALSE(a.contains(k1)); + EXPECT_TRUE(a.contains(k2)); +} + +// --- RAM store: out-of-order writes + availability via leaves[] -------------------------------- + +TEST(OtaStoreRamTest, RandomAccessAndErasedSentinel) { + OtaStoreRam<4096> s; + ASSERT_TRUE(s.begin(1000)); + EXPECT_EQ(s.staged_size(), 1000u); + uint8_t blk[8] = {1,2,3,4,5,6,7,8}; + EXPECT_TRUE(s.write(500, blk, 8)); // out-of-order offset + EXPECT_TRUE(s.write(0, blk, 8)); + EXPECT_FALSE(s.write(998, blk, 8)); // out of range + uint8_t rd[8]; + EXPECT_TRUE(s.read(500, rd, 8)); + EXPECT_EQ(0, memcmp(rd, blk, 8)); + // untouched region reads as erased 0xFF (so an unfilled leaf slot is "missing") + EXPECT_TRUE(s.read(100, rd, 8)); + for (int i = 0; i < 8; i++) EXPECT_EQ(rd[i], 0xFF); +} + +// --- merkle proof GENERATION (server side) matches the Python oracle --------------------------- + +TEST(OtaMerkle, GenProofMatchesPythonAndVerifies) { + for (int c = 0; c < N_PROOF_CASES; c++) { + const ProofCase& pc = PROOF_CASES[c]; + std::vector scratch(pc.count * 4); + uint8_t out[32 * 4]; + for (uint32_t i = 0; i < pc.count; i++) { + uint8_t n = merkle_gen_proof(pc.leaves, pc.count, i, scratch.data(), out); + ASSERT_EQ(n, pc.pnsib[i]) << "count=" << pc.count << " idx=" << i; + EXPECT_EQ(0, std::memcmp(out, pc.pblob + pc.poff[i], (size_t)n * 4)) + << "gen_proof != python count=" << pc.count << " idx=" << i; + EXPECT_TRUE(merkle_verify_from_leaf(pc.leaves + i * 4, i, out, n, pc.root, pc.count)); + } + } +} + +// --- protocol codec round-trips --------------------------------------------------------------- + +TEST(OtaProtocol, CodecRoundTrips) { + uint8_t buf[200]; + + // OTA_ADV is now a tiny per-node beacon: seeder_id + n_motas + set_digest + AdvMsg adv{{0x29,0x17,0xe4,0xf7}, 7, {0xde,0xad,0xbe,0xef}}; + uint16_t n = encode_adv(buf, sizeof(buf), adv); + ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_ADV); EXPECT_EQ(n, 10); + AdvMsg a2; ASSERT_TRUE(decode_adv(buf, n, a2)); + EXPECT_EQ(0, memcmp(a2.seeder_id, adv.seeder_id, 4)); + EXPECT_EQ(a2.n_motas, 7); + EXPECT_EQ(0, memcmp(a2.set_digest, adv.set_digest, 4)); + + // OTA_QUERY: ask a source (by seeder_id) for the offering set_digest, optionally filtered to a target + QueryMsg qy{{0x29,0x17,0xe4,0xf7}, {0xd1,0xd2,0xd3,0xd4}, 0x11223344}; + n = encode_query(buf, sizeof(buf), qy); + ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_QUERY); + QueryMsg q2; ASSERT_TRUE(decode_query(buf, n, q2)); + EXPECT_EQ(0, memcmp(q2.seeder_id, qy.seeder_id, 4)); + EXPECT_EQ(0, memcmp(q2.set_digest, qy.set_digest, 4)); + EXPECT_EQ(q2.filter_target, 0x11223344u); + + // OTA_HAVE: a 2-row catalog (mid, target, fwver, codec, flags per row) tagged with the offering digest + uint8_t rows[2 * OTA_HAVE_ROW_BYTES]; + for (int i = 0; i < 2 * OTA_HAVE_ROW_BYTES; i++) rows[i] = (uint8_t)(i + 1); + HaveMsg hv{{0x29,0x17,0xe4,0xf7}, {0xd1,0xd2,0xd3,0xd4}, 0, 1, 2, rows}; + n = encode_have(buf, sizeof(buf), hv); + ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_HAVE); + HaveMsg h2; ASSERT_TRUE(decode_have(buf, n, h2)); + EXPECT_EQ(0, memcmp(h2.seeder_id, hv.seeder_id, 4)); + EXPECT_EQ(0, memcmp(h2.set_digest, hv.set_digest, 4)); + EXPECT_EQ(h2.frag_total, 1); EXPECT_EQ(h2.n_rows, 2); + EXPECT_EQ(0, memcmp(h2.rows, rows, 2 * OTA_HAVE_ROW_BYTES)); + + GetManifestMsg gm{{1,2,3,4}}; + n = encode_get_manifest(buf, sizeof(buf), gm); + GetManifestMsg g2; ASSERT_TRUE(decode_get_manifest(buf, n, g2)); + EXPECT_EQ(0, memcmp(g2.manifest_id, gm.manifest_id, 4)); + + uint8_t mbytes[40]; for (int i = 0; i < 40; i++) mbytes[i] = (uint8_t)(i + 1); + ManifestMsg mm{{9,8,7,6}, 0, 1, mbytes, 40}; + n = encode_manifest(buf, sizeof(buf), mm); + ManifestMsg m2; ASSERT_TRUE(decode_manifest(buf, n, m2)); + EXPECT_EQ(m2.frag_idx, 0); EXPECT_EQ(m2.frag_total, 1); EXPECT_EQ(m2.len, 40); + EXPECT_EQ(0, memcmp(m2.bytes, mbytes, 40)); + + ReqMsg rq{{4,3,2,1}, 7, 5}; + n = encode_req(buf, sizeof(buf), rq); + ReqMsg r2; ASSERT_TRUE(decode_req(buf, n, r2)); + EXPECT_EQ(r2.start_block, 7); EXPECT_EQ(r2.count, 5); + + // DATA is one self-describing fragment of a block (frag_off places it; proof is fetched separately) + uint8_t data[100]; for (int i = 0; i < 100; i++) data[i] = (uint8_t)(i * 3); + DataMsg dm{{0,1,2,3}, 42, 0, data, 100}; // block 42, fragment at offset 0 + n = encode_data(buf, sizeof(buf), dm); + DataMsg d2; ASSERT_TRUE(decode_data(buf, n, d2)); + EXPECT_EQ(d2.block_idx, 42); EXPECT_EQ(d2.frag_off, 0); + EXPECT_EQ(d2.data_len, 100); EXPECT_EQ(0, memcmp(d2.data, data, 100)); + + // a later slice of the same block (non-zero frag_off) + DataMsg dm2{{0,1,2,3}, 42, 160, data, 50}; + n = encode_data(buf, sizeof(buf), dm2); + DataMsg d3; ASSERT_TRUE(decode_data(buf, n, d3)); + EXPECT_EQ(d3.block_idx, 42); EXPECT_EQ(d3.frag_off, 160); EXPECT_EQ(d3.data_len, 50); + + // REQ_PROOF: request the merkle proof for one (reassembled) block + ReqProofMsg rp{{7,7,8,8}, 13}; + n = encode_req_proof(buf, sizeof(buf), rp); + ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_REQ_PROOF); + ReqProofMsg rp2; ASSERT_TRUE(decode_req_proof(buf, n, rp2)); + EXPECT_EQ(0, memcmp(rp2.manifest_id, rp.manifest_id, 4)); EXPECT_EQ(rp2.block_idx, 13); + + // PROOF: ordered sibling digests for one block + uint8_t proof[12]; for (int i = 0; i < 12; i++) proof[i] = (uint8_t)(0xA0 + i); + ProofMsg pm{{7,7,8,8}, 13, 3, proof}; + n = encode_proof(buf, sizeof(buf), pm); + ASSERT_GT(n, 0); EXPECT_EQ(ota_msg_type(buf, n), OTA_PROOF); + ProofMsg pm2; ASSERT_TRUE(decode_proof(buf, n, pm2)); + EXPECT_EQ(0, memcmp(pm2.manifest_id, pm.manifest_id, 4)); + EXPECT_EQ(pm2.block_idx, 13); EXPECT_EQ(pm2.n_proof, 3); + EXPECT_EQ(0, memcmp(pm2.proof, proof, 12)); +} + +// --- full transfer simulation between two OtaManagers (P4b) ------------------------------------ + +namespace { +struct SimMsg { OtaManager* dest; std::vector bytes; }; +static std::vector g_q; +struct SendTo { OtaManager* dest; }; +static void sim_send(void* ctx, const uint8_t* msg, uint16_t len, bool /*flood*/) { + g_q.push_back({((SendTo*)ctx)->dest, std::vector(msg, msg + len)}); +} +// Drive the bus to quiescence: deliver queued messages; when idle, advance the client's clock (monotonic +// across calls, so a jittered query scheduled in a prior pump still comes due) and call loop() (fires the +// scheduled catalog query / block re-requests). Two idle ticks in a row = quiescent. +static uint32_t g_clk = 0; +static void pump(OtaManager& client, int guard_max = 200000) { + int idle = 0, guard = 0; + while (guard++ < guard_max) { + if (!g_q.empty()) { + SimMsg m = std::move(g_q.front()); g_q.erase(g_q.begin()); + m.dest->on_message(m.bytes.data(), (uint16_t)m.bytes.size()); + idle = 0; + } else { + g_clk += 5000; client.set_clock(g_clk); client.loop(); + if (!g_q.empty()) { idle = 0; continue; } + if (++idle >= 2) break; + } + } +} + +// A test MotaSource backing an external "folder" with one or more complete `.mota` images held in RAM — +// the simplest concrete transport (a real device uses serial/BLE/WiFi/FS, same interface). describe() +// parses each container for the catalog + region offsets; read() is a bounds-checked memcpy. +class RamMotaSource : public mesh::ota::MotaSource { +public: + void add(const uint8_t* buf, uint32_t len) { if (_n < 8) { _buf[_n] = buf; _len[_n] = len; _n++; } } + uint8_t count() override { return _n; } + bool describe(uint8_t idx, mesh::ota::MotaDesc& d) override { + if (idx >= _n) return false; + MotaManifest m; + if (!mota_parse(_buf[idx], _len[idx], m)) return false; + std::memcpy(d.mid, m.merkle_root, 4); + d.target_id = m.target_id; d.fw_version = m.fw_version; + d.codec_id = m.codec_id; d.flags = m.flags; + d.total_size = _len[idx]; + d.leaves_off = (uint32_t)(m.leaves - _buf[idx]); + d.block_count = m.block_count; + d.payload_off = (uint32_t)(m.payload - _buf[idx]); + d.payload_size = m.payload_size; + return true; + } + bool read(uint8_t idx, uint32_t off, uint8_t* out, uint32_t len) override { + if (idx >= _n || (uint64_t)off + len > _len[idx]) return false; + std::memcpy(out, _buf[idx] + off, len); + return true; + } +private: + const uint8_t* _buf[8] = {nullptr}; uint32_t _len[8] = {0}; uint8_t _n = 0; +}; +} + +TEST(OtaTransfer, TwoManagersFullTransfer) { + g_q.clear(); + OtaManager server, client; + OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + + server.begin(/*server's own target irrelevant for serving*/ 0, sim_send, &to_client); + client.begin(SIM_TARGET_ID, sim_send, &to_server); + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); // tests exercise fetch-on-advert; policy default is OFF + + ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN)); + server.announce(); // -> client hears the beacon, queries, catalogs, then fetches + + pump(client); // beacon -> query -> have -> startFetch -> full transfer + + EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE); + EXPECT_EQ(client.blocksHave(), client.blocksTotal()); + EXPECT_GT(client.blocksTotal(), 1u); + + // the client's reassembled container must be byte-identical to the original .mota... + ASSERT_EQ(store.staged_size(), SIM_MOTA_LEN); + EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA, SIM_MOTA_LEN)); + + // ...and independently re-verify it parses with a matching root + image_hash + MotaManifest m; + ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), m)); + EXPECT_TRUE(mota_check_root(m)); + EXPECT_TRUE(mota_check_image_hash_full(m)); +} + +// Same end-to-end transfer, but with 1 KB logical blocks: each block is delivered as several +// self-describing DATA fragments (frag_off), reassembled by the client, then its merkle PROOF is +// requested + verified separately before the block is committed. Exercises the multi-fragment path. +TEST(OtaTransfer, MultiFragmentBlocks) { + g_q.clear(); + OtaManager server, client; + OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + + server.begin(0, sim_send, &to_client); + client.begin(SIM_TARGET_ID, sim_send, &to_server); + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); + + ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN)); + server.announce(); + + pump(client); + + EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE); + EXPECT_EQ(client.blocksTotal(), SIM_MOTA_1K_BLOCKS); // 1 KB blocks => fewer, larger blocks + EXPECT_EQ(client.blocksHave(), client.blocksTotal()); + + ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN); + EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN)); + MotaManifest m; + ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), m)); + EXPECT_TRUE(mota_check_root(m)); + EXPECT_TRUE(mota_check_image_hash_full(m)); +} + +// Multi-mota folder serve: a node serves its OWN fw (view0) PLUS an external folder (RamMotaSource) of +// other `.mota`. Peers discover BOTH via the tiny beacon -> query -> broadcast HAVE catalog, then fetch an +// external mota end-to-end. The relaying node never holds the folder image in RAM — it streams the +// manifest/leaves/blocks from the source on demand (loadSource + srcReadTramp + proof-gen from read +// leaves). The fetched bytes must equal the original `.mota` (proves the trustless relay is byte-exact). +TEST(OtaFolder, ServesSelfPlusFolderAndFetchesExternal) { + g_q.clear(); + OtaManager server, client; + OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + + server.begin(/*own target irrelevant for serving*/ 0, sim_send, &to_client); + uint8_t srv_id[4] = {0xAB, 0xCD, 0xEF, 0x01}; server.set_seeder_id(srv_id); + client.begin(SIM_TARGET_ID, sim_send, &to_server); + client.set_fetch_store(&store); + + MotaManifest mSelf, mExt; + ASSERT_TRUE(mota_parse(SIM_MOTA, SIM_MOTA_LEN, mSelf)); // served as our own fw (view0) + ASSERT_TRUE(mota_parse(SIM_MOTA_1K, SIM_MOTA_1K_LEN, mExt)); // served from the external folder + + ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN)); // entry 0 = self + static RamMotaSource folder; + folder.add(SIM_MOTA_1K, SIM_MOTA_1K_LEN); // an external image (different mid) + folder.add(SIM_MOTA, SIM_MOTA_LEN); // same as self -> must be DEDUPED + ASSERT_TRUE(server.add_source(&folder)); + EXPECT_EQ(server.servedCount(), 2); // self + 1 distinct folder mota (dedup) + + // discovery: beacon -> the client catalogs the source, queries it, and the broadcast HAVE fills the + // catalog with BOTH served mids. + server.announce(); + pump(client); + client.queryAll(); + pump(client); + EXPECT_EQ(client.catalogCount(), 2); + + // fetch the EXTERNAL (folder) mota by mid -> served via the source, relayed block-by-block. + client.pull(mExt.merkle_root, mExt.target_id); + pump(client); + + EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE); + EXPECT_EQ(client.blocksHave(), client.blocksTotal()); + ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN); + EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN)); // byte-exact relay + MotaManifest got; + ASSERT_TRUE(mota_parse(store.data(), store.staged_size(), got)); + EXPECT_TRUE(mota_check_root(got)); + EXPECT_TRUE(mota_check_image_hash_full(got)); +} + +// --- swarm: a completed peer re-seeds beyond the origin (epidemic spread) --------------------- +namespace { +// A topology-aware bus: each node delivers only to its listed neighbours (so we can build multi-hop chains +// the origin can't reach directly). DATA emits are counted per node to observe who actually sources blocks. +struct TopoNode { int idx; }; +static std::vector g_tn; +static std::vector> g_adj; +static std::vector g_tdata; +static std::vector>> g_tq; +static size_t g_th = 0; +static uint32_t g_tclk = 0; +static void topo_send(void* ctx, const uint8_t* msg, uint16_t len, bool) { + int from = ((TopoNode*)ctx)->idx; + if (len && msg[0] == OTA_DATA) g_tdata[from]++; + for (int nb : g_adj[from]) g_tq.push_back({nb, std::vector(msg, msg + len)}); +} +static void topo_pump(int guard = 2000000) { + int idle = 0, g = 0; + while (g++ < guard) { + if (g_th < g_tq.size()) { + auto m = g_tq[g_th++]; + g_tn[m.first]->on_message(m.second.data(), (uint16_t)m.second.size()); + idle = 0; + if (g_th > 8192) { g_tq.erase(g_tq.begin(), g_tq.begin() + g_th); g_th = 0; } + } else { + g_tclk += 1000; + for (auto* nd : g_tn) { nd->set_clock(g_tclk); nd->loop(); } + if (g_th < g_tq.size()) { idle = 0; continue; } + if (++idle >= 3) break; + } + } +} +} + +// Line topology: origin <-> relay <-> leaf, with origin and leaf NOT connected. The relay fetches from the +// origin, COMPLETEs, and re-seeds; the leaf — which can ONLY hear the relay — must then obtain the whole +// firmware from the relay. If the leaf completes, the load provably spread off the origin (the origin never +// served the leaf). Validates re-serve-after-complete (and the partial-re-serve serve path it shares). +TEST(OtaSwarm, CompletedPeerReSeedsBeyondOrigin) { + g_tn.clear(); g_adj.clear(); g_tdata.clear(); g_tq.clear(); g_th = 0; g_tclk = 0; + static OtaManager origin, relay, leaf; + static OtaStoreRam<4096> rstore, lstore; + g_tn = {&origin, &relay, &leaf}; + g_adj = {{1}, {0, 2}, {1}}; // origin<->relay<->leaf + g_tdata = {0, 0, 0}; + static TopoNode t0{0}, t1{1}, t2{2}; + uint8_t id0[4] = {1,1,1,1}, id1[4] = {2,2,2,2}, id2[4] = {3,3,3,3}; + origin.begin(0, topo_send, &t0); origin.set_seeder_id(id0); + relay.begin(SIM_TARGET_ID, topo_send, &t1); relay.set_seeder_id(id1); + relay.set_fetch_store(&rstore); relay.set_autofetch(OtaManager::AUTOFETCH_ANY); + leaf.begin(SIM_TARGET_ID, topo_send, &t2); leaf.set_seeder_id(id2); + leaf.set_fetch_store(&lstore); leaf.set_autofetch(OtaManager::AUTOFETCH_ANY); + + ASSERT_TRUE(origin.serve(SIM_MOTA, SIM_MOTA_LEN)); + origin.announce(); + topo_pump(); + ASSERT_EQ(relay.fetchState(), OtaManager::COMPLETE); // relay sourced it from the origin + + relay.announce(); // relay now beacons its catalog (incl. the re-seeded mota) + topo_pump(); + EXPECT_EQ(leaf.fetchState(), OtaManager::COMPLETE); // leaf got it ONLY via the relay (re-serve) + ASSERT_EQ(lstore.staged_size(), SIM_MOTA_LEN); + EXPECT_EQ(0, std::memcmp(lstore.data(), SIM_MOTA, SIM_MOTA_LEN)); // byte-exact through the relay + EXPECT_GT(g_tdata[1], 0); // the relay actually served DATA (re-seeded) +} + +// Fetch-resume across a reboot: a client commits some blocks, "reboots" (a fresh OtaManager on the SAME +// persisted store), and resumeStaged() re-adopts the partial container and finishes the remaining blocks — +// without re-fetching the manifest or the blocks already present. +TEST(OtaTransfer, ResumeAfterReboot) { + g_q.clear(); + OtaManager server, client; + OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + + server.begin(0, sim_send, &to_client); + client.begin(SIM_TARGET_ID, sim_send, &to_server); + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); + + ASSERT_TRUE(server.serve(SIM_MOTA_1K, SIM_MOTA_1K_LEN)); + server.announce(); + + // drive only until the first block commits, then "crash" + int idle = 0, guard = 0; + while (guard++ < 100000) { + if (!g_q.empty()) { + SimMsg msg = std::move(g_q.front()); g_q.erase(g_q.begin()); + msg.dest->on_message(msg.bytes.data(), (uint16_t)msg.bytes.size()); + idle = 0; + } else { + g_clk += 5000; client.set_clock(g_clk); client.loop(); + if (!g_q.empty()) { idle = 0; } else if (++idle >= 2) break; + } + if (client.blocksHave() >= 1) break; + } + ASSERT_GE(client.blocksHave(), 1u); + ASSERT_LT(client.blocksHave(), client.blocksTotal()); // genuinely partial + uint32_t had = client.blocksHave(); + g_q.clear(); // in-flight packets are lost in the "reboot" + + // "reboot": a brand-new manager on the SAME store (its bytes survived) resumes the partial + OtaManager client2; + to_client.dest = &client2; // server now replies to the rebooted client + SendTo to_server2{&server}; + client2.begin(SIM_TARGET_ID, sim_send, &to_server2); + client2.set_fetch_store(&store); + ASSERT_TRUE(client2.resumeStaged(nullptr)); // adopt whatever is staged + EXPECT_EQ(client2.blocksHave(), had); // resumed exactly where we left off + EXPECT_EQ(client2.fetchState(), OtaManager::FETCHING); + EXPECT_EQ(client2.blocksTotal(), SIM_MOTA_1K_BLOCKS); + + pump(client2); + EXPECT_EQ(client2.fetchState(), OtaManager::COMPLETE); + ASSERT_EQ(store.staged_size(), SIM_MOTA_1K_LEN); + EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA_1K, SIM_MOTA_1K_LEN)); // byte-identical to the original +} + +TEST(OtaTransfer, ClientRejectsWrongTarget) { + g_q.clear(); + OtaManager server, client; + OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + server.begin(0, sim_send, &to_client); + client.begin(SIM_TARGET_ID ^ 0x1u, sim_send, &to_server); // different target -> not interested + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); // tests exercise fetch-on-advert; policy default is OFF + ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN)); + server.announce(); + pump(client); // catalogs the row but wantRow rejects it (wrong target) -> never fetches + EXPECT_EQ(client.fetchState(), OtaManager::IDLE); // never started +} + +TEST(OtaTransfer, ManualCrossTargetFetch) { + // A node whose own target differs from the served firmware normally won't fetch (role-switch case: + // e.g. companion wanting repeater firmware). An explicit want() override lets it fetch deliberately. + g_q.clear(); + OtaManager server, client; + OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + server.begin(0, sim_send, &to_client); + client.begin(SIM_TARGET_ID ^ 0xABCDu, sim_send, &to_server); // DIFFERENT own target + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); // tests exercise fetch-on-advert; policy default is OFF + ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN)); + + // without the override: catalogs the row but won't fetch (wrong target) + server.announce(); + pump(client); + EXPECT_EQ(client.fetchState(), OtaManager::IDLE); + + // with want(): deliberately fetch the different-target firmware to completion + client.want(SIM_TARGET_ID); + server.announce(); + pump(client); + EXPECT_EQ(client.fetchState(), OtaManager::COMPLETE); + ASSERT_EQ(store.staged_size(), SIM_MOTA_LEN); + EXPECT_EQ(0, std::memcmp(store.data(), SIM_MOTA, SIM_MOTA_LEN)); +} + +// Encode a 1-row OTA_HAVE catalog (the discovery reply a peer acts on). +static uint16_t make_have1(uint8_t* buf, uint16_t cap, const uint8_t mid[4], + uint32_t target, uint32_t fwver, uint8_t codec, uint8_t flags) { + uint8_t row[OTA_HAVE_ROW_BYTES]; + memcpy(row, mid, 4); + row[4]=target; row[5]=target>>8; row[6]=target>>16; row[7]=target>>24; + row[8]=fwver; row[9]=fwver>>8; row[10]=fwver>>16; row[11]=fwver>>24; + row[12]=codec; row[13]=flags; + row[14]=0; row[15]=0; // have_count (unused in this 1-row discovery test) + HaveMsg hv{{0xAA,0xBB,0xCC,0xDD}, {0,0,0,0}, 0, 1, 1, row}; + return encode_have(buf, cap, hv); +} + +// A node must not fetch firmware it can't apply: a catalog row whose codec the platform can't decode is +// not fetched. FULL + the platform's delta codec(s) are accepted. +TEST(OtaTransfer, RejectsIncompatibleCodec) { + g_q.clear(); + OtaManager client; OtaStoreRam<4096> store; + SendTo to_server{&client}; // dest unused (we only check client state) + client.begin(SIM_TARGET_ID, sim_send, &to_server); + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); + client.set_apply_codec(CODEC_DETOOLS_INPLACE); // nRF52-style: accepts only full + in-place + uint8_t b[64]; + + // a SEQUENTIAL delta for our target -> incompatible -> not fetched (stays IDLE) + uint8_t midA[4] = {1,2,3,4}; + client.on_message(b, make_have1(b, sizeof(b), midA, SIM_TARGET_ID, 0x01000000, CODEC_DETOOLS_SEQUENTIAL, 0)); + EXPECT_EQ(client.fetchState(), OtaManager::IDLE); + + // an IN-PLACE delta for our target -> compatible -> begins fetching (requests the manifest) + uint8_t midB[4] = {5,6,7,8}; + client.on_message(b, make_have1(b, sizeof(b), midB, SIM_TARGET_ID, 0x01000000, CODEC_DETOOLS_INPLACE, 0)); + EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST); + g_q.clear(); +} + +// Encode a 1-row OTA_HAVE from a specific seeder, carrying have_count (Phase-2 awareness). +static uint16_t make_have_row(uint8_t* buf, uint16_t cap, const uint8_t mid[4], uint32_t target, + uint32_t fwver, uint8_t codec, uint8_t flags, + const uint8_t seeder[4], uint16_t have_count) { + uint8_t row[OTA_HAVE_ROW_BYTES]; + memcpy(row, mid, 4); + row[4]=target; row[5]=target>>8; row[6]=target>>16; row[7]=target>>24; + row[8]=fwver; row[9]=fwver>>8; row[10]=fwver>>16; row[11]=fwver>>24; + row[12]=codec; row[13]=flags; + row[14]=(uint8_t)(have_count & 0xFF); row[15]=(uint8_t)(have_count >> 8); + HaveMsg hv; memcpy(hv.seeder_id, seeder, 4); memset(hv.set_digest, 0, 4); + hv.frag_idx=0; hv.frag_total=1; hv.n_rows=1; hv.rows=row; + return encode_have(buf, cap, hv); +} + +// Catalog accounting: "N nodes have it" must count DISTINCT seeders (a repeated HAVE from one node must +// not inflate it), and have_max tracks the best progress any source reported. +TEST(OtaCatalog, DistinctSeederCountAndHaveCount) { + OtaManager m; SendTo none{&m}; m.begin(SIM_TARGET_ID, sim_send, &none); + uint8_t b[64]; uint8_t mid[4]={9,9,9,9}; + uint8_t s1[4]={1,0,0,0}, s2[4]={2,0,0,0}; + m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s1, 5)); + m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s1, 7)); // same seeder + ASSERT_EQ(m.catalogCount(), 1); + EXPECT_EQ(m.catalogRow(0)->n_seeders, 1); // counted once despite two HAVEs + EXPECT_EQ(m.catalogRow(0)->have_max, 7u); // max progress seen + m.on_message(b, make_have_row(b, sizeof b, mid, SIM_TARGET_ID, 0x01020300, CODEC_FULL, 0, s2, 3)); // new seeder + EXPECT_EQ(m.catalogRow(0)->n_seeders, 2); + EXPECT_EQ(m.catalogRow(0)->have_max, 7u); // still the max, not overwritten by the lower one + g_q.clear(); +} + +// An unanswered GET_MANIFEST must not pin the fetch slot forever: after OTA_MANIFEST_MAX_RETRY ticks with +// no manifest, the session gives up (FAILED) so a new pull can take the slot. (Lowest-priority + bounded.) +TEST(OtaTransfer, ManifestGiveUpAfterRetries) { + g_q.clear(); + OtaManager client; OtaStoreRam<4096> store; SendTo to_none{&client}; + client.begin(SIM_TARGET_ID, sim_send, &to_none); + client.set_fetch_store(&store); + uint8_t mid[4]={7,7,7,7}; + client.pull(mid, SIM_TARGET_ID); // no server -> stuck WANT_MANIFEST + EXPECT_EQ(client.fetchState(), OtaManager::WANT_MANIFEST); + for (int i = 0; i < OTA_MANIFEST_MAX_RETRY + 2; i++) { g_clk += 5000; client.set_clock(g_clk); client.loop(); g_q.clear(); } + EXPECT_EQ(client.fetchState(), OtaManager::FAILED); +} + +// Re-seeding a completed download must stop when the session is dropped (the staging store is cleared right +// after), so the node never advertises a mota it can no longer serve. +TEST(OtaSwarm, ReSeedStopsAfterDrop) { + g_q.clear(); + OtaManager server, client; OtaStoreRam<4096> store; + SendTo to_client{&client}, to_server{&server}; + server.begin(0, sim_send, &to_client); + client.begin(SIM_TARGET_ID, sim_send, &to_server); + client.set_fetch_store(&store); + client.set_autofetch(OtaManager::AUTOFETCH_ANY); + ASSERT_TRUE(server.serve(SIM_MOTA, SIM_MOTA_LEN)); + server.announce(); + pump(client); + ASSERT_EQ(client.fetchState(), OtaManager::COMPLETE); + EXPECT_EQ(client.servedCount(), 1); // re-seeding the completed download to peers + client.reset_session(); + EXPECT_EQ(client.servedCount(), 0); // dropped -> stops advertising it +} + +// --- detools delta decode (vendored detools C decoder, CRLE-only build) ---------------------- +// Mirrors the device apply path (src/helpers/ota/OtaApply.cpp): base read via from_read/from_seek, +// patch streamed via patch_read, output written via to_write. Proves the on-device delta apply uses +// detools 0.53.0's own decoder and reproduces the exact target the host packager targeted. +namespace { +struct DTMem { + const uint8_t* base; long base_len; long base_pos; + const uint8_t* patch; long patch_len; long patch_pos; + std::vector out; +}; +int dt_from_read(void* a, uint8_t* b, size_t n) { + DTMem* c = (DTMem*)a; + if (c->base_pos < 0 || c->base_pos + (long)n > c->base_len) return -DETOOLS_IO_FAILED; + std::memcpy(b, c->base + c->base_pos, n); c->base_pos += (long)n; return DETOOLS_OK; +} +int dt_from_seek(void* a, int off) { + DTMem* c = (DTMem*)a; c->base_pos += off; + if (c->base_pos < 0 || c->base_pos > c->base_len) return -DETOOLS_IO_FAILED; + return DETOOLS_OK; +} +int dt_patch_read(void* a, uint8_t* b, size_t n) { + DTMem* c = (DTMem*)a; + if (c->patch_pos + (long)n > c->patch_len) return -DETOOLS_IO_FAILED; + std::memcpy(b, c->patch + c->patch_pos, n); c->patch_pos += (long)n; return DETOOLS_OK; +} +int dt_to_write(void* a, const uint8_t* b, size_t n) { + DTMem* c = (DTMem*)a; c->out.insert(c->out.end(), b, b + n); return DETOOLS_OK; +} + +// In-place apply over a flat memory region (models the nRF52 app workspace / the bootloader's flash). +struct DTInPlace { + std::vector mem; // [0,memory_size): base in, target out + const uint8_t* patch; long plen, ppos; int step; +}; +int ip_mem_read(void* a, void* dst, uintptr_t src, size_t n) { + DTInPlace* c = (DTInPlace*)a; if (src + n > c->mem.size()) return -DETOOLS_IO_FAILED; + std::memcpy(dst, c->mem.data() + src, n); return DETOOLS_OK; +} +int ip_mem_write(void* a, uintptr_t dst, void* src, size_t n) { + DTInPlace* c = (DTInPlace*)a; if (dst + n > c->mem.size()) return -DETOOLS_IO_FAILED; + std::memcpy(c->mem.data() + dst, src, n); return DETOOLS_OK; +} +int ip_mem_erase(void* a, uintptr_t addr, size_t n) { + DTInPlace* c = (DTInPlace*)a; if (addr + n > c->mem.size()) return -DETOOLS_IO_FAILED; + std::memset(c->mem.data() + addr, 0xFF, n); return DETOOLS_OK; +} +int ip_step_set(void* a, int s) { ((DTInPlace*)a)->step = s; return DETOOLS_OK; } +int ip_step_get(void* a, int* s) { *s = ((DTInPlace*)a)->step; return DETOOLS_OK; } +int ip_patch_read(void* a, uint8_t* b, size_t n) { + DTInPlace* c = (DTInPlace*)a; if (c->ppos + (long)n > c->plen) return -DETOOLS_IO_FAILED; + std::memcpy(b, c->patch + c->ppos, n); c->ppos += (long)n; return DETOOLS_OK; +} +} // namespace + +TEST(Detools, SequentialCrlePatchReproducesTarget) { + DTMem c{DT_BASE, (long)DT_BASE_LEN, 0, DT_PATCH, (long)DT_PATCH_LEN, 0, {}}; + int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read, + (size_t)DT_PATCH_LEN, dt_to_write, &c); + ASSERT_EQ(r, (int)DT_TARGET_LEN); // returns to-size on success + ASSERT_EQ(c.out.size(), (size_t)DT_TARGET_LEN); + EXPECT_EQ(0, std::memcmp(c.out.data(), DT_TARGET, DT_TARGET_LEN)); +} + +TEST(Detools, WrongBaseDoesNotReproduceTarget) { + // a base that differs from the one the patch was built against must NOT yield the target + std::vector bad(DT_BASE, DT_BASE + DT_BASE_LEN); + for (size_t i = 0; i < bad.size(); i += 7) bad[i] ^= 0xFF; + DTMem c{bad.data(), (long)bad.size(), 0, DT_PATCH, (long)DT_PATCH_LEN, 0, {}}; + int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read, + (size_t)DT_PATCH_LEN, dt_to_write, &c); + bool reproduced = (r == (int)DT_TARGET_LEN && c.out.size() == (size_t)DT_TARGET_LEN && + std::memcmp(c.out.data(), DT_TARGET, DT_TARGET_LEN) == 0); + EXPECT_FALSE(reproduced); // wrong base -> wrong/short output (the device then fails image_hash) +} + +TEST(Detools, TruncatedPatchFails) { + DTMem c{DT_BASE, (long)DT_BASE_LEN, 0, DT_PATCH, (long)(DT_PATCH_LEN / 2), 0, {}}; + int r = detools_apply_patch_callbacks(dt_from_read, dt_from_seek, dt_patch_read, + (size_t)(DT_PATCH_LEN / 2), dt_to_write, &c); + EXPECT_TRUE(r < 0 || c.out.size() != (size_t)DT_TARGET_LEN); +} + +// nRF52 path: the bootloader applies an in-place patch over the single app slot. Model the app +// region as a DT_IP_MEM buffer holding the base; after apply, region[0:to_size] must equal the target. +TEST(Detools, InPlaceCrlePatchReproducesTarget) { + DTInPlace c; c.mem.assign(DT_IP_MEM, 0xFF); + std::memcpy(c.mem.data(), DT_IP_BASE, DT_IP_BASE_LEN); // base loaded at offset 0 + c.patch = DT_IP_PATCH; c.plen = DT_IP_PATCH_LEN; c.ppos = 0; c.step = 0; + int r = detools_apply_patch_in_place_callbacks(ip_mem_read, ip_mem_write, ip_mem_erase, + ip_step_set, ip_step_get, ip_patch_read, + (size_t)DT_IP_PATCH_LEN, &c); + ASSERT_EQ(r, (int)DT_IP_TARGET_LEN); // returns to-size on success + EXPECT_EQ(0, std::memcmp(c.mem.data(), DT_IP_TARGET, DT_IP_TARGET_LEN)); +} + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/tools/mota/README.md b/tools/mota/README.md new file mode 100644 index 0000000000..c3f8a77d30 --- /dev/null +++ b/tools/mota/README.md @@ -0,0 +1,53 @@ +# `tools/mota/` — OTA Python reference library & build/test tooling + +The Python side of MeshCore's `.mota` OTA system. It is the **reference implementation** of the wire +spec ([`docs/ota_protocol.md`](../../docs/ota_protocol.md)) and the **build + test infrastructure** — it is +no longer a user-facing CLI. + +> **Want to build / verify / inspect / serve `.mota` from the command line?** Use the self-contained C++ +> tool [`tools/motatool/`](../motatool/). It supersedes the old `mota.py` / `mota_seeder.py` (now removed), +> produces byte-identical containers, and runs on small hardware. The files here are the spec oracle and +> the firmware build/test glue. + +## Setup + +Uses the repo's Python venv (`meshcore/`). Dependencies: `detools` (delta), `cryptography` (Ed25519), +`intelhex` (nRF52 `.hex` handling). + +```bash +./meshcore/bin/pip install detools cryptography intelhex +``` + +## Files + +| File | What | +|---|---| +| `motalib.py` | Core logic: multihash, EndF, merkle tree+proofs, manifest/container build/parse/verify. The **reference implementation** of the spec and the unit-test oracle. Imported by everything below. | +| `pio_endf.py` | **PlatformIO post-build hook** (wired in `platformio.ini`) that injects the `EndF` self-identity trailer into the flashed firmware (`-D ENABLE_OTA`). | +| `gen_vectors.py` | Generates `test/test_ota/mota_vectors.h` — the cross-check vectors the native C++ tests run against. | +| `gen_targets.py` | Generates `src/helpers/ota/OtaTargets.h` — the `target_id → env-name` table (every `ENABLE_OTA` env, resolved from `pio project config`). Shared by the firmware and `motatool` so a node can name a target seen over the air without sending the string. Regenerate when the OTA env set changes. | +| `test_mota.py` | Unit tests for `motalib` (run directly or via pytest). | + +## Tests + +```bash +./meshcore/bin/python tools/mota/test_mota.py # EndF, merkle+proofs, full/delta, signing, + # tamper detection, approval enforcement +./meshcore/bin/python tools/mota/gen_vectors.py # regenerate the native-test cross-check vectors +./meshcore/bin/python tools/mota/gen_targets.py # regenerate src/helpers/ota/OtaTargets.h (needs `pio`) +``` + +## `EndF` build integration + +`EndF` must live in the **flashed** firmware (not just inside the `.mota`), because a node serves its own +firmware and matches a delta's `base_hash` against its own `EndF`. Wiring (handled by `pio_endf.py`): + +- **ESP32 / RP2040** (emit `firmware.bin`): `post:tools/mota/pio_endf.py` in the env's `extra_scripts` plus + `-D ENABLE_OTA=1`. The hook appends the 56-byte `EndF` (with `target_id`/`fw_version`/`hw_id`) to the app + `.bin` before merge. +- **nRF52 / STM32** (emit `.hex` → `.uf2`): the same hook rewrites the `.hex` with the trailer at the image + end. The byte logic is `motalib.ensure_endf`, used everywhere. + +`target_id` = `sha2-256:4(pio_env_name)`, `hw_id` = `-D MOTA_HW_ID`, `fw_version` = parsed from +`FIRMWARE_VERSION` — so a node (and `motatool`, reading the firmware's `EndF`) auto-discovers identity +without relying on filenames. diff --git a/tools/mota/gen_targets.py b/tools/mota/gen_targets.py new file mode 100644 index 0000000000..ff25928d9f --- /dev/null +++ b/tools/mota/gen_targets.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 +"""Generate src/helpers/ota/OtaTargets.h — the target_id -> env-name table for OTA-capable envs. + +A target_id is sha2-256:4(pio_env_name); it travels in beacons / the .mota manifest as 4 bytes. This +table lets a node (and motatool) print the human-readable env name for a target seen over the air, WITHOUT +ever transmitting the string. The set of envs is resolved from PlatformIO (every env whose build_flags +include ENABLE_OTA), so it stays in sync with what actually ships OTA. + +Run: ./meshcore/bin/python tools/mota/gen_targets.py # resolves via `pio project config` + ./meshcore/bin/python tools/mota/gen_targets.py cfg.json # or reuse a cached config dump (faster) +""" +from __future__ import annotations + +import json +import os +import subprocess +import sys +from pathlib import Path + +import motalib as ml + +ROOT = Path(__file__).resolve().parents[2] +OUT = ROOT / "src" / "helpers" / "ota" / "OtaTargets.h" + + +def resolved_config(): + if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): + return json.load(open(sys.argv[1])) + pio = os.environ.get("PIO", "pio") + out = subprocess.run([pio, "project", "config", "--json-output"], + cwd=ROOT, capture_output=True, text=True, check=True).stdout + return json.loads(out) + + +def ota_envs(cfg): + envs = [] + for section, opts in cfg: + if not section.startswith("env:"): + continue + for k, v in opts: + if k == "build_flags": + flags = " ".join(v) if isinstance(v, list) else str(v) + if "ENABLE_OTA" in flags: + envs.append(section[4:]) + break + return sorted(set(envs)) + + +def main(): + envs = ota_envs(resolved_config()) + rows = [(ml.target_id_for_env(e), e) for e in envs] + + seen = {} + for tid, e in rows: # sha2-256:4 over ~300 names — verify no collisions + if tid in seen and seen[tid] != e: + print(f"WARNING: target_id collision 0x{tid:08x}: {seen[tid]!r} vs {e!r}", file=sys.stderr) + seen[tid] = e + rows.sort(key=lambda r: r[1].lower()) # by env name (readable, deterministic) + + out = [ + "#pragma once", + "#include ", + "", + "// AUTO-GENERATED by tools/mota/gen_targets.py — do not edit by hand.", + f"// {len(rows)} OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)", + "// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT", + "// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.", + "", + "namespace mesh { namespace ota {", + "", + "// target_id -> env name, or nullptr if unknown. Linear scan (table is small; lookups are rare).", + "inline const char* ota_target_env_name(uint32_t target_id) {", + " static const struct { uint32_t id; const char* env; } T[] = {", + ] + out += [f' {{ 0x{tid:08x}, "{e}" }},' for tid, e in rows] + out += [ + " };", + " for (unsigned i = 0; i < sizeof(T) / sizeof(T[0]); i++)", + " if (T[i].id == target_id) return T[i].env;", + " return nullptr;", + "}", + "", + "} } // namespace mesh::ota", + ] + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text("\n".join(out) + "\n") + print(f"wrote {OUT} ({len(rows)} OTA envs)") + + +if __name__ == "__main__": + main() diff --git a/tools/mota/gen_vectors.py b/tools/mota/gen_vectors.py new file mode 100644 index 0000000000..35b1a70265 --- /dev/null +++ b/tools/mota/gen_vectors.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +""" +Generate a C++ cross-check header from the reference implementation (motalib). + +Emits test/test_ota/mota_vectors.h containing a real .mota built by the Python tool plus the +expected parse results and a merkle proof, so the device-side C++ core (src/helpers/ota/) is +verified to agree byte-for-byte with the host packager. + +Run: ./meshcore/bin/python tools/mota/gen_vectors.py +""" +from __future__ import annotations + +import random +from pathlib import Path + +import motalib as ml + +OUT = Path(__file__).resolve().parents[2] / "test" / "test_ota" / "mota_vectors.h" + + +def _carr(name, data: bytes) -> str: + body = ",".join(str(b) for b in data) + return f"static const uint8_t {name}[{len(data)}] = {{{body}}};\n" + + +def build_full(): + random.seed(42) + fw = bytes(random.getrandbits(8) for _ in range(5 * 1024 + 137)) # 6 blocks, last short + image, _ = ml.ensure_endf(fw) + m = ml.build_manifest( + target_id=0x11223344, fw_version=ml.pack_version("1.16.0"), + image_size=len(image), payload=image, block_size=1024, + image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, is_full=True, hw_id="TESTHW") + return ml.build_container(m, image), m, image + + +def emit_proof_case(idx, count) -> str: + """A tree of `count` arbitrary 4-byte leaves + every block's proof, computed by the reference.""" + random.seed(1000 + count) + leaves = [bytes(random.getrandbits(8) for _ in range(4)) for _ in range(count)] + root = ml.merkle_root(leaves) + blob = b"" + offs, nsibs = [], [] + for i in range(count): + sib = ml.proof_siblings(leaves, i) + assert ml.verify_proof(leaves[i], i, ml.merkle_proof(leaves, i), root, count), (count, i) + offs.append(len(blob)) + nsibs.append(len(sib) // 4) + blob += sib + p = f"T{idx}" + out = [ + f"static const uint32_t {p}_COUNT = {count}u;", + _carr(f"{p}_LEAVES", b"".join(leaves)), + _carr(f"{p}_ROOT", root), + f"static const uint16_t {p}_POFF[{count}] = {{{','.join(str(o) for o in offs)}}};", + f"static const uint8_t {p}_PNSIB[{count}] = {{{','.join(str(n) for n in nsibs)}}};", + _carr(f"{p}_PBLOB", blob), + ] + return "\n".join(out) + + +def main(): + blob, m, image = build_full() + leaves = ml.leaf_hashes(image, 1024) + assert ml.merkle_root(leaves) == m.merkle_root + + proof_idx = 2 if m.block_count > 2 else 0 + siblings = ml.proof_siblings(leaves, proof_idx) + assert ml.verify_proof(leaves[proof_idx], proof_idx, ml.merkle_proof(leaves, proof_idx), + m.merkle_root, len(leaves)) + + lines = [ + "// AUTO-GENERATED by tools/mota/gen_vectors.py — do not edit by hand.", + "// Cross-check vectors: a real .mota from the reference packager (motalib.py).", + "#pragma once", + "#include ", + "", + _carr("MOTA_VEC", blob), + f"static const uint32_t MOTA_VEC_LEN = {len(blob)};", + f"static const uint32_t EXP_TARGET_ID = 0x{m.target_id:08x}u;", + f"static const uint32_t EXP_FW_VERSION = 0x{m.fw_version:08x}u;", + f"static const uint32_t EXP_IMAGE_SIZE = {m.image_size}u;", + f"static const uint32_t EXP_PAYLOAD_SIZE = {m.payload_size}u;", + f"static const uint32_t EXP_BLOCK_COUNT = {m.block_count}u;", + f"static const uint8_t EXP_BLOCK_SIZE_LOG2 = {m.block_size_log2};", + f"static const uint8_t EXP_CODEC_ID = {m.codec_id};", + _carr("EXP_MERKLE_ROOT", m.merkle_root), + _carr("EXP_IMAGE_HASH", m.image_hash), + _carr("EXP_HW_ID", m.hw_id), + f"static const uint32_t PROOF_INDEX = {proof_idx}u;", + f"static const uint8_t PROOF_NSIB = {len(siblings)//4};", + _carr("PROOF_SIBLINGS", siblings), + ] + + # exhaustive per-index proof cases for several tricky counts (deep promotion chains) + counts = [5, 7, 8, 65, 100, 255, 256] + for i, c in enumerate(counts): + lines.append(emit_proof_case(i, c)) + lines.append("struct ProofCase { uint32_t count; const uint8_t* leaves; const uint8_t* root;" + " const uint16_t* poff; const uint8_t* pnsib; const uint8_t* pblob; };") + rows = ",".join(f"{{T{i}_COUNT,T{i}_LEAVES,T{i}_ROOT,T{i}_POFF,T{i}_PNSIB,T{i}_PBLOB}}" + for i in range(len(counts))) + lines.append(f"static const ProofCase PROOF_CASES[] = {{{rows}}};") + lines.append(f"static const int N_PROOF_CASES = {len(counts)};") + + # small-block signed .mota for the host transfer simulation (each block fits one packet => no + # fragmentation needed to validate the manager/protocol end-to-end). Deterministic signing key. + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + random.seed(99) + sim_fw = bytes(random.getrandbits(8) for _ in range(2000)) + sim_image, _ = ml.ensure_endf(sim_fw) + sim_priv = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) + sm = ml.build_manifest(target_id=0xCAFEBABE, fw_version=ml.pack_version("3.0.0"), + image_size=len(sim_image), payload=sim_image, block_size=128, + image_hash=ml.mh32(sim_image), codec_id=ml.CODEC_FULL, + is_full=True, sign_priv=sim_priv) + sim_blob = ml.build_container(sm, sim_image) + lines.append("// small-block signed .mota for the OtaManager host transfer simulation") + lines.append(_carr("SIM_MOTA", sim_blob)) + lines.append(f"static const uint32_t SIM_MOTA_LEN = {len(sim_blob)};") + lines.append(f"static const uint32_t SIM_TARGET_ID = 0x{sm.target_id:08x}u;") + + # 1 KB-block signed .mota: each logical block spans MULTIPLE LoRa DATA fragments (1024 / 160 = 7), + # so this exercises the multi-fragment reassembly + split proof path the device actually uses. + sm1k = ml.build_manifest(target_id=0xCAFEBABE, fw_version=ml.pack_version("3.0.0"), + image_size=len(sim_image), payload=sim_image, block_size=1024, + image_hash=ml.mh32(sim_image), codec_id=ml.CODEC_FULL, + is_full=True, sign_priv=sim_priv) + sim1k_blob = ml.build_container(sm1k, sim_image) + lines.append("// 1 KB-block signed .mota (multi-fragment per block) for the reassembly transfer test") + lines.append(_carr("SIM_MOTA_1K", sim1k_blob)) + lines.append(f"static const uint32_t SIM_MOTA_1K_LEN = {len(sim1k_blob)};") + lines.append(f"static const uint32_t SIM_MOTA_1K_BLOCKS = {sm1k.block_count};") + + # detools sequential+crle delta vector: base image, the real detools 0.53.0 patch, and the + # expected target image. The native test applies DT_PATCH to DT_BASE with the *vendored detools + # C decoder* (src/helpers/ota/detools) and must reproduce DT_TARGET byte-for-byte -- proving the + # on-device delta apply path uses detools, not a reimplementation. + import io + import detools + random.seed(2024) + dt_base_body = bytes(random.getrandbits(8) for _ in range(3000)) + dt_tgt = bytearray(dt_base_body) + for off in (137, 138, 139, 1500, 2999): # localized edits, version-bump style + dt_tgt[off] ^= 0x5A + dt_tgt += bytes(random.getrandbits(8) for _ in range(200)) # small appended tail + dt_base_img, _ = ml.ensure_endf(dt_base_body) + dt_tgt_img, _ = ml.ensure_endf(bytes(dt_tgt)) + fp = io.BytesIO() + detools.create_patch(io.BytesIO(dt_base_img), io.BytesIO(dt_tgt_img), fp, + patch_type="sequential", compression="crle") + dt_patch = fp.getvalue() + lines.append("// detools sequential+crle delta: apply DT_PATCH to DT_BASE -> DT_TARGET") + lines.append(_carr("DT_BASE", dt_base_img)) + lines.append(f"static const uint32_t DT_BASE_LEN = {len(dt_base_img)};") + lines.append(_carr("DT_PATCH", dt_patch)) + lines.append(f"static const uint32_t DT_PATCH_LEN = {len(dt_patch)};") + lines.append(_carr("DT_TARGET", dt_tgt_img)) + lines.append(f"static const uint32_t DT_TARGET_LEN = {len(dt_tgt_img)};") + + # detools IN-PLACE+crle delta (nRF52 single-slot apply): apply DT_IP_PATCH to a memory region + # holding DT_IP_BASE -> region[0:to_size] == DT_IP_TARGET. The native test runs the vendored + # in-place decoder over a DT_IP_MEM-byte RAM buffer, exactly as the bootloader will over flash. + DT_IP_MEM, DT_IP_SEG = 0x8000, 0x1000 + ip_patch_io = io.BytesIO() + detools.create_patch(io.BytesIO(dt_base_img), io.BytesIO(dt_tgt_img), ip_patch_io, + patch_type="in-place", memory_size=DT_IP_MEM, segment_size=DT_IP_SEG, + compression="crle") + dt_ip_patch = ip_patch_io.getvalue() + lines.append("// detools in-place+crle delta: apply DT_IP_PATCH over a DT_IP_MEM buffer holding DT_IP_BASE") + lines.append(_carr("DT_IP_BASE", dt_base_img)) + lines.append(f"static const uint32_t DT_IP_BASE_LEN = {len(dt_base_img)};") + lines.append(_carr("DT_IP_PATCH", dt_ip_patch)) + lines.append(f"static const uint32_t DT_IP_PATCH_LEN = {len(dt_ip_patch)};") + lines.append(_carr("DT_IP_TARGET", dt_tgt_img)) + lines.append(f"static const uint32_t DT_IP_TARGET_LEN = {len(dt_tgt_img)};") + lines.append(f"static const uint32_t DT_IP_MEM = {DT_IP_MEM}u;") + + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text("\n".join(lines) + "\n") + print(f"wrote {OUT}") + print(f" blob={len(blob)}B blocks={m.block_count} proof_idx={proof_idx} nsib={len(siblings)//4}") + print(f" proof cases: counts={counts}") + print(f" detools delta: base={len(dt_base_img)}B target={len(dt_tgt_img)}B seq_patch={len(dt_patch)}B inplace_patch={len(dt_ip_patch)}B") + + +if __name__ == "__main__": + main() diff --git a/tools/mota/motalib.py b/tools/mota/motalib.py new file mode 100644 index 0000000000..626fcc8758 --- /dev/null +++ b/tools/mota/motalib.py @@ -0,0 +1,491 @@ +""" +motalib — build/parse/verify MeshCore ``.mota`` firmware-update containers. + +Pure logic, no CLI. Implements docs/ota_protocol.md (format_ver=2, fixed layout). + +The wire format (all integers little-endian): + + container = MAGIC(4) | MOTA_TOTAL_SIZE(4) | MANIFEST | PAYLOAD | TRAILER(5) + + manifest = format_ver(1) flags(1) hash_algo(1) target_id(4) fw_version(4) + image_size(4) payload_size(4) block_size_log2(1) merkle_root(4) + image_hash(32) codec_id(1) hw_id(32) + base_hash(8) signer_pubkey(32) signature(64) approval(4) + leaves[](4*BC) + +Fixed layout: every field is always present at a constant offset (base_hash/signer_pubkey/signature are +zero-filled for a full / unsigned container), so manifest-minus-leaves is always 197 bytes and `leaves[]` +is the only variable-length field. Hashes are SHA-256, truncated per multihash (sha2-256:N = first N bytes). +""" + +from __future__ import annotations + +import hashlib +import io +import struct +from dataclasses import dataclass, field +from typing import List, Optional, Tuple + +# --------------------------------------------------------------------------- +# Reference constants (must match docs/ota_protocol.md and the device code) +# --------------------------------------------------------------------------- + +MAGIC = b"mOTA" # 6D 4F 54 41 +TRAILER = b"vk496" # 76 6B 34 39 36 +ENDF_MAGIC = b"EndF" # 45 6E 64 46 +# Fixed-length trailer: marker(4) + body_len(4) + body_hash8(8) + fw_version(4) + target_id(4) + hw_id(32). +ENDF_LEN = 56 + +FORMAT_VER = 2 # the one manifest format (fixed layout, see Manifest); other values are rejected +HASH_ALGO_SHA256 = 0x12 # multihash code for sha2-256 + +FLAG_FULL = 0x01 +FLAG_SIGNED = 0x02 + +CODEC_FULL = 0 +CODEC_DETOOLS_SEQUENTIAL = 1 # detools `sequential` patch (decoded on-device by vendored detools C) +CODEC_DETOOLS_INPLACE = 2 # detools `in-place` patch (nRF52 bootloader-handoff path; TBD) +CODEC_NAMES = {CODEC_FULL: "full", CODEC_DETOOLS_SEQUENTIAL: "detools-sequential", + CODEC_DETOOLS_INPLACE: "detools-in-place"} + +APPROVAL_NOT = b"\xff\xff\xff\xff" # erased = not approved +APPROVAL_YES = b"APRV" # 41 50 52 56 = approved + +DEFAULT_BLOCK_SIZE = 1024 + + +# --------------------------------------------------------------------------- +# Multihash helpers (sha2-256 truncations) +# --------------------------------------------------------------------------- + +def sha256(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +def mh4(data: bytes) -> bytes: + return hashlib.sha256(data).digest()[:4] + + +def mh8(data: bytes) -> bytes: + return hashlib.sha256(data).digest()[:8] + + +def mh32(data: bytes) -> bytes: + return hashlib.sha256(data).digest() + + +# --------------------------------------------------------------------------- +# fw_version packing +# --------------------------------------------------------------------------- + +def pack_version(s) -> int: + """'1.16.0' or '1.16.0.2' -> uint32 (MAJOR<<24|MINOR<<16|PATCH<<8|pre). Ints pass through.""" + if isinstance(s, int): + return s & 0xFFFFFFFF + s = s.strip().lstrip("vV") + parts = [int(p) for p in s.split(".")] + parts += [0] * (4 - len(parts)) + maj, mnr, pat, pre = parts[:4] + return ((maj & 0xFF) << 24) | ((mnr & 0xFF) << 16) | ((pat & 0xFF) << 8) | (pre & 0xFF) + + +def unpack_version(v: int) -> str: + return f"{(v >> 24) & 0xFF}.{(v >> 16) & 0xFF}.{(v >> 8) & 0xFF}.{v & 0xFF}" + + +# --------------------------------------------------------------------------- +# target_id +# --------------------------------------------------------------------------- + +def target_id_for_env(env_name: str) -> int: + """4-byte build-target id = sha2-256:4(env_name), little-endian uint32. + + The PlatformIO env name (e.g. 'RAK_4631_companion_radio_usb') uniquely captures hardware AND + role/partition layout. build.sh injects the same value as -D MOTA_TARGET_ID so the device's + getOtaTargetId() matches what the packager stamps into the manifest. (Must match build.sh.) + """ + d = hashlib.sha256(env_name.encode()).digest()[:4] + return int.from_bytes(d, "little") + + +# --------------------------------------------------------------------------- +# EndF trailer +# --------------------------------------------------------------------------- + +@dataclass +class FwIdent: + """Self-describing firmware identity carried in the EndF trailer (docs/ota_protocol.md §2) so a node / + the packaging tool reads it straight from the firmware instead of relying on build flags or filenames.""" + fw_version: int = 0 # packed MAJOR<<24 | MINOR<<16 | PATCH<<8 | pre + target_id: int = 0 # sha2-256:4(pio_env) as uint32 LE — hw + role + partition (fetch routing) + hw_id: str = "" # readable hardware tag (brick-safety), e.g. "RAK4631" + + +def build_endf(body: bytes, ident: Optional["FwIdent"] = None) -> bytes: + """The fixed 56-byte EndF trailer for a firmware BODY (identity zero-filled if not given).""" + ident = ident or FwIdent() + hw = ident.hw_id.encode("ascii", "replace")[:32].ljust(32, b"\0") + return (ENDF_MAGIC + struct.pack(" bool: + """True iff `image` ends with a self-consistent (fixed 56-byte) EndF trailer (image == BODY || EndF).""" + if len(image) < ENDF_LEN: + return False + t = image[-ENDF_LEN:] + return (t[:4] == ENDF_MAGIC and struct.unpack(" Tuple[bytes, bytes]: + """Return (body, body_hash8) for an image that ends with a valid EndF. Raises otherwise.""" + if not has_endf(image): + raise ValueError("image has no valid EndF trailer") + return image[:-ENDF_LEN], image[-ENDF_LEN + 8:-ENDF_LEN + 16] + + +def parse_endf_ident(image: bytes) -> Optional["FwIdent"]: + """The self-describing identity from a valid EndF trailer, or None if there is no valid trailer.""" + if not has_endf(image): + return None + t = image[-ENDF_LEN:] + fw, tgt = struct.unpack(" Tuple[bytes, bytes]: + """Return (image_with_endf, body_hash8). Appends EndF (with `ident` if given) if not already present.""" + if has_endf(image): + _, h8 = parse_endf(image) + return image, h8 + body_hash8 = mh8(image) + return image + build_endf(image, ident), body_hash8 + + +# --------------------------------------------------------------------------- +# Merkle tree (sha2-256:4 leaves/nodes, promote-odd, no padding) +# --------------------------------------------------------------------------- + +def block_count(payload_size: int, block_size: int) -> int: + return (payload_size + block_size - 1) // block_size + + +def leaf_hashes(payload: bytes, block_size: int) -> List[bytes]: + return [mh4(payload[i:i + block_size]) for i in range(0, len(payload), block_size)] + + +def merkle_root(leaves: List[bytes]) -> bytes: + if not leaves: + raise ValueError("empty payload / no leaves") + level = list(leaves) + while len(level) > 1: + nxt = [] + n = len(level) + for i in range(0, n, 2): + if i + 1 < n: + nxt.append(mh4(level[i] + level[i + 1])) + else: + nxt.append(level[i]) # promote lone last node unchanged + level = nxt + return level[0] + + +def merkle_proof(leaves: List[bytes], index: int) -> List[Tuple[bytes, bool]]: + """Proof for block `index`: list of (sibling_digest, sibling_is_left).""" + proof: List[Tuple[bytes, bool]] = [] + level = list(leaves) + idx = index + while len(level) > 1: + n = len(level) + is_last_odd = (n % 2 == 1) and (idx == n - 1) + if not is_last_odd: + if idx % 2 == 0: + proof.append((level[idx + 1], False)) # sibling on the right + else: + proof.append((level[idx - 1], True)) # sibling on the left + nxt = [mh4(level[i] + level[i + 1]) if i + 1 < n else level[i] + for i in range(0, n, 2)] + idx //= 2 + level = nxt + return proof + + +def proof_siblings(leaves: List[bytes], index: int) -> bytes: + """Wire form of a proof: just the ordered sibling digests, concatenated. + + The left/right direction is derived by the verifier from the block index + count + (sibling is on the left iff the current index is odd), so no direction bits are sent. + """ + return b"".join(sib for sib, _ in merkle_proof(leaves, index)) + + +def verify_proof(leaf: bytes, index: int, proof: List[Tuple[bytes, bool]], + root: bytes, count: int) -> bool: + h = leaf + idx = index + n = count + p = 0 + while n > 1: + is_last_odd = (n % 2 == 1) and (idx == n - 1) + if is_last_odd: + pass # promoted, no proof element + else: + if p >= len(proof): + return False + sib, is_left = proof[p] + p += 1 + h = mh4(sib + h) if is_left else mh4(h + sib) + idx //= 2 + n = (n + 1) // 2 + return h == root and p == len(proof) + + +# --------------------------------------------------------------------------- +# Manifest + container +# --------------------------------------------------------------------------- + +@dataclass +class Manifest: + format_ver: int = FORMAT_VER + flags: int = 0 + hash_algo: int = HASH_ALGO_SHA256 + target_id: int = 0 + fw_version: int = 0 + image_size: int = 0 + payload_size: int = 0 + block_size_log2: int = 10 + merkle_root: bytes = b"\0\0\0\0" + image_hash: bytes = b"\0" * 32 + codec_id: int = CODEC_FULL + hw_id: bytes = b"\0" * 32 # 32-byte NUL-padded ASCII hardware tag (signed) + # Fixed-layout: these are ALWAYS present (zero-filled when not applicable), so the manifest has a + # constant size and a trivial offset-based parser; only leaves[] is variable. + base_hash: bytes = b"\0" * 8 # 8 bytes; zero for a full image (meaningful iff !FULL) + signer_pubkey: bytes = b"\0" * 32 # 32 bytes; zero when unsigned (meaningful iff SIGNED) + signature: bytes = b"\0" * 64 # 64 bytes; zero when unsigned (meaningful iff SIGNED) + approval: bytes = APPROVAL_NOT + leaves: List[bytes] = field(default_factory=list) + + @property + def is_full(self) -> bool: + return bool(self.flags & FLAG_FULL) + + @property + def is_signed(self) -> bool: + return bool(self.flags & FLAG_SIGNED) + + @property + def block_size(self) -> int: + return 1 << self.block_size_log2 + + @property + def block_count(self) -> int: + return block_count(self.payload_size, self.block_size) + + def signed_region(self) -> bytes: + """Bytes the Ed25519 signature covers: format_ver .. signer_pubkey (fixed 129 bytes).""" + out = bytearray() + out += bytes([self.format_ver, self.flags, self.hash_algo]) + out += struct.pack(" bytes: + """Fixed layout: signed_region(129) + signature(64) + approval(4) + leaves[4*BC].""" + out = bytearray(self.signed_region()) + out += self.signature # always present (zero when unsigned) + out += self.approval + for lf in self.leaves: + out += lf + return bytes(out) + + +def hw_id_bytes(s) -> bytes: + """Pack a hardware tag (str or bytes) into the fixed 32-byte NUL-padded field.""" + if s is None: + return b"\0" * 32 + raw = s.encode("ascii") if isinstance(s, str) else bytes(s) + if len(raw) > 32: + raise ValueError("hw_id must be <= 32 bytes") + return raw + b"\0" * (32 - len(raw)) + + +def _validate_lengths(m: Manifest): + assert len(m.merkle_root) == 4 + assert len(m.image_hash) == 32 + assert len(m.hw_id) == 32 + assert len(m.approval) == 4 + assert len(m.base_hash) == 8 # fixed layout: always present (zero for full) + assert len(m.signer_pubkey) == 32 + assert len(m.signature) == 64 + + +def build_manifest(*, target_id: int, fw_version: int, image_size: int, payload: bytes, + block_size: int, image_hash: bytes, codec_id: int, is_full: bool, + base_hash: Optional[bytes] = None, sign_priv=None, hw_id=None) -> Manifest: + assert (block_size & (block_size - 1)) == 0, "block_size must be a power of two" + leaves = leaf_hashes(payload, block_size) + m = Manifest( + flags=(FLAG_FULL if is_full else 0) | (FLAG_SIGNED if sign_priv is not None else 0), + target_id=target_id, + fw_version=fw_version, + image_size=image_size, + payload_size=len(payload), + block_size_log2=block_size.bit_length() - 1, + merkle_root=merkle_root(leaves), + image_hash=image_hash, + codec_id=codec_id, + hw_id=hw_id_bytes(hw_id), + base_hash=(b"\0" * 8 if is_full else base_hash), # always 8 bytes; zero for a full image + leaves=leaves, + ) + if not is_full and (base_hash is None or len(base_hash) != 8): + raise ValueError("delta requires an 8-byte base_hash") + if sign_priv is not None: + m.signer_pubkey = sign_priv.public_key().public_bytes_raw() + m.signature = sign_priv.sign(m.signed_region()) # else signer_pubkey/signature stay zero + _validate_lengths(m) + return m + + +def build_container(manifest: Manifest, payload: bytes) -> bytes: + assert len(payload) == manifest.payload_size + mser = manifest.serialize() + total = 4 + 4 + len(mser) + len(payload) + len(TRAILER) + return MAGIC + struct.pack(" Parsed: + if blob[:4] != MAGIC: + raise ValueError("bad MAGIC") + if blob[-5:] != TRAILER: + raise ValueError("bad TRAILER") + total = struct.unpack(" List[str]: + """Return a list of problem strings (empty == fully valid for what could be checked).""" + problems: List[str] = [] + m, payload = parsed.manifest, parsed.payload + + # block_count / leaves + if len(m.leaves) != m.block_count: + problems.append(f"leaves count {len(m.leaves)} != block_count {m.block_count}") + + # merkle root must match recomputation from the actual payload blocks + recomputed_leaves = leaf_hashes(payload, m.block_size) + if recomputed_leaves != m.leaves: + problems.append("stored leaves[] do not match payload blocks") + try: + if merkle_root(recomputed_leaves) != m.merkle_root: + problems.append("merkle_root does not match payload") + except ValueError as e: + problems.append(f"merkle: {e}") + + # spot-check a proof round-trips (block 0 and last) + if recomputed_leaves: + for idx in {0, len(recomputed_leaves) - 1}: + pr = merkle_proof(recomputed_leaves, idx) + if not verify_proof(recomputed_leaves[idx], idx, pr, m.merkle_root, len(recomputed_leaves)): + problems.append(f"merkle proof failed for block {idx}") + + # approval must be 'not approved' in a distributed container + if m.approval != APPROVAL_NOT: + problems.append(f"approval is not the erased sentinel (got {m.approval.hex()})") + + # signature + if m.is_signed: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey + from cryptography.exceptions import InvalidSignature + pub = Ed25519PublicKey.from_public_bytes(m.signer_pubkey) + try: + pub.verify(m.signature, m.signed_region()) + except InvalidSignature: + problems.append("Ed25519 signature INVALID") + if expect_pub is not None and m.signer_pubkey != expect_pub: + problems.append("signer_pubkey != expected key") + + # image_hash: directly checkable only for full images (payload IS the image) + if m.is_full: + if mh32(payload) != m.image_hash: + problems.append("image_hash does not match full payload") + elif base_image is not None: + # delta: optionally apply against a provided base to confirm image_hash + try: + import detools + out = io.BytesIO() + detools.apply_patch(io.BytesIO(_ensure_base(base_image)), io.BytesIO(payload), out) + rebuilt = out.getvalue() + if mh32(rebuilt) != m.image_hash: + problems.append("delta applied to base does not match image_hash") + if len(rebuilt) != m.image_size: + problems.append("delta result size != image_size") + except Exception as e: # noqa: BLE001 + problems.append(f"delta apply check failed: {e}") + return problems + + +def _ensure_base(base_image: bytes) -> bytes: + img, _ = ensure_endf(base_image) + return img diff --git a/tools/mota/pio_endf.py b/tools/mota/pio_endf.py new file mode 100644 index 0000000000..61ad0e425a --- /dev/null +++ b/tools/mota/pio_endf.py @@ -0,0 +1,111 @@ +""" +PlatformIO post-build extra-script: append the MeshCore ``EndF`` trailer to the +firmware image so a running node can self-locate its size/identity (docs/ota_protocol.md §2). + +Wire it (ONLY for OTA-enabled builds) from a variant/env, e.g.: + + extra_scripts = + ${nrf52_base.extra_scripts} + post:tools/mota/pio_endf.py + +and define ``-D ENABLE_OTA=1``. With ENABLE_OTA unset this script is a no-op, so it is safe to +leave wired everywhere. + +The byte logic is the same `motalib.ensure_endf` exercised by `endf.py` and the unit tests. + +ESP32 / RP2040 emit ${PROGNAME}.bin (the raw app image) -> EndF appended to the .bin. +nRF52 emits ${PROGNAME}.hex (the app, for DFU/UF2) -> EndF appended into the .hex right after the +app's last byte (so the downstream .uf2 / DFU .zip carry it). Both feed the same on-device EndF scan. +""" + +Import("env") # noqa: F821 (injected by PlatformIO/SCons) + +import os +import sys + +sys.path.insert(0, os.path.join(env["PROJECT_DIR"], "tools", "mota")) # noqa: F821 +import motalib as ml + + +def _ota_enabled() -> bool: + for d in env.get("CPPDEFINES", []): # noqa: F821 + name = d[0] if isinstance(d, (list, tuple)) else d + if name == "ENABLE_OTA": + return True + return False + + +def _is_nrf52() -> bool: + for d in env.get("CPPDEFINES", []): # noqa: F821 + name = d[0] if isinstance(d, (list, tuple)) else d + if name == "NRF52_PLATFORM": + return True + return False + + +def _cppdef(name): # value of a -D= build flag, or None + for d in env.get("CPPDEFINES", []): # noqa: F821 + if isinstance(d, (list, tuple)) and len(d) > 1 and d[0] == name: + return str(d[1]) + if d == name: + return "" + return None + + +def _firmware_ident(): + """Self-describing identity to embed in EndF (docs/ota_protocol.md §2): target_id is computed from the + PlatformIO env name (so it's correct even without build.sh's -D MOTA_TARGET_ID), hw_id from MOTA_HW_ID, + fw_version parsed from FIRMWARE_VERSION.""" + import re + target_id = ml.target_id_for_env(env["PIOENV"]) # noqa: F821 + hw_id = (_cppdef("MOTA_HW_ID") or "").replace("\\", "").strip().strip('"').strip("'") + ver_s = (_cppdef("FIRMWARE_VERSION") or "").replace("\\", "").strip().strip('"').strip("'") + m = re.search(r"(\d+)\.(\d+)(?:\.(\d+))?", ver_s) + fw_version = ml.pack_version(f"{m.group(1)}.{m.group(2)}.{m.group(3) or 0}") if m else 0 + return ml.FwIdent(fw_version=fw_version, target_id=target_id, hw_id=hw_id) + + +def _append_endf(source, target, env): # raw .bin path (ESP32 / RP2040) + path = str(target[0]) + with open(path, "rb") as f: + data = f.read() + ident = _firmware_ident() + out, h8 = ml.ensure_endf(data, ident) + if len(out) != len(data): + with open(path, "wb") as f: + f.write(out) + print(f"EndF: appended to {os.path.basename(path)} (body_len={len(data)} body_hash={h8.hex()} " + f"target={ident.target_id:#010x} hw='{ident.hw_id}' fw={ident.fw_version:#010x})") + else: + print(f"EndF: already present in {os.path.basename(path)} (no change)") + + +def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app for DFU/UF2) + from intelhex import IntelHex + path = str(target[0]) + ih = IntelHex(path) + segs = ih.segments() + if not segs: + print("EndF: empty .hex, skipping"); return + app_start, app_end = segs[0] # first (lowest) segment = the application image + body = bytes(ih.tobinarray(start=app_start, size=app_end - app_start)) + ident = _firmware_ident() + out, h8 = ml.ensure_endf(body, ident) + if len(out) == len(body): + print(f"EndF: already present in {os.path.basename(path)} (no change)"); return + trailer = out[len(body):] # the EndF trailer (60 bytes with identity) + for i, b in enumerate(trailer): + ih[app_end + i] = b # write it right after the app's last byte + ih.write_hex_file(path) + print(f"EndF: appended to {os.path.basename(path)} at 0x{app_end:X} " + f"(app=0x{app_start:X}.. body_len={len(body)} body_hash={h8.hex()} " + f"target={ident.target_id:#010x} hw='{ident.hw_id}' fw={ident.fw_version:#010x})") + + +if _ota_enabled(): + if _is_nrf52(): + env.AddPostAction("$BUILD_DIR/${PROGNAME}.hex", _append_endf_hex) # noqa: F821 + else: + env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", _append_endf) # noqa: F821 +else: + print("EndF: ENABLE_OTA not defined; skipping trailer injection") diff --git a/tools/mota/test_mota.py b/tools/mota/test_mota.py new file mode 100644 index 0000000000..cd9a522e9e --- /dev/null +++ b/tools/mota/test_mota.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +Tests for motalib — run with the meshcore venv: + + ./meshcore/bin/python tools/mota/test_mota.py + +(Also pytest-compatible: functions are named test_*.) +""" + +from __future__ import annotations + +import io +import os +import random +import struct + +import motalib as ml +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + + +def _fw(seed, size): + random.seed(seed) + return bytes(random.getrandbits(8) for _ in range(size)) + + +# --- multihash / version --------------------------------------------------- + +def test_version_pack_roundtrip(): + assert ml.pack_version("1.16.0") == (1 << 24) | (16 << 16) + assert ml.unpack_version(ml.pack_version("1.16.0.2")) == "1.16.0.2" + assert ml.pack_version(0x01100000) == 0x01100000 + + +def test_target_id_for_env(): + import hashlib + env = "RAK_4631_companion_radio_usb" + expect = int.from_bytes(hashlib.sha256(env.encode()).digest()[:4], "little") + assert ml.target_id_for_env(env) == expect + # distinct envs (same board, different role) get distinct ids + assert ml.target_id_for_env("RAK_4631_repeater") != ml.target_id_for_env("RAK_4631_companion_radio_usb") + + +# --- EndF ------------------------------------------------------------------ + +def test_endf_roundtrip_and_idempotent(): + body = _fw(1, 5000) + img, h8 = ml.ensure_endf(body) + assert len(img) == 5000 + ml.ENDF_LEN + assert ml.has_endf(img) + pbody, ph8 = ml.parse_endf(img) + assert pbody == body and ph8 == h8 == ml.mh8(body) + # idempotent: feeding an already-EndF'd image returns it unchanged + img2, h82 = ml.ensure_endf(img) + assert img2 == img and h82 == h8 + + +def test_endf_rejects_garbage_tail(): + assert not ml.has_endf(b"too short") + body = _fw(2, 1000) + img = body + ml.ENDF_MAGIC + struct.pack(" zero-filled (still fixed size, still self-consistent) + z, _ = ml.ensure_endf(body) + assert len(z) == len(body) + ml.ENDF_LEN and ml.parse_endf_ident(z) == ml.FwIdent(0, 0, "") + + +# --- merkle ---------------------------------------------------------------- + +def test_merkle_single_block(): + leaves = [ml.mh4(b"x")] + assert ml.merkle_root(leaves) == leaves[0] + + +def test_merkle_proofs_all_indices_various_counts(): + for count in [1, 2, 3, 4, 5, 7, 8, 9, 16, 17, 100]: + payload = _fw(count, count * 1024 - 13) # last block short, no padding + leaves = ml.leaf_hashes(payload, 1024) + assert len(leaves) == count + root = ml.merkle_root(leaves) + for i in range(count): + proof = ml.merkle_proof(leaves, i) + assert ml.verify_proof(leaves[i], i, proof, root, count), (count, i) + # a tampered leaf must fail its proof + bad = bytes([leaves[0][0] ^ 0xFF]) + leaves[0][1:] + assert not ml.verify_proof(bad, 0, ml.merkle_proof(leaves, 0), root, count) + + +# --- full container -------------------------------------------------------- + +def test_full_build_parse_verify(): + fw = _fw(10, 33 * 1024 + 7) + image, _ = ml.ensure_endf(fw) + m = ml.build_manifest( + target_id=0xDEADBEEF, fw_version=ml.pack_version("1.16.0"), + image_size=len(image), payload=image, block_size=1024, + image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, is_full=True) + blob = ml.build_container(m, image) + + parsed = ml.parse_container(blob) + assert parsed.manifest.target_id == 0xDEADBEEF + assert parsed.manifest.is_full and not parsed.manifest.is_signed + assert parsed.manifest.image_hash == ml.mh32(image) + assert parsed.payload == image + assert ml.verify(parsed) == [] + + +def test_hw_id_roundtrip_and_signed(): + # the v2 hw_id is a 32-byte NUL-padded ASCII tag in the SIGNED head; it must round-trip + be covered + # by the signature (tampering it breaks verification). + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + fw = _fw(77, 4 * 1024) + image, _ = ml.ensure_endf(fw) + priv = Ed25519PrivateKey.from_private_bytes(bytes(range(32))) + m = ml.build_manifest( + target_id=0xABCD, fw_version=ml.pack_version("2.0.0"), + image_size=len(image), payload=image, block_size=1024, + image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, is_full=True, + sign_priv=priv, hw_id="RAK4631") + blob = ml.build_container(m, image) + parsed = ml.parse_container(blob) + assert parsed.manifest.format_ver == 2 + assert parsed.manifest.hw_id == b"RAK4631" + b"\0" * (32 - 7) + assert parsed.manifest.hw_id.rstrip(b"\0").decode() == "RAK4631" + assert ml.verify(parsed) == [] + # flip a byte of the on-wire hw_id -> signature must fail (it's in the signed region) + bad = bytearray(blob) + hw_off = 8 + 57 # MAGIC(4)+total(4) + fixed head up to codec(57) = start of hw_id + bad[hw_off] ^= 0xFF + assert ml.verify(ml.parse_container(bytes(bad))) != [] + + +def test_tampered_payload_detected(): + fw = _fw(11, 10 * 1024) + image, _ = ml.ensure_endf(fw) + m = ml.build_manifest(target_id=1, fw_version=1, image_size=len(image), payload=image, + block_size=1024, image_hash=ml.mh32(image), + codec_id=ml.CODEC_FULL, is_full=True) + blob = bytearray(ml.build_container(m, image)) + # flip a byte inside the payload region + payload_off = blob.index(image) + blob[payload_off + 50] ^= 0xFF + problems = ml.verify(ml.parse_container(bytes(blob))) + assert any("leaves" in p or "merkle" in p or "image_hash" in p for p in problems), problems + + +# --- signing --------------------------------------------------------------- + +def test_signed_build_and_verify(): + priv = Ed25519PrivateKey.generate() + fw = _fw(12, 20 * 1024) + image, _ = ml.ensure_endf(fw) + m = ml.build_manifest(target_id=7, fw_version=ml.pack_version("2.0.0"), + image_size=len(image), payload=image, block_size=1024, + image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL, + is_full=True, sign_priv=priv) + parsed = ml.parse_container(ml.build_container(m, image)) + assert parsed.manifest.is_signed + assert ml.verify(parsed, expect_pub=priv.public_key().public_bytes_raw()) == [] + # wrong expected key -> flagged + other = Ed25519PrivateKey.generate().public_key().public_bytes_raw() + assert any("signer_pubkey" in p for p in ml.verify(parsed, expect_pub=other)) + + +def test_tampered_signature_detected(): + priv = Ed25519PrivateKey.generate() + fw = _fw(13, 8 * 1024) + image, _ = ml.ensure_endf(fw) + m = ml.build_manifest(target_id=7, fw_version=1, image_size=len(image), payload=image, + block_size=1024, image_hash=ml.mh32(image), + codec_id=ml.CODEC_FULL, is_full=True, sign_priv=priv) + blob = bytearray(ml.build_container(m, image)) + # flip a byte of target_id (inside signed region) without re-signing + blob[10] ^= 0xFF + problems = ml.verify(ml.parse_container(bytes(blob))) + assert any("signature INVALID" in p for p in problems), problems + + +# --- approval enforcement -------------------------------------------------- + +def test_approval_default_and_flagged_if_preapproved(): + fw = _fw(14, 4 * 1024) + image, _ = ml.ensure_endf(fw) + m = ml.build_manifest(target_id=1, fw_version=1, image_size=len(image), payload=image, + block_size=1024, image_hash=ml.mh32(image), + codec_id=ml.CODEC_FULL, is_full=True) + assert m.approval == ml.APPROVAL_NOT + # simulate a malicious pre-approved container -> verify must flag it + m.approval = ml.APPROVAL_YES + parsed = ml.parse_container(ml.build_container(m, image)) + assert any("approval" in p for p in ml.verify(parsed)) + + +# --- delta ----------------------------------------------------------------- + +def test_delta_build_apply_verify(): + old_body = _fw(20, 40 * 1024) + # new = old with a chunk changed + appended -> a real, small-ish delta + new_body = bytearray(old_body) + for i in range(1000, 1500): + new_body[i] = (new_body[i] + 1) & 0xFF + new_body += _fw(21, 2048) + old_image, base_hash = ml.ensure_endf(bytes(old_body)) + new_image, _ = ml.ensure_endf(bytes(new_body)) + + import detools + fp = io.BytesIO() + detools.create_patch(io.BytesIO(old_image), io.BytesIO(new_image), fp, + patch_type="sequential", compression="crle") + delta = fp.getvalue() + # with compression a near-identical-base delta is a fraction of the full image + assert len(delta) < len(new_image) // 2, (len(delta), len(new_image)) + + m = ml.build_manifest(target_id=0xABCD, fw_version=ml.pack_version("1.2.0"), + image_size=len(new_image), payload=delta, block_size=1024, + image_hash=ml.mh32(new_image), codec_id=ml.CODEC_DETOOLS_SEQUENTIAL, + is_full=False, base_hash=base_hash) + parsed = ml.parse_container(ml.build_container(m, delta)) + assert parsed.manifest.base_hash == base_hash == ml.mh8(bytes(old_body)) + # full verify incl. applying the delta to the base and checking image_hash + assert ml.verify(parsed, base_image=old_image) == [] + # wrong base must fail the delta->image_hash check + wrong = ml.verify(parsed, base_image=_fw(99, 40 * 1024)) + assert wrong, "delta verify against a wrong base should fail" + + +# --- runner ---------------------------------------------------------------- + +def _run(): + tests = {k: v for k, v in sorted(globals().items()) + if k.startswith("test_") and callable(v)} + failed = 0 + for name, fn in tests.items(): + try: + fn() + print(f"ok {name}") + except Exception as e: # noqa: BLE001 + failed += 1 + import traceback + print(f"FAIL {name}: {e}") + traceback.print_exc() + print(f"\n{len(tests) - failed}/{len(tests)} passed") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(_run()) diff --git a/tools/motatool/.gitignore b/tools/motatool/.gitignore new file mode 100644 index 0000000000..567609b123 --- /dev/null +++ b/tools/motatool/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/tools/motatool/CMakeLists.txt b/tools/motatool/CMakeLists.txt new file mode 100644 index 0000000000..4a15306377 --- /dev/null +++ b/tools/motatool/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.16) +project(motatool LANGUAGES CXX) + +# Self-contained MeshCore OTA host tool: build / verify / inspect / serve `.mota` containers. +# Portable C++17 — builds on Ubuntu, Raspberry Pi, and any arch with a C++17 compiler + OpenSSL. +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() +add_compile_options(-Wall -Wextra) + +# OpenSSL libcrypto provides SHA-256 + Ed25519 (sign/verify/keygen). On Debian/Ubuntu/Pi: +# sudo apt install libssl-dev +find_package(OpenSSL REQUIRED) + +# Core logic (everything except the CLI front-end) — shared by the tool and the test runner. +add_library(motatool_core STATIC + src/mota.cpp + src/crypto.cpp + src/input.cpp + src/serve.cpp +) +# `src` for the tool's own headers; the repo's helpers/ota for the shared, generated OtaTargets.h +# (target_id -> env-name table — single source of truth with the firmware, no hand-maintained copy). +target_include_directories(motatool_core PUBLIC + src + "${CMAKE_CURRENT_SOURCE_DIR}/../../src/helpers/ota" +) +target_link_libraries(motatool_core PUBLIC OpenSSL::Crypto) + +add_executable(motatool src/main.cpp) +target_link_libraries(motatool PRIVATE motatool_core) +install(TARGETS motatool RUNTIME DESTINATION bin) + +# Unit tests (no external test framework — a tiny built-in harness keeps the project self-contained). +# Run with: cmake --build build && ctest --test-dir build --output-on-failure +# or: ./build/motatool_tests +enable_testing() +add_executable(motatool_tests tests/test_motatool.cpp) +target_link_libraries(motatool_tests PRIVATE motatool_core) +add_test(NAME motatool_tests COMMAND motatool_tests) diff --git a/tools/motatool/README.md b/tools/motatool/README.md new file mode 100644 index 0000000000..235d8af472 --- /dev/null +++ b/tools/motatool/README.md @@ -0,0 +1,117 @@ +# motatool — self-contained MeshCore OTA host tool (C++) + +A small, portable C++17 tool that **builds**, **verifies**, and **serves** MeshCore `.mota` +firmware-update containers. It is an independent project (its own `CMakeLists.txt`) that compiles on a +laptop, a Raspberry Pi, or any architecture with a C++17 compiler — so it can run as a lightweight +folder-relay daemon on small hardware. + +It implements the wire spec in [`../../docs/ota_protocol.md`](../../docs/ota_protocol.md) and is +cross-checked byte-for-byte against the Python reference packager `tools/mota/motalib.py` (a full signed +`.mota` built by `motatool` is identical to the reference's output). + +## What it does + +| Command | Purpose | +|---|---| +| `build` | Create a **full** or **delta** `.mota` from a firmware (local file **or** http(s) URL). | +| `verify` | Validate one or more `.mota` (merkle tree, leaves vs payload, image hash, Ed25519 signature). `--pub` requires a specific signer; `--base` confirms a sequential delta rebuilds its image. | +| `inspect`| Print every field of a `.mota`'s manifest (debugging). | +| `serve` | Serve a **folder** of `.mota` to a node over USB serial (`--serial`) or WiFi (`--tcp`). Invalid files are warned about and skipped — one corrupt file never sinks the rest. | +| `keygen` | Generate an Ed25519 signing keypair (hex). | + +Every command has detailed, example-rich help: `motatool --help`. + +## Build + +Dependencies: a C++17 compiler, CMake, and **OpenSSL** (libcrypto: SHA-256 + Ed25519). For URL input +and delta creation, see the notes below. + +```bash +sudo apt install build-essential cmake libssl-dev # Debian / Ubuntu / Raspberry Pi OS +cmake -S tools/motatool -B tools/motatool/build +cmake --build tools/motatool/build -j +# -> tools/motatool/build/motatool +``` + +### Tests + +Unit tests (a tiny built-in harness — no external framework) cover the crypto, EndF, build/verify, +merkle, parse rejection, the folder scanner + seeder protocol, and a real detools delta round-trip +(auto-skipped if `detools` isn't installed): + +```bash +ctest --test-dir tools/motatool/build --output-on-failure +# or directly: tools/motatool/build/motatool_tests +``` + +## Usage + +```bash +MT=tools/motatool/build/motatool + +# 1. signing keypair (64-char hex) +$MT keygen --out signer.key # writes signer.key + signer.key.pub + +# 2a. FULL .mota — payload IS the flashable image (identity is auto-read from the firmware's EndF) +$MT build --fw firmware.bin --sign signer.key --out-dir ./motas/ + +# 2b. FULL .mota straight from a URL +$MT build --fw https://example.org/Heltec_v3_repeater.bin --sign signer.key --out-dir ./motas/ + +# 2c. DELTA .mota against a previous release (codec auto-selected from the hardware tag: +# nRF52 -> in-place, ESP32 -> sequential; override with --codec) +$MT build --fw new.bin --base old.bin --sign signer.key --out-dir ./motas/ +$MT build --fw new.bin --base old.bin --codec sequential --out-dir ./motas/ + +# 3. validate a folder of .mota (optionally require a signer / check a delta against its base) +$MT verify ./motas/*.mota +$MT verify update.mota --pub signer.key.pub +$MT verify delta.mota --base old_firmware.bin + +# 4. dump a single .mota's manifest fields +$MT inspect ./motas/RAK4631_04D413FD_v1.16.0_full_ABCD1234.mota + +# 5. serve a folder to a node (recursive; skips non-.mota; warns on corrupt) +$MT serve --dir ./motas --serial /dev/ttyUSB0 --baud 115200 -v # over USB serial +$MT serve --dir ./motas --tcp 192.168.1.50:5001 -v # …or over WiFi (ESP32 companion, port 5001) +``` + +`build` notes: +- **Identity is self-described.** A firmware built by the project's `pio_endf.py` carries its + `target_id`/`fw_version`/`hw_id` in its 56-byte `EndF` trailer; `build` reads them, so + `--target-env`/`--target-id`, `--fw-version`, and `--hw-id` are **optional** (explicit flags override). +- **Cross-hardware delta guard.** A delta is refused if the base and target firmware identities differ + (read from their `EndF`, not filenames); pass `--force` to override. +- **Delta codec** needs the `detools` encoder (the project's pinned codec — never reimplemented). Install + it (`pip install detools`) and ensure it's on `PATH`, or pass `--detools `. **Full** builds, + **verify**, and **serve** need no detools. In-place defaults: `--inplace-memory 0xAE000` + (nRF52 workspace), `--inplace-segment 4096`. +- **URL input** uses the system `curl` (falling back to `wget`) — no link-time dependency. +- All output is written into one folder (`--out-dir`, default `.`) with a descriptive, unique name: + `__v__.mota`. + +## Serving is transport-agnostic (USB serial today, BLE later) + +The protocol is split so the serving logic is reusable across links: + +- **`SeederCore`** (`src/serve.{h,cpp}`) is transport-free: it maps a request `(op, args)` to a response + `(status, payload)` — `COUNT` / `DESCRIBE(idx)` / `READ(idx, off, len)` over the validated catalog. +- **`Transport` + `serve_loop()`** wrap it with the byte-stream framing (magic + XOR checksum + resync, + `MotaSeederProto.h`). Two transports ship: **`SerialTransport`** (`--serial`, USB-UART) and + **`TcpTransport`** (`--tcp `, WiFi — connects to the node's dedicated seeder port, default + `5001`, which the ESP32 companion runs alongside its phone-app port so both work at once). + +To serve the same folder over **BLE** (e.g. an Android phone relaying to a MeshCore node), implement the +transport at the GATT layer — a request characteristic write hands `(op, args)` straight to +`SeederCore::handle()` and the reply is sent as a notification. No byte-stream framing is needed there +(BLE is reliable/segmented), and `SeederCore` + `Folder` are reused unchanged. OpenSSL builds for the +Android NDK, so the validation path ports as-is. + +## Relationship to `tools/mota/` + +`motatool` is the user-facing CLI: it replaced the old Python `mota.py` (build/verify/inspect/keygen), +`mota_seeder.py` (serve), and `dev_motas.py` (CI `.mota` packaging — its nRF52 `.hex` extraction is now a +built-in `motatool` input format), all removed. The Python `tools/mota/` directory remains as the +**reference implementation** (`motalib.py`, the spec oracle + unit tests) and the **firmware build/test +glue** (`pio_endf.py` build hook, `gen_vectors.py` test vectors). Their `.mota` output is byte-identical +(verified by cross-checks). diff --git a/tools/motatool/src/crypto.cpp b/tools/motatool/src/crypto.cpp new file mode 100644 index 0000000000..a4076a9c9b --- /dev/null +++ b/tools/motatool/src/crypto.cpp @@ -0,0 +1,61 @@ +#include "crypto.h" +#include +#include +#include + +namespace mota { + +void sha256_trunc(uint8_t* out, size_t out_len, const uint8_t* data, size_t len) { + uint8_t full[32]; + SHA256(data, len, full); + if (out_len > 32) out_len = 32; + std::memcpy(out, full, out_len); +} + +bool ed25519_verify(const uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t pub[32]) { + EVP_PKEY* key = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, nullptr, pub, 32); + if (!key) return false; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + bool ok = ctx && EVP_DigestVerifyInit(ctx, nullptr, nullptr, nullptr, key) == 1 && + EVP_DigestVerify(ctx, sig, 64, msg, msg_len) == 1; + if (ctx) EVP_MD_CTX_free(ctx); + EVP_PKEY_free(key); + return ok; +} + +bool ed25519_sign(uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t priv[32]) { + EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, priv, 32); + if (!key) return false; + EVP_MD_CTX* ctx = EVP_MD_CTX_new(); + size_t siglen = 64; + bool ok = ctx && EVP_DigestSignInit(ctx, nullptr, nullptr, nullptr, key) == 1 && + EVP_DigestSign(ctx, sig, &siglen, msg, msg_len) == 1 && siglen == 64; + if (ctx) EVP_MD_CTX_free(ctx); + EVP_PKEY_free(key); + return ok; +} + +bool ed25519_pub_from_priv(uint8_t pub[32], const uint8_t priv[32]) { + EVP_PKEY* key = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, nullptr, priv, 32); + if (!key) return false; + size_t len = 32; + bool ok = EVP_PKEY_get_raw_public_key(key, pub, &len) == 1 && len == 32; + EVP_PKEY_free(key); + return ok; +} + +bool ed25519_keygen(uint8_t priv[32], uint8_t pub[32]) { + EVP_PKEY* key = nullptr; + EVP_PKEY_CTX* pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr); + bool ok = pctx && EVP_PKEY_keygen_init(pctx) == 1 && EVP_PKEY_keygen(pctx, &key) == 1; + if (ok) { + size_t lp = 32, lk = 32; + ok = EVP_PKEY_get_raw_private_key(key, priv, &lp) == 1 && lp == 32 && + EVP_PKEY_get_raw_public_key(key, pub, &lk) == 1 && lk == 32; + } + if (key) EVP_PKEY_free(key); + if (pctx) EVP_PKEY_CTX_free(pctx); + return ok; +} + +} // namespace mota diff --git a/tools/motatool/src/crypto.h b/tools/motatool/src/crypto.h new file mode 100644 index 0000000000..eac5d8d348 --- /dev/null +++ b/tools/motatool/src/crypto.h @@ -0,0 +1,28 @@ +// SHA-256 (multihash truncations) and Ed25519 — backed by OpenSSL libcrypto, so the tool is portable +// across Ubuntu / Raspberry Pi / any arch that has OpenSSL (and Android NDK for a future BLE seeder). +#pragma once +#include +#include +#include +#include +#include + +namespace mota { + +// SHA-256 of `data`, truncated to out_len (sha2-256:N). out_len <= 32. +void sha256_trunc(uint8_t* out, size_t out_len, const uint8_t* data, size_t len); + +inline std::array mh4(const uint8_t* d, size_t n) { std::array o; sha256_trunc(o.data(),4,d,n); return o; } +inline std::array mh8(const uint8_t* d, size_t n) { std::array o; sha256_trunc(o.data(),8,d,n); return o; } +inline std::array mh32(const uint8_t* d, size_t n) { std::array o; sha256_trunc(o.data(),32,d,n); return o; } + +// Ed25519. Keys are raw 32-byte (private seed / public key), matching motalib / the device. +bool ed25519_verify(const uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t pub[32]); +// Sign `msg` with a 32-byte raw private seed -> 64-byte signature. Returns false on error. +bool ed25519_sign(uint8_t sig[64], const uint8_t* msg, size_t msg_len, const uint8_t priv[32]); +// Derive the 32-byte raw public key from a 32-byte raw private seed. +bool ed25519_pub_from_priv(uint8_t pub[32], const uint8_t priv[32]); +// Generate a fresh keypair (raw 32-byte each). +bool ed25519_keygen(uint8_t priv[32], uint8_t pub[32]); + +} // namespace mota diff --git a/tools/motatool/src/input.cpp b/tools/motatool/src/input.cpp new file mode 100644 index 0000000000..bb0a4b12a5 --- /dev/null +++ b/tools/motatool/src/input.cpp @@ -0,0 +1,102 @@ +#include "input.h" +#include "util.h" +#include +#include +#include + +namespace mota { + +bool is_url(const std::string& s) { + return s.rfind("http://", 0) == 0 || s.rfind("https://", 0) == 0; +} + +static bool ends_with_ci(const std::string& s, const std::string& suf) { + if (s.size() < suf.size()) return false; + for (size_t i = 0; i < suf.size(); i++) + if (std::tolower((unsigned char)s[s.size() - suf.size() + i]) != std::tolower((unsigned char)suf[i])) return false; + return true; +} + +// Parse Intel HEX text into the flat binary it represents (min..max address, gaps filled 0xFF). +// nRF52/STM32 PlatformIO builds emit firmware.hex; pio_endf appends the EndF inside it, so the extracted +// binary IS the OTA image (BODY||EndF) starting at the app base — exactly what `build` wants. +static std::string parse_intel_hex(const std::vector& in, std::vector& out) { + const std::string s((const char*)in.data(), in.size()); + auto nib = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + struct Seg { uint64_t addr; std::vector data; }; + std::vector segs; + uint64_t base = 0, lo = UINT64_MAX, hi = 0; + size_t i = 0; + bool saw_eof = false; + while (i < s.size()) { + if (s[i] != ':') { i++; continue; } // tolerate CR/LF/whitespace between records + size_t p = i + 1; + auto rb = [&](uint8_t& v) -> bool { + if (p + 2 > s.size()) return false; + int h = nib(s[p]), l = nib(s[p + 1]); + if (h < 0 || l < 0) return false; + v = (uint8_t)((h << 4) | l); p += 2; return true; + }; + uint8_t len, ah, al, type; + if (!rb(len) || !rb(ah) || !rb(al) || !rb(type)) return "malformed Intel HEX record"; + uint8_t sum = (uint8_t)(len + ah + al + type); + std::vector data(len); + for (uint8_t k = 0; k < len; k++) { if (!rb(data[k])) return "truncated Intel HEX data"; sum += data[k]; } + uint8_t cks; + if (!rb(cks)) return "missing Intel HEX checksum"; + if ((uint8_t)(sum + cks) != 0) return "Intel HEX checksum error"; + uint16_t addr = (uint16_t)((ah << 8) | al); + if (type == 0x00) { // data + uint64_t a = base + addr; + if (a < lo) lo = a; + if (a + len > hi) hi = a + len; + segs.push_back({a, std::move(data)}); + } else if (type == 0x04) { // extended linear address (upper 16 bits) + if (len != 2) return "bad Intel HEX ELA record"; + base = (uint64_t)((data[0] << 8) | data[1]) << 16; + } else if (type == 0x02) { // extended segment address + if (len != 2) return "bad Intel HEX ESA record"; + base = (uint64_t)((data[0] << 8) | data[1]) << 4; + } else if (type == 0x01) { // EOF + saw_eof = true; break; + } // 0x03/0x05 (start address) ignored + i = p; + } + if (segs.empty()) return "no data records in Intel HEX"; + if (!saw_eof) return "Intel HEX missing EOF record"; + out.assign((size_t)(hi - lo), 0xFF); + for (auto& sg : segs) std::memcpy(out.data() + (size_t)(sg.addr - lo), sg.data.data(), sg.data.size()); + return ""; +} + +std::string read_input(const std::string& src, std::vector& out) { + std::vector raw; + if (!is_url(src)) { + if (!read_file(src, raw)) return "cannot read file: " + src; + } else { + // URL: download to a temp file via curl (fallback wget), then read it back. + char tmp[] = "/tmp/motadlXXXXXX"; + int fd = mkstemp(tmp); + if (fd < 0) return "mkstemp failed"; + close(fd); + int rc = run_argv({"curl", "-fLsS", "-o", tmp, src}); // -f fail, -L follow, -sS silent+show-errors + if (rc == 127) rc = run_argv({"wget", "-q", "-O", tmp, src}); + std::string err; + if (rc == 127) err = "neither curl nor wget is available to fetch " + src; + else if (rc != 0) err = "download failed (exit " + std::to_string(rc) + "): " + src; + else if (!read_file(tmp, raw)) err = "could not read the downloaded file: " + src; + unlink(tmp); + if (!err.empty()) return err; + } + if (raw.empty()) return "input is empty: " + src; + if (ends_with_ci(src, ".hex")) return parse_intel_hex(raw, out); // Intel HEX -> flat image + out = std::move(raw); + return ""; +} + +} // namespace mota diff --git a/tools/motatool/src/input.h b/tools/motatool/src/input.h new file mode 100644 index 0000000000..ff92f5fd0a --- /dev/null +++ b/tools/motatool/src/input.h @@ -0,0 +1,15 @@ +// Read a firmware blob from a local file OR an http(s):// URL. URLs are fetched with the system `curl` +// (or `wget`) binary — no link-time dependency, so the tool stays self-contained and builds anywhere. +#pragma once +#include +#include +#include + +namespace mota { + +// Returns "" on success (fills `out`), else an error string. `what` is a label for error messages. +std::string read_input(const std::string& path_or_url, std::vector& out); + +bool is_url(const std::string& s); + +} // namespace mota diff --git a/tools/motatool/src/main.cpp b/tools/motatool/src/main.cpp new file mode 100644 index 0000000000..8596b9f3ca --- /dev/null +++ b/tools/motatool/src/main.cpp @@ -0,0 +1,526 @@ +// motatool — build, verify, and serve MeshCore `.mota` firmware-update containers. +// build create a full or delta .mota from a firmware (file or http(s) URL) +// verify validate one or more .mota (merkle / hashes / signature) +// serve serve a folder of .mota to a node (USB serial); invalid files are warned + skipped +// keygen generate an Ed25519 signing keypair (64-char hex) +#include "mota.h" +#include "input.h" +#include "serve.h" +#include "crypto.h" +#include "util.h" +#include +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace mota; + +static volatile bool g_stop = false; +static void on_sigint(int) { g_stop = true; } + +// minimal flag parser: --key value / --flag +struct Args { + std::map opt; + std::vector pos; + bool has(const std::string& k) const { return opt.count(k); } + std::string get(const std::string& k, const std::string& d = "") const { + auto it = opt.find(k); return it == opt.end() ? d : it->second; + } +}; +static Args parse_args(int argc, char** argv, int start, + const std::vector& bool_flags) { + Args a; + for (int i = start; i < argc; i++) { + std::string s = argv[i]; + if (s == "-h" || s == "--help" || s == "help") { a.opt["help"] = "1"; continue; } + if (s == "-v") { a.opt["verbose"] = "1"; continue; } + if (s.rfind("--", 0) == 0) { + std::string k = s.substr(2); + if (std::find(bool_flags.begin(), bool_flags.end(), k) != bool_flags.end()) a.opt[k] = "1"; + else if (i + 1 < argc) a.opt[k] = argv[++i]; + else a.opt[k] = ""; + } else a.pos.push_back(s); + } + return a; +} + +static bool load_priv(const std::string& path, std::vector& priv) { + std::vector raw; + if (!read_file(path, raw)) return false; + std::string txt((const char*)raw.data(), raw.size()); // try hex (mota.py format) first + while (!txt.empty() && (std::isspace((unsigned char)txt.back()))) txt.pop_back(); + std::vector hx; + if (from_hex(txt, hx) && hx.size() == 32) { priv = hx; return true; } + if (raw.size() == 32) { priv = raw; return true; } // else accept a raw 32-byte file + return false; +} + +static std::string version_str(uint32_t v) { + char b[24]; std::snprintf(b, sizeof(b), "%u.%u.%u", (v>>24)&0xFF, (v>>16)&0xFF, (v>>8)&0xFF); + return b; +} + +// ---- help text (the tool is meant to be usable from --help alone) --------------------------------- +static void help_top() { + std::cout << +"motatool — build, verify, and serve MeshCore .mota firmware-update containers.\n" +"\n" +"A .mota is a signed, self-verifying package of a firmware update that MeshCore nodes fetch\n" +"over LoRa, block by block. This tool makes those packages, checks them, and serves a folder\n" +"of them to a node over USB.\n" +"\n" +"USAGE\n" +" motatool [options]\n" +" motatool --help detailed help + examples for a command\n" +"\n" +"COMMANDS\n" +" build Package a firmware as a .mota (a full image, or a small delta vs a previous build).\n" +" verify Check that .mota files are valid (block hashes, image hash, signature).\n" +" inspect Print every field of a .mota's manifest (debugging).\n" +" serve Serve a folder of .mota to a node over USB serial or WiFi (corrupt files are skipped).\n" +" keygen Generate an Ed25519 signing keypair.\n" +"\n" +"TYPICAL WORKFLOW\n" +" 1. Make a signing key once:\n" +" motatool keygen --out signer.key\n" +" 2. Package a firmware (identity is read from the firmware itself):\n" +" motatool build --fw firmware.bin --sign signer.key --out-dir ./motas\n" +" 3. Serve the folder to a node plugged in over USB:\n" +" motatool serve --dir ./motas --serial /dev/ttyUSB0\n"; +} + +static void help_build() { + std::cout << +"motatool build — package a firmware as a .mota update container.\n" +"\n" +"USAGE\n" +" motatool build --fw [--base ] [options]\n" +"\n" +"WHAT IT DOES\n" +" With only --fw: builds a FULL update (the payload is the whole firmware image).\n" +" With --base too: builds a DELTA (a small patch from the base firmware to the new one),\n" +" which is far smaller to send over LoRa. The delta codec is picked for the target hardware\n" +" automatically: nRF52 (single-slot) -> in-place, ESP32 (A/B slots) -> sequential.\n" +"\n" +" Firmware identity (target, version, hardware tag) is read automatically from the firmware's\n" +" EndF trailer, so you normally don't pass --target-*/--fw-version/--hw-id. Pass them only to\n" +" override, or for a raw .bin that has no EndF identity.\n" +"\n" +"INPUT (--fw is required)\n" +" --fw NEW firmware. A local path OR an http(s):// URL (downloaded with curl/wget).\n" +" A .bin is used as-is; a .hex (nRF52/STM32 build) is parsed to its flat image first.\n" +" --base previous firmware to diff against -> makes a delta (omit it for a full image).\n" +"\n" +"IDENTITY (optional — auto-read from the firmware's EndF)\n" +" --target-env PlatformIO env name (e.g. RAK_4631_repeater); hashed into the target id.\n" +" --target-id raw target id instead of --target-env (e.g. 0x04D413FD).\n" +" --fw-version firmware version (e.g. 1.16.0).\n" +" --hw-id hardware tag (e.g. RAK4631, Heltec_v3). Same tag = bootable-compatible;\n" +" a node refuses a .mota whose hw-id is for different hardware (brick-safety).\n" +"\n" +"DELTA OPTIONS\n" +" --codec full|sequential|inplace force the codec (default: full with no --base, else auto-from-hw).\n" +" --inplace-memory nRF52 in-place workspace size (default 0xAE000).\n" +" --inplace-segment nRF52 in-place erase/segment size (default 4096).\n" +" --detools the detools encoder to call (default: 'detools' on PATH). Needed only for deltas.\n" +" --force build the delta even if base and target hardware identities differ.\n" +"\n" +"SIGNING\n" +" --sign Ed25519 private key (hex or raw 32 bytes, from 'motatool keygen'). Signing lets a\n" +" node auto-install the update if it trusts the matching public key. Unsigned still works\n" +" for manual installs.\n" +"\n" +"OUTPUT\n" +" --out-dir where to write the .mota (default: current directory). Keep all your .mota in one\n" +" folder and point 'serve' at it. The file is auto-named:\n" +" __v__.mota\n" +" --out write to exactly this path instead (overrides --out-dir and the auto-name).\n" +"\n" +"EXAMPLES\n" +" # full image, signed, into ./motas\n" +" motatool build --fw firmware.bin --sign signer.key --out-dir ./motas\n" +"\n" +" # full image fetched straight from a release URL\n" +" motatool build --fw https://example.org/RAK_4631_repeater.bin --sign signer.key --out-dir ./motas\n" +"\n" +" # delta from the previous release (codec auto-selected from the hardware)\n" +" motatool build --fw new.bin --base old.bin --sign signer.key --out-dir ./motas\n" +"\n" +" # raw .bin with no EndF identity: supply it explicitly\n" +" motatool build --fw app.bin --hw-id Heltec_v3 --target-env Heltec_v3_repeater --fw-version 1.16.0\n"; +} + +static void help_verify() { + std::cout << +"motatool verify — check that .mota files are valid.\n" +"\n" +"USAGE\n" +" motatool verify [more.mota ...] [--pub ] [--base ]\n" +"\n" +"For each file it checks the structure, that the per-block hashes match the payload, the merkle\n" +"root, the full-image hash (for full images), and the Ed25519 signature (for signed containers).\n" +"It prints 'OK' or 'FAIL ' per file; the exit code is non-zero if any file fails. This is\n" +"the same validation 'serve' runs on a folder before serving.\n" +"\n" +"OPTIONS\n" +" --pub require the container to be signed by THIS public key (hex/raw, *.pub from keygen).\n" +" --base for a sequential delta, apply it to this base and confirm it rebuilds the image.\n" +" (Applies to every file given; full images ignore it; in-place deltas are skipped.)\n" +"\n" +"EXAMPLES\n" +" motatool verify ./motas/*.mota\n" +" motatool verify update.mota --pub signer.key.pub\n" +" motatool verify delta.mota --base old_firmware.bin\n"; +} + +static void help_inspect() { + std::cout << +"motatool inspect — print every field of a .mota's manifest.\n" +"\n" +"USAGE\n" +" motatool inspect \n" +"\n" +"Dumps the parsed manifest (versions, sizes, target/hardware, codec, merkle root, image hash,\n" +"base hash, signer key + signature, approval state, block count) — handy for debugging a package.\n" +"It does not validate integrity; use 'verify' for that.\n" +"\n" +"EXAMPLE\n" +" motatool inspect ./motas/RAK4631_04D413FD_v1.16.0_full_ABCD1234.mota\n"; +} + +static void help_serve() { + std::cout << +"motatool serve — serve a folder of .mota to a MeshCore node over USB serial or WiFi (TCP).\n" +"\n" +"USAGE\n" +" motatool serve --dir --serial [options] # over USB serial\n" +" motatool serve --dir --tcp [options] # over WiFi (ESP32 companion)\n" +"\n" +"It scans the folder for .mota files, validates each, and serves the valid ones to the node.\n" +"Corrupt/invalid files are reported and skipped — one bad file never stops the rest. The node\n" +"advertises them to the mesh as if it held them, and any node whose hardware matches can fetch\n" +"them. The relay is trustless: fetchers verify every block, so this host never needs the keys.\n" +"\n" +"OPTIONS (--dir and one of --serial / --tcp are required)\n" +" --dir folder of .mota to serve (searched recursively by default).\n" +" --serial the node's USB serial port (e.g. /dev/ttyUSB0 or /dev/ttyACM0).\n" +" --tcp the node's WiFi seeder address (default port 5001). This is a DEDICATED\n" +" port, separate from the companion port (5000), so serving doesn't disturb a\n" +" phone app connected to the node. The node auto-enables relaying on connect.\n" +" --baud serial speed (default 115200; --serial only).\n" +" --no-recursive serve only the top folder; don't descend into sub-folders.\n" +" --no-enable (--serial only) don't auto-send 'ota folder on'/'off' on the node's CLI.\n" +" -v, --verbose log each request the node makes (COUNT / DESCRIBE / READ).\n" +"\n" +"Leave it running; press Ctrl-C to stop. Over serial it shares the USB cable with the node's text\n" +"console; over TCP it uses the node's dedicated seeder port. The node only pulls while fetching.\n" +"\n" +"EXAMPLES\n" +" motatool serve --dir ./motas --serial /dev/ttyUSB0 -v\n" +" motatool serve --dir ./motas --tcp 192.168.4.234 -v\n"; +} + +static void help_keygen() { + std::cout << +"motatool keygen — generate an Ed25519 signing keypair.\n" +"\n" +"USAGE\n" +" motatool keygen [--out ]\n" +"\n" +"Prints the public key. With --out it writes the private key to and the public key to\n" +".pub (hex). Sign updates with the private key ('build --sign '); trust the\n" +"public key on a node to let it auto-install updates signed by you.\n" +"\n" +"EXAMPLE\n" +" motatool keygen --out signer.key\n"; +} + +static std::string hex8(uint32_t v) { char x[9]; std::snprintf(x, 9, "%08X", v); return x; } + +// human-readable label for a target_id (its PlatformIO env name), or "N/A" if not in the known table +static std::string target_label(uint32_t t) { + std::string n = target_env_name(t); + return n.empty() ? "N/A" : n; +} + +static int cmd_verify(const Args& a) { + if (a.has("help")) { help_verify(); return 0; } + if (a.pos.empty()) { help_verify(); return 2; } + + std::vector expect_pub; + if (a.has("pub") && !load_priv(a.get("pub"), expect_pub)) { // load_priv accepts a 32-byte hex/raw key + std::cerr << "cannot load --pub key (expect 32-byte hex or raw)\n"; return 2; + } + std::vector base_img; + if (a.has("base")) { + std::vector b; std::string e = read_input(a.get("base"), b); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + std::array bh; base_img = ensure_endf(b, parse_endf_ident(b), bh); + } + + int bad = 0; + for (const auto& f : a.pos) { + std::vector blob; + if (!read_file(f, blob)) { std::cout << "FAIL " << f << " : cannot read\n"; bad++; continue; } + auto probs = verify(blob); + Manifest m; bool parsed = parse(blob, m).empty(); + if (parsed && !expect_pub.empty()) { // --pub: must be signed by this exact key + if (!m.is_signed()) probs.push_back("not signed (but --pub was given)"); + else if (std::memcmp(m.signer.data(), expect_pub.data(), 32) != 0) probs.push_back("signed by a different key than --pub"); + } + if (parsed && !base_img.empty() && !m.is_full()) { // --base: prove a delta rebuilds the image + if (m.codec_id == CODEC_DETOOLS_SEQUENTIAL) { + std::vector patch(blob.begin() + m.payload_off(), blob.begin() + m.payload_off() + m.payload_size); + std::vector recon; + std::string e = detools_apply_seq(a.get("detools", "detools"), base_img, patch, recon); + if (!e.empty()) probs.push_back("delta apply: " + e); + else { auto h = mota::mh32(recon.data(), recon.size()); + if (std::memcmp(h.data(), m.image_hash.data(), 32) != 0) probs.push_back("delta does not rebuild image_hash against --base"); } + } else { + std::cout << "note " << f << " : in-place delta — --base apply-check skipped (bootloader-applied)\n"; + } + } + if (probs.empty()) { + std::cout << "OK " << f << " : " << (m.is_full() ? "full" : "delta") + << " target=" << hex8(m.target_id) << " [" << target_label(m.target_id) << "]" + << " v" << version_str(m.fw_version) << " hw=" << (m.hw_id_str().empty()? "?":m.hw_id_str()) + << " " << (m.is_signed() ? "signed" : "unsigned") + << " blocks=" << m.block_count << " size=" << blob.size() << "\n"; + } else { + bad++; + std::cout << "FAIL " << f << " :"; + for (auto& p : probs) std::cout << " [" << p << "]"; + std::cout << "\n"; + } + } + return bad ? 1 : 0; +} + +static int cmd_inspect(const Args& a) { + if (a.has("help")) { help_inspect(); return 0; } + if (a.pos.empty()) { help_inspect(); return 2; } + std::vector blob; + if (!read_file(a.pos[0], blob)) { std::cerr << "cannot read " << a.pos[0] << "\n"; return 1; } + Manifest m; + std::string e = parse(blob, m); + if (!e.empty()) { std::cerr << "not a valid .mota: " << e << "\n"; return 1; } + auto z = [](const uint8_t* p, size_t n){ for (size_t i=0;i in-place, else sequential) +static int infer_codec(const std::string& hw) { + std::string h; for (char c : hw) h.push_back((char)std::tolower((unsigned char)c)); + if (h.rfind("rak", 0) == 0 || h.find("nrf") != std::string::npos || h.find("nordic") != std::string::npos) + return CODEC_DETOOLS_INPLACE; + if (h.find("heltec") != std::string::npos || h.find("esp32") != std::string::npos || + h.find("xiao") != std::string::npos || h.find("tbeam") != std::string::npos || + h.find("tlora") != std::string::npos || h.find("tdeck") != std::string::npos) + return CODEC_DETOOLS_SEQUENTIAL; + return -1; // unknown +} + +static int cmd_build(const Args& a) { + if (a.has("help")) { help_build(); return 0; } + if (!a.has("fw")) { std::cerr << "error: --fw is required\n\n"; help_build(); return 2; } + BuildOpts o; + std::string e = read_input(a.get("fw"), o.fw); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + if (a.has("base")) { + e = read_input(a.get("base"), o.base); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + } + if (a.has("target-id")) { o.have_target = true; o.target_id = (uint32_t)std::strtoul(a.get("target-id").c_str(), nullptr, 0); } + else if (a.has("target-env")) { o.have_target = true; o.target_id = target_id_for_env(a.get("target-env")); } + if (a.has("fw-version")) { + uint32_t v; if (!pack_version(a.get("fw-version"), v)) { std::cerr << "bad --fw-version\n"; return 2; } + o.have_fwver = true; o.fw_version = v; + } + if (a.has("hw-id")) o.hw_id = a.get("hw-id"); + o.force = a.has("force"); + if (a.has("detools")) o.detools = a.get("detools"); + if (a.has("inplace-memory")) o.inplace_memory = (uint32_t)std::strtoul(a.get("inplace-memory").c_str(), nullptr, 0); + if (a.has("inplace-segment")) o.inplace_segment = (uint32_t)std::strtoul(a.get("inplace-segment").c_str(), nullptr, 0); + if (a.has("sign")) { + if (!load_priv(a.get("sign"), o.sign_priv)) { std::cerr << "cannot load signing key (expect 32-byte hex or raw)\n"; return 1; } + } + + bool is_delta = !o.base.empty(); + // resolve hw (flags override EndF) to pick the codec + FwIdent fid = parse_endf_ident(o.fw); + std::string hw = o.hw_id.empty() ? fid.hw_id : o.hw_id; + std::string codec = a.get("codec", is_delta ? "auto" : "full"); + if (!is_delta) o.codec = CODEC_FULL; + else if (codec == "sequential") o.codec = CODEC_DETOOLS_SEQUENTIAL; + else if (codec == "inplace") o.codec = CODEC_DETOOLS_INPLACE; + else if (codec == "full") { std::cerr << "a --base delta cannot use --codec full\n"; return 2; } + else { // auto + int c = infer_codec(hw); + if (c < 0) { std::cerr << "cannot infer codec from hw '" << hw << "' — pass --codec sequential|inplace\n"; return 2; } + o.codec = (uint8_t)c; + std::cerr << "note: codec auto-selected = " << (c == CODEC_DETOOLS_INPLACE ? "inplace" : "sequential") + << " (from hw '" << (hw.empty()?"?":hw) << "')\n"; + } + + std::vector blob; std::string name; + e = build(o, blob, name); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + + // sanity: the tool's own output must verify + auto probs = verify(blob); + if (!probs.empty()) { + std::cerr << "internal error: built .mota fails verification:"; + for (auto& p : probs) std::cerr << " [" << p << "]"; + std::cerr << "\n"; return 1; + } + + std::string outpath; + if (a.has("out")) { // explicit output path (overrides --out-dir + auto-name) + outpath = a.get("out"); + fs::path parent = fs::path(outpath).parent_path(); + if (!parent.empty()) { std::error_code ec; fs::create_directories(parent, ec); } + } else { + std::string outdir = a.get("out-dir", "."); + std::error_code ec; fs::create_directories(outdir, ec); + outpath = (fs::path(outdir) / name).string(); + } + if (!write_file(outpath, blob.data(), blob.size())) { std::cerr << "cannot write " << outpath << "\n"; return 1; } + + Manifest m; parse(blob, m); + std::cout << "wrote " << outpath << "\n" + << " " << (m.is_full() ? "full" : (m.codec_id == CODEC_DETOOLS_INPLACE ? "in-place delta" : "sequential delta")) + << " target=" << [&]{char x[9];std::snprintf(x,9,"%08X",m.target_id);return std::string(x);}() + << " v" << version_str(m.fw_version) << " hw=" << (m.hw_id_str().empty()?"?":m.hw_id_str()) + << " " << (m.is_signed()?"signed":"unsigned") << "\n" + << " image=" << m.image_size << "B payload=" << m.payload_size << "B blocks=" << m.block_count + << " total=" << blob.size() << "B\n"; + return 0; +} + +static int cmd_keygen(const Args& a) { + if (a.has("help")) { help_keygen(); return 0; } + uint8_t priv[32], pub[32]; + if (!ed25519_keygen(priv, pub)) { std::cerr << "keygen failed\n"; return 1; } + std::string out = a.get("out", a.get("out-priv")); + std::string ph = to_hex(priv, 32), kh = to_hex(pub, 32); + if (!out.empty()) { + std::string pp = ph + "\n", kp = kh + "\n"; + if (!write_file(out, (const uint8_t*)pp.data(), pp.size()) || + !write_file(out + ".pub", (const uint8_t*)kp.data(), kp.size())) { std::cerr << "write failed\n"; return 1; } + std::cout << "private -> " << out << "\npublic -> " << out << ".pub\n"; + } + std::cout << "pubkey: " << kh << "\n"; + return 0; +} + +static int cmd_serve(const Args& a) { + if (a.has("help")) { help_serve(); return 0; } + bool use_tcp = a.has("tcp"); + if (!a.has("dir") || (!a.has("serial") && !use_tcp)) { + std::cerr << "error: --dir and one of --serial / --tcp are required\n\n"; help_serve(); return 2; + } + bool recursive = !a.has("no-recursive"); + bool verbose = a.has("verbose"); + Folder folder; + size_t n = folder.scan(a.get("dir"), recursive, + [](const std::string& p, const std::string& why) { + std::cerr << " ! skip " << p << " : " << why << "\n"; + }); + std::cout << "motatool serve: " << n << " valid .mota in " << a.get("dir") + << (recursive ? " (recursive)" : "") << "\n"; + for (const auto& s : folder.all()) { + std::cout << " - " << fs::path(s.path).filename().string() + << " : mid=" << to_hex(s.m.merkle_root.data(), 4) + << " target=" << hex8(s.m.target_id) << " [" << target_label(s.m.target_id) << "]" + << " v" << version_str(s.m.fw_version) + << " " << (s.m.is_full() ? "full" : (s.m.codec_id == CODEC_DETOOLS_INPLACE ? "ipdelta" : "seqdelta")) + << " " << (s.m.is_signed() ? "signed" : "unsigned") + << " blocks=" << s.m.block_count << " size=" << s.bytes.size() << "\n"; + } + if (n == 0) std::cerr << " (nothing valid to serve)\n"; + + // Pick the transport: a serial port, or a TCP connection to the node's WiFi seeder port (host[:port], + // default port 5001). The node runs the seeder on a DEDICATED port, separate from its companion port, + // so serving over WiFi doesn't disturb a phone app connected to the companion. + SerialTransport st; + TcpTransport tt; + Transport* t = nullptr; + std::string target; + if (use_tcp) { + std::string hp = a.get("tcp"); + size_t colon = hp.rfind(':'); + std::string host = (colon == std::string::npos) ? hp : hp.substr(0, colon); + int port = (colon == std::string::npos) ? 5001 : std::atoi(hp.substr(colon + 1).c_str()); + std::string e = tt.open(host, port); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + t = &tt; target = host + ":" + std::to_string(port); + } else { + std::string e = st.open(a.get("serial"), std::atoi(a.get("baud", "115200").c_str())); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + t = &st; target = a.get("serial") + " @ " + a.get("baud", "115200"); + } + + std::signal(SIGINT, on_sigint); + // The CLI auto-enable only applies to the serial console; the TCP seeder port auto-enables relaying on + // the node side when this connection opens (and stops when it closes), so there's nothing to send. + bool enable = !use_tcp && !a.has("no-enable"); + if (enable) { usleep(500000); t->write_str("ota folder on\r\n"); std::cout << "sent `ota folder on`\n"; } + std::cout << "serving on " << target << " — Ctrl-C to stop\n"; + + SeederCore core(folder); + serve_loop(*t, core, verbose, + [](const std::string& l) { std::cout << " [dev] " << l << "\n"; }, &g_stop); + + if (enable) { t->write_str("ota folder off\r\n"); usleep(200000); } + std::cout << "\nbye\n"; + return 0; +} + +int main(int argc, char** argv) { + if (argc < 2) { help_top(); return 2; } + std::string cmd = argv[1]; + if (cmd == "help" || cmd == "-h" || cmd == "--help") { help_top(); return 0; } + + std::vector bools = {"force","verbose","no-recursive","no-enable"}; + Args a = parse_args(argc, argv, 2, bools); + if (cmd == "build") return cmd_build(a); + if (cmd == "verify") return cmd_verify(a); + if (cmd == "inspect") return cmd_inspect(a); + if (cmd == "serve") return cmd_serve(a); + if (cmd == "keygen") return cmd_keygen(a); + std::cerr << "unknown command: " << cmd << "\n\n"; + help_top(); + return 2; +} diff --git a/tools/motatool/src/mota.cpp b/tools/motatool/src/mota.cpp new file mode 100644 index 0000000000..87478bb15f --- /dev/null +++ b/tools/motatool/src/mota.cpp @@ -0,0 +1,413 @@ +#include "mota.h" + +#include "OtaTargets.h" // shared with the firmware (generated): target_id -> env name +#include "crypto.h" +#include "util.h" + +#include +#include +#include +#include + +namespace mota { + +// ---- merkle (mirror of src/helpers/ota/MerkleTree.cpp) -------------------------------------------- +static void merkle_leaf(uint8_t out[4], const uint8_t *block, size_t len) { + sha256_trunc(out, 4, block, len); +} +static void merkle_combine(uint8_t out[4], const uint8_t *left, const uint8_t *right) { + uint8_t buf[8]; + std::memcpy(buf, left, 4); + std::memcpy(buf + 4, right, 4); + sha256_trunc(out, 4, buf, 8); +} +// Root via binary-counter / Merkle-Mountain-Range with right-to-left bagging (must stay byte-identical to +// src/helpers/ota/MerkleTree.cpp; the native tests cross-check both against the Python reference). +// peaks[k] holds the root of a complete 2^k-leaf subtree; adding a leaf "carries" upward like incrementing +// a binary counter, then the leftover peaks are bagged right-to-left into the final root. +static void merkle_root(uint8_t out[4], const uint8_t *leaves, uint32_t count) { + if (count == 0) { + std::memset(out, 0, 4); + return; + } + if (count == 1) { + std::memcpy(out, leaves, 4); + return; + } + uint8_t peaks[32][4]; + bool valid[32] = { false }; + for (uint32_t i = 0; i < count; i++) { + uint8_t cur[4]; + std::memcpy(cur, leaves + (size_t)i * 4, 4); + uint32_t level = 0; + while (valid[level]) { // carry: combine with the pending peak at this level + merkle_combine(cur, peaks[level], cur); // peak is earlier (left), cur is right + valid[level] = false; + level++; + } + std::memcpy(peaks[level], cur, 4); + valid[level] = true; + } + // bag peaks right-to-left: acc starts at the lowest set level (rightmost peak) + int level = 0; + while (level < 32 && !valid[level]) + level++; + uint8_t acc[4]; + std::memcpy(acc, peaks[level], 4); + for (int l = level + 1; l < 32; l++) + if (valid[l]) merkle_combine(acc, peaks[l], acc); // higher peak is left, acc is right + std::memcpy(out, acc, 4); +} +// leaves[] over the payload (last block short, no padding) +static std::vector leaf_hashes(const uint8_t *payload, uint32_t size, uint32_t bs) { + uint32_t bc = (size + bs - 1) / bs; + std::vector leaves(bc * 4); + for (uint32_t i = 0; i < bc; i++) { + uint32_t off = i * bs, blen = (off + bs <= size) ? bs : (size - off); + merkle_leaf(leaves.data() + (size_t)i * 4, payload + off, blen); + } + return leaves; +} + +std::string Manifest::hw_id_str() const { + size_t n = 0; + while (n < hw_id.size() && hw_id[n]) + n++; + return std::string((const char *)hw_id.data(), n); +} + +// ---- parse ----------------------------------------------------------------------------------------- +std::string parse(const std::vector &b, Manifest &m) { + m = Manifest(); + if (b.size() < 8 + MOTA_MFL + 5) return "too small for a .mota"; + if (std::memcmp(b.data(), MOTA_MAGIC, 4) != 0) return "bad MAGIC (not a .mota)"; + uint32_t total = rd_u32(b.data() + 4); + if (total != b.size()) return "MOTA_TOTAL_SIZE != file length"; + if (std::memcmp(b.data() + b.size() - 5, MOTA_TRAILER, 5) != 0) return "bad TRAILER"; + const uint8_t *mf = b.data() + 8; // manifest start + m.format_ver = mf[M_OFF_FORMAT_VER]; + if (m.format_ver != FORMAT_VER) return "unsupported format_ver"; + m.flags = mf[M_OFF_FLAGS]; + m.hash_algo = mf[M_OFF_HASH_ALGO]; + m.target_id = rd_u32(mf + M_OFF_TARGET_ID); + m.fw_version = rd_u32(mf + M_OFF_FW_VERSION); + m.image_size = rd_u32(mf + M_OFF_IMAGE_SIZE); + m.payload_size = rd_u32(mf + M_OFF_PAYLOAD_SIZE); + m.block_size_log2 = mf[M_OFF_BLOCK_SIZE_LOG2]; + std::memcpy(m.merkle_root.data(), mf + M_OFF_MERKLE_ROOT, 4); + std::memcpy(m.image_hash.data(), mf + M_OFF_IMAGE_HASH, 32); + m.codec_id = mf[M_OFF_CODEC_ID]; + std::memcpy(m.hw_id.data(), mf + M_OFF_HW_ID, 32); + std::memcpy(m.base_hash.data(), mf + M_OFF_BASE_HASH, 8); + std::memcpy(m.signer.data(), mf + M_OFF_SIGNER, 32); + std::memcpy(m.signature.data(), mf + M_OFF_SIGNATURE, 64); + std::memcpy(m.approval.data(), mf + M_OFF_APPROVAL, 4); + if (m.block_size_log2 == 0 || m.block_size_log2 > 24 || m.payload_size == 0) + return "bad block_size/payload"; + m.block_count = (m.payload_size + m.block_size() - 1) / m.block_size(); + if (m.block_count == 0 || m.block_count > 0xFFFFu) return "block_count out of range"; + if (m.total_size() != b.size()) return "geometry (leaves+payload) != file length"; + return ""; +} + +// ---- verify ---------------------------------------------------------------------------------------- +std::vector verify(const std::vector &b) { + std::vector probs; + Manifest m; + std::string e = parse(b, m); + if (!e.empty()) { + probs.push_back(e); + return probs; + } // unparseable: a single, fatal problem + + const uint8_t *leaves = b.data() + m.leaves_off(); + const uint8_t *payload = b.data() + m.payload_off(); + + // recompute leaves[] from the payload -> catches payload corruption + std::vector calc = leaf_hashes(payload, m.payload_size, m.block_size()); + if (calc.size() != m.block_count * 4u || std::memcmp(calc.data(), leaves, calc.size()) != 0) + probs.push_back("leaves[] do not match the payload (corruption)"); + + uint8_t root[4]; + merkle_root(root, leaves, m.block_count); + if (std::memcmp(root, m.merkle_root.data(), 4) != 0) probs.push_back("merkle_root mismatch"); + + if (m.is_full()) { + auto h = mh32(payload, m.payload_size); // full: payload IS the image + if (std::memcmp(h.data(), m.image_hash.data(), 32) != 0) + probs.push_back("image_hash mismatch (full image)"); + } + // (delta image_hash needs the base image -> not checked at relay time; payload integrity is covered above) + + if (m.is_signed()) { + if (!ed25519_verify(m.signature.data(), b.data() + 8, MOTA_SIGNED_LEN, m.signer.data())) + probs.push_back("Ed25519 signature INVALID"); + } + // a distributed .mota must not be pre-approved + if (std::memcmp(m.approval.data(), APPROVAL_YES, 4) == 0) + probs.push_back("container is pre-approved (must be FF FF FF FF on the wire)"); + return probs; +} + +// ---- EndF ----------------------------------------------------------------------------------------- +bool has_endf(const std::vector &img) { + if (img.size() < ENDF_LEN) return false; + const uint8_t *t = img.data() + img.size() - ENDF_LEN; + if (std::memcmp(t, ENDF_MAGIC, 4) != 0) return false; + if (rd_u32(t + 4) != img.size() - ENDF_LEN) return false; + auto h = mh8(img.data(), img.size() - ENDF_LEN); + return std::memcmp(h.data(), t + 8, 8) == 0; +} + +FwIdent parse_endf_ident(const std::vector &img) { + FwIdent id; + if (!has_endf(img)) return id; + const uint8_t *t = img.data() + img.size() - ENDF_LEN; + id.fw_version = rd_u32(t + ENDF_OFF_FWVER); + id.target_id = rd_u32(t + ENDF_OFF_TARGET); + size_t n = 0; + while (n < HW_ID_LEN && t[ENDF_OFF_HWID + n]) + n++; + id.hw_id.assign((const char *)t + ENDF_OFF_HWID, n); + return id; +} + +std::vector ensure_endf(const std::vector &img, const FwIdent &id, + std::array &body_hash8) { + if (has_endf(img)) { // already trailed: keep its identity, read its hash + const uint8_t *t = img.data() + img.size() - ENDF_LEN; + std::memcpy(body_hash8.data(), t + 8, 8); + return img; + } + body_hash8 = mh8(img.data(), img.size()); + std::vector out = img; + out.insert(out.end(), ENDF_MAGIC, ENDF_MAGIC + 4); + uint8_t u[4]; + wr_u32(u, (uint32_t)img.size()); + out.insert(out.end(), u, u + 4); + out.insert(out.end(), body_hash8.begin(), body_hash8.end()); + wr_u32(u, id.fw_version); + out.insert(out.end(), u, u + 4); + wr_u32(u, id.target_id); + out.insert(out.end(), u, u + 4); + uint8_t hw[HW_ID_LEN] = { 0 }; + std::memcpy(hw, id.hw_id.data(), id.hw_id.size() < HW_ID_LEN ? id.hw_id.size() : HW_ID_LEN); + out.insert(out.end(), hw, hw + HW_ID_LEN); + return out; +} + +uint32_t target_id_for_env(const std::string &env) { + auto h = mh4((const uint8_t *)env.data(), env.size()); + return rd_u32(h.data()); +} + +std::string target_env_name(uint32_t target_id) { + // Exhaustive target_id -> env-name table, shared verbatim with the firmware and generated from the live + // PlatformIO config (every ENABLE_OTA env) by tools/mota/gen_targets.py. See OtaTargets.h. + const char *env = mesh::ota::ota_target_env_name(target_id); + return env ? env : ""; +} + +bool pack_version(const std::string &s, uint32_t &out) { + uint32_t parts[4] = { 0, 0, 0, 0 }; + int n = 0; + bool any = false; + std::stringstream ss(s); + std::string tok; + while (std::getline(ss, tok, '.') && n < 4) { + if (tok.empty()) return false; + for (char c : tok) + if (c < '0' || c > '9') return false; + parts[n++] = (uint32_t)std::strtoul(tok.c_str(), nullptr, 10); + any = true; + } + if (!any) return false; + out = ((parts[0] & 0xFF) << 24) | ((parts[1] & 0xFF) << 16) | ((parts[2] & 0xFF) << 8) | (parts[3] & 0xFF); + return true; +} + +// ---- build ---------------------------------------------------------------------------------------- +// detools delta encode: write base/new images to temp files, run the detools CLI, read the patch back. +static std::string detools_delta(const std::string &detools, uint8_t codec, + const std::vector &base_img, const std::vector &new_img, + uint32_t mem, uint32_t seg, std::vector &patch) { + char fb[] = "/tmp/motaXXXXXX", fn[] = "/tmp/motaXXXXXX", fp[] = "/tmp/motaXXXXXX"; + int a = mkstemp(fb), c = mkstemp(fn), d = mkstemp(fp); + if (a < 0 || c < 0 || d < 0) return "mkstemp failed"; + close(a); + close(c); + close(d); + std::string err; + if (!write_file(fb, base_img.data(), base_img.size()) || !write_file(fn, new_img.data(), new_img.size())) { + err = "writing temp images failed"; + } else { + std::vector argv; + if (codec == CODEC_DETOOLS_SEQUENTIAL) + argv = { detools, "create_patch", "-c", "crle", "-t", "sequential", fb, fn, fp }; + else + argv = { detools, + "create_patch_in_place", + "-c", + "crle", + "--memory-size", + std::to_string(mem), + "--segment-size", + std::to_string(seg), + fb, + fn, + fp }; + int rc = run_argv(argv, /*quiet=*/true); // hide detools' success chatter; errors keep stderr + if (rc == 127) + err = "could not run detools (install it / pass --detools )"; + else if (rc != 0) + err = "detools exited with code " + std::to_string(rc); + else if (!read_file(fp, patch) || patch.empty()) + err = "reading detools patch failed"; + } + unlink(fb); + unlink(fn); + unlink(fp); + return err; +} + +std::string detools_apply_seq(const std::string &detools, const std::vector &base_img, + const std::vector &patch, std::vector &out) { + char ff[] = "/tmp/motaXXXXXX", fpp[] = "/tmp/motaXXXXXX", ft[] = "/tmp/motaXXXXXX"; + int a = mkstemp(ff), c = mkstemp(fpp), d = mkstemp(ft); + if (a < 0 || c < 0 || d < 0) return "mkstemp failed"; + close(a); + close(c); + close(d); + std::string err; + if (!write_file(ff, base_img.data(), base_img.size()) || !write_file(fpp, patch.data(), patch.size())) { + err = "writing temp files failed"; + } else { + int rc = run_argv({ detools, "apply_patch", ff, fpp, ft }, /*quiet=*/true); // + if (rc == 127) + err = "could not run detools (install it / pass --detools )"; + else if (rc != 0) + err = "detools apply_patch exited with code " + std::to_string(rc); + else if (!read_file(ft, out) || out.empty()) + err = "reading reconstructed image failed"; + } + unlink(ff); + unlink(fpp); + unlink(ft); + return err; +} + +std::string build(const BuildOpts &o, std::vector &out, std::string &suggested_name) { + bool is_delta = !o.base.empty(); + + // resolve identity: explicit flags override the firmware's self-describing EndF + FwIdent fid = parse_endf_ident(o.fw); + uint32_t target = o.have_target ? o.target_id : fid.target_id; + uint32_t fwver = o.have_fwver ? o.fw_version : fid.fw_version; + std::string hw = !o.hw_id.empty() ? o.hw_id : fid.hw_id; + FwIdent ident{ fwver, target, hw }; + + std::array new_bh{}, base_bh{}; + std::vector new_img = ensure_endf(o.fw, ident, new_bh); + + uint8_t codec = o.codec; + std::vector payload; + std::array base_hash{}; + + if (!is_delta) { + codec = CODEC_FULL; + payload = new_img; // full: payload IS the image + } else { + if (codec == CODEC_FULL) return "a base image was given but --codec is full"; + FwIdent base_id = parse_endf_ident(o.base); + std::vector base_img = ensure_endf(o.base, base_id, base_bh); + base_hash = base_bh; + // cross-hardware delta guard (read from EndF identity, not filenames) + if (!o.force && base_id.any() && fid.any()) { + bool hw_ok = base_id.hw_id.empty() || fid.hw_id.empty() || base_id.hw_id == fid.hw_id; + bool tgt_ok = !base_id.target_id || !fid.target_id || base_id.target_id == fid.target_id; + if (!hw_ok || !tgt_ok) + return "base/target firmware identity differ (hw '" + base_id.hw_id + "' vs '" + fid.hw_id + + "') — refusing cross-hardware delta (use --force to override)"; + } + std::string e = + detools_delta(o.detools, codec, base_img, new_img, o.inplace_memory, o.inplace_segment, payload); + if (!e.empty()) return e; + } + + uint32_t bs = o.block_size, image_size = (uint32_t)new_img.size(); + std::vector leaves = leaf_hashes(payload.data(), (uint32_t)payload.size(), bs); + uint32_t bc = (uint32_t)leaves.size() / 4; + if (bc == 0 || bc > 0xFFFFu) return "payload yields an invalid block count"; + uint8_t root[4]; + merkle_root(root, leaves.data(), bc); + auto image_hash = mh32(new_img.data(), new_img.size()); + + bool signed_ = !o.sign_priv.empty(); + uint8_t flags = (is_delta ? 0 : MFLAG_FULL) | (signed_ ? MFLAG_SIGNED : 0); + + // assemble the fixed 197-byte manifest-minus-leaves + std::vector mf(MOTA_MFL, 0); + mf[M_OFF_FORMAT_VER] = FORMAT_VER; + mf[M_OFF_FLAGS] = flags; + mf[M_OFF_HASH_ALGO] = HASH_ALGO_SHA256; + wr_u32(mf.data() + M_OFF_TARGET_ID, target); + wr_u32(mf.data() + M_OFF_FW_VERSION, fwver); + wr_u32(mf.data() + M_OFF_IMAGE_SIZE, image_size); + wr_u32(mf.data() + M_OFF_PAYLOAD_SIZE, (uint32_t)payload.size()); + // block_size_log2 + { + uint32_t v = bs, l = 0; + while (v > 1) { + v >>= 1; + l++; + } + mf[M_OFF_BLOCK_SIZE_LOG2] = (uint8_t)l; + } + std::memcpy(mf.data() + M_OFF_MERKLE_ROOT, root, 4); + std::memcpy(mf.data() + M_OFF_IMAGE_HASH, image_hash.data(), 32); + mf[M_OFF_CODEC_ID] = codec; + std::memcpy(mf.data() + M_OFF_HW_ID, hw.data(), hw.size() < HW_ID_LEN ? hw.size() : HW_ID_LEN); + if (is_delta) std::memcpy(mf.data() + M_OFF_BASE_HASH, base_hash.data(), 8); // zero for full + if (signed_) { + uint8_t pub[32]; + if (o.sign_priv.size() != 32) return "signing key must be a 32-byte raw private seed"; + if (!ed25519_pub_from_priv(pub, o.sign_priv.data())) return "bad signing key"; + std::memcpy(mf.data() + M_OFF_SIGNER, pub, 32); + uint8_t sig[64]; + if (!ed25519_sign(sig, mf.data(), MOTA_SIGNED_LEN, o.sign_priv.data())) return "signing failed"; + std::memcpy(mf.data() + M_OFF_SIGNATURE, sig, 64); + } + std::memcpy(mf.data() + M_OFF_APPROVAL, APPROVAL_NOT, 4); + + // container = MAGIC(4) total(4) manifest leaves[] payload trailer(5) + uint32_t total = 8 + MOTA_MFL + bc * 4 + (uint32_t)payload.size() + 5; + // nRF52 in-place: the staged container sits below the apply workspace; if it's too big it overruns the + // workspace and the bootloader apply fails (DETOOLS_IO_FAILED). Warn so it's caught before shipping. + if (is_delta && codec == CODEC_DETOOLS_INPLACE && total > NRF52_MAX_INPLACE_MOTA) + std::fprintf(stderr, "warning: in-place delta is %u B, exceeding the nRF52 staging room (%u B) — it will " + "NOT apply on the device. Shrink the delta (smaller change) or use a smaller image.\n", + total, NRF52_MAX_INPLACE_MOTA); + out.clear(); + out.reserve(total); + out.insert(out.end(), MOTA_MAGIC, MOTA_MAGIC + 4); + uint8_t u[4]; + wr_u32(u, total); + out.insert(out.end(), u, u + 4); + out.insert(out.end(), mf.begin(), mf.end()); + out.insert(out.end(), leaves.begin(), leaves.end()); + out.insert(out.end(), payload.begin(), payload.end()); + out.insert(out.end(), MOTA_TRAILER, MOTA_TRAILER + 5); + + // suggested name: __v__.mota (descriptive + unique, one folder) + const char *kind = !is_delta ? "full" : (codec == CODEC_DETOOLS_INPLACE ? "ipdelta" : "seqdelta"); + char tgt[9]; + std::snprintf(tgt, sizeof(tgt), "%08X", target); + char vbuf[24]; + std::snprintf(vbuf, sizeof(vbuf), "%u.%u.%u", (fwver >> 24) & 0xFF, (fwver >> 16) & 0xFF, + (fwver >> 8) & 0xFF); + suggested_name = (hw.empty() ? std::string("fw") : hw) + "_" + tgt + "_v" + vbuf + "_" + kind + "_" + + to_hex(root, 4) + ".mota"; + return ""; +} + +} // namespace mota diff --git a/tools/motatool/src/mota.h b/tools/motatool/src/mota.h new file mode 100644 index 0000000000..e3024027c4 --- /dev/null +++ b/tools/motatool/src/mota.h @@ -0,0 +1,83 @@ +// `.mota` container: parse, integrity-verify, and build (full / detools-delta). EndF identity helpers. +// Mirrors tools/mota/motalib.py and src/helpers/ota/MotaContainer.cpp (the single source of truth). +#pragma once +#include "mota_format.h" +#include +#include +#include + +namespace mota { + +struct FwIdent { + uint32_t fw_version = 0; + uint32_t target_id = 0; + std::string hw_id; // NUL-trimmed + bool any() const { return fw_version || target_id || !hw_id.empty(); } +}; + +struct Manifest { + uint8_t format_ver = 0, flags = 0, hash_algo = 0, codec_id = 0, block_size_log2 = 0; + uint32_t target_id = 0, fw_version = 0, image_size = 0, payload_size = 0, block_count = 0; + std::array merkle_root{}; + std::array image_hash{}; + std::array hw_id{}; // raw 32 bytes (NUL-padded) + std::array base_hash{}; + std::array signer{}; + std::array signature{}; + std::array approval{}; + + bool is_full() const { return flags & MFLAG_FULL; } + bool is_signed() const { return flags & MFLAG_SIGNED; } + uint32_t block_size() const { return 1u << block_size_log2; } + uint32_t leaves_off() const { return 8 + MOTA_MFL; } + uint32_t payload_off() const { return leaves_off() + block_count * 4; } + uint32_t total_size() const { return payload_off() + payload_size + 5; } + std::string hw_id_str() const; +}; + +// Parse + validate framing and the fixed layout. Returns "" on success (fills `out`), else an error. +std::string parse(const std::vector& blob, Manifest& out); + +// Content-integrity check: recompute leaves[] from the payload vs merkle_root, the merkle root, the +// FULL image_hash, and (if signed) the Ed25519 signature against the embedded signer key. Returns a list +// of problems (empty => valid). A delta's image_hash is not checked here (it needs the base image). +std::vector verify(const std::vector& blob); + +// ---- EndF identity (fixed 56-byte trailer) ---- +bool has_endf(const std::vector& image); +FwIdent parse_endf_ident(const std::vector& image); // zeros if no EndF +// Append a 56-byte EndF (with identity) if absent; returns the image and sets body_hash8. Idempotent. +std::vector ensure_endf(const std::vector& image, const FwIdent& id, + std::array& body_hash8); + +uint32_t target_id_for_env(const std::string& env); // sha2-256:4(env) as LE uint32 +bool pack_version(const std::string& s, uint32_t& out); // "1.16.0[.pre]" -> packed uint32 + +// Reverse-lookup a target_id to its PlatformIO env name from a static table of known OTA-capable envs +// (target_id = sha2-256:4(env_name)). Returns "" if not in the table. +std::string target_env_name(uint32_t target_id); + +// ---- build ---- +struct BuildOpts { + std::vector fw; // NEW firmware (raw or already-EndF'd) + std::vector base; // base image for a delta (empty => full) + uint8_t codec = CODEC_FULL; // CODEC_FULL / _SEQUENTIAL / _INPLACE + bool have_target = false; uint32_t target_id = 0; // overrides EndF + bool have_fwver = false; uint32_t fw_version = 0; // overrides EndF + std::string hw_id; // override; else from EndF + std::vector sign_priv; // 32-byte raw private seed (empty => unsigned) + uint32_t block_size = DEFAULT_BLOCK_SIZE; + uint32_t inplace_memory = NRF52_INPLACE_MEMORY, inplace_segment = NRF52_INPLACE_SEGMENT; + std::string detools = "detools"; // detools CLI path (delta encoding only) + bool force = false; // override the cross-hardware delta guard +}; +// Returns "" + fills `out` and a suggested file name on success; else an error string. +std::string build(const BuildOpts& o, std::vector& out, std::string& suggested_name); + +// Apply a SEQUENTIAL delta `patch` to `base_img` via the detools CLI -> reconstructed image in `out`. +// Used by `verify --base` to confirm a delta actually rebuilds the expected image (matches mota.py: +// in-place deltas aren't apply-checked here — the bootloader host-harness covers that path). +std::string detools_apply_seq(const std::string& detools, const std::vector& base_img, + const std::vector& patch, std::vector& out); + +} // namespace mota diff --git a/tools/motatool/src/mota_format.h b/tools/motatool/src/mota_format.h new file mode 100644 index 0000000000..d26ac64e9e --- /dev/null +++ b/tools/motatool/src/mota_format.h @@ -0,0 +1,100 @@ +// MeshCore `.mota` on-wire constants and fixed layout — a C++-friendly mirror of +// src/helpers/ota/OtaFormat.h. Keep byte-identical with that file and docs/ota_protocol.md. +// +// The format is FIXED-LAYOUT: every manifest field is at a constant offset and always present +// (base_hash/signer_pubkey/signature are zero-filled when not applicable). Only leaves[] varies. +#pragma once +#include +#include + +namespace mota { + +// container framing +static constexpr uint8_t MOTA_MAGIC[4] = {'m','O','T','A'}; +static constexpr uint8_t MOTA_TRAILER[5] = {'v','k','4','9','6'}; +static constexpr uint8_t ENDF_MAGIC[4] = {'E','n','d','F'}; + +static constexpr uint8_t HASH_ALGO_SHA256 = 0x12; +static constexpr uint8_t FORMAT_VER = 0x02; + +static constexpr uint8_t MFLAG_FULL = 0x01; +static constexpr uint8_t MFLAG_SIGNED = 0x02; + +static constexpr uint8_t CODEC_FULL = 0; +static constexpr uint8_t CODEC_DETOOLS_SEQUENTIAL = 1; // ESP32 A/B +static constexpr uint8_t CODEC_DETOOLS_INPLACE = 2; // nRF52 single-slot + +// hash truncations (sha2-256:N = first N bytes of the SHA-256 digest) +static constexpr size_t MH4 = 4, MH8 = 8, MH32 = 32; + +static constexpr uint8_t APPROVAL_NOT[4] = {0xFF,0xFF,0xFF,0xFF}; +static constexpr uint8_t APPROVAL_YES[4] = {'A','P','R','V'}; + +// EndF trailer (fixed 56 bytes): marker(4) body_len(4) body_hash8(8) fw_version(4) target_id(4) hw_id(32) +static constexpr uint32_t ENDF_LEN = 56; +static constexpr uint32_t ENDF_OFF_FWVER = 16; +static constexpr uint32_t ENDF_OFF_TARGET = 20; +static constexpr uint32_t ENDF_OFF_HWID = 24; + +static constexpr uint8_t HW_ID_LEN = 32; + +// manifest fixed offsets (within the manifest, i.e. container offset = 8 + these) +static constexpr uint32_t M_OFF_FORMAT_VER = 0; +static constexpr uint32_t M_OFF_FLAGS = 1; +static constexpr uint32_t M_OFF_HASH_ALGO = 2; +static constexpr uint32_t M_OFF_TARGET_ID = 3; +static constexpr uint32_t M_OFF_FW_VERSION = 7; +static constexpr uint32_t M_OFF_IMAGE_SIZE = 11; +static constexpr uint32_t M_OFF_PAYLOAD_SIZE = 15; +static constexpr uint32_t M_OFF_BLOCK_SIZE_LOG2 = 19; +static constexpr uint32_t M_OFF_MERKLE_ROOT = 20; // 4 +static constexpr uint32_t M_OFF_IMAGE_HASH = 24; // 32 +static constexpr uint32_t M_OFF_CODEC_ID = 56; // 1 +static constexpr uint32_t M_OFF_HW_ID = 57; // 32 +static constexpr uint32_t M_OFF_BASE_HASH = 89; // 8 (zero if FULL) +static constexpr uint32_t M_OFF_SIGNER = 97; // 32 (zero if unsigned) +static constexpr uint32_t M_OFF_SIGNATURE = 129; // 64 (zero if unsigned) +static constexpr uint32_t M_OFF_APPROVAL = 193; // 4 +static constexpr uint32_t MOTA_MFL = 197; // manifest-minus-leaves length (constant) +static constexpr uint32_t MOTA_SIGNED_LEN = 129; // signature covers manifest[0, 129) + +static constexpr uint32_t DEFAULT_BLOCK_SIZE = 1024; +static constexpr uint8_t DEFAULT_BLOCK_SIZE_LOG2 = 10; + +// nRF52 in-place apply workspace — MUST match src/helpers/ota/OtaFlashLayout_nrf52.h +// (MOTA_NRF52_INPLACE_MEMORY). It is NOT the full [APP_BASE, FS_START) span: the staged .mota itself sits +// just below FS_START, so the bootloader's workspace ends at the staged container (ws_hi = mota_addr), not +// at FS_START. The workspace is [APP_BASE, 0xBE000) = 0x98000, leaving 0xBE000..0xD4000 (~88 KB) for the +// staged delta. A patch built with a larger memory_size overruns the workspace at apply -> DETOOLS_IO_FAILED +// (the apply silently fails and the device just reboots). Reproduced + verified by the bootloader apply +// simulation (Adafruit_nRF52_Bootloader_OTAFIX/test/apply_sim). +static constexpr uint32_t NRF52_INPLACE_MEMORY = 0x00098000u; // 608 KB: [APP_BASE, 0xBE000) +static constexpr uint32_t NRF52_INPLACE_SEGMENT = 4096; +// The staged .mota sits in [APP_BASE+memory, FS_START); a larger container would push its start below the +// workspace end and break the apply the same way. Warn (motatool) / fail (bootloader) past this. +static constexpr uint32_t NRF52_FLASH_SPAN = 0x000D4000u - 0x00026000u; // 0xAE000 +static constexpr uint32_t NRF52_MAX_INPLACE_MOTA = NRF52_FLASH_SPAN - NRF52_INPLACE_MEMORY; // 0x16000 (~90 KB) +static_assert(NRF52_INPLACE_MEMORY < NRF52_FLASH_SPAN, "in-place workspace must leave room below FS_START for the staged .mota"); + +// ---- mota-seeder transport protocol (mirror of src/helpers/ota/MotaSeederProto.h) ---- +// Request (client -> server): 'M' 'S' op(1) args... xsum(1 = XOR of op+args) +// Response (server -> client): 'm' 's' op(1) status(1) payload... xsum(1 = XOR of all prior) +static constexpr uint8_t MS_REQ_MAGIC0 = 'M', MS_REQ_MAGIC1 = 'S'; +static constexpr uint8_t MS_RSP_MAGIC0 = 'm', MS_RSP_MAGIC1 = 's'; +static constexpr uint8_t MS_OP_COUNT = 0x01; // -> count(1) +static constexpr uint8_t MS_OP_DESCRIBE = 0x02; // idx(1) -> MotaDesc(38) +static constexpr uint8_t MS_OP_READ = 0x03; // idx(1) off(4) len(2) -> bytes +static constexpr uint8_t MS_STATUS_OK = 0x00; +static constexpr uint8_t MS_STATUS_ERR = 0x01; +static constexpr uint16_t MOTA_DESC_WIRE = 38; +// MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1) total_size(4) +// leaves_off(4) block_count(4) payload_off(4) payload_size(4) [+2 reserved] + +inline uint32_t rd_u32(const uint8_t* p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); +} +inline void wr_u32(uint8_t* p, uint32_t v) { + p[0] = (uint8_t)v; p[1] = (uint8_t)(v >> 8); p[2] = (uint8_t)(v >> 16); p[3] = (uint8_t)(v >> 24); +} + +} // namespace mota diff --git a/tools/motatool/src/serve.cpp b/tools/motatool/src/serve.cpp new file mode 100644 index 0000000000..ef89ed9f4c --- /dev/null +++ b/tools/motatool/src/serve.cpp @@ -0,0 +1,252 @@ +#include "serve.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "util.h" + +namespace fs = std::filesystem; + +namespace mota { + +// ---- Folder: recursive scan + per-file validation ------------------------------------------------- +size_t Folder::scan(const std::string& dir, bool recursive, + const std::function& warn) { + motas_.clear(); + std::error_code ec; + auto consider = [&](const fs::path& p) { + if (p.extension() != ".mota") return; // skip non-.mota files + ServedMota sm; sm.path = p.string(); + if (!read_file(sm.path, sm.bytes)) { warn(sm.path, "cannot read"); return; } + auto probs = verify(sm.bytes); // merkle + image_hash + signature + if (!probs.empty()) { + std::string why = probs[0]; + for (size_t i = 1; i < probs.size(); i++) why += "; " + probs[i]; + warn(sm.path, why); // warn + exclude (don't sink the rest) + return; + } + parse(sm.bytes, sm.m); // already validated above + motas_.push_back(std::move(sm)); + }; + if (recursive) { + for (auto it = fs::recursive_directory_iterator(dir, ec); + !ec && it != fs::recursive_directory_iterator(); it.increment(ec)) + if (it->is_regular_file(ec)) consider(it->path()); + } else { + for (auto it = fs::directory_iterator(dir, ec); + !ec && it != fs::directory_iterator(); it.increment(ec)) + if (it->is_regular_file(ec)) consider(it->path()); + } + // stable, deterministic catalog order (indices are how the device addresses motas) + std::sort(motas_.begin(), motas_.end(), + [](const ServedMota& a, const ServedMota& b) { return a.path < b.path; }); + return motas_.size(); +} + +// ---- SeederCore (transport-agnostic) -------------------------------------------------------------- +std::array SeederCore::describe(const ServedMota& s) { + std::array w{}; + std::memcpy(w.data(), s.m.merkle_root.data(), 4); // mid + wr_u32(w.data() + 4, s.m.target_id); + wr_u32(w.data() + 8, s.m.fw_version); + w[12] = s.m.codec_id; + w[13] = s.m.flags; + wr_u32(w.data() + 14, (uint32_t)s.bytes.size()); // total_size + wr_u32(w.data() + 18, s.m.leaves_off()); + wr_u32(w.data() + 22, s.m.block_count); + wr_u32(w.data() + 26, s.m.payload_off()); + wr_u32(w.data() + 30, s.m.payload_size); + // [34..38) reserved 0 + return w; +} + +bool SeederCore::handle(uint8_t op, const uint8_t* args, size_t arglen, + uint8_t& status, std::vector& payload) const { + payload.clear(); + status = MS_STATUS_OK; + if (op == MS_OP_COUNT) { + payload.push_back((uint8_t)(folder_.count() > 255 ? 255 : folder_.count())); + return true; + } + if (op == MS_OP_DESCRIBE) { + if (arglen < 1) return false; + const ServedMota* s = folder_.at(args[0]); + if (!s) { status = MS_STATUS_ERR; return true; } + auto w = describe(*s); + payload.assign(w.begin(), w.end()); + return true; + } + if (op == MS_OP_READ) { + if (arglen < 7) return false; + const ServedMota* s = folder_.at(args[0]); + uint32_t off = rd_u32(args + 1); + uint16_t len = (uint16_t)(args[5] | (args[6] << 8)); + if (!s || (uint64_t)off + len > s->bytes.size()) { status = MS_STATUS_ERR; return true; } + payload.assign(s->bytes.begin() + off, s->bytes.begin() + off + len); + return true; + } + return false; // unknown op -> ignore (device retries) +} + +// ---- SerialTransport ------------------------------------------------------------------------------ +static speed_t baud_const(int b) { + switch (b) { + case 9600: return B9600; case 19200: return B19200; case 38400: return B38400; + case 57600: return B57600; case 115200: return B115200; case 230400: return B230400; + case 460800: return B460800; case 921600: return B921600; default: return B115200; + } +} + +std::string SerialTransport::open(const std::string& dev, int baud) { + fd_ = ::open(dev.c_str(), O_RDWR | O_NOCTTY | O_NONBLOCK); + if (fd_ < 0) return "cannot open serial device: " + dev; + termios t{}; + if (tcgetattr(fd_, &t) != 0) { ::close(fd_); fd_ = -1; return "tcgetattr failed"; } + cfmakeraw(&t); + speed_t s = baud_const(baud); + cfsetispeed(&t, s); cfsetospeed(&t, s); + t.c_cflag |= (CLOCAL | CREAD); + t.c_cflag &= ~CRTSCTS; // no flow control (matches the daemon) + t.c_cc[VMIN] = 0; t.c_cc[VTIME] = 0; + if (tcsetattr(fd_, TCSANOW, &t) != 0) { ::close(fd_); fd_ = -1; return "tcsetattr failed"; } + return ""; +} + +SerialTransport::~SerialTransport() { if (fd_ >= 0) ::close(fd_); } + +int SerialTransport::read_byte(int timeout_ms) { + if (fd_ < 0) return -1; + fd_set rs; FD_ZERO(&rs); FD_SET(fd_, &rs); + timeval tv{ timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; + int r = select(fd_ + 1, &rs, nullptr, nullptr, &tv); + if (r <= 0) return -1; + uint8_t b; + ssize_t n = ::read(fd_, &b, 1); + return n == 1 ? (int)b : -1; +} + +bool SerialTransport::write(const uint8_t* p, size_t n) { + if (fd_ < 0) return false; + size_t off = 0; + while (off < n) { + ssize_t w = ::write(fd_, p + off, n - off); + if (w < 0) return false; + off += (size_t)w; + } + return true; +} + +// ---- TcpTransport --------------------------------------------------------------------------------- +std::string TcpTransport::open(const std::string& host, int port) { + addrinfo hints{}; hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; + addrinfo* res = nullptr; + std::string portstr = std::to_string(port); + if (getaddrinfo(host.c_str(), portstr.c_str(), &hints, &res) != 0 || !res) + return "cannot resolve host: " + host; + std::string err = "cannot connect to " + host + ":" + portstr; + for (addrinfo* p = res; p; p = p->ai_next) { + int fd = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol); + if (fd < 0) continue; + if (::connect(fd, p->ai_addr, p->ai_addrlen) == 0) { fd_ = fd; err.clear(); break; } + ::close(fd); + } + freeaddrinfo(res); + return err; +} + +TcpTransport::~TcpTransport() { if (fd_ >= 0) ::close(fd_); } + +int TcpTransport::read_byte(int timeout_ms) { + if (fd_ < 0) return -1; + fd_set rs; FD_ZERO(&rs); FD_SET(fd_, &rs); + timeval tv{ timeout_ms / 1000, (timeout_ms % 1000) * 1000 }; + if (select(fd_ + 1, &rs, nullptr, nullptr, &tv) <= 0) return -1; + uint8_t b; + ssize_t n = ::recv(fd_, &b, 1, 0); + return n == 1 ? (int)b : -1; // 0 == peer closed -> treated as timeout/closed +} + +bool TcpTransport::write(const uint8_t* p, size_t n) { + if (fd_ < 0) return false; + size_t off = 0; + while (off < n) { + ssize_t w = ::send(fd_, p + off, n - off, 0); + if (w < 0) return false; + off += (size_t)w; + } + return true; +} + +// ---- seeder framing loop -------------------------------------------------------------------------- +static uint8_t xor_bytes(const uint8_t* p, size_t n, uint8_t seed = 0) { + uint8_t x = seed; for (size_t i = 0; i < n; i++) x ^= p[i]; return x; +} + +static bool read_exact(Transport& t, uint8_t* buf, size_t n) { + for (size_t i = 0; i < n; i++) { + int b = t.read_byte(500); + if (b < 0) return false; + buf[i] = (uint8_t)b; + } + return true; +} + +static void send_response(Transport& t, uint8_t op, uint8_t status, const std::vector& payload) { + std::vector frame; + frame.reserve(4 + payload.size() + 1); + frame.push_back(MS_RSP_MAGIC0); frame.push_back(MS_RSP_MAGIC1); + frame.push_back(op); frame.push_back(status); + frame.insert(frame.end(), payload.begin(), payload.end()); + frame.push_back(xor_bytes(frame.data(), frame.size())); // xsum over all prior bytes (incl. magic) + t.write(frame.data(), frame.size()); +} + +void serve_loop(Transport& t, const SeederCore& core, bool verbose, + const std::function& devline, + const volatile bool* stop) { + std::string line; + int prev = -1; + auto flush_line = [&]() { + while (!line.empty() && (line.back() == '\r' || line.back() == '\n')) line.pop_back(); + if (!line.empty() && devline) devline(line); + line.clear(); + }; + while (!stop || !*stop) { + int b = t.read_byte(200); + if (b < 0) continue; + if (prev == MS_REQ_MAGIC0 && b == MS_REQ_MAGIC1) { // 'M''S' -> a request follows + prev = -1; + uint8_t op; + if (!read_exact(t, &op, 1)) continue; + size_t arglen = (op == MS_OP_DESCRIBE) ? 1 : (op == MS_OP_READ ? 7 : (op == MS_OP_COUNT ? 0 : SIZE_MAX)); + if (arglen == SIZE_MAX) continue; // unknown op + uint8_t args[7]; + if (arglen && !read_exact(t, args, arglen)) continue; + uint8_t xs; + if (!read_exact(t, &xs, 1)) continue; + if (xs != xor_bytes(args, arglen, op)) continue; // bad checksum -> ignore; device retries + uint8_t status; std::vector payload; + if (!core.handle(op, args, arglen, status, payload)) continue; + send_response(t, op, status, payload); + if (verbose) { + if (op == MS_OP_COUNT) devline("COUNT -> " + std::to_string(payload.empty() ? 0 : payload[0])); + else if (op == MS_OP_DESCRIBE) devline("DESCRIBE " + std::to_string(args[0]) + (status ? " ERR" : " OK")); + else if (op == MS_OP_READ) devline("READ " + std::to_string(args[0]) + " @" + + std::to_string(rd_u32(args + 1)) + (status ? " ERR" : " OK")); + } + continue; + } + if (prev >= 0) { // confirmed device text (not a frame start) + line.push_back((char)prev); + if (prev == '\n') flush_line(); + if (line.size() > 512) flush_line(); // guard runaway lines + } + prev = b; + } +} + +} // namespace mota diff --git a/tools/motatool/src/serve.h b/tools/motatool/src/serve.h new file mode 100644 index 0000000000..5177172460 --- /dev/null +++ b/tools/motatool/src/serve.h @@ -0,0 +1,81 @@ +// Serve a folder of .mota to a MeshCore node. Split into a transport-AGNOSTIC protocol core (reusable +// over USB-serial today and BLE/GATT in the future) and a serial byte-stream framing layer. +#pragma once +#include "mota.h" +#include "mota_format.h" +#include +#include +#include +#include + +namespace mota { + +struct ServedMota { + std::string path; + std::vector bytes; + Manifest m; +}; + +// Recursively collect every *.mota under `dir`, validate each, keep only the valid ones. Corrupt/invalid +// files are reported through `warn(path, reason)` and excluded — one bad file never sinks the rest. +class Folder { +public: + size_t scan(const std::string& dir, bool recursive, + const std::function& warn); + size_t count() const { return motas_.size(); } + const ServedMota* at(size_t i) const { return i < motas_.size() ? &motas_[i] : nullptr; } + const std::vector& all() const { return motas_; } +private: + std::vector motas_; +}; + +// Transport-agnostic seeder: turns a (op, args) request into a (status, payload) response. The BLE path +// would call this directly from a characteristic-write handler and notify the reply — no framing needed. +class SeederCore { +public: + explicit SeederCore(const Folder& f) : folder_(f) {} + bool handle(uint8_t op, const uint8_t* args, size_t arglen, + uint8_t& status, std::vector& payload) const; + static std::array describe(const ServedMota& s); +private: + const Folder& folder_; +}; + +// A bidirectional byte link (the serial seeder needs this; BLE would reuse SeederCore directly). +struct Transport { + virtual ~Transport() {} + virtual int read_byte(int timeout_ms) = 0; // a byte 0..255, or -1 on timeout/closed + virtual bool write(const uint8_t* p, size_t n) = 0; + bool write_str(const std::string& s) { return write((const uint8_t*)s.data(), s.size()); } +}; + +class SerialTransport : public Transport { +public: + std::string open(const std::string& dev, int baud); // "" on success + ~SerialTransport() override; + int read_byte(int timeout_ms) override; + bool write(const uint8_t* p, size_t n) override; +private: + int fd_ = -1; +}; + +// TCP client transport: connect to a node's WiFi seeder port (a dedicated port, NOT the companion port), +// then run the same seeder framing over the socket. Lets `serve` feed a WiFi node the same way as serial. +class TcpTransport : public Transport { +public: + std::string open(const std::string& host, int port); // "" on success + ~TcpTransport() override; + int read_byte(int timeout_ms) override; + bool write(const uint8_t* p, size_t n) override; +private: + int fd_ = -1; +}; + +// Seeder framing loop (transport-agnostic): resync on 'M''S', verify the request checksum, dispatch to +// `core`, frame the reply. Device text/log lines sharing the wire are surfaced via `devline` (serial +// only; the TCP seeder port carries no log text). Runs until *stop becomes true. +void serve_loop(Transport& t, const SeederCore& core, bool verbose, + const std::function& devline, + const volatile bool* stop); + +} // namespace mota diff --git a/tools/motatool/src/util.h b/tools/motatool/src/util.h new file mode 100644 index 0000000000..9e5e51af48 --- /dev/null +++ b/tools/motatool/src/util.h @@ -0,0 +1,77 @@ +// Small host-side helpers: file I/O, subprocess exec (no shell), hex. Header-only. +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include + +namespace mota { + +inline bool read_file(const std::string& path, std::vector& out) { + std::ifstream f(path, std::ios::binary); + if (!f) return false; + out.assign(std::istreambuf_iterator(f), std::istreambuf_iterator()); + return (bool)f || f.eof(); +} + +inline bool write_file(const std::string& path, const uint8_t* data, size_t len) { + std::ofstream f(path, std::ios::binary | std::ios::trunc); + if (!f) return false; + f.write((const char*)data, (std::streamsize)len); + return (bool)f; +} + +// Run argv[0] with argv (no shell, so no quoting pitfalls). Returns the child exit code, or -1 to spawn. +// `quiet` redirects the child's stdout to /dev/null (its chatter), keeping stderr so errors still show. +inline int run_argv(const std::vector& argv, bool quiet = false) { + if (argv.empty()) return -1; + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + if (quiet) { + int n = ::open("/dev/null", O_WRONLY); + if (n >= 0) { dup2(n, 1); if (n > 2) ::close(n); } + } + std::vector a; + for (auto& s : argv) a.push_back(const_cast(s.c_str())); + a.push_back(nullptr); + execvp(a[0], a.data()); + _exit(127); // exec failed + } + int st = 0; + if (waitpid(pid, &st, 0) < 0) return -1; + return WIFEXITED(st) ? WEXITSTATUS(st) : -1; +} + +inline std::string to_hex(const uint8_t* p, size_t n) { + static const char* H = "0123456789ABCDEF"; + std::string s; s.reserve(n * 2); + for (size_t i = 0; i < n; i++) { s.push_back(H[p[i] >> 4]); s.push_back(H[p[i] & 0xF]); } + return s; +} + +// Parse a hex string (optionally 0x-prefixed) into bytes. Returns false on odd length / bad char. +inline bool from_hex(const std::string& in, std::vector& out) { + std::string s = in; + if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s = s.substr(2); + if (s.size() % 2) return false; + out.clear(); + auto nib = [](char c) -> int { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; + }; + for (size_t i = 0; i < s.size(); i += 2) { + int hi = nib(s[i]), lo = nib(s[i + 1]); + if (hi < 0 || lo < 0) return false; + out.push_back((uint8_t)((hi << 4) | lo)); + } + return true; +} + +} // namespace mota diff --git a/tools/motatool/tests/test_motatool.cpp b/tools/motatool/tests/test_motatool.cpp new file mode 100644 index 0000000000..87b7bd9701 --- /dev/null +++ b/tools/motatool/tests/test_motatool.cpp @@ -0,0 +1,302 @@ +// Unit tests for motatool's core logic (no external framework — a tiny built-in harness keeps the +// project self-contained). Run: ./build/motatool_tests or ctest --test-dir build --output-on-failure +#include "mota.h" +#include "crypto.h" +#include "serve.h" +#include "input.h" +#include "util.h" +#include "mota_format.h" +#include +#include +#include +#include + +namespace fs = std::filesystem; +using namespace mota; + +static int g_checks = 0, g_fail = 0; +static const char* g_test = ""; +#define CHECK(cond) do { g_checks++; if (!(cond)) { g_fail++; \ + std::cerr << " FAIL [" << g_test << "] " << __LINE__ << ": " #cond "\n"; } } while (0) + +// deterministic synthetic firmware body +static std::vector body(unsigned seed, size_t n) { + std::vector v(n); + uint32_t s = seed * 2654435761u + 1; + for (size_t i = 0; i < n; i++) { s = s * 1103515245u + 12345u; v[i] = (uint8_t)(s >> 16); } + return v; +} + +static bool detools_available() { + return run_argv({"detools", "--help"}, /*quiet=*/true) != 127; // 127 = exec failed (not installed) +} + +// --------------------------------------------------------------------------- +static void t_crypto() { + g_test = "crypto"; + // sha256("abc") = ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad + const char* abc = "abc"; + auto h4 = mh4((const uint8_t*)abc, 3); + auto h8 = mh8((const uint8_t*)abc, 3); + auto h32 = mh32((const uint8_t*)abc, 3); + std::vector exp; + from_hex("ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", exp); + CHECK(std::memcmp(h4.data(), exp.data(), 4) == 0); + CHECK(std::memcmp(h8.data(), exp.data(), 8) == 0); + CHECK(std::memcmp(h32.data(), exp.data(), 32) == 0); + + uint8_t priv[32], pub[32], pub2[32]; + CHECK(ed25519_keygen(priv, pub)); + CHECK(ed25519_pub_from_priv(pub2, priv)); + CHECK(std::memcmp(pub, pub2, 32) == 0); + auto msg = body(1, 100); + uint8_t sig[64]; + CHECK(ed25519_sign(sig, msg.data(), msg.size(), priv)); + CHECK(ed25519_verify(sig, msg.data(), msg.size(), pub)); + msg[0] ^= 0xFF; // tampered message -> reject + CHECK(!ed25519_verify(sig, msg.data(), msg.size(), pub)); + msg[0] ^= 0xFF; + uint8_t other[32], opub[32]; ed25519_keygen(other, opub); + CHECK(!ed25519_verify(sig, msg.data(), msg.size(), opub)); // wrong key -> reject +} + +static void t_version_target() { + g_test = "version/target"; + uint32_t v; + CHECK(pack_version("1.16.0", v) && v == 0x01100000u); + CHECK(pack_version("2.0.0", v) && v == 0x02000000u); + CHECK(pack_version("1.16.0.2", v) && (v & 0xFF) == 2); + CHECK(!pack_version("", v)); + CHECK(!pack_version("x.y", v)); + // known target ids from the firmware builds (ties motatool's hashing to the device) + CHECK(target_id_for_env("Heltec_v3_repeater") == 0xd1b29b18u); + CHECK(target_id_for_env("RAK_4631_repeater") == 0x04d413fdu); + // reverse lookup table (human-readable target_id) -> env name, "" when unknown + CHECK(target_env_name(0xd1b29b18u) == "Heltec_v3_repeater"); + CHECK(target_env_name(0x04d413fdu) == "RAK_4631_repeater"); + CHECK(target_env_name(0xDEADBEEFu).empty()); + // the shared (generated) table must round-trip with our own hashing for a spread of OTA envs — + // this catches any drift between the table's stored ids and sha2-256:4(env) + for (const char* e : {"Tbeam_SX1262_repeater", "Xiao_C3_repeater", "ThinkNode_M2_room_server", + "Ebyte_EoRa-S3_companion_radio_ble", "Heltec_v3_companion_radio_usb"}) + CHECK(target_env_name(target_id_for_env(e)) == e); +} + +static void t_intel_hex() { + g_test = "intel hex"; + fs::path hx = fs::temp_directory_path() / "motatool_test.hex"; + // 3 data bytes (01 02 03) at addr 0, then EOF (checksum F7 = two's-complement of the byte sum) + std::string ok = ":03000000010203F7\n:00000001FF\n"; + write_file(hx.string(), (const uint8_t*)ok.data(), ok.size()); + std::vector out; + CHECK(read_input(hx.string(), out).empty()); + CHECK(out.size() == 3 && out[0] == 1 && out[1] == 2 && out[2] == 3); + // wrong checksum -> rejected + std::string bad = ":03000000010203F8\n:00000001FF\n"; + write_file(hx.string(), (const uint8_t*)bad.data(), bad.size()); + std::vector o2; + CHECK(!read_input(hx.string(), o2).empty()); + fs::remove(hx); +} + +static void t_endf() { + g_test = "endf"; + auto b = body(2, 1500); + std::array bh{}; + FwIdent id{0x01100000u, 0x04d413fdu, "RAK4631"}; + auto img = ensure_endf(b, id, bh); + CHECK(img.size() == b.size() + ENDF_LEN); + CHECK(has_endf(img)); + CHECK(std::memcmp(bh.data(), mh8(b.data(), b.size()).data(), 8) == 0); // body_hash is over BODY only + auto gi = parse_endf_ident(img); + CHECK(gi.fw_version == 0x01100000u && gi.target_id == 0x04d413fdu && gi.hw_id == "RAK4631"); + // idempotent: feeding an already-EndF'd image keeps it + reads the same hash + std::array bh2{}; + auto img2 = ensure_endf(img, FwIdent{}, bh2); + CHECK(img2 == img && bh2 == bh); + // zero identity -> still a 56-byte trailer, identity reads empty + std::array bz{}; + auto z = ensure_endf(b, FwIdent{}, bz); + CHECK(z.size() == b.size() + ENDF_LEN); + auto zi = parse_endf_ident(z); + CHECK(zi.fw_version == 0 && zi.target_id == 0 && zi.hw_id.empty()); + CHECK(!has_endf(std::vector{1,2,3})); // too short +} + +static BuildOpts full_opts(const std::vector& fw, const std::vector& priv = {}) { + BuildOpts o; o.fw = fw; o.codec = CODEC_FULL; + o.have_target = true; o.target_id = 0x04d413fdu; + o.have_fwver = true; o.fw_version = 0x01100000u; + o.hw_id = "RAK4631"; o.sign_priv = priv; + return o; +} + +static void t_build_full() { + g_test = "build full"; + auto fw = body(3, 5 * 1024 + 100); // 6 blocks @1024 + std::vector blob; std::string name; + CHECK(build(full_opts(fw), blob, name).empty()); + Manifest m; + CHECK(parse(blob, m).empty()); + CHECK(m.format_ver == FORMAT_VER && m.is_full() && !m.is_signed()); + CHECK(m.codec_id == CODEC_FULL); + CHECK(m.target_id == 0x04d413fdu && m.fw_version == 0x01100000u && m.hw_id_str() == "RAK4631"); + CHECK(m.block_count == 6); + // fixed layout offsets + CHECK(m.leaves_off() == 8 + MOTA_MFL && m.leaves_off() == 205); + CHECK(m.payload_off() == 205 + m.block_count * 4); + CHECK(m.total_size() == blob.size()); + // payload IS the image (body+EndF); image_hash matches + uint32_t poff = m.payload_off(); + auto img_h = mh32(blob.data() + poff, m.payload_size); + CHECK(std::memcmp(img_h.data(), m.image_hash.data(), 32) == 0); + CHECK(m.image_size == m.payload_size); + CHECK(verify(blob).empty()); // clean + CHECK(name.find(".mota") != std::string::npos && name.find("full") != std::string::npos); +} + +static void t_build_signed_and_tamper() { + g_test = "build signed/tamper"; + uint8_t priv[32], pub[32]; ed25519_keygen(priv, pub); + std::vector pv(priv, priv + 32); + auto fw = body(4, 3000); + std::vector blob; std::string name; + CHECK(build(full_opts(fw, pv), blob, name).empty()); + Manifest m; CHECK(parse(blob, m).empty()); + CHECK(m.is_signed()); + CHECK(std::memcmp(m.signer.data(), pub, 32) == 0); + CHECK(verify(blob).empty()); + // tamper a payload byte -> integrity fails + auto t1 = blob; t1[m.payload_off() + 10] ^= 0xFF; + CHECK(!verify(t1).empty()); + // tamper a signed-region byte (target_id @ manifest+3 = blob+11) -> signature invalid + auto t2 = blob; t2[8 + M_OFF_TARGET_ID] ^= 0xFF; + bool sig_flagged = false; + for (auto& p : verify(t2)) if (p.find("signature") != std::string::npos) sig_flagged = true; + CHECK(sig_flagged); +} + +static void t_corruption_and_approval() { + g_test = "corruption/approval"; + auto fw = body(5, 4096); + std::vector blob; std::string name; + build(full_opts(fw), blob, name); + Manifest m; parse(blob, m); + // flip a payload byte -> leaves/merkle/image_hash problems reported + auto c = blob; c[m.payload_off() + 1] ^= 0xFF; + CHECK(!verify(c).empty()); + // a pre-approved container must be flagged (approval is outside the signed region) + auto ap = blob; std::memcpy(ap.data() + 8 + M_OFF_APPROVAL, APPROVAL_YES, 4); + bool approved_flagged = false; + for (auto& p : verify(ap)) if (p.find("approved") != std::string::npos) approved_flagged = true; + CHECK(approved_flagged); +} + +static void t_parse_rejects() { + g_test = "parse rejects"; + auto fw = body(6, 2048); + std::vector blob; std::string name; build(full_opts(fw), blob, name); + Manifest m; + CHECK(!parse(std::vector(10, 0), m).empty()); // too small + auto bad = blob; bad[0] ^= 0xFF; CHECK(!parse(bad, m).empty()); // bad magic + bad = blob; bad[bad.size() - 1] ^= 0xFF; CHECK(!parse(bad, m).empty()); // bad trailer + bad = blob; bad[4] ^= 0xFF; CHECK(!parse(bad, m).empty()); // wrong total_size + bad = blob; bad[8 + M_OFF_FORMAT_VER] = 9; CHECK(!parse(bad, m).empty()); // bad format_ver +} + +static void t_delta() { + g_test = "delta"; + if (!detools_available()) { std::cerr << " SKIP [delta] detools not on PATH\n"; return; } + auto base_body = body(10, 4000); + auto new_body = base_body; + for (int i : {100, 101, 2000, 3999}) new_body[i] ^= 0x33; + auto tail = body(11, 250); + new_body.insert(new_body.end(), tail.begin(), tail.end()); + + for (uint8_t codec : {CODEC_DETOOLS_SEQUENTIAL, CODEC_DETOOLS_INPLACE}) { + BuildOpts o; o.fw = new_body; o.base = base_body; o.codec = codec; + o.have_target = true; o.target_id = 0x04d413fdu; o.have_fwver = true; o.fw_version = 0x02000000u; + o.hw_id = "RAK4631"; + std::vector blob; std::string name; + std::string e = build(o, blob, name); + CHECK(e.empty()); + if (!e.empty()) continue; + Manifest m; CHECK(parse(blob, m).empty()); + CHECK(!m.is_full() && m.codec_id == codec); + // base_hash == mh8 of the base BODY + CHECK(std::memcmp(m.base_hash.data(), mh8(base_body.data(), base_body.size()).data(), 8) == 0); + CHECK(verify(blob).empty()); + if (codec == CODEC_DETOOLS_SEQUENTIAL) { // round-trip: apply -> rebuilds the new image + std::array bh{}; + auto base_img = ensure_endf(base_body, FwIdent{}, bh); + std::vector patch(blob.begin() + m.payload_off(), blob.begin() + m.payload_off() + m.payload_size); + std::vector recon; + CHECK(detools_apply_seq("detools", base_img, patch, recon).empty()); + CHECK(std::memcmp(mh32(recon.data(), recon.size()).data(), m.image_hash.data(), 32) == 0); + } + } +} + +static void t_folder_and_seeder() { + g_test = "folder/seeder"; + fs::path dir = fs::temp_directory_path() / "motatool_test_folder"; + fs::remove_all(dir); fs::create_directories(dir / "sub"); + + // two valid motas (one in a sub-folder, to exercise recursion) + one corrupt + std::vector a, b; std::string n; + build(full_opts(body(20, 3000)), a, n); write_file((dir / "a.mota").string(), a.data(), a.size()); + build(full_opts(body(21, 6000)), b, n); write_file((dir / "sub" / "b.mota").string(), b.data(), b.size()); + auto bad = a; bad[a.size() / 2] ^= 0xFF; write_file((dir / "bad.mota").string(), bad.data(), bad.size()); + write_file((dir / "ignore.txt").string(), a.data(), 10); // non-.mota -> skipped silently + + int warns = 0; std::string warned_path; + Folder folder; + size_t kept = folder.scan(dir.string(), true, + [&](const std::string& p, const std::string&){ warns++; warned_path = p; }); + CHECK(kept == 2); // two valid kept + CHECK(warns == 1 && warned_path.find("bad.mota") != std::string::npos); // corrupt warned + excluded + + SeederCore core(folder); + uint8_t status; std::vector pl; + CHECK(core.handle(MS_OP_COUNT, nullptr, 0, status, pl) && status == MS_STATUS_OK && pl.size() == 1 && pl[0] == 2); + + uint8_t arg0[1] = {0}; + CHECK(core.handle(MS_OP_DESCRIBE, arg0, 1, status, pl) && status == MS_STATUS_OK && pl.size() == MOTA_DESC_WIRE); + const ServedMota* s0 = folder.at(0); + CHECK(std::memcmp(pl.data(), s0->m.merkle_root.data(), 4) == 0); // mid + CHECK(rd_u32(pl.data() + 4) == s0->m.target_id); + CHECK(rd_u32(pl.data() + 14) == (uint32_t)s0->bytes.size()); // total_size + CHECK(rd_u32(pl.data() + 18) == s0->m.leaves_off() && rd_u32(pl.data() + 18) == 205); + CHECK(rd_u32(pl.data() + 22) == s0->m.block_count); + CHECK(rd_u32(pl.data() + 26) == s0->m.payload_off()); + CHECK(rd_u32(pl.data() + 30) == s0->m.payload_size); + + uint8_t bad_idx[1] = {99}; + CHECK(core.handle(MS_OP_DESCRIBE, bad_idx, 1, status, pl) && status == MS_STATUS_ERR); + + // READ idx=0 off=0 len=8 -> first 8 bytes (MAGIC + total_size) + uint8_t rd[7] = {0, 0,0,0,0, 8,0}; + CHECK(core.handle(MS_OP_READ, rd, 7, status, pl) && status == MS_STATUS_OK && pl.size() == 8); + CHECK(std::memcmp(pl.data(), s0->bytes.data(), 8) == 0); + // READ past EOF -> error + uint8_t reof[7]; reof[0] = 0; wr_u32(reof + 1, (uint32_t)s0->bytes.size()); reof[5] = 16; reof[6] = 0; + CHECK(core.handle(MS_OP_READ, reof, 7, status, pl) && status == MS_STATUS_ERR); + + fs::remove_all(dir); +} + +int main() { + t_crypto(); + t_version_target(); + t_intel_hex(); + t_endf(); + t_build_full(); + t_build_signed_and_tamper(); + t_corruption_and_approval(); + t_parse_rejects(); + t_delta(); + t_folder_and_seeder(); + + std::cout << (g_fail ? "FAILED " : "OK ") << (g_checks - g_fail) << "/" << g_checks << " checks passed\n"; + return g_fail ? 1 : 0; +} diff --git a/variants/gat562_30s_mesh_kit/platformio.ini b/variants/gat562_30s_mesh_kit/platformio.ini index 2baac2561b..e940085e5e 100644 --- a/variants/gat562_30s_mesh_kit/platformio.ini +++ b/variants/gat562_30s_mesh_kit/platformio.ini @@ -1,8 +1,8 @@ [GAT562_30S_Mesh_Kit] -extends = nrf52_base +extends = rak4631_hw board = rak4631 board_check = true -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/gat562_30s_mesh_kit -D RAK_4631 @@ -19,13 +19,13 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BUZZER=33 -D SX126X_RX_BOOSTED_GAIN=1 -D SX126X_DIO2_AS_RF_SWITCH=true -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/gat562_30s_mesh_kit> + + + lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27 diff --git a/variants/gat562_mesh_evb_pro/platformio.ini b/variants/gat562_mesh_evb_pro/platformio.ini index b3e894174a..20dedd8412 100644 --- a/variants/gat562_mesh_evb_pro/platformio.ini +++ b/variants/gat562_mesh_evb_pro/platformio.ini @@ -1,8 +1,8 @@ [GAT562_Mesh_EVB_Pro] -extends = nrf52_base +extends = rak4631_hw board = rak4631 board_check = true -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_evb_pro -D NRF52_POWER_MANAGEMENT @@ -13,12 +13,12 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/gat562_mesh_evb_pro> + + lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27 diff --git a/variants/gat562_mesh_tracker_pro/platformio.ini b/variants/gat562_mesh_tracker_pro/platformio.ini index af153b8fc2..26bf49cbec 100644 --- a/variants/gat562_mesh_tracker_pro/platformio.ini +++ b/variants/gat562_mesh_tracker_pro/platformio.ini @@ -1,8 +1,8 @@ [GAT562_Mesh_Tracker_Pro] -extends = nrf52_base +extends = rak4631_hw board = rak4631 board_check = true -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/gat562_mesh_tracker_pro -D NRF52_POWER_MANAGEMENT @@ -15,13 +15,13 @@ build_flags = ${nrf52_base.build_flags} -D LORA_TX_POWER=22 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/gat562_mesh_tracker_pro> + + + lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27 diff --git a/variants/gat562_mesh_watch13/platformio.ini b/variants/gat562_mesh_watch13/platformio.ini index f3510b74aa..59ec79d311 100644 --- a/variants/gat562_mesh_watch13/platformio.ini +++ b/variants/gat562_mesh_watch13/platformio.ini @@ -1,8 +1,8 @@ [GAT562_Mesh_Watch13] -extends = nrf52_base +extends = rak4631_hw board = rak4631 board_check = true -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -UENV_INCLUDE_GPS -I variants/gat562_mesh_watch13 @@ -18,13 +18,13 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -D QSPIFLASH=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/gat562_mesh_watch13> + + + lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 diff --git a/variants/heltec_t114/platformio.ini b/variants/heltec_t114/platformio.ini index 135babb1a2..8e1f66fe4f 100644 --- a/variants/heltec_t114/platformio.ini +++ b/variants/heltec_t114/platformio.ini @@ -2,10 +2,10 @@ ; Heltec T114 without display ; [Heltec_t114] -extends = nrf52_base +extends = rak4631_hw board = heltec_t114 board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_6.1.1_API/include -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 @@ -36,12 +36,12 @@ build_flags = ${nrf52_base.build_flags} -D PIN_GPS_RESET_ACTIVE=LOW -D ENV_PIN_SDA=PIN_WIRE1_SDA -D ENV_PIN_SCL=PIN_WIRE1_SCL -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + +<../variants/heltec_t114> lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/heltec_v3/platformio.ini b/variants/heltec_v3/platformio.ini index a70a93a508..e20c408e0b 100644 --- a/variants/heltec_v3/platformio.ini +++ b/variants/heltec_v3/platformio.ini @@ -6,6 +6,7 @@ build_flags = ${sensor_base.build_flags} -I variants/heltec_v3 -D HELTEC_LORA_V3 + -D MOTA_HW_ID='"Heltec_v3"' ; OTA hardware tag (apply refuses a .mota for different hw) -D ESP32_CPU_FREQ=80 -D P_LORA_DIO_1=14 -D P_LORA_NSS=8 @@ -47,10 +48,16 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 + -D ENABLE_OTA=1 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 +extra_scripts = + merge-bin.py + post:tools/mota/pio_endf.py build_src_filter = ${Heltec_lora32_v3.build_src_filter} + + + + + +<../examples/simple_repeater> lib_deps = ${Heltec_lora32_v3.lib_deps} diff --git a/variants/ikoka_handheld_nrf/platformio.ini b/variants/ikoka_handheld_nrf/platformio.ini index 51b602e403..e14c4d4928 100644 --- a/variants/ikoka_handheld_nrf/platformio.ini +++ b/variants/ikoka_handheld_nrf/platformio.ini @@ -1,8 +1,8 @@ [ikoka_handheld_nrf] -extends = nrf52_base +extends = rak4631_hw board = seeed-xiao-afruitnrf52-nrf52840 board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 @@ -23,10 +23,10 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_DIO3_TCXO_VOLTAGE=1.8 -D SX126X_CURRENT_LIMIT=140 -D SX126X_RX_BOOSTED_GAIN=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/ikoka_handheld_nrf> + -lib_deps = ${nrf52_base.lib_deps} +lib_deps = ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} densaugeo/base64 @ ~1.4.0 diff --git a/variants/ikoka_nano_nrf/platformio.ini b/variants/ikoka_nano_nrf/platformio.ini index e72f83ce0d..537c40c0e2 100644 --- a/variants/ikoka_nano_nrf/platformio.ini +++ b/variants/ikoka_nano_nrf/platformio.ini @@ -1,8 +1,8 @@ [ikoka_nano_nrf] -extends = nrf52_base +extends = rak4631_hw board = seeed-xiao-afruitnrf52-nrf52840 board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -D NRF52_PLATFORM -D XIAO_NRF52 -I lib/nrf52/s140_nrf52_7.3.0_API/include @@ -29,7 +29,7 @@ build_flags = ${nrf52_base.build_flags} -UENV_INCLUDE_GPS debug_tool = jlink upload_protocol = nrfutil -lib_deps = ${nrf52_base.lib_deps} +lib_deps = ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} [ikoka_nano_nrf_e22_22dbm] diff --git a/variants/ikoka_stick_nrf/platformio.ini b/variants/ikoka_stick_nrf/platformio.ini index 06e39e84c3..1cf3286c30 100644 --- a/variants/ikoka_stick_nrf/platformio.ini +++ b/variants/ikoka_stick_nrf/platformio.ini @@ -1,8 +1,8 @@ [ikoka_stick_nrf] -extends = nrf52_base +extends = rak4631_hw board = seeed-xiao-afruitnrf52-nrf52840 board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -D NRF52_PLATFORM -D XIAO_NRF52 -I lib/nrf52/s140_nrf52_7.3.0_API/include @@ -29,7 +29,7 @@ build_flags = ${nrf52_base.build_flags} -D PIN_WIRE_SCL=7 -D PIN_WIRE_SDA=6 -UENV_INCLUDE_GPS -lib_deps = ${nrf52_base.lib_deps} +lib_deps = ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} [ikoka_stick_nrf_e22_22dbm] diff --git a/variants/lilygo_techo/platformio.ini b/variants/lilygo_techo/platformio.ini index 5df77f95cb..b57da999e0 100644 --- a/variants/lilygo_techo/platformio.ini +++ b/variants/lilygo_techo/platformio.ini @@ -1,8 +1,8 @@ [LilyGo_T-Echo] -extends = nrf52_base +extends = rak4631_hw board = t-echo board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I variants/lilygo_techo -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include @@ -34,7 +34,7 @@ build_flags = ${nrf52_base.build_flags} -D DISPLAY_CLASS=GxEPDDisplay -D BACKLIGHT_BTN=PIN_BUTTON2 -D AUTO_OFF_MILLIS=0 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + + @@ -42,7 +42,7 @@ build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/lilygo_techo> lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 adafruit/Adafruit BME280 Library @ ^2.3.0 zinggjm/GxEPD2 @ 1.6.2 diff --git a/variants/lilygo_techo_card/platformio.ini b/variants/lilygo_techo_card/platformio.ini index 07bcebfe41..2d9765c6cd 100644 --- a/variants/lilygo_techo_card/platformio.ini +++ b/variants/lilygo_techo_card/platformio.ini @@ -1,8 +1,8 @@ [LilyGo_T-Echo_Card] -extends = nrf52_base +extends = rak4631_hw board = t-echo board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I variants/lilygo_techo_card -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include @@ -19,7 +19,7 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_GPS=1 -D DISPLAY_CLASS=U8g2Display -D PIN_OLED_RESET=-1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + + @@ -27,7 +27,7 @@ build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/lilygo_techo_card> lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 olikraus/U8g2 @ ^2.35.19 adafruit/Adafruit NeoPixel@^1.10.0 diff --git a/variants/lilygo_techo_lite/platformio.ini b/variants/lilygo_techo_lite/platformio.ini index 4b5edf6918..3f7677c322 100644 --- a/variants/lilygo_techo_lite/platformio.ini +++ b/variants/lilygo_techo_lite/platformio.ini @@ -1,8 +1,8 @@ [LilyGo_T-Echo-Lite] -extends = nrf52_base +extends = rak4631_hw board = t-echo board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I variants/lilygo_techo_lite -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include @@ -28,7 +28,7 @@ build_flags = ${nrf52_base.build_flags} -D EINK_Y_OFFSET=10 -D DISPLAY_ROTATION=4 -D AUTO_OFF_MILLIS=0 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + + @@ -36,7 +36,7 @@ build_src_filter = ${nrf52_base.build_src_filter} + +<../variants/lilygo_techo_lite> lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 adafruit/Adafruit BME280 Library @ ^2.3.0 https://github.com/SoulOfNoob/GxEPD2.git @@ -104,7 +104,7 @@ upload_protocol = nrfutil board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = - ${nrf52_base.build_flags} + ${rak4631_hw.build_flags} -I variants/lilygo_techo_lite -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include @@ -131,7 +131,7 @@ build_flags = -D OFFLINE_QUEUE_SIZE=256 -D UI_RECENT_LIST_SIZE=9 -D AUTO_SHUTDOWN_MILLIVOLTS=3300 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + + @@ -149,7 +149,7 @@ upload_protocol = nrfutil board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld board_upload.maximum_size = 712704 build_flags = - ${nrf52_base.build_flags} + ${rak4631_hw.build_flags} -I variants/lilygo_techo_lite -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include @@ -175,7 +175,7 @@ build_flags = -D AUTO_SHUTDOWN_MILLIVOLTS=3300 ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + + diff --git a/variants/muziworks_r1_neo/platformio.ini b/variants/muziworks_r1_neo/platformio.ini index 3dbecf1e84..d12c52816f 100644 --- a/variants/muziworks_r1_neo/platformio.ini +++ b/variants/muziworks_r1_neo/platformio.ini @@ -1,8 +1,8 @@ [R1Neo] -extends = nrf52_base +extends = rak4631_hw board = rak4631 board_check = true -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/muziworks_r1_neo -I src/helpers/ui @@ -19,13 +19,13 @@ build_flags = ${nrf52_base.build_flags} -D PIN_GPS_TX=25 -D PIN_GPS_RX=24 -D PIN_GPS_EN=33 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/muziworks_r1_neo> + + + lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} sparkfun/SparkFun u-blox GNSS Arduino Library@^2.2.27 diff --git a/variants/promicro/platformio.ini b/variants/promicro/platformio.ini index 5415e15861..23a7d46b07 100644 --- a/variants/promicro/platformio.ini +++ b/variants/promicro/platformio.ini @@ -1,7 +1,7 @@ [Promicro] -extends = nrf52_base +extends = rak4631_hw board = promicro_nrf52840 -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I variants/promicro -D PROMICRO -D USE_SX1262 @@ -23,10 +23,10 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_BMP280=1 -D ENV_INCLUDE_INA3221=1 -D ENV_INCLUDE_INA219=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + +<../variants/promicro> -lib_deps= ${nrf52_base.lib_deps} +lib_deps= ${rak4631_hw.lib_deps} adafruit/Adafruit SSD1306 @ ^2.5.13 adafruit/Adafruit INA3221 Library @ ^1.0.1 adafruit/Adafruit INA219 @ ^1.2.3 diff --git a/variants/rak4631/platformio.ini b/variants/rak4631/platformio.ini index 2bbba31463..0d2ca7dc0e 100644 --- a/variants/rak4631/platformio.ini +++ b/variants/rak4631/platformio.ini @@ -4,11 +4,13 @@ board = rak4631 board_check = true extra_scripts = ${nrf52_base.extra_scripts} post:variants/rak4631/fix_bsec_lib.py + post:tools/mota/pio_endf.py ; EndF trailer for OTA self-identity (all RAK4631 roles) build_flags = ${nrf52_base.build_flags} ${sensor_base.build_flags} -I variants/rak4631 -D RAK_4631 -D RAK_BOARD + -D MOTA_HW_ID='"RAK4631"' ; OTA hardware tag (apply refuses a .mota for different hw) -D NRF52_POWER_MANAGEMENT -D PIN_BOARD_SCL=14 -D PIN_BOARD_SDA=13 @@ -25,11 +27,15 @@ build_flags = ${nrf52_base.build_flags} -D ENV_INCLUDE_RAK12035=1 -UENV_INCLUDE_BME680 -D ENV_INCLUDE_BME680_BSEC=1 + -D ENABLE_OTA=1 ; OTA delta updates on every RAK4631 role (single-slot, bootloader-applied) + -D OTA_FLASH_STORE=1 ; stage the received .mota in flash (survives reboot into the bootloader) + -D OTA_FOLDER_SERIAL ; `ota folder on` relays a host folder of .mota over the USB console (no extra HW) build_src_filter = ${nrf52_base.build_src_filter} +<../variants/rak4631> + + + + + lib_deps = ${nrf52_base.lib_deps} ${sensor_base.lib_deps} @@ -47,10 +53,12 @@ build_flags = -D ADVERT_LON=0.0 -D ADMIN_PASSWORD='"password"' -D MAX_NEIGHBOURS=50 +; -D OTA_DEBUG=1 ; bring-up: trace OTA fetch (REQ / block / page-flush) over Serial ; -D MESH_PACKET_LOGGING=1 ; -D MESH_DEBUG=1 build_src_filter = ${rak4631.build_src_filter} + + ; OTA (ENABLE_OTA + flash store + EndF + helpers/ota) is inherited from the [rak4631] base now +<../examples/simple_repeater> [env:RAK_4631_repeater_bridge_rs232_serial1] diff --git a/variants/rak_wismesh_tag/platformio.ini b/variants/rak_wismesh_tag/platformio.ini index e9cddb74dd..45669414e2 100644 --- a/variants/rak_wismesh_tag/platformio.ini +++ b/variants/rak_wismesh_tag/platformio.ini @@ -1,8 +1,8 @@ [rak_wismesh_tag] -extends = nrf52_base +extends = rak4631_hw board = rak4631 board_check = true -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I variants/rak_wismesh_tag -I src/helpers/ui @@ -25,13 +25,13 @@ build_flags = ${nrf52_base.build_flags} -D PIN_BUZZER=21 -D PIN_BOARD_SDA=PIN_WIRE_SDA -D PIN_BOARD_SCL=PIN_WIRE_SCL -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} +<../variants/rak_wismesh_tag> + + + lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} [env:RAK_WisMesh_Tag_repeater] diff --git a/variants/t1000-e/platformio.ini b/variants/t1000-e/platformio.ini index 43a3d93f61..37d37a4527 100644 --- a/variants/t1000-e/platformio.ini +++ b/variants/t1000-e/platformio.ini @@ -1,8 +1,8 @@ [t1000-e] -extends = nrf52_base +extends = rak4631_hw board = tracker-t1000-e board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 @@ -28,7 +28,7 @@ build_flags = ${nrf52_base.build_flags} -D LR11X0_DIO_AS_RF_SWITCH=true -D LR11X0_DIO3_TCXO_VOLTAGE=1.6 -D ENV_INCLUDE_GPS=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + +<../variants/t1000-e> diff --git a/variants/thinknode_m1/platformio.ini b/variants/thinknode_m1/platformio.ini index 356edfee69..e10ddaa739 100644 --- a/variants/thinknode_m1/platformio.ini +++ b/variants/thinknode_m1/platformio.ini @@ -1,8 +1,8 @@ [ThinkNode_M1] -extends = nrf52_base +extends = rak4631_hw board = thinknode_m1 board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 @@ -24,12 +24,12 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 -D LORA_TX_POWER=22 -D P_LORA_TX_LED=13 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + +<../variants/thinknode_m1> lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} stevemarple/MicroNMEA @ ^2.0.6 debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/thinknode_m3/platformio.ini b/variants/thinknode_m3/platformio.ini index 0a3d4eda92..7e6e2db288 100644 --- a/variants/thinknode_m3/platformio.ini +++ b/variants/thinknode_m3/platformio.ini @@ -1,8 +1,8 @@ [ThinkNode_M3] -extends = nrf52_base +extends = rak4631_hw board = thinknode_m3 board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include -I lib/nrf52/s140_nrf52_6.1.1_API/include/nrf52 @@ -29,13 +29,13 @@ build_flags = ${nrf52_base.build_flags} -D LR11X0_DIO3_TCXO_VOLTAGE=3.3 -D MESH_DEBUG=1 -D ENV_INCLUDE_GPS=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + +<../variants/thinknode_m3> + debug_tool = stlink upload_protocol = nrfutil -lib_deps= ${nrf52_base.lib_deps} +lib_deps= ${rak4631_hw.lib_deps} [env:ThinkNode_M3_repeater] extends = ThinkNode_M3 diff --git a/variants/thinknode_m6/platformio.ini b/variants/thinknode_m6/platformio.ini index 6fe9043668..9f1d8093e6 100644 --- a/variants/thinknode_m6/platformio.ini +++ b/variants/thinknode_m6/platformio.ini @@ -1,8 +1,8 @@ [ThinkNode_M6] -extends = nrf52_base +extends = rak4631_hw board = thinknode_m6 board_build.ldscript = boards/nrf52840_s140_v6.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I src/helpers/nrf52 -I lib/nrf52/s140_nrf52_6.1.1_API/include @@ -27,13 +27,13 @@ build_flags = ${nrf52_base.build_flags} -D P_LORA_TX_LED=PIN_LED_BLUE ; -D PERSISTANT_GPS=1 ; -D ENV_SKIP_GPS_DETECT=1 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + + +<../variants/thinknode_m6> lib_deps = - ${nrf52_base.lib_deps} + ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} debug_tool = jlink upload_protocol = nrfutil diff --git a/variants/wio-tracker-l1-eink/platformio.ini b/variants/wio-tracker-l1-eink/platformio.ini index 2c74baded3..4929213af9 100644 --- a/variants/wio-tracker-l1-eink/platformio.ini +++ b/variants/wio-tracker-l1-eink/platformio.ini @@ -1,8 +1,8 @@ [WioTrackerL1Eink] -extends = nrf52_base +extends = rak4631_hw board = seeed-wio-tracker-l1 board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 @@ -27,12 +27,12 @@ build_flags = ${nrf52_base.build_flags} -D GPS_BAUD_RATE=9600 -D ENV_PIN_SDA=PIN_WIRE1_SDA -D ENV_PIN_SCL=PIN_WIRE1_SCL -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + +<../variants/wio-tracker-l1> + + -lib_deps= ${nrf52_base.lib_deps} +lib_deps= ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} adafruit/Adafruit GFX Library @ ^1.12.1 zinggjm/GxEPD2 @ 1.6.2 diff --git a/variants/wio-tracker-l1/platformio.ini b/variants/wio-tracker-l1/platformio.ini index 7bb175bb9a..4bd413639b 100644 --- a/variants/wio-tracker-l1/platformio.ini +++ b/variants/wio-tracker-l1/platformio.ini @@ -1,8 +1,8 @@ [WioTrackerL1] -extends = nrf52_base +extends = rak4631_hw board = seeed-wio-tracker-l1 board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 @@ -16,12 +16,12 @@ build_flags = ${nrf52_base.build_flags} -D SX126X_RX_BOOSTED_GAIN=1 -D PIN_OLED_RESET=-1 -D GPS_BAUD_RATE=9600 -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + +<../variants/wio-tracker-l1> + + -lib_deps= ${nrf52_base.lib_deps} +lib_deps= ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} adafruit/Adafruit SH110X @ ^2.1.13 adafruit/Adafruit GFX Library @ ^1.12.1 diff --git a/variants/xiao_nrf52/platformio.ini b/variants/xiao_nrf52/platformio.ini index a085433688..2120704340 100644 --- a/variants/xiao_nrf52/platformio.ini +++ b/variants/xiao_nrf52/platformio.ini @@ -1,8 +1,8 @@ [Xiao_nrf52] -extends = nrf52_base +extends = rak4631_hw board = seeed-xiao-afruitnrf52-nrf52840 board_build.ldscript = boards/nrf52840_s140_v7.ld -build_flags = ${nrf52_base.build_flags} +build_flags = ${rak4631_hw.build_flags} ${sensor_base.build_flags} -I lib/nrf52/s140_nrf52_7.3.0_API/include -I lib/nrf52/s140_nrf52_7.3.0_API/include/nrf52 @@ -30,14 +30,14 @@ build_flags = ${nrf52_base.build_flags} -D PIN_WIRE_SDA=D7 -D PIN_USER_BTN=PIN_BUTTON1 -D DISPLAY_CLASS=NullDisplayDriver -build_src_filter = ${nrf52_base.build_src_filter} +build_src_filter = ${rak4631_hw.build_src_filter} + + +<../variants/xiao_nrf52> + debug_tool = jlink upload_protocol = nrfutil -lib_deps = ${nrf52_base.lib_deps} +lib_deps = ${rak4631_hw.lib_deps} ${sensor_base.lib_deps} [env:Xiao_nrf52_companion_radio_ble]