Flash backend, PyOCD and MPBuild integration#96
Conversation
…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>
- 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 Report❌ Patch coverage is 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. 🚀 New features to boost your workflow:
|
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>
There was a problem hiding this comment.
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
mpbuildinto 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 |
| "A download refresh was attempted but no compatible firmware was found." | ||
| ) | ||
|
|
||
| return fw_info |
| try: | ||
| info = eval(raw_info) | ||
| self.family = info["family"] |
| """ | ||
| if not PYOCD_AVAILABLE: | ||
| log.error("pyOCD is not installed. Install with: uv add pyocd") | ||
| return 1 |
| # 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 |
|
I think you mentioned
I'm afraid its gotten bigger as I had progressed with restructuring the database before I got around to this. 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>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
Signed-off-by: Jos Verlinde <Jos_Verlinde@hotmail.com>
This PR introduces
This is inspired by, and an alternative to #36 and #37