diff --git a/docs/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20.md b/docs/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20.md index e236a788c..6702fa18c 100644 --- a/docs/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20.md +++ b/docs/test-results/hardware_validation_mercerwood_1_18_0_1_2026-10-20.md @@ -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` diff --git a/docs/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21.md b/docs/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21.md new file mode 100644 index 000000000..158990e1e --- /dev/null +++ b/docs/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21.md @@ -0,0 +1,70 @@ + +
+ + +# 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. diff --git a/docs/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21.safe.json b/docs/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21.safe.json new file mode 100644 index 000000000..13c796cc1 --- /dev/null +++ b/docs/test-results/mesh_pocket_qualification_1_18_0_1_2026-10-21.safe.json @@ -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" + ] +} diff --git a/test/test_mercer_nrf_app_flash.py b/test/test_mercer_nrf_app_flash.py index 4a8fc1959..03bbe32f1 100644 --- a/test/test_mercer_nrf_app_flash.py +++ b/test/test_mercer_nrf_app_flash.py @@ -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() diff --git a/tools/hil/mercer_nrf_app_flash.sh b/tools/hil/mercer_nrf_app_flash.sh index 11f86035b..7d42634a5 100644 --- a/tools/hil/mercer_nrf_app_flash.sh +++ b/tools/hil/mercer_nrf_app_flash.sh @@ -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")"