Qualify MeshPocket and support pinned flasher USB helpers
This commit is contained in:
1 parent
d3661f66f0
commit
497138fad3
5 files changed
+485
-5
No files matched your search
@@ -4,12 +4,19 @@
|
||||
|
||||
# Mercerwood bench qualification: 12 active radios, 20 October 2026
|
||||
|
||||
**21 October follow-up:** the user lifted MeshPocket's bootloader-work
|
||||
exclusion. Its fresh Full Companion USB, BLE, contact persistence/sync and
|
||||
three two-way LoRa rounds passed in the
|
||||
[MeshPocket qualification](mesh_pocket_qualification_1_18_0_1_2026-10-21.md).
|
||||
The exclusion and source-bound measurements below describe the earlier
|
||||
20 October campaign; the other twelve boards were not retested for this update.
|
||||
|
||||
Status: **PASS**. 12/12 active boards have complete
|
||||
accepted bench coverage; 0 have a latest required failure and
|
||||
0 remain incomplete. The original inventory has thirteen
|
||||
radios: eleven Full Companions and two Full OTA repeaters. The active target
|
||||
is twelve radios: ten Companions and two repeaters. MeshPocket is
|
||||
operator-excluded from all further testing for separate bootloader work.
|
||||
is twelve radios: ten Companions and two repeaters. MeshPocket was
|
||||
operator-excluded at this campaign's close for separate bootloader work.
|
||||
|
||||
All thirteen 0865 candidate builds passed. Companion updates target
|
||||
`08650f644b680a5dfaacb95142e3cef8529da9d2`; the table records each board's
|
||||
@@ -51,8 +58,8 @@ records each axis's actual tested source, artifact, measurement interval,
|
||||
conditions and evidence digest. V3, Tag and T096 retain valid ABF USB/BLE/RF measurements; V4 retains ABF RF.
|
||||
Pocket's earlier ABF and 0865 results appear only in its excluded historical
|
||||
record; its current installed application and bootloader are unverified
|
||||
outside this report for operator bootloader work, and no further Pocket tests
|
||||
are planned. ABF is
|
||||
outside this report for operator bootloader work. The later Pocket tests are
|
||||
recorded separately in the follow-up linked above. ABF is
|
||||
`abf7ba5d5c3958580d52d6954d816c08a035a552`. For active boards, changed applications, current identities and new 64/32
|
||||
contact trials are separate measurements.
|
||||
Reviewed source assurance `577aee5162c7553f58bf13b550560da50bbf73860ab37febe3674a5ae73c01aa`
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<!-- meshcore-hosted-doc-link:start -->
|
||||
<p class="meshcore-hosted-doc-link"><a href="https://mikecarper.github.io/MeshCore/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21/">View this page on MeshCore Docs</a>.</p>
|
||||
<!-- meshcore-hosted-doc-link:end -->
|
||||
|
||||
# MeshPocket Companion qualification, 21 October 2026
|
||||
|
||||
Status: **PASS for the automated Companion bench checks below**. The user's
|
||||
request to test MeshPocket lifted its earlier bootloader-work exclusion.
|
||||
The board now runs Full Companion built from
|
||||
`21a0f318da26bdb1fa5257471c3d7a68a0684337`, version
|
||||
`v1.18.0.1-halo-keymind-cascade-dev-21a0f318`, protocol 14.
|
||||
Its measured application body is `54411895235BE9C4`; application ZIP SHA256 is
|
||||
`7841c4e7e8bbb8695ba0e796fa811efa41112367641c2790f22cc9085ba00da6`.
|
||||
No firmware change was needed for these checks.
|
||||
|
||||
| Check | Measured result |
|
||||
| --- | --- |
|
||||
| Build and application update | Full capability/memory checks passed; identity-bound USB application-only DFU succeeded; exact version and EndF body verified after boot |
|
||||
| USB | 100 query cycles, 10 reopens, owned contact CRUD and persistence through one verified warm restart passed; no framing errors or core error flags |
|
||||
| Bluetooth | Three authenticated sessions held for 30.269 / 30.271 / 30.271 seconds; saved bond, exact USB identity/version and three clean controller lifecycles; no bond deletion |
|
||||
| Full contact refresh | All 64 owned records verified in a 65-record stream, including the original contact; request to END: 2.491665 seconds |
|
||||
| Incremental refresh | Exact 32 owned timestamp-selected records in a 33-record stream; request to END: 1.305048 seconds |
|
||||
| Contact cleanup | All 64 synthetic contacts removed; zero remained after reopen; original contact fingerprint unchanged |
|
||||
| Two-way LoRa | Three rounds, six exact direct nonce-bound datagrams, six send commands; every round passed |
|
||||
| Handback | Owned channels removed; both radios' original modem settings, TX power and saved routes restored; ports closed; final Pocket core errors and outbound queue both zero |
|
||||
|
||||
Bluetooth passed while the USB data cable remained connected. Each contact
|
||||
refresh used the existing bond and a separately verified clean controller
|
||||
lifecycle. USB persistence covers one owned contact across a warm restart;
|
||||
the 64-record fixture separately covers a seven-second save allowance and
|
||||
exact reopen readback. Neither is a cold-power persistence measurement.
|
||||
|
||||
The RF partner was the already restored XIAO ESP32-S3 Full Companion, retained
|
||||
at `ebb59d48292de4a573250cf4c6a2ed7cf5f7ef69`, body `4EEB12667E5E554B`.
|
||||
Both identities and actual versions were verified before the three rounds.
|
||||
The temporary test tuple was 910.525 MHz / BW62.5 / SF7 / CR5, preamble 32.
|
||||
Exact source, destination, nonce, zero-hop route and physical packet counters
|
||||
were checked. This is an explicitly mixed-source pair, not a claim that every
|
||||
radio runs the new Pocket artifact. The RAK3401 gateway was not touched.
|
||||
|
||||
The existing bootloader and SoftDevice were preserved: the verified DFU ZIP
|
||||
contained only application BIN/DAT components. Its exact installed bootloader
|
||||
revision remains unverified because this Full Companion returns an unknown
|
||||
command for `get bootloader.ver`. Successful application DFU and subsequent
|
||||
runtime checks do not establish the independent bootloader's revision.
|
||||
|
||||
The first updater attempt failed before its DFU touch because the newer
|
||||
flasher's pinned USB helpers depend on startup globals absent from the
|
||||
definitions-only bench adapter. The compatible, previously reviewed updater
|
||||
then installed the app successfully. The adapter now validates and privately
|
||||
stages both bundled helpers and resolves them locally without running flasher
|
||||
startup. Fourteen adapter tests and five application-ZIP verifier tests passed;
|
||||
shell syntax and diff checks passed. This tooling-only fix did not trigger
|
||||
another firmware installation or repeated hardware campaign. The preinstall
|
||||
0865 application's core flag 8 and the failed updater attempt remain in the
|
||||
private evidence; no causal firmware fix is inferred from the clean new boot.
|
||||
|
||||
The [safe receipt](mesh_pocket_qualification_1_18_0_1_2026-10-21.safe.json)
|
||||
binds every measured axis to its source, artifact, interval and evidence
|
||||
digest. Earlier Pocket results and the temporary exclusion remain historical
|
||||
in the [20 October fleet report](hardware_validation_mercerwood_1_18_0_1_2026-10-20.md).
|
||||
The other twelve boards retain their accepted source-bound measurements;
|
||||
unchanged hardware tests were not repeated.
|
||||
|
||||
These finite checks do not establish RF range, long-duration reliability,
|
||||
phone interoperability, battery current or physical cold starts. Screen/page
|
||||
rendering awaits operator observation. MeshPocket has no native WiFi.
|
||||
Bluetooth DFU is build-verified but a physical wireless update was not run in
|
||||
this task. This Companion can seed LoRa firmware transfers; local LoRa firmware
|
||||
installation is disabled and was not claimed tested.
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"schema": 1,
|
||||
"report_date": "2026-10-21",
|
||||
"status": "PASS",
|
||||
"scope": "Fresh automated Full Companion USB/BLE/contact-persistence/direct-LoRa qualification after the user lifted MeshPocket bootloader-work exclusion. Screen, cold-power, range, phone interoperability and physical wireless DFU are not claimed.",
|
||||
"board": "Heltec MeshPocket",
|
||||
"target": "Mesh_pocket_companion_radio_full",
|
||||
"source_commit": "21a0f318da26bdb1fa5257471c3d7a68a0684337",
|
||||
"version": "v1.18.0.1-halo-keymind-cascade-dev-21a0f318 (Build: 21-Oct-2026)",
|
||||
"application_sha256": "7841c4e7e8bbb8695ba0e796fa811efa41112367641c2790f22cc9085ba00da6",
|
||||
"body_hash": "54411895235BE9C4",
|
||||
"firmware_modified_in_this_task": false,
|
||||
"bootloader": {
|
||||
"action": "preserved_by_application_only_DFU",
|
||||
"bootloader_or_softdevice_components_transferred": false,
|
||||
"current_exact_version": null,
|
||||
"version_query": "get bootloader.ver is unsupported by the measured Full Companion",
|
||||
"independent_bootloader_image_assumed": false
|
||||
},
|
||||
"axes": {
|
||||
"build": {
|
||||
"status": "PASS",
|
||||
"memory_available_internal_bytes": 85352,
|
||||
"memory_required_heap_bytes": 53056,
|
||||
"memory_required_contiguous_bytes": 8192
|
||||
},
|
||||
"application_install": {
|
||||
"status": "PASS",
|
||||
"method": "identity_bound_USB_serial_application_only_DFU",
|
||||
"application_return_verified_by_exact_version_and_EndF_body": true,
|
||||
"backups_created": false,
|
||||
"full_flash_erased": false
|
||||
},
|
||||
"usb": {
|
||||
"status": "PASS",
|
||||
"started_at": "2026-10-21T15:13:55.339146+00:00",
|
||||
"finished_at": "2026-10-21T15:14:50.564843+00:00",
|
||||
"query_cycles": 100,
|
||||
"reopens": 10,
|
||||
"owned_crud": true,
|
||||
"owned_record_survives_verified_warm_reboot": true,
|
||||
"warm_reboot_requests": 1,
|
||||
"stable_binary_transport": {
|
||||
"discarded_prefix_bytes": 0,
|
||||
"ignored_push_frames": 0,
|
||||
"invalid_length_candidates": 0,
|
||||
"ports_closed": 18,
|
||||
"ports_opened": 18,
|
||||
"request_bytes_sent": 5419,
|
||||
"request_frames_sent": 662,
|
||||
"response_frames_received": 667,
|
||||
"response_payload_bytes": 24927
|
||||
},
|
||||
"channel_state_unchanged": true,
|
||||
"initial_contacts": 1,
|
||||
"final_contacts": 1
|
||||
},
|
||||
"ble": {
|
||||
"status": "PASS",
|
||||
"started_at": "2026-10-21T15:14:58.890522+00:00",
|
||||
"finished_at": "2026-10-21T15:17:17.349636+00:00",
|
||||
"sessions": 3,
|
||||
"hold_seconds": [
|
||||
30.269,
|
||||
30.271,
|
||||
30.271
|
||||
],
|
||||
"preexisting_bond": true,
|
||||
"target_bond_deleted": false,
|
||||
"usb_identity_and_version_verified": true,
|
||||
"USB_data_cable_remained_connected": true,
|
||||
"hci_lifecycle": {
|
||||
"capture_ended_before_timeout": true,
|
||||
"classification_complete": true,
|
||||
"clean": true,
|
||||
"ignored_foreign_connection_events": 0,
|
||||
"lifecycle_parse_errors": 0,
|
||||
"minimum_required_sessions": 3,
|
||||
"target_command_failures": 0,
|
||||
"target_disconnect_reasons": [
|
||||
"0x16",
|
||||
"0x16",
|
||||
"0x16"
|
||||
],
|
||||
"target_disconnect_statuses": [
|
||||
"0x00",
|
||||
"0x00",
|
||||
"0x00"
|
||||
],
|
||||
"target_establishment_0x3e_disconnects": 0,
|
||||
"target_failed_establishments": 0,
|
||||
"target_ll_response_timeout_0x22_disconnects": 0,
|
||||
"target_open_handles_at_end": 0,
|
||||
"target_pending_attempts_at_end": 0,
|
||||
"target_successful_connection_events": 3,
|
||||
"target_timeout_0x08_disconnects": 0,
|
||||
"unattributed_controller_events": 0
|
||||
}
|
||||
},
|
||||
"contact_refresh": {
|
||||
"status": "PASS",
|
||||
"started_at": "2026-10-21T15:17:31.314046+00:00",
|
||||
"finished_at": "2026-10-21T15:18:41.651834+00:00",
|
||||
"owned_full_records": 64,
|
||||
"full_total_records": 65,
|
||||
"full_request_to_end_seconds": 2.491665,
|
||||
"owned_incremental_records": 32,
|
||||
"incremental_total_records": 33,
|
||||
"incremental_request_to_end_seconds": 1.305048,
|
||||
"owned_deleted": 64,
|
||||
"owned_remaining": 0,
|
||||
"unrelated_records_preserved": true,
|
||||
"existing_bond_and_clean_HCI_verified": true,
|
||||
"persist_allowance_seconds": 7,
|
||||
"cold_persistence_claimed": false
|
||||
},
|
||||
"lora": {
|
||||
"status": "PASS",
|
||||
"started_at": "2026-10-21T15:21:14.086947+00:00",
|
||||
"finished_at": "2026-10-21T15:22:40.088960+00:00",
|
||||
"rounds": 3,
|
||||
"directions_per_round": 2,
|
||||
"exact_direct_nonce_datagrams_received": 6,
|
||||
"send_commands": 6,
|
||||
"temporary_frequency_MHz": 910.525,
|
||||
"bandwidth_kHz": 62.5,
|
||||
"spreading_factor": 7,
|
||||
"coding_rate": 5,
|
||||
"preamble": 32,
|
||||
"mixed_source_pair": true,
|
||||
"partner": {
|
||||
"expected_version": "v1.18.0.1-halo-keymind-cascade-dev-ebb59d48",
|
||||
"source_commit": "ebb59d48292de4a573250cf4c6a2ed7cf5f7ef69",
|
||||
"target": "Xiao_S3_WIO_companion_radio_full"
|
||||
},
|
||||
"partner_body_hash": "4EEB12667E5E554B",
|
||||
"owned_channels_removed": true,
|
||||
"original_radio_TX_settings_and_routes_restored": true,
|
||||
"final_handback_passed": true,
|
||||
"ports_unowned_after_close": true,
|
||||
"final_core": {
|
||||
"battery_mv": 4130,
|
||||
"error_flags": 0,
|
||||
"outbound_queue_length": 0,
|
||||
"uptime_seconds": 480
|
||||
}
|
||||
},
|
||||
"screen": {
|
||||
"status": "NOT_OBSERVED",
|
||||
"operator_check_requested": true
|
||||
},
|
||||
"wifi": {
|
||||
"status": "NOT_APPLICABLE",
|
||||
"reason": "No native WiFi radio on this nRF52840 board"
|
||||
},
|
||||
"wireless_OTA": {
|
||||
"status": "NOT_MEASURED",
|
||||
"build_verified_bluetooth_DFU_capability": true,
|
||||
"USB_DFU_measured": true,
|
||||
"LoRa_role": "source-only; local install disabled"
|
||||
}
|
||||
},
|
||||
"evidence_sha256": {
|
||||
"initial-inventory.private.json": "faf876cf4835a7b6711595b9f3258f90de785a8edf1251c5856910523576971a",
|
||||
"bootloader-before.private.json": "185291ff70854e38cf94c0c61e06de522f46789597b0bb6523282e6381fe4678",
|
||||
"install.safe.json": "0c57088884a6ea50c28dc3a9cd3c07f81164f115075b99cff0f3d35a4aa7bccf",
|
||||
"install-retry.safe.json": "3b83c7097203bbaf42862b0e1c44fc5131d83f1b6cf626c3d76e0d2cefb6508d",
|
||||
"post-install-runtime.private.json": "a46998359e606591e1c3e4f2ac7e859445f5aa77a73d66e9c76598cdaf81e220",
|
||||
"build-verified.safe.json": "da2b471f951052ba69ddd13e935d7f127717f2f45f134bc41b01e7f289f9a70c",
|
||||
"usb/summary.safe.json": "58a52ce6d4c42450fa812ed09b3c44d229a21b8b3bc3188e1ef1647feda9451d",
|
||||
"usb-setup.safe.json": "ec43c8098837506110e9fec0248c2a3d85565b85165502def264fd2301e7020c",
|
||||
"ble/summary.safe.json": "5d4aef872afbac18a214074e063cbed1d10f6489a1540866675433e01e14b13a",
|
||||
"contacts/summary.safe.json": "7bdd680e899ca0ba633b0f8b346c4485928ebc86b581f52bb991254f6da6c94a",
|
||||
"contacts/sync/summary.safe.json": "4b8e2f0eb919c3f452812f721dabe9a62d5e58df2af9472494563950900bff45",
|
||||
"contacts/cleanup/summary.safe.json": "6f3e136bef3e3bad9fcedf85feecbb207286bcdc5ec5cc4030be3950a8715623",
|
||||
"rf/summary.safe.json": "e0d9e93f834e1758aaa7b59d1c5915883ec0f656146249e1269e5dad91af6f60"
|
||||
},
|
||||
"drivers": {
|
||||
"campaign_sha256": "863706b02cf88eb13c5e1ea0f1018663b6c3ecf16989c5cc24f490c1bdb404f0",
|
||||
"rf_driver_sha256": "83ed1d30e1c25371709d39c48e3a4bb4a9a44e659093ab4cd75737b639cb5880",
|
||||
"reviewed_harness_manifest_sha256": "580f758c2a70333205b3eaa41e4f45b6a7c85058477c959bc885b04c215cfcab"
|
||||
},
|
||||
"history": {
|
||||
"preinstall_source": "08650f644b680a5dfaacb95142e3cef8529da9d2",
|
||||
"preinstall_core_error_flags": 8,
|
||||
"postinstall_measured_core_error_flags": 0,
|
||||
"causal_firmware_fix_claimed": false,
|
||||
"first_updater_attempt": "Failed before touch/programming due to omitted hardened-helper startup globals; preserved separately",
|
||||
"successful_install_flasher_sha256": "4da05ba37c2cb8b4a7e455a4aa3ee24f6d8020912d71874c38afd099e98e75b2"
|
||||
},
|
||||
"test_tool_fix": {
|
||||
"files": [
|
||||
"tools/hil/mercer_nrf_app_flash.sh",
|
||||
"test/test_mercer_nrf_app_flash.py"
|
||||
],
|
||||
"description": "Definitions-only adapter validates and privately stages pinned bundled USB helpers; local-only resolution without sourcing flasher startup",
|
||||
"focused_adapter_tests_passed": 14,
|
||||
"application_zip_verifier_tests_passed": 5,
|
||||
"hardware_retest_after_test_tool_only_change": false
|
||||
},
|
||||
"fleet_scope": {
|
||||
"pocket_exclusion_lifted_by_user": true,
|
||||
"newly_measured_boards": 1,
|
||||
"previously_accepted_boards": 12,
|
||||
"previous_measurements_repeated": false,
|
||||
"all_thirteen_same_source_claimed": false,
|
||||
"gateway_service_touched": false
|
||||
},
|
||||
"limits": [
|
||||
"Finite bench operation, not RF range or long-duration reliability",
|
||||
"Screen and physical button rendering await operator observation",
|
||||
"No physical cold-power or battery-current measurement",
|
||||
"No physical Bluetooth DFU or LoRa firmware-source transfer in this task",
|
||||
"Exact installed bootloader revision is unavailable through this Companion getter",
|
||||
"Other boards retain their own source-bound reports"
|
||||
]
|
||||
}
|
||||
@@ -81,8 +81,14 @@ class MercerNrfAppShellTest(unittest.TestCase):
|
||||
self.inventory_file.write_text(json.dumps(inventory()), encoding="utf-8")
|
||||
self.startup_marker = self.work / "STARTUP-WAS-SOURCED"
|
||||
self.dfu_marker = self.work / "MOCK-DFU-ONLY"
|
||||
self.resolver_marker = self.work / "REMOTE-RESOLVER-WAS-CALLED"
|
||||
self.helper_marker = self.work / "LOCAL-HELPER-PATHS"
|
||||
self.port_check_marker = self.work / "SERIAL-PREFLIGHT-WAS-CALLED"
|
||||
self.env = dict(os.environ, SIM_STARTUP_MARKER=str(self.startup_marker),
|
||||
SIM_DFU_MARKER=str(self.dfu_marker))
|
||||
SIM_DFU_MARKER=str(self.dfu_marker),
|
||||
SIM_RESOLVER_MARKER=str(self.resolver_marker),
|
||||
SIM_HELPER_MARKER=str(self.helper_marker),
|
||||
SIM_PORT_CHECK_MARKER=str(self.port_check_marker))
|
||||
|
||||
def executable(name, text):
|
||||
path = self.bin / name
|
||||
@@ -129,6 +135,47 @@ class MercerNrfAppShellTest(unittest.TestCase):
|
||||
text += f"unused_mock_function_{index}() {{\n return 0\n}}\n"
|
||||
self.flasher.write_text(text, encoding="utf-8")
|
||||
|
||||
def newer_flasher(self):
|
||||
"""Model the bundled-helper contract added by meshfirmware 41a21fa.
|
||||
|
||||
The real helper entry points are never executed. Resolver stubs would
|
||||
mark an unwanted dependency fetch if the adapter did not replace them.
|
||||
"""
|
||||
directory = self.work / "tools"
|
||||
directory.mkdir()
|
||||
helpers = {
|
||||
"meshcore_serial_touch.py": b"# SIMULATED TOUCH ONLY\r\n",
|
||||
"meshcore_usb_reset.py": b"# SIMULATED IDENTITY ONLY\n",
|
||||
}
|
||||
declarations = ""
|
||||
for name, content in helpers.items():
|
||||
(directory / name).write_bytes(content)
|
||||
variable = ("MESHCORE_SERIAL_TOUCH_TOOL_SHA256" if "serial_touch" in name
|
||||
else "MESHCORE_USB_RESET_TOOL_SHA256")
|
||||
digest = hashlib.sha256(content.replace(b"\r\n", b"\n")).hexdigest()
|
||||
declarations += f'{variable}="{digest}"\n'
|
||||
text = self.flasher.read_text(encoding="utf-8") + declarations
|
||||
for name in ("resolve_meshcore_serial_touch_tool", "resolve_meshcore_usb_reset_tool"):
|
||||
text += (f"{name}() {{\n"
|
||||
' printf \'UNEXPECTED DOWNLOAD\\n\' > "$SIM_RESOLVER_MARKER"\n'
|
||||
" return 89\n}\n")
|
||||
text += (
|
||||
"preferred_flash_serial_port() {\n"
|
||||
' printf \'PREFLIGHT\\n\' > "$SIM_PORT_CHECK_MARKER"\n'
|
||||
' printf \'%s\\n\' "$1"\n}\n'
|
||||
"run_nrf52_dfu_package_buttonless() {\n"
|
||||
' [[ "$PWD_SCRIPT" == "$FIRMWARE_ROOT" ]] || return 90\n'
|
||||
' [[ -z "$MESHCORE_SERIAL_TOUCH_TOOL_URL" && -z "$MESHCORE_USB_RESET_TOOL_URL" ]] || return 91\n'
|
||||
' local touch identity\n'
|
||||
' touch=$(resolve_meshcore_serial_touch_tool) || return 92\n'
|
||||
' identity=$(resolve_meshcore_usb_reset_tool) || return 92\n'
|
||||
' printf \'%s\\n%s\\n\' "$touch" "$identity" > "$SIM_HELPER_MARKER"\n'
|
||||
' printf \'%s|%s|%s|%s\\n\' "$1" "$2" "$3" "$4" > "$SIM_DFU_MARKER"\n'
|
||||
"}\n"
|
||||
)
|
||||
self.flasher.write_text(text, encoding="utf-8")
|
||||
return helpers
|
||||
|
||||
def invoke(self, overrides=None, *, consent=True):
|
||||
arguments = {
|
||||
"--flasher": str(self.flasher), "--port": PORT, "--workdir": str(self.work),
|
||||
@@ -181,6 +228,68 @@ class MercerNrfAppShellTest(unittest.TestCase):
|
||||
self.assertEqual(Path(call[0]).name, self.package.name)
|
||||
self.assertIn("Application-only DFU completed", result.stdout)
|
||||
|
||||
def test_new_flasher_uses_only_verified_private_bundled_helpers(self):
|
||||
helpers = self.newer_flasher()
|
||||
# Ambient values cannot select different helper bytes or locations.
|
||||
self.env.update(PWD_SCRIPT="/unreviewed", FIRMWARE_ROOT="/unreviewed",
|
||||
MESHCORE_SERIAL_TOUCH_TOOL_SHA256="0" * 64,
|
||||
MESHCORE_USB_RESET_TOOL_SHA256="0" * 64)
|
||||
result = self.invoke()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertTrue(self.dfu_marker.exists())
|
||||
self.assertFalse(self.resolver_marker.exists(), "a remote-capable resolver was called")
|
||||
paths = [Path(value) for value in self.helper_marker.read_text().splitlines()]
|
||||
self.assertEqual(len(paths), 2)
|
||||
self.assertEqual(paths[0].parent, paths[1].parent)
|
||||
for path in paths:
|
||||
self.assertEqual(path.parent.name, "tools")
|
||||
self.assertTrue(path.parent.parent.name.startswith(".mcfirmware-nrf-app."))
|
||||
self.assertNotEqual(path, self.work / "tools" / path.name)
|
||||
self.assertEqual(path.read_bytes(), helpers[path.name])
|
||||
self.assertEqual(path.stat().st_mode & 0o777, 0o400)
|
||||
|
||||
def test_missing_bundled_helper_fails_before_serial_preflight(self):
|
||||
self.newer_flasher()
|
||||
(self.work / "tools/meshcore_usb_reset.py").unlink()
|
||||
self.assert_refused(self.invoke(), "Bundled helper unavailable")
|
||||
self.assertFalse(self.port_check_marker.exists())
|
||||
self.assertFalse(self.resolver_marker.exists())
|
||||
|
||||
def test_mismatched_bundled_helper_fails_before_serial_preflight(self):
|
||||
self.newer_flasher()
|
||||
(self.work / "tools/meshcore_serial_touch.py").write_bytes(b"UNREVIEWED HELPER")
|
||||
self.assert_refused(self.invoke(), "Bundled helper hash mismatch")
|
||||
self.assertFalse(self.port_check_marker.exists())
|
||||
self.assertFalse(self.resolver_marker.exists())
|
||||
|
||||
def test_symlink_bundled_helper_fails_before_serial_preflight(self):
|
||||
self.newer_flasher()
|
||||
source = self.work / "tools/meshcore_serial_touch.py"
|
||||
source.rename(self.work / "external-helper.py")
|
||||
source.symlink_to(self.work / "external-helper.py")
|
||||
self.assert_refused(self.invoke(), "Bundled helper unavailable")
|
||||
self.assertFalse(self.port_check_marker.exists())
|
||||
|
||||
def test_helper_hash_declaration_is_data_and_must_be_unique(self):
|
||||
self.newer_flasher()
|
||||
original = self.flasher.read_text(encoding="utf-8")
|
||||
for declaration, reason in (
|
||||
('MESHCORE_USB_RESET_TOOL_SHA256="' + "0" * 64 + '"\n',
|
||||
"Expected one literal helper hash declaration"),
|
||||
('MESHCORE_SERIAL_TOUCH_TOOL_SHA256="$(touch $SIM_STARTUP_MARKER)"\n',
|
||||
"Unsupported helper hash declaration"),
|
||||
):
|
||||
with self.subTest(declaration=declaration):
|
||||
if declaration.startswith("MESHCORE_SERIAL_TOUCH"):
|
||||
text = "\n".join(line for line in original.splitlines()
|
||||
if not line.startswith("MESHCORE_SERIAL_TOUCH_TOOL_SHA256=")) + "\n"
|
||||
else:
|
||||
text = original
|
||||
self.flasher.write_text(text + declaration, encoding="utf-8")
|
||||
self.assert_refused(self.invoke(), reason)
|
||||
self.assertFalse(self.port_check_marker.exists())
|
||||
self.assertFalse(self.resolver_marker.exists())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -14,6 +14,7 @@ nrf_adapter_usage() {
|
||||
' --artifact APPLICATION.zip --sha256 HEX --allow-write' \
|
||||
'Only an inventory-matched application-only DFU ZIP is accepted.' \
|
||||
'The sibling verified_nrf_flash.py helper and installed dependencies are required.' \
|
||||
'Newer flashers also require their matching bundled tools/ serial-touch helpers.' \
|
||||
'Successful DFU still requires a separate application-level boot/version check.'
|
||||
}
|
||||
|
||||
@@ -104,6 +105,82 @@ for nrf_adapter_function in preferred_flash_serial_port save_selected_serial_por
|
||||
declare -F "$nrf_adapter_function" >/dev/null || nrf_adapter_die "Flasher lacks $nrf_adapter_function."
|
||||
done
|
||||
|
||||
# Newer flashers use a pinned Python helper for the single 1200-baud touch.
|
||||
# Their normal startup initializes paths and hashes, but sourcing startup here
|
||||
# would also install traps and run unrelated device/service operations. Read
|
||||
# only the two literal hash declarations and freeze their bundled helpers.
|
||||
# Never let a definitions-only adapter fall back to downloading dependencies.
|
||||
if declare -F resolve_meshcore_serial_touch_tool >/dev/null; then
|
||||
nrf_adapter_bundled_hashes=$(python3 - "$nrf_adapter_flasher" "$nrf_adapter_state_dir" <<'PY'
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import stat
|
||||
import sys
|
||||
|
||||
flasher, state = map(Path, sys.argv[1:])
|
||||
text = flasher.read_text(encoding="utf-8")
|
||||
helpers = (
|
||||
("meshcore_serial_touch.py", "MESHCORE_SERIAL_TOUCH_TOOL_SHA256"),
|
||||
("meshcore_usb_reset.py", "MESHCORE_USB_RESET_TOOL_SHA256"),
|
||||
)
|
||||
verified = []
|
||||
for name, variable in helpers:
|
||||
declarations = re.findall(r"^" + variable + r"=.*$", text, re.MULTILINE)
|
||||
if len(declarations) != 1:
|
||||
raise SystemExit("Expected one literal helper hash declaration: " + variable)
|
||||
match = re.fullmatch(variable + r'="([0-9a-fA-F]{64})"', declarations[0])
|
||||
if match is None:
|
||||
raise SystemExit("Unsupported helper hash declaration: " + variable)
|
||||
expected = match[1].lower()
|
||||
source = flasher.parent / "tools" / name
|
||||
try:
|
||||
descriptor = os.open(source, os.O_RDONLY | os.O_NOFOLLOW)
|
||||
with os.fdopen(descriptor, "rb") as handle:
|
||||
if not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
|
||||
raise ValueError("Helper is not a regular file")
|
||||
content = handle.read(1024 * 1024 + 1)
|
||||
except (OSError, ValueError) as exc:
|
||||
raise SystemExit("Bundled helper unavailable: " + name) from exc
|
||||
if not content or len(content) > 1024 * 1024:
|
||||
raise SystemExit("Bundled helper is empty or oversized: " + name)
|
||||
if hashlib.sha256(content.replace(b"\r\n", b"\n")).hexdigest() != expected:
|
||||
raise SystemExit("Bundled helper hash mismatch: " + name)
|
||||
verified.append((name, content, expected))
|
||||
|
||||
directory = state / "tools"
|
||||
directory.mkdir(mode=0o700)
|
||||
for name, content, expected in verified:
|
||||
descriptor = os.open(directory / name,
|
||||
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o400)
|
||||
with os.fdopen(descriptor, "wb") as handle:
|
||||
handle.write(content)
|
||||
print(" ".join(expected for _, _, expected in verified))
|
||||
PY
|
||||
) || nrf_adapter_die 'Bundled serial-touch helpers failed verification; no DFU touch was requested.'
|
||||
read -r MESHCORE_SERIAL_TOUCH_TOOL_SHA256 MESHCORE_USB_RESET_TOOL_SHA256 \
|
||||
<<< "$nrf_adapter_bundled_hashes"
|
||||
PWD_SCRIPT=$nrf_adapter_state_dir
|
||||
FIRMWARE_ROOT=$nrf_adapter_state_dir
|
||||
MESHCORE_SERIAL_TOUCH_TOOL_URL=''
|
||||
MESHCORE_USB_RESET_TOOL_URL=''
|
||||
nrf_adapter_local_helper() {
|
||||
local helper="$nrf_adapter_state_dir/tools/$1" expected=$2 observed
|
||||
[[ -f "$helper" && ! -L "$helper" && -r "$helper" ]] || return 1
|
||||
observed=$(python3 -c 'import hashlib,sys; print(hashlib.sha256(open(sys.argv[1], "rb").read().replace(b"\r\n", b"\n")).hexdigest())' "$helper") || return 1
|
||||
[[ "$observed" == "$expected" ]] || return 1
|
||||
printf '%s\n' "$helper"
|
||||
}
|
||||
resolve_meshcore_usb_reset_tool() {
|
||||
nrf_adapter_local_helper meshcore_usb_reset.py "$MESHCORE_USB_RESET_TOOL_SHA256"
|
||||
}
|
||||
resolve_meshcore_serial_touch_tool() {
|
||||
resolve_meshcore_usb_reset_tool >/dev/null || return 1
|
||||
nrf_adapter_local_helper meshcore_serial_touch.py "$MESHCORE_SERIAL_TOUCH_TOOL_SHA256"
|
||||
}
|
||||
fi
|
||||
|
||||
# Stage a private, hash-verified snapshot, so the build output may not change
|
||||
# beneath a DFU operation. Keep the original board-qualified artifact basename.
|
||||
nrf_adapter_staged="$nrf_adapter_state_dir/$(basename -- "$nrf_adapter_artifact")"
|
||||
|
||||
Reference in new issue
Block a user