oep-client-python 0.0.26__py3-none-any.whl → 0.0.28__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.
oep_client/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.26"
5
+ __version__ = "0.0.28"
oep_client/__main__.py CHANGED
@@ -9,7 +9,11 @@
9
9
  oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
10
10
  oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
11
11
  oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
12
- oep speed <probe> 1500000,921600,500000 (port_speed, core §3.5: try the rates in order on a UART bridge, report)
12
+ oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, core §3.5 and the host
13
+ guide §7: try the candidates in order on a UART bridge, report)
14
+ oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
15
+ oep pins <probe> --power 5 --wire swio (find the target's debug pins and reset line: classify the channels, scan,
16
+ identify, hold-low + attach under reset; prints a slot, --save writes it)
13
17
 
14
18
  <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
15
19
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
@@ -30,14 +34,50 @@ def main(argv=None) -> int:
30
34
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
31
35
  sub = parser.add_subparsers(dest="command", required=True)
32
36
  _config_parser(sub)
33
- sp = sub.add_parser("speed", help="port_speed (core §3.5): try faster rates on a UART bridge, print the report")
37
+ sp = sub.add_parser("speed", help="port_speed (core §3.5, host guide §7): try faster rates on a UART bridge, "
38
+ "print the report")
34
39
  sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
35
- sp.add_argument("rates", help="comma-separated, in order of preference (the first that passes is kept)")
40
+ sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
41
+ "(the first that passes is kept; default 500000). Same as --candidates")
42
+ sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000)")
43
+ sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
44
+ "flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
45
+ form = sp.add_mutually_exclusive_group()
46
+ form.add_argument("--verify", action="store_true", help="the full form (guide §7.3): baseline, the flows measured "
47
+ "at every candidate, a failed flow run again one at a time")
48
+ form.add_argument("--minimal", action="store_true", help="the minimal form (guide §7.2, the default): try, confirm, "
49
+ "commit, no measurement")
50
+ sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
51
+ sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
52
+ "(~/.cache/oep-client/link-speed.json, 30 days; on by default here, off in the library)")
36
53
  sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
37
- sp.add_argument("--verify-bytes", type=int, default=32768)
38
- sp.add_argument("--verify-s", type=float, default=1.0)
39
- sp.add_argument("--duplex-s", type=float, default=1.0, help="both ways at once for this long (default 1)")
40
54
  sp.add_argument("--json", action="store_true")
55
+ lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
56
+ lt.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID]")
57
+ lt.add_argument("--rates", default="", help="comma-separated rates to switch to with port_speed and measure at "
58
+ "(each on its own; 0 or 'now' = the speed in force). Default: only the speed in force")
59
+ lt.add_argument("--patterns", default="in,out,duplex", help="in (probe->host), out (host->probe), duplex")
60
+ lt.add_argument("--inflight", default="1", help="comma-separated in-flight counts (above the probe's max: skipped)")
61
+ lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
62
+ lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
63
+ lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
64
+ lt.add_argument("--timeout", type=float, default=0.3, help="seconds to wait for one answer (default 0.3)")
65
+ lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
66
+ lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
67
+ lt.add_argument("--json", action="store_true", help="one JSON object per cell")
68
+ pn = sub.add_parser("pins", help="find where a target is wired: classify the channels, scan the wire, find the "
69
+ "reset line, suggest a slot (writes nothing without --save)")
70
+ pn.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
71
+ pn.add_argument("--power", type=int, help="the gpio channel that powers the target: switched off, then on, to "
72
+ "see which channels follow it (never touched without this)")
73
+ pn.add_argument("--exclude", default="", help="channels never to read, scan or hold, comma-separated")
74
+ pn.add_argument("--wire", choices=("swio", "rvswd", "swd"), default="swio")
75
+ pn.add_argument("--steps", default="classify,hold,scan,identify,reset,slot",
76
+ help="the steps to run, comma-separated (default all: classify, hold, scan, identify, reset, slot)")
77
+ pn.add_argument("--save", action="store_true", help="write the suggested slot (config set, then save)")
78
+ pn.add_argument("--slot", type=int, default=0, help="--save: the slot number (default 0)")
79
+ pn.add_argument("--name", help="--save: the slot's name (default: the target family, else 'target')")
80
+ pn.add_argument("--json", action="store_true", help="the report as JSON (the steps' lines go to stderr)")
41
81
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
42
82
  src = d.add_mutually_exclusive_group(required=True)
43
83
  src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
@@ -50,6 +90,10 @@ def main(argv=None) -> int:
50
90
  return _config(args)
51
91
  if args.command == "speed":
52
92
  return _speed(args)
93
+ if args.command == "linktest":
94
+ return _linktest(args)
95
+ if args.command == "pins":
96
+ return _pins_cmd(args)
53
97
 
54
98
  if args.fake:
55
99
  call = fake.PROFILES[args.fake]().call
@@ -61,17 +105,97 @@ def main(argv=None) -> int:
61
105
  return 0
62
106
 
63
107
 
108
+ # ---- oep pins -----------------------------------------------------------------------------------------------------
109
+
110
+ def _pins_cmd(args) -> int:
111
+ from . import pins
112
+ exclude = [int(x, 0) for x in args.exclude.replace(" ", "").split(",") if x]
113
+ say = (lambda t: print(t, file=sys.stderr, flush=True)) if args.json else (lambda t: print(t, flush=True)) # noqa: E731
114
+ hst = link.open_host(args.probe)
115
+ try:
116
+ core.take(hst, 10000, owner="oep pins")
117
+ finder = pins.PinFinder(hst, wire=args.wire, power=args.power, exclude=exclude, say=say, probe=args.probe,
118
+ save=args.save, slot=args.slot, name=args.name)
119
+ report = finder.run(steps=tuple(s for s in args.steps.split(",") if s))
120
+ say(f"done in {report.elapsed_s:.1f} s")
121
+ if args.json:
122
+ print(json.dumps(report.as_dict(), indent=2))
123
+ try:
124
+ hst.end()
125
+ except host.OepError:
126
+ pass
127
+ except host.Rejected as e:
128
+ print(f"refused: {e}", file=sys.stderr)
129
+ return 2
130
+ finally:
131
+ hst.link.close()
132
+ return 0 if report.found else 1
133
+
134
+
135
+ # ---- oep linktest -------------------------------------------------------------------------------------------------
136
+
137
+ def _linktest(args) -> int:
138
+ """Every parameter on the command line: no fixed numbers in the measurement."""
139
+ import dataclasses
140
+ import json
141
+ from . import linktest
142
+ ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
143
+ rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
144
+ hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
145
+ else link.open_host(args.probe)
146
+ stream = getattr(hst.link, "stream", None)
147
+ if hasattr(stream, "set_low_latency_mode"):
148
+ try:
149
+ stream.set_low_latency_mode(args.low_latency == "on")
150
+ except (OSError, ValueError, NotImplementedError):
151
+ pass
152
+ try:
153
+ core.take(hst, 30000, owner="oep linktest")
154
+ for result in linktest.matrix(hst, rates=rates, patterns=[p for p in args.patterns.split(",") if p],
155
+ inflight=ints(args.inflight), sizes=ints(args.sizes) or None,
156
+ frames=args.frames, seconds=args.seconds, timeout=args.timeout):
157
+ if args.json:
158
+ for c in result.cells:
159
+ print(json.dumps({**dataclasses.asdict(c), "actual": result.actual, "low_latency": args.low_latency}), flush=True)
160
+ if result.why:
161
+ print(json.dumps({"rate": result.rate, "why": result.why}), flush=True)
162
+ else:
163
+ print(result.text(), flush=True)
164
+ try:
165
+ hst.end()
166
+ except Exception:
167
+ pass
168
+ finally:
169
+ hst.link.close()
170
+ return 0
171
+
172
+
64
173
  # ---- oep speed ----------------------------------------------------------------------------------------------------
65
174
 
175
+ def _flows(text: str) -> list[tuple[str, int]] | None:
176
+ """--flows "in:2,out,duplex:1" -> [("in", 2), ("out", 0), ("duplex", 1)]; "" -> None (the default flows)."""
177
+ out = []
178
+ for item in text.replace(" ", "").split(","):
179
+ if not item:
180
+ continue
181
+ name, _, n = item.partition(":")
182
+ out.append((name, int(n) if n else 0))
183
+ return out or None
184
+
185
+
66
186
  def _speed(args) -> int:
67
- """Take the lock, raise_speed, print the report; the session's end puts the port back at its boot speed."""
187
+ """Take the lock, raise_speed by the host guide's procedure, print the report; the session's end puts the port
188
+ back at its boot speed. The record of passed / failed rates is on unless --no-record."""
68
189
  import dataclasses
69
- rates = [int(r) for r in args.rates.replace(" ", "").split(",") if r]
190
+ text = args.candidates or args.rates
191
+ candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
192
+ flows = _flows(args.flows)
193
+ verify = True if args.verify or flows else (False if args.minimal else None)
70
194
  hst = link.open_host(args.probe, baud=args.baud)
71
195
  try:
72
196
  core.take(hst, 5000, owner="oep speed")
73
- report = link.raise_speed(hst, rates, verify_bytes=args.verify_bytes, verify_s=args.verify_s,
74
- duplex_s=args.duplex_s)
197
+ report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
198
+ record=not args.no_record)
75
199
  if args.json:
76
200
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
77
201
  else:
oep_client/arm.py CHANGED
@@ -10,7 +10,7 @@ import struct
10
10
 
11
11
  from . import host as h, message as m, registry as reg
12
12
  from .core import Interface
13
- from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
13
+ from .riscv import OK, BlockLength, TargetError, WireBase, check, ran, status_name # noqa: F401
14
14
 
15
15
  DP_DPIDR = DP_ABORT = 0x0
16
16
  DP_CTRL_STAT = 0x4
@@ -60,7 +60,9 @@ def transfer_reads(steps: bytes) -> list[bool]:
60
60
  return out
61
61
 
62
62
 
63
- class ArmAdi(Interface):
63
+ class ArmAdi(Interface, BlockLength):
64
+ """oep.target.arm-adi on one connection. `max_length` / `max_words` bound read_block / write_block (from the
65
+ probe's describe, oep-if-debug §6: mandatory there)."""
64
66
  NAME = "oep.target.arm-adi"
65
67
  REVISION = 1
66
68
  TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
@@ -70,6 +72,7 @@ class ArmAdi(Interface):
70
72
  self.conn = conn
71
73
  self.adiv6 = adiv6
72
74
  self._select: int | None = None
75
+ self._max_length = None
73
76
 
74
77
  # ---- raw transfers ----
75
78
  @staticmethod
@@ -140,10 +143,8 @@ class MemAp:
140
143
  csw = adi.ap_read(ap, self.base)
141
144
  adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
142
145
  adi._ap_select(ap, self.base) # the bank the block operations assume
143
- # Words per block operation, from the probe's frame limit: request header 6 + session 4 + connection 2 +
144
- # address 4 + count 2 on the way in (the answer's 5 + done 2 + status 1 is smaller).
145
- from .core import confirm
146
- self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
146
+ # Words per block operation: the probe's declared max_length (oep-if-debug §6), never computed from max_frame
147
+ self.chunk = adi.max_words
147
148
 
148
149
  def write_many(self, pairs: list[tuple[int, int]]) -> None:
149
150
  """Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
oep_client/ch32_flash.py CHANGED
@@ -17,7 +17,7 @@ import struct
17
17
  import time
18
18
  from dataclasses import dataclass, field
19
19
 
20
- from . import core, host as h, riscv
20
+ from . import host as h, riscv
21
21
 
22
22
  # oep-spec experiments/flash-primitives/x035_loader.S, assembled at 0x20000000 (riscv32-esp-elf binutils)
23
23
  FAST_PAGE_LOADER = bytes.fromhex(
@@ -91,11 +91,10 @@ LOADER, FAST_BUFFER, FAST_DONE = 0x20000000, 0x20000400, 0x200000B0
91
91
  V003_INPUT, V003_STACK, V003_EBREAK, V003_RUN = 0x20000200, 0x20000800, 0x2000015C, 1024
92
92
 
93
93
 
94
- def _block_size(hst: h.Host) -> int:
95
- info = core.confirm(hst)
96
- # request header 6 + session 4 + connection 1 + address 4 + count 2 (the read answer's 5 + done 2 + status 1 is
97
- # smaller); a little slack for the result header
98
- return (info["max_frame"] - 5 - 6 - 4 - 1 - 4 - 2) // 4 * 4
94
+ def _block_size(dm: riscv.RiscvDm) -> int:
95
+ """Bytes per read_block / write_block: the probe's declared max_length (oep-if-debug §4.5), never a value computed
96
+ from max_frame (riscv.NoMaxLength when the probe declares none)."""
97
+ return dm.max_words * 4
99
98
 
100
99
 
101
100
  def _write_requests(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
@@ -142,7 +141,7 @@ def program(hst: h.Host, dm: riscv.RiscvDm, image: bytes, profile: FlashProfile)
142
141
  image = image + b"\xff" * (-len(image) % profile.page)
143
142
  if profile.size and len(image) > profile.size:
144
143
  raise ValueError(f"image of {len(image)} bytes exceeds {profile.size}")
145
- block = _block_size(hst)
144
+ block = _block_size(dm)
146
145
  t = {}
147
146
  t0 = time.perf_counter()
148
147
  if profile.method == "fast-page":
oep_client/cobs.py CHANGED
@@ -71,3 +71,8 @@ def unframe(raw: bytes) -> bytes:
71
71
  if crc16(body) != got:
72
72
  raise CorruptFrame("CRC mismatch")
73
73
  return body
74
+
75
+ def frame_max(message_len: int) -> int:
76
+ """The longest frame a message of this length makes: CRC16, a COBS code per 254 bytes and one more, the delimiter."""
77
+ n = message_len + 2
78
+ return n + (n + 253) // 254 + 1 + 1
oep_client/config.py CHANGED
@@ -73,14 +73,23 @@ class Label:
73
73
 
74
74
  @dataclass(kw_only=True)
75
75
  class Idle:
76
+ """The state of a channel no plan or connection uses (probe.config §1): at boot and after every release. mode:
77
+ hi-z, pull-up, pull-down, output-low, output-high (output_low / output_high too). An output mode keeps driving that
78
+ level while the channel is free - a target's power switch kept on - and a gpio plan that takes the channel keeps
79
+ it until its first set (fixture §1); a probe that cannot drive the channel refuses it unsupported."""
76
80
  channel: int
77
- mode: str = "pull-up" # hi-z, pull-up, pull-down
81
+ mode: str = "pull-up" # hi-z, pull-up, pull-down, output-low, output-high
78
82
  TAG = ITEM["idle"]
79
83
 
84
+ def __post_init__(self):
85
+ self.mode = self.mode.replace("_", "-")
86
+
80
87
  def key(self) -> tuple:
81
88
  return (self.channel,)
82
89
 
83
90
  def value(self) -> bytes:
91
+ if self.mode not in IDLE:
92
+ raise ValueError(f"idle mode {self.mode!r}: one of {', '.join(IDLE)}")
84
93
  return struct.pack("<HB", self.channel, IDLE[self.mode])
85
94
 
86
95
 
@@ -395,3 +404,50 @@ class ProbeConfig(Interface):
395
404
  return st
396
405
  first_slot += n_slots
397
406
  first_bind += n_binds
407
+
408
+
409
+ # The label names of a target's power and reset lines (host-development-guide §8.1): `nrst` its reset, `power_hi` high
410
+ # powers it, `power_lo` low powers it. On a probe with several slots they are `<slot name>.<name>`.
411
+ LINE_NAMES = ("nrst", "power_hi", "power_lo")
412
+
413
+
414
+ class AmbiguousLine(LookupError):
415
+ """find_line could not tell which channel is meant; `candidates` holds (label text, channel)."""
416
+
417
+ def __init__(self, message: str, candidates: list[tuple[str, int]]):
418
+ super().__init__(message)
419
+ self.candidates = candidates
420
+
421
+
422
+ def find_line(hst_or_items, name: str, slot: int | str | None = None) -> int | None:
423
+ """The channel labelled for `name` (nrst, power_hi, power_lo: host-development-guide §8.1), or None when there is
424
+ none. hst_or_items: a Host (its settings are read: `ProbeConfig.items()`, no lock) or the decoded items. slot: the
425
+ slot's name or number; None picks the probe's one slot.
426
+
427
+ `<slot>.<name>` first, then the bare `name` - the bare name only when the settings hold at most one slot (a bare
428
+ name is for a one-slot probe). With several slots and no `slot`, or two channels with the same label, it raises
429
+ AmbiguousLine listing the candidates."""
430
+ items = hst_or_items if isinstance(hst_or_items, (list, tuple)) else ProbeConfig(hst_or_items).items()
431
+ labels = [(it.text, it.channel) for it in items if isinstance(it, Label)]
432
+ slots = [it for it in items if isinstance(it, Slot)]
433
+ if isinstance(slot, int):
434
+ named = [s.name for s in slots if s.slot == slot]
435
+ if not named:
436
+ raise LookupError(f"no slot {slot} in the probe's settings")
437
+ slot = named[0]
438
+ if slot is None and len(slots) > 1:
439
+ candidates = sorted((t, c) for t, c in labels if t == name or t.endswith("." + name))
440
+ if candidates:
441
+ listed = ", ".join(f"{t} (channel {c})" for t, c in candidates)
442
+ raise AmbiguousLine(f"{name}: {len(slots)} slots and no slot given; candidates: {listed}", candidates)
443
+ return None
444
+ if slot is None and slots:
445
+ slot = slots[0].name
446
+ texts = ([f"{slot}.{name}"] if slot is not None else []) + ([name] if len(slots) <= 1 else [])
447
+ for text in texts:
448
+ found = sorted(c for t, c in labels if t == text)
449
+ if len(found) > 1:
450
+ raise AmbiguousLine(f"{text}: on {len(found)} channels", [(text, c) for c in found])
451
+ if found:
452
+ return found[0]
453
+ return None