Skip to content

Flash backend, PyOCD and MPBuild integration#96

Open
Josverl wants to merge 57 commits into
previewfrom
flash_plugins
Open

Flash backend, PyOCD and MPBuild integration#96
Josverl wants to merge 57 commits into
previewfrom
flash_plugins

Conversation

@Josverl

@Josverl Josverl commented Jun 11, 2026

Copy link
Copy Markdown
Owner

This PR introduces

  • a new flash backend architecture to make it simpler to add additional flash methods without needing spagetti code.
  • Add a pyOCD backend with dynamic target detection
  • Addresses known dependency issues with libusb on Windows for Python 3.14.
  • Integrates mpbuild.

This is inspired by, and an alternative to #36 and #37

pi-anl and others added 30 commits June 10, 2026 21:46
…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>
- avoid hardcoded tempfile

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
….14+

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
This should not be needed if the fallback version works good enough

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
They can be used by pyOCD flashing

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…ss outputs

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…ables and usage examples

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
leave just a shim behind

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…est instructions

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…guration

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…-root access

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Josverl and others added 12 commits June 10, 2026 21:46
- safe SWD frequency
- different reset modes

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…support

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
… logic

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Integrate mpbuild for building MicroPython firmware locally.

- Add BuildManager class with caching for 5-30 minute builds
- Implement firmware import to mpflash database
- Add --build CLI flag with comprehensive error handling
- Support Python 3.10+ requirement with clear messaging

Co-authored-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Co-authored-by: Andrew Leech <andrew@alelec.net>

(cherry picked from commit 6e6fa36)
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…ogic

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
…and clean functionality

Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 73.02885% with 561 lines in your changes missing coverage. Please review.
✅ Project coverage is 75.63%. Comparing base (98c1e0b) to head (25c2e41).

Files with missing lines Patch % Lines
mpflash/flash/builtins/pyocd/flash.py 40.12% 174 Missing and 14 partials ⚠️
mpflash/flash/builtins/pyocd/core.py 66.40% 93 Missing and 35 partials ⚠️
mpflash/build.py 78.76% 31 Missing and 24 partials ⚠️
mpflash/flash/__init__.py 78.90% 18 Missing and 9 partials ⚠️
mpflash/cli_pyocd.py 82.19% 14 Missing and 12 partials ⚠️
mpflash/cli_flash.py 77.63% 10 Missing and 7 partials ⚠️
mpflash/flash/builtins/uf2_backend.py 61.53% 12 Missing and 3 partials ⚠️
mpflash/bootloader/registry.py 77.77% 8 Missing and 4 partials ⚠️
mpflash/bootloader/activate.py 56.52% 5 Missing and 5 partials ⚠️
mpflash/flash/builtins/dfu_backend.py 85.50% 5 Missing and 5 partials ⚠️
... and 14 more
Additional details and impacted files
@@             Coverage Diff             @@
##           preview      #96      +/-   ##
===========================================
+ Coverage    75.60%   75.63%   +0.03%     
===========================================
  Files           54       74      +20     
  Lines         3033     4978    +1945     
  Branches       489      809     +320     
===========================================
+ Hits          2293     3765    +1472     
- Misses         621      964     +343     
- Partials       119      249     +130     

☔ View full report in Codecov by Harness.
📢 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.

Josverl added 4 commits June 11, 2026 21:39
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>

Copilot AI left a comment

Copy link
Copy Markdown

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 refactors MPFlash’s flashing workflow into a pluggable backend architecture, adds an opt-in pyOCD backend (with probe/target handling), and integrates mpbuild-based local firmware building—alongside new unit/integration and optional hardware-in-the-loop tests.

Changes:

  • Introduces a backend registry (FlashBackend, FlashContext, select_backend) and routes flashing through it.
  • Adds a pyOCD backend + CLI commands and options (--method pyocd, --probe-id, --target, CMSIS pack install).
  • Integrates mpbuild into the flash CLI flow and adds HIL test scaffolding + udev rules/docs for Linux USB permissions.

Reviewed changes

Copilot reviewed 82 out of 88 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
tests/test_build.py Tests mpbuild integration helpers
tests/mpremoteboard/test_mprb.py Adds mpremote reconnect/parse tests
tests/integration/test_cli_integration.py CLI integration tests for pyOCD flags
tests/hw/test_hw_backends.py Adds hardware smoke tests per backend
tests/hw/conftest.py HIL fixtures gated by env vars
tests/flash/test_worklist_refactored.py Tests new firmware ranking behavior
tests/flash/test_uf2_windows.py Updates UF2 import paths
tests/flash/test_uf2_mac.py Updates UF2 import paths
tests/flash/test_uf2_linux.py Updates UF2 import paths (commented)
tests/flash/test_uf2_boardid.py Updates UF2 import paths
tests/flash/test_stm32_dfu.py Updates DFU import paths
tests/flash/test_registry.py Tests backend registry + selection rules
tests/flash/test_flash_uf2_A.py Updates UF2 mocking paths
tests/flash/test_flash_tasks.py Adds path normalization + backend compatibility tests
tests/flash/test_flash_esp.py Updates esptool import paths
tests/flash/test_flash_1.py Updates bootloader/flash patch targets
tests/flash/test_boot_touch1200.py Updates touch1200 import paths
tests/fixtures/mock_pyocd_data.py Adds pyOCD mock datasets/MCUs
tests/db/test_loader.py Replaces mock with stdlib mock
tests/db/test_core.py Replaces mock with stdlib mock
tests/conftest.py Loads .env, adds pyOCD fixtures, resets probe registry
tests/cli/test_cli_flash.py Adds CLI behavior coverage for build/serial filtering
tests/cli/test_cli_download.py Replaces mock with stdlib mock
tests/bootloader/test_bootloader_registry.py Tests bootloader activator registry
tests/basicgit_test.py Replaces mock with stdlib mock
tests.md Documents HIL markers/env vars and usage
pyproject.toml Adds python-dotenv, defines pyocd/build extras, adds markers
mpflash/udev_rules/README.md Adds udev rules documentation
mpflash/udev_rules/65-mpflash-stm32-dfu.rules Adds STM32 DFU udev rule
mpflash/udev_rules/50-picoprobe.rules Adds Picoprobe udev rules
mpflash/udev_rules/50-cmsis-dap.rules Adds CMSIS-DAP udev rules
mpflash/udev_rules/49-wch-link.rules Adds WCH-Link udev rule
mpflash/udev_rules/49-vtlinkii.rules Adds ESLinkII udev rule
mpflash/udev_rules/49-stlinkv3.rules Adds STLinkV3 udev rules
mpflash/udev_rules/49-stlinkv2.rules Adds STLinkV2 udev rule
mpflash/udev_rules/49-stlinkv2-1.rules Adds STLinkV2-1 udev rules
mpflash/mpremoteboard/init.py Improves parsing/logging + restart probing behavior
mpflash/flash/worklist.py Adds firmware ranking + path normalization
mpflash/flash/services.py Adds backend services bundle (bootloader, platform, reenum)
mpflash/flash/registry.py Adds backend registry + entry-point discovery
mpflash/flash/context.py Adds FlashContext / FlashResult / Reason
mpflash/flash/builtins/uf2/wsl2.py Adds UF2 support for WSL2 mounts
mpflash/flash/builtins/uf2/windows.py Adds UF2 Windows helper module
mpflash/flash/builtins/uf2/volume.py Adds platform-aware UF2 volume handling
mpflash/flash/builtins/uf2/uf2disk.py Adds UF2 disk helper class
mpflash/flash/builtins/uf2/macos.py Adds UF2 macOS helper module
mpflash/flash/builtins/uf2/linux.py Adds UF2 Linux helper module
mpflash/flash/builtins/uf2/boardid.py Adds UF2 board-id parser
mpflash/flash/builtins/uf2/init.py Documents UF2 module role + retains UF2 logic
mpflash/flash/builtins/uf2_backend.py Adds UF2 backend wrapper + registration
mpflash/flash/builtins/pyocd/probes.py Adds pyOCD probe abstraction/registry
mpflash/flash/builtins/pyocd/init.py Adds pyOCD package marker
mpflash/flash/builtins/pyocd_backend.py Adds pyOCD backend wrapper + selection rules
mpflash/flash/builtins/esptool_backend.py Adds esptool backend wrapper + registration
mpflash/flash/builtins/esp/init.py Documents esp backend as internal
mpflash/flash/builtins/dfu/stm32_dfu.py Adds DFU polling + udev guidance integration
mpflash/flash/builtins/dfu/init.py Documents DFU module role
mpflash/flash/builtins/dfu_backend.py Adds DFU backend wrapper + readiness check
mpflash/flash/builtins/init.py Registers built-in backends on import
mpflash/flash/base.py Adds FlashBackend ABC contract
mpflash/flash/init.py Replaces port-ladder flashing with backend dispatcher
mpflash/custom/naming.py Minor log formatting cleanup
mpflash/common.py Adds FlashMethod enum + udev help builder
mpflash/cli_pyocd.py Adds pyOCD CLI commands
mpflash/cli_plugins.py Adds mpflash plugins backend listing
mpflash/cli_main.py Loads .env early + registers new CLI modules
mpflash/cli_flash.py Adds --method pyocd, probe/target flags, --build integration
mpflash/bootloader/registry.py Adds bootloader activator registry
mpflash/bootloader/detect.py Routes bootloader readiness via backend readiness probes
mpflash/bootloader/builtins/touch1200.py Converts touch1200 into activator + registers it
mpflash/bootloader/builtins/mpy.py Adds MicroPython activator implementation
mpflash/bootloader/builtins/manual.py Converts manual activator + registers it
mpflash/bootloader/builtins/init.py Imports built-in activators
mpflash/bootloader/base.py Adds BootloaderActivator ABC
mpflash/bootloader/activate.py Orchestrates bootloader entry via activator registry
justfile Adds pyocd extra + HIL recipes
docs/stm32_udev_rules.md Updates DFU udev rules documentation
.vscode/settings.json Updates VS Code settings + MCP sampling config
.vscode/mcp.json Adds local debug MCP server config
.github/workflows/pytest_mpflash.yml Installs all extras in CI
.env Adds HIL env var examples

Comment thread mpflash/flash/__init__.py Outdated
"A download refresh was attempted but no compatible firmware was found."
)

return fw_info
Comment on lines +217 to 219
try:
info = eval(raw_info)
self.family = info["family"]
Comment thread docs/stm32_udev_rules.md Outdated
Comment thread mpflash/udev_rules/50-picoprobe.rules Outdated
Comment thread mpflash/udev_rules/50-cmsis-dap.rules Outdated
Comment thread mpflash/udev_rules/49-vtlinkii.rules Outdated
Comment thread mpflash/cli_pyocd.py
Comment on lines +56 to +59
"""
if not PYOCD_AVAILABLE:
log.error("pyOCD is not installed. Install with: uv add pyocd")
return 1
Comment thread mpflash/bootloader/activate.py Outdated
Comment on lines 88 to 93
# todo - check every second or so for up to max wait time
time.sleep(wait_after)
# check if bootloader was entered
if in_bootloader(mcu):
if in_bootloader(mcu, backend=backend):
return True

return result
@Josverl

Josverl commented Jun 11, 2026

Copy link
Copy Markdown
Owner Author

@pi-anl,

I think you mentioned

I'm not sure what your appetite is for large changes

I'm afraid its gotten bigger as I had progressed with restructuring the database before I got around to this.
I want to expand to support more MCU families , and found that it got rather messy in all the decision trees.
So I tried to refactor into a (supposedly) pluggable flash-backend architecture ,
PyOCD is now refactored into one of the built-in plugins

$ mpflash plugins
                                      Flash backends (wsl2)                                      
┏━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━┓
┃ Name    ┃ Ports          ┃ Formats          ┃ Platforms       ┃ Prio ┃ Bootloader ┃ Available ┃
┡━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━┩
│ uf2     │ nrf, rp2, samd │ .uf2             │ linux, macos,   │   10 │    yes     │    yes    │
│         │                │                  │ windows, wsl2   │      │            │           │
│ dfu     │ stm32          │ .dfu, .bin       │ linux, macos,   │   10 │    yes     │    yes    │
│         │                │                  │ windows, wsl2   │      │            │           │
│ esptool │ esp32, esp8266 │ .bin             │ linux, macos,   │   10 │     no     │    yes    │
│         │                │                  │ windows, wsl2   │      │            │           │
│ pyocd   │ *              │ .bin, .hex,      │ linux, macos,   │  -10 │     no     │    yes    │
│         │                │ .elf, .axf       │ windows, wsl2   │      │            │           │
└─────────┴────────────────┴──────────────────┴─────────────────┴──────┴────────────┴───────────┘

I tested PyOCD on a limited number of probes and it works on my machine , but I would appreciate if you can check if it still works for your initial intent.

I also added the mpbuild integration, and added a --clean option and fixed several minor gotchas wrt to variants and versions , but that seems to work quite well.

I`m still cleaning things up , and will need to clean up the commits and write some documentation on the plugin stuff to make that useful.

…onsistency fixes

- Remove unreachable `return fw_info` after `raise MPFlashError` in flash/__init__.py
- Replace unsafe `eval()` with `ast.literal_eval()` in mpremoteboard/__init__.py
- Fix hard-coded developer path in docs/stm32_udev_rules.md
- Update udev rules to use explicit zero-padded MODE:=\"0666\" in 50-picoprobe.rules and 50-cmsis-dap.rules
- Add missing SUBSYSTEM==\"usb\" and fix MODE assignment in 49-vtlinkii.rules
- Fix install hint from `uv add pyocd` to `uv sync --extra pyocd` in cli_pyocd.py
- Fix enter_bootloader() false-positive: return False instead of result when no activator confirmed bootloader mode"
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Josverl added 2 commits June 12, 2026 12:05
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
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.

4 participants