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.
- pyflashgui/__init__.py +6 -0
- pyflashgui/__main__.py +334 -0
- pyflashgui/backends/__init__.py +40 -0
- pyflashgui/backends/base.py +272 -0
- pyflashgui/backends/jlink_backend.py +596 -0
- pyflashgui/backends/pyocd_backend.py +271 -0
- pyflashgui/backends/simulated.py +240 -0
- pyflashgui/core/__init__.py +2 -0
- pyflashgui/core/checkout.py +134 -0
- pyflashgui/core/compare.py +157 -0
- pyflashgui/core/context.py +37 -0
- pyflashgui/core/events.py +71 -0
- pyflashgui/core/excepthook.py +110 -0
- pyflashgui/core/flasher.py +419 -0
- pyflashgui/core/images.py +279 -0
- pyflashgui/core/installation.py +137 -0
- pyflashgui/core/layout.py +72 -0
- pyflashgui/core/memory_map.py +234 -0
- pyflashgui/core/packages.py +104 -0
- pyflashgui/core/paths.py +49 -0
- pyflashgui/core/settings.py +44 -0
- pyflashgui/core/shortcut.py +154 -0
- pyflashgui/core/slow_start.py +101 -0
- pyflashgui/core/timing.py +312 -0
- pyflashgui/core/updates.py +195 -0
- pyflashgui/core/worker.py +71 -0
- pyflashgui/core/workspaces.py +250 -0
- pyflashgui/help/__init__.py +129 -0
- pyflashgui/help/about.md +21 -0
- pyflashgui/help/changelog.md +46 -0
- pyflashgui/help/files.md +60 -0
- pyflashgui/help/main-window.png +0 -0
- pyflashgui/help/manual.md +44 -0
- pyflashgui/help/memory-map.md +28 -0
- pyflashgui/help/memory.md +59 -0
- pyflashgui/help/probe.md +71 -0
- pyflashgui/resources/__init__.py +10 -0
- pyflashgui/resources/pyflashgui.ico +0 -0
- pyflashgui/resources/pyflashgui.png +0 -0
- pyflashgui/resources/pyflashgui.svg +32 -0
- pyflashgui/resources/pyflashgui_small.svg +20 -0
- pyflashgui/ui/__init__.py +2 -0
- pyflashgui/ui/confirm.py +312 -0
- pyflashgui/ui/detached.py +101 -0
- pyflashgui/ui/erase_dialog.py +141 -0
- pyflashgui/ui/event_colours.py +66 -0
- pyflashgui/ui/files_view.py +716 -0
- pyflashgui/ui/folders.py +176 -0
- pyflashgui/ui/fonts.py +75 -0
- pyflashgui/ui/help_menu.py +587 -0
- pyflashgui/ui/main_window.py +754 -0
- pyflashgui/ui/map_view.py +215 -0
- pyflashgui/ui/memory_view.py +724 -0
- pyflashgui/ui/messages.py +78 -0
- pyflashgui/ui/pane_bar.py +92 -0
- pyflashgui/ui/panes.py +1068 -0
- pyflashgui/ui/persist.py +132 -0
- pyflashgui/ui/probe_bar.py +315 -0
- pyflashgui/ui/read_dialog.py +161 -0
- pyflashgui/ui/session.py +53 -0
- pyflashgui/ui/theme.py +85 -0
- pyflashgui-0.1.0.dist-info/METADATA +80 -0
- pyflashgui-0.1.0.dist-info/RECORD +68 -0
- pyflashgui-0.1.0.dist-info/WHEEL +5 -0
- pyflashgui-0.1.0.dist-info/entry_points.txt +2 -0
- pyflashgui-0.1.0.dist-info/licenses/LICENSE +202 -0
- pyflashgui-0.1.0.dist-info/licenses/NOTICE +30 -0
- 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)
|