From 07b20f042ecd1d812592e97224efdd0c19376e4d Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:05 +0200 Subject: [PATCH 01/15] ota: .mota container format, merkle tree, EndF self-identity, signature verify --- src/helpers/ota/BlockBitmap.h | 52 +++++ src/helpers/ota/FirmwareInfo.cpp | 42 ++++ src/helpers/ota/FirmwareInfo.h | 35 +++ src/helpers/ota/MerkleTree.cpp | 110 ++++++++++ src/helpers/ota/MerkleTree.h | 46 ++++ src/helpers/ota/MotaContainer.cpp | 83 ++++++++ src/helpers/ota/MotaContainer.h | 64 ++++++ src/helpers/ota/Multihash.h | 28 +++ src/helpers/ota/OtaByteIO.h | 58 +++++ src/helpers/ota/OtaFormat.h | 92 ++++++++ src/helpers/ota/OtaSelf.cpp | 186 ++++++++++++++++ src/helpers/ota/OtaSelf.h | 28 +++ src/helpers/ota/OtaTargets.h | 339 ++++++++++++++++++++++++++++++ src/helpers/ota/OtaVerify.cpp | 27 +++ src/helpers/ota/OtaVerify.h | 29 +++ src/helpers/ota/SignerAllowlist.h | 72 +++++++ 16 files changed, 1291 insertions(+) create mode 100644 src/helpers/ota/BlockBitmap.h create mode 100644 src/helpers/ota/FirmwareInfo.cpp create mode 100644 src/helpers/ota/FirmwareInfo.h create mode 100644 src/helpers/ota/MerkleTree.cpp create mode 100644 src/helpers/ota/MerkleTree.h create mode 100644 src/helpers/ota/MotaContainer.cpp create mode 100644 src/helpers/ota/MotaContainer.h create mode 100644 src/helpers/ota/Multihash.h create mode 100644 src/helpers/ota/OtaByteIO.h create mode 100644 src/helpers/ota/OtaFormat.h create mode 100644 src/helpers/ota/OtaSelf.cpp create mode 100644 src/helpers/ota/OtaSelf.h create mode 100644 src/helpers/ota/OtaTargets.h create mode 100644 src/helpers/ota/OtaVerify.cpp create mode 100644 src/helpers/ota/OtaVerify.h create mode 100644 src/helpers/ota/SignerAllowlist.h 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/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/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/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/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/OtaTargets.h b/src/helpers/ota/OtaTargets.h new file mode 100644 index 0000000000..e7ec61cef6 --- /dev/null +++ b/src/helpers/ota/OtaTargets.h @@ -0,0 +1,339 @@ +#pragma once +#include + +// AUTO-GENERATED by tools/mota/gen_targets.py — do not edit by hand. +// 319 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" }, + { 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_" }, + { 0xf1c1641c, "nibble_screen_connect_companion_radio_ble" }, + { 0x8dcc5d89, "nibble_screen_connect_companion_radio_usb" }, + { 0x6139a846, "nibble_screen_connect_companion_radio_wifi" }, + { 0xc0dbcae4, "nibble_screen_connect_kiss_modem" }, + { 0xcd0bdd06, "nibble_screen_connect_repeater" }, + { 0x9ecb53b1, "nibble_screen_connect_repeater_bridge_espnow" }, + { 0x82f6d321, "nibble_screen_connect_room_server" }, + { 0xb10261c8, "nibble_screen_connect_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" }, + { 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" }, + { 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" }, + { 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" }, + { 0x37443a5b, "WHY2025_badge_companion_radio_ble_" }, + { 0x603384a7, "WHY2025_badge_repeater_" }, + { 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_" }, + { 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 From 6b4f2355e90cfcd5db8233afd75a06a6deb92cad Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:05 +0200 Subject: [PATCH 02/15] ota: vendor detools 0.53.0 in-place/CRLE decoder (NONE+CRLE config) --- src/helpers/ota/detools/README.meshcore.txt | 42 + src/helpers/ota/detools/detools.c | 2783 +++++++++++++++++++ src/helpers/ota/detools/detools.h | 639 +++++ 3 files changed, 3464 insertions(+) create mode 100644 src/helpers/ota/detools/README.meshcore.txt create mode 100644 src/helpers/ota/detools/detools.c create mode 100644 src/helpers/ota/detools/detools.h 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 From 5da783040f67007d515b4cab6d44fde2a4f1d20f Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:05 +0200 Subject: [PATCH 03/15] ota: LoRa transfer protocol + session manager (discovery, block fetch/serve) --- src/helpers/ota/MotaSeederProto.h | 44 ++ src/helpers/ota/OtaContext.cpp | 12 + src/helpers/ota/OtaContext.h | 191 ++++++++ src/helpers/ota/OtaDebug.h | 11 + src/helpers/ota/OtaManager.cpp | 780 ++++++++++++++++++++++++++++++ src/helpers/ota/OtaManager.h | 358 ++++++++++++++ src/helpers/ota/OtaProtocol.cpp | 137 ++++++ src/helpers/ota/OtaProtocol.h | 112 +++++ src/helpers/ota/OtaSource.h | 55 +++ src/helpers/ota/OtaStore.h | 103 ++++ 10 files changed, 1803 insertions(+) create mode 100644 src/helpers/ota/MotaSeederProto.h create mode 100644 src/helpers/ota/OtaContext.cpp create mode 100644 src/helpers/ota/OtaContext.h create mode 100644 src/helpers/ota/OtaDebug.h create mode 100644 src/helpers/ota/OtaManager.cpp create mode 100644 src/helpers/ota/OtaManager.h create mode 100644 src/helpers/ota/OtaProtocol.cpp create mode 100644 src/helpers/ota/OtaProtocol.h create mode 100644 src/helpers/ota/OtaSource.h create mode 100644 src/helpers/ota/OtaStore.h 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/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/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..3428b453d8 --- /dev/null +++ b/src/helpers/ota/OtaManager.h @@ -0,0 +1,358 @@ +#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_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; } + 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) + 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/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 From 9b61169c16d2f4e5652eef17e665749d37ac3905 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:05 +0200 Subject: [PATCH 04/15] ota: platform apply + flash staging (ESP32 A/B, nRF52 in-place via bootloader) --- src/helpers/ota/OtaApply.cpp | 509 +++++++++++++++++++++++++ src/helpers/ota/OtaApply.h | 66 ++++ src/helpers/ota/OtaBlInfo.h | 54 +++ src/helpers/ota/OtaFlashLayout_nrf52.h | 39 ++ src/helpers/ota/OtaStoreFlashEsp32.cpp | 243 ++++++++++++ src/helpers/ota/OtaStoreFlashEsp32.h | 116 ++++++ src/helpers/ota/OtaStoreFlashNrf52.cpp | 159 ++++++++ src/helpers/ota/OtaStoreFlashNrf52.h | 77 ++++ 8 files changed, 1263 insertions(+) create mode 100644 src/helpers/ota/OtaApply.cpp create mode 100644 src/helpers/ota/OtaApply.h create mode 100644 src/helpers/ota/OtaBlInfo.h create mode 100644 src/helpers/ota/OtaFlashLayout_nrf52.h create mode 100644 src/helpers/ota/OtaStoreFlashEsp32.cpp create mode 100644 src/helpers/ota/OtaStoreFlashEsp32.h create mode 100644 src/helpers/ota/OtaStoreFlashNrf52.cpp create mode 100644 src/helpers/ota/OtaStoreFlashNrf52.h 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/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/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 From 9beaf6848e57c81f1a3fcbedcc0d0d82b9609508 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:05 +0200 Subject: [PATCH 05/15] ota: relay an external folder of .mota over USB serial (MotaSource) --- src/helpers/ota/MotaSourceSerial.cpp | 96 ++++++++++++++++++++++++++++ src/helpers/ota/MotaSourceSerial.h | 35 ++++++++++ 2 files changed, 131 insertions(+) create mode 100644 src/helpers/ota/MotaSourceSerial.cpp create mode 100644 src/helpers/ota/MotaSourceSerial.h 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 From 543c90119ec37045f57eb6853ce6c7d88a854ecc Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:05 +0200 Subject: [PATCH 06/15] ota: CLI commands + node/CommonCLI integration + example hooks --- examples/companion_radio/MyMesh.h | 2 +- examples/simple_repeater/MyMesh.cpp | 6 +- examples/simple_repeater/MyMesh.h | 6 +- examples/simple_room_server/MyMesh.h | 2 +- examples/simple_sensor/SensorMesh.h | 2 +- src/Mesh.cpp | 145 ++++++++++ src/Mesh.h | 37 +++ src/MeshCore.h | 21 ++ src/Packet.h | 1 + src/helpers/CommonCLI.cpp | 61 ++++- src/helpers/CommonCLI.h | 9 + src/helpers/ota/OtaCli.cpp | 396 +++++++++++++++++++++++++++ src/helpers/ota/OtaCli.h | 16 ++ 13 files changed, 695 insertions(+), 9 deletions(-) create mode 100644 src/helpers/ota/OtaCli.cpp create mode 100644 src/helpers/ota/OtaCli.h 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/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/src/Mesh.cpp b/src/Mesh.cpp index e9b92262ce..6443c24415 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -1,14 +1,111 @@ #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 30000UL // first self-advert ~30 s after boot (let the node settle) +#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 45000UL // spacing during the boot burst (~3 min total), then ... +#endif +#ifndef OTA_ANNOUNCE_INTERVAL_MS +#define OTA_ANNOUNCE_INTERVAL_MS 86400000UL // ... every 24 h — all lowest priority, duty-gated +#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); +} +#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(); + // To be discoverable as a source of our OWN firmware, set up flash-backed self-serve once; then the + // beacon (announce) advertises our served set and peers can QUERY + fetch it. + if (!oc.serving) oc.serving = ota::ota_serve_self(oc, 0); + oc.manager.announce(); + // boot burst (a few closely-spaced adverts so a co-booting peer catches one), then settle to daily + _next_ota_announce = futureMillis(_ota_announce_count < OTA_ANNOUNCE_BURST + ? OTA_ANNOUNCE_BURST_MS : OTA_ANNOUNCE_INTERVAL_MS); + if (_ota_announce_count < 250) _ota_announce_count++; + } + { // auto-install (once per COMPLETE fetch): only signed images, and apply_fetched enforces trust + ota::OtaContext& oc = ota::ota_ctx(); + 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 +410,31 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { } break; +#if defined(ENABLE_OTA) + case PAYLOAD_TYPE_OTA: { + // ALWAYS process every received 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 with a hop cap and the LOWEST priority, so OTA never competes with mesh traffic. + uint8_t n = pkt->getPathHashCount(); + if (!seen && pkt->isRouteFlood() && !pkt->isMarkedDoNotRetransmit() + && n < getOtaHopLimit() + && (n + 1) * pkt->getPathHashSize() <= MAX_PATH_SIZE + && 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 +745,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..0d7215d415 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -2,6 +2,16 @@ #include +#if defined(ENABLE_OTA) + // OTA-over-LoRa: lowest TX priority (selected only after all real traffic) + default hop cap. + #ifndef OTA_TX_PRIORITY + #define OTA_TX_PRIORITY 250 + #endif + #ifndef OTA_HOP_LIMIT_DEFAULT + #define OTA_HOP_LIMIT_DEFAULT 3 + #endif +#endif + namespace mesh { class GroupChannel { @@ -144,6 +154,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 the max hop count for forwarding OTA flood packets (default 3). */ + virtual uint8_t getOtaHopLimit() const { return OTA_HOP_LIMIT_DEFAULT; } + + // 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 +222,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..ca9d62644c 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,33 @@ 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.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 +115,16 @@ 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 + file.read((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 295 + file.read((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 296 + file.read((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 297 + file.read((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298 + file.read((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426 + // next: 428 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -125,6 +156,10 @@ 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_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys file.close(); } @@ -190,7 +225,12 @@ 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 + // next: 428 file.close(); } @@ -312,6 +352,21 @@ 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_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..8090f702bb 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -65,6 +65,12 @@ 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) }; class CommonCLICallbacks { @@ -129,6 +135,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/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp new file mode 100644 index 0000000000..b5cb340675 --- /dev/null +++ b/src/helpers/ota/OtaCli.cpp @@ -0,0 +1,396 @@ +#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 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 other hw / '?'. + 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 fit = (h->target_id == 0) ? "?" : "other hw"; + 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 { // show current policy + uint8_t af = c.manager.autofetch(); + sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%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.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 From 4680d1052f885b78d2538cf3d4bd57a0ff8ce0b7 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:06 +0200 Subject: [PATCH 07/15] ota: build/board enablement (platformio, build.sh, variants) + Python tooling (.mota reference, EndF/target/vector generators) --- .gitignore | 2 + build.sh | 7 + platformio.ini | 40 ++ tools/mota/README.md | 53 ++ tools/mota/gen_targets.py | 91 ++++ tools/mota/gen_vectors.py | 188 +++++++ tools/mota/motalib.py | 491 ++++++++++++++++++ tools/mota/pio_endf.py | 111 ++++ tools/mota/test_mota.py | 260 ++++++++++ variants/gat562_30s_mesh_kit/platformio.ini | 8 +- variants/gat562_mesh_evb_pro/platformio.ini | 8 +- .../gat562_mesh_tracker_pro/platformio.ini | 8 +- variants/gat562_mesh_watch13/platformio.ini | 8 +- variants/heltec_t114/platformio.ini | 8 +- variants/heltec_v3/platformio.ini | 7 + variants/muziworks_r1_neo/platformio.ini | 8 +- variants/rak4631/platformio.ini | 8 + variants/rak_wismesh_tag/platformio.ini | 8 +- 18 files changed, 1286 insertions(+), 28 deletions(-) create mode 100644 tools/mota/README.md create mode 100644 tools/mota/gen_targets.py create mode 100644 tools/mota/gen_vectors.py create mode 100644 tools/mota/motalib.py create mode 100644 tools/mota/pio_endf.py create mode 100644 tools/mota/test_mota.py 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/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/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/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/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/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] From 2092500d2b28d45184d972d2198f0b125cacdfdf Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:06 +0200 Subject: [PATCH 08/15] =?UTF-8?q?ota:=20motatool=20=E2=80=94=20portable=20?= =?UTF-8?q?C++=20.mota=20packager=20+=20relay=20(build/verify/inspect/serv?= =?UTF-8?q?e/keygen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/motatool/.gitignore | 1 + tools/motatool/CMakeLists.txt | 42 +++ tools/motatool/README.md | 114 ++++++ tools/motatool/src/crypto.cpp | 61 +++ tools/motatool/src/crypto.h | 28 ++ tools/motatool/src/input.cpp | 102 +++++ tools/motatool/src/input.h | 15 + tools/motatool/src/main.cpp | 501 +++++++++++++++++++++++++ tools/motatool/src/mota.cpp | 413 ++++++++++++++++++++ tools/motatool/src/mota.h | 83 ++++ tools/motatool/src/mota_format.h | 100 +++++ tools/motatool/src/serve.cpp | 209 +++++++++++ tools/motatool/src/serve.h | 68 ++++ tools/motatool/src/util.h | 77 ++++ tools/motatool/tests/test_motatool.cpp | 302 +++++++++++++++ 15 files changed, 2116 insertions(+) create mode 100644 tools/motatool/.gitignore create mode 100644 tools/motatool/CMakeLists.txt create mode 100644 tools/motatool/README.md create mode 100644 tools/motatool/src/crypto.cpp create mode 100644 tools/motatool/src/crypto.h create mode 100644 tools/motatool/src/input.cpp create mode 100644 tools/motatool/src/input.h create mode 100644 tools/motatool/src/main.cpp create mode 100644 tools/motatool/src/mota.cpp create mode 100644 tools/motatool/src/mota.h create mode 100644 tools/motatool/src/mota_format.h create mode 100644 tools/motatool/src/serve.cpp create mode 100644 tools/motatool/src/serve.h create mode 100644 tools/motatool/src/util.h create mode 100644 tools/motatool/tests/test_motatool.cpp 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..d934b8c4e8 --- /dev/null +++ b/tools/motatool/README.md @@ -0,0 +1,114 @@ +# 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. 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 over its USB serial (recursive; skips non-.mota; warns on corrupt) +$MT serve --dir ./motas --serial /dev/ttyUSB0 --baud 115200 -v +``` + +`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. +- **`SerialTransport` + `serve_serial()`** wrap it with the byte-stream framing (magic + XOR checksum + + resync) for the unreliable USB-UART link (`MotaSeederProto.h`). + +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..7e8bb0b6f5 --- /dev/null +++ b/tools/motatool/src/main.cpp @@ -0,0 +1,501 @@ +// 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 (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.\n" +"\n" +"USAGE\n" +" motatool serve --dir --serial [options]\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 --serial 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" +" --baud serial speed (default 115200).\n" +" --no-recursive serve only the top folder; don't descend into sub-folders.\n" +" --no-enable don't auto-send 'ota folder on'/'off' to the node (run them on its CLI yourself).\n" +" -v, --verbose log each request the node makes (COUNT / DESCRIBE / READ).\n" +"\n" +"Leave it running; press Ctrl-C to stop (it tells the node to stop relaying first). It shares the\n" +"same USB cable as the node's text console — the node only pulls bytes while actively fetching.\n" +"\n" +"EXAMPLE\n" +" motatool serve --dir ./motas --serial /dev/ttyUSB0 -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; } + if (!a.has("dir") || !a.has("serial")) { + std::cerr << "error: --dir and --serial 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"; + + SerialTransport t; + std::string e = t.open(a.get("serial"), std::atoi(a.get("baud", "115200").c_str())); + if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + + std::signal(SIGINT, on_sigint); + bool enable = !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 " << a.get("serial") << " @ " << a.get("baud", "115200") << " — Ctrl-C to stop\n"; + + SeederCore core(folder); + serve_serial(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..5812f0a9d7 --- /dev/null +++ b/tools/motatool/src/serve.cpp @@ -0,0 +1,209 @@ +#include "serve.h" +#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; +} + +// ---- serial 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_serial(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..7bff708e70 --- /dev/null +++ b/tools/motatool/src/serve.h @@ -0,0 +1,68 @@ +// 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; +}; + +// Serial framing loop: 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`. Runs until *stop becomes true. +void serve_serial(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; +} From e3dd6b417f0a9c7ba2a6516af4f8e7f1a4c95af0 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:06 +0200 Subject: [PATCH 09/15] ota: native unit tests (format/merkle/detools/apply vectors) --- test/mocks/SHA256.h | 101 +++- test/test_ota/mota_vectors.h | 122 ++++ test/test_ota/test_ota_core.cpp | 972 ++++++++++++++++++++++++++++++++ 3 files changed, 1189 insertions(+), 6 deletions(-) create mode 100644 test/test_ota/mota_vectors.h create mode 100644 test/test_ota/test_ota_core.cpp 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(); +} From 54c8b586c1e58b43a5b601fff5195a436fa35b8f Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 13:03:06 +0200 Subject: [PATCH 10/15] ota: protocol spec + user guide --- docs/ota_protocol.md | 584 +++++++++++++++++++++++++++++++++++++++++ docs/ota_user_guide.md | 229 ++++++++++++++++ 2 files changed, 813 insertions(+) create mode 100644 docs/ota_protocol.md create mode 100644 docs/ota_user_guide.md diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md new file mode 100644 index 0000000000..04620b3ee4 --- /dev/null +++ b/docs/ota_protocol.md @@ -0,0 +1,584 @@ +# 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. + +### 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 periodically): + +``` +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). The host is the self-contained C++ tool `tools/motatool/` (`motatool serve`, +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 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). Verified on hardware: a RAK4631 relays a host folder to a +Heltec V3 over one USB cable, 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 only because a shared USB-UART is an unreliable, unframed byte stream. Over a +framed/reliable link such as **BLE GATT** (e.g. an Android phone relaying a folder to a node), the same ops +carry over directly — a request characteristic write delivers `op + args` and the reply is a notification +of `status + payload`, with no magic/checksum needed. `motatool` reflects this split: a transport-free +`SeederCore` (the catalog logic) under a serial framing layer, so a BLE transport reuses the core verbatim. + +--- + +## 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..e078c6a9fe --- /dev/null +++ b/docs/ota_user_guide.md @@ -0,0 +1,229 @@ +# 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 # 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's USB port and the folder: + ``` + cmake -S tools/motatool -B tools/motatool/build && cmake --build tools/motatool/build + ./tools/motatool/build/motatool serve --dir ./my_firmware/ --serial /dev/ttyACM0 -v + ``` + It turns the relay on for you and then answers the node's requests. Your node now advertises those + updates to neighbours, who can `ota get` them like any other. (Details: [tools/motatool/README.md](../tools/motatool/README.md).) + +To turn it off, stop the daemon (or run `ota folder off` on the node). `ota folder` on its own lists what +your node is currently 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). From 280b9b1555e19a65e707ebf4a0171203330a3336 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 16:58:50 +0200 Subject: [PATCH 11/15] ota: serve & manage OTA over WiFi (motatool --tcp + ESP32 companion seeder/console) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the USB-serial folder relay to WiFi so an ESP32 companion can both serve .mota and be operated headlessly: - motatool `serve --tcp `: a TcpTransport sibling of the serial transport (default port 5001). SeederCore/Folder are reused unchanged — the COUNT/DESCRIBE/READ protocol is transport-agnostic. - ESP32 companion: a dedicated OTA seeder port (5001) for `serve --tcp`, plus an OTA text console on 5002 (`nc 5002` -> `ota status|ls|announce|...`, the same handle_ota_command CLI serial nodes have). Both run alongside the phone-app port (5000); all three coexist. - WiFi.setSleep(false): ESP32 STA mode's modem power-save periodically sleeps the modem/CPU and stalls the SX1262 SPI+DIO servicing, leaving LoRa deaf while WiFi is associated. Disabling it restores the radio (HW-validated: a V3 WiFi companion is then discovered over LoRa and discovers its peers). - docs: serving .mota over WiFi (protocol §10.2 + user guide). --- docs/ota_protocol.md | 32 ++++++----- docs/ota_user_guide.md | 16 ++++-- examples/companion_radio/main.cpp | 90 ++++++++++++++++++++++++++++++- tools/motatool/README.md | 13 +++-- tools/motatool/src/main.cpp | 75 +++++++++++++++++--------- tools/motatool/src/serve.cpp | 51 ++++++++++++++++-- tools/motatool/src/serve.h | 23 ++++++-- 7 files changed, 243 insertions(+), 57 deletions(-) diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md index 04620b3ee4..d4f867c886 100644 --- a/docs/ota_protocol.md +++ b/docs/ota_protocol.md @@ -496,11 +496,12 @@ blocks) and streams payload blocks from the source on demand; proofs are generat ### 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). The host is the self-contained C++ tool `tools/motatool/` (`motatool serve`, -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 binary frames coexist with the text -CLI/logs (resync on magic + checksum). Little-endian, XOR-checksummed: +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) @@ -516,16 +517,23 @@ 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). Verified on hardware: a RAK4631 relays a host folder to a -Heltec V3 over one USB cable, every block merkle-checked. +`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 only because a shared USB-UART is an unreliable, unframed byte stream. Over a -framed/reliable link such as **BLE GATT** (e.g. an Android phone relaying a folder to a node), the same ops -carry over directly — a request characteristic write delivers `op + args` and the reply is a notification -of `status + payload`, with no magic/checksum needed. `motatool` reflects this split: a transport-free -`SeederCore` (the catalog logic) under a serial framing layer, so a BLE transport reuses the core verbatim. +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. --- diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md index e078c6a9fe..bc0ba838ca 100644 --- a/docs/ota_user_guide.md +++ b/docs/ota_user_guide.md @@ -162,16 +162,22 @@ folder of firmware files to the mesh — without storing them itself. Useful for 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's USB port and the folder: +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 turns the relay on for you and then answers the node's requests. Your node now advertises those - updates to neighbours, who can `ota get` them like any other. (Details: [tools/motatool/README.md](../tools/motatool/README.md).) + 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 turn it off, stop the daemon (or run `ota folder off` on the node). `ota folder` on its own lists what -your node is currently offering. +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 diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index ef9b6bfca4..9a96b2100c 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -111,6 +111,79 @@ 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(); + 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; + 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 +281,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 +341,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/tools/motatool/README.md b/tools/motatool/README.md index d934b8c4e8..235d8af472 100644 --- a/tools/motatool/README.md +++ b/tools/motatool/README.md @@ -16,7 +16,7 @@ cross-checked byte-for-byte against the Python reference packager `tools/mota/mo | `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. Invalid files are warned about and skipped — one corrupt file never sinks the rest. | +| `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`. @@ -71,8 +71,9 @@ $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 over its USB serial (recursive; skips non-.mota; warns on corrupt) -$MT serve --dir ./motas --serial /dev/ttyUSB0 --baud 115200 -v +# 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: @@ -95,8 +96,10 @@ 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. -- **`SerialTransport` + `serve_serial()`** wrap it with the byte-stream framing (magic + XOR checksum + - resync) for the unreliable USB-UART link (`MotaSeederProto.h`). +- **`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 diff --git a/tools/motatool/src/main.cpp b/tools/motatool/src/main.cpp index 7e8bb0b6f5..8596b9f3ca 100644 --- a/tools/motatool/src/main.cpp +++ b/tools/motatool/src/main.cpp @@ -81,7 +81,7 @@ static void help_top() { " 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 (corrupt files are skipped).\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" @@ -194,29 +194,34 @@ static void help_inspect() { static void help_serve() { std::cout << -"motatool serve — serve a folder of .mota to a MeshCore node over USB serial.\n" +"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]\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 --serial 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" -" --baud serial speed (default 115200).\n" -" --no-recursive serve only the top folder; don't descend into sub-folders.\n" -" --no-enable don't auto-send 'ota folder on'/'off' to the node (run them on its CLI yourself).\n" -" -v, --verbose log each request the node makes (COUNT / DESCRIBE / READ).\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 (it tells the node to stop relaying first). It shares the\n" -"same USB cable as the node's text console — the node only pulls bytes while actively fetching.\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" -"EXAMPLE\n" -" motatool serve --dir ./motas --serial /dev/ttyUSB0 -v\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() { @@ -442,8 +447,9 @@ static int cmd_keygen(const Args& a) { static int cmd_serve(const Args& a) { if (a.has("help")) { help_serve(); return 0; } - if (!a.has("dir") || !a.has("serial")) { - std::cerr << "error: --dir and --serial are required\n\n"; help_serve(); return 2; + 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"); @@ -465,20 +471,39 @@ static int cmd_serve(const Args& a) { } if (n == 0) std::cerr << " (nothing valid to serve)\n"; - SerialTransport t; - std::string e = t.open(a.get("serial"), std::atoi(a.get("baud", "115200").c_str())); - if (!e.empty()) { std::cerr << "error: " << e << "\n"; return 1; } + // 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); - bool enable = !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 " << a.get("serial") << " @ " << a.get("baud", "115200") << " — Ctrl-C to stop\n"; + // 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_serial(t, core, verbose, - [](const std::string& l) { std::cout << " [dev] " << l << "\n"; }, &g_stop); + 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); } + if (enable) { t->write_str("ota folder off\r\n"); usleep(200000); } std::cout << "\nbye\n"; return 0; } diff --git a/tools/motatool/src/serve.cpp b/tools/motatool/src/serve.cpp index 5812f0a9d7..ef89ed9f4c 100644 --- a/tools/motatool/src/serve.cpp +++ b/tools/motatool/src/serve.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include "util.h" namespace fs = std::filesystem; @@ -138,7 +140,48 @@ bool SerialTransport::write(const uint8_t* p, size_t n) { return true; } -// ---- serial framing loop -------------------------------------------------------------------------- +// ---- 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; } @@ -162,9 +205,9 @@ static void send_response(Transport& t, uint8_t op, uint8_t status, const std::v t.write(frame.data(), frame.size()); } -void serve_serial(Transport& t, const SeederCore& core, bool verbose, - const std::function& devline, - const volatile bool* stop) { +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 = [&]() { diff --git a/tools/motatool/src/serve.h b/tools/motatool/src/serve.h index 7bff708e70..5177172460 100644 --- a/tools/motatool/src/serve.h +++ b/tools/motatool/src/serve.h @@ -59,10 +59,23 @@ class SerialTransport : public Transport { int fd_ = -1; }; -// Serial framing loop: 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`. Runs until *stop becomes true. -void serve_serial(Transport& t, const SeederCore& core, bool verbose, - const std::function& devline, - const volatile bool* stop); +// 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 From 4cb118b422074ef7f6e6dd0e778375bdad136219 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 16:59:09 +0200 Subject: [PATCH 12/15] ota: faster post-boot beacon + periodic re-announce + clearer `ota ls` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Discovery was hard to use — a node only advertised at boot, so a peer that ran `ota ls` minutes later saw "no neighbours". - First self-advert ~8s after boot, then a short burst (~1 min), then re-announce at a random 3-10 min interval so a long-running node stays discoverable without all nodes beaconing in lockstep. The beacon is tiny, lowest-priority and duty-gated, so a few-minute cadence is cheap. - `ota ls` now shows the raw target id (`hw XXXXXXXX`) when the env name is not in this build's OtaTargets.h table, instead of a blank "[other hw]". --- src/Mesh.cpp | 23 ++++++++++++++++------- src/helpers/ota/OtaCli.cpp | 9 ++++++--- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 6443c24415..5423fa3d3b 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -5,16 +5,22 @@ #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 30000UL // first self-advert ~30 s after boot (let the node settle) +#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 45000UL // spacing during the boot burst (~3 min total), then ... +#define OTA_ANNOUNCE_BURST_MS 20000UL // spacing during the boot burst (~1 min total), then ... #endif -#ifndef OTA_ANNOUNCE_INTERVAL_MS -#define OTA_ANNOUNCE_INTERVAL_MS 86400000UL // ... every 24 h — all lowest priority, duty-gated +// ... then re-announce at a RANDOM interval in [MIN, MAX] so a long-running node stays discoverable +// (a fresh `ota ls` neighbour sees it within minutes, not at boot only) without all nodes beaconing in +// lockstep. The beacon is tiny + lowest-priority + duty-gated, so a few-minute cadence is cheap. +#ifndef OTA_ANNOUNCE_INTERVAL_MIN_MS +#define OTA_ANNOUNCE_INTERVAL_MIN_MS 180000UL // 3 min +#endif +#ifndef OTA_ANNOUNCE_INTERVAL_MAX_MS +#define OTA_ANNOUNCE_INTERVAL_MAX_MS 600000UL // 10 min #endif #endif @@ -88,9 +94,12 @@ void Mesh::loop() { // beacon (announce) advertises our served set and peers can QUERY + fetch it. if (!oc.serving) oc.serving = ota::ota_serve_self(oc, 0); oc.manager.announce(); - // boot burst (a few closely-spaced adverts so a co-booting peer catches one), then settle to daily - _next_ota_announce = futureMillis(_ota_announce_count < OTA_ANNOUNCE_BURST - ? OTA_ANNOUNCE_BURST_MS : OTA_ANNOUNCE_INTERVAL_MS); + // boot burst (a few closely-spaced adverts so a co-booting peer catches one), then a random + // few-minute cadence so a node stays discoverable long after boot (not just at boot). + uint32_t gap = (_ota_announce_count < OTA_ANNOUNCE_BURST) + ? OTA_ANNOUNCE_BURST_MS + : _rng->nextInt(OTA_ANNOUNCE_INTERVAL_MIN_MS, OTA_ANNOUNCE_INTERVAL_MAX_MS); + _next_ota_announce = futureMillis(gap); if (_ota_announce_count < 250) _ota_announce_count++; } { // auto-install (once per COMPLETE fetch): only signed images, and apply_fetched enforces trust diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index b5cb340675..e35955b550 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -141,13 +141,16 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board 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 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 other hw / '?'. + // 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 fit = (h->target_id == 0) ? "?" : "other hw"; + 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]" : ""); From 0a1cf2b23181e883a632043b6e6b09463667531f Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Mon, 29 Jun 2026 16:59:09 +0200 Subject: [PATCH 13/15] ota: enable OTA on the nRF52 variants the OTAFIX bootloader supports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch these existing variants from `nrf52_base` to `rak4631_hw` (which adds ENABLE_OTA + the in-place flash store + the EndF post-build hook), so they get OTA-over-LoRa. Scope is limited to variants already covered by the Adafruit_nRF52_Bootloader_OTAFIX in-place apply — no new variants are added. OtaTargets.h is regenerated to include their target ids. --- src/helpers/ota/OtaTargets.h | 117 ++++++++++++++++++-- variants/ikoka_handheld_nrf/platformio.ini | 8 +- variants/ikoka_nano_nrf/platformio.ini | 6 +- variants/ikoka_stick_nrf/platformio.ini | 6 +- variants/lilygo_techo/platformio.ini | 8 +- variants/lilygo_techo_card/platformio.ini | 8 +- variants/lilygo_techo_lite/platformio.ini | 16 +-- variants/promicro/platformio.ini | 8 +- variants/t1000-e/platformio.ini | 6 +- variants/thinknode_m1/platformio.ini | 8 +- variants/thinknode_m3/platformio.ini | 8 +- variants/thinknode_m6/platformio.ini | 8 +- variants/wio-tracker-l1-eink/platformio.ini | 8 +- variants/wio-tracker-l1/platformio.ini | 8 +- variants/xiao_nrf52/platformio.ini | 8 +- 15 files changed, 165 insertions(+), 66 deletions(-) diff --git a/src/helpers/ota/OtaTargets.h b/src/helpers/ota/OtaTargets.h index e7ec61cef6..9fded575fe 100644 --- a/src/helpers/ota/OtaTargets.h +++ b/src/helpers/ota/OtaTargets.h @@ -2,7 +2,7 @@ #include // AUTO-GENERATED by tools/mota/gen_targets.py — do not edit by hand. -// 319 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32) +// 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. @@ -142,6 +142,58 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 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" }, @@ -205,14 +257,29 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 0xcea95e20, "Meshadventurer_sx1268_terminal_chat" }, { 0xd45277ff, "Meshimi_companion_radio_ble_" }, { 0x2cad9a9d, "Meshimi_repeater_" }, - { 0xf1c1641c, "nibble_screen_connect_companion_radio_ble" }, - { 0x8dcc5d89, "nibble_screen_connect_companion_radio_usb" }, - { 0x6139a846, "nibble_screen_connect_companion_radio_wifi" }, - { 0xc0dbcae4, "nibble_screen_connect_kiss_modem" }, - { 0xcd0bdd06, "nibble_screen_connect_repeater" }, - { 0x9ecb53b1, "nibble_screen_connect_repeater_bridge_espnow" }, - { 0x82f6d321, "nibble_screen_connect_room_server" }, - { 0xb10261c8, "nibble_screen_connect_terminal_chat" }, + { 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" }, @@ -262,6 +329,11 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 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" }, @@ -284,6 +356,11 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 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" }, @@ -293,6 +370,11 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 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" }, @@ -302,8 +384,20 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 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" }, @@ -313,6 +407,11 @@ inline const char* ota_target_env_name(uint32_t target_id) { { 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" }, 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/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/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] From 9ff1c883c21a94b337368a94ce046787ac5ee40d Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Tue, 30 Jun 2026 14:29:38 +0200 Subject: [PATCH 14/15] ota: configurable advertise interval (default 24h) + advertise on served-set change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The discovery beacon previously re-announced at a random 3-10 min interval. Replace that with a fixed, user-configurable cadence: - OtaManager::advert_mins() — re-advertise every N minutes after the boot burst; 0 disables periodic re-advertise (boot burst only). Default 24h. - Persisted in NodePrefs (CommonCLI) and runtime-tunable: `ota config advert ` (0..10080; 0 = off), and shown in `ota config`. - When periodic advert is disabled, the scheduler still re-checks the config on a slow timer, so a later `ota config advert ` takes effect live. Also advertise immediately whenever the served set changes — when a motatool folder is attached to / detached from the ESP32 WiFi seeder — so peers learn about newly-available firmware without waiting for the next interval (the `ota folder` serial path already announced on attach). Docs: protocol beacon-cadence note + user-guide `ota config advert`. --- docs/ota_protocol.md | 5 +++- docs/ota_user_guide.md | 3 +++ examples/companion_radio/main.cpp | 2 ++ src/Mesh.cpp | 38 ++++++++++++++++--------------- src/helpers/CommonCLI.cpp | 10 ++++++-- src/helpers/CommonCLI.h | 1 + src/helpers/ota/OtaCli.cpp | 9 ++++++-- src/helpers/ota/OtaManager.h | 10 ++++++++ 8 files changed, 55 insertions(+), 23 deletions(-) diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md index d4f867c886..cc5583feb9 100644 --- a/docs/ota_protocol.md +++ b/docs/ota_protocol.md @@ -310,7 +310,10 @@ Message types: Because a node may serve **many** mOTAs (its own firmware plus an external folder — §10), discovery is split so the periodic beacon stays tiny regardless of catalog size: -**Tier 1 — `OTA_ADV` beacon** (10 bytes, constant, flooded periodically): +**Tier 1 — `OTA_ADV` beacon** (10 bytes, constant). Flooded as a short burst at boot, then every +`advert_mins` minutes (default 24h; runtime-tunable via `ota config advert`, `0` disables the periodic +re-advertise). It is also emitted immediately whenever the served set changes (e.g. a `motatool` folder is +attached/detached), so peers learn about newly-available firmware without waiting for the next interval: ``` seeder_id[4] advertiser node id = pubkey[0:4]; the QUERY address + distinct-source id diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md index bc0ba838ca..7d4434ee66 100644 --- a/docs/ota_user_guide.md +++ b/docs/ota_user_guide.md @@ -129,6 +129,9 @@ ota config autofetch off # back to manual (default) ota config autoinstall trusted # auto-INSTALL a downloaded update IF it's signed by a key you trust ota config autoinstall off # never auto-install (default) +ota config advert 1440 # re-advertise this node every N minutes (default 1440 = 24h) +ota config advert 0 # disable periodic re-advertise (still advertises briefly at boot) + ota config # show the current settings ``` diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index 9a96b2100c..6844724b6d 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -130,6 +130,7 @@ void halt() { if (ota_seeder_client && ota_seeder_client.connected()) return; // still serving the current client if (ota_seeder_attached) { // previous client just disconnected mesh::ota::ota_ctx().detach_folder(); + mesh::ota::ota_ctx().manager.announce(); // served set shrank back to our own fw -> re-advertise ota_seeder_attached = false; WIFI_DEBUG_PRINTLN("OTA seeder: client disconnected, relay stopped"); } @@ -138,6 +139,7 @@ void halt() { ota_seeder_client = c; // rebind the persistent Stream to it if (mesh::ota::ota_ctx().manager.add_source(&ota_seeder_source)) { ota_seeder_attached = true; + mesh::ota::ota_ctx().manager.announce(); // new served set -> advertise the folder's fw to peers WIFI_DEBUG_PRINTLN("OTA seeder: client connected, relaying its folder"); } else { ota_seeder_client.stop(); // no free source slot diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 5423fa3d3b..167a314ba3 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -13,14 +13,12 @@ #ifndef OTA_ANNOUNCE_BURST_MS #define OTA_ANNOUNCE_BURST_MS 20000UL // spacing during the boot burst (~1 min total), then ... #endif -// ... then re-announce at a RANDOM interval in [MIN, MAX] so a long-running node stays discoverable -// (a fresh `ota ls` neighbour sees it within minutes, not at boot only) without all nodes beaconing in -// lockstep. The beacon is tiny + lowest-priority + duty-gated, so a few-minute cadence is cheap. -#ifndef OTA_ANNOUNCE_INTERVAL_MIN_MS -#define OTA_ANNOUNCE_INTERVAL_MIN_MS 180000UL // 3 min -#endif -#ifndef OTA_ANNOUNCE_INTERVAL_MAX_MS -#define OTA_ANNOUNCE_INTERVAL_MAX_MS 600000UL // 10 min +// ... then re-announce at a FIXED cadence so a long-running node stays discoverable (a fresh `ota ls` +// neighbour eventually sees it, not at boot only). The cadence is OtaManager::advert_mins() minutes (default +// 24h, runtime-tunable via `ota config advert` + persisted; 0 = disabled = boot burst only). The beacon is +// tiny + lowest-priority + duty-gated, so even a frequent cadence is cheap. +#ifndef OTA_ANNOUNCE_DISABLED_POLL_MS +#define OTA_ANNOUNCE_DISABLED_POLL_MS 600000UL // when periodic advert is off, re-check config every 10 min #endif #endif @@ -90,17 +88,21 @@ void Mesh::loop() { } if (millisHasNowPassed(_next_ota_announce)) { // auto-advertise so peers discover us (tiny beacon) ota::OtaContext& oc = ota::ota_ctx(); - // To be discoverable as a source of our OWN firmware, set up flash-backed self-serve once; then the - // beacon (announce) advertises our served set and peers can QUERY + fetch it. - if (!oc.serving) oc.serving = ota::ota_serve_self(oc, 0); - oc.manager.announce(); - // boot burst (a few closely-spaced adverts so a co-booting peer catches one), then a random - // few-minute cadence so a node stays discoverable long after boot (not just at boot). - uint32_t gap = (_ota_announce_count < OTA_ANNOUNCE_BURST) - ? OTA_ANNOUNCE_BURST_MS - : _rng->nextInt(OTA_ANNOUNCE_INTERVAL_MIN_MS, OTA_ANNOUNCE_INTERVAL_MAX_MS); + bool in_burst = _ota_announce_count < OTA_ANNOUNCE_BURST; + uint32_t mins = oc.manager.advert_mins(); // periodic cadence in minutes; 0 = disabled (boot burst only) + if (in_burst || mins != 0) { + // To be discoverable as a source of our OWN firmware, set up flash-backed self-serve once; then the + // beacon (announce) advertises our served set and peers can QUERY + fetch it. + if (!oc.serving) oc.serving = ota::ota_serve_self(oc, 0); + oc.manager.announce(); + if (_ota_announce_count < 250) _ota_announce_count++; + } + // Re-arm: tight spacing during the boot burst; afterwards the fixed cadence (default 24h). When periodic + // advert is disabled (0), re-check on a slow timer so a later `ota config advert ` takes effect live. + uint32_t gap = in_burst ? OTA_ANNOUNCE_BURST_MS + : (mins != 0) ? mins * 60000UL + : OTA_ANNOUNCE_DISABLED_POLL_MS; _next_ota_announce = futureMillis(gap); - if (_ota_announce_count < 250) _ota_announce_count++; } { // auto-install (once per COMPLETE fetch): only signed images, and apply_fetched enforces trust ota::OtaContext& oc = ota::ota_ctx(); diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index ca9d62644c..163971bf18 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -52,6 +52,7 @@ void CommonCLI::syncOtaConfigFromPrefs() { mesh::ota::OtaContext& c = mesh::ota::ota_ctx(); c.manager.set_autofetch(_prefs->ota_autofetch); c.manager.set_checkpoint_blocks(_prefs->ota_checkpoint_blocks); + c.manager.set_advert_mins(_prefs->ota_advert_interval); c.autoinstall = _prefs->ota_autoinstall; c.allow.clear(); for (uint8_t i = 0; i < _prefs->ota_signer_count && i < MAX_OTA_SIGNERS; i++) @@ -119,12 +120,14 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { // defaults: a short file makes the reads below no-ops (read returns 0 bytes, values unchanged). _prefs->ota_autofetch = 0; _prefs->ota_autoinstall = 0; _prefs->ota_signer_count = 0; _prefs->ota_checkpoint_blocks = 4; // = OTA_CHECKPOINT_BLOCKS; older prefs lack it -> stays at default + _prefs->ota_advert_interval = 1440; // = OTA_ADVERT_INTERVAL_MINS (24h); older prefs lack it -> stays default file.read((uint8_t *)&_prefs->ota_autofetch, sizeof(_prefs->ota_autofetch)); // 295 file.read((uint8_t *)&_prefs->ota_autoinstall, sizeof(_prefs->ota_autoinstall)); // 296 file.read((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 297 file.read((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298 file.read((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426 - // next: 428 + file.read((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428 + // next: 430 // sanitise bad pref values _prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f); @@ -159,6 +162,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _prefs->ota_autofetch = constrain(_prefs->ota_autofetch, 0, 2); _prefs->ota_autoinstall = constrain(_prefs->ota_autoinstall, 0, 1); if (_prefs->ota_checkpoint_blocks > 4096) _prefs->ota_checkpoint_blocks = 4; // 0=never; cap absurd + if (_prefs->ota_advert_interval > 10080) _prefs->ota_advert_interval = 1440; // 0=off; cap at 7 days if (_prefs->ota_signer_count > 4) _prefs->ota_signer_count = 0; // corrupt count -> drop keys file.close(); @@ -230,7 +234,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { file.write((uint8_t *)&_prefs->ota_signer_count, sizeof(_prefs->ota_signer_count)); // 297 file.write((uint8_t *)_prefs->ota_signers, sizeof(_prefs->ota_signers)); // 298 file.write((uint8_t *)&_prefs->ota_checkpoint_blocks, sizeof(_prefs->ota_checkpoint_blocks)); // 426 - // next: 428 + file.write((uint8_t *)&_prefs->ota_advert_interval, sizeof(_prefs->ota_advert_interval)); // 428 + // next: 430 file.close(); } @@ -359,6 +364,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re mesh::ota::OtaContext& c = mesh::ota::ota_ctx(); _prefs->ota_autofetch = c.manager.autofetch(); _prefs->ota_checkpoint_blocks = c.manager.checkpoint_blocks(); + _prefs->ota_advert_interval = c.manager.advert_mins(); _prefs->ota_autoinstall = c.autoinstall; _prefs->ota_signer_count = c.allow.count(); for (uint8_t i = 0; i < c.allow.count() && i < MAX_OTA_SIGNERS; i++) diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index 8090f702bb..e0359fc1f6 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -71,6 +71,7 @@ struct NodePrefs { // persisted to file uint8_t ota_signer_count; // # of allowlisted signer pubkeys below uint8_t ota_signers[4][32]; // trusted Ed25519 signer pubkeys (== MAX_OTA_SIGNERS) uint16_t ota_checkpoint_blocks; // resume checkpoint cadence (blocks); 0=never. Default 4 (runtime-tunable) + uint16_t ota_advert_interval; // OTA beacon re-advertise cadence (mins); 0=off. Default 1440 (24h, tunable) }; class CommonCLICallbacks { diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index e35955b550..465f42ba1f 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -272,12 +272,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board if (n < 0 || n > 4096) { strcpy(reply, "ERR usage: ota config checkpoint <0..4096> (blocks; 0=never)"); return true; } c.manager.set_checkpoint_blocks((uint16_t)n); c.config_dirty = true; sprintf(reply, "OK checkpoint every %ld blocks (saved)%s", n, n == 0 ? " — periodic resume disabled" : ""); + } else if (strncmp(p, "advert ", 7) == 0) { // beacon re-advertise cadence (minutes; 0=disable) + long m = atol(p + 7); + if (m < 0 || m > 10080) { strcpy(reply, "ERR usage: ota config advert <0..10080> (minutes; 0=disable)"); return true; } + c.manager.set_advert_mins((uint16_t)m); c.config_dirty = true; + sprintf(reply, "OK re-advertise every %ld min (saved)%s", m, m == 0 ? " — periodic advert disabled" : ""); } else { // show current policy uint8_t af = c.manager.autofetch(); - sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u keys=%u (persisted)", + sprintf(reply, "ota config: autofetch=%s autoinstall=%s checkpoint=%u advert=%umin keys=%u (persisted)", af == OtaManager::AUTOFETCH_ANY ? "any" : af == OtaManager::AUTOFETCH_SIGNED ? "signed" : "off", c.autoinstall == OtaContext::AUTOINSTALL_TRUSTED ? "trusted" : "off", - (unsigned)c.manager.checkpoint_blocks(), (unsigned)c.allow.count()); + (unsigned)c.manager.checkpoint_blocks(), (unsigned)c.manager.advert_mins(), (unsigned)c.allow.count()); } // ---- trusted signer allowlist (security config; persisted): `ota key add|rm ` / `ota key` lists ---- diff --git a/src/helpers/ota/OtaManager.h b/src/helpers/ota/OtaManager.h index 3428b453d8..79f22ebb47 100644 --- a/src/helpers/ota/OtaManager.h +++ b/src/helpers/ota/OtaManager.h @@ -37,6 +37,9 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) #ifndef OTA_CHECKPOINT_BLOCKS #define OTA_CHECKPOINT_BLOCKS 4 // persist progress (store.checkpoint) every N committed blocks (resume) #endif +#ifndef OTA_ADVERT_INTERVAL_MINS +#define OTA_ADVERT_INTERVAL_MINS 1440 // re-advertise our served set every N minutes after the boot burst (24h) +#endif #ifndef OTA_MF_FRAG #define OTA_MF_FRAG 176 // manifest bytes per OTA_MANIFEST fragment (<= MAX_PACKET_PAYLOAD - header) #endif @@ -204,6 +207,12 @@ class OtaManager { // committed blocks. 0 = never (resume only from a finalized container). Default OTA_CHECKPOINT_BLOCKS. void set_checkpoint_blocks(uint16_t n) { _checkpoint_blocks = n; } uint16_t checkpoint_blocks() const { return _checkpoint_blocks; } + + // Beacon re-advertise cadence in MINUTES (runtime-tunable, persisted in NodePrefs): after the boot burst, + // re-send the discovery beacon every N minutes so a long-running node stays discoverable. 0 = disabled + // (boot burst only). Default OTA_ADVERT_INTERVAL_MINS (24h). + void set_advert_mins(uint16_t m) { _advert_mins = m; } + uint16_t advert_mins() const { return _advert_mins; } bool fetched_is_signed() const { return (_fflags & MFLAG_SIGNED) != 0; } // flags of the fetched manifest // This node's id (pubkey[0:4]), stamped into adverts we send so receivers can count distinct seeders. @@ -325,6 +334,7 @@ class OtaManager { uint8_t _seeder_id[4] = {0,0,0,0}; // our node id (pubkey[0:4]) for advert seeder counting uint8_t _autofetch = AUTOFETCH_OFF; // auto-fetch policy (persisted in NodePrefs) uint16_t _checkpoint_blocks = OTA_CHECKPOINT_BLOCKS; // resume checkpoint cadence (persisted) + uint16_t _advert_mins = OTA_ADVERT_INTERVAL_MINS; // beacon re-advertise cadence, minutes; 0=off (persisted) uint8_t _fflags = 0; // flags of the manifest currently being fetched // multi-fragment reassembly of the current block (per-block 2-phase: fetch data, then its proof) uint32_t _reasm_block = NO_BLOCK; // block being reassembled / awaiting proof (none) From 259802b70645a66e512f9a454244f5b1b43b71d6 Mon Sep 17 00:00:00 2001 From: Valentin Kivachuk Burda Date: Tue, 30 Jun 2026 20:35:48 +0200 Subject: [PATCH 15/15] ota: configurable hop limit (`ota config hops`) + accept-gate + forward RAM guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bound OTA-over-LoRa duty cycle across repeaters with one runtime-tunable, persisted limit (OtaManager::max_hops, `ota config hops <0..8>`, default 3): - Accept-gate: a node ignores OTA that arrived from more than max_hops hops away (neither processes nor relays it). 0 = direct only. - Forward-cap: relay a flood only while still under max_hops, appending this node's path-hash (hop count increments like the mesh flood routing). - RAM guard: relay an OTA flood only while more than OTA_FWD_MIN_FREE packet- pool slots stay free, so heavy OTA (best-effort, lowest-priority) can never monopolise the shared pool and starve real traffic — a dropped relay is re-requested by the source. Persisted in NodePrefs (CommonCLI) and shown in `ota config`. Docs updated. --- docs/ota_protocol.md | 5 +++++ docs/ota_user_guide.md | 3 +++ src/Mesh.cpp | 22 ++++++++++++++++------ src/Mesh.h | 13 ++++++++----- src/helpers/CommonCLI.cpp | 10 ++++++++-- src/helpers/CommonCLI.h | 1 + src/helpers/ota/OtaCli.cpp | 10 ++++++++-- src/helpers/ota/OtaManager.h | 10 ++++++++++ 8 files changed, 59 insertions(+), 15 deletions(-) diff --git a/docs/ota_protocol.md b/docs/ota_protocol.md index cc5583feb9..4e7f5a28ea 100644 --- a/docs/ota_protocol.md +++ b/docs/ota_protocol.md @@ -304,6 +304,11 @@ Message types: - **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 diff --git a/docs/ota_user_guide.md b/docs/ota_user_guide.md index 7d4434ee66..0235be232f 100644 --- a/docs/ota_user_guide.md +++ b/docs/ota_user_guide.md @@ -132,6 +132,9 @@ 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 ``` diff --git a/src/Mesh.cpp b/src/Mesh.cpp index 167a314ba3..788c0f0cf5 100644 --- a/src/Mesh.cpp +++ b/src/Mesh.cpp @@ -31,6 +31,10 @@ void Mesh::otaSendAdapter(void* ctx, const uint8_t* msg, uint16_t len, bool /*fl 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() { @@ -423,21 +427,27 @@ DispatcherAction Mesh::onRecvPacket(Packet* pkt) { #if defined(ENABLE_OTA) case PAYLOAD_TYPE_OTA: { - // ALWAYS process every received 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. + 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 with a hop cap and the LOWEST priority, so OTA never competes with mesh traffic. - uint8_t n = pkt->getPathHashCount(); + // 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); diff --git a/src/Mesh.h b/src/Mesh.h index 0d7215d415..04fbcfe111 100644 --- a/src/Mesh.h +++ b/src/Mesh.h @@ -3,12 +3,15 @@ #include #if defined(ENABLE_OTA) - // OTA-over-LoRa: lowest TX priority (selected only after all real traffic) + default hop cap. + // 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 - #ifndef OTA_HOP_LIMIT_DEFAULT - #define OTA_HOP_LIMIT_DEFAULT 3 + // 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 @@ -161,8 +164,8 @@ class Mesh : public Dispatcher { */ virtual void onOtaRecv(Packet* packet) { } - /** \returns the max hop count for forwarding OTA flood packets (default 3). */ - virtual uint8_t getOtaHopLimit() const { return OTA_HOP_LIMIT_DEFAULT; } + /** \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. diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index 163971bf18..56201e99ff 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -53,6 +53,7 @@ void CommonCLI::syncOtaConfigFromPrefs() { 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++) @@ -121,13 +122,15 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _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 - // next: 430 + 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); @@ -163,6 +166,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) { _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(); @@ -235,7 +239,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) { 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 - // next: 430 + file.write((uint8_t *)&_prefs->ota_max_hops, sizeof(_prefs->ota_max_hops)); // 430 + // next: 431 file.close(); } @@ -365,6 +370,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re _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++) diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index e0359fc1f6..bf9a1861fc 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -72,6 +72,7 @@ struct NodePrefs { // persisted to file 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 { diff --git a/src/helpers/ota/OtaCli.cpp b/src/helpers/ota/OtaCli.cpp index 465f42ba1f..f514dea219 100644 --- a/src/helpers/ota/OtaCli.cpp +++ b/src/helpers/ota/OtaCli.cpp @@ -277,12 +277,18 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board 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 keys=%u (persisted)", + 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.allow.count()); + (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 ---- diff --git a/src/helpers/ota/OtaManager.h b/src/helpers/ota/OtaManager.h index 79f22ebb47..a0c8cf0419 100644 --- a/src/helpers/ota/OtaManager.h +++ b/src/helpers/ota/OtaManager.h @@ -40,6 +40,9 @@ typedef bool (*ServeReadFn)(void* ctx, uint32_t off, uint8_t* buf, uint32_t len) #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 @@ -213,6 +216,12 @@ class OtaManager { // (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. @@ -335,6 +344,7 @@ class OtaManager { 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)