pyflashgui 0.2.0__tar.gz → 0.2.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyflashgui-0.2.0/pyflashgui.egg-info → pyflashgui-0.2.2}/PKG-INFO +4 -3
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/PYPI.md +1 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/README.md +6 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/__init__.py +1 -1
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/__main__.py +3 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/base.py +10 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/jlink_backend.py +29 -6
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/pyocd_backend.py +58 -20
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/pyocd_packs.py +47 -10
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/events.py +12 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/excepthook.py +6 -7
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/flasher.py +46 -3
- pyflashgui-0.2.2/pyflashgui/core/library_log.py +96 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/memory_map.py +2 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/pack_manager.py +9 -1
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/worker.py +1 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/about.md +26 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/changelog.md +55 -5
- pyflashgui-0.2.2/pyflashgui/help/main-window.png +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/manual.md +10 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/memory.md +2 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/probe.md +3 -3
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/confirm.py +8 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/crop_dialog.py +2 -2
- pyflashgui-0.2.2/pyflashgui/ui/diagnostics.py +264 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/fonts.py +1 -1
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/help_menu.py +39 -15
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/main_window.py +45 -3
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/packs_dialog.py +17 -3
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/probe_bar.py +2 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/theme.py +59 -5
- {pyflashgui-0.2.0 → pyflashgui-0.2.2/pyflashgui.egg-info}/PKG-INFO +4 -3
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/SOURCES.txt +6 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/requires.txt +2 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyproject.toml +6 -3
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_backends.py +33 -2
- pyflashgui-0.2.2/tests/test_diagnostics.py +174 -0
- pyflashgui-0.2.2/tests/test_docking.py +713 -0
- pyflashgui-0.2.2/tests/test_event_log_times.py +72 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_flash_panes.py +17 -2
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_help.py +7 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_jlink_backend.py +1 -1
- pyflashgui-0.2.2/tests/test_layout.py +166 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_packs.py +37 -4
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_read_back.py +1 -1
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_theme.py +46 -2
- pyflashgui-0.2.0/pyflashgui/help/main-window.png +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/LICENSE +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/NOTICE +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/__init__.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/simulated.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/__init__.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/checkout.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/compare.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/context.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/images.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/installation.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/layout.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/packages.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/paths.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/settings.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/shortcut.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/slow_start.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/timing.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/updates.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/workspaces.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/__init__.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/console.md +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/files.md +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/memory-map.md +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/__init__.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.ico +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.png +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.svg +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui_small.svg +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/tick.svg +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/__init__.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/buttons.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/console_view.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/detached.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/erase_dialog.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/event_colours.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/files_view.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/folders.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/map_view.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/memory_view.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/messages.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/pane_bar.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/panes.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/persist.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/read_dialog.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/scripting.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/scripts_menu.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/session.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/workspace_menu.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/dependency_links.txt +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/entry_points.txt +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/top_level.txt +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/setup.cfg +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_build_version.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_changelog.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_checkout.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_compare.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_console.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_erase.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_events.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_excepthook.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_flasher.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_folders.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_fonts.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_headers.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_icon.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_images.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_installation.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_launcher.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_memory_map.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_messages.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_notice.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_packages.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_packaging.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_persist.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_readme.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_scripts_menu.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_shortcut.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_slow_start.py +0 -0
- {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_workspaces.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyflashgui
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Graphical flash programming tool: Arm Cortex-M with any probe pyOCD supports, and more through J-Link Commander
|
|
5
5
|
Author: davhodg
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -29,12 +29,12 @@ Description-Content-Type: text/markdown
|
|
|
29
29
|
License-File: LICENSE
|
|
30
30
|
License-File: NOTICE
|
|
31
31
|
Requires-Dist: PySide6-Essentials>=6.8
|
|
32
|
-
Requires-Dist: pyocd>=0.
|
|
32
|
+
Requires-Dist: pyocd>=0.46
|
|
33
33
|
Requires-Dist: bincopy>=20.0
|
|
34
34
|
Provides-Extra: dev
|
|
35
35
|
Requires-Dist: pytest; extra == "dev"
|
|
36
36
|
Requires-Dist: pytest-xdist; extra == "dev"
|
|
37
|
-
Requires-Dist: ruff==0.
|
|
37
|
+
Requires-Dist: ruff==0.17.*; extra == "dev"
|
|
38
38
|
Requires-Dist: trove-classifiers; extra == "dev"
|
|
39
39
|
Dynamic: license-file
|
|
40
40
|
|
|
@@ -97,6 +97,7 @@ A probe is usable without root on Linux once a udev rule allows it; pyOCD publis
|
|
|
97
97
|
- **Workspaces**: one for each board or project, holding its probe, target, files and layout.
|
|
98
98
|
- **A Python console** with the probe and the target as `flash`: read, write RAM, erase, program, verify and reset from a prompt or a script, each call waiting for its answer. *File > Run script...* runs one, and keeps the last five a click away.
|
|
99
99
|
- **A simulated probe and target**, to try all of it with no hardware.
|
|
100
|
+
- **Help > Diagnostics**: how the connection was made, the last operations in order and what pyOCD reported, ready to paste into a bug report.
|
|
100
101
|
|
|
101
102
|
The [manual](https://github.com/davhodg/pyflashgui/blob/master/pyflashgui/help/manual.md) covers each of these, and is also under **Help > Documentation** in the application.
|
|
102
103
|
|
|
@@ -57,6 +57,7 @@ A probe is usable without root on Linux once a udev rule allows it; pyOCD publis
|
|
|
57
57
|
- **Workspaces**: one for each board or project, holding its probe, target, files and layout.
|
|
58
58
|
- **A Python console** with the probe and the target as `flash`: read, write RAM, erase, program, verify and reset from a prompt or a script, each call waiting for its answer. *File > Run script...* runs one, and keeps the last five a click away.
|
|
59
59
|
- **A simulated probe and target**, to try all of it with no hardware.
|
|
60
|
+
- **Help > Diagnostics**: how the connection was made, the last operations in order and what pyOCD reported, ready to paste into a bug report.
|
|
60
61
|
|
|
61
62
|
The [manual](https://github.com/davhodg/pyflashgui/blob/master/pyflashgui/help/manual.md) covers each of these, and is also under **Help > Documentation** in the application.
|
|
62
63
|
|
|
@@ -126,6 +126,10 @@ pyflashgui can instead drive a J-Link through SEGGER's own J-Link Commander, whi
|
|
|
126
126
|
|
|
127
127
|
On Linux a probe is usable without root once a udev rule allows it; pyOCD publishes [a set of them](https://github.com/pyocd/pyOCD/tree/main/udev). Vendor software is never bundled with pyflashgui: install the vendor's own, and pyflashgui finds it.
|
|
128
128
|
|
|
129
|
+
### Isolating the probe
|
|
130
|
+
|
|
131
|
+
A USB isolator between the PC and the probe breaks a ground loop between them. [usb2-480mbps-isolator](https://github.com/davhodg/usb2-480mbps-isolator) is an open-source, hand-solderable one in the works, for USB 2.0 at every speed up to 480 Mbit/s, that limits the current it takes to what the PC's port can supply. **Available soon:** it is still being designed, and is for functional isolation only, not certified for safety isolation.
|
|
132
|
+
|
|
129
133
|
## Built with
|
|
130
134
|
|
|
131
135
|
| Package | For | Licence |
|
|
@@ -151,6 +155,8 @@ and no way of uninstalling touches them; delete that folder to remove them too.
|
|
|
151
155
|
|
|
152
156
|
See [CONTRIBUTING.md](CONTRIBUTING.md): the policy for changes, setting up to work on pyflashgui, running the tests, and building the Windows installer. Everyone taking part is asked to follow the [code of conduct](CODE_OF_CONDUCT.md). Security problems go through [SECURITY.md](SECURITY.md).
|
|
153
157
|
|
|
158
|
+
What is aimed for with a keyboard, a screen reader, large text and high contrast, what has been checked, and how to report a barrier, is in [ACCESSIBILITY.md](ACCESSIBILITY.md).
|
|
159
|
+
|
|
154
160
|
## Safety notice
|
|
155
161
|
|
|
156
162
|
pyflashgui talks to real equipment. It is intended only for people trained and experienced in working with microcontrollers, debug probes and the equipment they are part of. It is a tool for development and the bench, and is not meant for production programming.
|
|
@@ -89,7 +89,7 @@ def selftest() -> int:
|
|
|
89
89
|
|
|
90
90
|
# libusb is a native library carried inside a Python package, which is
|
|
91
91
|
# the kind of file a build leaves behind. Without it pyOCD lists no
|
|
92
|
-
# CMSIS-DAP v2 probe and no ST-Link,
|
|
92
|
+
# CMSIS-DAP v2 probe and no ST-Link, so the self-test checks for it.
|
|
93
93
|
try:
|
|
94
94
|
import libusb_package
|
|
95
95
|
|
|
@@ -160,7 +160,8 @@ def set_icon(app) -> None:
|
|
|
160
160
|
|
|
161
161
|
#: The libraries Qt's X11 plugin needs that Debian, Ubuntu and Linux Mint can
|
|
162
162
|
#: leave out, each with the package it comes in. Without one, Qt stops with
|
|
163
|
-
#: "could not load the Qt platform plugin xcb"
|
|
163
|
+
#: "could not load the Qt platform plugin xcb", so they are looked for first
|
|
164
|
+
#: and the package to install is named.
|
|
164
165
|
XCB_LIBRARIES = (
|
|
165
166
|
("libxcb-cursor.so.0", "libxcb-cursor0"),
|
|
166
167
|
("libxcb-icccm.so.4", "libxcb-icccm4"),
|
|
@@ -197,6 +197,16 @@ class Connection(ABC):
|
|
|
197
197
|
def close(self) -> None:
|
|
198
198
|
"""Let go of the probe. Safe to call twice."""
|
|
199
199
|
|
|
200
|
+
def details(self) -> list[str]:
|
|
201
|
+
"""What Help > Diagnostics says of this connection, a line each.
|
|
202
|
+
|
|
203
|
+
How the backend was asked to connect and what it found there, beyond
|
|
204
|
+
the probe and the target the window already knows. Read on the
|
|
205
|
+
window's thread while the worker may be using the connection, so
|
|
206
|
+
only what is already known is said: nothing here asks the probe.
|
|
207
|
+
"""
|
|
208
|
+
return []
|
|
209
|
+
|
|
200
210
|
def read_range(
|
|
201
211
|
self,
|
|
202
212
|
address: int,
|
|
@@ -72,13 +72,14 @@ NOT_FOUND = (
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
#: Said by Commander when the probe is there and the device does not answer.
|
|
75
|
-
#: Looked for as well as its exit code, which
|
|
75
|
+
#: Looked for as well as its exit code, which can be 0 with it.
|
|
76
76
|
NO_TARGET = "Cannot connect to target"
|
|
77
77
|
|
|
78
|
-
#: A J-Link script that does nothing where
|
|
78
|
+
#: A J-Link script that does nothing where J-Link's software would set the
|
|
79
79
|
#: device up after connecting to it. On some parts that set-up writes to
|
|
80
80
|
#: the device -- an S32K3's RAM is filled, for its error correction -- and
|
|
81
|
-
#: a read made to see what a running program left there finds
|
|
81
|
+
#: a read made to see what a running program left there finds the set-up's
|
|
82
|
+
#: bytes instead.
|
|
82
83
|
NO_SETUP_SCRIPT = "int SetupTarget(void) {\n return 0;\n}\n"
|
|
83
84
|
|
|
84
85
|
#: Where Commander is, when it has been said. None to go and look.
|
|
@@ -146,6 +147,20 @@ def find_commander() -> list[str] | None:
|
|
|
146
147
|
return None
|
|
147
148
|
|
|
148
149
|
|
|
150
|
+
def report() -> list[str]:
|
|
151
|
+
"""For Help > Diagnostics: the Commander used, how it was found, and every
|
|
152
|
+
J-Link software folder there is, newest first."""
|
|
153
|
+
found = find_commander()
|
|
154
|
+
if found is None:
|
|
155
|
+
lines = ["not found"]
|
|
156
|
+
else:
|
|
157
|
+
how = "chosen in Tools > Settings" if _chosen is not None else "found"
|
|
158
|
+
lines = [f"{' '.join(found)} ({how})"]
|
|
159
|
+
folders = _installed()
|
|
160
|
+
lines += [f"installed: {folder}" for folder in folders] or ["no J-Link software folder found"]
|
|
161
|
+
return lines
|
|
162
|
+
|
|
163
|
+
|
|
149
164
|
def _is_seggers(program: Path) -> bool:
|
|
150
165
|
"""Whether a program of Commander's name is Commander, and not Java's jlink.
|
|
151
166
|
|
|
@@ -407,6 +422,11 @@ class JLinkConnection(Connection):
|
|
|
407
422
|
def regions(self) -> list[Region]:
|
|
408
423
|
return list(self._regions)
|
|
409
424
|
|
|
425
|
+
def details(self) -> list[str]:
|
|
426
|
+
if self._arguments is None:
|
|
427
|
+
return ["closed"]
|
|
428
|
+
return [f"device: {self._device}", f"Commander run with: {' '.join(self._arguments)}"]
|
|
429
|
+
|
|
410
430
|
def erase_chip(self) -> None:
|
|
411
431
|
self._run(["H", "Erase"], WORK_S, f"Erasing {self._device}")
|
|
412
432
|
|
|
@@ -535,7 +555,10 @@ class JLinkConnection(Connection):
|
|
|
535
555
|
|
|
536
556
|
def reset(self, halt: bool = False, kind: str = RESET_DEFAULT) -> None:
|
|
537
557
|
if kind != RESET_DEFAULT:
|
|
538
|
-
raise BackendError(
|
|
558
|
+
raise BackendError(
|
|
559
|
+
"Through J-Link Commander, a device is reset the usual way only. The other "
|
|
560
|
+
"ways are offered through pyOCD."
|
|
561
|
+
)
|
|
539
562
|
# R restarts the device and leaves it stopped; G sets it going.
|
|
540
563
|
self._run(["R"] if halt else ["R", "G"], QUICK_S, f"Resetting {self._device}")
|
|
541
564
|
|
|
@@ -581,8 +604,8 @@ class JLinkBackend(Backend):
|
|
|
581
604
|
# it that is not, and latin-1 reads any byte there is.
|
|
582
605
|
text = path.read_text(encoding="latin-1")
|
|
583
606
|
except (BackendError, OSError):
|
|
584
|
-
#
|
|
585
|
-
#
|
|
607
|
+
# Without the list, a device whose name is typed in is still
|
|
608
|
+
# programmed.
|
|
586
609
|
text = ""
|
|
587
610
|
self._devices = {device.name.upper(): device for device in parse_devices(text)}
|
|
588
611
|
return self._devices
|
|
@@ -7,7 +7,7 @@ exception said again in pyflashgui's words. Anything worth testing without a
|
|
|
7
7
|
probe plugged in is in ``core`` or in the base class, and what is left here
|
|
8
8
|
can only be tried on hardware.
|
|
9
9
|
|
|
10
|
-
pyOCD is imported inside the methods that use it. It is the
|
|
10
|
+
pyOCD is imported inside the methods that use it. It is the largest thing
|
|
11
11
|
pyflashgui loads, and a window should not wait for it to say which probes
|
|
12
12
|
are plugged in before it can be drawn.
|
|
13
13
|
"""
|
|
@@ -17,6 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
import contextlib
|
|
18
18
|
import time
|
|
19
19
|
from collections.abc import Sequence
|
|
20
|
+
from pathlib import Path
|
|
20
21
|
|
|
21
22
|
from pyflashgui.backends import pyocd_packs
|
|
22
23
|
from pyflashgui.backends.base import (
|
|
@@ -90,9 +91,8 @@ def regions_of(memory_map, generic: bool = False) -> list[Region]:
|
|
|
90
91
|
def needs_one_packet_at_a_time(*names: str | None) -> bool:
|
|
91
92
|
"""Whether a probe called this must be sent one command and then waited for.
|
|
92
93
|
|
|
93
|
-
pyOCD's own advice for the TI XDS110
|
|
94
|
-
|
|
95
|
-
is not known which versions are free of it, and the cost is only speed.
|
|
94
|
+
pyOCD's own advice for the TI XDS110: one command at a time. Set for
|
|
95
|
+
every one of them, as pyOCD advises, and the cost is only speed.
|
|
96
96
|
"""
|
|
97
97
|
return any("xds110" in (name or "").lower() for name in names)
|
|
98
98
|
|
|
@@ -144,11 +144,11 @@ def _stages(progress: Progress) -> Progress:
|
|
|
144
144
|
def _let_go(session) -> Exception | None:
|
|
145
145
|
"""Close a pyOCD session, and its probe even where closing the session fails.
|
|
146
146
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Returns what went wrong, if anything did.
|
|
147
|
+
Closing a session lets go of the target, then of the probe. With the
|
|
148
|
+
board's power turned off, letting go of the target raises, and the probe
|
|
149
|
+
is still open: a J-Link still open is listed by Find probes, and every
|
|
150
|
+
connect says "No emulator with serial number". So the probe is closed
|
|
151
|
+
here as well. Returns what went wrong, if anything did.
|
|
152
152
|
"""
|
|
153
153
|
try:
|
|
154
154
|
session.close()
|
|
@@ -202,6 +202,44 @@ class PyocdConnection(Connection):
|
|
|
202
202
|
#: Makes a new session on the same probe and target, attaching to it
|
|
203
203
|
#: as it is, for when a reset has taken the old one with it.
|
|
204
204
|
self._reopen = reopen
|
|
205
|
+
#: The options pyOCD was given, for Help > Diagnostics.
|
|
206
|
+
self.options: dict = {}
|
|
207
|
+
#: How many times a reset took the connection with it and it was made again.
|
|
208
|
+
self.reattached = 0
|
|
209
|
+
|
|
210
|
+
def details(self) -> list[str]:
|
|
211
|
+
packs = self.options.get("pack") or []
|
|
212
|
+
given = ", ".join(f"{k}={v}" for k, v in sorted(self.options.items()) if k != "pack")
|
|
213
|
+
lines = [f"pyOCD options: {given or 'none'}"]
|
|
214
|
+
if packs:
|
|
215
|
+
lines.append(f"packs handed to pyOCD: {', '.join(Path(p).name for p in packs)}")
|
|
216
|
+
if self.reattached:
|
|
217
|
+
lines.append(f"connected again after a reset: {self.reattached} time(s)")
|
|
218
|
+
session = self._session
|
|
219
|
+
if session is None:
|
|
220
|
+
return [*lines, "session: closed"]
|
|
221
|
+
target, probe = getattr(session, "target", None), getattr(session, "probe", None)
|
|
222
|
+
if target is not None:
|
|
223
|
+
if is_generic(target):
|
|
224
|
+
where = "no device in particular: the generic Cortex-M"
|
|
225
|
+
elif "_pack_device" in vars(type(target)):
|
|
226
|
+
where = "from a pack"
|
|
227
|
+
else:
|
|
228
|
+
where = "built in"
|
|
229
|
+
lines.append(f"target class: {type(target).__name__}, {where}")
|
|
230
|
+
for core in getattr(target, "cores", {}).values():
|
|
231
|
+
architecture = getattr(getattr(core, "architecture", None), "name", "")
|
|
232
|
+
lines.append(
|
|
233
|
+
f"core {getattr(core, 'core_number', '?')}: {getattr(core, 'name', '?')}"
|
|
234
|
+
+ (f" ({architecture})" if architecture else "")
|
|
235
|
+
)
|
|
236
|
+
if probe is not None:
|
|
237
|
+
wire = getattr(getattr(probe, "wire_protocol", None), "name", "") or "not known"
|
|
238
|
+
lines.append(
|
|
239
|
+
f"probe: {probe.description}; {type(probe).__name__}; "
|
|
240
|
+
f"{probe.vendor_name or '?'} {probe.product_name or '?'}; wire protocol {wire}"
|
|
241
|
+
)
|
|
242
|
+
return lines
|
|
205
243
|
|
|
206
244
|
def _target(self):
|
|
207
245
|
if self._session is None:
|
|
@@ -277,13 +315,12 @@ class PyocdConnection(Connection):
|
|
|
277
315
|
"""Reset the target and stop it at its first instruction, before a flash
|
|
278
316
|
routine is run on it.
|
|
279
317
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
download does.
|
|
318
|
+
A flash routine runs on the processor as it is when stopped.
|
|
319
|
+
Reported on an XMC4500 attached to its firmware as it ran: stopped
|
|
320
|
+
amid the firmware, with its interrupts and the rest of what it had set
|
|
321
|
+
up still on, the erase routine ended in a HardFault (IPSR=3). Reset
|
|
322
|
+
first, the routine runs on a device as it starts, which is what flash
|
|
323
|
+
routines are written for.
|
|
287
324
|
"""
|
|
288
325
|
try:
|
|
289
326
|
self._target().reset_and_halt()
|
|
@@ -328,9 +365,8 @@ class PyocdConnection(Connection):
|
|
|
328
365
|
progress((before + fraction * size) / total)
|
|
329
366
|
|
|
330
367
|
# keep_unwritten: the rest of a page written to is read and
|
|
331
|
-
# written back as it is
|
|
332
|
-
# erased flash
|
|
333
|
-
# says FF for an XMC4500, whose erased flash reads 00.
|
|
368
|
+
# written back as it is, so it holds what it held, whatever
|
|
369
|
+
# erased flash reads as on this device.
|
|
334
370
|
builder.program(progress_cb=report, smart_flash=False, keep_unwritten=True)
|
|
335
371
|
written += size
|
|
336
372
|
except BackendError:
|
|
@@ -416,6 +452,7 @@ class PyocdConnection(Connection):
|
|
|
416
452
|
time.sleep(wait)
|
|
417
453
|
try:
|
|
418
454
|
self._session = self._reopen()
|
|
455
|
+
self.reattached += 1
|
|
419
456
|
break
|
|
420
457
|
except BackendError as exc:
|
|
421
458
|
failed = exc
|
|
@@ -444,7 +481,7 @@ class PyocdConnection(Connection):
|
|
|
444
481
|
|
|
445
482
|
class PyocdBackend(Backend):
|
|
446
483
|
# pyOCD's own, whatever the probe could do: 1 MHz, chosen to work with
|
|
447
|
-
# a long or untidy cable
|
|
484
|
+
# a long or untidy cable. A faster one can be chosen in the bar.
|
|
448
485
|
default_speed = "Default (1 MHz)"
|
|
449
486
|
can_skip_erase = True
|
|
450
487
|
can_attach = True
|
|
@@ -587,6 +624,7 @@ class PyocdBackend(Backend):
|
|
|
587
624
|
connection = PyocdConnection(session, lambda: self._open(chosen, again, target))
|
|
588
625
|
connection.speed = speed_text(session.options.get("frequency"))
|
|
589
626
|
connection.notes = tuple(notes)
|
|
627
|
+
connection.options = dict(options)
|
|
590
628
|
return connection
|
|
591
629
|
|
|
592
630
|
@staticmethod
|
|
@@ -93,7 +93,15 @@ class Device:
|
|
|
93
93
|
installed: bool
|
|
94
94
|
|
|
95
95
|
|
|
96
|
-
|
|
96
|
+
#: Called once, when fetching the list moves on from downloading to what
|
|
97
|
+
#: comes after: the library reads every description -- over fifteen hundred --
|
|
98
|
+
#: and writes the list of devices from them, twenty megabytes, which takes
|
|
99
|
+
#: half a minute or so with no count of its own to show.
|
|
100
|
+
Stage = Callable[[str], None]
|
|
101
|
+
PROCESSING = "Processing what was downloaded"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def open_cache(tick: Tick | None = None, stage: Stage | None = None) -> Any:
|
|
97
105
|
"""pyOCD's own store of packs. Replaced in the tests, which have no network."""
|
|
98
106
|
import cmsis_pack_manager
|
|
99
107
|
|
|
@@ -103,6 +111,14 @@ def open_cache(tick: Tick | None = None) -> Any:
|
|
|
103
111
|
if tick is not None:
|
|
104
112
|
tick(current, total or None)
|
|
105
113
|
|
|
114
|
+
# The first thing the library does after the last download. Should a
|
|
115
|
+
# later version of it name this otherwise, it is not called and the
|
|
116
|
+
# step is not said: nothing else changes.
|
|
117
|
+
def _call_rust_parse(self, pdsc_index):
|
|
118
|
+
if stage is not None:
|
|
119
|
+
stage(PROCESSING)
|
|
120
|
+
return super()._call_rust_parse(pdsc_index)
|
|
121
|
+
|
|
106
122
|
return Cache(tick is None, False)
|
|
107
123
|
|
|
108
124
|
|
|
@@ -158,14 +174,20 @@ def list_fetched() -> datetime | None:
|
|
|
158
174
|
return None
|
|
159
175
|
|
|
160
176
|
|
|
161
|
-
def fetch_list(tick: Tick | None = None) -> int:
|
|
162
|
-
"""Download the index of every pack. Returns how many devices it has.
|
|
163
|
-
|
|
177
|
+
def fetch_list(tick: Tick | None = None, stage: Stage | None = None) -> int:
|
|
178
|
+
"""Download the index of every pack. Returns how many devices it has.
|
|
179
|
+
|
|
180
|
+
``stage`` is told when the downloads have ended and what they brought is
|
|
181
|
+
processed, which is as long again.
|
|
182
|
+
"""
|
|
183
|
+
cache = open_cache(tick, stage)
|
|
164
184
|
try:
|
|
165
185
|
cache.cache_descriptors()
|
|
166
186
|
except Exception as exc:
|
|
167
187
|
raise _said("Fetching the list of devices", exc) from exc
|
|
168
|
-
|
|
188
|
+
# Through the copy kept of it, which the list of packs and the search
|
|
189
|
+
# that follow then use rather than reading twenty megabytes again.
|
|
190
|
+
count = len(_index(open_cache()))
|
|
169
191
|
if not count:
|
|
170
192
|
raise PackError(
|
|
171
193
|
"Fetching the list of devices brought nothing back. It comes from the internet: "
|
|
@@ -248,9 +270,8 @@ def read_pack(path: Path) -> tuple[Pack, tuple[tuple[str, str], ...]]:
|
|
|
248
270
|
from xml.etree import ElementTree
|
|
249
271
|
|
|
250
272
|
# By the way in pyOCD's own modules take, before the part of it wanted
|
|
251
|
-
# here
|
|
252
|
-
#
|
|
253
|
-
# thread at the same moment.
|
|
273
|
+
# here: its modules are written to be imported from the top, and the
|
|
274
|
+
# backend may be importing it on another thread at the same moment.
|
|
254
275
|
import pyocd.target # noqa: F401
|
|
255
276
|
from pyocd.target.pack.cmsis_pack import CmsisPack
|
|
256
277
|
|
|
@@ -263,8 +284,8 @@ def read_pack(path: Path) -> tuple[Pack, tuple[tuple[str, str], ...]]:
|
|
|
263
284
|
f"{path.name} is not a CMSIS-Pack: there is no description (.pdsc) in it."
|
|
264
285
|
)
|
|
265
286
|
root = ElementTree.fromstring(archive.read(min(described, key=lambda n: n.count("/"))))
|
|
266
|
-
# Read through this archive, which is closed after
|
|
267
|
-
#
|
|
287
|
+
# Read through this archive, which is closed after, so the file can
|
|
288
|
+
# be removed again: Windows will not delete an open file.
|
|
268
289
|
found = {
|
|
269
290
|
(d.part_number, (d.vendor or "").split(":")[0]) for d in CmsisPack(archive).devices
|
|
270
291
|
}
|
|
@@ -314,6 +335,22 @@ def file_devices() -> list[Device]:
|
|
|
314
335
|
return out
|
|
315
336
|
|
|
316
337
|
|
|
338
|
+
def report() -> list[str]:
|
|
339
|
+
"""For Help > Diagnostics: where the packs are, which there are, and how
|
|
340
|
+
old the list of devices is. Found from the files, without reading the
|
|
341
|
+
list itself, which is twenty megabytes."""
|
|
342
|
+
where = folder()
|
|
343
|
+
fetched = list_fetched()
|
|
344
|
+
lines = [
|
|
345
|
+
f"folder: {where}",
|
|
346
|
+
f"list of devices fetched {fetched:%Y-%m-%d %H:%M}" if fetched else "list never fetched",
|
|
347
|
+
]
|
|
348
|
+
downloaded = sorted(p.relative_to(where).as_posix() for p in where.rglob("*.pack"))
|
|
349
|
+
lines += [f"installed: {name}" for name in downloaded] or ["none installed"]
|
|
350
|
+
lines += [f"added from a file: {p.name}" for p in sorted(files_folder().glob("*.pack"))]
|
|
351
|
+
return lines
|
|
352
|
+
|
|
353
|
+
|
|
317
354
|
def same_name(part: str) -> str:
|
|
318
355
|
"""A device's name as pyOCD files it, whichever way it is spelt.
|
|
319
356
|
|
|
@@ -22,6 +22,8 @@ plain text, and nothing has to be imported to name one.
|
|
|
22
22
|
|
|
23
23
|
from __future__ import annotations
|
|
24
24
|
|
|
25
|
+
from datetime import datetime
|
|
26
|
+
|
|
25
27
|
from PySide6.QtCore import QObject, Signal
|
|
26
28
|
|
|
27
29
|
INFORMATION = "information"
|
|
@@ -43,6 +45,16 @@ LEVELS = (INFORMATION, GOOD, WARNING, ERROR)
|
|
|
43
45
|
PROBLEMS = (WARNING, ERROR)
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
def with_time(line: str, at: datetime) -> str:
|
|
49
|
+
"""A line with the time of day it was written in front, to the second.
|
|
50
|
+
|
|
51
|
+
To the second and no finer: a line is written a moment after it was said,
|
|
52
|
+
and the time is for reading a log back against a clock, not for timing
|
|
53
|
+
anything -- erasing, programming and reading say how long they took.
|
|
54
|
+
"""
|
|
55
|
+
return f"{at:%H:%M:%S} {line}"
|
|
56
|
+
|
|
57
|
+
|
|
46
58
|
class EventLog(QObject):
|
|
47
59
|
"""The one way in. What listens is somebody else's business."""
|
|
48
60
|
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
"""Put unhandled exceptions in the Event Log instead of nowhere.
|
|
4
4
|
|
|
5
5
|
pyflashgui is started with ``pythonw.exe``, which has no console. An exception
|
|
6
|
-
raised inside a Qt slot
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
raised inside a Qt slot is printed by PySide to standard error, which pythonw
|
|
7
|
+
does not have: the slot does not finish, and the window carries on looking
|
|
8
|
+
perfectly healthy.
|
|
9
9
|
|
|
10
10
|
A stale call to a widget method -- ``.text()`` on what had become a combo box
|
|
11
11
|
-- is enough: nothing is logged, no dialog appears, and whatever the slot was
|
|
@@ -27,10 +27,9 @@ from PySide6.QtCore import QObject, Qt, QtMsgType, Signal, Slot, qInstallMessage
|
|
|
27
27
|
|
|
28
28
|
from pyflashgui.core.events import ERROR, INFORMATION
|
|
29
29
|
|
|
30
|
-
#: Said of a Python traceback and of nothing else. Qt
|
|
31
|
-
#: same handler
|
|
32
|
-
#:
|
|
33
|
-
#: this application is both wrong and unhelpful.
|
|
30
|
+
#: Said of a Python traceback and of nothing else. Qt reports through the
|
|
31
|
+
#: same handler what other parts of the system say -- a platform plugin, a
|
|
32
|
+
#: window manager -- and those are not faults in this application.
|
|
34
33
|
BUG = "Unhandled error (this is a bug in pyflashgui; the action it interrupted did not finish):"
|
|
35
34
|
|
|
36
35
|
|
|
@@ -16,7 +16,10 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import threading
|
|
18
18
|
import time
|
|
19
|
+
from collections import deque
|
|
19
20
|
from collections.abc import Callable, Sequence
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from datetime import datetime
|
|
20
23
|
from typing import Any
|
|
21
24
|
|
|
22
25
|
from PySide6.QtCore import QObject, Signal
|
|
@@ -47,7 +50,7 @@ RESET_SAID = {
|
|
|
47
50
|
RESET_CORE: "by restarting its processor only",
|
|
48
51
|
}
|
|
49
52
|
#: How long letting go of the probe is waited for when the window closes, in
|
|
50
|
-
#: seconds: a
|
|
53
|
+
#: seconds: a probe that does not answer must not keep the window waiting.
|
|
51
54
|
CLOSE_S = 5.0
|
|
52
55
|
|
|
53
56
|
|
|
@@ -59,6 +62,30 @@ def _close_quietly(connection) -> None:
|
|
|
59
62
|
pass
|
|
60
63
|
|
|
61
64
|
|
|
65
|
+
#: How many operations are remembered for Help > Diagnostics.
|
|
66
|
+
REMEMBERED = 20
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class Operation:
|
|
71
|
+
"""One thing done, or asked for, for Help > Diagnostics to list in order."""
|
|
72
|
+
|
|
73
|
+
#: When it was asked for, as a time of day.
|
|
74
|
+
when: str
|
|
75
|
+
name: str
|
|
76
|
+
#: done, failed, cancelled, or not done as asked -- a verify that found
|
|
77
|
+
#: differences, which is not a failure of the probe.
|
|
78
|
+
outcome: str
|
|
79
|
+
#: How long it took, in seconds.
|
|
80
|
+
took: float
|
|
81
|
+
#: What it failed with, where it did, as the Event Log said it.
|
|
82
|
+
said: str = ""
|
|
83
|
+
|
|
84
|
+
def line(self) -> str:
|
|
85
|
+
why = f": {self.said.splitlines()[0]}" if self.said else ""
|
|
86
|
+
return f"{self.when} {self.name}: {self.outcome}{why}, {self.took:.1f} s"
|
|
87
|
+
|
|
88
|
+
|
|
62
89
|
#: Progress is passed on when it has moved this far, and not otherwise: a
|
|
63
90
|
#: library reporting every page would otherwise queue a thousand signals to
|
|
64
91
|
#: move a bar a thousand times by less than a pixel.
|
|
@@ -128,6 +155,13 @@ class Flasher(QObject):
|
|
|
128
155
|
self.regions: list[Region] = []
|
|
129
156
|
self._regions_for = ""
|
|
130
157
|
self.busy = False
|
|
158
|
+
#: What is being done while busy: "Programming". Empty otherwise.
|
|
159
|
+
self.doing = ""
|
|
160
|
+
#: The last few operations, oldest first, for Help > Diagnostics: the
|
|
161
|
+
#: order things happened in is most of what a fault report needs.
|
|
162
|
+
self.history: deque[Operation] = deque(maxlen=REMEMBERED)
|
|
163
|
+
#: The probes found when they were last looked for.
|
|
164
|
+
self.probes_found: list = []
|
|
131
165
|
self._cancel = threading.Event()
|
|
132
166
|
self._worker = Worker(self)
|
|
133
167
|
self._last_progress = -1.0
|
|
@@ -156,14 +190,19 @@ class Flasher(QObject):
|
|
|
156
190
|
if self.busy:
|
|
157
191
|
self.events.warning(f"{name} was not started: something else is still running.")
|
|
158
192
|
return False
|
|
193
|
+
self.doing = name
|
|
159
194
|
self._set_busy(True)
|
|
160
195
|
self._cancel.clear()
|
|
161
196
|
self._last_progress = -1.0
|
|
197
|
+
asked, started = f"{datetime.now():%H:%M:%S}", time.monotonic()
|
|
198
|
+
|
|
199
|
+
def remember(outcome: str, said: str = "") -> None:
|
|
200
|
+
took = time.monotonic() - started
|
|
201
|
+
self.history.append(Operation(asked, name, outcome, took, said))
|
|
162
202
|
|
|
163
203
|
def job() -> tuple[Any, str | None, bool]:
|
|
164
204
|
# A backend's own error is a sentence already and is passed on as
|
|
165
|
-
# one. Anything else
|
|
166
|
-
# and says what kind it was.
|
|
205
|
+
# one. Anything else was not expected, and says what kind it was.
|
|
167
206
|
try:
|
|
168
207
|
return work(), None, False
|
|
169
208
|
except CancelledError as exc:
|
|
@@ -176,13 +215,16 @@ class Flasher(QObject):
|
|
|
176
215
|
if error is None:
|
|
177
216
|
result, error, cancelled = result
|
|
178
217
|
if not quiet:
|
|
218
|
+
self.doing = ""
|
|
179
219
|
self._set_busy(False)
|
|
180
220
|
if error is not None:
|
|
221
|
+
remember("cancelled" if cancelled else "failed", error)
|
|
181
222
|
(self.events.warning if cancelled else self.events.error)(error)
|
|
182
223
|
if not quiet:
|
|
183
224
|
self.finished.emit(name, False)
|
|
184
225
|
return
|
|
185
226
|
ok = done(result)
|
|
227
|
+
remember("done" if ok is not False else "not done as asked")
|
|
186
228
|
if not quiet:
|
|
187
229
|
self.finished.emit(name, ok is not False)
|
|
188
230
|
|
|
@@ -243,6 +285,7 @@ class Flasher(QObject):
|
|
|
243
285
|
|
|
244
286
|
def done(result: tuple[list, str]) -> None:
|
|
245
287
|
probes, error = result
|
|
288
|
+
self.probes_found = list(probes)
|
|
246
289
|
if error:
|
|
247
290
|
self.events.warning(error)
|
|
248
291
|
self.probes_changed.emit(probes)
|