Support stock nRF52 bootloader version formats and audit images
This commit is contained in:
1 parent
dbc7971d88
commit
98af9161c6
7 files changed
+640
-8
No files matched your search
@@ -0,0 +1,173 @@
|
||||
# Stock nRF52 bootloader version audit
|
||||
|
||||
Checked **2026-09-29**, against the 40 nRF52 variant families resolving to 26
|
||||
board configurations in this checkout. This concerns `get bootloader.ver`, not
|
||||
bootloader installation or OTA eligibility. No devices were flashed, and no
|
||||
features, storage sizes, or partitions were changed.
|
||||
|
||||
## Result
|
||||
|
||||
The real firmware reader successfully reads **26 distinct manufacturer-published
|
||||
bootloader images** from 32 artifact records, including older RAK revisions,
|
||||
current and historical LilyGO images, and vendor HEX, UF2 and DFU ZIP packages.
|
||||
Repeated copies and identical normalized bootloader images are counted once.
|
||||
These are publicly supplied stock/recovery images, **not proof that every
|
||||
manufacturing batch shipped identical bytes**. No untouched live unit was read
|
||||
in this audit.
|
||||
|
||||
One format gap was fixed: Seeed's T1000-E **LoRaWAN development-board** bootloader
|
||||
declares `UF2 Bootloader 1.00`. The reader now accepts that two-component version
|
||||
and returns `1.00` unchanged. It does not invent `1.00.0`. That development image
|
||||
is separate from Seeed's Meshtastic/MeshCore recovery image, which reports
|
||||
`0.9.1-5-g488711a` and already worked.
|
||||
|
||||
All other tested images already expose a readable UF2 bootloader record.
|
||||
Generic bare dotted-number searches remain deliberately unsupported: TinyUSB,
|
||||
nrfx, and SoftDevice can embed their own versions inside the same bootloader.
|
||||
The bounded standalone fallback still requires the explicit OTAFIX version
|
||||
shape. Ambiguous records, invalid vectors, and corrupt canonical OTAFIX metadata
|
||||
retain the existing rejection rules.
|
||||
|
||||
## Published images actually tested
|
||||
|
||||
The exact source URLs, download hashes, normalized-image hashes, extraction
|
||||
layout and expected strings are pinned in
|
||||
[`stock_images.json`](../test/fixtures/nrf52_bootloader_version/stock_images.json).
|
||||
|
||||
| Manufacturer image family | Version captured from the image |
|
||||
| --- | --- |
|
||||
| RAK4631/RAK4630 Arduino bootloader, historical through current | `0.3.2-109-gd6b28e6`, `0.4.1-13-g5e6690e-dirty`, `0.4.2`, `0.4.3`, `0.4.4` |
|
||||
| Heltec HT-n5262, SDK and T114 vendor recovery HEX | `0.9.0-2-g836c8dc-dirty` |
|
||||
| LilyGO T-Echo | `0.6.1-2-g1224915` |
|
||||
| LilyGO T-Echo Lite, Card, T-Impulse Plus (including historical builds) | `0.9.2-dirty` |
|
||||
| Seeed XIAO nRF52840 and Sense | `0.6.1` |
|
||||
| Seeed XIAO nRF52840 Plus and Sense Plus | `0.9.2-29-g6a9a6a3` |
|
||||
| Seeed Wio Tracker 1110 SDK/conversion bootloader | `0.6.2-26-g949425a-dirty` |
|
||||
| Seeed Wio Tracker L1 vendor recovery ZIP | `0.7.0-22-g277a0c8` |
|
||||
| Seeed T1000-E Meshtastic/MeshCore recovery ZIP | `0.9.1-5-g488711a` |
|
||||
| Seeed T1000-E LoRaWAN development-board HEX | `1.00` (newly supported) |
|
||||
| Seeed MeshTracker X1 recovery ZIP | `0.10.0-18-gb93789f` |
|
||||
| MTools Tec MeshTiny vendor bootloader UF2 | `0.4.5` |
|
||||
| BQ/Unit Engineering Nano G2 Ultra vendor HEX ZIP | `0.6.4-dirty` |
|
||||
| Muzi Works R1 Neo release HEX | `0.9.2-31-g990aa7f-dirty` |
|
||||
|
||||
**Use the embedded version, not the filename.** For example, Seeed's XIAO file
|
||||
named `...bootloader-0.6.2...hex` reports `0.6.1`, and its L1 ZIP named
|
||||
`...bootloader-0.10.0...zip` reports `0.7.0-22-g277a0c8`. These are actual tested
|
||||
bytes, not renamed or inferred release numbers.
|
||||
|
||||
All these normalized images use the Adafruit nRF52840 `F4000..FE000` bootloader
|
||||
window. HEX/UF2 address records identify the bytes. The boot-only Seeed DFU
|
||||
packages omit MBR/UICR address words; their bootloader binary is extracted using
|
||||
the manifest's `sd_size`/`bl_size` and the
|
||||
[Adafruit nRF52840 linker layout](https://github.com/adafruit/Adafruit_nRF52_Bootloader/blob/master/linker/nrf52840.ld).
|
||||
The runtime reader independently selects the installed MBR/UICR boot address;
|
||||
this audit does not hardcode that layout into the firmware getter.
|
||||
|
||||
## Coverage of every variant family
|
||||
|
||||
- **Image verified:** a published bootloader for that named product/module was
|
||||
tested. Factory batch identification still requires reading the actual unit.
|
||||
- **Representative only:** a related module/SDK bootloader was tested; its exact
|
||||
factory image was not obtained. Shared build configuration alone does not
|
||||
establish identical factory bootloaders.
|
||||
- **Unverified:** no accessible factory bootloader image was obtained. This is
|
||||
not a demonstrated reader failure.
|
||||
|
||||
| Variant family | Status | Evidence or remaining gap |
|
||||
| --- | --- | --- |
|
||||
| `gat562_30s_mesh_kit` | Unverified | GAT documentation/firmware repository checked; no product-specific stock bootloader found. |
|
||||
| `gat562_mesh_evb_pro` | Unverified | Same GAT gap; RAK board configuration is not factory-image proof. |
|
||||
| `gat562_mesh_tracker_pro` | Unverified | Same GAT gap; custom vendor/OTAFIX builds also exist. |
|
||||
| `gat562_mesh_watch13` | Unverified | Same GAT gap. |
|
||||
| `heltec_mesh_solar` | Representative only | HT-n5262 SDK image passes; Solar downloads found application images, not a board-specific stock bootloader. |
|
||||
| `heltec_t096` | Representative only | HT-n5262 SDK image passes; T096 factory revision not established. |
|
||||
| `heltec_t1` | Representative only | SDK image passes; vendor F&T archive contains application-only UF2s. |
|
||||
| `heltec_t114` | Image verified | Both SDK HEX and T114-specific vendor recovery HEX pass. |
|
||||
| `heltec_tower_v2` | Representative only | SDK image passes; vendor matching-firmware archive contains applications, not the stock bootloader. |
|
||||
| `ikoka_handheld_nrf` | Representative only | XIAO board configuration; XIAO module images pass, exact installed factory image not obtained. |
|
||||
| `ikoka_nano_nrf` | Representative only | Same XIAO representative coverage. |
|
||||
| `ikoka_stick_nrf` | Representative only | Same XIAO representative coverage. |
|
||||
| `keepteen_lt1` | Unverified | Manufacturer product/download search did not yield a factory bootloader binary. |
|
||||
| `lilygo_t_impulse_plus` | Image verified | Both historical and newer vendor bootloader HEX images pass. |
|
||||
| `lilygo_techo` | Image verified | Vendor T-Echo bootloader passes. |
|
||||
| `lilygo_techo_card` | Image verified | Three distinct vendor historical/current bootloader images pass. NFC UICR repair application excluded. |
|
||||
| `lilygo_techo_lite` | Image verified | Vendor bootloader HEX passes. |
|
||||
| `mesh_pocket` | Representative only | HT-n5262 SDK image passes; exact Pocket factory revision not established. |
|
||||
| `meshtiny` | Image verified | Direct manufacturer bootloader UF2 passes; newer ADV/factory revisions require their actual image. |
|
||||
| `meshtracker_x1` | Image verified | Seeed product-specific recovery ZIP passes. |
|
||||
| `minewsemi_me25ls01` | Unverified | Manufacturer module page lists firmware as none; no stock bootloader binary found. OEM/dev-kit installations can differ. |
|
||||
| `muzi_base` | Unverified | Manufacturer publishes bootloader source, but its release assets found were for R1 Neo, not BASE. |
|
||||
| `muziworks_r1_neo` | Image verified | Manufacturer R1 Neo release HEX passes; do not substitute a generic RAK version. |
|
||||
| `nano_g2_ultra` | Image verified | Manufacturer's preinstalled-bootloader recovery HEX ZIP passes. |
|
||||
| `promicro` | Unverified | Nologo documents a Nice!Nano UF2 drive; its linked download requires Baidu access. Generic clones need separate factory images. |
|
||||
| `rak3401` | Representative only | RAK4631 Arduino images pass; RAK3401-specific untouched image not obtained. |
|
||||
| `rak4631` | Image verified | Five distinct published stock Arduino bootloader revisions pass. RAK4631-R/RUI3 is a different bootloader family. |
|
||||
| `rak_wismesh_tag` | Representative only | RAK core images pass; exact Tag factory image not obtained. |
|
||||
| `sensecap_solar` | Representative only | Seeed documentation identifies XIAO DFU; XIAO images pass, exact Solar factory image not obtained. |
|
||||
| `t1000-e` | Image verified | Both vendor recovery ZIP and separate LoRaWAN development-board HEX pass. |
|
||||
| `thinknode_m1` | Unverified | Manufacturer repository checked; no factory bootloader binary found. |
|
||||
| `thinknode_m3` | Unverified | Published factory UF2 is application-only (`26000..52BFF`), not a bootloader image. |
|
||||
| `thinknode_m4` | Unverified | Published factory UF2 is application-only (`26000..521FF`). |
|
||||
| `thinknode_m6` | Unverified | Published factory UF2 is application-only (`26000..46CFF`). |
|
||||
| `thinknode_m8` | Unverified | No accessible product-specific factory bootloader binary found. |
|
||||
| `wio-tracker-l1` | Image verified | Seeed's L1 recovery ZIP passes. |
|
||||
| `wio-tracker-l1-1w` | Representative only | L1 recovery image passes; exact 1W/ADV factory revision not established. |
|
||||
| `wio-tracker-l1-eink` | Representative only | L1 recovery image passes; exact e-ink factory revision not established. |
|
||||
| `wio_wm1110` | Representative only | Published Wio Tracker 1110 and XIAO SDK images pass; the variant's XIAO config does not prove the shipped module's image. |
|
||||
| `xiao_nrf52` | Image verified | XIAO, Sense, Plus and Sense Plus vendor images pass. |
|
||||
|
||||
Representative/unverified products use the same shared reader and should report
|
||||
a version **if their installed bootloader exposes one of the recognized records**.
|
||||
There is no evidence yet for claiming an exact version or success on every
|
||||
untouched unit. ESP32, RP2040 and STM32 are outside this nRF52 flash reader;
|
||||
their getters were not changed and this report does not claim stock-version
|
||||
capture on those architectures.
|
||||
|
||||
## Reproduce the artifact tests
|
||||
|
||||
Normal tests are offline and include all observed version strings, malformed and
|
||||
conflicting versions, dependency-version false positives, region/vector bounds,
|
||||
and the existing OTAFIX metadata/CRC/fallback cases:
|
||||
|
||||
```sh
|
||||
python3 test/test_nrf52_bootloader_version.py -v
|
||||
```
|
||||
|
||||
To replay the real images, download each `url` in `stock_images.json` into a
|
||||
cache directory using its `file` name, then run:
|
||||
|
||||
```sh
|
||||
MESHCORE_STOCK_BOOTLOADER_CACHE=/absolute/path/to/cache \
|
||||
python3 test/test_nrf52_bootloader_version.py -v
|
||||
```
|
||||
|
||||
This checks the exact download SHA-256, extracts only the bootloader window,
|
||||
checks the normalized-image SHA-256, and invokes the actual C++ reader with an
|
||||
exact expected version. It never downloads automatically or flashes a device.
|
||||
The ordinary regression cases cover every captured format even without the
|
||||
optional cached artifacts. RAK4631 and T1000-E repeater firmware builds passed.
|
||||
|
||||
## What is needed to close the remaining gaps
|
||||
|
||||
For each untouched unit, collect its DFU drive's **`INFO_UF2.TXT`** if present.
|
||||
To prove the application's flash reader can capture that exact revision, also
|
||||
obtain a read-only bootloader dump plus the MBR word at `0xFF8`, UICR `NRFFW[0]`,
|
||||
and flash geometry (or a full vendor HEX including those address records).
|
||||
Keep the bootloader unchanged while collecting this evidence. Only the identified
|
||||
bootloader region is needed; private application keys/configuration are not.
|
||||
If no version bytes or startup register value exist, report `unknown`; a regex
|
||||
cannot recover a version that was never embedded.
|
||||
|
||||
## Manufacturer sources
|
||||
|
||||
- [RAK stock bootloader catalog](https://github.com/RAKWireless/WisBlock/tree/master/bootloader/RAK4630)
|
||||
- [Heltec SDK](https://github.com/HelTecAutomation/Heltec_nRF52), [vendor downloads](https://resource.heltec.cn/download)
|
||||
- LilyGO [T-Echo](https://github.com/Xinyuan-LilyGO/T-Echo), [T-Echo Lite](https://github.com/Xinyuan-LilyGO/T-Echo-Lite), [T-Echo Card](https://github.com/Xinyuan-LilyGO/T-Echo-Card), [T-Impulse Plus](https://github.com/Xinyuan-LilyGO/T-Impulse-Plus)
|
||||
- [Seeed Arduino bootloader catalog](https://github.com/Seeed-Studio/Adafruit_nRF52_Arduino/tree/master/bootloader), [T1000-E recovery](https://wiki.seeedstudio.com/sensecap_t1000_e/), [T1000-E development board](https://github.com/Seeed-Studio/Seeed-Tracker-T1000-E-for-LoRaWAN-dev-board), [MeshTracker X1 recovery](https://wiki.seeedstudio.com/sensecap_meshtracker_x1_meshcore/), [Wio L1 bootloader guidance](https://wiki.seeedstudio.com/get_started_with_other_mesh_firmware/)
|
||||
- [MeshTiny manufacturer bootloader download](https://shop.mtoolstec.com/bootloader-update-on-meshtiny.html)
|
||||
- [Nano G2 Ultra manufacturer bootloader documentation](https://wiki.bqvoy.com/en/meshtastic/nano-g2-ultra)
|
||||
- [Muzi bootloader source/releases](https://github.com/muzi-works/Adafruit_nRF52_Bootloader), [BASE source guidance](https://muzi.works/pages/base-system-downloads)
|
||||
- [GAT manufacturer documentation](https://github.com/gat-iot/GAT562-family), [Elecrow manufacturer repositories](https://github.com/Elecrow-RD), [Keepteen](https://www.keepteen.com/)
|
||||
- [Nologo ProMicro documentation](https://www.nologo.tech/product/otherboard/NRF52840.html)
|
||||
- [Minewsemi ME25LS01 module specifications](https://en.minewsemi.com/lora-module/lr1110-nrf52840-me25LS01)
|
||||
@@ -4740,9 +4740,10 @@ selected by the MBR/UICR boot address and bounded by the chip's flash geometry;
|
||||
it does not search application firmware or staged update files.
|
||||
|
||||
The reader preserves a complete Adafruit/UF2 version string, including OTAFIX,
|
||||
preview, and vendor/build suffixes. If that string is absent or ambiguous, it
|
||||
uses the existing whole-image CRC and unique BLMF/BLM2 validation to read the
|
||||
embedded OTAFIX version (for example, `> OTAFIX2.4.6`). Some current release
|
||||
preview, and vendor/build suffixes. Manufacturer two-component versions such as
|
||||
Seeed's `1.00` are preserved without inventing a patch number. If that string is
|
||||
absent or ambiguous, it uses the existing whole-image CRC and unique BLMF/BLM2
|
||||
validation to read the embedded OTAFIX version (for example, `> OTAFIX2.4.6`). Some current release
|
||||
images, including MeshTower V2 SD 2.4.6, omit the UF2 text entirely. As a last
|
||||
OTAFIX-specific fallback, it accepts one unambiguous standalone version token
|
||||
such as `v0.11.0-OTAFIX2.4.9` from only the first 40 KiB of the identified
|
||||
@@ -4758,6 +4759,9 @@ displaying a version neither grants OTA capability nor bypasses install checks.
|
||||
The fix is in MeshCore application firmware; installing another bootloader alone
|
||||
does not update an older application's version reader.
|
||||
|
||||
See the [stock bootloader audit](bootloader_version_stock_audit.md) for tested
|
||||
manufacturer images and the boards still requiring untouched factory dumps.
|
||||
|
||||
---
|
||||
|
||||
#### View power management support
|
||||
|
||||
@@ -50,13 +50,17 @@ inline bool nrf52BootVersionChar(uint8_t c) {
|
||||
|
||||
// Return the complete token, never a truncated (and possibly different) version.
|
||||
// An embedded marker literal with no actual version is not an INFO_UF2 record.
|
||||
// Some Seeed stock bootloaders declare two components (e.g. "1.00"). Preserve
|
||||
// those bytes; adding a synthetic patch number would misreport their version.
|
||||
inline size_t nrf52BootVersionTokenSize(const uint8_t* token, size_t available) {
|
||||
size_t pos = available && token[0] == 'v' ? 1u : 0u;
|
||||
for (unsigned component = 0; component < 3u; ++component) {
|
||||
const size_t begin = pos;
|
||||
while (pos < available && token[pos] >= '0' && token[pos] <= '9') ++pos;
|
||||
if (pos == begin) return 0;
|
||||
if (component != 2u && (pos >= available || token[pos++] != '.')) return 0;
|
||||
if (component == 2u) break;
|
||||
if (component == 1u && (pos >= available || token[pos] != '.')) break;
|
||||
if (pos >= available || token[pos++] != '.') return 0;
|
||||
}
|
||||
size_t n = 0;
|
||||
for (; n < available && n <= 127u; ++n) {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Read cached manufacturer artifacts for the optional, hash-pinned audit.
|
||||
|
||||
No network access or device writes. Normalize only the declared bootloader
|
||||
window; application/SoftDevice bytes in a combined image are never searched.
|
||||
"""
|
||||
import io
|
||||
import json
|
||||
import struct
|
||||
import zipfile
|
||||
|
||||
|
||||
def bootloader_image(raw, record):
|
||||
start = int(record["start"], 0)
|
||||
image = bytearray(b"\xff" * record["size"])
|
||||
end = start + len(image)
|
||||
written = {}
|
||||
|
||||
def put(address, data):
|
||||
for pos, value in enumerate(data, address):
|
||||
if start <= pos < end:
|
||||
if pos in written and written[pos] != value:
|
||||
raise ValueError("conflicting flash bytes")
|
||||
written[pos] = value
|
||||
image[pos - start] = value
|
||||
|
||||
if record.get("member"):
|
||||
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
||||
raw = archive.read(record["member"])
|
||||
kind = record["format"]
|
||||
if kind == "hex":
|
||||
base = 0
|
||||
eof = False
|
||||
for line in raw.decode("ascii").splitlines():
|
||||
if not line:
|
||||
continue
|
||||
if eof or not line.startswith(":"):
|
||||
raise ValueError("invalid Intel HEX record")
|
||||
data = bytes.fromhex(line[1:])
|
||||
if len(data) < 5 or len(data) != data[0] + 5 or sum(data) & 0xff:
|
||||
raise ValueError("Intel HEX length/checksum")
|
||||
address = int.from_bytes(data[1:3], "big")
|
||||
payload, record_type = data[4:-1], data[3]
|
||||
if record_type == 0:
|
||||
put(base + address, payload)
|
||||
elif record_type in (2, 4):
|
||||
if len(payload) != 2 or address != 0:
|
||||
raise ValueError("invalid Intel HEX extended address")
|
||||
base = int.from_bytes(payload, "big") << (4 if record_type == 2 else 16)
|
||||
elif record_type == 1:
|
||||
if payload or address != 0:
|
||||
raise ValueError("invalid Intel HEX EOF")
|
||||
eof = True
|
||||
elif record_type not in (3, 5):
|
||||
raise ValueError("unknown Intel HEX record type")
|
||||
if not eof:
|
||||
raise ValueError("missing Intel HEX EOF")
|
||||
elif kind == "uf2":
|
||||
if not raw or len(raw) % 512:
|
||||
raise ValueError("invalid UF2 length")
|
||||
for offset in range(0, len(raw), 512):
|
||||
block = raw[offset:offset + 512]
|
||||
magic0, magic1, flags, address, length = struct.unpack_from("<IIIII", block)
|
||||
if (magic0, magic1) != (0x0a324655, 0x9e5d5157) or \
|
||||
struct.unpack_from("<I", block, 508)[0] != 0x0ab16f30 or length > 476:
|
||||
raise ValueError("invalid UF2 block")
|
||||
if not flags & 1: # NOT_MAIN_FLASH blocks aren't executable flash
|
||||
put(address, block[32:32 + length])
|
||||
elif kind == "dfu":
|
||||
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
|
||||
manifest = json.loads(archive.read("manifest.json"))["manifest"]
|
||||
part = manifest["softdevice_bootloader"]
|
||||
data = archive.read(part["bin_file"])
|
||||
sd_size, bl_size = part["sd_size"], part["bl_size"]
|
||||
if sd_size + bl_size != len(data) or bl_size > len(image):
|
||||
raise ValueError("DFU bootloader length mismatch")
|
||||
put(start, data[sd_size:])
|
||||
else:
|
||||
raise ValueError("unknown artifact format: " + kind)
|
||||
return bytes(image)
|
||||
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"audit_date": "2026-09-29",
|
||||
"artifacts": [
|
||||
{
|
||||
"file": "88e0b86aa24eefc6c6c85ba2bbaf9908271100be.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Impulse-Plus/main/bootloader/%28%E4%BF%AE%E6%94%B9%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%E4%B8%BA700ms%29t_impulse_plus_nrf52840_bootloader-0.9.2-dirty_s140_6.1.1_202508251059.hex",
|
||||
"sha256": "b370eea5b238fecf4dd162a190a35e18129a3b23adbf27450ab1147c5d3b5572",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "3bd7d638548bfc08d20898521aa0c2647ecb226d07296ad4c6b63bb7914aa6aa",
|
||||
"version": "0.9.2-dirty"
|
||||
},
|
||||
{
|
||||
"file": "a60a6b39854ea6bc69e75acad47524181ab53b28.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Impulse-Plus/main/bootloader/deprecated/t_impulse_plus_nrf52840_bootloader-0.9.2-dirty_s140_6.1.1_202507211712.hex",
|
||||
"sha256": "71fc6319904641d4d60300abaca57d7c187fb215b8f02db954fb1807bd67e99a",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "d047b1f9f23a7aae48118b1da777f0f0053c044667cc4c42f2d8c01331b2e5b6",
|
||||
"version": "0.9.2-dirty"
|
||||
},
|
||||
{
|
||||
"file": "5605ef4a7f53eb32e428b6fdfd83a251e2291012.hex",
|
||||
"url": "https://raw.githubusercontent.com/Seeed-Studio/Seeed-Tracker-T1000-E-for-LoRaWAN-dev-board/main/firmware/t1000_e_bootloader_1th_ota_uf2.hex",
|
||||
"sha256": "fdca62eace7dee87265a0f6c567a4a8ab6889bf1f8ff6dc7cef4c1b368309cd3",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "dd1599f1885a5bab04e46e81e95197574a877bf427e420b39f7353f47f93a8e2",
|
||||
"version": "1.00"
|
||||
},
|
||||
{
|
||||
"file": "f2e0a8e78eeaf9357eac6d85d8a05ba0eac485f8.hex",
|
||||
"url": "https://raw.githubusercontent.com/HelTecAutomation/Heltec_nRF52/main/bootloader/HT-n5262/HT-n5262_bootloader-0.9.0_s140_6.1.1.hex",
|
||||
"sha256": "b0eb140afac57633208ace8a98908e8c90149a38c1b0b62cf65e0ec48acea1b7",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "8618c2d91d855afab573d4e9e7efcdc61d80242faa7b9e74f403a81e744abe16",
|
||||
"version": "0.9.0-2-g836c8dc-dirty"
|
||||
},
|
||||
{
|
||||
"file": "1aa29101857a9b3973fe51028ea7c7a2df97f497.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Echo-Lite/main/bootloader/t-echo_lite_nrf52840_bootloader-0.9.2-dirty_s140_6.1.1_202411271344.hex",
|
||||
"sha256": "278a059c0288d919a166373da6241f36a53b33518c915f380878daee7b0a09e8",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "911bd9a317c3b69c8db1406dae2857410d8e608b256a9c06b5d351d90da63995",
|
||||
"version": "0.9.2-dirty"
|
||||
},
|
||||
{
|
||||
"file": "153c400ac21ab646231871b6f15a3be25316178a.hex",
|
||||
"url": "https://raw.githubusercontent.com/RAKWireless/WisBlock/master/bootloader/RAK4630/Latest/WisCore_RAK4631_Bootloader/wiscore_rak4631_board_bootloader-0.4.3.hex",
|
||||
"sha256": "e537340f695523b80c36c3cba6b74ae9b25c2d5047cf195eea19535b7171c356",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "6cbfa569a05cbd232dabe4a654abe9c04b7a03ab457615028d28f2d1329e58d2",
|
||||
"version": "0.4.3"
|
||||
},
|
||||
{
|
||||
"file": "c59a5156a0ec065fa9e56faf1da124297080014c.hex",
|
||||
"url": "https://raw.githubusercontent.com/RAKWireless/WisBlock/master/bootloader/RAK4630/Latest/wiscore_rak4631_board_bootloader-0.4.4.hex",
|
||||
"sha256": "29635d90eccd181ce206bfa10aa0ca92b9cff61735a6205321efe241e0f1298a",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "34d59ed3c584b973ced76917cc9c7a7bc979f0b71865055729dc9a8326dc326b",
|
||||
"version": "0.4.4"
|
||||
},
|
||||
{
|
||||
"file": "9fe7f40ac0344b8db514ea6770f8bf65ad8b08da.hex",
|
||||
"url": "https://raw.githubusercontent.com/RAKWireless/WisBlock/master/bootloader/RAK4630/Old/V0.3.2/RAK4630_bootloader.hex",
|
||||
"sha256": "5c44ea2eb151c20c2cb5a63ca3ff4c4d10ae11b29f97812fb7a21167de7acb44",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "f345e1b2b85c88c32b3b1ad95d3330fd1d9bcf2ee0df25289c3e2f0d5fb3228d",
|
||||
"version": "0.3.2-109-gd6b28e6"
|
||||
},
|
||||
{
|
||||
"file": "f33019e13e85f424f850a418a0c6628a5f7c864f.hex",
|
||||
"url": "https://raw.githubusercontent.com/RAKWireless/WisBlock/master/bootloader/RAK4630/Old/V0.4.1/RAK4630_bootloader-0.4.1.hex",
|
||||
"sha256": "3f317b2d05c8c65a98a5de306c7055078426ed5398dabb99ed1d2af93c270579",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "15003850514750f2606c9edb91ed81bf636d75a47d0961f3a8644847be2a9ef7",
|
||||
"version": "0.4.1-13-g5e6690e-dirty"
|
||||
},
|
||||
{
|
||||
"file": "c7c4a3c3468da4cc3680ca38caf6c5212e953c3f.hex",
|
||||
"url": "https://raw.githubusercontent.com/RAKWireless/WisBlock/master/bootloader/RAK4630/Old/V0.4.2/WisCore_RAK4631_Board_Bootloader.hex",
|
||||
"sha256": "44013dd02aa5335cbeb85a6b838408dc34b267e8d732965ab4c62730fa81b9a1",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "c77b8a4de1c33bedcb96d28cfa68d5a1c510e7cd886cfc62e6d07fbf2126ccea",
|
||||
"version": "0.4.2"
|
||||
},
|
||||
{
|
||||
"file": "f7dab5b353598db337a0e83416eab51895706a6a.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Echo/main/bootloader/lilygo_techo_bootloader-0.6.1_s140_6.1.1.hex",
|
||||
"sha256": "0f19e2a83ab76ead26e729f9c6ee5e5db6b8c6e45e6ed47b01d648d531f2418b",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "f5f3a732241590d49a355b78d12aee4952115d773796f8e40e86ff1cf0fbcc50",
|
||||
"version": "0.6.1-2-g1224915"
|
||||
},
|
||||
{
|
||||
"file": "25900da2245a7a727e22e6c3e6f727e1e83c0508.hex",
|
||||
"url": "https://raw.githubusercontent.com/Seeed-Studio/Adafruit_nRF52_Arduino/master/bootloader/Seeed_Wio_Tracker_1110/Seeed_Wio_Tracker_1110_bootloader-0.6.2_s140_6.1.1.hex",
|
||||
"sha256": "ed8c28b406a40e1d8a79e9ffe4d93a7f1aa388dba0459c912f91ad6d67fef0c3",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "a1f8e7d932801e6f50818182cfd3536e610df8ff83bc7582ac8a6fdd4fd24121",
|
||||
"version": "0.6.2-26-g949425a-dirty"
|
||||
},
|
||||
{
|
||||
"file": "b63bd32af33a6f1392483e3d7915016f8d871410.hex",
|
||||
"url": "https://raw.githubusercontent.com/Seeed-Studio/Adafruit_nRF52_Arduino/master/bootloader/Seeed_XIAO_nRF52840/Seeed_XIAO_nRF52840_bootloader-0.6.2_s140_7.3.0.hex",
|
||||
"sha256": "c79c8cf75ebb7abfa53b02fd3584aa9a5aeb8dc1b073f674f4fa162225630c6c",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "77941feb9ea9d99acaeb1f815a905e23f618381390d40f38374a894505e6798f",
|
||||
"version": "0.6.1"
|
||||
},
|
||||
{
|
||||
"file": "fced8e3e3c0cacc89db9a2ba5c00ceb97345ab9b.hex",
|
||||
"url": "https://raw.githubusercontent.com/Seeed-Studio/Adafruit_nRF52_Arduino/master/bootloader/Seeed_XIAO_nRF52840_Plus/Seeed_XIAO_nRF52840_Plus_bootloader-0.6.2_s140_7.3.0.hex",
|
||||
"sha256": "6bd76f49e1dfa839d0084b9af5563115df2ee438f1980327dd4364792475048b",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "caf05e859193001ef745d21ae71b218f2f23a0ed995a4adc69debcdb31e35ba8",
|
||||
"version": "0.9.2-29-g6a9a6a3"
|
||||
},
|
||||
{
|
||||
"file": "6341509efdc55c5fc04b1423ca81afdd9e4673d8.hex",
|
||||
"url": "https://raw.githubusercontent.com/Seeed-Studio/Adafruit_nRF52_Arduino/master/bootloader/Seeed_XIAO_nRF52840_Sense/Seeed_XIAO_nRF52840_Sense_bootloader-0.6.2_s140_7.3.0.hex",
|
||||
"sha256": "ac654c6cab225a933278c8be09b92b41bc4c044d064bd8c9c3314c1c4a0cc8c8",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "894f2e6df7663fcc5c398e210db7f2e7f21d3da53036e0a11576909c849f78fb",
|
||||
"version": "0.6.1"
|
||||
},
|
||||
{
|
||||
"file": "3c2e9de75c21b7f50ef7a99710c33df51cf7d337.hex",
|
||||
"url": "https://raw.githubusercontent.com/Seeed-Studio/Adafruit_nRF52_Arduino/master/bootloader/Seeed_XIAO_nRF52840_Sense_Plus/Seeed_XIAO_nRF52840_Sense_Plus_bootloader-0.6.2_s140_7.3.0.hex",
|
||||
"sha256": "b401776e6586f6065c46187d190dfa4acf9f7897a7d7c5f12a34662dff405e64",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "b19bd74a743a8311b12a07ae5cc17f28966d6e61b66f93f2afb2ceb52c5240d2",
|
||||
"version": "0.9.2-29-g6a9a6a3"
|
||||
},
|
||||
{
|
||||
"file": "9f030583154e432000b1cae8a18a411cac9db146.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Echo-Card/main/bootloader/T-Echo-Card%20bootloader/deprecated/%28%E5%88%A0%E9%99%A4%E5%90%AF%E5%8A%A8%E6%97%B6%E9%97%B4%29t_echo_card_nrf52840_bootloader-0.9.2-dirty_s140_6.1.1.hex",
|
||||
"sha256": "f54ce5e646f35257491a250712aefc3b1402e4526af5b5ed356bd395b918c364",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "5f9ccec2c1cb7cf85d4271f0a3c472a64d40458bf1e05779daf3d49b81e1fe98",
|
||||
"version": "0.9.2-dirty"
|
||||
},
|
||||
{
|
||||
"file": "291209d1108f1f61e5096edd64f633920556ef7f.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Echo-Card/main/bootloader/T-Echo-Card%20bootloader/deprecated/t_echo_card_nrf52840_bootloader-0.9.2-dirty_s140_6.1.1_202510090919.hex",
|
||||
"sha256": "6a7a64dc2b9337dfb76f6eeb2de3eaeeab7b953d46754afe5297ec0b1f78785f",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "17a6c1b72aaed448aa94a09aee36dbfb75343c559a2e84df849e476eee01d430",
|
||||
"version": "0.9.2-dirty"
|
||||
},
|
||||
{
|
||||
"file": "8c1a827f68e388cc067ba58542123db70e339f2a.hex",
|
||||
"url": "https://raw.githubusercontent.com/Xinyuan-LilyGO/T-Echo-Card/main/bootloader/T-Echo-Card%20bootloader/t_echo_card_nrf52840_bootloader-0.9.2-dirty_s140_6.1.1_202608131146.hex",
|
||||
"sha256": "c6b8025cb855d15fb06cbaba24d460a12bfc87b22df10667fb88d9ff13c5941e",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "efd403e7d211534a019467e9c39781bf49d4252fbb4850e8bb21fdc7895dab12",
|
||||
"version": "0.9.2-dirty"
|
||||
},
|
||||
{
|
||||
"file": "meshtiny.uf2",
|
||||
"url": "https://dl.mtoolstec.com/meshtiny/bootloader.uf2",
|
||||
"sha256": "7bdbb6eedaca1cf62d4479bb23b612964e838f6c06a6e67113ec9edde0244aee",
|
||||
"format": "uf2",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "8c666e29d64d4875b7f076574454e9ac99fc2e5eb25af06500ad05ee245d6440",
|
||||
"version": "0.4.5"
|
||||
},
|
||||
{
|
||||
"file": "nano-g2.zip",
|
||||
"url": "https://wiki.bqvoy.com/meshtastic/nano_g2_ultra/nano_g2_nrf52840_bootloader-0.6.4_s140_6.1.1.hex.zip",
|
||||
"sha256": "8f10154689f8e922062ace1fe2177513ca846c166b9a3eaa48e76387dc459f5d",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "c0607db64762d2b3203fcb13f3abed9adde1ec0f6ce2f5f204291f940511c5a5",
|
||||
"version": "0.6.4-dirty",
|
||||
"member": "nano_g2_nrf52840_bootloader-0.6.4_s140_6.1.1.hex"
|
||||
},
|
||||
{
|
||||
"file": "wio-l1.zip",
|
||||
"url": "https://files.seeedstudio.com/wiki/SenseCAP/Meshtastic/adafruit/wio_tracker_l1_bootloader-0.10.0_s140_7.3.0.zip",
|
||||
"sha256": "6d8fab06f8fd146653b93196ebb86203e336d74bb7f06d1dabc89f31d23783d1",
|
||||
"format": "dfu",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "4e9c4b9a9d610ccb7c7e9f16757eb51bedeeb2b30b438d5c1ad07a852f2a24b4",
|
||||
"version": "0.7.0-22-g277a0c8"
|
||||
},
|
||||
{
|
||||
"file": "t1000-e.zip",
|
||||
"url": "https://files.seeedstudio.com/wiki/SenseCAP/lorahub/t1000_e_bootloader-0.9.1-5-g488711a_s140_7.3.0.zip",
|
||||
"sha256": "87c4ba0721be16cca9b784ccb70ea41103100767978431ba47c692d2644bdccc",
|
||||
"format": "dfu",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "9f286a5c8c634a65e4f6d5b63c35eeb1466f900de9abb58ae109e67a82092562",
|
||||
"version": "0.9.1-5-g488711a"
|
||||
},
|
||||
{
|
||||
"file": "meshtracker-x1.zip",
|
||||
"url": "https://files.seeedstudio.com/wiki/SenseCAP/Meshcore/MeshTrackerX1/Bootloader.zip",
|
||||
"sha256": "69c246a8f29bc2ce7d1448581076be97a7232e8b548279ee6e1ec3a0d77f9e0c",
|
||||
"format": "dfu",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "082eb4cd2cd3db971b9bf4b659c220473cdf13b9400de6fac84b0b1fd62dc005",
|
||||
"version": "0.10.0-18-gb93789f"
|
||||
},
|
||||
{
|
||||
"file": "r1-neo.hex",
|
||||
"url": "https://github.com/muzi-works/Adafruit_nRF52_Bootloader/releases/download/0.9.2/r1_neo_board_bootloader-0.9.2-31-g990aa7f-dirty_s140_6.1.1.hex",
|
||||
"sha256": "54f77f1e2c6cf5675aec9730cff27087f3f838d7f7075dba5a266e6e0ac355ff",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "8bfe28094a824f08bf703bf3ccc929e0b21b0b37f94ccee3fbd89413b4eedfeb",
|
||||
"version": "0.9.2-31-g990aa7f-dirty"
|
||||
},
|
||||
{
|
||||
"file": "t114-vendor.hex",
|
||||
"url": "https://resource.heltec.cn/download/Mesh_Node_T114/firmware/HT-n5262-bootloader-20240709.hex",
|
||||
"sha256": "e950057bebc4beddafdf30955b2d473c4cc26ed008858cf114c613ba0c6c8f88",
|
||||
"format": "hex",
|
||||
"start": "0xf4000",
|
||||
"size": 40960,
|
||||
"image_sha256": "906fbbe787e18b789a695ae8bec6a5c31320f6948a656344c30bde0eaca1afbd",
|
||||
"version": "0.9.0-2-g836c8dc-dirty"
|
||||
}
|
||||
]
|
||||
}
|
||||
+30
-2
@@ -77,7 +77,8 @@ int main(int argc, char** argv) {
|
||||
bytes.assign(std::istreambuf_iterator<char>(file), {});
|
||||
}
|
||||
char output[128];
|
||||
if (!nrf52BootloaderVersion(bytes.data(), bytes.size(), 0xf4000, 0, output, sizeof(output))) return 3;
|
||||
const uint32_t start = argc > 2 ? (uint32_t)std::stoul(argv[2], nullptr, 0) : 0xf4000;
|
||||
if (!nrf52BootloaderVersion(bytes.data(), bytes.size(), start, 0, output, sizeof(output))) return 3;
|
||||
std::cout << output << '\n';
|
||||
return 0;
|
||||
}
|
||||
@@ -98,13 +99,40 @@ int main(int argc, char** argv) {
|
||||
// long release/preview/fork/build versions that the old 32-byte buffer cut.
|
||||
const char* versions[] = {"0.6.4", "0.9.2", "0.11.0-OTAFIX2.4.2", "0.11.0-OTAFIX2.4.6",
|
||||
"0.11.0-OTAFIX2.4.6-preview.12", "0.11.0-OTAFIX2.4.3-dirty-test-version-0x02040401",
|
||||
"v0.9.2-Seeed", "0.8.2+build.abcdef", "0.6.1_RAK4631"};
|
||||
"v0.9.2-Seeed", "0.8.2+build.abcdef", "0.6.1_RAK4631",
|
||||
// Embedded values from manufacturer images, not filename versions.
|
||||
"0.3.2-109-gd6b28e6", "0.4.1-13-g5e6690e-dirty", "0.4.2", "0.4.3", "0.4.4", "0.4.5",
|
||||
"0.9.0-2-g836c8dc-dirty", "0.6.2-26-g949425a-dirty", "0.6.1",
|
||||
"0.9.2-29-g6a9a6a3", "0.6.1-2-g1224915", "0.9.2-dirty", "0.6.4-dirty", "1.00",
|
||||
"0.7.0-22-g277a0c8", "0.9.1-5-g488711a", "0.10.0-18-gb93789f", "0.9.2-31-g990aa7f-dirty"};
|
||||
for (const char* version : versions) {
|
||||
for (size_t offset : {0x100u, 0x101u, 0x102u, 0x103u, 0x6ff0u, 0x7000u, 0x9e00u}) {
|
||||
auto bytes = image(); text(bytes, offset, version); expect(bytes, version);
|
||||
}
|
||||
}
|
||||
auto bytes = image();
|
||||
for (const char* bad : {"1", "1.", "1..00", "1.00.", "1.00..0", "v.1.00"}) {
|
||||
bytes = image(); text(bytes, 0x100, bad); expect(bytes, nullptr);
|
||||
}
|
||||
bytes = image();
|
||||
const std::string stock_boundary = "UF2 Bootloader 1.00\n";
|
||||
memcpy(bytes.data() + bytes.size() - stock_boundary.size(),
|
||||
stock_boundary.data(), stock_boundary.size());
|
||||
expect(bytes, "1.00"); // two-component record ending at the region boundary
|
||||
bytes.back() = '0'; expect(bytes, nullptr); // still require an in-bounds terminator
|
||||
bytes = image(); text(bytes, 0x100, "1.00");
|
||||
char stock_tiny[4];
|
||||
assert(!nrf52BootloaderVersion(bytes.data(), bytes.size(), 0xf4000, 0,
|
||||
stock_tiny, sizeof(stock_tiny)) && stock_tiny[0] == 0);
|
||||
bytes = image(); text(bytes, 0x100, "1.00"); text(bytes, 0x8000, "1.01");
|
||||
expect(bytes, nullptr); // conflicting two-component records remain unknown
|
||||
bytes = image(); standalone(bytes, 0x100, "1.00"); expect(bytes, nullptr);
|
||||
bytes = image(); standalone(bytes, 0x100, "v1.00"); expect(bytes, nullptr);
|
||||
// A generic regex must not pick a dependency version as the bootloader.
|
||||
for (const char* library : {"TinyUSB 0.12.0", "nrfx 2.0.0", "SoftDevice 7.3.0",
|
||||
"v0.9.2", "0.6.4", "1.00"}) {
|
||||
bytes = image(); standalone(bytes, 0x100, library); expect(bytes, nullptr);
|
||||
}
|
||||
// OTAFIX 2.4.9 and later board images use a standalone INFO_UF2 value.
|
||||
// Search only the first 40 KiB of the identified bootloader region.
|
||||
for (const char* version : {"v0.11.0-OTAFIX2.4.9", "v0.11.0-OTAFIX2.4.10",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
"""Compile the real version reader; optionally verify pinned OTAFIX releases."""
|
||||
"""Compile the real reader; optionally verify pinned stock/OTAFIX artifacts."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -10,9 +14,66 @@ import unittest
|
||||
import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FIXTURES = ROOT / "test/fixtures/nrf52_bootloader_version"
|
||||
SPEC = importlib.util.spec_from_file_location("nrf52_stock_artifacts", FIXTURES / "stock_artifacts.py")
|
||||
STOCK = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(STOCK)
|
||||
|
||||
|
||||
class BootloaderVersionTest(unittest.TestCase):
|
||||
def test_stock_artifact_manifest(self):
|
||||
records = json.loads((FIXTURES / "stock_images.json").read_text())["artifacts"]
|
||||
self.assertEqual(len(records), 26)
|
||||
self.assertEqual(len({record["image_sha256"] for record in records}), len(records))
|
||||
for record in records:
|
||||
with self.subTest(source=record["url"]):
|
||||
self.assertRegex(record["sha256"], r"^[0-9a-f]{64}$")
|
||||
self.assertRegex(record["image_sha256"], r"^[0-9a-f]{64}$")
|
||||
self.assertRegex(record["version"], r"^\d+\.\d+(?:\.\d+)?(?:[-_+].+)?$")
|
||||
self.assertIn(record["format"], ("hex", "uf2", "dfu"))
|
||||
self.assertEqual(record["start"], "0xf4000")
|
||||
self.assertEqual(record["size"], 40960)
|
||||
self.assertEqual(Path(record["file"]).name, record["file"])
|
||||
|
||||
def test_stock_artifact_decoders(self):
|
||||
record = {"start": "0xf4000", "size": 40960, "format": "hex"}
|
||||
def ihex(address, kind, payload):
|
||||
data = bytes((len(payload), address >> 8, address & 255, kind)) + payload
|
||||
return ":" + (data + bytes((-sum(data) & 255,))).hex() + "\n"
|
||||
raw = (ihex(0, 4, b"\x00\x0f") + ihex(0x4000, 0, b"abcd") +
|
||||
ihex(0, 1, b"")).encode()
|
||||
self.assertEqual(STOCK.bootloader_image(raw, record)[:8], b"abcd\xff\xff\xff\xff")
|
||||
with self.assertRaises(ValueError):
|
||||
STOCK.bootloader_image(raw.replace(b"61626364", b"61626365"), record)
|
||||
with self.assertRaises(ValueError):
|
||||
STOCK.bootloader_image(raw.split(b":00000001")[0], record)
|
||||
conflict = (ihex(0, 4, b"\x00\x0f") + ihex(0x4000, 0, b"a") +
|
||||
ihex(0x4000, 0, b"b") + ihex(0, 1, b"")).encode()
|
||||
with self.assertRaises(ValueError):
|
||||
STOCK.bootloader_image(conflict, record)
|
||||
record["format"] = "uf2"
|
||||
block = bytearray(512)
|
||||
struct.pack_into("<IIIIIIII", block, 0, 0x0a324655, 0x9e5d5157,
|
||||
0x2000, 0xf4000, 4, 0, 1, 0xada52840)
|
||||
block[32:36] = b"abcd"
|
||||
struct.pack_into("<I", block, 508, 0x0ab16f30)
|
||||
self.assertEqual(STOCK.bootloader_image(block, record)[:4], b"abcd")
|
||||
struct.pack_into("<I", block, 8, 0x2001)
|
||||
self.assertEqual(STOCK.bootloader_image(block, record), b"\xff" * 40960)
|
||||
struct.pack_into("<I", block, 8, 0x2000)
|
||||
struct.pack_into("<I", block, 12, 0x26000) # application-only UF2
|
||||
self.assertEqual(STOCK.bootloader_image(block, record), b"\xff" * 40960)
|
||||
block[-1] ^= 1
|
||||
with self.assertRaises(ValueError):
|
||||
STOCK.bootloader_image(block, record)
|
||||
record["format"] = "dfu"
|
||||
archive_bytes = io.BytesIO()
|
||||
with zipfile.ZipFile(archive_bytes, "w") as archive:
|
||||
archive.writestr("manifest.json", json.dumps({"manifest": {"softdevice_bootloader": {
|
||||
"sd_size": 2, "bl_size": 4, "bin_file": "sd_bl.bin"}}}))
|
||||
archive.writestr("sd_bl.bin", b"SDabcd")
|
||||
self.assertEqual(STOCK.bootloader_image(archive_bytes.getvalue(), record)[:4], b"abcd")
|
||||
|
||||
def test_firmware_uses_shared_reader(self):
|
||||
board = (ROOT / "src/helpers/NRF52Board.cpp").read_text()
|
||||
getter = board.split("bool NRF52Board::getBootloaderVersion(", 1)[1].split("\n}", 1)[0]
|
||||
@@ -41,6 +102,23 @@ class BootloaderVersionTest(unittest.TestCase):
|
||||
result = subprocess.run([str(exe)], capture_output=True, text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertIn("native cases passed", result.stdout)
|
||||
# Optional manufacturer downloads: exact hashes, normalized layout,
|
||||
# and exact reported versions, not merely a dotted-number regex.
|
||||
stock_cache = os.environ.get("MESHCORE_STOCK_BOOTLOADER_CACHE")
|
||||
if stock_cache:
|
||||
records = json.loads((FIXTURES / "stock_images.json").read_text())["artifacts"]
|
||||
for record in records:
|
||||
with self.subTest(stock_image=record["url"]):
|
||||
raw = (Path(stock_cache) / record["file"]).read_bytes()
|
||||
self.assertEqual(hashlib.sha256(raw).hexdigest(), record["sha256"])
|
||||
payload = STOCK.bootloader_image(raw, record)
|
||||
self.assertEqual(hashlib.sha256(payload).hexdigest(), record["image_sha256"])
|
||||
result = subprocess.run([str(exe), "--hex", record["start"]],
|
||||
input=payload.hex(), capture_output=True,
|
||||
text=True, timeout=20)
|
||||
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
||||
self.assertEqual(result.stdout.strip(), record["version"])
|
||||
print(f"Verified version reader against {len(records)} distinct manufacturer bootloader images")
|
||||
# Optional local/release artifacts; never downloaded or flashed by
|
||||
# this test. Each must be a raw bootloader image starting at F4000.
|
||||
for path in os.environ.get("MESHCORE_BOOTLOADER_IMAGES", "").split(os.pathsep):
|
||||
@@ -48,7 +126,7 @@ class BootloaderVersionTest(unittest.TestCase):
|
||||
with self.subTest(image=path):
|
||||
result = subprocess.run([str(exe), path], check=True, capture_output=True,
|
||||
text=True, timeout=20)
|
||||
self.assertRegex(result.stdout, r"\d+\.\d+\.\d+")
|
||||
self.assertRegex(result.stdout, r"\d+\.\d+(?:\.\d+)?")
|
||||
release = os.environ.get("MESHCORE_BOOTLOADER_MOTA_ZIP")
|
||||
if release:
|
||||
self.assertEqual(hashlib.sha256(Path(release).read_bytes()).hexdigest(),
|
||||
|
||||
Reference in new issue
Block a user