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 +1 -1
- oep_client/__main__.py +3 -1
- oep_client/arm.py +1 -1
- oep_client/capture.py +3 -2
- oep_client/config.py +3 -1
- oep_client/core.py +15 -5
- oep_client/endpoint.py +30 -8
- oep_client/fake_serial.py +34 -5
- oep_client/fake_serve.py +7 -4
- oep_client/host.py +30 -5
- oep_client/link.py +285 -69
- oep_client/registry.py +2 -2
- oep_client/riscv.py +20 -3
- {oep_client_python-0.0.24.dist-info → oep_client_python-0.0.26.dist-info}/METADATA +12 -4
- {oep_client_python-0.0.24.dist-info → oep_client_python-0.0.26.dist-info}/RECORD +18 -18
- {oep_client_python-0.0.24.dist-info → oep_client_python-0.0.26.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.24.dist-info → oep_client_python-0.0.26.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.24.dist-info → oep_client_python-0.0.26.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
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
|
|
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
|
|
200
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
|
34
|
-
only frames of MIN_SIZE bytes and more on the wire
|
|
35
|
-
host (in) or the probe (out) (default both)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
361
|
-
except
|
|
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
|
-
|
|
369
|
-
except
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
415
|
-
"""
|
|
416
|
-
|
|
417
|
-
if
|
|
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).
|
|
633
|
-
rate
|
|
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} {'
|
|
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:
|
|
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}':>
|
|
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)
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
|
696
|
-
moved, t0 = 0, time.perf_counter()
|
|
697
|
-
while moved
|
|
698
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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,
|
|
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.
|
|
728
|
-
if
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
749
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 = "
|
|
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)
|
|
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.
|
|
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 ->
|
|
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
|
|
133
|
-
|
|
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=
|
|
2
|
-
oep_client/__main__.py,sha256=
|
|
3
|
-
oep_client/arm.py,sha256=
|
|
4
|
-
oep_client/capture.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
17
|
-
oep_client/fake_serve.py,sha256=
|
|
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=
|
|
21
|
+
oep_client/host.py,sha256=rLSHJhQFgPo5e75Z8dNjDx7zRC8vebAGxbmSiFH0mWU,21700
|
|
22
22
|
oep_client/interfaces.py,sha256=8HXr9iGJASIwBpTHBrCK_0b6NfbclEjCbBCsc62euzg,5693
|
|
23
|
-
oep_client/link.py,sha256=
|
|
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=
|
|
27
|
-
oep_client/riscv.py,sha256=
|
|
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.
|
|
32
|
-
oep_client_python-0.0.
|
|
33
|
-
oep_client_python-0.0.
|
|
34
|
-
oep_client_python-0.0.
|
|
35
|
-
oep_client_python-0.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|