oep-client-python 0.0.23__py3-none-any.whl → 0.0.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
oep_client/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.23"
5
+ __version__ = "0.0.24"
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,17 @@ 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 with no good frame or 3 broken
40
+ candidates within 1 s, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
41
+ (rate -> BrokenRate): frames at such a rate break, from a size and in the directions given (`fake_serial` applies it).
35
42
 
36
43
  Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
37
44
  nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
@@ -301,6 +308,23 @@ class Flow:
301
308
  last_ms: int = 0
302
309
 
303
310
 
311
+ @dataclass
312
+ class BrokenRate:
313
+ """How a line rate breaks frames in the fake (port_speed tests): frames of `min_size` bytes and more (on the wire)
314
+ break, towards the host and / or towards the probe. A broken frame towards the probe is a candidate whose CRC does
315
+ not match; towards the host its CRC is spoiled."""
316
+ min_size: int = 0
317
+ to_host: bool = True
318
+ to_probe: bool = True
319
+
320
+
321
+ PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
322
+ OP_PORT_SPEED = reg.CORE.op["port_speed"]
323
+ SPEED_STEP = reg.CORE.enum["port_speed_step"]
324
+ SPEED_BAD_MAX, SPEED_BAD_WINDOW_MS = 3, 1000 # committed: this many broken candidates within the window revert
325
+ SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
326
+
327
+
304
328
  class Endpoint:
305
329
  MARKS_PER_ANSWER = 3 # small, so hosts must follow `more`
306
330
  CHUNK = 64 # raw bytes a serial port takes at a time (probe guide §3.6)
@@ -387,6 +411,10 @@ class Endpoint:
387
411
  self.bind_modes = struct.unpack_from("<I", cfg[CFG_DESCRIBE["bind_modes"]])[0] if CFG_DESCRIBE["bind_modes"] in cfg else 0
388
412
  self.console_accept = 64
389
413
  self.uart_accept = 256
414
+ # port_speed (core §3.5): on when the profile declares it; the boot speed every revert goes back to
415
+ self.port_speed_base: int | None = 115200 if any(t[0] == PORT_SPEED_TAG for t in self.static.get(0, ())) else None
416
+ self.broken_rates: dict[int, BrokenRate] = {} # the line: rates that break frames (fake_serial applies it)
417
+ self._transport = 0 # the transport the request being handled came in on
390
418
  self._boot()
391
419
 
392
420
  def _boot(self) -> None:
@@ -438,8 +466,49 @@ class Endpoint:
438
466
  self.mixed_out: dict[int, bytearray] = {}
439
467
  self.held_ports: set[int] = set()
440
468
  self.session_resets: dict[tuple[int, int], tuple[object, int]] = {} # stream key -> (sid, position)
469
+ self.parked: dict[int, int] = {} # channel -> the idle mode the probe put the free pin in
470
+ self.speed_state = "base" # port_speed: "base", "try" or "committed" (core §3.5)
471
+ self.speed_port: int | None = None # the port off its boot speed
472
+ self.speed_rate = 0 # the rate it runs at
473
+ self.speed_asked = 0 # the baud the try asked (the commit names it again)
474
+ self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
475
+ self.speed_idle_ms = 0 # committed: revert after this long with no good frame (0 never)
476
+ self.speed_good_ms = 0 # the last good frame on that port
477
+ self.speed_bad: list[int] = [] # committed: the times of the recent broken candidates
478
+ self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
479
+ self.speed_log: list[tuple[int, int]] = [] # (port, rate) every switch, reverts included
441
480
  if self.saved is not None:
442
- self._apply_saved()
481
+ self._apply_saved() # the saved disable items first: those pins are never parked
482
+ self._park(self._all_channels()) # then the free pins' idle state (probe.config §2 boot order)
483
+
484
+ @property
485
+ def disabled(self) -> set[int]:
486
+ """The channels the settings' disable items take away (probe.config §1): never used, driven or configured."""
487
+ return {key for tag, key in self.config if tag == ITEM["disable"]}
488
+
489
+ def _refuse_disabled(self, channels, extra: bytes = b"") -> None:
490
+ """A request naming a disabled channel: rejected unavailable cause 5 (held by settings) with the channel."""
491
+ for ch in channels:
492
+ if ch != 0xFFFF and ch in self.disabled:
493
+ raise unavailable("held_by_settings", ch, holder_kind="disabled", extra=extra)
494
+
495
+ def _all_channels(self) -> set[int]:
496
+ """Every channel some fn's describe offers (role_channels, channel_group, the wires' pairs and reset lines)."""
497
+ out = {ch for fn in self.static if fn != m.CORE_FN for ch in self._declared_channels(fn)}
498
+ out |= {p for pairs in self.pairs.values() for pair in pairs for p in pair}
499
+ out |= {ch for chs in self.reset_channels.values() for ch in chs}
500
+ out.discard(0xFFFF)
501
+ return out
502
+
503
+ def _park(self, channels) -> None:
504
+ """Free pins go to their idle state (the idle item, else Hi-Z) at boot and whenever released (probe.config §1);
505
+ a disabled channel stays as the reset left it (never parked), a held one is not free."""
506
+ busy = self._held()
507
+ for ch in channels:
508
+ if ch == 0xFFFF or ch in self.disabled or ch in busy:
509
+ continue
510
+ item = self.config.get((ITEM["idle"], ch))
511
+ self.parked[ch] = item[2] if item else 0 # 0 = Hi-Z
443
512
 
444
513
  @property
445
514
  def target_id(self) -> int | None:
@@ -676,6 +745,7 @@ class Endpoint:
676
745
  return None
677
746
  req = m.Request.unpack(data)
678
747
  self.requests.append(req)
748
+ self._transport = transport
679
749
  remembered = self._resent(req)
680
750
  if remembered is not None:
681
751
  return remembered
@@ -696,6 +766,8 @@ class Endpoint:
696
766
  out = m.Result(req.corr, res, detail, payload).pack()
697
767
  if req.session is not None and req.session == self.last:
698
768
  self._remember(req, out)
769
+ if transport not in self.serial_ports:
770
+ self.speed_after_answer() # the answer is not on a port whose speed changes
699
771
  return out
700
772
 
701
773
  def _interface(self, fn: int):
@@ -732,6 +804,8 @@ class Endpoint:
732
804
  return self._open(req.payload)
733
805
  if req.fn != m.CORE_FN and req.fn not in self.names:
734
806
  return m.REJECTED, m.UNKNOWN_FUNCTION, b""
807
+ if req.fn == m.CORE_FN and req.op == OP_PORT_SPEED and self.port_speed_base is None:
808
+ return m.REJECTED, m.UNKNOWN_OPERATION, b"" # the optional feature off (core §3.5)
735
809
  if not self._lock_free(req.fn, req.op):
736
810
  refused = self._check(req.session)
737
811
  if refused:
@@ -919,6 +993,8 @@ class Endpoint:
919
993
  if op == m.OP_KEEPALIVE:
920
994
  _, ignored = t.tail()
921
995
  return self._answer(b"", ignored)
996
+ if op == OP_PORT_SPEED:
997
+ return self._port_speed(t)
922
998
  if op == m.OP_SUBSCRIBE:
923
999
  fn, _min_bytes, max_delay_ms = t.take("HHI") # max_delay_ms u32 (core §11.3)
924
1000
  t.tail()
@@ -959,6 +1035,7 @@ class Endpoint:
959
1035
  raise unavailable("held_by_settings", holder_fn=min(named & self.plan_from_config),
960
1036
  holder_kind="settings_plan")
961
1037
  self._check_plan(got)
1038
+ self._refuse_disabled(ch for _, _, ch in got) # a disabled channel (probe.config §1): cause 5
962
1039
  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
1040
  raise unavailable("limit") # plan_roles (core §8)
964
1041
  self.plan = {a for a in self.plan if a[0] not in named} | set(got)
@@ -1034,11 +1111,14 @@ class Endpoint:
1034
1111
  return out
1035
1112
 
1036
1113
  def _drop_plan(self, fn: int) -> None:
1114
+ released = set()
1037
1115
  for a in [a for a in self.plan if a[0] == fn]:
1038
1116
  self.gpio_modes.pop(a[2], None)
1039
1117
  self.plan.discard(a)
1118
+ released.add(a[2])
1040
1119
  self.plan_from_config.discard(fn)
1041
1120
  self._uart_plan_changed(fn)
1121
+ self._park(released)
1042
1122
 
1043
1123
  # ---- oep.fixture.uart's stream: made by the plan, gone with it (fixture §2) ---------------------
1044
1124
  def _uart_plan_changed(self, fn: int) -> None:
@@ -1097,9 +1177,96 @@ class Endpoint:
1097
1177
  # ---- describe: the static declarations plus the live ones -----------------------------------
1098
1178
  def _declarations(self, fn: int) -> list[bytes]:
1099
1179
  """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)."""
1180
+ only, the settings' are read by get; probe.config's state is its op state). fn 0's port_speed follows
1181
+ `port_speed_base` (a test turns the feature off or on)."""
1182
+ if fn == m.CORE_FN:
1183
+ out = [t for t in self.static[fn] if t[0] != PORT_SPEED_TAG]
1184
+ return out + [catalog.u8(PORT_SPEED_TAG, 1)] if self.port_speed_base is not None else out
1101
1185
  return list(self.static[fn])
1102
1186
 
1187
+ # ---- port_speed (core §3.5) -----------------------------------------------------------------
1188
+ def _port_speed(self, t: "Take") -> tuple[int, int, bytes]:
1189
+ """port(u8) baud(u32) step(u8) verify_ms(u16) idle_ms(u32) [TLV] -> baud(u32): the rate that applies."""
1190
+ port, baud, step, verify_ms, idle_ms = t.take("BIBHI")
1191
+ _, ignored = t.tail()
1192
+ if step not in SPEED_STEP.values():
1193
+ raise unsupported_fixed()
1194
+ if port != self._transport or port >= len(self.transports) or \
1195
+ self.transports[port] != fake.TRANSPORT["uart_bridge"]:
1196
+ raise unavailable("wrong_state") # only the UART bridge the request came in on
1197
+ if step == SPEED_STEP["try"]:
1198
+ if not SPEED_RATES[0] <= baud <= SPEED_RATES[1]:
1199
+ raise unsupported_fixed() # a baud this UART cannot make
1200
+ self.speed_pending = ("switch", port, baud, verify_ms)
1201
+ return self._answer(struct.pack("<I", baud), ignored)
1202
+ if step == SPEED_STEP["commit"]:
1203
+ if self.speed_state == "base" or port != self.speed_port or baud != self.speed_asked:
1204
+ raise unavailable("wrong_state") # nothing tried, or another baud
1205
+ self.speed_state = "committed"
1206
+ self.speed_idle_ms = idle_ms
1207
+ self.speed_good_ms = self.now()
1208
+ self.speed_bad.clear()
1209
+ return self._answer(struct.pack("<I", self.speed_rate), ignored)
1210
+ self.speed_pending = ("revert",)
1211
+ return self._answer(struct.pack("<I", self.port_speed_base), ignored)
1212
+
1213
+ def port_baud(self, port: int) -> int:
1214
+ """The rate serial port `port` runs at now (its boot speed, or what port_speed set)."""
1215
+ if self.speed_state != "base" and port == self.speed_port:
1216
+ return self.speed_rate
1217
+ return self.port_speed_base or 115200
1218
+
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`)."""
1221
+ b = self.broken_rates.get(self.port_baud(port))
1222
+ return b is not None and size >= b.min_size and (b.to_host if to_host else b.to_probe)
1223
+
1224
+ def speed_after_answer(self) -> None:
1225
+ """The answer that asked for a switch or a revert is out (at the old speed): now do it."""
1226
+ pending, self.speed_pending = self.speed_pending, None
1227
+ if pending is None:
1228
+ return
1229
+ if pending[0] == "revert":
1230
+ self._speed_revert()
1231
+ return
1232
+ _, port, baud, verify_ms = pending
1233
+ self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "try", port, baud, baud
1234
+ self.speed_until_ms = self.now() + verify_ms
1235
+ self.speed_good_ms = self.now()
1236
+ self.speed_bad.clear()
1237
+ self.speed_log.append((port, baud))
1238
+
1239
+ def _speed_revert(self) -> None:
1240
+ if self.speed_state == "base":
1241
+ return
1242
+ self.speed_log.append((self.speed_port, self.port_speed_base))
1243
+ self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "base", None, 0, 0
1244
+ self.speed_bad.clear()
1245
+
1246
+ def speed_frame(self, port: int, good: bool) -> None:
1247
+ """A candidate closed on serial port `port`: a frame (good) or not (a broken candidate)."""
1248
+ if self.speed_state == "base" or port != self.speed_port:
1249
+ return
1250
+ if good:
1251
+ self.speed_good_ms = self.now()
1252
+ return
1253
+ if self.speed_state == "try":
1254
+ self._speed_revert() # the new speed breaks frames: not this one
1255
+ return
1256
+ now = self.now()
1257
+ self.speed_bad = [t for t in self.speed_bad if now - t < SPEED_BAD_WINDOW_MS] + [now]
1258
+ if len(self.speed_bad) >= SPEED_BAD_MAX:
1259
+ self._speed_revert()
1260
+
1261
+ def _speed_tick(self) -> None:
1262
+ if self.speed_pending and self.speed_pending[0] == "revert":
1263
+ self.speed_after_answer()
1264
+ now = self.now()
1265
+ if self.speed_state == "try" and now >= self.speed_until_ms:
1266
+ self._speed_revert() # no commit within verify_ms
1267
+ elif self.speed_state == "committed" and self.speed_idle_ms and now - self.speed_good_ms >= self.speed_idle_ms:
1268
+ self._speed_revert()
1269
+
1103
1270
  def _page(self, tlvs: list[bytes], first: int) -> bytes:
1104
1271
  budget = self.probe.max_frame - m.RESULT_HEADER - 1
1105
1272
  out, sent = b"", 0
@@ -1129,12 +1296,13 @@ class Endpoint:
1129
1296
  raise Reject(m.MALFORMED)
1130
1297
  live = [c.pair for c in self.conns.values() if c.fn == fn]
1131
1298
  full = len(live) >= self.max_connections.get(fn, 1) # every seat taken: the live pairs only
1299
+ self._refuse_disabled(ch for p in pairs if self._allows(fn, p) for ch in p) # cause 5 (probe.config §1)
1132
1300
  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
1301
  raise Reject(m.UNAVAILABLE) # not allowed, held (§8.1), or no seat to try it on
1134
1302
  if not pairs: # the count-0 list, from `skip` on (oep-if-debug §1)
1135
1303
  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
- if not set(p) & self._held(fn, p) and (not full or p in live)][skip:]
1304
+ pairs = [p for p in self._allowed_pairs(fn) if not set(p) & self.disabled # disabled: not listed
1305
+ and not set(p) & self._held(fn, p) and (not full or p in live)][skip:]
1138
1306
  pairs = pairs[:255] # tried is a u8
1139
1307
  found = []
1140
1308
  for p in pairs:
@@ -1164,6 +1332,7 @@ class Endpoint:
1164
1332
  raise Reject(m.UNSUPPORTED, bytes([_T_ATTACH["reset"] | m.TAG_CRITICAL])) # not a reset line here (§3)
1165
1333
  if hold_ms > self.max_op_ms:
1166
1334
  raise Reject(m.UNSUPPORTED, bytes([_T_ATTACH["reset"] | m.TAG_CRITICAL])) # longer than one request may take
1335
+ self._refuse_disabled([channel]) # a disabled reset line: cause 5 (probe.config §1)
1167
1336
  if any(a[2] == channel for a in self.plan):
1168
1337
  raise unavailable("pin_in_use", channel, next(a[0] for a in self.plan if a[2] == channel), "plan")
1169
1338
  idle_clock = idle_tlv[0] if idle_tlv is not None else 0
@@ -1241,13 +1410,17 @@ class Endpoint:
1241
1410
  pair = struct.unpack("<HH", got[0x03])
1242
1411
  if not self._allows(fn, pair):
1243
1412
  raise Reject(m.UNSUPPORTED, bytes([_T_ATTACH["pins"] | m.TAG_CRITICAL])) # not a pair this wire offers (core §4.3 order 6)
1413
+ self._refuse_disabled(pair) # a disabled channel: cause 5 (probe.config §1)
1244
1414
  if set(pair) & self._held(fn, pair):
1245
1415
  raise unavailable("pin_in_use", next(iter(set(pair) & self._held(fn, pair)))) # held (§8.1)
1246
1416
  return pair
1247
1417
  live = [c.pair for c in self.conns.values() if c.fn == fn]
1248
1418
  if len(live) == 1:
1249
1419
  return live[0] # no pins: the wire's one live connection
1250
- allowed = self._allowed_pairs(fn)
1420
+ offered = self._allowed_pairs(fn)
1421
+ allowed = [p for p in offered if not set(p) & self.disabled] # the probe never picks a disabled channel
1422
+ if not live and len(offered) == 1 and not allowed:
1423
+ self._refuse_disabled(offered[0]) # its one pair is disabled: cause 5
1251
1424
  if live or len(allowed) != 1:
1252
1425
  raise Reject(m.UNAVAILABLE) # the host chooses among several
1253
1426
  return allowed[0]
@@ -1281,8 +1454,10 @@ class Endpoint:
1281
1454
 
1282
1455
  def _close_conn(self, cid: int, mark: int) -> None:
1283
1456
  """The connection goes; its streams close (mark `mark`, then closed 4) and stay readable (console §2)."""
1284
- self.conns.pop(cid, None)
1457
+ gone = self.conns.pop(cid, None)
1285
1458
  self.resources.pop(cid, None)
1459
+ if gone is not None:
1460
+ self._park(gone.pair) # its pins are free again: their idle state
1286
1461
  for (c, _), sid in list(self.stream_keys.items()):
1287
1462
  if c == cid and not self.streams[sid].closed:
1288
1463
  self.streams[sid].add_mark(mark, self.now_ns())
@@ -1612,6 +1787,7 @@ class Endpoint:
1612
1787
  if not self.gpio_allowed.get(fn, 0xFF) >> mode & 1: # a mode it does not drive (fixture §1)
1613
1788
  raise unsupported_fixed(m.tlv(_UNA["channel"], struct.pack("<H", ch)), m.tlv(index_tag, bytes([i])))
1614
1789
  for i, (ch, mode) in enumerate(pairs):
1790
+ self._refuse_disabled([ch], m.tlv(index_tag, bytes([i]))) # disabled: cause 5 (probe.config §1)
1615
1791
  if ch not in mine: # the position as the gpio's TLV (fixture §1)
1616
1792
  raise unavailable(channel=ch, extra=m.tlv(index_tag, bytes([i])))
1617
1793
  for ch, mode in pairs:
@@ -1623,6 +1799,7 @@ class Endpoint:
1623
1799
  chans = [t.take("H") for _ in range(n)]
1624
1800
  _, ignored = t.tail()
1625
1801
  for i, ch in enumerate(chans):
1802
+ self._refuse_disabled([ch], m.tlv(_GPIO.tlv["unavailable_payload"]["index"], bytes([i])))
1626
1803
  if ch not in mine:
1627
1804
  raise unavailable(channel=ch, extra=m.tlv(_GPIO.tlv["unavailable_payload"]["index"], bytes([i])))
1628
1805
  levels = []
@@ -1872,6 +2049,10 @@ class Endpoint:
1872
2049
  places = [(s.wire_fn, s.pair) for s in slots.values()]
1873
2050
  if len(set(places)) != len(places):
1874
2051
  raise Reject(m.MALFORMED) # two slots on one place: the settings contradict
2052
+ disabled = {key for tag, key in new if tag == ITEM["disable"]}
2053
+ idles = {key for tag, key in new if tag == ITEM["idle"]}
2054
+ if disabled & idles:
2055
+ raise Reject(m.MALFORMED) # idle and disable for one channel (probe.config §1)
1875
2056
  for fn in self.pairs: # every wire (pin_roles wires have an empty list)
1876
2057
  if sum(1 for s in slots.values() if s.wire_fn == fn and s.attach == SLOT_ATTACH["at_boot"]) > \
1877
2058
  self.max_connections.get(fn, 1):
@@ -1891,6 +2072,9 @@ class Endpoint:
1891
2072
  elif tag == ITEM["idle"]:
1892
2073
  if len(value) != 3 or value[2] > 2:
1893
2074
  raise Reject(m.MALFORMED)
2075
+ elif tag == ITEM["disable"] and key not in self._all_channels():
2076
+ # a channel the firmware does not declare: unsupported, as idle (probe.config §1)
2077
+ raise unsupported_fixed(m.tlv(_UNA["channel"], struct.pack("<H", key)))
1894
2078
  elif tag == ITEM["label"]:
1895
2079
  if len(value) < 3:
1896
2080
  raise Reject(m.MALFORMED)
@@ -1911,13 +2095,16 @@ class Endpoint:
1911
2095
  self.slots = slots # the pin check below sees the new slots
1912
2096
  try:
1913
2097
  self._check_plan(want)
2098
+ self._check_disabled(new, disabled, plans, want, slots)
1914
2099
  except Reject:
1915
2100
  self.slots = {k: self._parse_slot(v) for (t, k), v in self.config.items() if t == ITEM["slot"]}
1916
2101
  raise
1917
2102
  # accepted: make it current
1918
- for fn in old_plan_fns - set(plans):
2103
+ old_idle = {key: v for (tag, key), v in self.config.items() if tag == ITEM["idle"]}
2104
+ repark = (self.disabled - disabled) | {key for key in idles | set(old_idle) if new.get((ITEM["idle"], key)) != old_idle.get(key)}
2105
+ self.config = new # the disable items first: a dropped plan's pins
2106
+ for fn in old_plan_fns - set(plans): # are not parked when the same set disables them
1919
2107
  self._drop_plan(fn)
1920
- self.config = new
1921
2108
  for fn, assigned in plans.items():
1922
2109
  self.plan = {a for a in self.plan if a[0] != fn} | set(assigned)
1923
2110
  self.plan_from_config.add(fn)
@@ -1937,12 +2124,40 @@ class Endpoint:
1937
2124
  self.flows.pop((port, key), None)
1938
2125
  self.mixed_out.pop(port, None)
1939
2126
  self.binds = binds
2127
+ if not boot:
2128
+ self._park(repark) # an idle item set, or a channel enabled again: now
1940
2129
  for n, s in slots.items():
1941
2130
  if s.attach == SLOT_ATTACH["at_boot"] and (boot or changed_slots is None or n in changed_slots):
1942
2131
  self.slot_rt[n].evicted = False
1943
2132
  self._auto_attach(n)
1944
2133
  self._refresh()
1945
2134
 
2135
+ def _check_disabled(self, new: dict, disabled: set[int], plans: dict, want: list, slots: dict) -> None:
2136
+ """probe.config §1 disable: a channel in use now (a plan, a connection, a slot this set keeps) cannot be disabled
2137
+ (unavailable cause 1); an item naming a disabled channel (a plan, a slot's pins) is cause 5 with the channel."""
2138
+ for ch in sorted(disabled - self.disabled):
2139
+ for fn, _, c in sorted(self.plan):
2140
+ if c != ch:
2141
+ continue
2142
+ if fn in self.plan_from_config:
2143
+ if new.get((ITEM["plan"], fn)) == self.config.get((ITEM["plan"], fn)):
2144
+ raise unavailable("pin_in_use", ch, fn, "settings_plan")
2145
+ elif fn not in plans: # a session's plan this set does not replace
2146
+ raise unavailable("pin_in_use", ch, fn, "plan")
2147
+ conn = next((c for c in self.conns.values() if ch in c.pair), None)
2148
+ if conn is not None:
2149
+ raise unavailable("pin_in_use", ch, conn.fn, "connection")
2150
+ for (tag, key), value in self.config.items():
2151
+ if tag == ITEM["slot"] and new.get((tag, key)) == value and ch in self._parse_slot(value).pair:
2152
+ raise unavailable("pin_in_use", ch, holder_kind="slot")
2153
+ self._refuse_disabled_in(disabled, [c for _, _, c in want] + [p for s in slots.values() for p in s.pair])
2154
+
2155
+ @staticmethod
2156
+ def _refuse_disabled_in(disabled: set[int], channels: list[int]) -> None:
2157
+ for ch in channels:
2158
+ if ch != 0xFFFF and ch in disabled:
2159
+ raise unavailable("held_by_settings", ch, holder_kind="disabled")
2160
+
1946
2161
  def _parse_slot(self, v: bytes) -> Slot:
1947
2162
  """probe.config §1.1, refused in core §4.3's order: the form (malformed), then what this probe lacks
1948
2163
  (unsupported), then unknown fns."""
@@ -2093,8 +2308,9 @@ class Endpoint:
2093
2308
  self.flows[(port, key)] = Flow(sid, 0)
2094
2309
 
2095
2310
  def tick(self) -> None:
2096
- """Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
2311
+ """Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures, port_speed's timers."""
2097
2312
  self._lapse()
2313
+ self._speed_tick()
2098
2314
  now = self.now()
2099
2315
  for fn, cap in self.captures.items():
2100
2316
  self._events(fn, cap.tick(now))
@@ -2229,7 +2445,10 @@ class Endpoint:
2229
2445
  s.written += data
2230
2446
 
2231
2447
  def _session_over(self) -> None:
2232
- """The session ended (end, lapse, force): the ports it held resume from its last host reset (or now)."""
2448
+ """The session ended (end, lapse, force): the ports it held resume from its last host reset (or now); a port
2449
+ off its boot speed goes back after the answer (core §3.5)."""
2450
+ if self.speed_state != "base":
2451
+ self.speed_pending = ("revert",)
2233
2452
  for port in self.held_ports:
2234
2453
  b = self.binds.get(port)
2235
2454
  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),