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.
Files changed (126) hide show
  1. {pyflashgui-0.2.0/pyflashgui.egg-info → pyflashgui-0.2.2}/PKG-INFO +4 -3
  2. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/PYPI.md +1 -0
  3. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/README.md +6 -0
  4. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/__init__.py +1 -1
  5. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/__main__.py +3 -2
  6. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/base.py +10 -0
  7. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/jlink_backend.py +29 -6
  8. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/pyocd_backend.py +58 -20
  9. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/pyocd_packs.py +47 -10
  10. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/events.py +12 -0
  11. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/excepthook.py +6 -7
  12. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/flasher.py +46 -3
  13. pyflashgui-0.2.2/pyflashgui/core/library_log.py +96 -0
  14. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/memory_map.py +2 -2
  15. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/pack_manager.py +9 -1
  16. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/worker.py +1 -2
  17. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/about.md +26 -2
  18. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/changelog.md +55 -5
  19. pyflashgui-0.2.2/pyflashgui/help/main-window.png +0 -0
  20. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/manual.md +10 -2
  21. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/memory.md +2 -2
  22. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/probe.md +3 -3
  23. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/confirm.py +8 -0
  24. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/crop_dialog.py +2 -2
  25. pyflashgui-0.2.2/pyflashgui/ui/diagnostics.py +264 -0
  26. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/fonts.py +1 -1
  27. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/help_menu.py +39 -15
  28. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/main_window.py +45 -3
  29. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/packs_dialog.py +17 -3
  30. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/probe_bar.py +2 -2
  31. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/theme.py +59 -5
  32. {pyflashgui-0.2.0 → pyflashgui-0.2.2/pyflashgui.egg-info}/PKG-INFO +4 -3
  33. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/SOURCES.txt +6 -0
  34. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/requires.txt +2 -2
  35. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyproject.toml +6 -3
  36. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_backends.py +33 -2
  37. pyflashgui-0.2.2/tests/test_diagnostics.py +174 -0
  38. pyflashgui-0.2.2/tests/test_docking.py +713 -0
  39. pyflashgui-0.2.2/tests/test_event_log_times.py +72 -0
  40. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_flash_panes.py +17 -2
  41. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_help.py +7 -0
  42. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_jlink_backend.py +1 -1
  43. pyflashgui-0.2.2/tests/test_layout.py +166 -0
  44. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_packs.py +37 -4
  45. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_read_back.py +1 -1
  46. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_theme.py +46 -2
  47. pyflashgui-0.2.0/pyflashgui/help/main-window.png +0 -0
  48. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/LICENSE +0 -0
  49. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/NOTICE +0 -0
  50. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/__init__.py +0 -0
  51. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/backends/simulated.py +0 -0
  52. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/__init__.py +0 -0
  53. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/checkout.py +0 -0
  54. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/compare.py +0 -0
  55. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/context.py +0 -0
  56. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/images.py +0 -0
  57. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/installation.py +0 -0
  58. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/layout.py +0 -0
  59. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/packages.py +0 -0
  60. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/paths.py +0 -0
  61. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/settings.py +0 -0
  62. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/shortcut.py +0 -0
  63. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/slow_start.py +0 -0
  64. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/timing.py +0 -0
  65. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/updates.py +0 -0
  66. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/core/workspaces.py +0 -0
  67. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/__init__.py +0 -0
  68. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/console.md +0 -0
  69. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/files.md +0 -0
  70. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/help/memory-map.md +0 -0
  71. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/__init__.py +0 -0
  72. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.ico +0 -0
  73. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.png +0 -0
  74. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui.svg +0 -0
  75. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/pyflashgui_small.svg +0 -0
  76. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/resources/tick.svg +0 -0
  77. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/__init__.py +0 -0
  78. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/buttons.py +0 -0
  79. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/console_view.py +0 -0
  80. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/detached.py +0 -0
  81. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/erase_dialog.py +0 -0
  82. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/event_colours.py +0 -0
  83. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/files_view.py +0 -0
  84. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/folders.py +0 -0
  85. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/map_view.py +0 -0
  86. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/memory_view.py +0 -0
  87. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/messages.py +0 -0
  88. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/pane_bar.py +0 -0
  89. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/panes.py +0 -0
  90. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/persist.py +0 -0
  91. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/read_dialog.py +0 -0
  92. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/scripting.py +0 -0
  93. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/scripts_menu.py +0 -0
  94. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/session.py +0 -0
  95. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui/ui/workspace_menu.py +0 -0
  96. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/dependency_links.txt +0 -0
  97. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/entry_points.txt +0 -0
  98. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/pyflashgui.egg-info/top_level.txt +0 -0
  99. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/setup.cfg +0 -0
  100. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_build_version.py +0 -0
  101. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_changelog.py +0 -0
  102. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_checkout.py +0 -0
  103. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_compare.py +0 -0
  104. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_console.py +0 -0
  105. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_erase.py +0 -0
  106. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_events.py +0 -0
  107. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_excepthook.py +0 -0
  108. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_flasher.py +0 -0
  109. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_folders.py +0 -0
  110. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_fonts.py +0 -0
  111. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_headers.py +0 -0
  112. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_icon.py +0 -0
  113. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_images.py +0 -0
  114. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_installation.py +0 -0
  115. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_launcher.py +0 -0
  116. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_memory_map.py +0 -0
  117. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_messages.py +0 -0
  118. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_notice.py +0 -0
  119. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_packages.py +0 -0
  120. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_packaging.py +0 -0
  121. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_persist.py +0 -0
  122. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_readme.py +0 -0
  123. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_scripts_menu.py +0 -0
  124. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_shortcut.py +0 -0
  125. {pyflashgui-0.2.0 → pyflashgui-0.2.2}/tests/test_slow_start.py +0 -0
  126. {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.0
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.45
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.16.*; extra == "dev"
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.
@@ -2,5 +2,5 @@
2
2
  # SPDX-FileCopyrightText: 2026 davhodg
3
3
  """pyflashgui — a graphical flash programming tool."""
4
4
 
5
- __version__ = "0.2.0"
5
+ __version__ = "0.2.2"
6
6
  APP_NAME = "pyflashgui" # single place to change the display name
@@ -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, and says nothing about why.
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" and does not say which.
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 has been seen to be 0 with it.
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 SEGGER's software would set the
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 it gone.
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("J-Link Commander resets each device its own way, and no other.")
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
- # A Commander that will not write the list still programs a
585
- # device whose name is typed in.
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 slowest thing
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, whose firmware loses track when
94
- several commands are outstanding. Set for every one of them, since it
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
- pyOCD's close lets go of the target before the probe, and when the
148
- first fails -- the board's power was turned off -- never closes the
149
- probe. A J-Link left open that way stays open: Find probes lists it, and
150
- every connect says "No emulator with serial number".
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
- pyOCD stops the processor wherever it happens to be and runs its
281
- routine there. Reported on an XMC4500 attached to its firmware as it
282
- ran: stopped amid the firmware, with its interrupts and the rest of
283
- what it had set up still on, the erase routine ended in a HardFault
284
- (IPSR=3). Reset first, the routine runs on a device as it starts,
285
- which is what flash routines are written for, and what SEGGER's own
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. Filled instead with what pyOCD takes
332
- # erased flash to read as, it would be written to: pyOCD
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, and slow for anything else.
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
- def open_cache(tick: Tick | None = None) -> Any:
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
- cache = open_cache(tick)
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
- count = len(open_cache().index)
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. Started part way in, pyOCD can import itself in a circle, which
252
- # it does not survive; and the backend may be importing it on another
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: one pyOCD opened
267
- # itself would stay open, and Windows will not delete an open file.
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 does not propagate anywhere useful: PySide prints the
7
- traceback to a standard error stream that does not exist, the slot simply
8
- does not finish, and the window carries on looking perfectly healthy.
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 complains through the
31
- #: same handler about things that are often not pyflashgui's doing at all -- a
32
- #: platform plugin, a window manager -- and telling somebody those are bugs in
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 library that never comes back must not keep the window frozen.
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 is a fault in pyflashgui or in a library,
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)