oep-client-python 0.0.24__py3-none-any.whl → 0.0.26__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.24"
5
+ __version__ = "0.0.26"
oep_client/__main__.py CHANGED
@@ -36,6 +36,7 @@ def main(argv=None) -> int:
36
36
  sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
37
37
  sp.add_argument("--verify-bytes", type=int, default=32768)
38
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)")
39
40
  sp.add_argument("--json", action="store_true")
40
41
  d = sub.add_parser("dump", help="list and describe every interface a probe offers")
41
42
  src = d.add_mutually_exclusive_group(required=True)
@@ -69,7 +70,8 @@ def _speed(args) -> int:
69
70
  hst = link.open_host(args.probe, baud=args.baud)
70
71
  try:
71
72
  core.take(hst, 5000, owner="oep speed")
72
- report = link.raise_speed(hst, rates, verify_bytes=args.verify_bytes, verify_s=args.verify_s)
73
+ report = link.raise_speed(hst, rates, verify_bytes=args.verify_bytes, verify_s=args.verify_s,
74
+ duplex_s=args.duplex_s)
73
75
  if args.json:
74
76
  sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
75
77
  else:
oep_client/arm.py CHANGED
@@ -37,7 +37,7 @@ class SwdWire(WireBase):
37
37
  body = bytes([0]) + self._speed_tlv(self._speed_or_default(max_speed)) + self._pins_tlv(pins) + self._reset_tlv(reset)
38
38
  if targetsel is not None:
39
39
  body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
40
- rd = m.Reader(self._call(self.ATTACH, body).payload)
40
+ rd = m.Reader(self._call(self.ATTACH, body, expect_ms=reset[1] if reset else 0).payload)
41
41
  conn, dpidr, self.flags, self.speed_hz = rd.take("HIBI")
42
42
  self.existing = bool(self.flags & self.FLAGS["existing"])
43
43
  rd.tail()
oep_client/capture.py CHANGED
@@ -335,7 +335,7 @@ class LogicCapture(Interface):
335
335
 
336
336
  def start(self) -> int:
337
337
  """-> blocking_ms (0: the probe keeps answering while it captures). self.generation: the new capture's."""
338
- rd = m.Reader(self._call(self.START).payload)
338
+ rd = m.Reader(self._call(self.START, expect_ms=self.config.blocking_ms if self.config else 0).payload)
339
339
  blocking, self.generation = rd.take("II")
340
340
  rd.tail()
341
341
  self.armed_s = time.monotonic()
@@ -576,7 +576,8 @@ class CaptureGroup(Interface):
576
576
  def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
577
577
  """-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s and generation to set (the answer's TLV
578
578
  generations names each track's new generation; self.generations keeps them by fn)."""
579
- rd = m.Reader(self._call(self.START).payload)
579
+ rd = m.Reader(self._call(self.START, expect_ms=max([t.config.blocking_ms for t in tracks if t.config] or [0]))
580
+ .payload)
580
581
  blocking, start_ns = rd.take("IQ")
581
582
  gens = rd.tail().get(GROUP_GENERATIONS) or b""
582
583
  self.generations = {fn: g for fn, g in struct.iter_unpack("<HI", gens[:len(gens) // 6 * 6])}
oep_client/config.py CHANGED
@@ -292,6 +292,8 @@ class State:
292
292
  binds: list[BindState] = field(default_factory=list)
293
293
 
294
294
 
295
+ SAVE_EXPECT_MS = 2000 # a save writes the probe's flash: the link waits at least this (Host.expecting)
296
+
295
297
  class ProbeConfig(Interface):
296
298
  NAME = "oep.probe.config"
297
299
  REVISION = 1
@@ -346,7 +348,7 @@ class ProbeConfig(Interface):
346
348
 
347
349
  def save(self) -> int:
348
350
  """Save the current settings (a probe with storage; the whole is replaced). -> the hash saved."""
349
- return self._hash_answer(self._call(self.SAVE))
351
+ return self._hash_answer(self._call(self.SAVE, expect_ms=SAVE_EXPECT_MS))
350
352
 
351
353
  def erase(self) -> None:
352
354
  self._call(self.ERASE)
oep_client/core.py CHANGED
@@ -4,6 +4,8 @@ entry, and calls that raise unless they worked)."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import contextlib
8
+
7
9
  import struct
8
10
 
9
11
  from . import catalog, host as h, message as m, registry as reg
@@ -196,12 +198,20 @@ class Interface:
196
198
  raise UnsupportedRevision(f"{self.name} (fn {self.fn}) is revision {rev} on this probe; this client "
197
199
  f"speaks revision {self.REVISION} only")
198
200
 
199
- def _call(self, op: int, body: bytes = b"", *, locked: bool = True) -> m.Result:
200
- return self.host.call(self.fn, op, self.prefix + body, locked=locked)
201
+ def _expecting(self, ms: int):
202
+ """Host.expecting(ms) when the host has it (a stand-in host may not)."""
203
+ expecting = getattr(self.host, "expecting", None) if ms else None
204
+ return expecting(ms) if expecting else contextlib.nullcontext()
205
+
206
+ def _call(self, op: int, body: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
207
+ with self._expecting(expect_ms):
208
+ return self.host.call(self.fn, op, self.prefix + body, locked=locked)
201
209
 
202
- def _request(self, op: int, body: bytes = b"", *, locked: bool = True) -> m.Result:
203
- """Rejections raise; completed results of any outcome come back for the caller to decode."""
204
- return self.host.request(self.fn, op, self.prefix + body, locked=locked)
210
+ def _request(self, op: int, body: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
211
+ """Rejections raise; completed results of any outcome come back for the caller to decode. expect_ms: how
212
+ long it may take on the probe (Host.expecting)."""
213
+ with self._expecting(expect_ms):
214
+ return self.host.request(self.fn, op, self.prefix + body, locked=locked)
205
215
 
206
216
  def request(self, op: int, body: bytes = b"") -> tuple[int, int, bytes]:
207
217
  """The raw (fn, op, payload) of one operation, for Host.pipeline / pipeline_calls."""
oep_client/endpoint.py CHANGED
@@ -36,9 +36,11 @@ define:
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
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 with no good frame or 3 broken
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
40
41
  candidates within 1 s, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
41
- (rate -> BrokenRate): frames at such a rate break, from a size and in the directions given (`fake_serial` applies it).
42
+ (rate -> BrokenRate): frames at such a rate break, from a size and in the directions given, only both ways at once
43
+ (`duplex`), only every Nth (`every`) (`fake_serial` applies it).
42
44
 
43
45
  Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
44
46
  nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
@@ -316,11 +318,22 @@ class BrokenRate:
316
318
  min_size: int = 0
317
319
  to_host: bool = True
318
320
  to_probe: bool = True
321
+ duplex: bool = False # only while both ways carry such frames at once (a request of min_size and more comes in
322
+ # while an answer of min_size and more is still unread): the request breaks (to_probe),
323
+ # the unread answer breaks (to_host)
324
+ every: int = 1 # of the frames that would break, only every Nth does (1: all)
325
+ seen: int = 0 # the frames that would have broken so far (`every` counts these)
326
+
327
+ def hit(self) -> bool:
328
+ """One more frame that would break: True when this one does (`every`)."""
329
+ self.seen += 1
330
+ return self.seen % max(1, self.every) == 0
319
331
 
320
332
 
321
333
  PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
322
334
  OP_PORT_SPEED = reg.CORE.op["port_speed"]
323
335
  SPEED_STEP = reg.CORE.enum["port_speed_step"]
336
+ SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
324
337
  SPEED_BAD_MAX, SPEED_BAD_WINDOW_MS = 3, 1000 # committed: this many broken candidates within the window revert
325
338
  SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
326
339
 
@@ -472,7 +485,7 @@ class Endpoint:
472
485
  self.speed_rate = 0 # the rate it runs at
473
486
  self.speed_asked = 0 # the baud the try asked (the commit names it again)
474
487
  self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
475
- self.speed_idle_ms = 0 # committed: revert after this long with no good frame (0 never)
488
+ self.speed_idle_ms = 0 # committed: revert after this long with no good frame
476
489
  self.speed_good_ms = 0 # the last good frame on that port
477
490
  self.speed_bad: list[int] = [] # committed: the times of the recent broken candidates
478
491
  self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
@@ -1203,7 +1216,7 @@ class Endpoint:
1203
1216
  if self.speed_state == "base" or port != self.speed_port or baud != self.speed_asked:
1204
1217
  raise unavailable("wrong_state") # nothing tried, or another baud
1205
1218
  self.speed_state = "committed"
1206
- self.speed_idle_ms = idle_ms
1219
+ self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
1207
1220
  self.speed_good_ms = self.now()
1208
1221
  self.speed_bad.clear()
1209
1222
  return self._answer(struct.pack("<I", self.speed_rate), ignored)
@@ -1216,10 +1229,19 @@ class Endpoint:
1216
1229
  return self.speed_rate
1217
1230
  return self.port_speed_base or 115200
1218
1231
 
1219
- def breaks(self, port: int, size: int, to_host: bool) -> bool:
1220
- """The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`)."""
1232
+ def breaks(self, port: int, size: int, to_host: bool, duplex: bool = False) -> bool:
1233
+ """The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`). duplex: the other
1234
+ way carries a frame of the rate's min_size or more at the same time (a BrokenRate with `duplex` breaks only
1235
+ then)."""
1236
+ b = self.broken_rates.get(self.port_baud(port))
1237
+ if b is None or size < b.min_size or not (b.to_host if to_host else b.to_probe) or (b.duplex and not duplex):
1238
+ return False
1239
+ return b.hit()
1240
+
1241
+ def duplex_rate(self, port: int) -> BrokenRate | None:
1242
+ """The rate `port` runs at now when it breaks only both ways at once (fake_serial checks the unread answers)."""
1221
1243
  b = self.broken_rates.get(self.port_baud(port))
1222
- return b is not None and size >= b.min_size and (b.to_host if to_host else b.to_probe)
1244
+ return b if b is not None and b.duplex else None
1223
1245
 
1224
1246
  def speed_after_answer(self) -> None:
1225
1247
  """The answer that asked for a switch or a revert is out (at the old speed): now do it."""
@@ -1264,7 +1286,7 @@ class Endpoint:
1264
1286
  now = self.now()
1265
1287
  if self.speed_state == "try" and now >= self.speed_until_ms:
1266
1288
  self._speed_revert() # no commit within verify_ms
1267
- elif self.speed_state == "committed" and self.speed_idle_ms and now - self.speed_good_ms >= self.speed_idle_ms:
1289
+ elif self.speed_state == "committed" and now - self.speed_good_ms >= self.speed_idle_ms:
1268
1290
  self._speed_revert()
1269
1291
 
1270
1292
  def _page(self, tlvs: list[bytes], first: int) -> bytes:
oep_client/fake_serial.py CHANGED
@@ -17,7 +17,8 @@ bytes to send, or None to send nothing (fault injection for tests).
17
17
  port_speed (core §3.5): every closed candidate tells the endpoint whether it was a frame (`Endpoint.speed_frame`), a
18
18
  switch or revert a request asked for happens once its answer is queued (at the old speed), and the endpoint's
19
19
  `broken_rates` break frames at the port's rate now: a candidate from the host is then not a frame, an answer or push
20
- goes out with a spoiled CRC. `FakeSerialStream` is the port as a pyserial-shaped stream for an in-process host, with
20
+ goes out with a spoiled CRC (a `duplex` one only while a request of its size comes in with such an answer still
21
+ unread: the request, or that answer, breaks). `FakeSerialStream` is the port as a pyserial-shaped stream for an in-process host, with
21
22
  the host's own `baudrate`: while it differs from the probe's, every byte either way arrives garbled.
22
23
  """
23
24
 
@@ -40,6 +41,7 @@ class FakeSerialPort:
40
41
  self.frames: list[bytes] = [] # framed answers waiting to go out
41
42
  self.answers = 0
42
43
  self.garble: Callable[[bytes, int], bytes] | None = None # (frame, the rate it goes at) -> what arrives
44
+ self.spoiled: set[int] = set() # the waiting answers a duplex BrokenRate already broke
43
45
 
44
46
  def feed(self, data: bytes) -> None:
45
47
  now = self.ep.now()
@@ -65,8 +67,10 @@ class FakeSerialPort:
65
67
  self.cand = None
66
68
  if not body:
67
69
  return # 0x00 0x00: an empty frame
70
+ size = len(body) + 2
71
+ duplex = self._both_ways(size)
68
72
  try:
69
- if self.ep.breaks(self.index, len(body) + 2, to_host=False):
73
+ if self.ep.breaks(self.index, size, to_host=False, duplex=duplex):
70
74
  raise cobs.CorruptFrame("the line's rate breaks this frame")
71
75
  msg = cobs.unframe(body)
72
76
  except cobs.CorruptFrame:
@@ -90,6 +94,22 @@ class FakeSerialPort:
90
94
  self.frames.append(self._line(wire))
91
95
  self.ep.speed_after_answer() # port_speed: switch (or revert) now the answer is out
92
96
 
97
+ def _both_ways(self, size: int) -> bool:
98
+ """A request of `size` bytes came in: with a BrokenRate that breaks only both ways at once (`duplex`), whether
99
+ an answer of its min_size or more is still unread (both ways busy) - and then that answer breaks if the rate
100
+ breaks frames towards the host."""
101
+ b = self.ep.duplex_rate(self.index)
102
+ if b is None or size < b.min_size:
103
+ return False
104
+ big = [k for k, f in enumerate(self.frames) if len(f) >= b.min_size]
105
+ if not big:
106
+ return False
107
+ k = big[-1] # the answer on the line now (the latest); never spoiled twice
108
+ if k not in self.spoiled and self.ep.breaks(self.index, len(self.frames[k]), to_host=True, duplex=True):
109
+ self.frames[k] = _spoil(self.frames[k])
110
+ self.spoiled.add(k)
111
+ return True
112
+
93
113
  def _gap(self, now: int) -> None:
94
114
  if self.cand is not None and now - self.last_ms >= GAP_MS:
95
115
  raw, self.cand = bytes(self.cand), None
@@ -109,9 +129,7 @@ class FakeSerialPort:
109
129
  def _line(self, wire: bytes) -> bytes:
110
130
  """A framed message as the line delivers it at the port's rate now (`Endpoint.broken_rates`, `garble`)."""
111
131
  if self.ep.breaks(self.index, len(wire), to_host=True):
112
- body = bytearray(cobs.decode(wire[1:-1]))
113
- body[-1] ^= 0xFF # the CRC's high byte
114
- wire = b"\x00" + cobs.encode(bytes(body)) + b"\x00"
132
+ wire = _spoil(wire)
115
133
  return self.garble(wire, self.ep.port_baud(self.index)) if self.garble else wire
116
134
 
117
135
  def output(self, room: int = 4096) -> bytes:
@@ -119,6 +137,7 @@ class FakeSerialPort:
119
137
  out = bytearray()
120
138
  while self.frames and len(out) + len(self.frames[0]) <= max(room, len(self.frames[0])):
121
139
  out += self.frames.pop(0)
140
+ self.spoiled = {k - 1 for k in self.spoiled if k}
122
141
  if len(out) >= room:
123
142
  return bytes(out)
124
143
  while len(out) < room:
@@ -129,6 +148,16 @@ class FakeSerialPort:
129
148
  return bytes(out)
130
149
 
131
150
 
151
+ def _spoil(wire: bytes) -> bytes:
152
+ """A framed message with its CRC's high byte spoiled (a frame the line broke); bytes that are no frame as they are."""
153
+ try:
154
+ body = bytearray(cobs.decode(wire[1:-1]))
155
+ except cobs.CorruptFrame:
156
+ return wire
157
+ body[-1] ^= 0xFF
158
+ return b"\x00" + cobs.encode(bytes(body)) + b"\x00"
159
+
160
+
132
161
  def _garble(data: bytes) -> bytes:
133
162
  """Bytes sent at one rate and read at another: a different byte for every byte, 0x00s among them (so the reader
134
163
  sees broken candidates, not silence)."""
oep_client/fake_serve.py CHANGED
@@ -30,9 +30,10 @@ Options:
30
30
  "DUT RX" labels when the profile has them, else the first free channels); configure then works
31
31
  --uart-rx TEXT what arrives on that UART's RX every --every ms once it is configured (%d = a counter)
32
32
  --no-port-speed the profile's port_speed (core §3.5; esp32-v003 has it) off: the op is unknown_operation
33
- --broken-rate SPEC port_speed's line model: frames at RATE break (repeatable). SPEC is RATE[:MIN_SIZE][:in|out]:
34
- only frames of MIN_SIZE bytes and more on the wire (default every frame), only towards the
35
- host (in) or the probe (out) (default both). The fake cannot see the host's own rate (a pty,
33
+ --broken-rate SPEC port_speed's line model: frames at RATE break (repeatable). SPEC is
34
+ RATE[:MIN_SIZE][:in|out][:duplex][:everyN]: only frames of MIN_SIZE bytes and more on the wire
35
+ (default every frame), only towards the host (in) or the probe (out) (default both), only
36
+ while both ways carry such frames at once (duplex), only every Nth of them (everyN). The fake cannot see the host's own rate (a pty,
36
37
  TCP): only the probe's rate decides
37
38
  --run-hook SPEC what riscv-dm run does on every target: SPEC is module:function or path/file.py:function,
38
39
  called as function(target, pc, regs) -> (stopped, dpc, elapsed_us). `target` is the
@@ -83,7 +84,9 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
83
84
  rate, *rest = spec.split(":")
84
85
  size = next((int(x) for x in rest if x.isdigit()), 0)
85
86
  way = next((x for x in rest if x in ("in", "out")), None)
86
- ep.broken_rates[int(rate)] = endpoint.BrokenRate(size, to_host=way != "out", to_probe=way != "in")
87
+ every = next((int(x[5:]) for x in rest if x.startswith("every") and x[5:].isdigit()), 1)
88
+ ep.broken_rates[int(rate)] = endpoint.BrokenRate(size, to_host=way != "out", to_probe=way != "in",
89
+ duplex="duplex" in rest, every=every)
87
90
  for n in a.absent:
88
91
  ep.targets[(wire, ep.pairs[wire][n])].present = False
89
92
  items = []
oep_client/host.py CHANGED
@@ -15,6 +15,7 @@ opens again (host guide §2.5).
15
15
 
16
16
  from __future__ import annotations
17
17
 
18
+ import contextlib
18
19
  import random
19
20
  import struct
20
21
  from dataclasses import dataclass, field
@@ -194,14 +195,32 @@ class Host:
194
195
  _revisions: dict = field(default_factory=dict) # fn -> interface revision from list
195
196
  _describes: dict = field(default_factory=dict) # fn -> its describe TLVs (declarations: valid for one boot_id)
196
197
  _boot_id: int | None = None
198
+ expect_ms: int = 0 # how long the request going out now may take on the probe (`expecting`)
199
+ # Called before a request takes its corr (the link's keepalive at a raised port_speed rate): what it sends first
200
+ # must carry the lower corr, or the probe takes the request for an old one (core §4.1)
201
+ before_request: Callable[[], None] | None = None
202
+
203
+ @contextlib.contextmanager
204
+ def expecting(self, ms: int):
205
+ """Requests sent inside take up to `ms` on the probe (a run's timeout_ms, a dmi list's waits, an attach's
206
+ hold_ms, a capture's blocking, a save; core §6.1: the probe does not count the lease meanwhile). The link waits
207
+ at least that and a margin for each answer - also at a raised port_speed rate, where an ordinary request waits
208
+ a quarter of the lease. Nested: the longest holds."""
209
+ saved = self.expect_ms
210
+ self.expect_ms = max(saved, int(ms or 0))
211
+ try:
212
+ yield
213
+ finally:
214
+ self.expect_ms = saved
197
215
 
198
216
  def next_corr(self) -> int:
199
217
  self._corr = self._corr % 0xFFFF + 1
200
218
  return self._corr
201
219
 
202
- def call(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True) -> m.Result:
220
+ def call(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
203
221
  """request() that also raises Failed unless the probe says it worked: what every operation wants."""
204
- r = self.request(fn, op, payload, locked=locked)
222
+ with self.expecting(expect_ms):
223
+ r = self.request(fn, op, payload, locked=locked)
205
224
  if not r.succeeded:
206
225
  raise Failed(r)
207
226
  return r
@@ -212,11 +231,15 @@ class Host:
212
231
  self.require_v1()
213
232
  return self.session
214
233
 
215
- def request(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True) -> m.Result:
234
+ def request(self, fn: int, op: int, payload: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
216
235
  """locked=True sends the session id (role 0x81) once a session is open; lock-free requests may leave it off.
217
- Rejections raise; any other answer is returned (Result.succeeded / .ran say what it was)."""
236
+ expect_ms: how long this request may take on the probe (`expecting`). Rejections raise; any other answer is
237
+ returned (Result.succeeded / .ran say what it was)."""
238
+ if self.before_request is not None:
239
+ self.before_request()
218
240
  req = m.Request(self.next_corr(), fn, op, payload, self._session_for(locked))
219
- result = m.Result.unpack(self.send(req.pack()))
241
+ with self.expecting(expect_ms):
242
+ result = m.Result.unpack(self.send(req.pack()))
220
243
  if result.corr != req.corr:
221
244
  raise ProtocolError(f"result for correlation {result.corr}, expected {req.corr}")
222
245
  if result.resolution == m.REJECTED:
@@ -253,6 +276,8 @@ class Host:
253
276
  """Several requests in flight (`exchange` keeps the probe's in-flight and window limits); results in
254
277
  order, rejects NOT raised - the caller looks at each result. Without `exchange`, one at a time."""
255
278
  session = self._session_for(locked)
279
+ if self.before_request is not None:
280
+ self.before_request()
256
281
  reqs = [m.Request(self.next_corr(), fn, op, payload, session) for fn, op, payload in requests]
257
282
  packed = [r.pack() for r in reqs]
258
283
  exchange = exchange or self.exchange
oep_client/link.py CHANGED
@@ -23,9 +23,16 @@ throughput of a UART bridge, oep-spec docs/uart-speed-negotiation.ja.md §3b), w
23
23
  port_speed (oep-core §3.5, optional, opt-in): `raise_speed` (or `open_host(..., port_speed=[rates])`) asks a probe that
24
24
  declares it for faster rates on the UART bridge this host opened: try a rate, switch the port, verify it with a sized
25
25
  transfer both ways (link_source / link_sink with max_frame-sized frames) that counts broken frames and measures the
26
- throughput, then commit it, or revert and wait the probe out and re-confirm at the boot speed. The report stays on the
26
+ throughput, then both ways at once (the two interleaved, pipelined as the in/out passes did), then commit it, or revert and wait the probe out and re-confirm at the boot speed. The report stays on the
27
27
  link (`link.speed`). Once raised, a request whose answer never comes even after its resend sends the link back to the
28
- boot speed (the probe reverts by itself) and goes once more there: the link never wedges at a rate the probe left.
28
+ boot speed (the probe reverts by itself) and goes once more there: the link never wedges at a rate the probe left;
29
+ while raised each wait is at most a quarter of the lease, so this ends inside it. In use, STRIKE_MAX (3) broken frames
30
+ or resends within STRIKE_WINDOW_S (5 s) step down: port_speed revert at the raised rate, the boot speed, a confirm.
31
+ Either way the rate is not used again in that session (`link.speed.stepped_down`, `down_why`).
32
+ The probe also goes back after port_speed_idle_max_ms (3 s) with no good frame, so while raised the link sends a
33
+ keepalive before a request when it has been quiet for 1 s, and `keep_alive()` does the same for a caller that sits
34
+ idle for long. A host opening a serial port retries its first confirm for that maximum and a little (4 s in all): a
35
+ host that raised the speed and died leaves the probe at its rate until then.
29
36
  """
30
37
 
31
38
  from __future__ import annotations
@@ -55,6 +62,15 @@ class PortBusy(OSError):
55
62
 
56
63
 
57
64
  BASE_BAUD = 115200 # the boot speed of every reference UART bridge (the board's profile decides; core §3.5)
65
+ IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # a committed rate goes back after this with no good frame (§3.5)
66
+ KEEPALIVE_S = 1.0 # raised: a keepalive once the link has been quiet this long (well inside IDLE_MAX_MS)
67
+ OPEN_RETRY_S = IDLE_MAX_MS / 1000 + 1.0 # opening a serial port: confirm retried this long (a raised rate left over)
68
+ OPEN_TRY_S = 0.5 # each of those confirms waits this long (at most the link's timeout)
69
+ STRIKE_MAX, STRIKE_WINDOW_S = 3, 5.0 # raised, in use: this many broken frames / resends within the window step down
70
+ STEP_DOWN_WAIT_S = 0.2 # the step down's revert (step 2) at the raised rate waits this long, never sent again
71
+ RAISED_WAIT_MIN_S = 0.3 # raised, in use: each wait for an answer is a quarter of the lease, at least this
72
+ EXPECT_MARGIN_S = 0.5 # a request that may take long on the probe (Host.expecting): waited that long and this
73
+ LINK_ERRORS = (cobs.CorruptFrame, TimeoutError, FramingLost)
58
74
 
59
75
 
60
76
  def open_serial(port: str, baud: int = BASE_BAUD):
@@ -156,8 +172,19 @@ class SerialLink:
156
172
  self.baud = self.base_baud # the rate the host side runs at now
157
173
  self.speed: SpeedReport | None = None # port_speed: the last raise_speed's report (rate in force, KB/s)
158
174
  self.speed_lost = 0 # times a raised rate was found gone (back to the boot speed)
159
- self.fallback = True # a raised rate that stops answering: back to the boot speed
175
+ self.fallback = True # a raised rate in use (not raise_speed's own trial): fall back
176
+ self.strikes: list[float] = [] # raised, in use: when frames broke / requests went again (window)
177
+ self.step_due = "" # raised, in use: why the link steps down at the next safe point
178
+ self.speed_port: int | None = None # the transport index the raised rate is on (the revert names it)
179
+ self.unusable: dict[int, str] = {} # rates stepped down from in this session -> why (not tried again)
180
+ self.unusable_session: int | None = None # the session `unusable` belongs to
181
+ self.session_frame = None # (op, payload) -> a core request in the session, once bound
182
+ self.lease_s = lambda: None # the session's lease, once bound (raised: bounds every wait)
183
+ self.expected_s = lambda: 0.0 # how long the request going out may take on the probe (Host.expecting)
184
+ self._own = 0 # >0: the link's own short requests (confirms, keepalive, revert)
160
185
  self.corr_source = self._own_corr # the host's correlation counter once bound (open_host)
186
+ self.keepalive_frame = None # raised: a keepalive request in the session, once bound (attach_host)
187
+ self.last_tx = time.monotonic() # when the link last wrote (raised: quiet for KEEPALIVE_S = keepalive)
161
188
  self.held = lambda: False # a session holds the port (its raw transfer stopped): broken = resend
162
189
  self.blind = lambda: [] # the host's blind stops (unsubscribe / end) once bound
163
190
  self._corr_n = 0x8000
@@ -178,14 +205,15 @@ class SerialLink:
178
205
  self.frames.send_many(messages)
179
206
  else:
180
207
  self.stream.write(b"".join(cobs.frame(msg) for msg in messages))
208
+ self.last_tx = time.monotonic()
181
209
 
182
210
  def _recv(self) -> bytes:
183
211
  if self.framing == "length":
184
- reply = self.frames.recv(self.timeout)
212
+ reply = self.frames.recv(self._wait())
185
213
  if reply is None:
186
214
  raise TimeoutError("no result from the probe")
187
215
  return reply
188
- deadline = time.monotonic() + self.timeout
216
+ deadline = time.monotonic() + self._wait()
189
217
  while True:
190
218
  end = self._buf.find(0)
191
219
  if end >= 0:
@@ -248,19 +276,28 @@ class SerialLink:
248
276
  saved = self.timeout
249
277
  deadline = time.monotonic() + timeout
250
278
  n = 0
279
+ self._own += 1 # its reads wait what it says, whatever a caller expects
251
280
  try:
252
281
  while True:
282
+ if self._raised(): # a long pump at a raised rate keeps the line alive (core §3.5)
283
+ self.timeout = saved
284
+ self._keep_raised()
253
285
  self.timeout = max(0.0, deadline - time.monotonic())
286
+ if self._raised():
287
+ self.timeout = min(self.timeout, KEEPALIVE_S)
254
288
  try:
255
289
  frame = self._recv()
256
290
  except TimeoutError:
291
+ if self._raised() and time.monotonic() < deadline:
292
+ continue
257
293
  return n
258
294
  except FramingLost:
259
295
  self.timeout = saved
260
296
  self.resync()
261
297
  return n
262
- except cobs.CorruptFrame:
298
+ except cobs.CorruptFrame as e:
263
299
  self.corrupt += 1
300
+ self._strike(e)
264
301
  continue
265
302
  n += 1
266
303
  if not self._route(frame):
@@ -269,6 +306,8 @@ class SerialLink:
269
306
  return n
270
307
  finally:
271
308
  self.timeout = saved
309
+ self._own -= 1
310
+ self._step_down_if_due()
272
311
 
273
312
  def resync(self, tries: int = 3) -> None:
274
313
  """oep-core §5.1: read and discard until the input is quiet for 50 ms, then prove the link with a confirm (a read,
@@ -307,16 +346,18 @@ class SerialLink:
307
346
  self.resync()
308
347
 
309
348
  def send(self, message: bytes) -> bytes:
349
+ self._keep_raised()
310
350
  try:
311
351
  reply = self._send(message)
312
- except TimeoutError:
313
- if not self._speed_fallback():
352
+ except LINK_ERRORS as e:
353
+ if not self._speed_fallback(e):
314
354
  raise
315
355
  reply = self._send(message) # once more at the boot speed (the probe answers a repeat from what it kept)
316
356
  if self.baud != self.base_baud and self._reverts(message, reply):
317
357
  self.set_baud(self.base_baud) # the probe went back right after this answer (core §3.5)
318
358
  if self.speed is not None:
319
359
  self.speed.rate, self.speed.chosen = self.base_baud, None
360
+ self._step_down_if_due()
320
361
  return reply
321
362
 
322
363
  @staticmethod
@@ -339,6 +380,7 @@ class SerialLink:
339
380
  except (cobs.CorruptFrame, TimeoutError, FramingLost) as e:
340
381
  if isinstance(e, cobs.CorruptFrame):
341
382
  self.corrupt += 1
383
+ self._strike(e)
342
384
  self._recover()
343
385
  if attempt or not self.resend or (message[0] & m.ROLE_SESSION and self.ended_blind):
344
386
  raise
@@ -355,22 +397,27 @@ class SerialLink:
355
397
  answered go once more with the same corr (oep-core §5.2: the probe answers a repeat from what it kept); a second
356
398
  failure is raised.
357
399
  """
400
+ self._keep_raised()
358
401
  replies: list[bytes] = []
359
402
  try:
360
- return self._exchange_once(messages, max_inflight, window_bytes, replies)
361
- except (cobs.CorruptFrame, TimeoutError, FramingLost):
403
+ out = self._exchange_once(messages, max_inflight, window_bytes, replies)
404
+ except LINK_ERRORS as e:
405
+ self._strike(e)
362
406
  if self.ended_blind or not self.resend:
363
407
  raise
364
408
  self.retries += 1
365
409
  rest = messages[len(replies):]
366
410
  more: list[bytes] = []
367
411
  try:
368
- return replies + self._exchange_once(rest, max_inflight, window_bytes, more)
369
- except TimeoutError:
370
- if not self._speed_fallback():
412
+ out = replies + self._exchange_once(rest, max_inflight, window_bytes, more)
413
+ except LINK_ERRORS as e2:
414
+ self._strike(e2)
415
+ if not self._speed_fallback(e2):
371
416
  raise
372
417
  rest = rest[len(more):]
373
- return replies + more + self._exchange_once(rest, max_inflight, window_bytes, [])
418
+ out = replies + more + self._exchange_once(rest, max_inflight, window_bytes, [])
419
+ self._step_down_if_due()
420
+ return out
374
421
 
375
422
  # ---- port_speed (core §3.5) ------------------------------------------------------------------
376
423
  def set_baud(self, rate: int) -> None:
@@ -389,6 +436,7 @@ class SerialLink:
389
436
  self._write([m.Request(corr, m.CORE_FN, m.OP_CONFIRM, m.CONFIRM_REQUEST + bytes([0, 0xFF])).pack()])
390
437
  saved = self.timeout
391
438
  self.timeout = timeout
439
+ self._own += 1
392
440
  try:
393
441
  self._recv_for(corr)
394
442
  return True
@@ -396,6 +444,16 @@ class SerialLink:
396
444
  return False
397
445
  finally:
398
446
  self.timeout = saved
447
+ self._own -= 1
448
+
449
+ def _confirm_within(self, wait_s: float, each_s: float) -> bool:
450
+ """Confirms, each waiting `each_s`, until one is answered (True) or `wait_s` has passed (False)."""
451
+ deadline = time.monotonic() + wait_s
452
+ while True:
453
+ if self.confirm_raw(each_s):
454
+ return True
455
+ if time.monotonic() >= deadline:
456
+ return False
399
457
 
400
458
  def back_to_base(self, wait_s: float = 3.0) -> bool:
401
459
  """The host at the boot speed again, and the probe confirmed there: confirms every 0.25 s up to `wait_s` (a
@@ -404,25 +462,119 @@ class SerialLink:
404
462
  if self.base_baud is None:
405
463
  return False
406
464
  self.set_baud(self.base_baud)
407
- deadline = time.monotonic() + wait_s
408
- while True:
409
- if self.confirm_raw(0.25):
410
- return True
411
- if time.monotonic() >= deadline:
412
- return False
465
+ return self._confirm_within(wait_s, 0.25)
466
+
467
+ def _raised(self) -> bool:
468
+ return self.base_baud is not None and self.baud != self.base_baud
469
+
470
+ def keep_alive(self) -> bool:
471
+ """While a raised rate is in force and a session holds the port: a keepalive when the link has been quiet for
472
+ KEEPALIVE_S (1 s). The probe goes back to the boot speed after port_speed_idle_max_ms (3 s) with no good frame
473
+ (core §3.5); every request already does this before it goes out, so only a caller that sits idle for long
474
+ (waiting on a person, a sleep between requests) calls it - often is fine, it sends nothing otherwise.
475
+ True when a keepalive went out."""
476
+ if not self._raised() or self.keepalive_frame is None or not self.held():
477
+ return False
478
+ if time.monotonic() - self.last_tx < KEEPALIVE_S:
479
+ return False
480
+ self.last_tx = time.monotonic() # before sending: send() asks again and must not recurse
481
+ self._own += 1
482
+ try:
483
+ self.send(self.keepalive_frame())
484
+ finally:
485
+ self._own -= 1
486
+ return True
487
+
488
+ def _keep_raised(self) -> None:
489
+ """keep_alive before a request; a keepalive that fails is left to the request itself to find out."""
490
+ try:
491
+ self.keep_alive()
492
+ except (TimeoutError, cobs.CorruptFrame, FramingLost):
493
+ pass
494
+
495
+ def _in_use(self) -> bool:
496
+ """A raised rate in force outside raise_speed's own trial (where every failure is handled there)."""
497
+ return self.fallback and self._raised()
498
+
499
+ def _wait(self) -> float:
500
+ """How long one answer is waited for: the link's timeout; raised and in use, at most a quarter of the
501
+ session's lease (at least RAISED_WAIT_MIN_S) - a probe that went back by itself (broken candidates, core §3.5
502
+ item 5) hears nothing at the raised rate, and the fallback (both waits, the confirm at the boot speed, the
503
+ request again there) must end well inside the lease. A request that may take longer on the probe
504
+ (Host.expecting: a run's timeout_ms, a dmi list's waits, ...; the probe does not count the lease meanwhile,
505
+ core §6.1) waits at least that and EXPECT_MARGIN_S, at any rate; the link's own requests never do."""
506
+ wait = self.timeout
507
+ if self._in_use():
508
+ lease = self.lease_s()
509
+ if lease is not None:
510
+ wait = min(self.timeout, max(RAISED_WAIT_MIN_S, lease / 4))
511
+ expected = 0.0 if self._own else self.expected_s()
512
+ return max(wait, expected + EXPECT_MARGIN_S) if expected > 0 else wait
513
+
514
+ def _strike(self, e: Exception) -> None:
515
+ """Raised and in use: a frame broke or a request is sent again (`e`). STRIKE_MAX within STRIKE_WINDOW_S: the
516
+ link steps down at the next safe point (core §3.5 item 7)."""
517
+ if not self._in_use():
518
+ return
519
+ now = time.monotonic()
520
+ self.strikes = [t for t in self.strikes if now - t < STRIKE_WINDOW_S] + [now]
521
+ if len(self.strikes) >= STRIKE_MAX and not self.step_due:
522
+ self.step_due = (f"{len(self.strikes)} broken frames / resends within {STRIKE_WINDOW_S:g} s at {self.baud} "
523
+ f"(the last: {type(e).__name__})")
524
+
525
+ def _step_down_if_due(self) -> None:
526
+ if self.step_due and self._in_use():
527
+ self._step_down(self.step_due)
528
+
529
+ def _step_down(self, why: str) -> None:
530
+ """Leave the raised rate for the rest of the session: port_speed step 2 at it (STEP_DOWN_WAIT_S, never sent
531
+ again: a probe that already went back cannot hear it, and a committed one reverts at the broken candidates
532
+ the confirms make), the host at the boot speed, a confirm there. ConnectionError when none is answered."""
533
+ rate = self.baud
534
+ self.step_due = ""
535
+ self.strikes.clear()
536
+ if self.session_frame is not None and self.speed_port is not None:
537
+ saved = self.timeout, self.resend, self.fallback
538
+ self.timeout, self.resend, self.fallback = STEP_DOWN_WAIT_S, False, False
539
+ self._own += 1
540
+ try:
541
+ self._send(self.session_frame(OP_PORT_SPEED, struct.pack("<BIBHI", self.speed_port, 0,
542
+ SPEED_STEP["revert"], 0, 0)))
543
+ except LINK_ERRORS:
544
+ pass
545
+ finally:
546
+ self.timeout, self.resend, self.fallback = saved
547
+ self._own -= 1
548
+ if not self.back_to_base():
549
+ raise ConnectionError(f"the probe answers neither at {rate} nor at the boot speed {self.base_baud}")
550
+ self._stepped(rate, why)
413
551
 
414
- def _speed_fallback(self) -> bool:
415
- """A request went unanswered (its resend too) while the link ran above the boot speed: the probe went back
416
- (idle_ms, broken candidates, a lapse). Back to the boot speed, confirmed: True = send again there."""
417
- if not self.fallback or self.base_baud is None or self.baud == self.base_baud:
552
+ def _stepped(self, rate: int, why: str) -> None:
553
+ """`rate` is not used again in this session (raise_speed skips it), and the report says so."""
554
+ self.unusable[rate] = why
555
+ if self.speed is not None:
556
+ self.speed.rate, self.speed.chosen = self.base_baud, None
557
+ self.speed.stepped_down, self.speed.down_why = True, why
558
+
559
+ def _speed_fallback(self, e: Exception | None = None) -> bool:
560
+ """A request failed (its resend too) while a raised rate was in use. Broken frames (the probe still answers
561
+ at that rate) or a step down already due: step down (`_step_down`). No answer at all: the probe went back
562
+ (idle_ms, broken candidates, a lapse) - back to the boot speed, confirmed. Either way the rate is not used
563
+ again in this session. True = send again there."""
564
+ if not self._in_use() or self.base_baud is None:
418
565
  return False
566
+ rate = self.baud
567
+ if self.step_due or not isinstance(e, TimeoutError):
568
+ self._step_down(self.step_due or f"frames kept breaking at {rate} ({type(e).__name__})")
569
+ return True
419
570
  if not self.back_to_base():
420
571
  raise ConnectionError(f"the probe answers neither at {self.baud} nor at the boot speed {self.base_baud}")
421
572
  self.speed_lost += 1
573
+ self.step_due = ""
574
+ self.strikes.clear()
422
575
  if self.speed is not None:
423
- self.speed.rate = self.base_baud
424
- self.speed.chosen = None
425
576
  self.speed.lost = True
577
+ self._stepped(rate, f"no answer at {rate} (the probe went back by itself)")
426
578
  return True
427
579
 
428
580
  def _exchange_once(self, messages: list[bytes], max_inflight: int, window_bytes: int,
@@ -461,12 +613,27 @@ class SerialLink:
461
613
  self.corr_source = hst.next_corr
462
614
  self.blind = hst.blind_stop
463
615
  self.held = lambda: hst.session is not None
616
+ self.session_frame = lambda op, payload: m.Request(hst.next_corr(), m.CORE_FN, op, payload, hst.session).pack()
617
+ self.keepalive_frame = lambda: self.session_frame(m.OP_KEEPALIVE, b"")
618
+ self.lease_s = lambda: hst.lease_ms / 1000 if hst.session is not None and hst.lease_ms else None
619
+ hst.before_request = self._keep_raised # the keepalive before the request's corr is taken (core §4.1)
620
+ self.expected_s = lambda: hst.expect_ms / 1000
464
621
  hst.link = self
622
+ if self.framing == "cobs" and getattr(self, "transport", None) == "serial":
623
+ self.wait_boot_speed()
465
624
  limits = hst.confirm()
466
625
  hst.exchange = self.bind(limits)
467
626
  if self.framing == "length" and limits.get("max_frame"):
468
627
  self.frames.max_frame = limits["max_frame"]
469
628
 
629
+ def wait_boot_speed(self, wait_s: float | None = None) -> None:
630
+ """A serial port just opened: confirm at the boot speed, retried for OPEN_RETRY_S (port_speed_idle_max_ms and
631
+ a second; at least the link's timeout) - a host that raised the speed and died leaves the probe at that rate
632
+ until its idle limit runs out (core §3.5 item 6). TimeoutError when none was answered."""
633
+ wait_s = max(OPEN_RETRY_S, self.timeout) if wait_s is None else wait_s
634
+ if not self._confirm_within(wait_s, min(self.timeout, OPEN_TRY_S)):
635
+ raise TimeoutError(f"no answer to confirm at {self.baud} for {wait_s:.1f} s")
636
+
470
637
  def close(self) -> None:
471
638
  self.stream.close()
472
639
 
@@ -611,7 +778,7 @@ UART_BRIDGE = reg.CORE.enum["transport_kind"]["uart_bridge"]
611
778
  @dataclass
612
779
  class SpeedTrial:
613
780
  """One rate tried: what the probe said it runs at, the verify's bytes, KB/s (1000 B/s) and broken frames each way
614
- (in = probe to host, link_source; out = host to probe, link_sink), and whether it was committed (why not)."""
781
+ (in = probe to host, link_source; out = host to probe, link_sink; duplex = both at once), and whether it was committed (why not)."""
615
782
  rate: int
616
783
  actual: int | None = None
617
784
  in_bytes: int = 0
@@ -620,6 +787,9 @@ class SpeedTrial:
620
787
  out_kb_s: float | None = None
621
788
  broken_in: int = 0
622
789
  broken_out: int = 0
790
+ duplex_bytes: int = 0 # both ways at once (link_source and link_sink interleaved), at the in-flight that passed
791
+ duplex_kb_s: float | None = None
792
+ broken_duplex: int = 0
623
793
  committed: bool = False
624
794
  why: str = ""
625
795
  inflight: int = 0 # the requests kept in flight the verify passed with (0: none passed)
@@ -629,8 +799,10 @@ class SpeedTrial:
629
799
  class SpeedReport:
630
800
  """raise_speed's answer, kept as `link.speed`: the boot speed, the rate in force now (`rate`), the committed one
631
801
  (`chosen`, None: the boot speed), every trial in order, and why nothing was tried (`supported` False).
632
- `lost`: a raised rate was later found gone (the link went back to the boot speed). in_kb_s / out_kb_s: the chosen
633
- rate's measured throughput (None at the boot speed) - for budgeting a transfer."""
802
+ `lost`: a raised rate was later found gone (the link went back to the boot speed). `stepped_down`: in use, the
803
+ link left the raised rate for the rest of the session (frames kept breaking, or no answer came), `down_why` says
804
+ why. in_kb_s / out_kb_s / duplex_kb_s: the chosen rate's measured throughput (None at the boot speed) - for
805
+ budgeting a transfer."""
634
806
  base: int
635
807
  supported: bool
636
808
  rate: int
@@ -638,6 +810,8 @@ class SpeedReport:
638
810
  trials: list[SpeedTrial] = field(default_factory=list)
639
811
  why: str = ""
640
812
  lost: bool = False
813
+ stepped_down: bool = False # in use, the raised rate was left for the rest of the session (`down_why`)
814
+ down_why: str = ""
641
815
 
642
816
  def _chosen(self) -> SpeedTrial | None:
643
817
  return next((t for t in self.trials if t.committed), None) if self.chosen else None
@@ -652,15 +826,23 @@ class SpeedReport:
652
826
  t = self._chosen()
653
827
  return t.out_kb_s if t else None
654
828
 
829
+ @property
830
+ def duplex_kb_s(self) -> float | None:
831
+ t = self._chosen()
832
+ return t.duplex_kb_s if t else None
833
+
655
834
  def to_text(self) -> str:
656
835
  if not self.supported:
657
836
  return f"port_speed not supported: {self.why} (stays at {self.rate})\n"
658
- lines = [f"{'rate':>9} {'actual':>9} {'in KB/s':>8} {'out KB/s':>8} {'broken in/out':>13} result"]
837
+ lines = [f"{'rate':>9} {'actual':>9} {'in KB/s':>8} {'out KB/s':>8} {'duplex KB/s':>11} "
838
+ f"{'broken in/out/duplex':>20} result"]
659
839
  for t in self.trials:
660
- kb = lambda v: f"{v:8.1f}" if v is not None else f"{'-':>8}" # noqa: E731
840
+ kb = lambda v, w=8: f"{v:{w}.1f}" if v is not None else f"{'-':>{w}}" # noqa: E731
661
841
  lines.append(f"{t.rate:>9} {t.actual if t.actual else '-':>9} {kb(t.in_kb_s)} {kb(t.out_kb_s)} "
662
- f"{f'{t.broken_in}/{t.broken_out}':>13} "
842
+ f"{kb(t.duplex_kb_s, 11)} {f'{t.broken_in}/{t.broken_out}/{t.broken_duplex}':>20} "
663
843
  f"{(f'committed (in flight {t.inflight})' if t.committed else t.why)}")
844
+ if self.stepped_down:
845
+ lines.append(f"stepped down: {self.down_why} - the boot speed for the rest of the session")
664
846
  lines.append(f"in force: {self.rate}" + (" (raised)" if self.chosen else " (the boot speed)"))
665
847
  return "\n".join(lines) + "\n"
666
848
 
@@ -678,10 +860,12 @@ def _speed_port(hst) -> tuple[int | None, str]:
678
860
 
679
861
 
680
862
  def _verify(hst, lk: SerialLink, rate: int, trial: SpeedTrial, verify_bytes: int, verify_s: float,
681
- inflight: int | None = None) -> bool:
682
- """Both ways with max_frame-sized frames, pipelined as `inflight` (default: as the probe allows): up to half of
683
- verify_bytes or verify_s each (in first, then out). Stops at the first frame that breaks (lost, or its content
684
- wrong)."""
863
+ inflight: int | None = None, ways: tuple[str, ...] = ("in", "out"), duplex_s: float = 1.0,
864
+ duplex_frames: int = 64) -> bool:
865
+ """Max_frame-sized frames, pipelined as `inflight` (default: as the probe allows), for each of `ways`: "in"
866
+ (link_source) and "out" (link_sink) up to half of verify_bytes or verify_s each; "duplex" both interleaved
867
+ (link_source, link_sink, ...) for duplex_s and at least duplex_frames, so frames go both ways at once. Stops at
868
+ the first frame that breaks (lost, or its content wrong)."""
685
869
  limits = hst.limits or hst.confirm()
686
870
  max_frame, window = limits["max_frame"], limits["window"]
687
871
  inflight = max(1, inflight or limits["max_inflight"])
@@ -691,41 +875,48 @@ def _verify(hst, lk: SerialLink, rate: int, trial: SpeedTrial, verify_bytes: int
691
875
  saved = lk.timeout, lk.resend
692
876
  lk.timeout, lk.resend = max(0.3, 4 * wire * inflight + 0.1), False
693
877
  pattern = bytes(k & 0xFF for k in range(n_in))
878
+ sink_body = bytes((k * 7) & 0xFF for k in range(n_out))
879
+
880
+ def request(k: int, way: str) -> tuple[bytes, bool]:
881
+ source = way == "in" or (way == "duplex" and k % 2 == 0)
882
+ body, op = (struct.pack("<I", n_in), m.OP_LINK_SOURCE) if source else (sink_body, m.OP_LINK_SINK)
883
+ return m.Request(hst.next_corr(), m.CORE_FN, op, body).pack(), source
884
+
885
+ def more(way: str, moved: int, frames: int, t0: float) -> bool:
886
+ if way == "duplex":
887
+ return time.perf_counter() - t0 < duplex_s or frames < duplex_frames
888
+ return moved < verify_bytes / 2 and time.perf_counter() - t0 < verify_s / 2
889
+
694
890
  try:
695
- for way in ("in", "out"):
696
- moved, t0 = 0, time.perf_counter()
697
- while moved < verify_bytes / 2 and time.perf_counter() - t0 < verify_s / 2:
698
- if way == "in":
699
- body, op = struct.pack("<I", n_in), m.OP_LINK_SOURCE
700
- else:
701
- body, op = bytes((k * 7) & 0xFF for k in range(n_out)), m.OP_LINK_SINK
702
- msgs = [m.Request(hst.next_corr(), m.CORE_FN, op, body).pack() for _ in range(inflight * 2)]
891
+ for way in ways:
892
+ moved, frames, broken, t0 = 0, 0, 0, time.perf_counter()
893
+ while more(way, moved, frames, t0):
894
+ batch = [request(k, way) for k in range(inflight * 2)]
703
895
  replies: list[bytes] = []
704
896
  try:
705
- lk._exchange_once(msgs, inflight, window, replies)
897
+ lk._exchange_once([msg for msg, _ in batch], inflight, window, replies)
706
898
  except (cobs.CorruptFrame, TimeoutError, FramingLost):
707
899
  pass # the frames not answered are the broken ones
708
900
  good = 0
709
- for r in replies:
901
+ for r, (_, source) in zip(replies, batch):
710
902
  res = m.Result.unpack(r)
711
- if not (res.succeeded and (res.payload == pattern if way == "in"
903
+ if not (res.succeeded and (res.payload == pattern if source
712
904
  else res.payload[:4] == struct.pack("<I", n_out))):
713
905
  break
714
906
  good += 1
715
- moved += good * (n_in if way == "in" else n_out)
716
- if good == len(msgs):
717
- continue
718
- if way == "in":
719
- trial.broken_in += len(msgs) - good
720
- else:
721
- trial.broken_out += len(msgs) - good
722
- break
723
- seconds = max(time.perf_counter() - t0, 1e-6)
907
+ moved += n_in if source else n_out
908
+ frames += good
909
+ if good < len(batch):
910
+ broken = len(batch) - good
911
+ break
912
+ kb_s = moved / max(time.perf_counter() - t0, 1e-6) / 1000
724
913
  if way == "in":
725
- trial.in_bytes, trial.in_kb_s = moved, moved / seconds / 1000
914
+ trial.in_bytes, trial.in_kb_s, trial.broken_in = moved, kb_s, broken
915
+ elif way == "out":
916
+ trial.out_bytes, trial.out_kb_s, trial.broken_out = moved, kb_s, broken
726
917
  else:
727
- trial.out_bytes, trial.out_kb_s = moved, moved / seconds / 1000
728
- if trial.broken_in or trial.broken_out:
918
+ trial.duplex_bytes, trial.duplex_kb_s, trial.broken_duplex = moved, kb_s, broken
919
+ if broken:
729
920
  return False
730
921
  return True
731
922
  finally:
@@ -736,17 +927,23 @@ SWITCH_SETTLE_S = 0.02 # after a baud change, before the first byte at the new
736
927
 
737
928
 
738
929
  def raise_speed(hst, rates: list[int], *, verify_bytes: int = 32768, verify_s: float = 1.0,
739
- verify_ms: int | None = None, idle_ms: int = 0, port: int | None = None) -> SpeedReport:
930
+ verify_ms: int | None = None, idle_ms: int = IDLE_MAX_MS, port: int | None = None,
931
+ duplex_s: float = 1.0, duplex_frames: int = 64) -> SpeedReport:
740
932
  """port_speed (oep-core §3.5), opt-in: try `rates` in order on the UART bridge this host opened, and commit the
741
933
  first that passes. Each: try (answered at the speed now) -> the host switches -> verify both ways with
742
- max_frame-sized frames for verify_bytes or verify_s in all (broken frames counted, KB/s measured each way) ->
743
- commit at the new speed when nothing broke; else revert (step 2, at the new speed) and back to the boot speed,
934
+ max_frame-sized frames for verify_bytes or verify_s in all (broken frames counted, KB/s measured each way),
935
+ pipelined as the probe allows and, when that breaks, one at a time -> both ways at once at the in-flight that
936
+ passed (link_source and link_sink interleaved, duplex_s and at least duplex_frames; a broken frame fails the rate:
937
+ "broke both ways at once") -> commit at the new speed when nothing broke; else revert (step 2, at the new speed) and back to the boot speed,
744
938
  re-confirmed there (waiting out the probe's verify_ms when the revert was lost), and the next rate. A rate the
745
939
  probe's UART cannot make is skipped (unsupported). The session must be open (the rate lasts as long as it does).
746
940
 
747
941
  verify_ms: how long the probe waits for the commit (default: verify_s + 1.5 s, at most 65535). idle_ms: once
748
- committed, the probe reverts after this long with no good frame (0: never; it reverts at the session's end anyway).
749
- port: the transport index (default: the probe's first UART bridge). -> the report, also kept as `hst.link.speed`."""
942
+ committed, the probe reverts after this long with no good frame (default and at most port_speed_idle_max_ms,
943
+ 3000; 0 and anything longer mean that maximum). The link keeps the line alive meanwhile (`keep_alive`), and
944
+ steps down to the boot speed for the rest of the session when frames break or requests go again STRIKE_MAX times
945
+ within STRIKE_WINDOW_S, or an answer does not come at all (`report.stepped_down`); a rate stepped down from in
946
+ this session is not tried again. port: the transport index (default: the probe's first UART bridge). -> the report, also kept as `hst.link.speed`."""
750
947
  lk = getattr(hst, "link", None)
751
948
  base = getattr(lk, "base_baud", None)
752
949
  report = SpeedReport(base or 0, False, getattr(lk, "baud", None) or 0)
@@ -764,18 +961,26 @@ def raise_speed(hst, rates: list[int], *, verify_bytes: int = 32768, verify_s: f
764
961
  raise _host.OepError("raise_speed needs an open session (the rate lasts as long as the session)")
765
962
  port = where if port is None else port
766
963
  report.supported = True
767
- wait = (verify_ms if verify_ms is not None else min(65535, int(verify_s * 1000) + 1500))
964
+ wait = (verify_ms if verify_ms is not None
965
+ else min(65535, int((verify_s + duplex_s) * 1000) + 1500))
966
+ if lk.unusable_session != hst.session:
967
+ lk.unusable, lk.unusable_session = {}, hst.session
768
968
  lk.fallback = False # every failure here is handled here
969
+ idle_ms = idle_ms if 0 < idle_ms <= IDLE_MAX_MS else IDLE_MAX_MS
769
970
  try:
770
- return _raise(hst, lk, report, rates, port, base, wait, verify_bytes, verify_s, idle_ms)
971
+ return _raise(hst, lk, report, rates, port, base, wait, verify_bytes, verify_s, idle_ms, duplex_s, duplex_frames)
771
972
  finally:
772
973
  lk.fallback = True
773
974
 
774
975
 
775
- def _raise(hst, lk, report, rates, port, base, wait, verify_bytes, verify_s, idle_ms) -> SpeedReport:
976
+ def _raise(hst, lk, report, rates, port, base, wait, verify_bytes, verify_s, idle_ms, duplex_s,
977
+ duplex_frames) -> SpeedReport:
776
978
  for rate in rates:
777
979
  trial = SpeedTrial(rate)
778
980
  report.trials.append(trial)
981
+ if rate in lk.unusable:
982
+ trial.why = f"stepped down from earlier in this session ({lk.unusable[rate]})"
983
+ continue
779
984
  try:
780
985
  r = hst.call(m.CORE_FN, OP_PORT_SPEED, struct.pack("<BIBHI", port, rate, SPEED_STEP["try"], wait, 0))
781
986
  except TimeoutError:
@@ -802,24 +1007,35 @@ def _raise(hst, lk, report, rates, port, base, wait, verify_bytes, verify_s, idl
802
1007
  # 500 kbaud and up behind usbip, 2026-10-01) gets a second verify with one request at a time
803
1008
  full = max(1, (hst.limits or hst.confirm())["max_inflight"])
804
1009
  tries = [full] if full == 1 else [full, 1]
1010
+ broke = "frames broke"
805
1011
  for n in tries if ok else []:
806
- trial.broken_in = trial.broken_out = 0
807
1012
  ok = _verify(hst, lk, rate, trial, verify_bytes, verify_s, n)
808
1013
  if ok:
809
1014
  trial.inflight = n
810
1015
  break
811
1016
  any(lk.confirm_raw(0.2) for _ in range(3)) # the broken frames' leftovers read past
1017
+ if ok:
1018
+ # both ways at once at the in-flight that passed: a line that carries each way alone can still break
1019
+ # under sustained duplex use (a CH340 at 921600 broke a frame every ~0.5 s, 2026-10-01)
1020
+ ok = _verify(hst, lk, rate, trial, verify_bytes, verify_s, trial.inflight, ("duplex",), duplex_s,
1021
+ duplex_frames)
1022
+ if not ok:
1023
+ broke, trial.inflight = "broke both ways at once", 0
1024
+ any(lk.confirm_raw(0.2) for _ in range(3))
812
1025
  if ok:
813
1026
  try:
814
1027
  hst.call(m.CORE_FN, OP_PORT_SPEED, struct.pack("<BIBHI", port, rate, SPEED_STEP["commit"], 0, idle_ms))
815
1028
  trial.committed = True
816
1029
  report.rate, report.chosen = rate, rate
817
1030
  lk.inflight_cap = trial.inflight if trial.inflight < full else 0
1031
+ lk.speed_port = port
1032
+ lk.strikes.clear()
1033
+ lk.step_due = ""
818
1034
  return report
819
1035
  except (TimeoutError, _host.Rejected) as e:
820
1036
  trial.why = f"the commit failed: {e}"
821
1037
  else:
822
- trial.why = "frames broke" if heard else "no confirm at the new rate"
1038
+ trial.why = broke if heard else "no confirm at the new rate"
823
1039
  saved = lk.timeout, lk.resend
824
1040
  lk.timeout, lk.resend = 0.3, False
825
1041
  try:
oep_client/registry.py CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  from types import SimpleNamespace as _NS
4
4
 
5
- REGISTRY_HASH = "1043a67bfcb96a43"
5
+ REGISTRY_HASH = "f98b4992f5b8ebe7"
6
6
  SCHEMA = 1
7
7
  PROTOCOL_REVISION = 1
8
8
  CONFIRM_REQUEST_MAGIC = 'OEP?'
@@ -20,7 +20,7 @@ OUTCOMES = {"success": 0x00, "failed": 0x01, "partial": 0x02}
20
20
  REJECT_REASONS = {"unknown_function": 0x01, "unknown_operation": 0x02, "malformed": 0x03, "unavailable": 0x04, "busy": 0x05, "window_exceeded": 0x06, "no_session": 0x07, "locked": 0x08, "session_required": 0x09, "no_connection": 0x0A, "unsupported": 0x0B, "result_lost": 0x0C, "corr_reused": 0x0D, "expired": 0x0E}
21
21
  STATUS = {"ok": 0x00, "wait": 0x01, "line": 0x02, "fault": 0x03, "timeout": 0x04, "state": 0x05}
22
22
  DESCRIBE_COMMON = {"role_channels": 0x01, "max_clock_hz": 0x02, "max_length": 0x03, "min_clock_hz": 0x05, "features": 0x06, "implementation": 0x07, "channel_group": 0x08}
23
- TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause_max_ms": 0x64, "heartbeat_default_ms": 0x3E8}
23
+ TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause_max_ms": 0x64, "heartbeat_default_ms": 0x3E8, "port_speed_idle_max_ms": 0xBB8}
24
24
  USB = {"iproduct_prefix": 'OEP', "vendor_bulk_class": 0xFF, "vendor_bulk_subclass": 0x4F, "vendor_bulk_protocol": 0x45, "hid_usage_page": 0xFF4F, "hid_usage": 0x45, "reference_vid": 0x1209, "reference_pid": 0x4F45}
25
25
  LIMITS = {"lease_min_ms": 0x3E8, "lease_max_ms": 0xEA60, "owner_max_bytes": 0x20, "unit_id_max_bytes": 0x20, "slot_name_max_bytes": 0x20, "interface_name_max_bytes": 0x40, "max_op_ms_reference": 0x2710}
26
26
  COMMON = _NS(enum={"read_from": {"position": 0x00, "oldest": 0x01, "now": 0x02, "last_mark": 0x03}, "read_flags": {"more": 0x01, "gap": 0x02}, "mark_kind": {"reset": 0x01, "restart": 0x02, "attach": 0x03, "detach": 0x04, "lost": 0x05, "clear": 0x06, "host": 0x07, "link_lost": 0x08, "closed": 0x09}, "mark_detail_reset": {"ndmreset": 0x01, "nrst": 0x02, "attach_reset": 0x03}, "mark_detail_restart": {"havereset": 0x01, "resync": 0x02}, "mark_detail_lost": {"overflow": 0x01, "framing": 0x02, "parity": 0x03, "target_timeout": 0x04}, "mark_detail_closed": {"all_released": 0x01, "expired": 0x02, "slot_changed": 0x03, "connection_closed": 0x04}})
oep_client/riscv.py CHANGED
@@ -238,7 +238,8 @@ class Wire(WireBase):
238
238
  low for hold_ms, then attach - halting before the first instruction with halt=True - the way back from firmware
239
239
  that turns the debug pins into GPIOs. self.target_id: (scheme, value) of the target's identity when the probe
240
240
  could read one."""
241
- rd = m.Reader(self._call(self.ATTACH, self.attach_body(halt, max_speed, pins, idle_clock, reset)).payload)
241
+ rd = m.Reader(self._call(self.ATTACH, self.attach_body(halt, max_speed, pins, idle_clock, reset),
242
+ expect_ms=reset[1] if reset else 0).payload)
242
243
  conn, status, self.flags, self.speed_hz = rd.take("HIBI")
243
244
  self.had_reset = bool(self.flags & self.FLAGS["havereset_acked"])
244
245
  self.existing = bool(self.flags & self.FLAGS["existing"])
@@ -336,6 +337,22 @@ def count_steps(steps: bytes) -> list[int]:
336
337
  return kinds
337
338
 
338
339
 
340
+ def dmi_wait_ms(raw: bytes) -> int:
341
+ """The time a step list may take by its waits and time-bounded polls (wait_us, poll_us), in ms (rounded up)."""
342
+ us, at = 0, 0
343
+ while at < len(raw):
344
+ kind = raw[at]
345
+ if kind == STEP_WAIT_US:
346
+ us += struct.unpack_from("<I", raw, at + 1)[0]
347
+ elif kind == STEP_POLL_US:
348
+ us += struct.unpack_from("<I", raw, at + 10)[0]
349
+ size = STEP_SIZES.get(kind)
350
+ if size is None:
351
+ break
352
+ at += size
353
+ return -(-us // 1000)
354
+
355
+
339
356
  def dmi_value_count(kinds: list[int], done: int, status: int) -> int:
340
357
  """§5.5: the reads and polls among the first `done` steps, plus the failed step's last value when it is a poll
341
358
  that timed out (a poll whose read failed on the line adds nothing)."""
@@ -483,7 +500,7 @@ class RiscvDm(Interface):
483
500
  it). timeout_ms None: the probe's max_op_ms (core §7.5), the most it allows. Other statuses raise TargetError."""
484
501
  if timeout_ms is None:
485
502
  timeout_ms = max_op_ms(self.host)
486
- r = self._request(self.RUN, self.run_body(pc, regs, timeout_ms, outs))
503
+ r = self._request(self.RUN, self.run_body(pc, regs, timeout_ms, outs), expect_ms=timeout_ms)
487
504
  res = self.run_result(r)
488
505
  if res.status == STATUS["timeout"] and not res.stopped:
489
506
  return res
@@ -496,7 +513,7 @@ class RiscvDm(Interface):
496
513
  raises StepListError (with what it did read): a caller cannot mistake an unfinished poll for a met one."""
497
514
  raw = b"".join(steps) if isinstance(steps, list) else steps
498
515
  kinds = count_steps(raw)
499
- r = self._request(self.DMI, struct.pack("<H", len(kinds)) + raw)
516
+ r = self._request(self.DMI, struct.pack("<H", len(kinds)) + raw, expect_ms=dmi_wait_ms(raw))
500
517
  rd = ran(r)
501
518
  done, status, nvals = rd.take("HBH")
502
519
  values = rd.words(nvals)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.24
3
+ Version: 0.0.26
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
5
5
  Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
6
  Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
@@ -122,15 +122,23 @@ print(hst.link.speed.to_text()) # every rate tried: actual, in / out
122
122
 
123
123
  Each rate in order: `try` (answered at the speed now, then the probe switches) -> the host switches -> a verify both ways
124
124
  with max_frame-sized frames (link_source / link_sink, pipelined) for `verify_bytes` or `verify_s`, counting broken frames
125
- and measuring KB/s each way -> `commit` when nothing broke, else revert and back to the boot speed, confirmed there (a
125
+ and measuring KB/s each way -> both ways at once at the in-flight that passed (link_source and link_sink interleaved,
126
+ about 1 s and at least 64 frames, `duplex_kb_s`; a line that carries each way alone can still break under duplex use - a
127
+ CH340 at 921600 did) -> `commit` when nothing broke, else revert and back to the boot speed, confirmed there (a
126
128
  probe whose revert was lost waits out its `verify_ms`). The first rate that passes is kept; a rate the probe's UART cannot
127
129
  make is skipped. Which rates pass depends on the bridge chip and its driver (an FTDI took only 3 MHz / n, a CH340 921600
128
130
  but not 1500000 towards the host: oep-spec docs/uart-speed-negotiation.ja.md), so the host chooses them. The report
129
131
  (`link.speed`: `rate`, `chosen`, `in_kb_s` / `out_kb_s`, `trials`) is there to budget a capture or a write.
130
132
 
131
133
  The probe goes back to the boot speed by itself when the session ends (`end`, a lapse, a force), when frames break or the
132
- line goes quiet (`idle_ms`): the link follows an `end` at once, and a request that goes unanswered at a raised rate (its
133
- resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges. A probe
134
+ line goes quiet (`idle_ms`, at most `port_speed_idle_max_ms` = 3 s; a host that died leaves the rate no longer than
135
+ that): while raised the link sends a keepalive before a request when it has been quiet for 1 s, and `hst.link.keep_alive()`
136
+ does the same for a caller that sits idle for long; opening a serial port retries its first confirm for about 4 s to
137
+ wait out a rate left over. The link follows an `end` at once, and a request that goes unanswered at a raised rate (its
138
+ resend too) takes the link back to the boot speed, confirmed, and goes once more there - it never wedges; while raised
139
+ each wait for an answer is at most a quarter of the lease, so this ends well inside it. In use, 3 broken frames or
140
+ resends within 5 s step the link down: port_speed revert at the raised rate, the boot speed, a confirm, the request
141
+ again there. A rate left either way is not used again in that session (`speed.stepped_down`, `speed.down_why`). A probe
134
142
  without the feature answers `not supported` and stays at its speed. Behind a broker (TCP) the broker does this, not the
135
143
  client. Serial ports are also opened in the driver's low-latency mode where it has one (an FTDI's latency timer 16 -> 1 ms
136
144
  tripled a UART bridge's throughput). `open_host(..., baud=)` names the boot speed when the board's profile is not 115200.
@@ -1,35 +1,35 @@
1
- oep_client/__init__.py,sha256=7PyV8fyij6tsSg9yAupMCbleZUUqr8ywU7_McgXM7UE,164
2
- oep_client/__main__.py,sha256=xrdBG4uBsJjYF5CLKcYmWdf392S-JUhyDGoOkUsk4R8,19682
3
- oep_client/arm.py,sha256=JGrevyxH8W-ke33rCv1_zyfekDDZrUv2kJhNRGyKFCA,12708
4
- oep_client/capture.py,sha256=goD0of-bClOK88PHR0PpWZ_XJqOWhOL9UVtVU0k3NLA,31994
1
+ oep_client/__init__.py,sha256=KqFhyYXLqtEQs_WvY0ZfnJTyG1JZxKIBKfe0wUuglJc,164
2
+ oep_client/__main__.py,sha256=em34bE60p_ut43LFGCRgxsofMOiTRL0wWiC-HGIKLVA,19852
3
+ oep_client/arm.py,sha256=X7xrKDOXkv3lFtc9CG6ls051xZxy8C3XUaGoIrJ2_nM,12744
4
+ oep_client/capture.py,sha256=R3ItIUjE6-Kqe3tjTVfYW8rKO4cYF-d480FAVbWr-SE,32148
5
5
  oep_client/catalog.py,sha256=m43MARRSJ5AwzNBMvRJUMAZxZc53t5eN-QaJ9049YHY,7016
6
6
  oep_client/ch32_flash.py,sha256=GxlpZY-o7pOjT6Yod9Gk0EflNtVKT_w_fXwkXBq5zrM,12641
7
7
  oep_client/cobs.py,sha256=hqddQI_X7rsBvBvcP_MBE3bXPyB13-Azjqp_9JfdQ1g,2288
8
- oep_client/config.py,sha256=MWzizoZ4qSCADsJaeWAubRm6HsIs6Sq__fO13tsrAuM,17544
8
+ oep_client/config.py,sha256=7KQxMhTklUhT9uzlj4rnT1xyXpJeFFfNNE4oIqeyMy0,17676
9
9
  oep_client/console.py,sha256=Srzlbto7-NZmkxUFMy_nUAanf9X0ZW1ib5tl6tX4rXU,9790
10
- oep_client/core.py,sha256=tbRV2eZNtGtQihNVthZytwVGkXFPAvSKamr2I0Uj3Bc,11525
10
+ oep_client/core.py,sha256=TooBzLBYcZ5-ymNJwcYYFSbbWDqkb7J6ccjedZpKe-0,12008
11
11
  oep_client/decode.py,sha256=6pTFVBT0MLAKedZLVKyd5yT8hjrRw4fdDjnoDiE9XRs,2380
12
12
  oep_client/dump.py,sha256=GAnmu2abuDmGmbWcbbEfC3EUuPp5Hady9vQVNcQ-724,6743
13
- oep_client/endpoint.py,sha256=dnYv8V7S89u1uSGL03Kg2UoTRRqv-cdEXwq8Ya1EQ4A,136740
13
+ oep_client/endpoint.py,sha256=k8O_izvyb05jNIyjlE5ILVhpCO0RaH9dhMUlGh6-5GM,138248
14
14
  oep_client/fake.py,sha256=LG56WG0RrFtmgfodVZhfLWvbojtNFWnk2lycjR6zhiE,19385
15
15
  oep_client/fake_capture.py,sha256=6oo-2byIBuZS0g5hk5KXHkqHNrO9PSRO-5MuetZvk80,26916
16
- oep_client/fake_serial.py,sha256=bPzQifttzfQ6SedZ11KBI3aiWuKbN2WXuBnVEqYJoQE,8381
17
- oep_client/fake_serve.py,sha256=n5GIwBDfu3lrxEIg_KN0I4uHIlaYszFUAdc9dO6HUVs,16200
16
+ oep_client/fake_serial.py,sha256=WEh-1WIboot1Z_Mq30S4Ny_ceWt1DTUJTjIl9giUdGQ,9831
17
+ oep_client/fake_serve.py,sha256=pwdJPcB8MnPpzoFWtxmLMpLZLJ3kFkzkWOgA_4fiRic,16524
18
18
  oep_client/fixture.py,sha256=sskFYFxI_7SOR_E5AxNf6tSYoUvoJOh0z_VjVdXcp6o,12527
19
19
  oep_client/frames.py,sha256=xjgjnXmXUqNJZ5m0h91r4Cy48WoVK1mEMW4gRg-mlMI,3991
20
20
  oep_client/hid_stream.py,sha256=99Dsm4Io1keukuETG_UNqjBGiVYZZeb__Pv4ytaKg9k,10695
21
- oep_client/host.py,sha256=3NiPV5rnruaC6PR48LOCEoMRpC6985PA8VpSnr7gB2A,20296
21
+ oep_client/host.py,sha256=rLSHJhQFgPo5e75Z8dNjDx7zRC8vebAGxbmSiFH0mWU,21700
22
22
  oep_client/interfaces.py,sha256=8HXr9iGJASIwBpTHBrCK_0b6NfbclEjCbBCsc62euzg,5693
23
- oep_client/link.py,sha256=0HS79DOse4xmUCWUXIshhebGInPLex-VdK6ztm_Jj34,42438
23
+ oep_client/link.py,sha256=1dlZTEfqV4md4jicl0yd1x1GOG1F67D5UhnmA8lRBbQ,56592
24
24
  oep_client/message.py,sha256=eQ-27lxN2Y9JadnUFpEmkEW2X7oucrKFFFs4_nnDClU,12322
25
25
  oep_client/names.py,sha256=ztCZFWs__aR-a5G14TnJP-Q4YfLhbyrzvOVOMJuG93k,3653
26
- oep_client/registry.py,sha256=vp-D_4W6KI7zSN2IgL-khcBD_qaR_TSrM3y5yhUsOnc,15693
27
- oep_client/riscv.py,sha256=vqd2aiLf2MaXcsGqh7Hsg51ng_OQi1DgI-vLZvHJ8VQ,29291
26
+ oep_client/registry.py,sha256=YEL_iMWFmqAaUY0zTkhs5CqbXBHBx4W8kjm37KPs190,15726
27
+ oep_client/riscv.py,sha256=245t-x1qM-wTTxM5OYNJz8c3oH0H8ORH6aexqcUE8oA,29941
28
28
  oep_client/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
29
29
  oep_client/uiapduino.py,sha256=kDB8C9GvIPBw94PaCKMzVWNtoQDu_axrz_n_6zbYJGU,6082
30
30
  oep_client/usb_stream.py,sha256=_KQvWq906DnQ4EjW1to7d9ufh3pnGkkkbCzti6GYVCY,11131
31
- oep_client_python-0.0.24.dist-info/METADATA,sha256=5KPnkaDN_mXTZySkNLXlBhK7s4lwPkcdaTWA0kfHJ-E,12869
32
- oep_client_python-0.0.24.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
33
- oep_client_python-0.0.24.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
34
- oep_client_python-0.0.24.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
35
- oep_client_python-0.0.24.dist-info/RECORD,,
31
+ oep_client_python-0.0.26.dist-info/METADATA,sha256=jVnIRVW010EUmGV9-b3drDiPRe84Vi4DZCsR6hswjOQ,13812
32
+ oep_client_python-0.0.26.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
33
+ oep_client_python-0.0.26.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
34
+ oep_client_python-0.0.26.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
35
+ oep_client_python-0.0.26.dist-info/RECORD,,