Support 2 KiB DEFLATE OTA transport
This commit is contained in:
1 parent
066f1abca6
commit
e2da2701da
32 files changed
+3877
-357
No files matched your search
@@ -44,7 +44,9 @@ static bool parse_manifest_fields(ByteReader& r, MotaManifest& out) {
|
||||
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();
|
||||
const uint32_t block_size = out.block_size();
|
||||
out.block_count = out.payload_size / block_size +
|
||||
(out.payload_size % block_size != 0 ? 1u : 0u);
|
||||
if (out.is_bootloader()) {
|
||||
static const uint8_t zero_base[8] = {0};
|
||||
if (out.fw_version == 0 || out.codec_id != CODEC_FULL || out.image_size != 0xA000u ||
|
||||
|
||||
+20
-10
@@ -212,7 +212,9 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
uint8_t dig[4]; c.manager.servedDigest(dig);
|
||||
char dighx[9]; mesh::Utils::toHex(dighx, dig, 4);
|
||||
snprintf(reply, 160,
|
||||
"OTA seeder | install:disabled | folder:%s | serving:%u dg=%s | target:00000000 (source only)",
|
||||
"OTA seeder | install:disabled | target:00000000 | maxblk:%u | folder:%s | "
|
||||
"serving:%u dg=%s (source only)",
|
||||
(unsigned)ota_max_block_capability(),
|
||||
c.folder_active ? c.folder_dest_info : "not connected",
|
||||
(unsigned)c.manager.servedCount(), dighx);
|
||||
#else
|
||||
@@ -252,22 +254,27 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
#endif
|
||||
const uint8_t last_rc = ota_bootloader_last_rc();
|
||||
const char* rc_name = ota_nrf52_boot_update_result(last_rc) ? "blup" : "blrc";
|
||||
// Keep the fixed-width parser/diagnostic fields ahead of the variable download text and optional
|
||||
// human target name. hw is bounded to 32 bytes, so target + the complete blup/blrc token always fit
|
||||
// in the 160-byte reply; snprintf may truncate only the descriptive tail.
|
||||
// Keep the fixed-width parser/capability fields ahead of the variable download text and optional
|
||||
// human target name. hw is bounded to 32 bytes, so target, maxblk, and the complete blup/blrc token
|
||||
// always fit in the 160-byte reply; snprintf may truncate only the descriptive tail.
|
||||
snprintf(reply, 160,
|
||||
"OTA | this fw %s (%uK) hw=%s | target:%08X | bl:%s %s:%02X | %s | "
|
||||
"OTA | this fw %s (%uK) hw=%s | target:%08X | maxblk:%u | bl:%s %s:%02X | %s | "
|
||||
"serving:%s (%u) | keys:%u | env:%s",
|
||||
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw,
|
||||
(unsigned)c.manager.target(), bl_state, rc_name, last_rc, dl,
|
||||
(unsigned)c.manager.target(), (unsigned)ota_max_block_capability(),
|
||||
bl_state, rc_name, last_rc, dl,
|
||||
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
||||
(unsigned)c.allow.count(), tenv ? tenv : "?");
|
||||
#else
|
||||
// target and maxblk precede the variable download text so remote-admin truncation cannot hide the
|
||||
// package-geometry preflight fields.
|
||||
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,
|
||||
"OTA | this fw %s (%uK) hw=%s | target:%08X | maxblk:%u | %s | "
|
||||
"serving:%s (%u) | keys:%u | env:%s",
|
||||
selfhx, (unsigned)((s ? fi.image_len : 0) / 1024), hw,
|
||||
(unsigned)c.manager.target(), (unsigned)ota_max_block_capability(), dl,
|
||||
c.serving ? "on" : "off", (unsigned)c.manager.servedCount(),
|
||||
(unsigned)c.allow.count(), (unsigned)c.manager.target(), tenv ? tenv : "?");
|
||||
(unsigned)c.allow.count(), tenv ? tenv : "?");
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -720,7 +727,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
|
||||
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);
|
||||
// maxblk is before the optional bootloader diagnostics so it survives the 160-byte remote reply cap.
|
||||
int n = snprintf(reply, 160, "self body=%u image=%u base_hash=%s | maxblk:%u",
|
||||
(unsigned)fi.body_len, (unsigned)fi.image_len, hx,
|
||||
(unsigned)ota_max_block_capability());
|
||||
#if defined(NRF52_PLATFORM)
|
||||
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store.
|
||||
const OtaBlCaps& bl = c.bootloaderAppCaps(); // cached (flash scanned once)
|
||||
|
||||
@@ -128,7 +128,7 @@ struct OtaContext {
|
||||
// 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).
|
||||
// count (the manager's fixed 4 KiB scratch covers <=1024 blocks, about 2 MiB at the new default).
|
||||
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)
|
||||
|
||||
@@ -119,7 +119,11 @@ inline bool ota_is_transfer_message(uint8_t type) {
|
||||
}
|
||||
}
|
||||
|
||||
static const uint16_t OTA_DEFAULT_BLOCK_SIZE = 1024;
|
||||
// New application containers use 2 KiB Merkle/transport blocks. Parsers still
|
||||
// accept the deployed 1 KiB geometry, while bootloader-update containers keep
|
||||
// their separate, fixed 1 KiB contract.
|
||||
static const uint16_t OTA_DEFAULT_BLOCK_SIZE = 2048;
|
||||
static const uint16_t OTA_MAX_APPLICATION_BLOCK_SIZE = 2048;
|
||||
static const uint8_t OTA_DEFAULT_HOP_LIMIT = 3;
|
||||
|
||||
} // namespace ota
|
||||
|
||||
@@ -67,10 +67,11 @@ bool OtaManager::serve(const uint8_t* mota, uint32_t len) {
|
||||
uint8_t* scratch = ensureScratch();
|
||||
if (!scratch) return false;
|
||||
if (!mota_parse(mota, len, _view0.m)) return false;
|
||||
if (_view0.m.block_size() == 0 || _view0.m.block_size() > OTA_MAX_BLOCK) 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.read_deflated = nullptr; _view0.deflate_ctx = nullptr;
|
||||
_view0.scratch = scratch; _view0.scratch_sz = OTA_PROOFGEN_SCRATCH; // <=1024 blocks (RAM .mota is small)
|
||||
_view0.scratch = scratch; _view0.scratch_sz = OTA_PROOFGEN_SCRATCH; // <=1024 leaves/blocks
|
||||
_view0.valid = true;
|
||||
clearPendingEgress();
|
||||
if (_n_src_obj) refresh_sources(); else registerSelfEntry();
|
||||
@@ -82,6 +83,7 @@ bool OtaManager::serve_self(const uint8_t* manifest, uint16_t mfl, const uint8_t
|
||||
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
|
||||
if (_view0.m.block_size() == 0 || _view0.m.block_size() > OTA_MAX_BLOCK) return false;
|
||||
_view0.m.manifest_start = manifest;
|
||||
_view0.m.leaves = leaves; // pre-computed, caller-owned (heap)
|
||||
_view0.m.payload = nullptr; // read on demand via `read`
|
||||
@@ -417,7 +419,8 @@ void OtaManager::handleGetLeaves(const uint8_t* m, uint16_t n) {
|
||||
}
|
||||
|
||||
// Smallest mask covering `nf` fragments: bit k set for k in [0, nf). Caps at 16 (matches each pipeline
|
||||
// slot mask and the manifest reassembly), which bounds a block at 16 fragments - our 1 KB blocks are 7.
|
||||
// slot mask and the manifest reassembly), which bounds a block at 16 fragments. A 2 KiB block is 13 legacy
|
||||
// fragments or 12 v2 fragments.
|
||||
static inline uint16_t frag_full_mask(uint32_t nf) {
|
||||
return (nf >= 16) ? 0xFFFFu : (uint16_t)((1u << nf) - 1);
|
||||
}
|
||||
@@ -426,14 +429,15 @@ static inline uint16_t frag_full_mask(uint32_t nf) {
|
||||
// while a block is already queued merge only fragments not yet admitted to the radio queue; a later retry can
|
||||
// enqueue the block again if one of those admitted packets was actually lost over the air.
|
||||
bool OtaManager::queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want_mask,
|
||||
bool wire_v2, bool allow_deflate) {
|
||||
bool wire_v2, bool allow_deflate, bool extended_length) {
|
||||
for (uint8_t i = 0; i < _n_serve_jobs; i++) {
|
||||
ServeJob& job = _serve_jobs[i];
|
||||
if (job.block != block || memcmp(job.mid, mid, 4) != 0) continue;
|
||||
// A proof-only request can complete whichever representation is already queued. DATA requests with
|
||||
// different geometries remain distinct because their bitmap positions do not describe the same bytes.
|
||||
if (want_mask != 0 && (job.wire_v2 != wire_v2 ||
|
||||
(wire_v2 && job.allow_deflate != allow_deflate))) continue;
|
||||
(wire_v2 && (job.allow_deflate != allow_deflate ||
|
||||
job.extended_length != extended_length)))) continue;
|
||||
job.pending_mask |= (uint16_t)(want_mask & ~job.emitted_mask);
|
||||
if (want_mask == 0) job.proof_requested = true;
|
||||
return true;
|
||||
@@ -448,6 +452,7 @@ bool OtaManager::queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want
|
||||
job.proof_requested = want_mask == 0;
|
||||
job.wire_v2 = wire_v2;
|
||||
job.allow_deflate = allow_deflate;
|
||||
job.extended_length = extended_length;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -633,6 +638,10 @@ bool OtaManager::handleReq(const uint8_t* m, uint16_t n) {
|
||||
const bool wire_v2 = ota_req_is_v2(rq.items[i].want_mask);
|
||||
const bool allow_deflate = wire_v2 &&
|
||||
(rq.items[i].want_mask & OTA_REQ_V2_ALLOW_DEFLATE) != 0;
|
||||
const bool extended_length = wire_v2 &&
|
||||
ota_req_v2_extended_length(rq.items[i].want_mask);
|
||||
const bool block_requires_extended = v->m.block_size() > OTA_DATA_V2_LEGACY_MAX_ENCODED;
|
||||
if (wire_v2 && extended_length != block_requires_extended) continue;
|
||||
const uint16_t valid_mask = frag_full_mask((blen +
|
||||
(wire_v2 ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA) - 1) /
|
||||
(wire_v2 ? OTA_FRAG_DATA_V2 : OTA_FRAG_DATA));
|
||||
@@ -640,7 +649,7 @@ bool OtaManager::handleReq(const uint8_t* m, uint16_t n) {
|
||||
? ota_req_v2_fragments(rq.items[i].want_mask) : rq.items[i].want_mask;
|
||||
const uint16_t want = (uint16_t)(requested & valid_mask);
|
||||
if (want != 0) accepted |= queueServeJob(v->m.merkle_root, (uint16_t)idx, want,
|
||||
wire_v2, allow_deflate);
|
||||
wire_v2, allow_deflate, extended_length);
|
||||
}
|
||||
return accepted;
|
||||
}
|
||||
@@ -710,7 +719,10 @@ void OtaManager::serviceEgress() {
|
||||
dm.block_idx = job.block;
|
||||
uint8_t v2_data[OTA_DATA_V2_STREAM_ID_BYTES + OTA_FRAG_DATA_V2];
|
||||
if (job.wire_v2) {
|
||||
if (!ota_data_v2_pack(fragment, _serve_wire_len, _serve_wire_deflated, dm.frag_off)) {
|
||||
const bool packed = job.extended_length
|
||||
? ota_data_v2_pack_extended(fragment, _serve_wire_len, dm.frag_off)
|
||||
: ota_data_v2_pack(fragment, _serve_wire_len, _serve_wire_deflated, dm.frag_off);
|
||||
if (!packed) {
|
||||
popServeJob();
|
||||
return;
|
||||
}
|
||||
@@ -1342,7 +1354,7 @@ void OtaManager::handleManifest(const uint8_t* m, uint16_t n) {
|
||||
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) { failFetch(FETCH_ERROR_GEOMETRY); return; }
|
||||
uint32_t bc = (payload_size + bs - 1) / bs;
|
||||
uint32_t bc = payload_size / bs + (payload_size % bs != 0 ? 1u : 0u);
|
||||
if (bc > 0xFFFFu) { failFetch(FETCH_ERROR_TOO_LARGE); return; } // uint16 block index on the wire
|
||||
if (_archive_fetch && (uint64_t)bc * 4 > OTA_PROOFGEN_SCRATCH) {
|
||||
failFetch(FETCH_ERROR_TOO_LARGE); return; // retaining an image we cannot seed is useless
|
||||
@@ -1659,7 +1671,12 @@ bool OtaManager::handleData(const uint8_t* m, uint16_t n) {
|
||||
uint8_t fragment = 0;
|
||||
uint16_t encoded_len = 0;
|
||||
bool deflated = false;
|
||||
if (!ota_data_v2_unpack(dm.frag_off, fragment, encoded_len, deflated)) return false;
|
||||
const bool extended_length = _fbs > OTA_DATA_V2_LEGACY_MAX_ENCODED;
|
||||
const bool unpacked = extended_length
|
||||
? ota_data_v2_unpack_extended(dm.frag_off, (uint16_t)blen,
|
||||
fragment, encoded_len, deflated)
|
||||
: ota_data_v2_unpack(dm.frag_off, fragment, encoded_len, deflated);
|
||||
if (!unpacked) return false;
|
||||
if (encoded_len > blen || (deflated ? encoded_len >= blen : encoded_len != blen)) return false;
|
||||
const uint16_t fragment_len = (uint16_t)(dm.data_len - OTA_DATA_V2_STREAM_ID_BYTES);
|
||||
const uint32_t fragment_off = (uint32_t)fragment * OTA_FRAG_DATA_V2;
|
||||
@@ -1795,7 +1812,8 @@ uint16_t OtaManager::wireRequestMask(uint32_t block, uint16_t need, uint16_t rec
|
||||
want = frag_full_mask((blockLen(block) + OTA_FRAG_DATA - 1) / OTA_FRAG_DATA);
|
||||
}
|
||||
if (want == 0) want = need;
|
||||
return ota_req_make_v2(want, _wire_allow_deflate);
|
||||
return ota_req_make_v2(
|
||||
want, _wire_allow_deflate, _fbs > OTA_DATA_V2_LEGACY_MAX_ENCODED);
|
||||
}
|
||||
if (want == 0) want = need;
|
||||
return want;
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
// 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 keeps 1 KB logical blocks. A server paces each block's self-describing DATA fragments through a
|
||||
// bounded response queue and follows them with the Merkle PROOF. A short radio-aware turnaround gap lets a
|
||||
// Transfer accepts deployed 1 KiB and new 2 KiB logical blocks. A server paces each block's self-describing
|
||||
// DATA fragments through a bounded response queue and follows them with the Merkle PROOF. A short
|
||||
// radio-aware turnaround gap lets a
|
||||
// legacy fetcher send its immediate REQ_PROOF without colliding with that proactive proof; newer fetchers wait
|
||||
// briefly for the proactive proof and retain REQ_PROOF as a loss/legacy-server fallback.
|
||||
|
||||
@@ -49,15 +50,21 @@ typedef bool (*ServeDeflateReadFn)(void* ctx, uint16_t block, uint8_t* dst,
|
||||
|
||||
#ifndef OTA_PROOFGEN_SCRATCH
|
||||
#if defined(OTA_SD_STORE)
|
||||
#define OTA_PROOFGEN_SCRATCH 8192 // SD archive can seed <=2048 blocks (about 2 MB at 1 KB/block)
|
||||
#define OTA_PROOFGEN_SCRATCH 8192 // SD archive can seed <=2048 blocks (about 4 MiB at 2 KiB/block)
|
||||
#else
|
||||
#define OTA_PROOFGEN_SCRATCH 4096 // server proof-gen working buffer (supports up to 1024 blocks)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef OTA_MAX_BLOCK
|
||||
#define OTA_MAX_BLOCK 1024 // largest logical block (merkle leaf unit) = reassembly buffer size
|
||||
#define OTA_MAX_BLOCK 2048 // largest logical block (Merkle leaf / independent DEFLATE unit)
|
||||
#endif
|
||||
static_assert(OTA_MAX_BLOCK <= OTA_DATA_V2_MAX_ENCODED,
|
||||
"OTA_MAX_BLOCK exceeds the negotiated v2 DATA descriptor");
|
||||
|
||||
// Live application capability advertised by `ota status` / `ota self`. Keep this tied to the actual
|
||||
// receive/reassembly buffers rather than a release label so a host can safely choose package geometry.
|
||||
static constexpr uint16_t ota_max_block_capability() { return (uint16_t)OTA_MAX_BLOCK; }
|
||||
#ifndef OTA_CHECKPOINT_BLOCKS
|
||||
#define OTA_CHECKPOINT_BLOCKS 4 // persist progress (store.checkpoint) every N committed blocks (resume)
|
||||
#endif
|
||||
@@ -562,7 +569,8 @@ private:
|
||||
static bool srcDeflateReadTramp(void* c, uint16_t block, uint8_t* buf,
|
||||
uint16_t cap, uint16_t* len);
|
||||
bool queueServeJob(const uint8_t* mid, uint16_t block, uint16_t want_mask,
|
||||
bool wire_v2 = false, bool allow_deflate = false);
|
||||
bool wire_v2 = false, bool allow_deflate = false,
|
||||
bool extended_length = false);
|
||||
bool queueManifestJob(const uint8_t* mid, uint16_t want_mask);
|
||||
uint32_t manifestEgressGapMs() const;
|
||||
uint32_t proofEgressGapMs() const;
|
||||
@@ -625,7 +633,7 @@ private:
|
||||
uint8_t _scratch[OTA_PROOFGEN_SCRATCH]; // proof-gen / fetch root-check working buffer
|
||||
#endif
|
||||
|
||||
// Server-side response descriptors are tiny. The active 1 KB block has its own buffer so proof generation
|
||||
// Server-side response descriptors are tiny. The active logical block has its own buffer so proof generation
|
||||
// or a simultaneous fetch cannot overwrite DATA retained behind radio-queue backpressure.
|
||||
struct ServeJob {
|
||||
uint8_t mid[4] = {0};
|
||||
@@ -636,6 +644,7 @@ private:
|
||||
bool proof_requested = false;
|
||||
bool wire_v2 = false;
|
||||
bool allow_deflate = false;
|
||||
bool extended_length = false;
|
||||
};
|
||||
ServeJob _serve_jobs[OTA_SERVE_QUEUE];
|
||||
uint8_t _n_serve_jobs = 0;
|
||||
@@ -698,7 +707,7 @@ private:
|
||||
// Bounded adaptive multi-block client flight. Each slot independently reassembles DATA and awaits its
|
||||
// Merkle proof. One append-only OTA_REQ packet opens the whole flight; the receiver then stays quiet until
|
||||
// every slot completes or an airtime-aware deadline expires. Clean flights grow 1->2->3->4; any recovery
|
||||
// makes the next flight smaller. Logical/signed block geometry remains 1 KB.
|
||||
// makes the next flight smaller. New application containers use 2 KiB logical/signed blocks.
|
||||
struct ReassemblySlot {
|
||||
uint32_t block = NO_BLOCK;
|
||||
uint16_t mask = 0; // received FRAG_DATA-slice bitmap
|
||||
|
||||
@@ -71,9 +71,11 @@ struct ManifestMsg {
|
||||
//
|
||||
// Legacy want_mask bit k = "send fragment k" (the slice at byte offset k*OTA_FRAG_DATA). A recovery request
|
||||
// uses only the missing bits, so one lost fragment costs one fragment to re-send rather than the whole block.
|
||||
// New receivers reserve bits 15..14 as an in-band request profile: bit 15 asks for the v2 171-byte geometry
|
||||
// and bit 14 permits per-block raw DEFLATE. An old source masks those bits away and returns the ordinary
|
||||
// 160-byte DATA train; the new receiver detects that response and downgrades without stranding old fleets.
|
||||
// New receivers reserve bits 15..13 as an in-band request profile: bit 15 asks for the v2 171-byte geometry,
|
||||
// bit 14 permits per-block raw DEFLATE, and bit 13 selects the 2 KiB descriptor layout. Bit 12 must be clear
|
||||
// in that extended profile, preserving the deployed 0xFFFF legacy "all fragments" request. An old source
|
||||
// masks the profile bits away and returns ordinary 160-byte DATA; the new receiver detects that response and
|
||||
// downgrades without stranding old fleets.
|
||||
struct ReqMsg { uint8_t manifest_id[4]; uint16_t block_idx; uint16_t want_mask; };
|
||||
struct ReqItem { uint16_t block_idx; uint16_t want_mask; };
|
||||
#ifndef OTA_REQ_MAX_ITEMS
|
||||
@@ -101,43 +103,65 @@ struct DataMsg {
|
||||
const uint8_t* data; uint16_t data_len;
|
||||
};
|
||||
|
||||
// OTA_REQ want_mask extension. The v2 marker and DEFLATE permission are deliberately outside every valid
|
||||
// fragment bit for the fixed <=1 KiB blocks used by this protocol. A v2 request includes all seven legacy
|
||||
// fragment bits on its first attempt, so a deployed source which ignores these flags can answer completely.
|
||||
// OTA_REQ want_mask extension. The profile bits are deliberately outside every valid fragment bit: a 2 KiB
|
||||
// block needs at most 13 legacy 160-byte fragments or 12 v2 171-byte fragments. A first request includes every
|
||||
// legacy fragment bit, so a deployed source which ignores the profile flags can still answer a 1 KiB block.
|
||||
static const uint16_t OTA_REQ_V2_MARK = 0x8000u;
|
||||
static const uint16_t OTA_REQ_V2_ALLOW_DEFLATE = 0x4000u;
|
||||
static const uint16_t OTA_REQ_V2_RESERVED = 0x2000u;
|
||||
static const uint16_t OTA_REQ_V2_EXTENDED_LENGTH = 0x2000u;
|
||||
static const uint16_t OTA_REQ_V2_FRAGMENT_MASK = 0x1FFFu;
|
||||
static const uint16_t OTA_REQ_V2_EXT_FRAGMENT_MASK = 0x0FFFu;
|
||||
static const uint16_t OTA_REQ_V2_EXT_RESERVED = 0x1000u;
|
||||
|
||||
// OTA_DATA v2 frag_off packing:
|
||||
// OTA_DATA v2 frag_off packing has two negotiated layouts. The deployed-compatible 1 KiB layout is retained:
|
||||
// bit 15 v2 marker
|
||||
// bit 14 data[] is one raw-RFC1951-DEFLATE stream fragment (clear = raw block bytes)
|
||||
// bits 13..10 fragment index (0..15; byte offset = index * OTA_FRAG_DATA_V2)
|
||||
// bits 9..0 total encoded block length minus one (1..1024 bytes)
|
||||
// bit 14 raw-RFC1951-DEFLATE stream (clear = raw block bytes)
|
||||
// bits 13..10 fragment index; bits 9..0 encoded length minus one (1..1024)
|
||||
// A request carrying OTA_REQ_V2_EXTENDED_LENGTH selects the 2 KiB layout:
|
||||
// bit 15 v2 marker
|
||||
// bits 14..11 fragment index; bits 10..0 encoded length minus one (1..2048)
|
||||
// In the extended layout DEFLATE is unambiguous without a flag: encoders may select it only when the encoded
|
||||
// stream is strictly shorter than the manifest-derived raw block. Equal length therefore means raw bytes.
|
||||
static const uint16_t OTA_DATA_V2_MARK = 0x8000u;
|
||||
static const uint16_t OTA_DATA_V2_DEFLATED = 0x4000u;
|
||||
static const uint16_t OTA_DATA_V2_FRAGMENT_BITS = 0x3C00u;
|
||||
static const uint16_t OTA_DATA_V2_LENGTH_BITS = 0x03FFu;
|
||||
static const uint8_t OTA_DATA_V2_FRAGMENT_SHIFT = 10;
|
||||
static const uint16_t OTA_DATA_V2_MAX_ENCODED = 1024;
|
||||
static const uint16_t OTA_DATA_V2_LEGACY_MAX_ENCODED = 1024;
|
||||
static const uint16_t OTA_DATA_V2_EXT_FRAGMENT_BITS = 0x7800u;
|
||||
static const uint16_t OTA_DATA_V2_EXT_LENGTH_BITS = 0x07FFu;
|
||||
static const uint8_t OTA_DATA_V2_EXT_FRAGMENT_SHIFT = 11;
|
||||
static const uint16_t OTA_DATA_V2_MAX_ENCODED = 2048;
|
||||
static const uint8_t OTA_DATA_V2_STREAM_ID_BYTES = 4;
|
||||
|
||||
inline bool ota_req_is_v2(uint16_t want_mask) {
|
||||
return (want_mask & OTA_REQ_V2_MARK) != 0 && (want_mask & OTA_REQ_V2_RESERVED) == 0;
|
||||
if ((want_mask & OTA_REQ_V2_MARK) == 0) return false;
|
||||
return (want_mask & OTA_REQ_V2_EXTENDED_LENGTH) == 0 ||
|
||||
(want_mask & OTA_REQ_V2_EXT_RESERVED) == 0;
|
||||
}
|
||||
|
||||
inline uint16_t ota_req_v2_fragments(uint16_t want_mask) {
|
||||
return (uint16_t)(want_mask & OTA_REQ_V2_FRAGMENT_MASK);
|
||||
const uint16_t mask = (want_mask & OTA_REQ_V2_EXTENDED_LENGTH)
|
||||
? OTA_REQ_V2_EXT_FRAGMENT_MASK : OTA_REQ_V2_FRAGMENT_MASK;
|
||||
return (uint16_t)(want_mask & mask);
|
||||
}
|
||||
|
||||
inline uint16_t ota_req_make_v2(uint16_t fragments, bool allow_deflate) {
|
||||
return (uint16_t)((fragments & OTA_REQ_V2_FRAGMENT_MASK) | OTA_REQ_V2_MARK |
|
||||
(allow_deflate ? OTA_REQ_V2_ALLOW_DEFLATE : 0));
|
||||
inline bool ota_req_v2_extended_length(uint16_t want_mask) {
|
||||
return (want_mask & OTA_REQ_V2_EXTENDED_LENGTH) != 0;
|
||||
}
|
||||
|
||||
inline uint16_t ota_req_make_v2(uint16_t fragments, bool allow_deflate,
|
||||
bool extended_length = false) {
|
||||
const uint16_t fragment_mask = extended_length
|
||||
? OTA_REQ_V2_EXT_FRAGMENT_MASK : OTA_REQ_V2_FRAGMENT_MASK;
|
||||
return (uint16_t)((fragments & fragment_mask) | OTA_REQ_V2_MARK |
|
||||
(allow_deflate ? OTA_REQ_V2_ALLOW_DEFLATE : 0) |
|
||||
(extended_length ? OTA_REQ_V2_EXTENDED_LENGTH : 0));
|
||||
}
|
||||
|
||||
inline bool ota_data_v2_pack(uint8_t fragment, uint16_t encoded_len, bool deflated,
|
||||
uint16_t& packed) {
|
||||
if (fragment >= 16 || encoded_len == 0 || encoded_len > OTA_DATA_V2_MAX_ENCODED) return false;
|
||||
if (fragment >= 16 || encoded_len == 0 || encoded_len > OTA_DATA_V2_LEGACY_MAX_ENCODED) return false;
|
||||
packed = (uint16_t)(OTA_DATA_V2_MARK |
|
||||
(deflated ? OTA_DATA_V2_DEFLATED : 0) |
|
||||
((uint16_t)fragment << OTA_DATA_V2_FRAGMENT_SHIFT) |
|
||||
@@ -154,6 +178,27 @@ inline bool ota_data_v2_unpack(uint16_t packed, uint8_t& fragment, uint16_t& enc
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ota_data_v2_pack_extended(uint8_t fragment, uint16_t encoded_len,
|
||||
uint16_t& packed) {
|
||||
if (fragment >= 16 || encoded_len == 0 || encoded_len > OTA_DATA_V2_MAX_ENCODED) return false;
|
||||
packed = (uint16_t)(OTA_DATA_V2_MARK |
|
||||
((uint16_t)fragment << OTA_DATA_V2_EXT_FRAGMENT_SHIFT) |
|
||||
(encoded_len - 1u));
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool ota_data_v2_unpack_extended(uint16_t packed, uint16_t raw_len,
|
||||
uint8_t& fragment, uint16_t& encoded_len,
|
||||
bool& deflated) {
|
||||
if ((packed & OTA_DATA_V2_MARK) == 0 || raw_len == 0 || raw_len > OTA_DATA_V2_MAX_ENCODED) return false;
|
||||
fragment = (uint8_t)((packed & OTA_DATA_V2_EXT_FRAGMENT_BITS) >>
|
||||
OTA_DATA_V2_EXT_FRAGMENT_SHIFT);
|
||||
encoded_len = (uint16_t)((packed & OTA_DATA_V2_EXT_LENGTH_BITS) + 1u);
|
||||
if (encoded_len > raw_len) return false;
|
||||
deflated = encoded_len < raw_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- OTA_GET_LEAVES: request the target's merkle leaves[] in bulk (direct; motatool warm-start only) ----
|
||||
// body: manifest_id(4) want_mask(2). want_mask bit k = "send leaves fragment k"; 0xFFFF = "send all" on the
|
||||
// first ask (before frag_total is known); a retry requests only the still-missing fragments (anti-burst,
|
||||
|
||||
@@ -144,8 +144,9 @@ bool ota_serve_self(OtaContext& c, uint32_t fw_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).
|
||||
// 2 KiB logical blocks (delivered as multiple LoRa fragments) reduce Merkle/proof overhead and give each
|
||||
// independent transport-DEFLATE stream more useful history than the deployed 1 KiB geometry. A ~530 KiB
|
||||
// image is about 265 blocks (roughly 1 KiB of leaves) instead of ~4,240 128-byte leaves.
|
||||
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;
|
||||
@@ -186,7 +187,7 @@ bool ota_serve_self(OtaContext& c, uint32_t fw_version) {
|
||||
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)
|
||||
m[19] = 11; // block_size_log2 = 11 (2048 B logical block)
|
||||
memcpy(m + 20, root, 4);
|
||||
memcpy(m + 24, image_hash, 32);
|
||||
m[56] = CODEC_FULL;
|
||||
|
||||
@@ -47,7 +47,7 @@ static VerifyResult ota_verify_store(const OtaStore& store,
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) return r;
|
||||
const uint32_t leaves_off = 8 + MOTA_MFL;
|
||||
const uint64_t payload_off64 = (uint64_t)leaves_off + (uint64_t)m.block_count * 4;
|
||||
if (m.block_size() > OTA_DEFAULT_BLOCK_SIZE ||
|
||||
if (m.block_size() > OTA_MAX_APPLICATION_BLOCK_SIZE ||
|
||||
payload_off64 > UINT32_MAX ||
|
||||
payload_off64 + m.payload_size + 5 != total) return r;
|
||||
const uint32_t payload_off = (uint32_t)payload_off64;
|
||||
|
||||
Reference in new issue
Block a user