oep-client-python 0.0.26__py3-none-any.whl → 0.0.27__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.27"
oep_client/__main__.py CHANGED
@@ -9,7 +9,9 @@
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
13
15
 
14
16
  <probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
15
17
  ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
@@ -30,14 +32,37 @@ def main(argv=None) -> int:
30
32
  parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
31
33
  sub = parser.add_subparsers(dest="command", required=True)
32
34
  _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")
35
+ sp = sub.add_parser("speed", help="port_speed (core §3.5, host guide §7): try faster rates on a UART bridge, "
36
+ "print the report")
34
37
  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)")
38
+ sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
39
+ "(the first that passes is kept; default 500000). Same as --candidates")
40
+ sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000)")
41
+ sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
42
+ "flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
43
+ form = sp.add_mutually_exclusive_group()
44
+ form.add_argument("--verify", action="store_true", help="the full form (guide §7.3): baseline, the flows measured "
45
+ "at every candidate, a failed flow run again one at a time")
46
+ form.add_argument("--minimal", action="store_true", help="the minimal form (guide §7.2, the default): try, confirm, "
47
+ "commit, no measurement")
48
+ sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
49
+ sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
50
+ "(~/.cache/oep-client/link-speed.json, 30 days; on by default here, off in the library)")
36
51
  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
52
  sp.add_argument("--json", action="store_true")
53
+ lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
54
+ lt.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID]")
55
+ lt.add_argument("--rates", default="", help="comma-separated rates to switch to with port_speed and measure at "
56
+ "(each on its own; 0 or 'now' = the speed in force). Default: only the speed in force")
57
+ lt.add_argument("--patterns", default="in,out,duplex", help="in (probe->host), out (host->probe), duplex")
58
+ lt.add_argument("--inflight", default="1", help="comma-separated in-flight counts (above the probe's max: skipped)")
59
+ lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
60
+ lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
61
+ lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
62
+ lt.add_argument("--timeout", type=float, default=0.3, help="seconds to wait for one answer (default 0.3)")
63
+ lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
64
+ lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
65
+ lt.add_argument("--json", action="store_true", help="one JSON object per cell")
41
66
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
42
67
  src = d.add_mutually_exclusive_group(required=True)
43
68
  src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
@@ -50,6 +75,8 @@ def main(argv=None) -> int:
50
75
  return _config(args)
51
76
  if args.command == "speed":
52
77
  return _speed(args)
78
+ if args.command == "linktest":
79
+ return _linktest(args)
53
80
 
54
81
  if args.fake:
55
82
  call = fake.PROFILES[args.fake]().call
@@ -61,17 +88,70 @@ def main(argv=None) -> int:
61
88
  return 0
62
89
 
63
90
 
91
+ # ---- oep linktest -------------------------------------------------------------------------------------------------
92
+
93
+ def _linktest(args) -> int:
94
+ """Every parameter on the command line: no fixed numbers in the measurement."""
95
+ import dataclasses
96
+ import json
97
+ from . import linktest
98
+ ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
99
+ rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
100
+ hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
101
+ else link.open_host(args.probe)
102
+ stream = getattr(hst.link, "stream", None)
103
+ if hasattr(stream, "set_low_latency_mode"):
104
+ try:
105
+ stream.set_low_latency_mode(args.low_latency == "on")
106
+ except (OSError, ValueError, NotImplementedError):
107
+ pass
108
+ try:
109
+ core.take(hst, 30000, owner="oep linktest")
110
+ for result in linktest.matrix(hst, rates=rates, patterns=[p for p in args.patterns.split(",") if p],
111
+ inflight=ints(args.inflight), sizes=ints(args.sizes) or None,
112
+ frames=args.frames, seconds=args.seconds, timeout=args.timeout):
113
+ if args.json:
114
+ for c in result.cells:
115
+ print(json.dumps({**dataclasses.asdict(c), "actual": result.actual, "low_latency": args.low_latency}), flush=True)
116
+ if result.why:
117
+ print(json.dumps({"rate": result.rate, "why": result.why}), flush=True)
118
+ else:
119
+ print(result.text(), flush=True)
120
+ try:
121
+ hst.end()
122
+ except Exception:
123
+ pass
124
+ finally:
125
+ hst.link.close()
126
+ return 0
127
+
128
+
64
129
  # ---- oep speed ----------------------------------------------------------------------------------------------------
65
130
 
131
+ def _flows(text: str) -> list[tuple[str, int]] | None:
132
+ """--flows "in:2,out,duplex:1" -> [("in", 2), ("out", 0), ("duplex", 1)]; "" -> None (the default flows)."""
133
+ out = []
134
+ for item in text.replace(" ", "").split(","):
135
+ if not item:
136
+ continue
137
+ name, _, n = item.partition(":")
138
+ out.append((name, int(n) if n else 0))
139
+ return out or None
140
+
141
+
66
142
  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."""
143
+ """Take the lock, raise_speed by the host guide's procedure, print the report; the session's end puts the port
144
+ back at its boot speed. The record of passed / failed rates is on unless --no-record."""
68
145
  import dataclasses
69
- rates = [int(r) for r in args.rates.replace(" ", "").split(",") if r]
146
+ text = args.candidates or args.rates
147
+ candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
148
+ flows = _flows(args.flows)
149
+ verify = True if args.verify or flows else (False if args.minimal else None)
70
150
  hst = link.open_host(args.probe, baud=args.baud)
71
151
  try:
72
152
  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)
153
+ report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
154
+ record=not args.no_record)
75
155
  if args.json:
76
156
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
77
157
  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/endpoint.py CHANGED
@@ -35,10 +35,12 @@ define:
35
35
  session ends, then resumes from the session's last host reset. The byte framing itself is `fake_serial`.
36
36
  - port_speed (core §3.5, optional): on when the profile's describe declares it (`port_speed_base`, the boot speed;
37
37
  None = off: the op is unknown_operation). try / commit / revert on the UART bridge the request came in on (else
38
- unavailable cause 6), a rate the fake UART cannot make (outside 300..5000000) unsupported; a try reverts after
39
- verify_ms without a commit or at a broken candidate, a commit after idle_ms (at most port_speed_idle_max_ms, 3000;
40
- 0 and anything longer count as that maximum) with no good frame or 3 broken
41
- candidates within 1 s, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
38
+ unavailable cause 6), a step that does not fit the port's state (commit at the boot speed or when committed,
39
+ revert at the boot speed, try while trying or committed) unavailable cause 6, a step above 2 malformed, a rate the
40
+ fake UART cannot make (outside 300..5000000) unsupported; a try reverts after verify_ms without a commit or at a
41
+ broken candidate once a good frame came at the new speed, a commit after idle_ms (at most port_speed_idle_max_ms,
42
+ 3000; 0 and anything longer count as that maximum) with no good frame or at 3 broken candidates in a row with no
43
+ good frame between, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
42
44
  (rate -> BrokenRate): frames at such a rate break, from a size and in the directions given, only both ways at once
43
45
  (`duplex`), only every Nth (`every`) (`fake_serial` applies it).
44
46
 
@@ -334,7 +336,7 @@ PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
334
336
  OP_PORT_SPEED = reg.CORE.op["port_speed"]
335
337
  SPEED_STEP = reg.CORE.enum["port_speed_step"]
336
338
  SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
337
- SPEED_BAD_MAX, SPEED_BAD_WINDOW_MS = 3, 1000 # committed: this many broken candidates within the window revert
339
+ SPEED_BAD_MAX = 3 # committed: this many broken candidates in a row (no good frame between) revert
338
340
  SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
339
341
 
340
342
 
@@ -487,7 +489,8 @@ class Endpoint:
487
489
  self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
488
490
  self.speed_idle_ms = 0 # committed: revert after this long with no good frame
489
491
  self.speed_good_ms = 0 # the last good frame on that port
490
- self.speed_bad: list[int] = [] # committed: the times of the recent broken candidates
492
+ self.speed_heard = False # try: a good frame came at the new speed (before it, no broken counts)
493
+ self.speed_bad = 0 # committed: broken candidates in a row since the last good frame
491
494
  self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
492
495
  self.speed_log: list[tuple[int, int]] = [] # (port, rate) every switch, reverts included
493
496
  if self.saved is not None:
@@ -1203,23 +1206,28 @@ class Endpoint:
1203
1206
  port, baud, step, verify_ms, idle_ms = t.take("BIBHI")
1204
1207
  _, ignored = t.tail()
1205
1208
  if step not in SPEED_STEP.values():
1206
- raise unsupported_fixed()
1209
+ raise Reject(m.MALFORMED) # not a defined step (core §4.3 order 5)
1207
1210
  if port != self._transport or port >= len(self.transports) or \
1208
1211
  self.transports[port] != fake.TRANSPORT["uart_bridge"]:
1209
1212
  raise unavailable("wrong_state") # only the UART bridge the request came in on
1213
+ # the port's state (boot speed / trying / committed) decides which step fits (core §3.5): any other is cause 6
1210
1214
  if step == SPEED_STEP["try"]:
1215
+ if self.speed_state != "base":
1216
+ raise unavailable("wrong_state") # already trying or committed
1211
1217
  if not SPEED_RATES[0] <= baud <= SPEED_RATES[1]:
1212
1218
  raise unsupported_fixed() # a baud this UART cannot make
1213
1219
  self.speed_pending = ("switch", port, baud, verify_ms)
1214
1220
  return self._answer(struct.pack("<I", baud), ignored)
1215
1221
  if step == SPEED_STEP["commit"]:
1216
- if self.speed_state == "base" or port != self.speed_port or baud != self.speed_asked:
1217
- raise unavailable("wrong_state") # nothing tried, or another baud
1222
+ if self.speed_state != "try" or port != self.speed_port or baud != self.speed_asked:
1223
+ raise unavailable("wrong_state") # nothing tried, committed already, or another baud
1218
1224
  self.speed_state = "committed"
1219
1225
  self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
1220
1226
  self.speed_good_ms = self.now()
1221
- self.speed_bad.clear()
1227
+ self.speed_bad = 0
1222
1228
  return self._answer(struct.pack("<I", self.speed_rate), ignored)
1229
+ if self.speed_state == "base":
1230
+ raise unavailable("wrong_state") # a revert at the boot speed: nothing to go back from
1223
1231
  self.speed_pending = ("revert",)
1224
1232
  return self._answer(struct.pack("<I", self.port_speed_base), ignored)
1225
1233
 
@@ -1255,7 +1263,8 @@ class Endpoint:
1255
1263
  self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "try", port, baud, baud
1256
1264
  self.speed_until_ms = self.now() + verify_ms
1257
1265
  self.speed_good_ms = self.now()
1258
- self.speed_bad.clear()
1266
+ self.speed_heard = False
1267
+ self.speed_bad = 0
1259
1268
  self.speed_log.append((port, baud))
1260
1269
 
1261
1270
  def _speed_revert(self) -> None:
@@ -1263,24 +1272,32 @@ class Endpoint:
1263
1272
  return
1264
1273
  self.speed_log.append((self.speed_port, self.port_speed_base))
1265
1274
  self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "base", None, 0, 0
1266
- self.speed_bad.clear()
1275
+ self.speed_heard, self.speed_bad = False, 0
1267
1276
 
1268
1277
  def speed_frame(self, port: int, good: bool) -> None:
1269
- """A candidate closed on serial port `port`: a frame (good) or not (a broken candidate)."""
1278
+ """A candidate closed on serial port `port`: a frame (good) or not (a broken candidate). core §3.5's conditions
1279
+ 2 (trying: one broken candidate after the first good frame at the new speed; the ones before it are the
1280
+ switch-over's leftovers and do not count) and 4 (committed: SPEED_BAD_MAX broken candidates in a row with no
1281
+ good frame between)."""
1270
1282
  if self.speed_state == "base" or port != self.speed_port:
1271
1283
  return
1272
1284
  if good:
1273
1285
  self.speed_good_ms = self.now()
1286
+ self.speed_heard = True
1287
+ self.speed_bad = 0
1274
1288
  return
1275
1289
  if self.speed_state == "try":
1276
- self._speed_revert() # the new speed breaks frames: not this one
1290
+ if self.speed_heard:
1291
+ self._speed_revert() # the new speed breaks frames: not this one
1277
1292
  return
1278
- now = self.now()
1279
- self.speed_bad = [t for t in self.speed_bad if now - t < SPEED_BAD_WINDOW_MS] + [now]
1280
- if len(self.speed_bad) >= SPEED_BAD_MAX:
1293
+ self.speed_bad += 1
1294
+ if self.speed_bad >= SPEED_BAD_MAX:
1281
1295
  self._speed_revert()
1282
1296
 
1283
1297
  def _speed_tick(self) -> None:
1298
+ # Condition 3 (committed, idle_ms with no good frame) is not counted while a request executes (core §3.5, as
1299
+ # the lease, §6.1). The fake handles every request within one call and its clock does not move meanwhile, so
1300
+ # there is nothing to pause here: a long-running request would need `speed_good_ms` set when its answer goes out.
1284
1301
  if self.speed_pending and self.speed_pending[0] == "revert":
1285
1302
  self.speed_after_answer()
1286
1303
  now = self.now()
@@ -1573,8 +1590,10 @@ class Endpoint:
1573
1590
  if op == _RV.op["read_block"]:
1574
1591
  address, count = t.take("IH")
1575
1592
  t.tail()
1576
- if 4 * count > self.block_max.get(fn, 1 << 16) or address % 4:
1577
- raise Reject(m.MALFORMED) # past the declared max_length (bytes), or not a word
1593
+ if address % 4:
1594
+ raise Reject(m.MALFORMED) # not a word address (core §4.3 order 5)
1595
+ if 4 * count > self.block_max.get(fn, 1 << 16):
1596
+ raise unsupported_fixed() # past the declared max_length (bytes; debug §4.5)
1578
1597
  if not tg.halted:
1579
1598
  return m.COMPLETED, m.FAILED, struct.pack("<HB", 0, STATE) # a running hart (debug §4.5)
1580
1599
  words, status = [], OK
@@ -1587,8 +1606,10 @@ class Endpoint:
1587
1606
  struct.pack(f"<HB{len(words)}I", len(words), status, *words))
1588
1607
  if op == _RV.op["write_block"]:
1589
1608
  address, count = t.take("IH")
1590
- if 4 * count > self.block_max.get(fn, 1 << 16) or address % 4:
1609
+ if address % 4:
1591
1610
  raise Reject(m.MALFORMED)
1611
+ if 4 * count > self.block_max.get(fn, 1 << 16):
1612
+ raise unsupported_fixed() # past the declared max_length (debug §4.5)
1592
1613
  words = struct.unpack(f"<{count}I", t.bytes(4 * count))
1593
1614
  t.tail()
1594
1615
  if not tg.halted:
oep_client/fake.py CHANGED
@@ -157,6 +157,16 @@ MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
157
157
  _CFG = reg.PROBE_CONFIG.tlv["describe"]
158
158
 
159
159
 
160
+ BLOCK_HEADERS = 24 # read_block's answer (5 + done 2 + status 1) and write_block's request (10 + connection 2 + address 4 + count 2) both fit
161
+
162
+
163
+ def block_max_length(max_frame: int) -> int:
164
+ """The max_length (bytes, a multiple of 4) a probe with read_block / write_block declares for its max_frame
165
+ (oep-if-debug §4.5): max_frame - 24 rounded down to a word, so a read's answer and a write's request both fit one
166
+ frame. The host takes this value; it never computes a block size from max_frame."""
167
+ return max(0, (max_frame - BLOCK_HEADERS) // 4 * 4)
168
+
169
+
160
170
  def _transports(kinds: list[tuple[int, int]]) -> tuple[bytes, ...]:
161
171
  """(kind, USB interface or 0xFF) per transport, index = position (core §7.5)."""
162
172
  return tuple(catalog.tlv(CORE_TRANSPORT, bytes([i, k, itf])) for i, (k, itf) in enumerate(kinds))
@@ -219,7 +229,7 @@ def p4_x035() -> FakeProbe:
219
229
  catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
220
230
  catalog.u8(MAX_CONNECTIONS, 1))),
221
231
  Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
222
- catalog.u16(MAX_LENGTH, 1000))),
232
+ catalog.u16(MAX_LENGTH, block_max_length(1024)))),
223
233
  Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
224
234
  _gpio(4, pins),
225
235
  _uart(5, 0, pins, 3_000_000),
@@ -256,7 +266,7 @@ def esp32_v003() -> FakeProbe:
256
266
  Offered(1, 0, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
257
267
  catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
258
268
  Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
259
- catalog.u16(MAX_LENGTH, 40))),
269
+ catalog.u16(MAX_LENGTH, block_max_length(64)))),
260
270
  Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
261
271
  _gpio(4, wired + [23], modes=0x7F), # no mode 7 (both pulls): unsupported there
262
272
  _uart(5, 0, wired, 115_200),
@@ -290,7 +300,7 @@ def p4_bench() -> FakeProbe:
290
300
  catalog.channel_group(3, [(1, 6), (2, 7)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000),
291
301
  catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 2))),
292
302
  Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
293
- catalog.u16(MAX_LENGTH, 1000))),
303
+ catalog.u16(MAX_LENGTH, block_max_length(1024)))),
294
304
  Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
295
305
  _gpio(4, pins),
296
306
  _uart(5, 0, pins, 3_000_000),
@@ -310,7 +320,7 @@ def rp2350_pins() -> FakeProbe:
310
320
  Offered(1, 0, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
311
321
  catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
312
322
  Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
313
- catalog.u16(MAX_LENGTH, 1000))),
323
+ catalog.u16(MAX_LENGTH, block_max_length(1024)))),
314
324
  Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
315
325
  _gpio(4, pins),
316
326
  _uart(5, 0, pins, 3_000_000),