Support SenseCAP Solar P1 bootloader identity separately from XIAO
This commit is contained in:
1 parent
d808d964d2
commit
9da754b137
9 files changed
+478
-53
No files matched your search
@@ -262,16 +262,21 @@ jobs:
|
||||
- name: Verify bootloader version regressions without hardware or downloads
|
||||
run: python3 -B test/test_nrf52_bootloader_version.py -v
|
||||
|
||||
- name: Install host identity test dependencies
|
||||
run: |
|
||||
python3 -m pip install cryptography
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libssl-dev
|
||||
|
||||
- name: Verify embedded bootloader variant and guarded activation
|
||||
run: |
|
||||
python3 -B test/test_nrf52_embedded_variant.py -v
|
||||
python3 -B test/test_nrf52_embedded_bootloader.py -v
|
||||
python3 -B test/test_sensecap_bootloader_identity.py -v
|
||||
python3 -B test/test_sensecap_bootloader_runtime.py -v
|
||||
python3 -B tools/mota/test_bootloader_migration.py -v
|
||||
python3 -B tools/mota/test_bootloader_migration_runtime.py -v
|
||||
|
||||
- name: Install signed-package verification dependency
|
||||
run: python3 -m pip install cryptography
|
||||
|
||||
- name: Verify old-firmware compatibility in both OTA launchers
|
||||
run: python3 -B -m unittest discover -s tools/lora_ota -p test_lora_ota_bootloader_version.py -v
|
||||
|
||||
|
||||
@@ -34,8 +34,9 @@ MESHCORE_NRF52_EMBED_BOOTLOADER=2.4.12 pio run -e TARGET
|
||||
|
||||
The generator matches the board and storage to the 2.4.12 catalog and verifies
|
||||
the release asset before embedding only its 40 KiB bootloader region. Host
|
||||
validation covers 22 profiles. SenseCAP Solar P1 is excluded because its shared
|
||||
XIAO board ID and different device name need separate identity support.
|
||||
validation covers all 23 published profiles, including SenseCAP Solar P1.
|
||||
P1 uses its exact `SCAP_DFU` identity and S140 v7 layout; XIAO's `XIAO_DFU`
|
||||
identity remains separate despite the shared USB board ID.
|
||||
Unsupported profiles are refused. The usual application,
|
||||
capacity and memory checks still apply: the resource adds 40 KiB plus update
|
||||
code, so some profiles will not fit. The build does not remove features or
|
||||
@@ -60,6 +61,12 @@ SHA-256 is `64bfe69d56eefc2d0fe51770902aff70ea0cc06cad261d3d219e4a3c0efac8dc`.
|
||||
A matching version string alone is insufficient; another board's image or an
|
||||
unknown stock bootloader is not accepted by that exception.
|
||||
|
||||
A P1 already running a XIAO bootloader needs a deliberate identity migration;
|
||||
the embedded installer does not change one board identity into another. Use
|
||||
the documented temporary recovery bridge for the installed identity, followed
|
||||
by the normal P1 bootloader. The all-in-one P1 variant embeds the normal 2.4.12
|
||||
P1 image, not a recovery image.
|
||||
|
||||
## Install and activate
|
||||
|
||||
1. Install the application through its normal USB UF2 or serial/Bluetooth DFU
|
||||
|
||||
@@ -141,16 +141,10 @@ inline bool nrf52BootloaderVersion(const uint8_t* image, size_t size,
|
||||
identity.continuity_present) {
|
||||
const uint32_t version = identity.boot_version;
|
||||
const unsigned preview = version & 0xffu;
|
||||
int written;
|
||||
if (preview == 0xffu) {
|
||||
written = snprintf(out, capacity, "OTAFIX%u.%u.%u",
|
||||
(unsigned)(version >> 24), (unsigned)((version >> 16) & 0xffu),
|
||||
(unsigned)((version >> 8) & 0xffu));
|
||||
} else {
|
||||
written = snprintf(out, capacity, "OTAFIX%u.%u.%u-preview.%u",
|
||||
(unsigned)(version >> 24), (unsigned)((version >> 16) & 0xffu),
|
||||
(unsigned)((version >> 8) & 0xffu), preview);
|
||||
}
|
||||
const int written = snprintf(out, capacity,
|
||||
preview == 0xffu ? "OTAFIX%u.%u.%u" : "OTAFIX%u.%u.%u-preview.%u",
|
||||
(unsigned)(version >> 24), (unsigned)((version >> 16) & 0xffu),
|
||||
(unsigned)((version >> 8) & 0xffu), preview);
|
||||
if (written > 0 && (size_t)written < capacity) return true;
|
||||
out[0] = 0;
|
||||
return false;
|
||||
@@ -168,7 +162,6 @@ inline bool nrf52BootloaderVersion(const uint8_t* image, size_t size,
|
||||
const bool magic0 = ota::ota_boot_rd32(image + offset) == ota::OTA_BOOT_MANIFEST_MAGIC0;
|
||||
const bool magic1 = ota::ota_boot_rd32(image + offset + 4u) == ota::OTA_BOOT_MANIFEST_MAGIC1;
|
||||
if (magic0 || magic1) {
|
||||
ota::OtaBootloaderIdentity continuity;
|
||||
allow_standalone =
|
||||
magic0 && magic1 &&
|
||||
ota::ota_boot_rd16(image + offset + 8u) == ota::OTA_BOOT_MANIFEST_VERSION &&
|
||||
@@ -179,8 +172,8 @@ inline bool nrf52BootloaderVersion(const uint8_t* image, size_t size,
|
||||
ota::ota_boot_image_crc32(image, size, offset + 40u) ==
|
||||
ota::ota_boot_rd32(image + offset + 40u) &&
|
||||
ota::ota_bootloader_continuity_parse(image + offset, size - offset,
|
||||
continuity) &&
|
||||
continuity.continuity_present;
|
||||
identity) &&
|
||||
identity.continuity_present;
|
||||
}
|
||||
}
|
||||
const size_t scan_size = size < ota::OTA_BOOT_IMAGE_SIZE ? size : ota::OTA_BOOT_IMAGE_SIZE;
|
||||
|
||||
@@ -143,13 +143,16 @@ inline bool ota_bootloader_live_bank_preserves_scratch(uint32_t app_base,
|
||||
}
|
||||
#endif
|
||||
|
||||
// Only the two official XIAO nRF52840 OTAFIX identities are eligible. Runtime matching against the
|
||||
// installed, CRC-valid manifest distinguishes the base and Sense modules even for carrier-board aliases.
|
||||
// XIAO's deployed wire IDs remain stable. SenseCAP Solar P1 shares the base
|
||||
// VID/PID-derived board ID, so its distinct device name is part of its identity.
|
||||
static const uint32_t OTA_XIAO_BOARD_ID_BASE = 0x28860044UL;
|
||||
static const uint32_t OTA_XIAO_BOARD_ID_SENSE = 0x28860045UL;
|
||||
static const uint8_t OTA_XIAO_BOOT_DEVICE_NAME[OTA_BOOT_DEVICE_NAME_SIZE] = {
|
||||
'X','I','A','O','_','D','F','U', 0,0,0,0,0,0,0,0
|
||||
};
|
||||
static const uint8_t OTA_SENSECAP_BOOT_DEVICE_NAME[OTA_BOOT_DEVICE_NAME_SIZE] = {
|
||||
'S','C','A','P','_','D','F','U', 0,0,0,0,0,0,0,0
|
||||
};
|
||||
|
||||
struct OtaBootloaderIdentity {
|
||||
bool present = false;
|
||||
@@ -204,6 +207,11 @@ inline bool ota_xiao_bootloader_board_id(uint32_t board_id) {
|
||||
return board_id == OTA_XIAO_BOARD_ID_BASE || board_id == OTA_XIAO_BOARD_ID_SENSE;
|
||||
}
|
||||
|
||||
inline bool ota_xiao_bootloader_identity(uint32_t board_id, const uint8_t name[16]) {
|
||||
return ota_xiao_bootloader_board_id(board_id) && name &&
|
||||
memcmp(name, OTA_XIAO_BOOT_DEVICE_NAME, OTA_BOOT_DEVICE_NAME_SIZE) == 0;
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_board_id_valid(uint32_t board_id) {
|
||||
return board_id != 0u && board_id != UINT32_MAX;
|
||||
}
|
||||
@@ -213,8 +221,13 @@ inline bool ota_bootloader_board_id_valid(uint32_t board_id) {
|
||||
// identities retain their exact legacy name and signed hardware IDs.
|
||||
inline bool ota_bootloader_device_name_valid(uint32_t board_id, const uint8_t name[16]) {
|
||||
if (!name) return false;
|
||||
if (ota_xiao_bootloader_board_id(board_id))
|
||||
return memcmp(name, OTA_XIAO_BOOT_DEVICE_NAME, OTA_BOOT_DEVICE_NAME_SIZE) == 0;
|
||||
if (ota_xiao_bootloader_board_id(board_id)) {
|
||||
// Both qualified names share the complete suffix and zero padding.
|
||||
if (memcmp(name + 4, OTA_XIAO_BOOT_DEVICE_NAME + 4, OTA_BOOT_DEVICE_NAME_SIZE - 4) != 0)
|
||||
return false;
|
||||
return memcmp(name, "XIAO", 4) == 0 ||
|
||||
(board_id == OTA_XIAO_BOARD_ID_BASE && memcmp(name, "SCAP", 4) == 0);
|
||||
}
|
||||
size_t end = 0;
|
||||
while (end < OTA_BOOT_DEVICE_NAME_SIZE && name[end] != 0) {
|
||||
if (name[end] < 0x21u || name[end] > 0x7Eu) return false;
|
||||
@@ -245,7 +258,8 @@ inline bool ota_bootloader_hw_id(uint32_t board_id, const uint8_t device_name[16
|
||||
uint8_t out[32]) {
|
||||
if (!out || !ota_bootloader_board_id_valid(board_id) ||
|
||||
!ota_bootloader_device_name_valid(board_id, device_name)) return false;
|
||||
if (ota_xiao_bootloader_board_id(board_id)) return ota_xiao_bootloader_hw_id(board_id, out);
|
||||
if (ota_xiao_bootloader_identity(board_id, device_name))
|
||||
return ota_xiao_bootloader_hw_id(board_id, out);
|
||||
static const char hex[] = "0123456789ABCDEF";
|
||||
memset(out, 0, 32);
|
||||
memcpy(out, "NRF_BL_", 7);
|
||||
@@ -261,7 +275,7 @@ inline bool ota_bootloader_hw_id(uint32_t board_id, const uint8_t device_name[16
|
||||
inline bool ota_bootloader_hw_id(const OtaBootloaderIdentity& identity, uint8_t out[32]);
|
||||
|
||||
inline uint32_t ota_bootloader_target_id(uint32_t board_id, const uint8_t device_name[16]) {
|
||||
if (ota_xiao_bootloader_board_id(board_id)) return board_id;
|
||||
if (ota_xiao_bootloader_identity(board_id, device_name)) return board_id;
|
||||
uint8_t hw_id[32], digest[4];
|
||||
if (!ota_bootloader_hw_id(board_id, device_name, hw_id)) return 0;
|
||||
mh4(digest, hw_id, sizeof(hw_id));
|
||||
|
||||
+10
-1
@@ -164,10 +164,19 @@ int main(int argc, char** argv) {
|
||||
put32(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 20, OTA_XIAO_BOARD_ID_BASE);
|
||||
memset(bytes.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 24, 0, 16);
|
||||
memcpy(bytes.data() + OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 24, "SCAP_DFU", 8);
|
||||
put16(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 62, 0x123);
|
||||
put32(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 64, 0x27000);
|
||||
refreshManifestCrc(bytes);
|
||||
OtaBootloaderIdentity sensecap;
|
||||
assert(ota_bootloader_identity_from_image(bytes.data(), bytes.size(), sensecap));
|
||||
assert(sensecap.board_id == OTA_XIAO_BOARD_ID_BASE);
|
||||
assert(strcmp(sensecap.device_name, "SCAP_DFU") == 0);
|
||||
expect(bytes, "OTAFIX2.4.9"); // exact SenseCAP P1 shares the base USB board ID
|
||||
put32(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 20, OTA_XIAO_BOARD_ID_SENSE);
|
||||
refreshManifestCrc(bytes);
|
||||
OtaBootloaderIdentity unexpected_board;
|
||||
assert(!ota_bootloader_identity_from_image(bytes.data(), bytes.size(), unexpected_board));
|
||||
expect(bytes, "v0.11.0-OTAFIX2.4.9"); // diagnostic fallback for an unexpected board name
|
||||
expect(bytes, "v0.11.0-OTAFIX2.4.9"); // SCAP_DFU is not valid on the Sense board ID
|
||||
bytes = image(); standalone(bytes, 0x100, "v0.11.0-OTAFIX2.4.9");
|
||||
manifest(bytes, 0x020409ff);
|
||||
put16(bytes, OTA_BOOT_CANDIDATE_MANIFEST_OFFSET + 8, 0);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
"""SenseCAP Solar P1 keeps an exact identity distinct from deployed XIAOs."""
|
||||
import copy
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "tools/mota"))
|
||||
import motalib as ml
|
||||
from test_nrf52_embedded_bootloader import boot_image, candidate, profile
|
||||
import nrf52_embedded_bootloader as embed
|
||||
|
||||
|
||||
P1_IDENTITY = (0x28860044, "SCAP_DFU")
|
||||
P1_TARGET = 0xD677B472
|
||||
P1_HW_ID = b"NRF_BL_28860044_SCAP_DFU".ljust(32, b"\0")
|
||||
|
||||
|
||||
class SensecapBootloaderIdentityTest(unittest.TestCase):
|
||||
def image(self, **kwargs):
|
||||
return boot_image(v7=True, name="SCAP_DFU", **kwargs)
|
||||
|
||||
def package_options(self, image=None):
|
||||
image = self.image() if image is None else image
|
||||
return dict(target_id=P1_TARGET, fw_version=embed.BOOT_VERSION,
|
||||
image_size=len(image), payload=image, block_size=1024,
|
||||
image_hash=ml.mh32(image), codec_id=ml.CODEC_FULL,
|
||||
is_full=True, sign_priv=Ed25519PrivateKey.generate(), bootloader=True)
|
||||
|
||||
def test_p1_parses_and_uses_its_generic_exact_identity(self):
|
||||
image = self.image()
|
||||
identity = ml.validate_bootloader_image(image, target_id=P1_TARGET, signed_hw_id=P1_HW_ID)
|
||||
self.assertEqual((identity.board_id, identity.device_name), P1_IDENTITY)
|
||||
self.assertEqual((identity.softdevice_family, identity.softdevice_fwid, identity.app_base,
|
||||
identity.layout_abi), (140, 0x123, 0x27000, 1))
|
||||
self.assertEqual(ml.bootloader_target_id(*P1_IDENTITY), P1_TARGET)
|
||||
self.assertEqual(ml.bootloader_hw_id(*P1_IDENTITY), P1_HW_ID)
|
||||
self.assertTrue(ml.bootloader_identity_is_buildable(*P1_IDENTITY))
|
||||
self.assertEqual(ml.bootloader_qualified_storage_profiles(*P1_IDENTITY), (0x0E,))
|
||||
self.assertEqual(ml.bootloader_qualified_platform_profile(*P1_IDENTITY), (140, 0x123, 0x27000, 1))
|
||||
|
||||
def test_signed_p1_package_builds_parses_and_verifies(self):
|
||||
options = self.package_options()
|
||||
manifest = ml.build_manifest(**options)
|
||||
parsed = ml.parse_container(ml.build_container(manifest, options["payload"]))
|
||||
self.assertEqual(parsed.manifest.target_id, P1_TARGET)
|
||||
self.assertEqual(parsed.manifest.hw_id, P1_HW_ID)
|
||||
self.assertEqual(ml.verify(parsed, expect_pub=ml.ed25519_public_bytes(options["sign_priv"])), [])
|
||||
|
||||
def test_existing_xiao_wire_ids_and_wrappers_remain_unchanged(self):
|
||||
for board_id, expected in ((0x28860044, b"XIAO_BL_28860044"),
|
||||
(0x28860045, b"XIAO_BL_28860045")):
|
||||
with self.subTest(board_id=hex(board_id)):
|
||||
image = boot_image(v7=True, board_id=board_id)
|
||||
self.assertEqual(ml.bootloader_target_id(board_id, "XIAO_DFU"), board_id)
|
||||
self.assertEqual(ml.bootloader_hw_id(board_id, "XIAO_DFU"), expected.ljust(32, b"\0"))
|
||||
self.assertEqual(ml.parse_xiao_bootloader_identity(image).device_name, "XIAO_DFU")
|
||||
self.assertEqual(ml.validate_xiao_bootloader_image(image, board_id).board_id, board_id)
|
||||
self.assertIsNone(ml.parse_xiao_bootloader_identity(self.image()))
|
||||
with self.assertRaisesRegex(ValueError, "not a deployed XIAO"):
|
||||
ml.validate_xiao_bootloader_image(self.image())
|
||||
|
||||
def test_only_the_exact_p1_pair_is_admitted_on_shared_xiao_ids(self):
|
||||
for board_id, name in ((0x28860045, "SCAP_DFU"), (0x28860044, "OTHER_DFU"),
|
||||
(0x28860045, "OTHER_DFU"), (0x28860044, "R_SCAP_DFU"),
|
||||
(0x28860044, "SCAP_DFU_V2"), (0x28860044, "SCAP_DFU\0X")):
|
||||
with self.subTest(board_id=hex(board_id), name=name):
|
||||
image = boot_image(v7=True, board_id=board_id, name=name)
|
||||
self.assertIsNone(ml.parse_bootloader_identity(image))
|
||||
self.assertFalse(ml.bootloader_identity_is_buildable(board_id, name))
|
||||
with self.assertRaises(ValueError):
|
||||
ml.bootloader_hw_id(board_id, name)
|
||||
with self.assertRaises(ValueError):
|
||||
ml.bootloader_target_id(board_id, name)
|
||||
with self.assertRaises(ValueError):
|
||||
ml.validate_bootloader_image(image)
|
||||
|
||||
def test_cross_board_wire_ids_cannot_be_used_to_sign_p1_or_xiao(self):
|
||||
for changes in ({"target_id": 0x28860044}, {"target_id": 0x28860045},
|
||||
{"hw_id": "XIAO_BL_28860044"}, {"hw_id": "XIAO_BL_28860045"}):
|
||||
with self.subTest(changes=changes):
|
||||
options = self.package_options()
|
||||
options.update(changes)
|
||||
with self.assertRaises(ValueError):
|
||||
ml.build_manifest(**options)
|
||||
options = self.package_options(boot_image(v7=True))
|
||||
with self.assertRaisesRegex(ValueError, "target ID"):
|
||||
ml.build_manifest(**options)
|
||||
|
||||
def test_p1_rejects_wrong_softdevice_storage_and_corrupt_crc(self):
|
||||
for image in (boot_image(name="SCAP_DFU", board_id=0x28860044),
|
||||
self.image(app_base=0x26000), self.image(storage=0x0A)):
|
||||
with self.subTest(digest=hashlib.sha256(image).hexdigest()), self.assertRaises(ValueError):
|
||||
ml.validate_bootloader_image(image)
|
||||
corrupt = bytearray(self.image())
|
||||
corrupt[0x300] ^= 1
|
||||
self.assertIsNone(ml.parse_bootloader_identity(bytes(corrupt)))
|
||||
|
||||
def test_p1_target_is_disjoint_from_all_generated_application_targets(self):
|
||||
header = (ROOT / "src/helpers/ota/OtaTargets.h").read_text()
|
||||
application_ids = {int(value, 16) for value in re.findall(r"\{ 0x([0-9a-fA-F]{8}),", header)}
|
||||
self.assertTrue(application_ids)
|
||||
inventory = ml.audit_bootloader_target_inventory(application_ids)
|
||||
self.assertEqual(inventory[P1_TARGET], P1_IDENTITY)
|
||||
self.assertEqual(inventory[0x28860044], (0x28860044, "XIAO_DFU"))
|
||||
self.assertEqual(inventory[0x28860045], (0x28860045, "XIAO_DFU"))
|
||||
with self.assertRaisesRegex(ValueError, "collides with an application"):
|
||||
ml.audit_bootloader_target_inventory(application_ids | {P1_TARGET})
|
||||
|
||||
def test_embedding_accepts_p1_normal_asset_and_rejects_recovery_substitution(self):
|
||||
data = candidate(self.image(), v7=True)
|
||||
item = profile(data, v7=True, name="SCAP_DFU")
|
||||
image, identity = embed.validate_candidate_zip(data, item)
|
||||
self.assertEqual(image, self.image())
|
||||
self.assertEqual(identity.device_name, "SCAP_DFU")
|
||||
# Even a CRC-valid same-name image cannot replace the pinned normal ZIP.
|
||||
different = candidate(self.image(boot_version=0x02040BFF), v7=True)
|
||||
with self.assertRaisesRegex(ValueError, "pinned release asset"):
|
||||
embed.validate_candidate_zip(different, item)
|
||||
recovery = copy.deepcopy(item)
|
||||
recovery["files"]["zip"]["url"] = recovery["files"]["zip"]["url"].replace(
|
||||
"/releases/download/", "/releases/download/R_")
|
||||
with self.assertRaisesRegex(ValueError, "pinned bootloader ZIP metadata"):
|
||||
embed.validate_candidate_zip(data, recovery)
|
||||
|
||||
def test_cached_production_2_4_12_p1_asset_if_available(self):
|
||||
manifest = json.loads((ROOT / "docs/_data/bootloader_manifest.json").read_text())
|
||||
item = next(item for item in manifest["profiles"] if item["id"] == "sensecap_solar_p1")
|
||||
asset = item["files"]["zip"]
|
||||
self.assertEqual(asset["sha256"], "694fca8e68fb695ac1a5ef701baf26e8d52a7eb188f886496dcb6bc8b6fdc7cb")
|
||||
path = ROOT / ".pio/otafix-cache" / (asset["sha256"] + "-" + asset["name"])
|
||||
if not path.is_file():
|
||||
self.skipTest("published P1 ZIP is not cached; offline fixtures cover validation")
|
||||
image, identity = embed.validate_candidate_zip(path.read_bytes(), item)
|
||||
self.assertEqual(hashlib.sha256(image).hexdigest(),
|
||||
"d33c035c783e555049d2b41fe2f12367c6a87ee30b041c627d19088a2a6d8855")
|
||||
self.assertEqual((identity.board_id, identity.device_name), P1_IDENTITY)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,249 @@
|
||||
"""Exercise exact P1/XIAO bootloader identities in the real C++ validators."""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zlib
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / 'tools/mota'))
|
||||
from test_mota import _set_two_valid_xiao_manifest_crcs, _xiao_manifest_crc
|
||||
|
||||
SIZE, OFFSET = 40960, 40960 - 76
|
||||
|
||||
|
||||
def bootloader(name='SCAP_DFU', board=0x28860044, version=0x02040CFF):
|
||||
image = bytearray(b'\xff' * SIZE)
|
||||
struct.pack_into('<II', image, 0, 0x20030000, 0xF4101)
|
||||
image[0x100:0x110] = b'MOTABLDR' + struct.pack('<HHBBBB', 3, 5, 0x0E, 0, 0, 0)
|
||||
struct.pack_into('<8sHHIII16sI', image, OFFSET, b'BLMFCRC1', 1, 44,
|
||||
0xF4000, SIZE, board, name.encode().ljust(16, b'\0'), 0)
|
||||
struct.pack_into('<8sHHIHHIHHI', image, OFFSET + 44, b'BLM2SOFT', 2, 32,
|
||||
version, 140, 0x123, 0x27000, 1, 0, 0)
|
||||
struct.pack_into('<I', image, OFFSET + 40, zlib.crc32(image) & 0xFFFFFFFF)
|
||||
return bytes(image)
|
||||
|
||||
|
||||
SOURCE = r'''
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <openssl/evp.h>
|
||||
#include "helpers/ota/BootloaderMigrationPolicy.h"
|
||||
namespace mesh {
|
||||
void Utils::sha256(uint8_t *out,size_t count,const uint8_t *a,int alen,const uint8_t *b,int blen) {
|
||||
assert(count<=32 && alen>=0 && blen>=0);
|
||||
uint8_t digest[32]; unsigned size=0;
|
||||
EVP_MD_CTX *ctx=EVP_MD_CTX_new(); assert(ctx);
|
||||
assert(EVP_DigestInit_ex(ctx,EVP_sha256(),nullptr)==1);
|
||||
assert(EVP_DigestUpdate(ctx,a,alen)==1);
|
||||
if (blen) assert(EVP_DigestUpdate(ctx,b,blen)==1);
|
||||
assert(EVP_DigestFinal_ex(ctx,digest,&size)==1 && size==32);
|
||||
EVP_MD_CTX_free(ctx); std::memcpy(out,digest,count);
|
||||
}
|
||||
void Utils::sha256(uint8_t *out,size_t count,const uint8_t *a,int length) {
|
||||
sha256(out,count,a,length,nullptr,0);
|
||||
}
|
||||
}
|
||||
using namespace mesh::ota;
|
||||
static std::vector<uint8_t> load(const std::string &directory,const char *name) {
|
||||
std::ifstream input(directory+"/"+name,std::ios::binary); assert(input.good());
|
||||
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(input)),{});
|
||||
assert(bytes.size()==OTA_BOOT_IMAGE_SIZE); return bytes;
|
||||
}
|
||||
static OtaBootloaderIdentity parse(const std::vector<uint8_t> &bytes) {
|
||||
OtaBootloaderIdentity id;
|
||||
assert(ota_bootloader_vector_sane(bytes.data()));
|
||||
assert(ota_bootloader_candidate_identity_from_image(bytes.data(),bytes.size(),id));
|
||||
return id;
|
||||
}
|
||||
struct Store {
|
||||
const std::vector<uint8_t> &bytes;
|
||||
bool read(uint32_t offset,uint8_t *out,uint32_t size) {
|
||||
if (offset>bytes.size() || size>bytes.size()-offset) return false;
|
||||
std::memcpy(out,bytes.data()+offset,size); return true;
|
||||
}
|
||||
};
|
||||
int main(int argc,char **argv) {
|
||||
assert(argc==3);
|
||||
const std::string directory=argv[1], scenario=argv[2];
|
||||
auto image=load(directory,"p1.bin"); auto p1=parse(image);
|
||||
auto base=parse(load(directory,"xiao-base.bin"));
|
||||
auto sense=parse(load(directory,"xiao-sense.bin"));
|
||||
auto old=parse(load(directory,"p1-old.bin"));
|
||||
const uint8_t p1_name[16]={'S','C','A','P','_','D','F','U'};
|
||||
const uint8_t xiao_name[16]={'X','I','A','O','_','D','F','U'};
|
||||
if (scenario=="canonical") {
|
||||
assert(p1.board_id==0x28860044 && p1.crc_ok && p1.continuity_present);
|
||||
assert(p1.softdevice_fwid==0x123 && p1.app_base==0x27000 && p1.boot_version==0x02040CFF);
|
||||
uint8_t hw[32]; assert(ota_bootloader_hw_id(p1,hw));
|
||||
const char expected[32]="NRF_BL_28860044_SCAP_DFU";
|
||||
assert(!std::memcmp(hw,expected,32));
|
||||
assert(ota_bootloader_target_id(p1)==0xD677B472);
|
||||
assert(ota_bootloader_target_id(p1)!=base.board_id);
|
||||
} else if (scenario=="legacy") {
|
||||
for (const auto &id:{base,sense}) {
|
||||
uint8_t hw[32]; assert(ota_bootloader_hw_id(id,hw));
|
||||
const char expected_base[32]="XIAO_BL_28860044", expected_sense[32]="XIAO_BL_28860045";
|
||||
assert(!std::memcmp(hw,id.board_id==base.board_id?expected_base:expected_sense,32));
|
||||
assert(ota_bootloader_target_id(id)==id.board_id);
|
||||
assert(ota_xiao_bootloader_identity(id.board_id,xiao_name));
|
||||
}
|
||||
assert(!ota_xiao_bootloader_identity(p1.board_id,p1_name));
|
||||
assert(!ota_bootloader_device_name_valid(0x28860045,p1_name));
|
||||
assert(ota_bootloader_target_id(0x28860045,p1_name)==0);
|
||||
uint8_t wrong[16]={'B','O','G','U','S'};
|
||||
assert(!ota_bootloader_device_name_valid(0x28860044,wrong));
|
||||
assert(ota_bootloader_target_id(0x28860044,wrong)==0);
|
||||
uint8_t unpadded[16]; std::memcpy(unpadded,p1_name,16); unpadded[15]=1;
|
||||
assert(!ota_bootloader_device_name_valid(0x28860044,unpadded));
|
||||
assert(ota_bootloader_target_id(0x28860044,unpadded)==0);
|
||||
} else if (scenario=="qspi") {
|
||||
Store store{image}; OtaBootloaderIdentity parsed; OtaBootloaderCapsMarker caps;
|
||||
assert(ota_bootloader_external_image_metadata(store,0,0x0E,parsed,caps));
|
||||
assert(ota_bootloader_identity_matches(p1,parsed) && caps.storage_flags==0x0E);
|
||||
assert(!ota_bootloader_external_image_metadata(store,0,0x0A,parsed,caps));
|
||||
image[0x200]^=1;
|
||||
assert(!ota_bootloader_candidate_identity_from_image(image.data(),image.size(),parsed));
|
||||
assert(!ota_bootloader_external_image_metadata(store,0,0x0E,parsed,caps));
|
||||
} else if (scenario=="migration") {
|
||||
using namespace mesh::ota::migration;
|
||||
assert(candidate_matches_resource(p1,0x28860044,reinterpret_cast<const char*>(p1_name),
|
||||
p1.boot_version,0x27000,0x123));
|
||||
assert(installed_can_migrate(old,p1,false,false,0x27000,0x123));
|
||||
assert(!installed_can_migrate(base,p1,true,true,0x27000,0x123));
|
||||
assert(!installed_can_migrate(p1,base,true,true,0x27000,0x123));
|
||||
assert(!installed_can_migrate(sense,p1,true,true,0x27000,0x123));
|
||||
assert(!installed_can_migrate(p1,p1,true,true,0x27000,0x123));
|
||||
assert(!installed_can_migrate(p1,old,true,true,0x27000,0x123));
|
||||
assert(!installed_can_migrate(old,p1,false,false,0x26000,0xB6));
|
||||
} else if (scenario=="duplicate") {
|
||||
image=load(directory,"duplicate.bin"); OtaBootloaderIdentity id;
|
||||
OtaBootloaderIdentity first,second;
|
||||
assert(ota_bootloader_manifest_parse(image.data()+0x400,0x400,first,76));
|
||||
assert(ota_bootloader_manifest_parse(image.data()+OTA_BOOT_CANDIDATE_MANIFEST_OFFSET,
|
||||
OTA_BOOT_CANDIDATE_MANIFEST_OFFSET,second,76));
|
||||
assert(ota_boot_image_crc32(image.data(),image.size(),0x400+40)==first.crc32);
|
||||
assert(ota_boot_image_crc32(image.data(),image.size(),OTA_BOOT_CANDIDATE_MANIFEST_OFFSET+40)==second.crc32);
|
||||
assert(!ota_bootloader_identity_from_image(image.data(),image.size(),id));
|
||||
Store store{image}; OtaBootloaderCapsMarker caps;
|
||||
assert(!ota_bootloader_external_image_metadata(store,0,0x0E,id,caps));
|
||||
} else {
|
||||
uint8_t hw[32],xhw[32],digest[32],zero[8]={},mid[4]={1,2,3,4};
|
||||
assert(ota_bootloader_hw_id(p1,hw) && ota_bootloader_hw_id(base,xhw));
|
||||
mesh::Utils::sha256(digest,32,image.data(),image.size());
|
||||
MotaManifest manifest;
|
||||
manifest.format_ver=3; manifest.flags=MFLAG_FULL|MFLAG_SIGNED|MFLAG_BOOTLOADER;
|
||||
manifest.codec_id=CODEC_FULL; manifest.image_size=manifest.payload_size=OTA_BOOT_IMAGE_SIZE;
|
||||
manifest.block_size_log2=10; manifest.block_count=40; manifest.base_hash=zero;
|
||||
manifest.target_id=ota_bootloader_target_id(p1); manifest.hw_id=hw;
|
||||
manifest.merkle_root=mid; manifest.image_hash=digest;
|
||||
assert(ota_bootloader_confirmation_gate(manifest,old,mid,mid,digest)==OTA_BOOT_CONFIRM_OK);
|
||||
assert(ota_bootloader_confirmation_gate(manifest,base,mid,mid,digest)==OTA_BOOT_CONFIRM_TARGET);
|
||||
manifest.target_id=base.board_id;
|
||||
assert(ota_bootloader_confirmation_gate(manifest,old,mid,mid,digest)==OTA_BOOT_CONFIRM_TARGET);
|
||||
manifest.target_id=ota_bootloader_target_id(p1); manifest.hw_id=xhw;
|
||||
assert(ota_bootloader_confirmation_gate(manifest,old,mid,mid,digest)==OTA_BOOT_CONFIRM_HW_ID);
|
||||
manifest.hw_id=hw; uint8_t wrong[8]={};
|
||||
assert(ota_bootloader_confirmation_gate(manifest,old,mid,wrong,digest)==OTA_BOOT_CONFIRM_MID);
|
||||
assert(ota_bootloader_confirmation_gate(manifest,old,mid,mid,wrong)==OTA_BOOT_CONFIRM_IMAGE_HASH);
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
UTILS = '''#pragma once
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
namespace mesh { class Utils { public:
|
||||
static void sha256(uint8_t*,size_t,const uint8_t*,int);
|
||||
static void sha256(uint8_t*,size_t,const uint8_t*,int,const uint8_t*,int);
|
||||
}; }
|
||||
'''
|
||||
|
||||
|
||||
class SensecapBootloaderRuntimeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.directory = tempfile.TemporaryDirectory(prefix='sensecap-runtime-')
|
||||
cls.addClassCleanup(cls.directory.cleanup)
|
||||
cls.path = Path(cls.directory.name)
|
||||
(cls.path / 'Utils.h').write_text(UTILS)
|
||||
(cls.path / 'test.cpp').write_text(SOURCE)
|
||||
for name, image in {
|
||||
'p1.bin': bootloader(), 'p1-old.bin': bootloader(version=0x02040BFF),
|
||||
'xiao-base.bin': bootloader('XIAO_DFU', version=0x02040BFF),
|
||||
'xiao-sense.bin': bootloader('XIAO_DFU', 0x28860045, 0x02040BFF),
|
||||
}.items():
|
||||
(cls.path / name).write_bytes(image)
|
||||
duplicate = bytearray(bootloader())
|
||||
duplicate[0x400:0x400+76] = bootloader('XIAO_DFU')[OFFSET:]
|
||||
_set_two_valid_xiao_manifest_crcs(duplicate, 0x400, OFFSET)
|
||||
assert _xiao_manifest_crc(duplicate, 0x400) == struct.unpack_from('<I', duplicate, 0x428)[0]
|
||||
(cls.path / 'duplicate.bin').write_bytes(duplicate)
|
||||
cls.binary = cls.compile(cls.path)
|
||||
|
||||
@staticmethod
|
||||
def compile(path, include=None):
|
||||
flags = [] if os.name == 'nt' else [
|
||||
'-fsanitize=address,undefined', '-fno-sanitize-recover=all', '-fno-pie', '-no-pie']
|
||||
binary = path / 'runtime'
|
||||
command = [os.environ.get('CXX', 'g++'), '-std=c++17', '-O1', '-Wall', '-Wextra', '-Werror', *flags]
|
||||
if include:
|
||||
command += ['-I', str(include)]
|
||||
command += ['-I', str(path), '-I', str(ROOT / 'src'), '-I', str(ROOT / 'src/helpers/ota'),
|
||||
str(path / 'test.cpp'), '-lcrypto', '-o', str(binary)]
|
||||
subprocess.run(command, capture_output=True, text=True, check=True)
|
||||
return binary
|
||||
|
||||
def scenario(self, name):
|
||||
subprocess.run([str(self.binary), str(self.path), name], check=True)
|
||||
|
||||
def test_p1_canonical_signed_identity_uses_real_sha256(self):
|
||||
hw = b'NRF_BL_28860044_SCAP_DFU'.ljust(32, b'\0')
|
||||
self.assertEqual(int.from_bytes(hashlib.sha256(hw).digest()[:4], 'little'), 0xD677B472)
|
||||
self.scenario('canonical')
|
||||
|
||||
def test_legacy_xiao_pairing_and_ids_remain_exact(self):
|
||||
self.scenario('legacy')
|
||||
|
||||
def test_real_qspi_scanner_requires_crc_and_exact_storage(self):
|
||||
self.scenario('qspi')
|
||||
|
||||
def test_embedded_migration_requires_exact_identity_and_strict_upgrade(self):
|
||||
self.scenario('migration')
|
||||
|
||||
def test_two_independently_crc_valid_identities_are_rejected(self):
|
||||
self.scenario('duplicate')
|
||||
|
||||
def test_signed_package_confirmation_cannot_cross_xiao_and_p1(self):
|
||||
self.scenario('confirmation')
|
||||
|
||||
def test_negative_control_board_only_target_shortcut_is_caught(self):
|
||||
header = (ROOT / 'src/helpers/ota/OtaBootloaderUpdate.h').read_text()
|
||||
changed = header.replace('if (ota_xiao_bootloader_identity(board_id, device_name)) return board_id;',
|
||||
'if (ota_xiao_bootloader_board_id(board_id)) return board_id;')
|
||||
self.assertNotEqual(header, changed)
|
||||
with tempfile.TemporaryDirectory(prefix='sensecap-shortcut-negative-') as directory:
|
||||
path = Path(directory)
|
||||
headers = path / 'helpers/ota'
|
||||
headers.mkdir(parents=True)
|
||||
(headers / 'OtaBootloaderUpdate.h').write_text(changed)
|
||||
(headers / 'BootloaderMigrationPolicy.h').write_text(
|
||||
(ROOT / 'src/helpers/ota/BootloaderMigrationPolicy.h').read_text())
|
||||
(path / 'Utils.h').write_text(UTILS)
|
||||
(path / 'test.cpp').write_text(SOURCE)
|
||||
binary = self.compile(path, path)
|
||||
result = subprocess.run([str(binary), str(self.path), 'canonical'], capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn('ota_bootloader_target_id(p1)==0xD677B472', result.stderr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
+25
-20
@@ -90,6 +90,13 @@ NRF52_LAYOUT_FLAG_AUTO_STORE = 0x20
|
||||
|
||||
XIAO_BOOT_BOARD_ID_BASE = 0x28860044
|
||||
XIAO_BOOT_BOARD_ID_SENSE = 0x28860045
|
||||
# Solar P1 shares XIAO's USB-derived board ID, but retains its own name and wire identity.
|
||||
XIAO_BOOTLOADER_IDENTITIES = (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
)
|
||||
SENSECAP_BOOTLOADER_IDENTITY = (XIAO_BOOT_BOARD_ID_BASE, "SCAP_DFU")
|
||||
QSPI_BOOTLOADER_IDENTITIES = XIAO_BOOTLOADER_IDENTITIES + (SENSECAP_BOOTLOADER_IDENTITY,)
|
||||
XIAO_BOOT_IMAGE_START = 0x000F4000
|
||||
XIAO_BOOT_IMAGE_SIZE = 0x0000A000
|
||||
XIAO_BOOT_MANIFEST_MAGIC = b"BLMFCRC1"
|
||||
@@ -155,12 +162,10 @@ SD_BOOTLOADER_IDENTITIES = (
|
||||
# Exact runtime continuity profiles for signed, remotely installable
|
||||
# bootloaders. A CRC-valid BLM2 envelope is not sufficient for package
|
||||
# creation: signing the wrong SoftDevice/application layout only produces a
|
||||
# package every qualified device will refuse. XIAO/Sense, Minew MX25LE01, and
|
||||
# T1000-E run S140 v7; the remaining inventory (including both Tower storage
|
||||
# package every qualified device will refuse. XIAO/Sense, Solar P1, Minew
|
||||
# MX25LE01, and T1000-E run S140 v7; the remaining inventory (including both Tower storage
|
||||
# profiles) runs S140 v6.
|
||||
BOOTLOADER_S140_V7_IDENTITIES = (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
BOOTLOADER_S140_V7_IDENTITIES = QSPI_BOOTLOADER_IDENTITIES + (
|
||||
(0x239A0029, "MX25_DFU"),
|
||||
(0x28860057, "T1KE_DFU"),
|
||||
)
|
||||
@@ -666,7 +671,12 @@ def _bootloader_device_name(board_id: int, name_raw: bytes) -> Optional[str]:
|
||||
if len(name_raw) != 16 or board_id in (0, 0xFFFFFFFF):
|
||||
return None
|
||||
if board_id in (XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE):
|
||||
return "XIAO_DFU" if name_raw == XIAO_BOOT_DEVICE_NAME else None
|
||||
if name_raw == XIAO_BOOT_DEVICE_NAME:
|
||||
return "XIAO_DFU"
|
||||
if (board_id == SENSECAP_BOOTLOADER_IDENTITY[0] and
|
||||
name_raw == SENSECAP_BOOTLOADER_IDENTITY[1].encode("ascii").ljust(16, b"\0")):
|
||||
return SENSECAP_BOOTLOADER_IDENTITY[1]
|
||||
return None
|
||||
try:
|
||||
end = name_raw.index(0)
|
||||
except ValueError:
|
||||
@@ -680,13 +690,13 @@ def bootloader_hw_id(board_id: int, device_name: str) -> bytes:
|
||||
name_raw = device_name.encode("ascii", "strict")
|
||||
if len(name_raw) > 15 or _bootloader_device_name(board_id, name_raw.ljust(16, b"\0")) is None:
|
||||
raise ValueError("invalid embedded bootloader board/name identity")
|
||||
if board_id in (XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE):
|
||||
if (board_id, device_name) in XIAO_BOOTLOADER_IDENTITIES:
|
||||
return hw_id_bytes(xiao_bootloader_hw_id(board_id))
|
||||
return hw_id_bytes(f"NRF_BL_{board_id:08X}_{device_name}")
|
||||
|
||||
|
||||
def bootloader_target_id(board_id: int, device_name: str) -> int:
|
||||
if board_id in (XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE):
|
||||
if (board_id, device_name) in XIAO_BOOTLOADER_IDENTITIES:
|
||||
return board_id
|
||||
return int.from_bytes(sha256(bootloader_hw_id(board_id, device_name))[:4], "little")
|
||||
|
||||
@@ -699,10 +709,8 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
|
||||
to enter a newly built signed package, and release tests pass the generated
|
||||
application target table here to keep the two wire namespaces disjoint.
|
||||
"""
|
||||
identities = (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
) + INTERNAL_BOOTLOADER_IDENTITIES + RAK_AUTO_BOOTLOADER_IDENTITIES
|
||||
identities = (QSPI_BOOTLOADER_IDENTITIES + INTERNAL_BOOTLOADER_IDENTITIES +
|
||||
RAK_AUTO_BOOTLOADER_IDENTITIES)
|
||||
app_ids = set(application_target_ids)
|
||||
targets = {}
|
||||
for identity in identities:
|
||||
@@ -721,9 +729,7 @@ def audit_bootloader_target_inventory(application_target_ids=()) -> dict:
|
||||
def bootloader_identity_is_buildable(board_id: int, device_name: str) -> bool:
|
||||
return ((board_id, device_name) in INTERNAL_BOOTLOADER_IDENTITIES or
|
||||
(board_id, device_name) in RAK_AUTO_BOOTLOADER_IDENTITIES or
|
||||
(board_id, device_name) in (
|
||||
(XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")))
|
||||
(board_id, device_name) in QSPI_BOOTLOADER_IDENTITIES)
|
||||
|
||||
|
||||
def bootloader_qualified_platform_profile(board_id: int, device_name: str):
|
||||
@@ -743,8 +749,7 @@ def bootloader_qualified_storage_profiles(board_id: int, device_name: str):
|
||||
identity = (board_id, device_name)
|
||||
if identity in RAK_AUTO_BOOTLOADER_IDENTITIES:
|
||||
return (BOOT_STORAGE_RAK_AUTO_UPDATE,)
|
||||
if identity in ((XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU")):
|
||||
if identity in QSPI_BOOTLOADER_IDENTITIES:
|
||||
return (BOOT_STORAGE_QSPI_UPDATE,)
|
||||
if identity in SD_BOOTLOADER_IDENTITIES:
|
||||
# One embedded Tower identity supports two distinct application
|
||||
@@ -823,8 +828,8 @@ def parse_bootloader_identity(image: bytes) -> Optional[BootloaderIdentity]:
|
||||
def parse_xiao_bootloader_identity(image: bytes) -> Optional[XiaoBootloaderIdentity]:
|
||||
"""Compatibility wrapper restricted to the two deployed XIAO identities."""
|
||||
identity = parse_bootloader_identity(image)
|
||||
return identity if identity and identity.board_id in (
|
||||
XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE) else None
|
||||
return identity if identity and (identity.board_id, identity.device_name) in \
|
||||
XIAO_BOOTLOADER_IDENTITIES else None
|
||||
|
||||
|
||||
def bootloader_caps_storage(image: bytes) -> Optional[int]:
|
||||
@@ -931,7 +936,7 @@ def validate_bootloader_image(image: bytes, target_id: Optional[int] = None,
|
||||
|
||||
def validate_xiao_bootloader_image(image: bytes, board_id: Optional[int] = None) -> XiaoBootloaderIdentity:
|
||||
identity = validate_bootloader_image(image)
|
||||
if identity.board_id not in (XIAO_BOOT_BOARD_ID_BASE, XIAO_BOOT_BOARD_ID_SENSE):
|
||||
if (identity.board_id, identity.device_name) not in XIAO_BOOTLOADER_IDENTITIES:
|
||||
raise ValueError("bootloader is not a deployed XIAO identity")
|
||||
if board_id is not None and identity.board_id != board_id:
|
||||
raise ValueError("bootloader embedded board ID does not match package target")
|
||||
|
||||
@@ -1693,7 +1693,8 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
for value in re.findall(r"\{ 0x([0-9a-fA-F]{8}),", target_header)
|
||||
}
|
||||
audited = ml.audit_bootloader_target_inventory(app_targets)
|
||||
assert len(audited) == len(expected) + 4 # internal, two XIAOs, two adaptive RAKs
|
||||
assert len(audited) == len(expected) + 5 # internal, two XIAOs, Solar P1, two adaptive RAKs
|
||||
assert audited[0xD677B472] == (0x28860044, "SCAP_DFU")
|
||||
assert audited[0xD04AB3AB] == (0x239A0029, "3401_AUTO_DFU")
|
||||
assert audited[0xFEEAFD1B] == (0x239A0029, "4631_AUTO_DFU")
|
||||
|
||||
@@ -1714,10 +1715,7 @@ def test_bootloader_build_inventory_is_unique_and_disjoint_from_app_targets():
|
||||
|
||||
|
||||
def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
|
||||
qualified = (
|
||||
(ml.XIAO_BOOT_BOARD_ID_BASE, "XIAO_DFU"),
|
||||
(ml.XIAO_BOOT_BOARD_ID_SENSE, "XIAO_DFU"),
|
||||
) + ml.INTERNAL_BOOTLOADER_IDENTITIES
|
||||
qualified = ml.QSPI_BOOTLOADER_IDENTITIES + ml.INTERNAL_BOOTLOADER_IDENTITIES
|
||||
for board_id, device_name in qualified:
|
||||
family, fwid, app_base, layout_abi = \
|
||||
ml.bootloader_qualified_platform_profile(board_id, device_name)
|
||||
@@ -1731,8 +1729,7 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
|
||||
assert (fwid, app_base) == (ml.BOOTLOADER_S140_V6_FWID,
|
||||
ml.NRF52_APP_BASE_S140_V6)
|
||||
for storage in storages:
|
||||
if board_id in (ml.XIAO_BOOT_BOARD_ID_BASE,
|
||||
ml.XIAO_BOOT_BOARD_ID_SENSE):
|
||||
if (board_id, device_name) in ml.XIAO_BOOTLOADER_IDENTITIES:
|
||||
image = _xiao_bootloader_image(board_id)
|
||||
else:
|
||||
image = bytearray(_generic_bootloader_image(
|
||||
@@ -1767,7 +1764,7 @@ def test_qualified_bootloader_platform_and_storage_profiles_are_exact():
|
||||
pass
|
||||
|
||||
# Capability storage is part of the profile too. Tower alone admits both
|
||||
# internal and SD; QSPI is XIAO-only and non-Tower internals cannot claim SD.
|
||||
# internal and SD; QSPI is qualified per exact name, and other internals cannot claim SD.
|
||||
for invalid in (
|
||||
_generic_bootloader_image(storage=ml.BOOT_STORAGE_SD_UPDATE),
|
||||
_generic_bootloader_image(board_id=0x239A0071,
|
||||
|
||||
Reference in new issue
Block a user