Skip to content

Implement pyOCD SWD/JTAG programming support with dynamic target detection#36

Draft
andrewleech wants to merge 12 commits into
Josverl:mainfrom
andrewleech:pyocd
Draft

Implement pyOCD SWD/JTAG programming support with dynamic target detection#36
andrewleech wants to merge 12 commits into
Josverl:mainfrom
andrewleech:pyocd

Conversation

@andrewleech

Copy link
Copy Markdown

Major Features Added

pyOCD Integration

  • Add SWD/JTAG programming as alternative to usb bootloader methods
  • Optional pyOCD dependency via pyocd extra
  • Support for debug probe discovery and management
  • Automated target chip selection using dynamic detection

Dynamic Target Detection

  • Replace hardcoded target mappings with dynamic API-based detection
  • Parse MCU info from sys.implementation._machine strings
  • Fuzzy matching algorithm for target selection
  • Direct probe-based target detection with fallback to fuzzy matching
  • Extensible architecture for future OpenOCD/J-Link support

CLI Integration

  • Add --method pyocd option for explicit SWD/JTAG programming
  • Add --probe option for specific debug probe selection
  • Maintain existing bootloader behavior as default
  • Clean integration with existing flash method selection

Architecture Improvements

  • Abstract debug probe layer for extensibility
  • Target detector abstraction with registry system
  • Proper error handling and fallback mechanisms
  • Performance optimized with caching and lazy loading

Technical Details

Files Added

  • mpflash/flash/debug_probe.py - Debug probe abstraction layer
  • mpflash/flash/pyocd_probe.py - pyOCD-specific probe implementation
  • mpflash/flash/pyocd_flash.py - pyOCD flash programming interface
  • mpflash/flash/pyocd_targets.py - Target detection wrapper functions
  • mpflash/flash/dynamic_targets.py - Dynamic target detection engine
  • mpflash/cli_pyocd.py - pyOCD-specific CLI commands (future)

Files Modified

  • mpflash/common.py - Add FlashMethod enum for different programming methods
  • mpflash/flash/__init__.py - Integrate pyOCD into flash method selection
  • mpflash/cli_flash.py - Add CLI options for pyOCD method and probe selection
  • pyproject.toml - Add optional pyOCD dependency
  • mpflash/cli_download.py - Fix unused pytest import

Key Benefits

  • No hardware requirements change - existing serial methods remain default
  • Automated target selection - no manual target configuration needed
  • Extensible design - easy to add OpenOCD, J-Link, etc. in future
  • Performance optimized - direct API calls instead of subprocess shells
  • Maintainable - eliminates hardcoded target mappings

Usage

# Existing behavior unchanged (serial bootloader methods)
mpflash flash

# Explicit pyOCD SWD/JTAG programming
mpflash flash --method pyocd

# Specific debug probe selection
mpflash flash --method pyocd --probe-id stlink

# Install with pyOCD support
uv sync --extra pyocd

Breaking Changes

None - all existing functionality preserved with same default behavior.

@andrewleech

andrewleech commented Aug 19, 2025

Copy link
Copy Markdown
Author

Hi @Josverl I'm not sure what your appetite is for large changes or if this a direction you like, but I wanted to use it so claude build it :-)

Also, I haven't code reviewed this at all yet, so feel free to not look at it at all either until I do so and un-draft the PR!

@andrewleech andrewleech mentioned this pull request Aug 19, 2025
@Josverl

Josverl commented Aug 20, 2025

Copy link
Copy Markdown
Owner

If you build it, you test it😜

I have found the challenge that in the HIL tests needed. I'm not sure if I have the equipment to test it.

Other than that, if others can benefit from it, I think it is useful

@codecov

codecov Bot commented May 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 48.86076% with 404 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.59%. Comparing base (f75f06a) to head (f5e0760).

Files with missing lines Patch % Lines
mpflash/cli_pyocd.py 14.38% 125 Missing ⚠️
mpflash/flash/pyocd_core.py 60.81% 100 Missing and 25 partials ⚠️
mpflash/flash/pyocd_flash.py 39.22% 109 Missing and 1 partial ⚠️
mpflash/flash/__init__.py 34.54% 32 Missing and 4 partials ⚠️
mpflash/flash/debug_probe.py 87.09% 7 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #36      +/-   ##
==========================================
- Coverage   75.55%   70.59%   -4.96%     
==========================================
  Files          54       58       +4     
  Lines        3031     3809     +778     
  Branches      488      622     +134     
==========================================
+ Hits         2290     2689     +399     
- Misses        621      974     +353     
- Partials      120      146      +26     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

pi-anl and others added 8 commits May 15, 2026 23:00
…ction

- Add SWD/JTAG programming as alternative to serial bootloader methods
- Support for debug probe discovery and management
- Automated target chip selection using dynamic detection
- Optional pyOCD dependency via `pyocd` extra

- Replace hardcoded target mappings with dynamic API-based detection
- Parse MCU info from `sys.implementation._machine` strings
- Fuzzy matching algorithm for target selection
- Direct probe-based target detection with fallback to fuzzy matching
- Extensible architecture for future OpenOCD/J-Link support

- Add `--method pyocd` option for explicit SWD/JTAG programming
- Add `--probe-id` option for specific debug probe selection
- Maintain existing serial bootloader behavior as default
- Clean integration with existing flash method selection

- Abstract debug probe layer for extensibility
- Target detector abstraction with registry system
- Proper error handling and fallback mechanisms
- Performance optimized with caching and lazy loading

- `mpflash/flash/debug_probe.py` - Debug probe abstraction layer
- `mpflash/flash/pyocd_probe.py` - pyOCD-specific probe implementation
- `mpflash/flash/pyocd_flash.py` - pyOCD flash programming interface
- `mpflash/flash/pyocd_targets.py` - Target detection wrapper functions
- `mpflash/flash/dynamic_targets.py` - Dynamic target detection engine
- `mpflash/cli_pyocd.py` - pyOCD-specific CLI commands (future)

- `mpflash/common.py` - Add FlashMethod enum for different programming methods
- `mpflash/flash/__init__.py` - Integrate pyOCD into flash method selection
- `mpflash/cli_flash.py` - Add CLI options for pyOCD method and probe selection
- `pyproject.toml` - Add optional pyOCD dependency
- `mpflash/cli_download.py` - Fix unused pytest import

- **No hardware requirements change** - existing serial methods remain default
- **Automated target selection** - no manual target configuration needed
- **Extensible design** - easy to add OpenOCD, J-Link, etc. in future
- **Performance optimized** - direct API calls instead of subprocess shells
- **Maintainable** - eliminates hardcoded target mappings

```bash
mpflash flash

mpflash flash --method pyocd

mpflash flash --method pyocd --probe-id stlink

uv sync --extra pyocd
```

None - all existing functionality preserved with same default behavior.
cli_flash_board previously returned 0/1/2 from the callback, but Click
ignores function return values for exit_code in standalone_mode, so the
CLI always exited 0 even on flash failure or user cancellation.

Switch to ctx.exit(N) so the documented exit codes (0 success, 1 flash
failure, 2 user cancellation) actually reach the shell and CliRunner.

Test adjustments:
- tests/integration/test_cli_integration.py:
  * Remove xfail from test_flash_failure_handling and
    test_interactive_parameter_prompting now that exit codes propagate.
  * test_flash_failure_handling now asserts on mock calls instead of
    loguru log output, which is order-dependent across the full suite.
- tests/cli/test_cli_flash.py:
  * test_mpflash_connected_comports: when serial ports are detected the
    test expects success, so make flash_tasks return a non-empty list
    and stub show_mcus to keep the success path quiet.
test_complete_pyocd_workflow_success previously asserted on the loguru
log message 'Flashed 1 boards' reaching result.output, which is fragile
because loguru handler configuration can change across tests run earlier
in the suite. The test passed in isolation but failed in full-suite runs.

Replace the log-output assertion with assertions on the show_mcus mock:
the mock must have been called once, with the boards returned by
flash_tasks. This verifies the same code path without depending on
loguru capture.
The flash_pyocd() implementation in mpflash/flash/pyocd_flash.py imports
is_pyocd_supported and get_unsupported_reason from pyocd_core (not the
_from_mcu variants), and probe selection happens inside PyOCDFlash, not
via find_probe_for_target / get_pyocd_target_from_mcu. The previous tests
patched names that do not exist on the pyocd_flash module, so the class
was xfailed.

Rewrite the three tests to:
- patch mpflash.flash.pyocd_flash.is_pyocd_supported
- patch mpflash.flash.pyocd_flash.get_unsupported_reason
- assert PyOCDFlash is constructed with probe_id / auto_install_packs
- simulate 'no probe' by having PyOCDFlash.flash_firmware raise the same
  MPFlashError the real code raises (probe lookup is internal to it now)

Remove the @pytest.mark.xfail marker on TestFlashPyOCDFunction.
PyOCDFlash.__init__ calls detect_pyocd_target() and is_pyocd_available()
(imported from pyocd_core) and stores the resulting target on
self.target_type. PyOCDFlash.flash_firmware() looks up the probe via
find_pyocd_probe() (defined in pyocd_flash itself), then calls
probe.program_flash(fw, target_type, **options).

The previous tests patched is_debug_programming_available,
get_pyocd_target_dynamic and find_debug_probe on pyocd_flash, none of
which exist there, so the whole class was xfailed.

Rewrite all six tests to patch the correct names on the pyocd_flash
module and to give Mock(spec=PyOCDProbe) a description attribute so the
debug log line in flash_firmware does not blow up. Remove the
@pytest.mark.xfail marker on TestPyOCDFlash.
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
see: pyocd/libusb-package#28

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Josverl added 2 commits May 15, 2026 23:09
- avoid hardcoded tempfile

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Josverl added 2 commits May 15, 2026 23:23
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
….14+

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants