oep-client-python 0.0.23__py3-none-any.whl → 0.0.25__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 +38 -1
- oep_client/config.py +21 -5
- oep_client/core.py +3 -1
- oep_client/endpoint.py +232 -11
- oep_client/fake.py +3 -2
- oep_client/fake_serial.py +81 -2
- oep_client/fake_serve.py +14 -0
- oep_client/link.py +433 -11
- oep_client/registry.py +5 -5
- {oep_client_python-0.0.23.dist-info → oep_client_python-0.0.25.dist-info}/METADATA +34 -2
- {oep_client_python-0.0.23.dist-info → oep_client_python-0.0.25.dist-info}/RECORD +15 -15
- {oep_client_python-0.0.23.dist-info → oep_client_python-0.0.25.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.23.dist-info → oep_client_python-0.0.25.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.23.dist-info → oep_client_python-0.0.25.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
oep config bind <probe> --port 1 --mode last-reset --stream slot:x035
|
|
8
8
|
oep config plan <probe> oep.fixture.uart#1 rx=48 tx=49 (the fn's whole plan; fn number or name#instance)
|
|
9
9
|
oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
|
|
10
|
+
oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
|
|
10
11
|
oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
|
|
12
|
+
oep speed <probe> 1500000,921600,500000 (port_speed, core §3.5: try the rates in order on a UART bridge, report)
|
|
11
13
|
|
|
12
14
|
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
13
15
|
ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
|
|
@@ -28,6 +30,13 @@ def main(argv=None) -> int:
|
|
|
28
30
|
parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
|
|
29
31
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
30
32
|
_config_parser(sub)
|
|
33
|
+
sp = sub.add_parser("speed", help="port_speed (core §3.5): try faster rates on a UART bridge, print the report")
|
|
34
|
+
sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
|
|
35
|
+
sp.add_argument("rates", help="comma-separated, in order of preference (the first that passes is kept)")
|
|
36
|
+
sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
|
|
37
|
+
sp.add_argument("--verify-bytes", type=int, default=32768)
|
|
38
|
+
sp.add_argument("--verify-s", type=float, default=1.0)
|
|
39
|
+
sp.add_argument("--json", action="store_true")
|
|
31
40
|
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
32
41
|
src = d.add_mutually_exclusive_group(required=True)
|
|
33
42
|
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
@@ -38,6 +47,8 @@ def main(argv=None) -> int:
|
|
|
38
47
|
args = parser.parse_args(argv)
|
|
39
48
|
if args.command == "config":
|
|
40
49
|
return _config(args)
|
|
50
|
+
if args.command == "speed":
|
|
51
|
+
return _speed(args)
|
|
41
52
|
|
|
42
53
|
if args.fake:
|
|
43
54
|
call = fake.PROFILES[args.fake]().call
|
|
@@ -49,6 +60,26 @@ def main(argv=None) -> int:
|
|
|
49
60
|
return 0
|
|
50
61
|
|
|
51
62
|
|
|
63
|
+
# ---- oep speed ----------------------------------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _speed(args) -> int:
|
|
66
|
+
"""Take the lock, raise_speed, print the report; the session's end puts the port back at its boot speed."""
|
|
67
|
+
import dataclasses
|
|
68
|
+
rates = [int(r) for r in args.rates.replace(" ", "").split(",") if r]
|
|
69
|
+
hst = link.open_host(args.probe, baud=args.baud)
|
|
70
|
+
try:
|
|
71
|
+
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
|
+
if args.json:
|
|
74
|
+
sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
|
|
75
|
+
else:
|
|
76
|
+
sys.stdout.write(report.to_text())
|
|
77
|
+
hst.end()
|
|
78
|
+
finally:
|
|
79
|
+
hst.link.close()
|
|
80
|
+
return 0 if report.supported else 2
|
|
81
|
+
|
|
82
|
+
|
|
52
83
|
# ---- oep config ----------------------------------------------------------------------------------------------------
|
|
53
84
|
|
|
54
85
|
def _config_parser(sub) -> None:
|
|
@@ -98,13 +129,17 @@ def _config_parser(sub) -> None:
|
|
|
98
129
|
idle.add_argument("channel", type=int)
|
|
99
130
|
idle.add_argument("mode", choices=sorted(config.IDLE))
|
|
100
131
|
idle.add_argument("--save", action="store_true")
|
|
132
|
+
dis = cs.add_parser("disable", help="channels the probe never uses or touches (not on this board)")
|
|
133
|
+
dis.add_argument("probe")
|
|
134
|
+
dis.add_argument("channels", type=int, nargs="+")
|
|
135
|
+
dis.add_argument("--save", action="store_true")
|
|
101
136
|
uart = cs.add_parser("uart", help="a fixture UART's baud / format, applied whenever its plan has RX or TX")
|
|
102
137
|
uart.add_argument("probe")
|
|
103
138
|
uart.add_argument("fn", help="an fn, or name#instance of the oep.fixture.uart")
|
|
104
139
|
uart.add_argument("baud", type=int)
|
|
105
140
|
uart.add_argument("--format", default="8N1", help="data bits, parity, stop bits: 8N1 (default), 8E1, 7O2 ...")
|
|
106
141
|
uart.add_argument("--save", action="store_true")
|
|
107
|
-
rm = cs.add_parser("remove", help="remove one item (unset): slot N, bind PORT, plan FN, label CH, idle CH, uart FN")
|
|
142
|
+
rm = cs.add_parser("remove", help="remove one item (unset): slot N, bind PORT, plan FN, label CH, idle CH, uart FN, disable CH")
|
|
108
143
|
rm.add_argument("probe")
|
|
109
144
|
rm.add_argument("kind", choices=sorted(config.ITEM))
|
|
110
145
|
rm.add_argument("key", type=int)
|
|
@@ -235,6 +270,8 @@ def _config(args) -> int:
|
|
|
235
270
|
_change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
|
|
236
271
|
elif args.action == "idle":
|
|
237
272
|
_change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
|
|
273
|
+
elif args.action == "disable":
|
|
274
|
+
_change(hst, cfg, [config.Disable(channel=ch) for ch in args.channels], args.save)
|
|
238
275
|
elif args.action == "remove":
|
|
239
276
|
_change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
|
|
240
277
|
else:
|
oep_client/config.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""oep.probe.config revision 1 (oep-spec docs/oep-if-probe-config.ja.md): the probe's settings - plan, labels, idle
|
|
2
|
-
pins, slots, binds, fixture UART settings - read and set as items, removed with unset, saved when the host says so, and
|
|
2
|
+
pins, slots, binds, fixture UART settings, disabled channels - read and set as items, removed with unset, saved when the host says so, and
|
|
3
3
|
the live slot / bind / storage state as its own lock-free operation (describe is declarations only, core §7.3).
|
|
4
4
|
|
|
5
5
|
cfg = config.ProbeConfig(hst)
|
|
@@ -84,6 +84,20 @@ class Idle:
|
|
|
84
84
|
return struct.pack("<HB", self.channel, IDLE[self.mode])
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
@dataclass(kw_only=True)
|
|
88
|
+
class Disable:
|
|
89
|
+
"""A channel the probe never uses or touches (probe.config §1, item 0x07): not on this board, or wired to another
|
|
90
|
+
part. Any request naming it is refused unavailable (cause 5, held by settings); describe still declares it."""
|
|
91
|
+
channel: int
|
|
92
|
+
TAG = ITEM["disable"]
|
|
93
|
+
|
|
94
|
+
def key(self) -> tuple:
|
|
95
|
+
return (self.channel,)
|
|
96
|
+
|
|
97
|
+
def value(self) -> bytes:
|
|
98
|
+
return struct.pack("<H", self.channel)
|
|
99
|
+
|
|
100
|
+
|
|
87
101
|
@dataclass(kw_only=True)
|
|
88
102
|
class Slot:
|
|
89
103
|
"""A place a target is wired to (probe.config §1.1). pins: (swdio, swclk), swclk 0xFFFF on one wire (swio).
|
|
@@ -160,7 +174,7 @@ class Removal:
|
|
|
160
174
|
key: int
|
|
161
175
|
|
|
162
176
|
def encoded(self) -> bytes:
|
|
163
|
-
"""len(u8) tag(u8) key: the key is fn(u16) for plan / uart, channel(u16) for label / idle, slot(u8), port(u8)."""
|
|
177
|
+
"""len(u8) tag(u8) key: the key is fn(u16) for plan / uart, channel(u16) for label / idle / disable, slot(u8), port(u8)."""
|
|
164
178
|
tag = ITEM[self.kind]
|
|
165
179
|
key = bytes([self.key]) if self.kind in ("slot", "bind") else struct.pack("<H", self.key)
|
|
166
180
|
return bytes([1 + len(key), tag]) + key
|
|
@@ -172,7 +186,7 @@ def item(it) -> bytes:
|
|
|
172
186
|
|
|
173
187
|
def remove(kind: str, key: int) -> Removal:
|
|
174
188
|
"""The removal of the item of this key (for `unset`, or in a `set` list): kind plan (key fn: its whole plan), label
|
|
175
|
-
/ idle (channel), slot, bind (port), uart (fn)."""
|
|
189
|
+
/ idle / disable (channel), slot, bind (port), uart (fn)."""
|
|
176
190
|
if kind not in ITEM:
|
|
177
191
|
raise ValueError(f"no item kind {kind!r}")
|
|
178
192
|
return Removal(kind, key)
|
|
@@ -187,6 +201,8 @@ def decode(tag: int, v: bytes):
|
|
|
187
201
|
return Label(channel=struct.unpack_from("<H", v)[0], text=v[2:].decode("utf-8", "replace"))
|
|
188
202
|
if tag == ITEM["idle"] and len(v) >= 3:
|
|
189
203
|
return Idle(channel=struct.unpack_from("<H", v)[0], mode=_name(IDLE, v[2]))
|
|
204
|
+
if tag == ITEM["disable"] and len(v) >= 2:
|
|
205
|
+
return Disable(channel=struct.unpack_from("<H", v)[0])
|
|
190
206
|
if tag == ITEM["slot"] and len(v) >= SLOT_HEAD.size + 1:
|
|
191
207
|
n, wire_fn, swdio, swclk, attach, retry_ms, max_speed, idle, mech, name_len = SLOT_HEAD.unpack_from(v)
|
|
192
208
|
at = SLOT_HEAD.size
|
|
@@ -219,7 +235,7 @@ def decode(tag: int, v: bytes):
|
|
|
219
235
|
|
|
220
236
|
def _sort_key(tag: int, value: bytes) -> tuple:
|
|
221
237
|
"""The canonical order's key of one item (probe.config §2): tag, then the key - plan (fn, role, channel), label /
|
|
222
|
-
idle channel, slot, port, uart fn."""
|
|
238
|
+
idle / disable channel, slot, port, uart fn."""
|
|
223
239
|
if tag == ITEM["plan"] and len(value) >= 5:
|
|
224
240
|
return (tag,) + struct.unpack_from("<HBH", value)
|
|
225
241
|
if tag in (ITEM["slot"], ITEM["bind"]):
|
|
@@ -299,7 +315,7 @@ class ProbeConfig(Interface):
|
|
|
299
315
|
return h, out
|
|
300
316
|
|
|
301
317
|
def items(self) -> list:
|
|
302
|
-
"""The current settings, decoded (Plan, Label, Idle, Slot, Bind, Uart)."""
|
|
318
|
+
"""The current settings, decoded (Plan, Label, Idle, Slot, Bind, Uart, Disable)."""
|
|
303
319
|
return [decode(t, v) for t, v in self.get()[1]]
|
|
304
320
|
|
|
305
321
|
def set(self, items: list) -> int:
|
oep_client/core.py
CHANGED
|
@@ -140,7 +140,7 @@ def plan_apply(hst: h.Host, assignments: list[tuple[int, int, int]]) -> None:
|
|
|
140
140
|
|
|
141
141
|
class PinsTaken(h.Rejected):
|
|
142
142
|
"""plan_apply refused (unavailable) and the probe's settings say why: a pin kept by another fn's saved plan or by a
|
|
143
|
-
slot (oep.probe.config). `holders`: (channel, what holds it)."""
|
|
143
|
+
slot, or a channel the settings disable (oep.probe.config). `holders`: (channel, what holds it)."""
|
|
144
144
|
|
|
145
145
|
def __init__(self, result, holders: list[tuple[int, str]]):
|
|
146
146
|
super().__init__(result)
|
|
@@ -167,6 +167,8 @@ def _pin_holders(hst: h.Host, assignments: list[tuple[int, int, int]]) -> list[t
|
|
|
167
167
|
out.append((ch, f"the saved plan of fn {it.fn} (role {it.role})"))
|
|
168
168
|
elif isinstance(it, config.Slot) and ch in it.pins:
|
|
169
169
|
out.append((ch, f"slot {it.slot} {it.name}"))
|
|
170
|
+
elif isinstance(it, config.Disable) and it.channel == ch:
|
|
171
|
+
out.append((ch, "the settings (disabled; oep config remove <probe> disable <channel> enables it)"))
|
|
170
172
|
return out
|
|
171
173
|
|
|
172
174
|
|
oep_client/endpoint.py
CHANGED
|
@@ -28,10 +28,18 @@ define:
|
|
|
28
28
|
`FakeTarget` per pin pair (dmi / run answers count their values; run's stopped 2), oep.target.console streams (one
|
|
29
29
|
live stream per connection, lifetime by its users, the streams list), oep.fixture.gpio, oep.fixture.uart (its stream
|
|
30
30
|
made by the plan, status, the settings' uart item), and oep.probe.config (plan / label / idle / slot / bind / uart
|
|
31
|
-
items, get / set / unset / save / erase, the state op - describe is declarations only
|
|
31
|
+
/ disable items, get / set / unset / save / erase, the state op - describe is declarations only; a disabled channel
|
|
32
|
+
is refused everywhere with unavailable cause 5 and never parked: `parked` records the free pins' states it set)
|
|
32
33
|
- the serial ports' raw side (core §3.4, probe.config §1.2): `port_input` / `port_output` carry the bytes outside
|
|
33
34
|
the frames for each serial port by its bind; a port the lock holder's requests came in on is held until the
|
|
34
35
|
session ends, then resumes from the session's last host reset. The byte framing itself is `fake_serial`.
|
|
36
|
+
- port_speed (core §3.5, optional): on when the profile's describe declares it (`port_speed_base`, the boot speed;
|
|
37
|
+
None = off: the op is unknown_operation). try / commit / revert on the UART bridge the request came in on (else
|
|
38
|
+
unavailable cause 6), a rate the fake UART cannot make (outside 300..5000000) unsupported; a try reverts after
|
|
39
|
+
verify_ms without a commit or at a broken candidate, a commit after idle_ms (at most port_speed_idle_max_ms, 3000;
|
|
40
|
+
0 and anything longer count as that maximum) with no good frame or 3 broken
|
|
41
|
+
candidates within 1 s, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
|
|
42
|
+
(rate -> BrokenRate): frames at such a rate break, from a size and in the directions given (`fake_serial` applies it).
|
|
35
43
|
|
|
36
44
|
Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
|
|
37
45
|
nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
|
|
@@ -301,6 +309,24 @@ class Flow:
|
|
|
301
309
|
last_ms: int = 0
|
|
302
310
|
|
|
303
311
|
|
|
312
|
+
@dataclass
|
|
313
|
+
class BrokenRate:
|
|
314
|
+
"""How a line rate breaks frames in the fake (port_speed tests): frames of `min_size` bytes and more (on the wire)
|
|
315
|
+
break, towards the host and / or towards the probe. A broken frame towards the probe is a candidate whose CRC does
|
|
316
|
+
not match; towards the host its CRC is spoiled."""
|
|
317
|
+
min_size: int = 0
|
|
318
|
+
to_host: bool = True
|
|
319
|
+
to_probe: bool = True
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
|
|
323
|
+
OP_PORT_SPEED = reg.CORE.op["port_speed"]
|
|
324
|
+
SPEED_STEP = reg.CORE.enum["port_speed_step"]
|
|
325
|
+
SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
|
|
326
|
+
SPEED_BAD_MAX, SPEED_BAD_WINDOW_MS = 3, 1000 # committed: this many broken candidates within the window revert
|
|
327
|
+
SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
|
|
328
|
+
|
|
329
|
+
|
|
304
330
|
class Endpoint:
|
|
305
331
|
MARKS_PER_ANSWER = 3 # small, so hosts must follow `more`
|
|
306
332
|
CHUNK = 64 # raw bytes a serial port takes at a time (probe guide §3.6)
|
|
@@ -387,6 +413,10 @@ class Endpoint:
|
|
|
387
413
|
self.bind_modes = struct.unpack_from("<I", cfg[CFG_DESCRIBE["bind_modes"]])[0] if CFG_DESCRIBE["bind_modes"] in cfg else 0
|
|
388
414
|
self.console_accept = 64
|
|
389
415
|
self.uart_accept = 256
|
|
416
|
+
# port_speed (core §3.5): on when the profile declares it; the boot speed every revert goes back to
|
|
417
|
+
self.port_speed_base: int | None = 115200 if any(t[0] == PORT_SPEED_TAG for t in self.static.get(0, ())) else None
|
|
418
|
+
self.broken_rates: dict[int, BrokenRate] = {} # the line: rates that break frames (fake_serial applies it)
|
|
419
|
+
self._transport = 0 # the transport the request being handled came in on
|
|
390
420
|
self._boot()
|
|
391
421
|
|
|
392
422
|
def _boot(self) -> None:
|
|
@@ -438,8 +468,49 @@ class Endpoint:
|
|
|
438
468
|
self.mixed_out: dict[int, bytearray] = {}
|
|
439
469
|
self.held_ports: set[int] = set()
|
|
440
470
|
self.session_resets: dict[tuple[int, int], tuple[object, int]] = {} # stream key -> (sid, position)
|
|
471
|
+
self.parked: dict[int, int] = {} # channel -> the idle mode the probe put the free pin in
|
|
472
|
+
self.speed_state = "base" # port_speed: "base", "try" or "committed" (core §3.5)
|
|
473
|
+
self.speed_port: int | None = None # the port off its boot speed
|
|
474
|
+
self.speed_rate = 0 # the rate it runs at
|
|
475
|
+
self.speed_asked = 0 # the baud the try asked (the commit names it again)
|
|
476
|
+
self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
|
|
477
|
+
self.speed_idle_ms = 0 # committed: revert after this long with no good frame
|
|
478
|
+
self.speed_good_ms = 0 # the last good frame on that port
|
|
479
|
+
self.speed_bad: list[int] = [] # committed: the times of the recent broken candidates
|
|
480
|
+
self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
|
|
481
|
+
self.speed_log: list[tuple[int, int]] = [] # (port, rate) every switch, reverts included
|
|
441
482
|
if self.saved is not None:
|
|
442
|
-
self._apply_saved()
|
|
483
|
+
self._apply_saved() # the saved disable items first: those pins are never parked
|
|
484
|
+
self._park(self._all_channels()) # then the free pins' idle state (probe.config §2 boot order)
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def disabled(self) -> set[int]:
|
|
488
|
+
"""The channels the settings' disable items take away (probe.config §1): never used, driven or configured."""
|
|
489
|
+
return {key for tag, key in self.config if tag == ITEM["disable"]}
|
|
490
|
+
|
|
491
|
+
def _refuse_disabled(self, channels, extra: bytes = b"") -> None:
|
|
492
|
+
"""A request naming a disabled channel: rejected unavailable cause 5 (held by settings) with the channel."""
|
|
493
|
+
for ch in channels:
|
|
494
|
+
if ch != 0xFFFF and ch in self.disabled:
|
|
495
|
+
raise unavailable("held_by_settings", ch, holder_kind="disabled", extra=extra)
|
|
496
|
+
|
|
497
|
+
def _all_channels(self) -> set[int]:
|
|
498
|
+
"""Every channel some fn's describe offers (role_channels, channel_group, the wires' pairs and reset lines)."""
|
|
499
|
+
out = {ch for fn in self.static if fn != m.CORE_FN for ch in self._declared_channels(fn)}
|
|
500
|
+
out |= {p for pairs in self.pairs.values() for pair in pairs for p in pair}
|
|
501
|
+
out |= {ch for chs in self.reset_channels.values() for ch in chs}
|
|
502
|
+
out.discard(0xFFFF)
|
|
503
|
+
return out
|
|
504
|
+
|
|
505
|
+
def _park(self, channels) -> None:
|
|
506
|
+
"""Free pins go to their idle state (the idle item, else Hi-Z) at boot and whenever released (probe.config §1);
|
|
507
|
+
a disabled channel stays as the reset left it (never parked), a held one is not free."""
|
|
508
|
+
busy = self._held()
|
|
509
|
+
for ch in channels:
|
|
510
|
+
if ch == 0xFFFF or ch in self.disabled or ch in busy:
|
|
511
|
+
continue
|
|
512
|
+
item = self.config.get((ITEM["idle"], ch))
|
|
513
|
+
self.parked[ch] = item[2] if item else 0 # 0 = Hi-Z
|
|
443
514
|
|
|
444
515
|
@property
|
|
445
516
|
def target_id(self) -> int | None:
|
|
@@ -676,6 +747,7 @@ class Endpoint:
|
|
|
676
747
|
return None
|
|
677
748
|
req = m.Request.unpack(data)
|
|
678
749
|
self.requests.append(req)
|
|
750
|
+
self._transport = transport
|
|
679
751
|
remembered = self._resent(req)
|
|
680
752
|
if remembered is not None:
|
|
681
753
|
return remembered
|
|
@@ -696,6 +768,8 @@ class Endpoint:
|
|
|
696
768
|
out = m.Result(req.corr, res, detail, payload).pack()
|
|
697
769
|
if req.session is not None and req.session == self.last:
|
|
698
770
|
self._remember(req, out)
|
|
771
|
+
if transport not in self.serial_ports:
|
|
772
|
+
self.speed_after_answer() # the answer is not on a port whose speed changes
|
|
699
773
|
return out
|
|
700
774
|
|
|
701
775
|
def _interface(self, fn: int):
|
|
@@ -732,6 +806,8 @@ class Endpoint:
|
|
|
732
806
|
return self._open(req.payload)
|
|
733
807
|
if req.fn != m.CORE_FN and req.fn not in self.names:
|
|
734
808
|
return m.REJECTED, m.UNKNOWN_FUNCTION, b""
|
|
809
|
+
if req.fn == m.CORE_FN and req.op == OP_PORT_SPEED and self.port_speed_base is None:
|
|
810
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b"" # the optional feature off (core §3.5)
|
|
735
811
|
if not self._lock_free(req.fn, req.op):
|
|
736
812
|
refused = self._check(req.session)
|
|
737
813
|
if refused:
|
|
@@ -919,6 +995,8 @@ class Endpoint:
|
|
|
919
995
|
if op == m.OP_KEEPALIVE:
|
|
920
996
|
_, ignored = t.tail()
|
|
921
997
|
return self._answer(b"", ignored)
|
|
998
|
+
if op == OP_PORT_SPEED:
|
|
999
|
+
return self._port_speed(t)
|
|
922
1000
|
if op == m.OP_SUBSCRIBE:
|
|
923
1001
|
fn, _min_bytes, max_delay_ms = t.take("HHI") # max_delay_ms u32 (core §11.3)
|
|
924
1002
|
t.tail()
|
|
@@ -959,6 +1037,7 @@ class Endpoint:
|
|
|
959
1037
|
raise unavailable("held_by_settings", holder_fn=min(named & self.plan_from_config),
|
|
960
1038
|
holder_kind="settings_plan")
|
|
961
1039
|
self._check_plan(got)
|
|
1040
|
+
self._refuse_disabled(ch for _, _, ch in got) # a disabled channel (probe.config §1): cause 5
|
|
962
1041
|
if self.plan_roles is not None and len([a for a in self.plan if a[0] not in named]) + len(got) > self.plan_roles:
|
|
963
1042
|
raise unavailable("limit") # plan_roles (core §8)
|
|
964
1043
|
self.plan = {a for a in self.plan if a[0] not in named} | set(got)
|
|
@@ -1034,11 +1113,14 @@ class Endpoint:
|
|
|
1034
1113
|
return out
|
|
1035
1114
|
|
|
1036
1115
|
def _drop_plan(self, fn: int) -> None:
|
|
1116
|
+
released = set()
|
|
1037
1117
|
for a in [a for a in self.plan if a[0] == fn]:
|
|
1038
1118
|
self.gpio_modes.pop(a[2], None)
|
|
1039
1119
|
self.plan.discard(a)
|
|
1120
|
+
released.add(a[2])
|
|
1040
1121
|
self.plan_from_config.discard(fn)
|
|
1041
1122
|
self._uart_plan_changed(fn)
|
|
1123
|
+
self._park(released)
|
|
1042
1124
|
|
|
1043
1125
|
# ---- oep.fixture.uart's stream: made by the plan, gone with it (fixture §2) ---------------------
|
|
1044
1126
|
def _uart_plan_changed(self, fn: int) -> None:
|
|
@@ -1097,9 +1179,96 @@ class Endpoint:
|
|
|
1097
1179
|
# ---- describe: the static declarations plus the live ones -----------------------------------
|
|
1098
1180
|
def _declarations(self, fn: int) -> list[bytes]:
|
|
1099
1181
|
"""describe: the profile's declarations as they are (core §7.3: nothing that changes - the firmware's labels
|
|
1100
|
-
only, the settings' are read by get; probe.config's state is its op state).
|
|
1182
|
+
only, the settings' are read by get; probe.config's state is its op state). fn 0's port_speed follows
|
|
1183
|
+
`port_speed_base` (a test turns the feature off or on)."""
|
|
1184
|
+
if fn == m.CORE_FN:
|
|
1185
|
+
out = [t for t in self.static[fn] if t[0] != PORT_SPEED_TAG]
|
|
1186
|
+
return out + [catalog.u8(PORT_SPEED_TAG, 1)] if self.port_speed_base is not None else out
|
|
1101
1187
|
return list(self.static[fn])
|
|
1102
1188
|
|
|
1189
|
+
# ---- port_speed (core §3.5) -----------------------------------------------------------------
|
|
1190
|
+
def _port_speed(self, t: "Take") -> tuple[int, int, bytes]:
|
|
1191
|
+
"""port(u8) baud(u32) step(u8) verify_ms(u16) idle_ms(u32) [TLV] -> baud(u32): the rate that applies."""
|
|
1192
|
+
port, baud, step, verify_ms, idle_ms = t.take("BIBHI")
|
|
1193
|
+
_, ignored = t.tail()
|
|
1194
|
+
if step not in SPEED_STEP.values():
|
|
1195
|
+
raise unsupported_fixed()
|
|
1196
|
+
if port != self._transport or port >= len(self.transports) or \
|
|
1197
|
+
self.transports[port] != fake.TRANSPORT["uart_bridge"]:
|
|
1198
|
+
raise unavailable("wrong_state") # only the UART bridge the request came in on
|
|
1199
|
+
if step == SPEED_STEP["try"]:
|
|
1200
|
+
if not SPEED_RATES[0] <= baud <= SPEED_RATES[1]:
|
|
1201
|
+
raise unsupported_fixed() # a baud this UART cannot make
|
|
1202
|
+
self.speed_pending = ("switch", port, baud, verify_ms)
|
|
1203
|
+
return self._answer(struct.pack("<I", baud), ignored)
|
|
1204
|
+
if step == SPEED_STEP["commit"]:
|
|
1205
|
+
if self.speed_state == "base" or port != self.speed_port or baud != self.speed_asked:
|
|
1206
|
+
raise unavailable("wrong_state") # nothing tried, or another baud
|
|
1207
|
+
self.speed_state = "committed"
|
|
1208
|
+
self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
|
|
1209
|
+
self.speed_good_ms = self.now()
|
|
1210
|
+
self.speed_bad.clear()
|
|
1211
|
+
return self._answer(struct.pack("<I", self.speed_rate), ignored)
|
|
1212
|
+
self.speed_pending = ("revert",)
|
|
1213
|
+
return self._answer(struct.pack("<I", self.port_speed_base), ignored)
|
|
1214
|
+
|
|
1215
|
+
def port_baud(self, port: int) -> int:
|
|
1216
|
+
"""The rate serial port `port` runs at now (its boot speed, or what port_speed set)."""
|
|
1217
|
+
if self.speed_state != "base" and port == self.speed_port:
|
|
1218
|
+
return self.speed_rate
|
|
1219
|
+
return self.port_speed_base or 115200
|
|
1220
|
+
|
|
1221
|
+
def breaks(self, port: int, size: int, to_host: bool) -> bool:
|
|
1222
|
+
"""The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`)."""
|
|
1223
|
+
b = self.broken_rates.get(self.port_baud(port))
|
|
1224
|
+
return b is not None and size >= b.min_size and (b.to_host if to_host else b.to_probe)
|
|
1225
|
+
|
|
1226
|
+
def speed_after_answer(self) -> None:
|
|
1227
|
+
"""The answer that asked for a switch or a revert is out (at the old speed): now do it."""
|
|
1228
|
+
pending, self.speed_pending = self.speed_pending, None
|
|
1229
|
+
if pending is None:
|
|
1230
|
+
return
|
|
1231
|
+
if pending[0] == "revert":
|
|
1232
|
+
self._speed_revert()
|
|
1233
|
+
return
|
|
1234
|
+
_, port, baud, verify_ms = pending
|
|
1235
|
+
self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "try", port, baud, baud
|
|
1236
|
+
self.speed_until_ms = self.now() + verify_ms
|
|
1237
|
+
self.speed_good_ms = self.now()
|
|
1238
|
+
self.speed_bad.clear()
|
|
1239
|
+
self.speed_log.append((port, baud))
|
|
1240
|
+
|
|
1241
|
+
def _speed_revert(self) -> None:
|
|
1242
|
+
if self.speed_state == "base":
|
|
1243
|
+
return
|
|
1244
|
+
self.speed_log.append((self.speed_port, self.port_speed_base))
|
|
1245
|
+
self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "base", None, 0, 0
|
|
1246
|
+
self.speed_bad.clear()
|
|
1247
|
+
|
|
1248
|
+
def speed_frame(self, port: int, good: bool) -> None:
|
|
1249
|
+
"""A candidate closed on serial port `port`: a frame (good) or not (a broken candidate)."""
|
|
1250
|
+
if self.speed_state == "base" or port != self.speed_port:
|
|
1251
|
+
return
|
|
1252
|
+
if good:
|
|
1253
|
+
self.speed_good_ms = self.now()
|
|
1254
|
+
return
|
|
1255
|
+
if self.speed_state == "try":
|
|
1256
|
+
self._speed_revert() # the new speed breaks frames: not this one
|
|
1257
|
+
return
|
|
1258
|
+
now = self.now()
|
|
1259
|
+
self.speed_bad = [t for t in self.speed_bad if now - t < SPEED_BAD_WINDOW_MS] + [now]
|
|
1260
|
+
if len(self.speed_bad) >= SPEED_BAD_MAX:
|
|
1261
|
+
self._speed_revert()
|
|
1262
|
+
|
|
1263
|
+
def _speed_tick(self) -> None:
|
|
1264
|
+
if self.speed_pending and self.speed_pending[0] == "revert":
|
|
1265
|
+
self.speed_after_answer()
|
|
1266
|
+
now = self.now()
|
|
1267
|
+
if self.speed_state == "try" and now >= self.speed_until_ms:
|
|
1268
|
+
self._speed_revert() # no commit within verify_ms
|
|
1269
|
+
elif self.speed_state == "committed" and now - self.speed_good_ms >= self.speed_idle_ms:
|
|
1270
|
+
self._speed_revert()
|
|
1271
|
+
|
|
1103
1272
|
def _page(self, tlvs: list[bytes], first: int) -> bytes:
|
|
1104
1273
|
budget = self.probe.max_frame - m.RESULT_HEADER - 1
|
|
1105
1274
|
out, sent = b"", 0
|
|
@@ -1129,12 +1298,13 @@ class Endpoint:
|
|
|
1129
1298
|
raise Reject(m.MALFORMED)
|
|
1130
1299
|
live = [c.pair for c in self.conns.values() if c.fn == fn]
|
|
1131
1300
|
full = len(live) >= self.max_connections.get(fn, 1) # every seat taken: the live pairs only
|
|
1301
|
+
self._refuse_disabled(ch for p in pairs if self._allows(fn, p) for ch in p) # cause 5 (probe.config §1)
|
|
1132
1302
|
if any(not self._allows(fn, p) or set(p) & self._held(fn, p) or (full and p not in live) for p in pairs):
|
|
1133
1303
|
raise Reject(m.UNAVAILABLE) # not allowed, held (§8.1), or no seat to try it on
|
|
1134
1304
|
if not pairs: # the count-0 list, from `skip` on (oep-if-debug §1)
|
|
1135
1305
|
skip = struct.unpack("<H", skip_tlv)[0] if skip_tlv is not None else 0
|
|
1136
|
-
pairs = [p for p in self._allowed_pairs(fn)
|
|
1137
|
-
|
|
1306
|
+
pairs = [p for p in self._allowed_pairs(fn) if not set(p) & self.disabled # disabled: not listed
|
|
1307
|
+
and not set(p) & self._held(fn, p) and (not full or p in live)][skip:]
|
|
1138
1308
|
pairs = pairs[:255] # tried is a u8
|
|
1139
1309
|
found = []
|
|
1140
1310
|
for p in pairs:
|
|
@@ -1164,6 +1334,7 @@ class Endpoint:
|
|
|
1164
1334
|
raise Reject(m.UNSUPPORTED, bytes([_T_ATTACH["reset"] | m.TAG_CRITICAL])) # not a reset line here (§3)
|
|
1165
1335
|
if hold_ms > self.max_op_ms:
|
|
1166
1336
|
raise Reject(m.UNSUPPORTED, bytes([_T_ATTACH["reset"] | m.TAG_CRITICAL])) # longer than one request may take
|
|
1337
|
+
self._refuse_disabled([channel]) # a disabled reset line: cause 5 (probe.config §1)
|
|
1167
1338
|
if any(a[2] == channel for a in self.plan):
|
|
1168
1339
|
raise unavailable("pin_in_use", channel, next(a[0] for a in self.plan if a[2] == channel), "plan")
|
|
1169
1340
|
idle_clock = idle_tlv[0] if idle_tlv is not None else 0
|
|
@@ -1241,13 +1412,17 @@ class Endpoint:
|
|
|
1241
1412
|
pair = struct.unpack("<HH", got[0x03])
|
|
1242
1413
|
if not self._allows(fn, pair):
|
|
1243
1414
|
raise Reject(m.UNSUPPORTED, bytes([_T_ATTACH["pins"] | m.TAG_CRITICAL])) # not a pair this wire offers (core §4.3 order 6)
|
|
1415
|
+
self._refuse_disabled(pair) # a disabled channel: cause 5 (probe.config §1)
|
|
1244
1416
|
if set(pair) & self._held(fn, pair):
|
|
1245
1417
|
raise unavailable("pin_in_use", next(iter(set(pair) & self._held(fn, pair)))) # held (§8.1)
|
|
1246
1418
|
return pair
|
|
1247
1419
|
live = [c.pair for c in self.conns.values() if c.fn == fn]
|
|
1248
1420
|
if len(live) == 1:
|
|
1249
1421
|
return live[0] # no pins: the wire's one live connection
|
|
1250
|
-
|
|
1422
|
+
offered = self._allowed_pairs(fn)
|
|
1423
|
+
allowed = [p for p in offered if not set(p) & self.disabled] # the probe never picks a disabled channel
|
|
1424
|
+
if not live and len(offered) == 1 and not allowed:
|
|
1425
|
+
self._refuse_disabled(offered[0]) # its one pair is disabled: cause 5
|
|
1251
1426
|
if live or len(allowed) != 1:
|
|
1252
1427
|
raise Reject(m.UNAVAILABLE) # the host chooses among several
|
|
1253
1428
|
return allowed[0]
|
|
@@ -1281,8 +1456,10 @@ class Endpoint:
|
|
|
1281
1456
|
|
|
1282
1457
|
def _close_conn(self, cid: int, mark: int) -> None:
|
|
1283
1458
|
"""The connection goes; its streams close (mark `mark`, then closed 4) and stay readable (console §2)."""
|
|
1284
|
-
self.conns.pop(cid, None)
|
|
1459
|
+
gone = self.conns.pop(cid, None)
|
|
1285
1460
|
self.resources.pop(cid, None)
|
|
1461
|
+
if gone is not None:
|
|
1462
|
+
self._park(gone.pair) # its pins are free again: their idle state
|
|
1286
1463
|
for (c, _), sid in list(self.stream_keys.items()):
|
|
1287
1464
|
if c == cid and not self.streams[sid].closed:
|
|
1288
1465
|
self.streams[sid].add_mark(mark, self.now_ns())
|
|
@@ -1612,6 +1789,7 @@ class Endpoint:
|
|
|
1612
1789
|
if not self.gpio_allowed.get(fn, 0xFF) >> mode & 1: # a mode it does not drive (fixture §1)
|
|
1613
1790
|
raise unsupported_fixed(m.tlv(_UNA["channel"], struct.pack("<H", ch)), m.tlv(index_tag, bytes([i])))
|
|
1614
1791
|
for i, (ch, mode) in enumerate(pairs):
|
|
1792
|
+
self._refuse_disabled([ch], m.tlv(index_tag, bytes([i]))) # disabled: cause 5 (probe.config §1)
|
|
1615
1793
|
if ch not in mine: # the position as the gpio's TLV (fixture §1)
|
|
1616
1794
|
raise unavailable(channel=ch, extra=m.tlv(index_tag, bytes([i])))
|
|
1617
1795
|
for ch, mode in pairs:
|
|
@@ -1623,6 +1801,7 @@ class Endpoint:
|
|
|
1623
1801
|
chans = [t.take("H") for _ in range(n)]
|
|
1624
1802
|
_, ignored = t.tail()
|
|
1625
1803
|
for i, ch in enumerate(chans):
|
|
1804
|
+
self._refuse_disabled([ch], m.tlv(_GPIO.tlv["unavailable_payload"]["index"], bytes([i])))
|
|
1626
1805
|
if ch not in mine:
|
|
1627
1806
|
raise unavailable(channel=ch, extra=m.tlv(_GPIO.tlv["unavailable_payload"]["index"], bytes([i])))
|
|
1628
1807
|
levels = []
|
|
@@ -1872,6 +2051,10 @@ class Endpoint:
|
|
|
1872
2051
|
places = [(s.wire_fn, s.pair) for s in slots.values()]
|
|
1873
2052
|
if len(set(places)) != len(places):
|
|
1874
2053
|
raise Reject(m.MALFORMED) # two slots on one place: the settings contradict
|
|
2054
|
+
disabled = {key for tag, key in new if tag == ITEM["disable"]}
|
|
2055
|
+
idles = {key for tag, key in new if tag == ITEM["idle"]}
|
|
2056
|
+
if disabled & idles:
|
|
2057
|
+
raise Reject(m.MALFORMED) # idle and disable for one channel (probe.config §1)
|
|
1875
2058
|
for fn in self.pairs: # every wire (pin_roles wires have an empty list)
|
|
1876
2059
|
if sum(1 for s in slots.values() if s.wire_fn == fn and s.attach == SLOT_ATTACH["at_boot"]) > \
|
|
1877
2060
|
self.max_connections.get(fn, 1):
|
|
@@ -1891,6 +2074,9 @@ class Endpoint:
|
|
|
1891
2074
|
elif tag == ITEM["idle"]:
|
|
1892
2075
|
if len(value) != 3 or value[2] > 2:
|
|
1893
2076
|
raise Reject(m.MALFORMED)
|
|
2077
|
+
elif tag == ITEM["disable"] and key not in self._all_channels():
|
|
2078
|
+
# a channel the firmware does not declare: unsupported, as idle (probe.config §1)
|
|
2079
|
+
raise unsupported_fixed(m.tlv(_UNA["channel"], struct.pack("<H", key)))
|
|
1894
2080
|
elif tag == ITEM["label"]:
|
|
1895
2081
|
if len(value) < 3:
|
|
1896
2082
|
raise Reject(m.MALFORMED)
|
|
@@ -1911,13 +2097,16 @@ class Endpoint:
|
|
|
1911
2097
|
self.slots = slots # the pin check below sees the new slots
|
|
1912
2098
|
try:
|
|
1913
2099
|
self._check_plan(want)
|
|
2100
|
+
self._check_disabled(new, disabled, plans, want, slots)
|
|
1914
2101
|
except Reject:
|
|
1915
2102
|
self.slots = {k: self._parse_slot(v) for (t, k), v in self.config.items() if t == ITEM["slot"]}
|
|
1916
2103
|
raise
|
|
1917
2104
|
# accepted: make it current
|
|
1918
|
-
for
|
|
2105
|
+
old_idle = {key: v for (tag, key), v in self.config.items() if tag == ITEM["idle"]}
|
|
2106
|
+
repark = (self.disabled - disabled) | {key for key in idles | set(old_idle) if new.get((ITEM["idle"], key)) != old_idle.get(key)}
|
|
2107
|
+
self.config = new # the disable items first: a dropped plan's pins
|
|
2108
|
+
for fn in old_plan_fns - set(plans): # are not parked when the same set disables them
|
|
1919
2109
|
self._drop_plan(fn)
|
|
1920
|
-
self.config = new
|
|
1921
2110
|
for fn, assigned in plans.items():
|
|
1922
2111
|
self.plan = {a for a in self.plan if a[0] != fn} | set(assigned)
|
|
1923
2112
|
self.plan_from_config.add(fn)
|
|
@@ -1937,12 +2126,40 @@ class Endpoint:
|
|
|
1937
2126
|
self.flows.pop((port, key), None)
|
|
1938
2127
|
self.mixed_out.pop(port, None)
|
|
1939
2128
|
self.binds = binds
|
|
2129
|
+
if not boot:
|
|
2130
|
+
self._park(repark) # an idle item set, or a channel enabled again: now
|
|
1940
2131
|
for n, s in slots.items():
|
|
1941
2132
|
if s.attach == SLOT_ATTACH["at_boot"] and (boot or changed_slots is None or n in changed_slots):
|
|
1942
2133
|
self.slot_rt[n].evicted = False
|
|
1943
2134
|
self._auto_attach(n)
|
|
1944
2135
|
self._refresh()
|
|
1945
2136
|
|
|
2137
|
+
def _check_disabled(self, new: dict, disabled: set[int], plans: dict, want: list, slots: dict) -> None:
|
|
2138
|
+
"""probe.config §1 disable: a channel in use now (a plan, a connection, a slot this set keeps) cannot be disabled
|
|
2139
|
+
(unavailable cause 1); an item naming a disabled channel (a plan, a slot's pins) is cause 5 with the channel."""
|
|
2140
|
+
for ch in sorted(disabled - self.disabled):
|
|
2141
|
+
for fn, _, c in sorted(self.plan):
|
|
2142
|
+
if c != ch:
|
|
2143
|
+
continue
|
|
2144
|
+
if fn in self.plan_from_config:
|
|
2145
|
+
if new.get((ITEM["plan"], fn)) == self.config.get((ITEM["plan"], fn)):
|
|
2146
|
+
raise unavailable("pin_in_use", ch, fn, "settings_plan")
|
|
2147
|
+
elif fn not in plans: # a session's plan this set does not replace
|
|
2148
|
+
raise unavailable("pin_in_use", ch, fn, "plan")
|
|
2149
|
+
conn = next((c for c in self.conns.values() if ch in c.pair), None)
|
|
2150
|
+
if conn is not None:
|
|
2151
|
+
raise unavailable("pin_in_use", ch, conn.fn, "connection")
|
|
2152
|
+
for (tag, key), value in self.config.items():
|
|
2153
|
+
if tag == ITEM["slot"] and new.get((tag, key)) == value and ch in self._parse_slot(value).pair:
|
|
2154
|
+
raise unavailable("pin_in_use", ch, holder_kind="slot")
|
|
2155
|
+
self._refuse_disabled_in(disabled, [c for _, _, c in want] + [p for s in slots.values() for p in s.pair])
|
|
2156
|
+
|
|
2157
|
+
@staticmethod
|
|
2158
|
+
def _refuse_disabled_in(disabled: set[int], channels: list[int]) -> None:
|
|
2159
|
+
for ch in channels:
|
|
2160
|
+
if ch != 0xFFFF and ch in disabled:
|
|
2161
|
+
raise unavailable("held_by_settings", ch, holder_kind="disabled")
|
|
2162
|
+
|
|
1946
2163
|
def _parse_slot(self, v: bytes) -> Slot:
|
|
1947
2164
|
"""probe.config §1.1, refused in core §4.3's order: the form (malformed), then what this probe lacks
|
|
1948
2165
|
(unsupported), then unknown fns."""
|
|
@@ -2093,8 +2310,9 @@ class Endpoint:
|
|
|
2093
2310
|
self.flows[(port, key)] = Flow(sid, 0)
|
|
2094
2311
|
|
|
2095
2312
|
def tick(self) -> None:
|
|
2096
|
-
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
|
|
2313
|
+
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures, port_speed's timers."""
|
|
2097
2314
|
self._lapse()
|
|
2315
|
+
self._speed_tick()
|
|
2098
2316
|
now = self.now()
|
|
2099
2317
|
for fn, cap in self.captures.items():
|
|
2100
2318
|
self._events(fn, cap.tick(now))
|
|
@@ -2229,7 +2447,10 @@ class Endpoint:
|
|
|
2229
2447
|
s.written += data
|
|
2230
2448
|
|
|
2231
2449
|
def _session_over(self) -> None:
|
|
2232
|
-
"""The session ended (end, lapse, force): the ports it held resume from its last host reset (or now)
|
|
2450
|
+
"""The session ended (end, lapse, force): the ports it held resume from its last host reset (or now); a port
|
|
2451
|
+
off its boot speed goes back after the answer (core §3.5)."""
|
|
2452
|
+
if self.speed_state != "base":
|
|
2453
|
+
self.speed_pending = ("revert",)
|
|
2233
2454
|
for port in self.held_ports:
|
|
2234
2455
|
b = self.binds.get(port)
|
|
2235
2456
|
if b is None:
|
oep_client/fake.py
CHANGED
|
@@ -118,6 +118,7 @@ _CAPD = reg.FIXTURE_LOGIC.tlv["describe"]
|
|
|
118
118
|
_ANAD = reg.FIXTURE_ANALOG.tlv["describe"]
|
|
119
119
|
_GRPD = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
|
|
120
120
|
CORE_CHIP = reg.CORE.tlv["describe"]["chip"]
|
|
121
|
+
CORE_PORT_SPEED = reg.CORE.tlv["describe"]["port_speed"] # the optional port_speed is on (core §3.5)
|
|
121
122
|
|
|
122
123
|
|
|
123
124
|
def _analog_decl(frontends: list[tuple[int, int, int, int]], max_samples: int) -> tuple[bytes, ...]:
|
|
@@ -245,13 +246,13 @@ def p4_x035() -> FakeProbe:
|
|
|
245
246
|
|
|
246
247
|
def esp32_v003() -> FakeProbe:
|
|
247
248
|
"""A small probe with 64-byte frames over a 115200 bps UART bridge (its only transport, serial port 0):
|
|
248
|
-
classic ESP32 on a CH32V003 (SWIO) jig."""
|
|
249
|
+
classic ESP32 on a CH32V003 (SWIO) jig. port_speed on (core §3.5), as the reference classic ESP32 firmware."""
|
|
249
250
|
reserved = [0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 15, 16]
|
|
250
251
|
wired = [4, 5, 13, 14, 17, 18, 19, 21, 22, 25, 26, 27, 32, 33]
|
|
251
252
|
return FakeProbe("esp32-v003", 64, [
|
|
252
253
|
_core("3.0.0", "esp32", "0070070d9394", 40, reserved, f"{NS}.esp32-v003",
|
|
253
254
|
{16: "SWIO", 23: "NRST", 22: "DUT TX", 21: "DUT RX"},
|
|
254
|
-
_transports([(TRANSPORT["uart_bridge"], 0xFF)])),
|
|
255
|
+
_transports([(TRANSPORT["uart_bridge"], 0xFF)]) + (catalog.u8(CORE_PORT_SPEED, 1),)),
|
|
255
256
|
Offered(1, 0, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
|
|
256
257
|
catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
|
|
257
258
|
Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
|