pyflashgui 0.2.0__tar.gz → 0.2.1__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 (122) hide show
  1. {pyflashgui-0.2.0/pyflashgui.egg-info → pyflashgui-0.2.1}/PKG-INFO +3 -2
  2. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/PYPI.md +1 -0
  3. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/__init__.py +1 -1
  4. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/__main__.py +3 -2
  5. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/backends/base.py +10 -0
  6. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/backends/jlink_backend.py +29 -6
  7. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/backends/pyocd_backend.py +58 -20
  8. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/backends/pyocd_packs.py +20 -5
  9. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/excepthook.py +6 -7
  10. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/flasher.py +46 -3
  11. pyflashgui-0.2.1/pyflashgui/core/library_log.py +96 -0
  12. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/memory_map.py +2 -2
  13. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/worker.py +1 -2
  14. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/about.md +26 -2
  15. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/changelog.md +28 -5
  16. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/manual.md +1 -1
  17. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/memory.md +2 -2
  18. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/probe.md +2 -2
  19. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/confirm.py +8 -0
  20. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/crop_dialog.py +2 -2
  21. pyflashgui-0.2.1/pyflashgui/ui/diagnostics.py +264 -0
  22. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/fonts.py +1 -1
  23. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/help_menu.py +33 -13
  24. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/main_window.py +13 -1
  25. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/packs_dialog.py +17 -3
  26. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/probe_bar.py +2 -2
  27. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/theme.py +1 -1
  28. {pyflashgui-0.2.0 → pyflashgui-0.2.1/pyflashgui.egg-info}/PKG-INFO +3 -2
  29. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui.egg-info/SOURCES.txt +3 -0
  30. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui.egg-info/requires.txt +1 -1
  31. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyproject.toml +5 -2
  32. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_backends.py +33 -2
  33. pyflashgui-0.2.1/tests/test_diagnostics.py +174 -0
  34. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_jlink_backend.py +1 -1
  35. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_packs.py +2 -0
  36. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_read_back.py +1 -1
  37. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/LICENSE +0 -0
  38. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/NOTICE +0 -0
  39. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/README.md +0 -0
  40. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/backends/__init__.py +0 -0
  41. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/backends/simulated.py +0 -0
  42. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/__init__.py +0 -0
  43. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/checkout.py +0 -0
  44. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/compare.py +0 -0
  45. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/context.py +0 -0
  46. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/events.py +0 -0
  47. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/images.py +0 -0
  48. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/installation.py +0 -0
  49. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/layout.py +0 -0
  50. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/pack_manager.py +0 -0
  51. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/packages.py +0 -0
  52. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/paths.py +0 -0
  53. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/settings.py +0 -0
  54. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/shortcut.py +0 -0
  55. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/slow_start.py +0 -0
  56. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/timing.py +0 -0
  57. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/updates.py +0 -0
  58. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/core/workspaces.py +0 -0
  59. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/__init__.py +0 -0
  60. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/console.md +0 -0
  61. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/files.md +0 -0
  62. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/main-window.png +0 -0
  63. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/help/memory-map.md +0 -0
  64. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/resources/__init__.py +0 -0
  65. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/resources/pyflashgui.ico +0 -0
  66. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/resources/pyflashgui.png +0 -0
  67. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/resources/pyflashgui.svg +0 -0
  68. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/resources/pyflashgui_small.svg +0 -0
  69. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/resources/tick.svg +0 -0
  70. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/__init__.py +0 -0
  71. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/buttons.py +0 -0
  72. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/console_view.py +0 -0
  73. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/detached.py +0 -0
  74. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/erase_dialog.py +0 -0
  75. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/event_colours.py +0 -0
  76. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/files_view.py +0 -0
  77. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/folders.py +0 -0
  78. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/map_view.py +0 -0
  79. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/memory_view.py +0 -0
  80. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/messages.py +0 -0
  81. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/pane_bar.py +0 -0
  82. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/panes.py +0 -0
  83. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/persist.py +0 -0
  84. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/read_dialog.py +0 -0
  85. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/scripting.py +0 -0
  86. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/scripts_menu.py +0 -0
  87. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/session.py +0 -0
  88. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui/ui/workspace_menu.py +0 -0
  89. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui.egg-info/dependency_links.txt +0 -0
  90. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui.egg-info/entry_points.txt +0 -0
  91. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/pyflashgui.egg-info/top_level.txt +0 -0
  92. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/setup.cfg +0 -0
  93. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_build_version.py +0 -0
  94. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_changelog.py +0 -0
  95. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_checkout.py +0 -0
  96. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_compare.py +0 -0
  97. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_console.py +0 -0
  98. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_erase.py +0 -0
  99. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_events.py +0 -0
  100. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_excepthook.py +0 -0
  101. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_flash_panes.py +0 -0
  102. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_flasher.py +0 -0
  103. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_folders.py +0 -0
  104. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_fonts.py +0 -0
  105. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_headers.py +0 -0
  106. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_help.py +0 -0
  107. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_icon.py +0 -0
  108. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_images.py +0 -0
  109. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_installation.py +0 -0
  110. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_launcher.py +0 -0
  111. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_memory_map.py +0 -0
  112. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_messages.py +0 -0
  113. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_notice.py +0 -0
  114. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_packages.py +0 -0
  115. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_packaging.py +0 -0
  116. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_persist.py +0 -0
  117. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_readme.py +0 -0
  118. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_scripts_menu.py +0 -0
  119. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_shortcut.py +0 -0
  120. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_slow_start.py +0 -0
  121. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/tests/test_theme.py +0 -0
  122. {pyflashgui-0.2.0 → pyflashgui-0.2.1}/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.1
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,7 +29,7 @@ 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"
@@ -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
 
@@ -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.1"
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
@@ -248,9 +248,8 @@ def read_pack(path: Path) -> tuple[Pack, tuple[tuple[str, str], ...]]:
248
248
  from xml.etree import ElementTree
249
249
 
250
250
  # 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.
251
+ # here: its modules are written to be imported from the top, and the
252
+ # backend may be importing it on another thread at the same moment.
254
253
  import pyocd.target # noqa: F401
255
254
  from pyocd.target.pack.cmsis_pack import CmsisPack
256
255
 
@@ -263,8 +262,8 @@ def read_pack(path: Path) -> tuple[Pack, tuple[tuple[str, str], ...]]:
263
262
  f"{path.name} is not a CMSIS-Pack: there is no description (.pdsc) in it."
264
263
  )
265
264
  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.
265
+ # Read through this archive, which is closed after, so the file can
266
+ # be removed again: Windows will not delete an open file.
268
267
  found = {
269
268
  (d.part_number, (d.vendor or "").split(":")[0]) for d in CmsisPack(archive).devices
270
269
  }
@@ -314,6 +313,22 @@ def file_devices() -> list[Device]:
314
313
  return out
315
314
 
316
315
 
316
+ def report() -> list[str]:
317
+ """For Help > Diagnostics: where the packs are, which there are, and how
318
+ old the list of devices is. Found from the files, without reading the
319
+ list itself, which is twenty megabytes."""
320
+ where = folder()
321
+ fetched = list_fetched()
322
+ lines = [
323
+ f"folder: {where}",
324
+ f"list of devices fetched {fetched:%Y-%m-%d %H:%M}" if fetched else "list never fetched",
325
+ ]
326
+ downloaded = sorted(p.relative_to(where).as_posix() for p in where.rglob("*.pack"))
327
+ lines += [f"installed: {name}" for name in downloaded] or ["none installed"]
328
+ lines += [f"added from a file: {p.name}" for p in sorted(files_folder().glob("*.pack"))]
329
+ return lines
330
+
331
+
317
332
  def same_name(part: str) -> str:
318
333
  """A device's name as pyOCD files it, whichever way it is spelt.
319
334
 
@@ -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)
@@ -0,0 +1,96 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 davhodg
3
+ """What pyOCD and pylink say for themselves, kept for Help > Diagnostics.
4
+
5
+ Both write to Python's logging rather than raising everything they have to
6
+ say: a probe that did not answer on disconnecting, a flash routine that
7
+ reported a fault, the parts of the debug port found on connecting. With no
8
+ handler of their own those lines went to a console pyflashgui started with
9
+ pythonw does not have, and were lost.
10
+
11
+ So the last of them are kept, from information up, for the diagnostics
12
+ report -- which is where the question "what did pyOCD make of this target?"
13
+ is answered -- and errors are passed to the Event Log as they happen.
14
+ Warnings are only kept: pyOCD gives several on an ordinary connection to
15
+ some targets, and an Event Log that opens itself for those would soon be
16
+ closed and not opened again.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ from collections import deque
23
+ from datetime import datetime
24
+
25
+ from pyflashgui.core.events import EventLog
26
+
27
+ #: The loggers listened to, and what each library is called when it is named.
28
+ LIBRARIES = {"pyocd": "pyOCD", "pylink": "pylink"}
29
+ #: How many lines are kept for the report.
30
+ KEPT = 100
31
+
32
+
33
+ def library_of(logger_name: str) -> str:
34
+ """The library a logger belongs to, as it is written: pyocd.coresight.ap is pyOCD."""
35
+ return LIBRARIES.get(logger_name.split(".")[0], logger_name)
36
+
37
+
38
+ def text_of(record: logging.LogRecord) -> str:
39
+ """The line a record says, with the exception it carries where it adds anything."""
40
+ try:
41
+ text = record.getMessage()
42
+ except Exception: # a library's own mistake in its format string
43
+ text = str(record.msg)
44
+ if record.exc_info and (exc := record.exc_info[1]) is not None:
45
+ said = str(exc).strip() or type(exc).__name__
46
+ if said not in text:
47
+ text = f"{text.rstrip(':')}: {said}" if text else said
48
+ return text.strip()
49
+
50
+
51
+ class LibraryLog(logging.Handler):
52
+ """Keeps what the libraries said, and passes their errors to the Event Log.
53
+
54
+ Called on whichever thread the library was on -- usually the worker
55
+ talking to the probe -- which is safe for both: a deque appends from any
56
+ thread, and the Event Log crosses to the window's thread itself.
57
+ """
58
+
59
+ def __init__(self, events: EventLog, kept: int = KEPT) -> None:
60
+ super().__init__(logging.INFO)
61
+ self.events = events
62
+ #: (time, level, library, text), oldest first.
63
+ self.recent: deque[tuple[str, str, str, str]] = deque(maxlen=kept)
64
+
65
+ def emit(self, record: logging.LogRecord) -> None:
66
+ try:
67
+ library, text = library_of(record.name), text_of(record)
68
+ self.recent.append(
69
+ (f"{datetime.now():%H:%M:%S}", record.levelname.lower(), library, text)
70
+ )
71
+ if record.levelno >= logging.ERROR:
72
+ self.events.error(f"{library}: {text}")
73
+ except Exception:
74
+ # Nothing a library says may stop it doing what it was doing, and
75
+ # a handler that fails would be reported through logging again.
76
+ pass
77
+
78
+ def lines(self) -> list[str]:
79
+ return [f"{when} {library} {level}: {text}" for when, level, library, text in self.recent]
80
+
81
+ def install(self) -> None:
82
+ """Listen to the libraries, from information up.
83
+
84
+ Their loggers are left at information once set there: one handler
85
+ removed and another added, as when a workspace is opened in a new
86
+ window, must not leave the next listening to less.
87
+ """
88
+ for name in LIBRARIES:
89
+ logger = logging.getLogger(name)
90
+ if logger.getEffectiveLevel() > logging.INFO:
91
+ logger.setLevel(logging.INFO)
92
+ logger.addHandler(self)
93
+
94
+ def remove(self) -> None:
95
+ for name in LIBRARIES:
96
+ logging.getLogger(name).removeHandler(self)
@@ -46,8 +46,8 @@ class Region:
46
46
  #: show the same flash at several addresses and take writes at only one.
47
47
  alias_of: str = ""
48
48
  #: What a byte of this flash reads as once erased, as far as the backend
49
- #: knows: FF for most devices, 00 for some. Not to be relied on -- pyOCD
50
- #: says FF for an XMC4500, whose erased flash reads 00.
49
+ #: knows: FF for most devices, 00 for some, an XMC4500 among them. FF
50
+ #: where nothing says, so a Memory pane lets it be chosen as well.
51
51
  erased: int = 0xFF
52
52
 
53
53
  @property
@@ -56,8 +56,7 @@ class Worker(QThread):
56
56
 
57
57
  The ``done`` connection is dropped first: a queued result arriving
58
58
  after the owner has been torn down would be delivered to a dead
59
- object, which Qt punishes with an access violation rather than an
60
- exception.
59
+ object, which ends in an access violation rather than an exception.
61
60
  """
62
61
  if self._delivering:
63
62
  self._delivering = False
@@ -4,7 +4,31 @@
4
4
 
5
5
  **Help > Documentation** opens this manual. It ships inside pyflashgui; *Back* and *Contents* get you around it.
6
6
 
7
- **Help > Diagnostics...** writes a report of the versions of pyflashgui, Python and every package it runs on, and any panes out on their own, to the Event Log and copies it to the clipboard -- ready to paste into a bug report.
7
+ **Help > Diagnostics...** opens a report in a window of its own, and copies it to the
8
+ clipboard as it opens -- ready to paste into a bug report. **Refresh** makes it again,
9
+ **Copy** copies it again and **Save...** writes it to a file. It gives:
10
+
11
+ - the versions of pyflashgui, Python and every package it runs on;
12
+ - **the connection**: what is chosen under Using, Probe, Target and Speed, what is
13
+ connected, and whether anything is being done; through pyOCD, the options pyOCD was
14
+ given, whether the target is built in, from a pack or the generic Cortex-M, its cores and
15
+ the probe's wire protocol; through J-Link Commander, how Commander is run;
16
+ - the probes found when they were last looked for;
17
+ - the target's memory as pyflashgui has it, and what the memory map says of the files
18
+ ticked;
19
+ - each entry in the Files pane, and the Memory panes open;
20
+ - **the last twenty operations**, in order -- connecting, erasing, programming, reading,
21
+ resetting -- each with how it ended and how long it took;
22
+ - **what pyOCD and pylink said for themselves**: their last hundred lines, from information
23
+ up. Their errors are also put in the Event Log as they happen;
24
+ - the last twenty warnings and errors from the Event Log;
25
+ - J-Link Commander's location and every J-Link software folder found, and pyOCD's packs;
26
+ - the workspace, any confirmation not being asked, and the threads running;
27
+ - the panes out on their own, and how long starting took.
28
+
29
+ A part of it that cannot be read says so and the rest is still given. It names the probes'
30
+ serial numbers, the workspace folder and the files in the Files pane: remove any of that
31
+ from a bug report if you would rather.
8
32
 
9
33
  The report names the version, and -- when pyflashgui is being run from a source checkout rather than an installed build -- the branch or tag and the commit it is on, read from `.git` without running git. Between two releases every build calls itself the same version, so that line is what tells one pull from another. It does not say whether any file has been edited since: that needs git itself, and the launcher runs without a console for it to use.
10
34
 
@@ -16,6 +40,6 @@ The report names the version, and -- when pyflashgui is being run from a source
16
40
 
17
41
  ## If pyflashgui will not start
18
42
 
19
- Started from the source folder, `pyflashgui.cmd` and `pyflashgui.sh` install anything missing before they start pyflashgui, and a library that is still missing is reported in a window rather than as nothing happening. The entry made by *Tools > Settings > Add shortcut to Start menu* (*Add shortcut to applications menu* on Linux), or by *Add shortcut to desktop* on Windows, starts the launcher too, so starting from it after a pull does the same. Installed with pip, the same two make a shortcut that starts pyflashgui with the Python it is installed in. The first start says where they are in the Event Log; the launchers ask nothing. On Linux, a library Qt needs to draw the window is named, with the package it comes in, before Qt is started: Qt itself says only that it could not load the "xcb" plugin. `python -m pyflashgui --selftest` imports everything a build is most likely to be missing, checks that pyOCD can find its probe drivers and targets, and exits with 0 if it is all there, which is how the build checks itself.
43
+ Started from the source folder, `pyflashgui.cmd` and `pyflashgui.sh` install anything missing before they start pyflashgui, and a library that is still missing is reported in a window rather than as nothing happening. The entry made by *Tools > Settings > Add shortcut to Start menu* (*Add shortcut to applications menu* on Linux), or by *Add shortcut to desktop* on Windows, starts the launcher too, so starting from it after a pull does the same. Installed with pip, the same two make a shortcut that starts pyflashgui with the Python it is installed in. The first start says where they are in the Event Log; the launchers ask nothing. On Linux, a library Qt needs to draw the window is looked for before Qt is started, and one that is missing is named with the package it comes in. `python -m pyflashgui --selftest` imports everything a build is most likely to be missing, checks that pyOCD can find its probe drivers and targets, and exits with 0 if it is all there, which is how the build checks itself.
20
44
 
21
45
  **If pyflashgui is slow to start**, the diagnostics report says where the seconds went: starting is measured on every run, step by step and package by package, so the breakdown is already there when somebody thinks to ask. A start that takes longer than usual says so in the Event Log and points to *Help > Diagnostics*. The first start after an update is the usual one: Python compiles every changed file again, and that start says how many it compiled. To have the whole report in the Event Log after every start, and in `startup-timing.txt` in pyflashgui's own folder, start pyflashgui with `--timing` -- `pyflashgui.cmd --timing`, or `pyflashgui.sh --timing` -- or set `PYFLASHGUI_TIMING=1`.