Skip to content

hil: convoy-safe openocd_seq recovery flasher for seven J-Link boards - #3832

Open
hathach wants to merge 4 commits into
masterfrom
hil-openocd-recover-flasher
Open

hil: convoy-safe openocd_seq recovery flasher for seven J-Link boards#3832
hathach wants to merge 4 commits into
masterfrom
hil-openocd-recover-flasher

Conversation

@hathach

@hathach hathach commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #3803. That PR landed the wedge-containment machinery (recover_flasher(), the convoy_safe() gate, hil_test substituting the recovery flasher into --recover-board) but left it inert: no roster entry defined flasher_recover, and no flasher backend actually worked over openocd's jlink transport. This PR completes it:

  • openocd_seq flasher backend (test/hil/hil_flash.py): explicit init / reset halt / flash write_image erase / verify_image / reset run / shutdown sequence. The existing flash_openocd's program <fw> verify reset exit form fails over the jlink transport (Examination failedauto_probe failed, measured on stm32f4x and stm32f0x); the explicit sequence succeeded on all seven candidate boards.
  • Seven flasher_recover roster entries (test/hil/tinyusb.json): stm32f407disco, stm32f072disco, stm32f723disco, stm32l476disco, feather_nrf52840_express, metro_m4_express, frdm_k64f — each the same J-Link probe as its primary flasher, driven through openocd's jlink driver instead of the SEGGER tools.
  • Lint/tests (test/hil/test/test_ci_select.py): roster lint for the recovery entries (name, jlink args, required adapter speed, uid must match the primary probe, convoy-safe), dispatch/suffix lints extended to flasher_recover, command-form regression test pinning "no program over jlink".
  • The false "no vid_pid pin → probe discovery will open every usbfs node" warning is silenced for interface/jlink.cfg args: libjaylink's discovery gates on idVendor == 0x1366 before ever calling libusb_open, so a jlink-args entry never touches foreign usbfs nodes — which is exactly what makes it convoy-safe without a pin. The entries deliberately carry no vid_pid: openocd's jlink driver never reads adapter usb vid_pid.

Why this matters: when a DUT wedges its usbfs node mid-usbtest, the primary SEGGER-tool flashers (JLinkExe) block on libusb enumeration of the poisoned node — the convoy effect. The recovery flasher reaches the same probe without opening any non-SEGGER usbfs node, so the post-hang reflash/reset can run while the bus is still poisoned.

Bench validation (ci.lan, 2026-08-19, all seven boards, locks held ~8 min)

Each board was flashed and reset through its flasher_recover entry exactly as usbtest.py receives it (board['flasher'] replaced by the recovery dict), timeout 90 s / 30 s:

Board Flash Reset Verified by
stm32f407disco 1.81 s 0.13 s fresh enumeration, SN matches roster uid
stm32f072disco 1.76 s 0.13 s fresh enumeration, SN matches roster uid
stm32f723disco 1.53 s 0.13 s fresh enumeration; transient CM7 errata-3092511 probe retry, non-fatal
stm32l476disco 2.80 s 0.15 s fresh enumeration, SN matches roster uid
feather_nrf52840_express 1.26 s 0.12 s clean disconnect → reconnect
metro_m4_express 0.91 s 0.13 s see caveat below
frdm_k64f (host-only) 20.98 s 0.14 s UART boot banner; both attached fixtures enumerated per roster

All flashes finish far inside RECOVER_FLASH_TIMEOUT (90 s). openocd on the rig: 0.12.0+dev-02620-g0ce743125.

Known caveats (documented, deliberately not coded around)

  • metro_m4_express: the SAMD51 UF2 double-tap bootloader stays resident if two reset-run events land within its detection window (~1 s apart reproduced it; isolated flash-only and reset-only calls each boot the app fine). The real usbtest.py recovery separates its reset and reflash steps by a ≥5 s settle plus a wedged-pids re-check plus flash time, so the chained case needs unusually fast fall-through; the residual worst case is visible (subsequent cases fail against a healthy, enumerable 239a:0021 bootloader — no bus poison), and the next primary flash restores the app. Accepted as a known limitation.
  • frdm_k64f: openocd's on-target CRC-verify algorithm fails on this MK64FN1M0/k60.cfg/jlink combination and falls back to byte-wise read-back verify (~20 s of the 21 s total). Still comfortably inside budget; verify stays enabled — trading documented slowness for silent-bad-flash risk would be the wrong direction.
  • target/nrf52.cfg is deprecated in favor of target/nordic/nrf52.cfg; the committed args are the bench-validated ones and the alias works on the rig's build.

Out of scope

  • mimxrt1064_evk — the board that wedges most still has no automated recovery: the rig's openocd build has no i.MX RT target config. Sourcing/writing one is its own investigation.
  • flash_openocd keeps the program form — it's what the nine pinned CMSIS-DAP boards run in CI daily and was not re-validated here.
  • rescue_openocd (the deeper vid_pid-pinned rescue) remains exact-openocd-gated; openocd_seq entries don't take that path.
  • The convoy-unsafe operator hint in usbtest.py still suggests only "pin vid_pid on an openocd flasher"; mentioning the jlink-args flasher_recover alternative there is a possible follow-up touch.

Testing

  • test/hil/test/test_ci_select.py 224/224, test/hil/test/test_hil_bounded.py 108/108 (both offline, post-rebase on master's test_hil_select→test_ci_select move).
  • pre-commit run --all-files fully green (incl. unit-test, hil-test, ci-select-test hooks).
  • Hardware evidence above; boards and rig left idle/clean, locks released.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NWTa5C6ahUcWo7Z4kDjiju

Copilot AI lite review requested due to automatic review settings August 19, 2026 06:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the “convoy-safe recovery flasher” mechanism introduced in #3803 by adding an openocd_seq flasher backend plus flasher_recover roster entries for seven J-Link–based boards, enabling recovery flashing/reset over OpenOCD’s J-Link transport without triggering usbfs convoy hangs.

Changes:

  • Added openocd_seq flasher backend implementing an explicit OpenOCD command sequence (init/reset halt/flash write_image erase/verify_image/reset run/shutdown) and extended convoy_safe() + FLASHER_SUFFIX to recognize it.
  • Added flasher_recover entries to test/hil/tinyusb.json for seven boards, pairing each board’s existing J-Link probe UID with OpenOCD-over-J-Link arguments.
  • Extended test_hil_select.py roster/dispatch linting and added regression coverage ensuring openocd_seq does not use OpenOCD’s program command over the J-Link transport.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
test/hil/tinyusb.json Adds flasher_recover entries for seven J-Link boards using openocd_seq with OpenOCD J-Link interface args.
test/hil/test/test_hil_select.py Extends dispatch/suffix checks to include flasher_recover and adds focused tests for openocd_seq convoy-safety + command form.
test/hil/hil_flash.py Implements flash_openocd_seq/reset_openocd_seq, updates missing-vid_pid warning behavior for J-Link interface, and allows openocd_seq in convoy_safe/FLASHER_SUFFIX.
docs/superpowers/followup/pr3803-flasher-recover.md Removes the follow-up plan document now that the work is implemented.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@claude

claude Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Code-size comparison skipped: PR selection built no families on this push.

@github-actions

github-actions Bot commented Aug 19, 2026

Copy link
Copy Markdown

Hardware-in-the-loop (HIL) Test Report

hfp-iar

✅ 56 passed · ❌ 0 failed · ⚪ 0 skipped · blank not run

Board usbtest cdc_msc_throughput audio_test_freertos cdc_dual_ports cdc_msc cdc_msc_freertos dfu dfu_runtime hid_boot_interface hid_generic_inout midi_test msc_dual_lun mtp printer_to_cdc duration
stm32l412nucleo ✅ 30/30 ✅ C 501/511k M 511/511k 114s
lpcxpresso43s67 ✅ 30/30 ✅ C 10.8/10.2M M 33.9/31.8M 170s
stm32f746disco ✅ 30/30 ✅ C 13.5/13.7M M 22.8/30.7M 116s
stm32f746disco-DMA ✅ 30/30 ✅ C 13.5/12.7M M 25.9/32.8M 88s

hfp.json

✅ 56 passed · ❌ 0 failed · ⚪ 0 skipped · blank not run

Board usbtest cdc_msc_throughput audio_test_freertos cdc_dual_ports cdc_msc cdc_msc_freertos dfu dfu_runtime hid_boot_interface hid_generic_inout midi_test msc_dual_lun mtp printer_to_cdc duration
stm32l412nucleo ✅ 30/30 ✅ C 509/509k M 511/511k 136s
lpcxpresso43s67 ✅ 30/30 ✅ C 12.2/10.6M M 33.6/31.5M 136s
stm32f746disco ✅ 30/30 ✅ C 13.7/13.4M M 23.8/31.3M 115s
stm32f746disco-DMA ✅ 30/30 ✅ C 15/13.2M M 25.4/32.5M 86s

tinyusb-esp.json

✅ 20 passed · ❌ 4 failed · ⚪ 0 skipped · blank not run

Board usbtest msc_file_explorer_freertos audio_test_freertos cdc_msc_freertos device_info hid_composite_freertos duration
espressif_p4_function_ev ✅ 30/30 409 KB/s 117s
espressif_p4_function_ev-DMA ✅ 30/30 409 KB/s 107s
espressif_s3_devkitm ✅ 30/30 159s
espressif_s3_devkitm-DMA ✅ 30/30 197s

tinyusb.json

✅ 333 passed · ❌ 48 failed · ⚪ 17 skipped · blank not run

Board usbtest cdc_msc_throughput msc_file_explorer msc_file_explorer_freertos audio_test_freertos cdc_dual_ports cdc_msc cdc_msc_freertos cdc_msc_hid device_info dfu dfu_runtime hid_boot_interface hid_generic_inout host_info_to_device_cdc midi_test msc_dual_lun mtp printer_to_cdc duration
nanoch32v203-fsdev 9s
nanoch32v203-usbfs 8s
raspberry_pi_pico_w 1103 KB/s 1022 KB/s 21s
frdm_k64f 23s
raspberry_pi_pico2 76s
stm32g0b1nucleo ✅ 30/30 ✅ C 568/479k M 557/529k 168s
raspberry_pi_pico ❌ 0/30 ✅ C 505/537k M 649/515k 171s
ek_tm4c123gxl ✅ 30/30 ✅ C 507/511k M 511/512k 205s
ch32v103r_r1_1v0 ✅ 30/30 ✅ C 507/508k M 514/512k 214s
ra4m1_ek ✅ 30/30 ✅ C 533/510k M 533/518k 250s
lpcxpresso11u37 ✅ 30/30 ✅ C 489/268k M 510/511k 246s
metro_m4_express ✅ 30/30 ✅ C 544/515k M 537/520k 260s
stm32f407disco ✅ 30/30 ✅ C 507/539k M 557/530k 276s
lpcxpresso55s28 ✅ 30/30 ✅ C 8.6/8.2M M 31.3/28.4M 295s
max32666fthr ✅ 30/30 ✅ C 7/14.3M M 20.9/20.1M 298s
stm32l476disco ✅ 30/30 ✅ C 502/513k M 511/512k 309s
adafruit_fruit_jam ✅ 30/30 ✅ C 507/517k M 537/588k 62 KB/s 62 KB/s 273s
feather_nrf52840_express ✅ 30/30 ✅ C 507/511k M 511/550k 348s
stm32u083nucleo ✅ 30/30 ✅ C 500/511k M 511/512k 226s
stm32h743nucleo ✅ 30/30 ✅ C 569/550k M 600/583k 199s
stm32h743nucleo-DMA ✅ 30/30 ✅ C 527/511k M 511/511k 238s
stm32f072disco ✅ 30/30 ✅ C 507/256k M 510/483k 453s
mimxrt1064_evk ✅ 30/30 ✅ C 23.6/17.5M M 37.1/33.5M 1365 KB/s 253s
nrf54lm20dk ✅ 30/30 ✅ C 1.9/4.1M M 7.2/8.1M 253s
ch582m_evt ✅ 30/30 ✅ C 231/218k M 480/481k 283s
ch32v307v_r1_1v0-usbhs ✅ 30/30 ✅ C 7/8.2M M 24.1/14.5M 249s
ch32v307v_r1_1v0-usbfs ✅ 30/30 ✅ C 507/510k M 511/511k 120s
stm32f723disco ✅ 30/30 ✅ C 578/615k M 512/511k 13443 KB/s 3912 KB/s 415s
stm32f723disco-DMA ✅ 30/30 ✅ C 513/530k M 526/520k 14563 KB/s 3942 KB/s 160s

@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected across 2488 targets. View Project Dashboard →

@hathach
hathach force-pushed the hil-openocd-recover-flasher branch from fb195c8 to 4802ecd Compare August 26, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants