This commit is contained in:
mikecarper
2026-09-03 09:33:55 +00:00
parent 88e70a2515
commit 1db82ad422
6 changed files with 184 additions and 92 deletions
+8 -6
View File
@@ -2187,14 +2187,16 @@ mode when the hardware can report disconnect. A USB-to-UART bridge may not be
able to report this event. A different baud rate, including 57600, does not
select ASCII mode.</p>
<p>On an ESP32 Full Companion built with <code>OTA_FOLDER_SERIAL</code>, <code>motatool</code> can keep
the shared serial console open as an mOTA folder source when WiFi is
unavailable:</p>
the USB serial port open as an mOTA folder source when WiFi is unavailable:</p>
<pre><code class="language-sh">motatool serve --serial /dev/ttyACM0 --dir ./motas -v
</code></pre>
<p>The tool sends <code>ota folder on</code> through the text CLI, then uses the shared
serial mOTA framing. This is separate from the nRF52 exclusive USB ownership
mode described below. TCP port 5001 remains the preferred unattended source
transport.</p>
<p>The tool sends the exact completed <code>ota folder on</code> line when it opens the port.
Firmware recognizes it in startup ASCII or idle Binary Companion mode, then
transfers the port to exclusive USB mOTA ownership before binary request/reply
frames begin. The text terminal and Binary Companion are unavailable on that
TTY until <code>motatool</code> detaches the folder. This matches the nRF52 ownership
behavior described below. TCP port 5001 remains the preferred unattended
source transport.</p>
<h2 id="nrf52-usb-mota-mode">nRF52 USB mOTA mode</h2>
<p>The nRF52 full target has a third, exclusive USB mode for the host folder.
Unmodified <code>motatool serve --serial</code> sends <code>ota folder on</code> when it opens the
+12 -3
View File
@@ -1671,8 +1671,10 @@ rebooted, its separate interface <code>02</code> continues to provide plaintext
and is not a controller or source port. With default logging off, interface
<code>02</code> is not enumerated. BLE remains available.</p>
<p>ESP32 Full Companion normally uses its dedicated TCP seeder on port 5001.
ESP32 builds that also retain serial folder support use the shared-console
mOTA framing rather than the nRF52 exclusive USB ownership state.</p>
ESP32 builds that also retain serial folder support use the same exclusive USB
mOTA ownership state as nRF52. The exact <code>ota folder on</code> line transfers the USB
port from startup ASCII or idle Binary Companion mode before binary mOTA frames
begin; neither console mode shares that port until the folder detaches.</p>
<h2 id="destination-requirements">Destination requirements</h2>
<table>
<thead>
@@ -1798,6 +1800,12 @@ reported bootloader ABI and codec mask against the selected package. If the
version command is unavailable on older firmware, the script warns and falls
back to the legacy <code>ota self</code> platform marker. If an nRF52 bootloader lacks
the required capabilities, install the exact-board OTAFIX bootloader first.
Current firmware also reports <code>maxblk:2048</code> near the front of both OTA replies.
The runner treats a missing marker as the deployed 1 KiB receive limit, rejects
a ready 2 KiB package for such a target before transfer, and passes the live
limit to <code>motatool --block-size</code> when it builds from raw firmware. Thus updating
an older target from a release ZIP produces a compatible 1 KiB package while a
current target retains the 2 KiB application default.
<code>ota stats</code> is only an optional EndF version probe. It uses one bounded retry
cycle; unsupported firmware or a lost reply falls back directly to the
required <code>ver</code> command instead of entering an operator continuation loop.</p>
@@ -2162,7 +2170,8 @@ the destination.</p>
capabilities, and save the controller's normal radio tuple.</li>
<li>Select or build one compatible <strong>v2 application</strong> mOTA and verify all block
hashes, Merkle root, full-image hash where applicable, identity fields,
signature, codec, base, and the firmware's 1024-byte maximum block size.
signature, codec, base, and the live firmware's reported maximum block size
(1 KiB for legacy replies without <code>maxblk</code>, currently 2 KiB otherwise).
Version-3 bootloader packages are refused before any target state changes.</li>
<li>Read every participant's version, save the destination's RXPS state,
select the qualified destination policy, and show the confirmation prompt.</li>
+52 -33
View File
@@ -2100,9 +2100,14 @@ document is the technical/wire specification.</p>
<td>usable bytes per packet (<code>src/MeshCore.h</code>)</td>
</tr>
<tr>
<td>Default block size</td>
<td>Default application block size</td>
<td><code>2048</code></td>
<td><code>block_size_log2 = 0x0B</code>; deployed 1 KiB application packages remain accepted</td>
</tr>
<tr>
<td>Bootloader-package block size</td>
<td><code>1024</code></td>
<td><code>block_size_log2 = 0x0A</code></td>
<td>fixed <code>block_size_log2 = 0x0A</code> for format 3</td>
</tr>
<tr>
<td>OTA discovery TX priority</td>
@@ -2116,6 +2121,12 @@ document is the technical/wire specification.</p>
</tr>
</tbody>
</table>
<p>Current application firmware publishes its receive/reassembly ceiling as the compact <code>maxblk:2048</code> field
near the front of both <code>ota status</code> and <code>ota self</code>. Keeping the field ahead of variable diagnostics ensures
it survives the 160-byte remote-admin reply limit. Host updaters must treat an absent field as the deployed
legacy capability (<code>1024</code>), reject a ready package whose manifest block size exceeds that capability, and use
the reported size when turning a raw firmware image into a new application package. This capability does not
change the format-3 bootloader package's fixed 1 KiB geometry.</p>
<hr />
<h2 id="2-firmware-image-the-endf-trailer">2. Firmware image &amp; the <code>EndF</code> trailer</h2>
<p>Every OTA-capable build appends a fixed <strong>56-byte</strong> <code>EndF</code> trailer to its flashed image so a running node
@@ -2256,7 +2267,7 @@ Only <code>leaves[]</code> is variable (one 4-byte hash per block). So the manif
7 4 fw_version MAJOR&lt;&lt;24 | MINOR&lt;&lt;16 | PATCH&lt;&lt;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
19 1 block_size_log2 e.g. 0x0B = 2048 (new application default); 0x0A = deployed 1024
20 4 merkle_root sha2-256:4 over PAYLOAD blocks (Section 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
@@ -2370,8 +2381,9 @@ deltas use <code>--compression crle</code>. <strong>Do not reimplement the codec
<p>Verifies each PAYLOAD block against the signed <code>merkle_root</code> <strong>before</strong> the whole payload exists, so
corruption/forgery is localized to a block. Implemented in <code>MerkleTree.cpp</code>.</p>
<ul>
<li><strong>Blocks:</strong> PAYLOAD splits into <code>BC = ceil(payload_size / B)</code> blocks, <code>B = 2^block_size_log2</code> (default
1024). The last block is its real length (<strong>no zero padding</strong>).</li>
<li><strong>Blocks:</strong> PAYLOAD splits into <code>BC = ceil(payload_size / B)</code> blocks, <code>B = 2^block_size_log2</code> (new
application default 2048; deployed 1024-byte application packages remain valid). The last block is its
real length (<strong>no zero padding</strong>). Format-3 bootloader packages remain fixed at 1024 bytes.</li>
<li><strong>Leaf:</strong> <code>leaves[i] = sha2-256:4( block_i_bytes )</code>.</li>
<li><strong>Internal node:</strong> <code>node = sha2-256:4( left || right )</code> (4+4 input bytes).</li>
<li><strong>Odd level:</strong> an odd count promotes the <strong>last node unchanged</strong> to the next level (no duplication).</li>
@@ -2595,12 +2607,17 @@ label and stage a different manifest.</p>
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] { block_idx(uint16) want_mask(uint16) }[1..4]
# legacy: bit k requests its 160-byte fragment
# v2: bit15=marker, bit14=allow transport DEFLATE, bits0..12=fragment bitmap
# v2: bit15=marker, bit14=allow transport DEFLATE,
# bit13=2 KiB descriptor, bits0..12=fragment bitmap;
# bit12 must be zero when bit13 is set (0xFFFF stays legacy)
OTA_DATA legacy: manifest_id[4] block_idx(uint16) frag_off(uint16) data[&lt;=160]
OTA_DATA v2: manifest_id[4] block_idx(uint16) descriptor(uint16)
stream_id[4] data[&lt;=171]
# descriptor: bit15=marker, bit14=DEFLATE, bits13..10=fragment,
# bits9..0=complete encoded length minus one
# deployed 1 KiB descriptor: bit15=marker, bit14=DEFLATE,
# bits13..10=fragment, bits9..0=complete encoded length minus one
# negotiated 2 KiB descriptor: bit15=marker, bits14..11=fragment,
# bits10..0=complete encoded length minus one; DEFLATE iff that
# length is strictly less than the manifest-derived raw block length
OTA_REQ_PROOF: manifest_id[4] block_idx(uint16)
OTA_PROOF: manifest_id[4] block_idx(uint16) n_proof(1) proof[] # n_proof x 4 bytes
OTA_GET_LEAVES: manifest_id[4] want_mask(uint16) # bit k = send leaves fragment k; 0xFFFF = all
@@ -2623,22 +2640,25 @@ OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up
</li>
<li>
<p><strong>Negotiated 171-byte fragments:</strong> the deployed profile remains unchanged: <code>frag_off</code> is a byte offset and
<code>data[]</code> carries at most 160 bytes. A new fetcher marks an <code>OTA_REQ</code> row as v2 and includes all seven legacy
bits on its first request. Old sources ignore/mask the high flags and can therefore return a complete legacy
block immediately; receipt of an untagged <code>OTA_DATA</code> switches that fetch session to the old geometry. If no
v2 data appears by the first adaptive deadline, the fetcher retries with an ordinary legacy mask. New
sources answer v2 with a packed descriptor, a repeated 4-byte representation ID, and exactly 171 data bytes
except the final fragment. A raw 1 KiB block therefore falls from seven data packets to six. Message type
IDs do not change, so multi-hop relays continue to forward request/data/proof packets opaquely with the same
priority.</p>
<code>data[]</code> carries at most 160 bytes. A new fetcher marks an <code>OTA_REQ</code> row as v2. For a deployed 1 KiB block,
its first request also includes every fragment bit needed by the legacy representation, so an old source can
answer it completely. The extended 2 KiB profile reserves bit 12 as zero; this both fits its twelve v2
fragments and ensures the deployed <code>0xFFFF</code> legacy all-fragments request is never misclassified. Receipt of
untagged <code>OTA_DATA</code> switches the fetch session to the old geometry. If no v2 data appears by the first
adaptive deadline, the fetcher retries with an ordinary legacy mask. New sources answer v2 with a packed descriptor, a repeated 4-byte representation
ID, and exactly 171 data bytes except the final fragment. A raw 1 KiB block falls from seven data packets to
six; a raw 2 KiB block falls from thirteen to twelve. Message type IDs do not change, so multi-hop relays
continue to forward request/data/proof packets opaquely with the same priority.</p>
</li>
<li><strong>Transport-only DEFLATE:</strong> a fetcher sets the v2 DEFLATE-permission bit only when its application includes
and configures the decoder. A source may encode each logical block as an independent raw RFC 1951 stream
with full stored, fixed-Huffman, and dynamic-Huffman support (<code>BTYPE=0/1/2</code>). Compression is used only when
strictly smaller, otherwise the source returns raw v2 data. The
receiver inflates to the manifest-derived block length, then performs the unchanged Merkle proof and writes
the original bytes to staging. Thus existing <code>.mota</code> containers, signatures, resume markers, and legacy
bootloader apply remain unchanged. The 4-byte <code>stream_id</code> is SHA-256:4 of the complete raw or compressed
the original bytes to staging. Thus staging and bootloader apply remain uncompressed. Deployed 1 KiB
<code>.mota</code> containers remain compatible; new 2 KiB containers require a receiver advertising the extended
descriptor. Format-3 bootloader packages retain their strict 1 KiB geometry. The 4-byte <code>stream_id</code> is
SHA-256:4 of the complete raw or compressed
representation and appears in every fragment. A receiver locks <code>{encoding, encoded length, stream_id}</code> for
a block, preventing fragments from independently encoded seeders from being mixed. After one sparse retry,
a second stalled interval clears the full in-flight window and switches the session to legacy geometry, so
@@ -2647,12 +2667,11 @@ OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up
bounds output to the exact logical block length and rejects truncation, malformed streams, and trailing
whole bytes. Builds without OTA do not link the decoder. This application capability is independent of the
bootloader because transport data is inflated before the unchanged staged container is written.</li>
<li><strong>Adaptive flight size is not signed block size.</strong> The container continues to use 1 KiB Merkle leaves and
each slot is one existing 1 KiB block. A clean link changes how many of those blocks one <code>OTA_REQ</code> names:
1, then 2, then 3, then the compiled cap. The manifest stores <code>block_size_log2</code>, so 3 KiB is not a valid
logical geometry; enabling 2 KiB would require larger device reassembly buffers and new-package/bootloader
validation while saving only one request/proof pair per 2 KiB. It does not address premature retries, which
were the dominant packet multiplier.</li>
<li><strong>Adaptive flight size is not signed block size.</strong> Each slot holds one manifest-defined block: 1 KiB for
deployed application containers or 2 KiB for new ones. A clean link changes how many complete blocks one
<code>OTA_REQ</code> names: 1, then 2, then 3, then the compiled cap. It never changes block geometry. Two KiB halves
the leaf, block-request, and proof count for a payload while giving each independent DEFLATE stream a larger
history window.</li>
<li><strong>Append-only request-window compatibility:</strong> the first four-byte request row is exactly the original
<code>block_idx + want_mask</code> body. Old sources decode that row and ignore appended bytes. New sources queue all
rows. If a new fetcher meets an old source, the unserved tail rows eventually time out and are recovered as
@@ -2667,8 +2686,8 @@ OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up
half-duplex radios: re-requesting a whole multi-fragment burst let the periodic retry (a transmit) collide
with the tail of the in-flight burst and drop the same fragment forever - a hang. Requesting only the hole
removes the burst, so there is nothing to collide with. The block/manifest mask matches the 16-bit
reassembly bitmap (&lt;=16 fragments/block; legacy 1 KB blocks = 7, raw v2 = 6). <code>OTA_PROOF</code> is a single packet
and needs no mask.</li>
reassembly bitmap (&lt;=16 fragments/block; 1 KiB is 7 legacy or 6 raw-v2 fragments; 2 KiB is 13 legacy or 12
raw-v2 fragments). <code>OTA_PROOF</code> is a single packet and needs no mask.</li>
<li><strong>Data and proof remain separate packets, without a normal extra round trip.</strong> A server retains requested
blocks in a bounded descriptor queue, admits at most one response per main-loop pass, and sends one
<code>OTA_PROOF</code> after each block's requested fragments. Before admitting that proactive proof, the source leaves
@@ -2693,12 +2712,12 @@ OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up
<tr>
<td><code>OTA_DATA</code> legacy</td>
<td>9 B (type+mid4+idx2+off2)</td>
<td><code>OTA_FRAG_DATA = 160</code> -&gt; 7 frags per 1 KB block</td>
<td><code>OTA_FRAG_DATA = 160</code> -&gt; 7 raw frags/1 KiB or 13/2 KiB</td>
</tr>
<tr>
<td><code>OTA_DATA</code> v2</td>
<td>13 B (legacy header + stream ID)</td>
<td><code>OTA_FRAG_DATA_V2 = 171</code> -&gt; 6 raw frags per 1 KB block; fewer when compressed</td>
<td><code>OTA_FRAG_DATA_V2 = 171</code> -&gt; 6 raw frags/1 KiB or 12/2 KiB; fewer when compressed</td>
</tr>
<tr>
<td><code>OTA_MANIFEST</code></td>
@@ -2717,8 +2736,8 @@ OTA_LEAVES: manifest_id[4] frag_idx(1) frag_total(1) bytes[] # up
</tr>
</tbody>
</table>
<p>A served mota supports up to <code>OTA_MAX_BLOCK/4</code> leaves in the default 4 KB proof scratch (&lt;=1024 blocks ~ 1 MB
payload); larger self-images pass a bigger scratch buffer.</p>
<p>A served mota supports up to 1024 leaves in the default 4 KiB proof scratch (about 2 MiB of payload at the
new 2 KiB default, or 1 MiB for a deployed 1 KiB container); larger self-images pass a bigger scratch buffer.</p>
<h3 id="86-temporary-radio-and-transfer-boundary">8.6 Temporary-radio and transfer boundary</h3>
<p>OTA packets may cross normal mesh relay hops, but each participating node processes or relays them only while
its <code>tempradio</code> window is actually running. A receiver selects missing blocks in serial order into a bounded
@@ -2821,7 +2840,7 @@ class MotaSource {
uint8_t* buf, uint16_t cap, uint16_t* len); // optional raw RFC 1951
};
</code></pre>
<p>To serve an external mota the node reads its manifest-minus-leaves + <code>leaves[]</code> into RAM (&lt;=4 KB for &lt;=1024
<p>To serve an external mota the node reads its manifest-minus-leaves + <code>leaves[]</code> into RAM (&lt;=4 KiB for &lt;=1024
blocks) and streams payload blocks from the source on demand; proofs are generated from the read leaves.</p>
<h3 id="102-the-mota-seeder-transport-motaseederprotoh">10.2 The <code>mota-seeder</code> transport (<code>MotaSeederProto.h</code>)</h3>
<p>A <code>MotaSource</code> is fed by a host that serves a folder over the device's <strong>USB serial</strong> (the same console the
@@ -2846,8 +2865,8 @@ MotaDesc wire (38 B): mid[4] target_id(4) fw_version(4) codec(1) flags(1)
status: 0 = OK, non-zero = error (out of range / past EOF).
</code></pre>
<p><code>SerialMotaSource</code> splits logical reads into replies of at most 192 payload
bytes. A manifest's leaf table can exceed 256 bytes and payload blocks are
normally 1 KiB; requesting either in one transaction can overrun common USB
bytes. A manifest's leaf table can exceed 256 bytes and new payload blocks are
normally 2 KiB; requesting either in one transaction can overrun common USB
CDC/UART receive rings even though the host successfully wrote the complete
reply. Chunking is internal to the transport and does not change <code>OP_READ</code> or
the <code>MotaSource</code> random-access contract.</p>
+3 -3
View File
@@ -1887,12 +1887,12 @@ re-running a <code>validate</code> pull re-begins fresh (it never resumes a stal
trusted - every kept block is checked against the target's own fingerprints, so a mismatched or missing seed
just means those blocks are fetched over the radio (correct result, only slower).</p>
<p>The node fetches from one source as <strong>primary traffic</strong>, with bounded adaptive request flights. Every session
probes with one 1 KiB block, then clean flights grow <code>1 -&gt; 2 -&gt; 3 -&gt; 4</code> blocks on RAK3401 OTA builds. All blocks
probes with one manifest block, then clean flights grow <code>1 -&gt; 2 -&gt; 3 -&gt; 4</code> blocks on RAK3401 OTA builds. New
application packages use 2 KiB blocks; deployed 1 KiB packages remain supported. All blocks
in a flight share one request packet, and the receiver stays silent until the source/relays finish returning
them. A recovery halves the next flight. Retry timing follows the active SF/BW airtime, duty budget, and path
length, so faster settings recover sooner without a fixed one-second request colliding with a valid multi-hop
response. The signed block size itself remains 1 KiB. Mesh repeaters carry the packets only while their
temporary-radio windows are active.
response. Mesh repeaters carry the packets only while their temporary-radio windows are active.
Check progress with <code>ota status</code>.</p>
<p>If a <code>folder</code> pull loses its link mid-transfer, <code>ota status</code> shows <strong>paused</strong> - the host keeps the
partial and the pull resumes (filling only what's missing) the moment you reconnect motatool; it never
+108 -46
View File
@@ -1184,10 +1184,10 @@
</li>
<li class="md-nav__item">
<a href="#why-preview-5-and-the-old-staging-ceiling-work" class="md-nav__link">
<a href="#why-the-deployed-otafix24-and-old-staging-ceiling-work" class="md-nav__link">
<span class="md-ellipsis">
Why Preview 5 and the old staging ceiling work
Why the deployed OTAFIX2.4 and old staging ceiling work
</span>
</a>
@@ -1225,6 +1225,17 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#superseded-b40d2e6c-prerelease" class="md-nav__link">
<span class="md-ellipsis">
Superseded b40d2e6c prerelease
</span>
</a>
</li>
<li class="md-nav__item">
@@ -1504,10 +1515,10 @@
</li>
<li class="md-nav__item">
<a href="#why-preview-5-and-the-old-staging-ceiling-work" class="md-nav__link">
<a href="#why-the-deployed-otafix24-and-old-staging-ceiling-work" class="md-nav__link">
<span class="md-ellipsis">
Why Preview 5 and the old staging ceiling work
Why the deployed OTAFIX2.4 and old staging ceiling work
</span>
</a>
@@ -1545,6 +1556,17 @@
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#superseded-b40d2e6c-prerelease" class="md-nav__link">
<span class="md-ellipsis">
Superseded b40d2e6c prerelease
</span>
</a>
</li>
<li class="md-nav__item">
@@ -1710,23 +1732,23 @@
<p>Status: <strong>published prerelease for controlled, recoverable lab use.</strong> On
29-Aug-2026 steps 1-9 completed on the target RAK3401 and are byte-for-byte
identical to the corresponding transitions in the physically passed
<code>fd98bc90</code> chain. The replacement step 10 installs the current <code>b40d2e6c</code>
<code>fd98bc90</code> chain. The replacement step 10 installs the current <code>3f6eddd5</code>
endpoint. That replacement package and the hardened current runner have
passed offline reconstruction plus both required bootloader simulators, but
passed offline reconstruction plus the exact deployed bootloader simulator, but
have not had a new physical end-to-end run. Use the lab gate and keep local
DFU/SWD recovery available. Multi-hop and alternate-bandwidth estimates
remain planning data, not physical qualification.</p>
</blockquote>
<p>The prerelease is
<a href="https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c"><code>rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c</code></a>.
<a href="https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5"><code>rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.6-3f6eddd5</code></a>.
Use this asset:</p>
<pre><code class="language-text">RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip
<pre><code class="language-text">RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
</code></pre>
<ul>
<li>prerelease ZIP SHA-256: <code>8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4</code></li>
<li>inner <code>SHA256SUMS.txt</code> SHA-256: <code>29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a</code></li>
<li>endpoint image SHA-256: <code>a7761da8af1f2447d7f01d72e2b1456b99c0e874a165d6483f48ab1f58e8d905</code></li>
<li>endpoint EndF body hash: <code>1FA6AAA3C28D8BD8</code></li>
<li>prerelease ZIP SHA-256: <code>135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4</code></li>
<li>inner <code>SHA256SUMS.txt</code> SHA-256: <code>93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4</code></li>
<li>endpoint image SHA-256: <code>cd1e9b819f9e918f9f2ceaf37b8a74bf242e2be78f319af2cf02a42d7729d0f4</code></li>
<li>endpoint EndF body hash: <code>9BD7CF682EE065AE</code></li>
</ul>
<p><code>--verify-only</code> remains the safe default. Live preflight and installation of
this prerelease require <code>--accept-test-candidate</code>. That switch bypasses only
@@ -1740,11 +1762,19 @@ target:</p>
<li>hardware: <code>RAK_3401</code></li>
<li>role: <code>RAK_3401_repeater_lora_ota_no_external_sensors</code></li>
<li>start: <code>v1.16.7.0-c1caa5ad</code>, EndF <code>71F4026CBE4B8B74</code></li>
<li>endpoint source: MeshCore <code>b40d2e6cb2a185db740175e8b161ea71736d19d9</code></li>
<li>endpoint label: <code>v1.17.1.5-halo-keymind-cascade-dev-b40d2e6c</code></li>
<li>endpoint packed version: <code>0x01110105</code></li>
<li>endpoint source: MeshCore <code>3f6eddd5c20f1fa3df6ba1cc208efbe3e2fffb43</code></li>
<li>endpoint label: <code>v1.17.1.6-halo-keymind-cascade-dev-3f6eddd5</code></li>
<li>endpoint packed version: <code>0x01110106</code></li>
<li>deployment target key: <code>63d8df6387eaffd2e25db7d2a8ad967a65202182a48d681d7e7a9260f917280d</code></li>
</ul>
<p>The exact starting application is the
<a href="https://github.com/mikecarper/MeshCore/releases/tag/lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf"><code>lora-ota-v1.16.07-halo-keymind-cascade-dev-3a6af3bf</code></a>
RAK3401 asset. Its outer ZIP SHA-256 is
<code>58ead2ea18efafda186bc9d983fe08cdf65f69b0e5a3a1c72c18fe35948a254c</code>;
its 566,912-byte <code>firmware.bin</code> SHA-256 is
<code>4301fc63ebd661c70f9bc40e7eca44d3cb8a358cb3c0075e700d9676a57478cb</code>.
The endpoint build ZIP SHA-256 is
<code>a9fca9be145a96feb650b57c43b5ed173d74d2f3194da12e31b1c54352635b56</code>.</p>
<h3 id="retained-and-omitted-hardware-support">Retained and omitted hardware support</h3>
<p>Despite the legacy <code>no_external_sensors</code> target name, the endpoint retains the
RAK12501 GPS provider and the common INA3221, INA219, INA226, and INA260 I2C
@@ -1761,7 +1791,7 @@ would discard desired voltage/current monitoring and create another firmware
identity without helping this route.</p>
<p>Do not use the chain on another target ID, hardware family, starting image, or
firmware body hash. The runner checks all four.</p>
<h2 id="why-preview-5-and-the-old-staging-ceiling-work">Why Preview 5 and the old staging ceiling work</h2>
<h2 id="why-the-deployed-otafix24-and-old-staging-ceiling-work">Why the deployed OTAFIX2.4 and old staging ceiling work</h2>
<p>The deployed bootloader is not changed and no package uses the newer expanded
<code>0xED000</code> ceiling. Each package remains bottom-aligned below the old
bootloader's <code>0xD4000</code> scan ceiling. Its detools workspace is page-aligned and
@@ -1779,18 +1809,28 @@ workspace.</p>
<p>Every package in this candidate is an ordinary application container using
format ABI 2 and in-place codec 2. The historical bridge applications scan the
bootloader capability marker byte-by-byte, so they recognize the released
Preview 5 RAK4631 marker at absolute address <code>0xFCDEA</code> even though it is two
bytes off a word boundary. The <code>b40d2e6c</code> endpoint retains that exact Preview
5 application-update fallback. Its separately compiled internal
bootloader-update feature remains unavailable on Preview 5 because privileged
self-update requires a current ABI 3 marker with the exact storage profile.
<code>0.9.2-OTAFIX2.4</code> marker at absolute address <code>0xFCD16</code>. That immutable marker
advertises ABI 2, codec mask <code>0x0004</code>, and internal storage profile 0. The
<code>3f6eddd5</code> endpoint retains the matching legacy application-update fallback.
Its separately compiled internal bootloader-update feature remains unavailable
because privileged self-update requires a current ABI 3 marker with the exact
storage profile.
Installing this chain therefore does not replace the bootloader, and the final
application can still accept a future valid ABI-2/codec-2 application update.</p>
<p>The schema-2 exhaustive search checked 17,371 candidate geometries and found
121 feasible forward edges in the declared 33-image inventory. Fourteen
ten-package routes tied for the minimum package count; the selected route has
the smallest total container size among them. That minimum applies to the
declared inventory, not every conceivable firmware image. The selected route
<p>The exact installed bootloader is the
<a href="https://github.com/mikecarper/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.4/wiscore_rak4631_board_bootloader-0.9.2-OTAFIX2.4_s140_6.1.1.zip"><code>0.9.2-OTAFIX2.4</code> RAK4631 asset</a>.
Its outer ZIP SHA-256 is
<code>5e29e7a8982cc2ed5f8868435556eb09387250fe5b9a7e8443bac325247dfed1</code>;
its 191,040-byte <code>sd_bl.bin</code> SHA-256 is
<code>a97f834388db0f1da6c29f6d8f458ff6d3e6a26034709044566384838919bb23</code>.
The exact simulator was compiled from release commit
<code>d73de8372e89b8ef352747c8bc7a1aaeab80fbfe</code>; its executable SHA-256 is
<code>9dacf24b1023fe2f4c620419417649ccc9538dd4a611b47bc19b7fedb97ceba6</code>.</p>
<p>The schema-2 exhaustive search checked 1,799 candidate geometries and found 12
feasible forward edges in the declared exact 12-node inventory. It found one
shortest ten-package route and selected the smallest total container bytes as
the secondary objective. This minimum applies only to that declared inventory
and geometry table, not every conceivable new bridge image. The selected route
is:</p>
<table>
<thead>
@@ -1879,31 +1919,38 @@ is:</p>
<tr>
<td style="text-align: right;">10</td>
<td>1.17.1.3</td>
<td>1.17.1.5</td>
<td style="text-align: right;"><code>0x74000</code></td>
<td style="text-align: right;">174,744</td>
<td style="text-align: right;">61,440</td>
<td>1.17.1.6</td>
<td style="text-align: right;"><code>0x76000</code></td>
<td style="text-align: right;">197,825</td>
<td style="text-align: right;">28,672</td>
</tr>
</tbody>
</table>
<p>Total mOTA transfer data is 1,384,252 bytes. <code>ROUTE.json</code>, <code>CHAIN.csv</code>, and
<p>Total mOTA transfer data is 1,407,333 bytes. <code>ROUTE.json</code>, <code>CHAIN.csv</code>, and
<code>validation-results.json</code> in the bundle pin the exact geometry and image hash
for every transition.</p>
<p>Steps 1-9 are byte-for-byte identical to the transitions that passed on the
physical RAK3401. The replacement step 10, from the physically reached
<code>FE65A6135A1E7B3F</code> v1.17.1.3 body to <code>1FA6AAA3C28D8BD8</code>, has offline
<code>FE65A6135A1E7B3F</code> v1.17.1.3 body to <code>9BD7CF682EE065AE</code>, has offline
qualification only. All ten packages passed independent container checks,
zero-filled and erased-workspace reconstruction, the OTAFIX 2.4.1 Preview 5
simulator, and the current OTAFIX simulator. Those results prove the package
bytes and apply geometry; they are not a physical claim for the replacement
step 10 or the hardened runner.</p>
zero-filled and erased-workspace reconstruction, and the exact deployed
<code>0.9.2-OTAFIX2.4</code> simulator. Those results prove the package bytes and apply
geometry; they are not a physical claim for the replacement step 10 or the
hardened runner.</p>
<p>The 171-byte transport DEFLATE support lands only in the final <code>3f6eddd5</code>
application. The starting receiver and every receiver that accepts steps 1-10
lack that inflate path, so all ten legacy-chain transfers remain raw and the
1,407,333-byte total above is not automatically reduced over LoRa. A later
update can negotiate transport compression after this endpoint is running;
the application inflates before staging, and the immutable bootloader still
applies an ordinary ABI-2/codec-2 container.</p>
<h2 id="host-requirements">Host requirements</h2>
<p>Install Python 3.9 or newer, <code>meshcli</code> 1.6.0 or newer, the current <code>motatool</code>,
and an OTA-enabled Full Companion or repeater that can seed mOTA files.</p>
<h2 id="verify-offline">Verify offline</h2>
<p>No password or device is needed:</p>
<pre><code class="language-bash">python3 tools/lora_ota/rak3401_mota_chain.py \
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \
--work-dir ./rak3401-mota-chain-work \
--motatool /path/to/motatool \
--verify-only
@@ -1926,7 +1973,7 @@ watchdog, reachability, and post-boot gates still run.</p>
<pre><code class="language-bash">export MESHCORE_ADMIN_PASSWORD='password'
python3 tools/lora_ota/rak3401_mota_chain.py \
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip \
--bundle /path/to/RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip \
--work-dir ./rak3401-mota-chain-work \
--controller-serial /dev/ttyACM0 \
--source-tcp 192.168.1.51:5001 \
@@ -1946,6 +1993,21 @@ EndF body hash matches an exact chain node. Never manually skip a package.
zero and restores it at the endpoint. Use that option only where the selected
frequency and local duty-cycle rules permit a full transmit budget; omit it
otherwise.</p>
<h2 id="superseded-b40d2e6c-prerelease">Superseded <code>b40d2e6c</code> prerelease</h2>
<p>The immediately preceding prerelease remains pinned for offline provenance:</p>
<ul>
<li>release: <a href="https://github.com/mikecarper/MeshCore/releases/tag/rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c"><code>rak3401-mota-v1.16.07-c1caa5ad-to-v1.17.1.5-b40d2e6c</code></a></li>
<li>asset: <code>RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip</code></li>
<li>outer ZIP SHA-256: <code>8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4</code></li>
<li>inner <code>SHA256SUMS.txt</code> SHA-256: <code>29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a</code></li>
<li>endpoint image SHA-256: <code>a7761da8af1f2447d7f01d72e2b1456b99c0e874a165d6483f48ab1f58e8d905</code></li>
<li>endpoint EndF body hash: <code>1FA6AAA3C28D8BD8</code></li>
</ul>
<p>Its first nine packages are byte-for-byte identical to the current release and
the physical <code>fd98bc90</code> evidence. Its different step 10 had offline
qualification only. The runner recognizes this exact archive separately and
keeps live use gated; it is not interchangeable with the current <code>3f6eddd5</code>
asset.</p>
<h2 id="legacy-fd98bc90-physical-and-swd-qualification">Legacy <code>fd98bc90</code> physical and SWD qualification</h2>
<p>The earlier local candidate remains the exact physical evidence source for
this route:</p>
@@ -1962,9 +2024,9 @@ deployed <code>0.9.2-OTAFIX2.4</code> bootloader and a Heltec V4 source at 909.9
500 kHz / SF5 / CR5. Every intermediate EndF hash matched. Independent SWD
readback then matched the endpoint application, original bootloader,
SoftDevice/MBR, and UICR byte-for-byte. Steps 1-9 are also the exact first nine
packages in the <code>b40d2e6c</code> prerelease. The two candidates have different step
10 packages and endpoints, so the physical result and step-10 timing must not
be attributed to <code>b40d2e6c</code>.</p>
packages in the <code>b40d2e6c</code> and <code>3f6eddd5</code> prereleases. All three candidates
have different step 10 packages or endpoints, so the physical result and
step-10 timing must not be attributed to either later prerelease.</p>
<h3 id="measured-direct-link-timing-for-fd98bc90">Measured direct-link timing for <code>fd98bc90</code></h3>
<p>The first <code>ready to install</code> transfer ages were:</p>
<table>
@@ -2109,16 +2171,16 @@ during qualification and recovery; they are not permission to skip the
runner's candidate gate. Keep a written copy of each original setting and
never skip a step even when a later package appears in <code>ota ls</code>.</p>
<h3 id="1-verify-and-extract-the-asset">1. Verify and extract the asset</h3>
<pre><code class="language-bash">sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip
unzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c.zip
cd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.5-b40d2e6c
<pre><code class="language-bash">sha256sum RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
unzip RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5.zip
cd RAK3401-update-chain-v1.16.7-c1caa5ad-to-v1.17.1.6-3f6eddd5
sha256sum -c SHA256SUMS.txt
for package in motas/*.mota; do motatool verify &quot;$package&quot; || exit 1; done
</code></pre>
<p>The outer hash must be
<code>8e7f0c2565f37d4372fa51dc3ba72b591e7f38b74be09303e48641623f99bce4</code>,
<code>135783dd8777490db2422a7543f7d80e3cf03f621d5ac362da930abbc1850dc4</code>,
and the SHA-256 of the extracted <code>SHA256SUMS.txt</code> must be
<code>29a02a6c4c83eafa80168a80562b0ded5d8432e6cc02cfaca289cb2ed426681a</code>.
<code>93835ed497c579be0a2322292e7f15ab095e7c8ab7892833fe7df3130dbdaba4</code>.
Do not continue after any checksum or verification error.</p>
<h3 id="2-record-and-prepare-the-destination">2. Record and prepare the destination</h3>
<p>Before changing anything, save the complete replies from:</p>
File diff suppressed because one or more lines are too long