pyflashgui 0.1.0__py3-none-any.whl

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 (68) hide show
  1. pyflashgui/__init__.py +6 -0
  2. pyflashgui/__main__.py +334 -0
  3. pyflashgui/backends/__init__.py +40 -0
  4. pyflashgui/backends/base.py +272 -0
  5. pyflashgui/backends/jlink_backend.py +596 -0
  6. pyflashgui/backends/pyocd_backend.py +271 -0
  7. pyflashgui/backends/simulated.py +240 -0
  8. pyflashgui/core/__init__.py +2 -0
  9. pyflashgui/core/checkout.py +134 -0
  10. pyflashgui/core/compare.py +157 -0
  11. pyflashgui/core/context.py +37 -0
  12. pyflashgui/core/events.py +71 -0
  13. pyflashgui/core/excepthook.py +110 -0
  14. pyflashgui/core/flasher.py +419 -0
  15. pyflashgui/core/images.py +279 -0
  16. pyflashgui/core/installation.py +137 -0
  17. pyflashgui/core/layout.py +72 -0
  18. pyflashgui/core/memory_map.py +234 -0
  19. pyflashgui/core/packages.py +104 -0
  20. pyflashgui/core/paths.py +49 -0
  21. pyflashgui/core/settings.py +44 -0
  22. pyflashgui/core/shortcut.py +154 -0
  23. pyflashgui/core/slow_start.py +101 -0
  24. pyflashgui/core/timing.py +312 -0
  25. pyflashgui/core/updates.py +195 -0
  26. pyflashgui/core/worker.py +71 -0
  27. pyflashgui/core/workspaces.py +250 -0
  28. pyflashgui/help/__init__.py +129 -0
  29. pyflashgui/help/about.md +21 -0
  30. pyflashgui/help/changelog.md +46 -0
  31. pyflashgui/help/files.md +60 -0
  32. pyflashgui/help/main-window.png +0 -0
  33. pyflashgui/help/manual.md +44 -0
  34. pyflashgui/help/memory-map.md +28 -0
  35. pyflashgui/help/memory.md +59 -0
  36. pyflashgui/help/probe.md +71 -0
  37. pyflashgui/resources/__init__.py +10 -0
  38. pyflashgui/resources/pyflashgui.ico +0 -0
  39. pyflashgui/resources/pyflashgui.png +0 -0
  40. pyflashgui/resources/pyflashgui.svg +32 -0
  41. pyflashgui/resources/pyflashgui_small.svg +20 -0
  42. pyflashgui/ui/__init__.py +2 -0
  43. pyflashgui/ui/confirm.py +312 -0
  44. pyflashgui/ui/detached.py +101 -0
  45. pyflashgui/ui/erase_dialog.py +141 -0
  46. pyflashgui/ui/event_colours.py +66 -0
  47. pyflashgui/ui/files_view.py +716 -0
  48. pyflashgui/ui/folders.py +176 -0
  49. pyflashgui/ui/fonts.py +75 -0
  50. pyflashgui/ui/help_menu.py +587 -0
  51. pyflashgui/ui/main_window.py +754 -0
  52. pyflashgui/ui/map_view.py +215 -0
  53. pyflashgui/ui/memory_view.py +724 -0
  54. pyflashgui/ui/messages.py +78 -0
  55. pyflashgui/ui/pane_bar.py +92 -0
  56. pyflashgui/ui/panes.py +1068 -0
  57. pyflashgui/ui/persist.py +132 -0
  58. pyflashgui/ui/probe_bar.py +315 -0
  59. pyflashgui/ui/read_dialog.py +161 -0
  60. pyflashgui/ui/session.py +53 -0
  61. pyflashgui/ui/theme.py +85 -0
  62. pyflashgui-0.1.0.dist-info/METADATA +80 -0
  63. pyflashgui-0.1.0.dist-info/RECORD +68 -0
  64. pyflashgui-0.1.0.dist-info/WHEEL +5 -0
  65. pyflashgui-0.1.0.dist-info/entry_points.txt +2 -0
  66. pyflashgui-0.1.0.dist-info/licenses/LICENSE +202 -0
  67. pyflashgui-0.1.0.dist-info/licenses/NOTICE +30 -0
  68. pyflashgui-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,596 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # SPDX-FileCopyrightText: 2026 davhodg
3
+ """A J-Link, through SEGGER's own J-Link Commander.
4
+
5
+ Commander is the command line program that comes with SEGGER's J-Link
6
+ software. Driving it reaches every device that software knows -- far more
7
+ than Cortex-M -- at the price of needing a J-Link and SEGGER's software,
8
+ which the user installs and pyflashgui never carries.
9
+
10
+ It is run as a separate program, once for each thing asked of it, with a file
11
+ of commands to carry out. So there is no connection held open between one
12
+ operation and the next: connecting checks that the probe and the device
13
+ answer, and each operation after it attaches again by itself.
14
+
15
+ **Tried on one board so far**: listing the probe, connecting and reading
16
+ worked on an XMC4500 through its on-board J-Link. The rest is written from
17
+ SEGGER's documentation. How Commander's replies are read is the part most
18
+ likely to need putting right, and is kept in a few small functions for that
19
+ reason.
20
+
21
+ Commander says little while it works, so progress is the start and the end
22
+ and nothing between.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import os
28
+ import re
29
+ import shutil
30
+ import subprocess
31
+ import sys
32
+ import tempfile
33
+ from collections.abc import Sequence
34
+ from pathlib import Path
35
+
36
+ from pyflashgui.backends.base import (
37
+ ERASE_CHIP,
38
+ ERASE_SECTORS,
39
+ Backend,
40
+ BackendError,
41
+ Cancelled,
42
+ CancelledError,
43
+ Connection,
44
+ ProbeInfo,
45
+ Progress,
46
+ TargetInfo,
47
+ Verification,
48
+ _never,
49
+ _nothing,
50
+ )
51
+ from pyflashgui.core.images import Segment
52
+ from pyflashgui.core.memory_map import FLASH, RAM, Region
53
+
54
+ #: What Commander is called: JLink.exe on Windows, JLinkExe elsewhere.
55
+ PROGRAM = "JLink.exe" if sys.platform == "win32" else "JLinkExe"
56
+
57
+ #: The debug interfaces offered, the first being the one used when none is
58
+ #: chosen. As Commander's ``-If`` takes them.
59
+ INTERFACES = ("SWD", "JTAG", "cJTAG", "FINE")
60
+
61
+ #: How long each kind of run may take, in seconds, before it is given up on.
62
+ #: Generous: a large image on a slow interface takes minutes, and a run cut
63
+ #: short part way through programming is worse than one waited for.
64
+ QUICK_S = 30
65
+ LIST_S = 120
66
+ WORK_S = 900
67
+
68
+ NOT_FOUND = (
69
+ "J-Link Commander was not found. It comes with SEGGER's J-Link software: install that, "
70
+ "or say where it is with Tools > Settings > J-Link Commander location."
71
+ )
72
+
73
+ #: Said by Commander when the probe is there and the device does not answer.
74
+ #: Looked for as well as its exit code, which has been seen to be 0 with it.
75
+ NO_TARGET = "Cannot connect to target"
76
+
77
+ #: A J-Link script that does nothing where SEGGER's software would set the
78
+ #: device up after connecting to it. On some parts that set-up writes to
79
+ #: the device -- an S32K3's RAM is filled, for its error correction -- and
80
+ #: a read made to see what a running program left there finds it gone.
81
+ NO_SETUP_SCRIPT = "int SetupTarget(void) {\n return 0;\n}\n"
82
+
83
+ #: Where Commander is, when it has been said. None to go and look.
84
+ _chosen: list[str] | None = None
85
+
86
+
87
+ def set_commander(command: str | Sequence[str] | None) -> None:
88
+ """Use this Commander, or go back to looking for one.
89
+
90
+ A path, or a whole command line as a list -- which is how the tests stand
91
+ a script in for it.
92
+ """
93
+ global _chosen
94
+ if not command:
95
+ _chosen = None
96
+ elif isinstance(command, str):
97
+ _chosen = [command]
98
+ else:
99
+ _chosen = list(command)
100
+
101
+
102
+ def _version(folder: Path) -> tuple[int, int, str]:
103
+ """The version in a folder's name, for sorting: JLink_V812a is (8, 12, "a").
104
+
105
+ A folder with none sorts first, as the one SEGGER's installer keeps up
106
+ to date when it is told to replace what is there.
107
+ """
108
+ found = re.search(r"V(\d+)(\d\d)([a-z]?)", folder.name, re.I)
109
+ if found is None:
110
+ return (10**6, 0, "")
111
+ return (int(found.group(1)), int(found.group(2)), found.group(3).lower())
112
+
113
+
114
+ def _installed() -> list[Path]:
115
+ """The folders SEGGER's installer uses, newest version first."""
116
+ if sys.platform == "win32":
117
+ roots = [
118
+ Path(os.environ.get(name, default)) / "SEGGER"
119
+ for name, default in (
120
+ ("ProgramFiles", r"C:\Program Files"),
121
+ ("ProgramFiles(x86)", r"C:\Program Files (x86)"),
122
+ )
123
+ ]
124
+ else:
125
+ roots = [Path("/opt/SEGGER"), Path("/Applications/SEGGER")]
126
+ found: list[Path] = []
127
+ for root in roots:
128
+ if root.is_dir():
129
+ found += [p for p in root.glob("JLink*") if p.is_dir()]
130
+ return sorted(found, key=_version, reverse=True)
131
+
132
+
133
+ def find_commander() -> list[str] | None:
134
+ """The command that starts Commander, or None if there is none to be found."""
135
+ if _chosen is not None:
136
+ return list(_chosen)
137
+ # Where SEGGER installs it before anywhere on the PATH: Java's
138
+ # development kit has a program of the same name, and on a machine with
139
+ # both it is as likely to be the one the PATH finds.
140
+ for folder in _installed():
141
+ if (candidate := folder / PROGRAM).is_file():
142
+ return [str(candidate)]
143
+ if (on_path := shutil.which(PROGRAM)) is not None and _is_seggers(Path(on_path)):
144
+ return [on_path]
145
+ return None
146
+
147
+
148
+ def _is_seggers(program: Path) -> bool:
149
+ """Whether a program of Commander's name is Commander, and not Java's jlink.
150
+
151
+ SEGGER's sits beside the library it is a front for; Java's does not.
152
+ """
153
+ folder = program.resolve().parent
154
+ return any(folder.glob("JLink*.dll")) or any(folder.glob("libjlinkarm*"))
155
+
156
+
157
+ def _short(path: str) -> str:
158
+ """Windows' short name for a path, which has no spaces in it. The path
159
+ itself where there is none to be had."""
160
+ if sys.platform != "win32":
161
+ return path
162
+ try:
163
+ import ctypes
164
+
165
+ buffer = ctypes.create_unicode_buffer(1024)
166
+ got = ctypes.windll.kernel32.GetShortPathNameW(path, buffer, len(buffer))
167
+ return buffer.value if 0 < got < len(buffer) else path
168
+ except (AttributeError, OSError):
169
+ return path
170
+
171
+
172
+ def _named(path: Path | str) -> str:
173
+ """A path as a command in a command file can take it.
174
+
175
+ Commander is not sure to take a space in one, quoted or not. So a path
176
+ with a space in it, or a letter outside ASCII, is given by the short name
177
+ Windows keeps for it; and it goes in quotes regardless, which a J-Link
178
+ has been seen to accept.
179
+ """
180
+ text = str(path)
181
+ if " " in text or not text.isascii():
182
+ text = _short(text)
183
+ return f'"{text}"'
184
+
185
+
186
+ def _why(output: str) -> str:
187
+ """The lines of Commander's output that say what went wrong.
188
+
189
+ Its output is a transcript of everything it did, most of which went
190
+ well. The lines that name a failure are the ones worth showing; with
191
+ none that look like one, the last few are the best there is.
192
+ """
193
+ lines = [
194
+ line.strip()
195
+ for line in output.splitlines()
196
+ # Its own announcement that it is stopping says nothing about why.
197
+ if line.strip() and "will now exit" not in line.lower()
198
+ ]
199
+ telling = [
200
+ line
201
+ for line in lines
202
+ if re.search(r"error|fail|cannot|could not|unknown|not found|timeout", line, re.I)
203
+ ]
204
+ return " ".join((telling or lines)[-3:]) or "it said nothing about why"
205
+
206
+
207
+ def run(arguments: Sequence[str], commands: Sequence[str], timeout: float, doing: str) -> str:
208
+ """Run Commander once with these commands, and give back what it printed.
209
+
210
+ ``arguments`` go on its command line and ``commands`` in a file it is
211
+ pointed at, with Exit added so that it always comes back.
212
+ """
213
+ commander = find_commander()
214
+ if commander is None:
215
+ raise BackendError(NOT_FOUND)
216
+ with tempfile.TemporaryDirectory(prefix="pyflashgui-jlink-") as folder:
217
+ script = Path(folder) / "commands.jlink"
218
+ script.write_text("\n".join([*commands, "Exit"]) + "\n", encoding="utf-8")
219
+ command = [*commander, "-NoGui", "1", *arguments, "-CommandFile", str(script)]
220
+ try:
221
+ done = subprocess.run(
222
+ command,
223
+ capture_output=True,
224
+ text=True,
225
+ errors="replace",
226
+ timeout=timeout,
227
+ check=False,
228
+ stdin=subprocess.DEVNULL,
229
+ # No console of its own: pyflashgui has none, and Windows
230
+ # would otherwise open one for every run.
231
+ creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
232
+ )
233
+ except FileNotFoundError as exc:
234
+ raise BackendError(NOT_FOUND) from exc
235
+ except subprocess.TimeoutExpired as exc:
236
+ raise BackendError(
237
+ f"{doing} was given up on: J-Link Commander had not finished after "
238
+ f"{timeout:.0f} seconds."
239
+ ) from exc
240
+ except OSError as exc:
241
+ raise BackendError(f"{doing} failed: J-Link Commander would not start: {exc}") from exc
242
+ output = (done.stdout or "") + (done.stderr or "")
243
+ wanted_a_target = "-AutoConnect" in arguments
244
+ if done.returncode != 0 or (wanted_a_target and NO_TARGET.lower() in output.lower()):
245
+ raise BackendError(f"{doing} failed: {_why(output)}")
246
+ return output
247
+
248
+
249
+ # --- reading what Commander says ---------------------------------------------------------
250
+ #: One line of ShowEmuList for each J-Link:
251
+ #: ``J-Link[0]: Connection: USB, Serial number: 801012345, ProductName: J-Link PLUS``
252
+ _PROBE = re.compile(r"Serial number:\s*(\d+)(?:.*?ProductName:\s*([^,\r\n]+))?", re.I)
253
+
254
+ #: One line of the device list ExpDevList writes: the maker, the device and
255
+ #: its core, each in quotes, and then its memory in braces.
256
+ _DEVICE = re.compile(r'^\s*"([^"]*)"\s*,\s*"([^"]*)"\s*,\s*"([^"]*)"\s*,?(.*)$')
257
+ #: A stretch of memory inside the braces: where it starts and how long it is.
258
+ _AREA = re.compile(r"0x([0-9A-Fa-f]+)\s*,\s*0x([0-9A-Fa-f]+)")
259
+
260
+
261
+ def parse_probes(output: str) -> list[ProbeInfo]:
262
+ """The J-Links that ShowEmuList named, each once."""
263
+ probes: dict[str, ProbeInfo] = {}
264
+ for found in _PROBE.finditer(output):
265
+ serial, product = found.group(1), (found.group(2) or "J-Link").strip()
266
+ probes.setdefault(serial, ProbeInfo(serial, product, vendor="SEGGER", product=product))
267
+ return list(probes.values())
268
+
269
+
270
+ def _groups(text: str) -> list[str]:
271
+ """The outermost ``{...}`` groups of a line, braces inside them included."""
272
+ groups, depth, start = [], 0, 0
273
+ for index, char in enumerate(text):
274
+ if char == "{":
275
+ if depth == 0:
276
+ start = index
277
+ depth += 1
278
+ elif char == "}" and depth:
279
+ depth -= 1
280
+ if depth == 0:
281
+ groups.append(text[start : index + 1])
282
+ return groups
283
+
284
+
285
+ def _areas(group: str) -> list[tuple[int, int]]:
286
+ """The (start, length) of each stretch of memory in one group, empty ones left out."""
287
+ pairs = [(int(start, 16), int(length, 16)) for start, length in _AREA.findall(group)]
288
+ return [pair for pair in pairs if pair[1] > 0]
289
+
290
+
291
+ class Device:
292
+ """One device Commander knows: who makes it, its core and its memory."""
293
+
294
+ def __init__(
295
+ self,
296
+ maker: str,
297
+ name: str,
298
+ core: str,
299
+ flash: list[tuple[int, int]],
300
+ ram: list[tuple[int, int]],
301
+ ) -> None:
302
+ self.maker, self.name, self.core = maker, name, core
303
+ self.flash, self.ram = flash, ram
304
+
305
+ def regions(self) -> list[Region]:
306
+ """Its memory as pyflashgui's regions, lowest address first.
307
+
308
+ Numbered, since SEGGER's list gives the areas no names. Nothing is
309
+ known of how the flash is divided into sectors, so that is left as
310
+ not known rather than made up.
311
+ """
312
+ made = [
313
+ Region(f"Flash {index}", start, length, FLASH)
314
+ for index, (start, length) in enumerate(self.flash)
315
+ ]
316
+ made += [
317
+ Region(f"RAM {index}", start, length, RAM)
318
+ for index, (start, length) in enumerate(self.ram)
319
+ ]
320
+ return sorted(made, key=lambda region: region.start)
321
+
322
+
323
+ def parse_devices(text: str) -> list[Device]:
324
+ """The devices in the list Commander writes out with ExpDevList.
325
+
326
+ A line for each: three fields in quotes -- maker, device, core -- then a
327
+ group in braces of its flash areas and another of its RAM areas, each
328
+ area a start and a length in hexadecimal. A line that is not one of
329
+ those, the line of headings among them, is passed over.
330
+ """
331
+ devices: dict[str, Device] = {}
332
+ for line in text.splitlines():
333
+ found = _DEVICE.match(line)
334
+ if found is None:
335
+ continue
336
+ maker, name, core = (found.group(i).strip() for i in (1, 2, 3))
337
+ if not name or name.lower() == "device":
338
+ continue
339
+ groups = _groups(found.group(4))
340
+ flash = _areas(groups[0]) if groups else []
341
+ ram = _areas(groups[1]) if len(groups) > 1 else []
342
+ devices.setdefault(name.upper(), Device(maker, name, core, flash, ram))
343
+ return sorted(devices.values(), key=lambda device: device.name.lower())
344
+
345
+
346
+ class JLinkConnection(Connection):
347
+ """A probe and a device that have answered once, and how to reach them again."""
348
+
349
+ def __init__(self, arguments: list[str], device: str, regions: list[Region]) -> None:
350
+ self._arguments: list[str] | None = arguments
351
+ self._device = device
352
+ self._regions = regions
353
+
354
+ def _run(
355
+ self,
356
+ commands: Sequence[str],
357
+ timeout: float,
358
+ doing: str,
359
+ skip_setup: bool = False,
360
+ ) -> str:
361
+ if self._arguments is None:
362
+ raise BackendError("This connection has been closed.")
363
+ if not skip_setup:
364
+ return run(self._arguments, commands, timeout, doing)
365
+ with tempfile.TemporaryDirectory(prefix="pyflashgui-jlink-") as folder:
366
+ # Written afresh each time rather than kept anywhere: it is
367
+ # three lines, and a file somebody could edit would be a
368
+ # setting nobody knew they had.
369
+ script = Path(folder) / "no_setup.JLinkScript"
370
+ script.write_text(NO_SETUP_SCRIPT, encoding="ascii")
371
+ arguments = [*self._arguments, "-JLinkScriptFile", str(script)]
372
+ return run(arguments, commands, timeout, doing)
373
+
374
+ def regions(self) -> list[Region]:
375
+ return list(self._regions)
376
+
377
+ def erase_chip(self) -> None:
378
+ self._run(["H", "Erase"], WORK_S, f"Erasing {self._device}")
379
+
380
+ def erase_ranges(self, ranges: Sequence[tuple[int, int]]) -> None:
381
+ # Erase takes the first and the last address, and erases every
382
+ # sector that any of the range falls in.
383
+ commands = ["H"] + [
384
+ f"Erase 0x{start:08X} 0x{start + length - 1:08X}" for start, length in ranges
385
+ ]
386
+ self._run(commands, WORK_S, f"Erasing part of {self._device}")
387
+
388
+ def program(
389
+ self,
390
+ segments: Sequence[Segment],
391
+ erase: str = ERASE_SECTORS,
392
+ progress: Progress = _nothing,
393
+ ) -> None:
394
+ if erase not in (ERASE_SECTORS, ERASE_CHIP):
395
+ raise BackendError(f"There is no way of erasing called {erase!r}.")
396
+ import bincopy
397
+
398
+ image = bincopy.BinFile()
399
+ for segment in segments:
400
+ image.add_binary(segment.data, address=segment.address)
401
+ progress(0.0)
402
+ with tempfile.TemporaryDirectory(prefix="pyflashgui-jlink-") as folder:
403
+ # Handed over as one Intel HEX file whatever the files were:
404
+ # the bytes are the ones pyflashgui read and showed in its
405
+ # memory map, and Commander reads them once.
406
+ path = Path(folder) / "image.hex"
407
+ path.write_text(image.as_ihex(), encoding="ascii")
408
+ # Stopped, and not restarted first: a device is programmed as it
409
+ # is found, and restarting it is Reset's to do afterwards.
410
+ commands = ["H"]
411
+ if erase == ERASE_CHIP:
412
+ commands.append("Erase")
413
+ # LoadFile erases the sectors it writes to, and checks what it
414
+ # wrote, by itself.
415
+ commands.append(f"LoadFile {_named(path)}")
416
+ self._run(commands, WORK_S, f"Programming {self._device}")
417
+ progress(1.0)
418
+
419
+ def read(
420
+ self, address: int, length: int, resume: bool = False, skip_setup: bool = False
421
+ ) -> bytes:
422
+ """Stopped while it is read, so that what is read is one moment's.
423
+
424
+ ``resume`` sets the device going again afterwards, for a read that
425
+ somebody asked for of a device that was running. ``skip_setup``
426
+ leaves out SEGGER's own setting up of the device: see
427
+ NO_SETUP_SCRIPT.
428
+ """
429
+ with tempfile.TemporaryDirectory(prefix="pyflashgui-jlink-") as folder:
430
+ path = Path(folder) / "read.bin"
431
+ commands = ["H", f"SaveBin {_named(path)} 0x{address:X} 0x{length:X}"]
432
+ if resume:
433
+ commands.append("G")
434
+ self._run(commands, WORK_S, f"Reading {length} bytes at {address:08X}", skip_setup)
435
+ try:
436
+ data = path.read_bytes()
437
+ except OSError as exc:
438
+ raise BackendError(
439
+ f"Reading {length} bytes at {address:08X} failed: J-Link Commander "
440
+ "wrote no file."
441
+ ) from exc
442
+ if len(data) != length:
443
+ raise BackendError(
444
+ f"Reading at {address:08X} gave {len(data)} bytes where {length} were asked for."
445
+ )
446
+ return data
447
+
448
+ def read_range(
449
+ self,
450
+ address: int,
451
+ length: int,
452
+ progress: Progress = _nothing,
453
+ cancelled: Cancelled = _never,
454
+ skip_setup: bool = False,
455
+ ) -> bytes:
456
+ """All of it in one run of Commander, which is quicker than many."""
457
+ if cancelled():
458
+ raise CancelledError("Reading was cancelled.")
459
+ progress(0.0)
460
+ data = self.read(address, length, resume=True, skip_setup=skip_setup)
461
+ progress(1.0)
462
+ return data
463
+
464
+ def verify(
465
+ self,
466
+ segments: Sequence[Segment],
467
+ progress: Progress = _nothing,
468
+ cancelled: Cancelled = _never,
469
+ ) -> Verification:
470
+ """Read each run of bytes back whole, and compare.
471
+
472
+ The same comparison as every other backend's, a run at a time rather
473
+ than a few kilobytes at a time: each read starts Commander afresh,
474
+ and a thousand of those for one image would take the afternoon.
475
+ """
476
+ total = sum(len(s) for s in segments)
477
+ checked = different = 0
478
+ first: tuple[int, int, int] | None = None
479
+ progress(0.0)
480
+ for segment in segments:
481
+ if cancelled():
482
+ raise CancelledError("Verifying was cancelled.")
483
+ found = self.read(segment.address, len(segment))
484
+ if found != segment.data:
485
+ for index, (want, have) in enumerate(zip(segment.data, found, strict=True)):
486
+ if want != have:
487
+ different += 1
488
+ if first is None:
489
+ first = (segment.address + index, want, have)
490
+ checked += len(segment)
491
+ progress(checked / total)
492
+ return Verification(checked=checked, different=different, first=first)
493
+
494
+ def reset(self, halt: bool = False) -> None:
495
+ # R restarts the device and leaves it stopped; G sets it going.
496
+ self._run(["R"] if halt else ["R", "G"], QUICK_S, f"Resetting {self._device}")
497
+
498
+ def close(self) -> None:
499
+ self._arguments = None
500
+
501
+
502
+ class JLinkBackend(Backend):
503
+ name = "jlink"
504
+ label = "J-Link Commander"
505
+ interfaces = INTERFACES
506
+ #: SEGGER's list says where a device's memory is, and not every address
507
+ #: it can be reached by: flash that also appears at a second address is
508
+ #: listed once. So a file outside the list may still be one it takes.
509
+ map_is_complete = False
510
+ can_skip_setup = True
511
+
512
+ def __init__(self) -> None:
513
+ #: The devices Commander knows, by name in capitals, once it has been
514
+ #: asked: the list is thousands long and does not change while
515
+ #: pyflashgui runs.
516
+ self._devices: dict[str, Device] | None = None
517
+
518
+ def probes(self) -> list[ProbeInfo]:
519
+ # No -ExitOnError: with no J-Link plugged in Commander fails to open
520
+ # one, and that is an empty list rather than a fault.
521
+ output = run([], ["ShowEmuList USB"], QUICK_S, "Looking for J-Links")
522
+ return parse_probes(output)
523
+
524
+ def _known(self) -> dict[str, Device]:
525
+ if self._devices is not None:
526
+ return self._devices
527
+ if find_commander() is None:
528
+ # Said once, by the probes: the target can still be typed in.
529
+ return {}
530
+ with tempfile.TemporaryDirectory(prefix="pyflashgui-jlink-") as folder:
531
+ path = Path(folder) / "devices.csv"
532
+ try:
533
+ run([], [f"ExpDevList {_named(path)}"], LIST_S, "Listing the devices")
534
+ # Not UTF-8: a maker's name has been seen with a letter in
535
+ # it that is not, and latin-1 reads any byte there is.
536
+ text = path.read_text(encoding="latin-1")
537
+ except (BackendError, OSError):
538
+ # A Commander that will not write the list still programs a
539
+ # device whose name is typed in.
540
+ text = ""
541
+ self._devices = {device.name.upper(): device for device in parse_devices(text)}
542
+ return self._devices
543
+
544
+ def device(self, target: str) -> Device | None:
545
+ """What is known of the device of this name, whatever its capitals."""
546
+ return self._known().get(target.strip().upper())
547
+
548
+ def targets(self) -> list[TargetInfo]:
549
+ return [
550
+ TargetInfo(device.name, vendor=device.maker, source="J-Link software", core=device.core)
551
+ for device in self._known().values()
552
+ ]
553
+
554
+ def regions(self, target: str) -> list[Region]:
555
+ device = self.device(target)
556
+ return device.regions() if device is not None else []
557
+
558
+ def connect(
559
+ self,
560
+ probe: str,
561
+ target: str,
562
+ frequency: int | None = None,
563
+ interface: str | None = None,
564
+ ) -> Connection:
565
+ if not probe:
566
+ raise BackendError("No J-Link is chosen.")
567
+ if not target:
568
+ raise BackendError("No device is chosen.")
569
+ chosen = interface or INTERFACES[0]
570
+ if chosen not in INTERFACES:
571
+ raise BackendError(f"{chosen} is not an interface J-Link Commander is asked for here.")
572
+ speed = "auto" if frequency is None else str(max(1, int(frequency) // 1000))
573
+ arguments = [
574
+ "-ExitOnError",
575
+ "1",
576
+ "-AutoConnect",
577
+ "1",
578
+ "-USB",
579
+ probe,
580
+ "-Device",
581
+ target,
582
+ "-If",
583
+ chosen,
584
+ "-Speed",
585
+ speed,
586
+ ]
587
+ if chosen in ("JTAG", "cJTAG"):
588
+ # Let it work out where the device sits in the scan chain.
589
+ arguments += ["-JTAGConf", "-1,-1"]
590
+ # Looked up first, while nothing is attached: it may mean asking
591
+ # Commander for its list of devices, which is a run of its own.
592
+ regions = self.regions(target)
593
+ # With nothing to do but leave: a run that comes back clean has
594
+ # opened the probe and found the device answering.
595
+ run(arguments, [], QUICK_S, f"Connecting to {target}")
596
+ return JLinkConnection(arguments, target, regions)