oep-client-python 0.0.18__py3-none-any.whl → 0.0.20__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.18"
5
+ __version__ = "0.0.20"
oep_client/__main__.py CHANGED
@@ -186,12 +186,14 @@ def _config(args) -> int:
186
186
  if args.lock:
187
187
  mask, _, value = args.lock.partition(":")
188
188
  lock = (1, struct.pack("<I", int(mask, 16)), struct.pack("<I", int(value, 16)))
189
- it = config.Slot(args.slot, fn, _pins(hst, fn, args.pins, args.wire), args.name, args.attach, args.retry,
190
- args.mechanism, lock, args.max_speed, args.idle_clock)
189
+ it = config.Slot(slot=args.slot, wire_fn=fn, pins=_pins(hst, fn, args.pins, args.wire), name=args.name,
190
+ attach=args.attach, retry_s=args.retry, mechanism=args.mechanism, lock=lock,
191
+ max_speed=args.max_speed, idle_clock=args.idle_clock)
191
192
  _change(hst, cfg, [it], args.save)
192
193
  elif args.action == "bind":
193
194
  slots = {it.name: it.slot for it in cfg.items() if isinstance(it, config.Slot)}
194
- it = config.Bind(args.port, args.mode, [_stream(s, slots) for s in args.stream], args.select)
195
+ it = config.Bind(port=args.port, mode=args.mode, streams=[_stream(s, slots) for s in args.stream],
196
+ selected=args.select)
195
197
  _change(hst, cfg, [it], args.save)
196
198
  elif args.action == "plan":
197
199
  fn, roles = _plan_fn(hst, args.fn)
@@ -203,12 +205,12 @@ def _config(args) -> int:
203
205
  number = int(role) if role.isdigit() else roles.get(role.lower())
204
206
  if number is None:
205
207
  raise SystemExit(f"{role}: not a role of fn {fn} (roles: {', '.join(sorted(roles)) or 'numbers only'})")
206
- items.append(config.Plan(fn, number, int(ch, 0)))
208
+ items.append(config.Plan(fn=fn, role=number, channel=int(ch, 0)))
207
209
  _change(hst, cfg, items, args.save)
208
210
  elif args.action == "label":
209
- _change(hst, cfg, [config.Label(args.channel, args.text)], args.save)
211
+ _change(hst, cfg, [config.Label(channel=args.channel, text=args.text)], args.save)
210
212
  elif args.action == "idle":
211
- _change(hst, cfg, [config.Idle(args.channel, args.mode)], args.save)
213
+ _change(hst, cfg, [config.Idle(channel=args.channel, mode=args.mode)], args.save)
212
214
  elif args.action == "remove":
213
215
  _change(hst, cfg, [config.remove(args.kind, args.key)], args.save)
214
216
  else:
oep_client/capture.py CHANGED
@@ -1,4 +1,4 @@
1
- """oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
1
+ """oep.fixture.logic / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
2
2
  §1 layouts, §2 segments, §3 operations, §3.8 calibration, §4 groups). Numbers from `registry`.
3
3
 
4
4
  Times are the probe's one clock (ns since its boot, comparable within one boot_id): estimates with an uncertainty, the
@@ -20,7 +20,7 @@ from . import cobs, host as h, message as m, registry as reg
20
20
  from .frames import FramingLost
21
21
  from .core import Interface, confirm
22
22
 
23
- _CAP = reg.FIXTURE_CAPTURE
23
+ _CAP = reg.FIXTURE_LOGIC
24
24
  _ANA = reg.FIXTURE_ANALOG
25
25
  _GRP = reg.FIXTURE_CAPTURE_GROUP
26
26
  # configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
@@ -158,7 +158,7 @@ def _config(payload: bytes, analog: bool) -> Config:
158
158
  elif tag == RATE_ACCURACY:
159
159
  c.rate_measured, c.rate_ppm = v[0] == 1, struct.unpack_from("<I", v, 1)[0]
160
160
  elif tag == SCALE and analog:
161
- role, zero, scale = struct.unpack("<BII", v)
161
+ role, zero, scale = struct.unpack("<Bii", v) # signed: an inverting frontend (§3.3)
162
162
  c.zero[role], c.scale_nv[role] = zero, scale
163
163
  elif tag == SKEW and analog:
164
164
  role, ns = struct.unpack("<BI", v)
@@ -177,7 +177,7 @@ def _config(payload: bytes, analog: bool) -> Config:
177
177
 
178
178
  class LogicCapture(Interface):
179
179
  """Basic logic capture. Channels are the plan's roles 0..C-1."""
180
- NAME = "oep.fixture.capture"
180
+ NAME = "oep.fixture.logic"
181
181
  REVISION = 1
182
182
  ANALOG = False
183
183
  CONFIGURE, START, STOP, FORCE, STATUS, READ, SEGMENTS, RELEASE, QUERY_OP = (
@@ -301,7 +301,7 @@ class LogicCapture(Interface):
301
301
 
302
302
  def segments(self, from_serial: int = 0) -> list[Segment]:
303
303
  rd = m.Reader(self._call(self.SEGMENTS, struct.pack("<I", from_serial), locked=False).payload)
304
- out = [Segment.unpack(rd.bytes(SEGMENT_BYTES)) for _ in range(rd.u8())]
304
+ out = [Segment.unpack(rd.element().bytes(SEGMENT_BYTES)) for _ in range(rd.u8())]
305
305
  rd.tail()
306
306
  return out
307
307
 
oep_client/catalog.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Draft wire forms for capability discovery by name (oep-spec docs/capability-declaration-model.ja.md).
2
2
 
3
3
  list request : flags(u8) first(u16) prefix_len(u8) prefix flags bit0 = exact
4
- list result : total(u16) count(u8) entries [TLV tail] oep.core (fn 0) is the first entry
4
+ list result : total(u16) count(u8) count x (len(u8) entry) [TLV tail] oep.core (fn 0) is the first entry
5
5
  list entry : fn(u16) instance(u16) revision(u8) flags(u8) name_len(u8) name
6
6
  describe : request fn(u16) first(u16); result more(u8) then TLV bytes (tag u8, len u8, value;
7
7
  tag bit 7 = critical). more = 1: TLVs remain after this page, ask again from first + count
@@ -15,7 +15,7 @@ from __future__ import annotations
15
15
  import struct
16
16
  from dataclasses import dataclass, field
17
17
 
18
- from .message import Reader, split_tlvs
18
+ from .message import Reader, element, split_tlvs
19
19
 
20
20
  LIST_EXACT = 0x01
21
21
 
@@ -65,7 +65,7 @@ def pack_entry(e: ListEntry) -> bytes:
65
65
 
66
66
 
67
67
  def pack_list_result(total: int, entries: list[ListEntry]) -> bytes:
68
- return struct.pack("<HB", total, len(entries)) + b"".join(pack_entry(e) for e in entries)
68
+ return struct.pack("<HB", total, len(entries)) + b"".join(element(pack_entry(e)) for e in entries)
69
69
 
70
70
 
71
71
  def unpack_list_result(payload: bytes) -> tuple[int, list[ListEntry]]:
@@ -74,8 +74,9 @@ def unpack_list_result(payload: bytes) -> tuple[int, list[ListEntry]]:
74
74
  total, count = rd.take("HB")
75
75
  out = []
76
76
  for _ in range(count):
77
- fn, instance, revision, flags, n = rd.take("HHBBB")
78
- out.append(ListEntry(fn, instance, revision, flags, rd.bytes(n).decode("ascii", "replace")))
77
+ e = rd.element()
78
+ fn, instance, revision, flags, n = e.take("HHBBB")
79
+ out.append(ListEntry(fn, instance, revision, flags, e.bytes(n).decode("ascii", "replace")))
79
80
  rd.tail()
80
81
  return total, out
81
82
 
oep_client/ch32_flash.py CHANGED
@@ -17,7 +17,7 @@ import struct
17
17
  import time
18
18
  from dataclasses import dataclass, field
19
19
 
20
- from . import host as h, riscv, target
20
+ from . import core, host as h, riscv
21
21
 
22
22
  # oep-spec experiments/flash-primitives/x035_loader.S, assembled at 0x20000000 (riscv32-esp-elf binutils)
23
23
  FAST_PAGE_LOADER = bytes.fromhex(
@@ -92,13 +92,13 @@ V003_INPUT, V003_STACK, V003_EBREAK, V003_RUN = 0x20000200, 0x20000800, 0x200001
92
92
 
93
93
 
94
94
  def _block_size(hst: h.Host) -> int:
95
- info = target.confirm(hst)
95
+ info = core.confirm(hst)
96
96
  # request header 6 + session 4 + connection 1 + address 4 + count 2 (the read answer's 5 + done 2 + status 1 is
97
97
  # smaller); a little slack for the result header
98
98
  return (info["max_frame"] - 5 - 6 - 4 - 1 - 4 - 2) // 4 * 4
99
99
 
100
100
 
101
- def _write_requests(dm: target.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
101
+ def _write_requests(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
102
102
  return [dm.request(dm.WRITE_BLOCK, dm.write_block_body(address + off, data[off:off + block]))
103
103
  for off in range(0, len(data), block)]
104
104
 
@@ -108,20 +108,20 @@ def _write_ok(r) -> bool:
108
108
  if not r.succeeded:
109
109
  return False
110
110
  try:
111
- rd = target.ran(r)
111
+ rd = riscv.ran(r)
112
112
  rd.u16()
113
113
  return rd.u8() == riscv.OK
114
114
  except h.OepError:
115
115
  return False
116
116
 
117
117
 
118
- def _write(dm: target.RiscvDm, address: int, data: bytes, block: int) -> None:
118
+ def _write(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> None:
119
119
  for r in dm.host.pipeline_calls(_write_requests(dm, address, data, block)):
120
120
  if not _write_ok(r):
121
- raise riscv.TargetError("write_block", target.ran(r).take("HB")[1], r)
121
+ raise riscv.TargetError("write_block", riscv.ran(r).take("HB")[1], r)
122
122
 
123
123
 
124
- def _read(dm: target.RiscvDm, address: int, length: int, block: int) -> bytes:
124
+ def _read(dm: riscv.RiscvDm, address: int, length: int, block: int) -> bytes:
125
125
  """Read back in blocks, pipelined when the host has the link's exchange (reads are independent)."""
126
126
  spans = [(address + off, min(block, length - off) // 4) for off in range(0, length, block)]
127
127
  out = b""
@@ -137,7 +137,7 @@ def _read(dm: target.RiscvDm, address: int, length: int, block: int) -> bytes:
137
137
  PIPELINE_PAGES = 16 # pages per pipelined batch: enough to keep the link busy, small enough to show progress
138
138
 
139
139
 
140
- def program(hst: h.Host, dm: target.RiscvDm, image: bytes, profile: FlashProfile) -> ProgramResult:
140
+ def program(hst: h.Host, dm: riscv.RiscvDm, image: bytes, profile: FlashProfile) -> ProgramResult:
141
141
  """Program `image` at profile.base on a halted, attached target (dm), verify by reading back."""
142
142
  image = image + b"\xff" * (-len(image) % profile.page)
143
143
  if profile.size and len(image) > profile.size:
oep_client/config.py CHANGED
@@ -2,8 +2,8 @@
2
2
  pins, slots, binds - read and set as items, saved when the host says so, and the live slot / bind state.
3
3
 
4
4
  cfg = config.ProbeConfig(hst)
5
- cfg.set([config.Slot(0, wire_fn, (2, 54), "x035", attach="at-boot", retry_s=1, mechanism="dmseq"),
6
- config.Bind(1, "last-reset", [("slot", 0)])])
5
+ cfg.set([config.Slot(slot=0, wire_fn=wire_fn, pins=(2, 54), name="x035", attach="at-boot", retry_s=1),
6
+ config.Bind(port=1, mode="last-reset", streams=[("slot", 0)])])
7
7
  cfg.save()
8
8
  cfg.items(), cfg.state()
9
9
 
@@ -31,13 +31,14 @@ BIND_FLOW = {v: k for k, v in _CFG.enum["bind_flow"].items()}
31
31
  IDLE = {k.replace("_", "-"): v for k, v in _CFG.enum["idle_mode"].items()}
32
32
  IDLE_CLOCK = dict(reg.WIRE_RVSWD.enum["idle_clock"]) # a slot's idle_clock (oep-if-debug §3)
33
33
  STORAGE_STATE = {v: k for k, v in _CFG.enum["storage_state"].items()}
34
+ UNREADABLE = {1: "unreadable form", 2: "an interface it names is gone or of another revision", 3: "refused when applied"}
34
35
 
35
36
 
36
37
  def _name(table: dict[str, int], value: int) -> str:
37
38
  return next((k for k, v in table.items() if v == value), str(value))
38
39
 
39
40
 
40
- @dataclass
41
+ @dataclass(kw_only=True)
41
42
  class Plan:
42
43
  fn: int
43
44
  role: int
@@ -48,7 +49,7 @@ class Plan:
48
49
  return struct.pack("<HBH", self.fn, self.role, self.channel)
49
50
 
50
51
 
51
- @dataclass
52
+ @dataclass(kw_only=True)
52
53
  class Label:
53
54
  channel: int
54
55
  text: str
@@ -58,7 +59,7 @@ class Label:
58
59
  return struct.pack("<H", self.channel) + self.text.encode()
59
60
 
60
61
 
61
- @dataclass
62
+ @dataclass(kw_only=True)
62
63
  class Idle:
63
64
  channel: int
64
65
  mode: str = "pull-up" # hi-z, pull-up, pull-down
@@ -68,7 +69,7 @@ class Idle:
68
69
  return struct.pack("<HB", self.channel, IDLE[self.mode])
69
70
 
70
71
 
71
- @dataclass
72
+ @dataclass(kw_only=True)
72
73
  class Slot:
73
74
  """A place a target is wired to (probe.config §1.1). pins: (swdio, swclk), swclk 0xFFFF on one wire (swio).
74
75
  lock: (scheme, mask, value) - the target_id a connection must show, e.g. (1, mask u32 LE, value u32 LE)."""
@@ -90,14 +91,14 @@ class Slot:
90
91
  self.retry_s if self.attach == "at-boot" else 0, self.max_speed, IDLE_CLOCK[self.idle_clock],
91
92
  MECHANISM[self.mechanism], len(name)) + name
92
93
  if self.lock is None:
93
- return v + b"\x00"
94
+ return v + b"\x00" # lock_len 0: no lock
94
95
  scheme, mask, value = self.lock
95
- if len(mask) != len(value) or not mask:
96
- raise ValueError("a lock's mask and value have the same length, at least 1 byte")
97
- return v + bytes([scheme]) + mask + value
96
+ if len(mask) != len(value) or not mask or not scheme:
97
+ raise ValueError("a lock has a scheme and a mask and value of the same length, at least 1 byte")
98
+ return v + bytes([1 + 2 * len(mask), scheme]) + mask + value
98
99
 
99
100
 
100
- @dataclass
101
+ @dataclass(kw_only=True)
101
102
  class Bind:
102
103
  """What serial port `port` (the describe transport index) carries (probe.config §1.2). streams: ("slot", n) or
103
104
  ("uart", fn); selected: manual's choice (an index into streams)."""
@@ -126,25 +127,28 @@ def remove(kind: str, key: int) -> bytes:
126
127
  def decode(tag: int, v: bytes):
127
128
  """One item as one of the classes above (an unknown tag: (tag, value))."""
128
129
  if tag == ITEM["plan"] and len(v) == 5:
129
- return Plan(*struct.unpack("<HBH", v))
130
+ fn, role, channel = struct.unpack("<HBH", v)
131
+ return Plan(fn=fn, role=role, channel=channel)
130
132
  if tag == ITEM["label"] and len(v) >= 2:
131
- return Label(struct.unpack_from("<H", v)[0], v[2:].decode("utf-8", "replace"))
133
+ return Label(channel=struct.unpack_from("<H", v)[0], text=v[2:].decode("utf-8", "replace"))
132
134
  if tag == ITEM["idle"] and len(v) == 3:
133
- return Idle(struct.unpack_from("<H", v)[0], _name(IDLE, v[2]))
135
+ return Idle(channel=struct.unpack_from("<H", v)[0], mode=_name(IDLE, v[2]))
134
136
  if tag == ITEM["slot"] and len(v) >= 18:
135
137
  n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle, mech, name_len = struct.unpack_from("<BHHHBHIBBB", v)
136
138
  name = v[17:17 + name_len].decode("ascii", "replace")
137
- rest = v[17 + name_len:]
139
+ at = 17 + name_len
140
+ lock_len = v[at] if at < len(v) else 0
141
+ part = v[at + 1:at + 1 + lock_len] # after it: later fields (core §2.3), skipped
138
142
  lock = None
139
- if rest and rest[0]:
140
- half = (len(rest) - 1) // 2
141
- lock = (rest[0], rest[1:1 + half], rest[1 + half:])
142
- return Slot(n, wire_fn, (swdio, swclk), name, _name(ATTACH, attach), retry_s, _name(MECHANISM, mech), lock,
143
- max_speed, _name(IDLE_CLOCK, idle))
143
+ if lock_len >= 3:
144
+ half = (lock_len - 1) // 2
145
+ lock = (part[0], part[1:1 + half], part[1 + half:])
146
+ return Slot(slot=n, wire_fn=wire_fn, pins=(swdio, swclk), name=name, attach=_name(ATTACH, attach), retry_s=retry_s,
147
+ mechanism=_name(MECHANISM, mech), lock=lock, max_speed=max_speed, idle_clock=_name(IDLE_CLOCK, idle))
144
148
  if tag == ITEM["bind"] and len(v) >= 4:
145
149
  port, mode, selected, n = struct.unpack_from("<BBBB", v)
146
150
  streams = [(_name(STREAM, v[4 + 3 * k]), struct.unpack_from("<H", v, 5 + 3 * k)[0]) for k in range(n)]
147
- return Bind(port, _name(MODE, mode), streams, selected)
151
+ return Bind(port=port, mode=_name(MODE, mode), streams=streams, selected=selected)
148
152
  return (tag, v)
149
153
 
150
154
 
@@ -170,6 +174,7 @@ class State:
170
174
  storage_bytes: int = 0
171
175
  storage: str = "none" # none, applied, unreadable
172
176
  saved_hash: int = 0
177
+ unreadable: str | None = None # why, when unreadable (probe.config §4)
173
178
  items: list[int] = field(default_factory=list)
174
179
  slots_max: int = 0
175
180
  bind_modes: list[str] = field(default_factory=list)
@@ -216,6 +221,8 @@ class ProbeConfig(Interface):
216
221
  if tag == DESCRIBE["storage"] and len(v) >= 9:
217
222
  st.storage_bytes, state, st.saved_hash = struct.unpack_from("<IBI", v)
218
223
  st.storage = STORAGE_STATE.get(state, str(state))
224
+ if len(v) >= 14 and v[13]:
225
+ st.unreadable = UNREADABLE.get(v[13], str(v[13]))
219
226
  elif tag == DESCRIBE["items"]:
220
227
  st.items = list(v)
221
228
  elif tag == DESCRIBE["slots_max"] and v:
oep_client/console.py CHANGED
@@ -65,7 +65,7 @@ class PositionStream(Interface):
65
65
  """One answer's marks with serial >= from_serial (in serial order). -> (marks, more)."""
66
66
  rd = m.Reader(self._call(self.MARKS, self._stream_prefix() + struct.pack("<I", from_serial), locked=False).payload)
67
67
  more, count = rd.take("BB")
68
- marks = [Mark(*rd.take("IQBIB")) for _ in range(count)]
68
+ marks = [Mark(*rd.element().take("IQBIB")) for _ in range(count)]
69
69
  rd.tail()
70
70
  return marks, bool(more)
71
71
 
@@ -136,10 +136,12 @@ class StreamIO:
136
136
  self.lost = 0
137
137
 
138
138
  def _limits(self) -> tuple[int, int]:
139
- """(read, write) chunk sizes that fit the probe's frame: request header 6 + session 4 + stream 1 + count 2,
140
- result header 5 + start 8 + flags 1."""
139
+ """(read, write) chunk sizes that fit the probe's frame: a write is request header 6 + session 4 + the stream's
140
+ prefix (the console's stream u16, none on a fixture UART) + count 2; a read's result is header 5 + start 8 +
141
+ flags 1. (The write was frame - 13 for both: one byte over on the console's 64-byte frames.)"""
141
142
  frame = self.source.host.confirmed()["max_frame"]
142
- return max(1, min(self.MAX_READ, frame - 14)), max(1, min(self.MAX_WRITE, frame - 13))
143
+ write = frame - 12 - len(self.source._stream_prefix())
144
+ return max(1, min(self.MAX_READ, frame - 14)), max(1, min(self.MAX_WRITE, write))
143
145
 
144
146
  def read(self, n: int = 512) -> bytes:
145
147
  c = self.source.read_from(self.position, min(n, self._limits()[0]))
oep_client/decode.py CHANGED
@@ -1,4 +1,4 @@
1
- """Host-side decoders for capture channels (oep.fixture.capture: `LogicCapture.channel(data, k)` gives one value
1
+ """Host-side decoders for capture channels (oep.fixture.logic: `LogicCapture.channel(data, k)` gives one value
2
2
  per sample)."""
3
3
 
4
4
  from __future__ import annotations
oep_client/endpoint.py CHANGED
@@ -68,6 +68,25 @@ class Reject(Exception):
68
68
  self.reason, self.payload = reason, payload
69
69
 
70
70
 
71
+ _UNA = reg.CORE.tlv["unavailable_payload"]
72
+ UNAVAILABLE_CAUSE, HOLDER_KIND = reg.CORE.enum["unavailable_cause"], reg.CORE.enum["holder_kind"]
73
+
74
+
75
+ def unavailable(cause: str | None = None, channel: int | None = None, holder_fn: int | None = None,
76
+ holder_kind: str | None = None, extra: bytes = b"") -> Reject:
77
+ """rejected unavailable with core §4.3's payload: why, the channel, who holds it (each optional)."""
78
+ body = b""
79
+ if cause:
80
+ body += m.tlv(_UNA["cause"], bytes([UNAVAILABLE_CAUSE[cause]]))
81
+ if channel is not None:
82
+ body += m.tlv(_UNA["channel"], struct.pack("<H", channel))
83
+ if holder_fn is not None:
84
+ body += m.tlv(_UNA["holder_fn"], struct.pack("<H", holder_fn))
85
+ if holder_kind:
86
+ body += m.tlv(_UNA["holder_kind"], bytes([HOLDER_KIND[holder_kind]]))
87
+ return Reject(m.UNAVAILABLE, body + extra)
88
+
89
+
71
90
  class Take:
72
91
  """Reads a request's fixed part; too short -> rejected malformed. `tail(known)` applies the request-tail rule."""
73
92
 
@@ -241,6 +260,7 @@ class Endpoint:
241
260
  self.window, self.max_inflight = window, max_inflight
242
261
  self.remember_max = remember_max
243
262
  self.names = {o.fn: o.name for o in probe.offered}
263
+ self.identity = {o.fn: (o.name, o.instance, o.revision) for o in probe.offered} # what a saved item names
244
264
  self.fns = {name: fn for fn, name in sorted(self.names.items(), reverse=True)} # first fn of each name
245
265
  self.static = {o.fn: o.tlvs for o in probe.offered}
246
266
  self.static_labels: dict[int, str] = {}
@@ -277,7 +297,7 @@ class Endpoint:
277
297
  self.target = next(iter(self.targets.values()), FakeTarget())
278
298
  self.captures: dict[int, fake_capture.FakeCapture] = {
279
299
  fn: self._capture_from(self.static[fn]) for fn, name in self.names.items()
280
- if name in ("oep.fixture.capture", "oep.fixture.analog")}
300
+ if name in ("oep.fixture.logic", "oep.fixture.analog")}
281
301
  self.groups: dict[int, fake_capture.FakeGroup] = {
282
302
  fn: self._group_from(self.static[fn]) for fn, name in self.names.items() if name == "oep.fixture.capture-group"}
283
303
  self.mechanisms = set()
@@ -329,6 +349,8 @@ class Endpoint:
329
349
  self.uart_tx: dict[int, bytearray] = {} # what a serial port's raw bytes sent out on a fixture UART
330
350
  self.config: dict[tuple[int, int], bytes | list[bytes]] = {} # (item tag, key) -> value (plan: list)
331
351
  self.saved: dict | None = getattr(self, "saved", None)
352
+ self.saved_ids: dict[int, tuple] = getattr(self, "saved_ids", {}) # saved fn -> (name, instance, revision)
353
+ self.saved_reason = 0 # why the saved settings were not applied (probe.config §4)
332
354
  self.slots: dict[int, Slot] = {}
333
355
  self.binds: dict[int, Bind] = {}
334
356
  self.slot_rt: dict[int, SlotRuntime] = {}
@@ -338,7 +360,7 @@ class Endpoint:
338
360
  self.held_ports: set[int] = set()
339
361
  self.session_resets: dict[tuple[int, int], tuple[object, int]] = {} # stream key -> (sid, position)
340
362
  if self.saved is not None:
341
- self._apply_config(dict(self.saved), boot=True)
363
+ self._apply_saved()
342
364
 
343
365
  @property
344
366
  def target_id(self) -> int | None:
@@ -351,7 +373,7 @@ class Endpoint:
351
373
  @staticmethod
352
374
  def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
353
375
  """A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
354
- d = reg.FIXTURE_CAPTURE.tlv["describe"]
376
+ d = reg.FIXTURE_LOGIC.tlv["describe"]
355
377
  modes, widths, lo, hi, ring, most, fronts = set(), {8}, 1, 1_000_000, 1, 1024, {}
356
378
  for t in tlvs:
357
379
  tag, v = t[0], t[2:2 + t[1]]
@@ -427,7 +449,7 @@ class Endpoint:
427
449
  cap.slipped = self.capture_slipped
428
450
  return self._answer(cap.answer(settled), ignored)
429
451
  if op in (O["start"], O["stop"], O["force"]) and cap.group is not None:
430
- raise Reject(m.UNAVAILABLE) # bound: the group starts and stops it
452
+ raise unavailable("bound_in_group") # bound: the group starts and stops it
431
453
  if op == O["start"]:
432
454
  t.tail()
433
455
  self._events(fn, cap.start(self.now()))
@@ -786,7 +808,8 @@ class Endpoint:
786
808
  return m.REJECTED, m.UNSUPPORTED, bytes([tag])
787
809
  named = {fn for fn, _, _ in got}
788
810
  if named & self.plan_from_config: # the settings' plan is the settings' (core §8)
789
- raise Reject(m.UNAVAILABLE)
811
+ raise unavailable("held_by_settings", holder_fn=min(named & self.plan_from_config),
812
+ holder_kind="settings_plan")
790
813
  self._check_plan(got)
791
814
  self.plan = {a for a in self.plan if a[0] not in named} | set(got)
792
815
  return m.COMPLETED, m.SUCCESS, b""
@@ -812,21 +835,31 @@ class Endpoint:
812
835
  slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
813
836
  slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
814
837
  analog = {fn for fn, cap in self.captures.items() if cap.analog}
838
+ def held(ch: int, holder: int | None) -> Reject:
839
+ if holder is not None:
840
+ return unavailable("pin_in_use", ch, holder, "plan")
841
+ slot = next((n for n, s in self.slots.items() if ch in s.pair), None)
842
+ conn = next((c.fn for c in self.conns.values() if ch in c.pair), None)
843
+ return unavailable("pin_in_use", ch, conn, "slot" if slot is not None else "connection")
815
844
  for fn, role, ch in got:
816
845
  beside = [f for f, _, c in got if c == ch and f != fn] + [a[0] for a in others if a[2] == ch]
817
846
  if fn in analog: # the analog shares its pin with nothing: another fn's plan, a slot, a connection
818
- if role not in self._declared_roles(fn, ch) or beside or ch in slot_pins:
819
- raise Reject(m.UNAVAILABLE)
847
+ if role not in self._declared_roles(fn, ch):
848
+ raise unavailable(channel=ch)
849
+ if beside or ch in slot_pins:
850
+ raise held(ch, beside[0] if beside else None)
820
851
  continue
821
852
  if any(f in analog for f in beside): # nor may anything come onto an analog pin
822
- raise Reject(m.UNAVAILABLE)
853
+ raise held(ch, next(f for f in beside if f in analog))
823
854
  if fn in self.captures:
824
855
  if role not in self._declared_roles(fn, ch):
825
- raise Reject(m.UNAVAILABLE)
856
+ raise unavailable(channel=ch)
826
857
  continue
827
858
  roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
828
- if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
829
- raise Reject(m.UNAVAILABLE)
859
+ if role not in roles:
860
+ raise unavailable(channel=ch)
861
+ if any(k[2] == ch for k in kept) or ch in slot_pins:
862
+ raise held(ch, next((k[0] for k in kept if k[2] == ch), None))
830
863
 
831
864
  def _declared_roles(self, fn: int, channel: int) -> set[int]:
832
865
  """The roles fn's describe offers on `channel` (role_channels)."""
@@ -853,8 +886,9 @@ class Endpoint:
853
886
  elif self.names[fn] == "oep.probe.config":
854
887
  tlvs = [t for t in tlvs if t[0] != CFG_DESCRIBE["storage"]]
855
888
  saved_hash = self._hash(self.saved) if self.saved is not None else 0
889
+ state = 0 if self.saved is None else 2 if self.saved_reason else 1
856
890
  tlvs.insert(0, catalog.tlv(CFG_DESCRIBE["storage"], struct.pack(
857
- "<IBII", self.storage_max, 0 if self.saved is None else 1, saved_hash, 20)))
891
+ "<IBIIB", self.storage_max, state, saved_hash, 20, self.saved_reason)))
858
892
  tlvs += [catalog.tlv(CFG_DESCRIBE["slot_state"], self._slot_state(n)) for n in sorted(self.slots)]
859
893
  tlvs += [catalog.tlv(CFG_DESCRIBE["bind_state"], self._bind_state(p)) for p in sorted(self.binds)]
860
894
  return tlvs
@@ -890,7 +924,7 @@ class Endpoint:
890
924
  for p in pairs:
891
925
  tg = self._target(fn, p)
892
926
  if tg.present: # a live connection's pair: read over it, no restart
893
- found.append(struct.pack("<BHHI", 1, *p, tg.dmstatus()))
927
+ found.append(m.element(struct.pack("<BHHI", 1, *p, tg.dmstatus())))
894
928
  return m.COMPLETED, m.SUCCESS, struct.pack("<BB", len(pairs), len(found)) + b"".join(found)
895
929
  if op in (0x02, 0x04): # attach, attach_under_reset
896
930
  if op == 0x02:
@@ -956,7 +990,7 @@ class Endpoint:
956
990
  users = (1 if "host" in c.users else 0) | (2 if any(u != "host" for u in c.users) else 0)
957
991
  slot = next((n for n, s in self.slots.items() if s.wire_fn == fn and s.pair == c.pair), NO_SLOT)
958
992
  tid = b"" if c.tid is None else struct.pack("<I", c.tid)
959
- out += struct.pack("<HHHIBBBB", cid, *c.pair, c.speed, users, slot, 1 if tid else 0, len(tid)) + tid
993
+ out += m.element(struct.pack("<HHHIBBBB", cid, *c.pair, c.speed, users, slot, 1 if tid else 0, len(tid)) + tid)
960
994
  return m.COMPLETED, m.SUCCESS, bytes(out)
961
995
  return m.REJECTED, m.UNKNOWN_OPERATION, b""
962
996
 
@@ -1188,7 +1222,7 @@ class Endpoint:
1188
1222
  hits = [mk for mk in s.marks if m.serial_diff(mk[0], frm) >= 0]
1189
1223
  page = hits[:self.MARKS_PER_ANSWER]
1190
1224
  body = struct.pack("<BB", int(len(hits) > len(page)), len(page))
1191
- body += b"".join(struct.pack("<IQBIB", *mk) for mk in page)
1225
+ body += b"".join(m.element(struct.pack("<IQBIB", *mk)) for mk in page)
1192
1226
  return self._answer(body, ignored)
1193
1227
  if s.closed:
1194
1228
  raise Reject(m.UNAVAILABLE)
@@ -1225,7 +1259,7 @@ class Endpoint:
1225
1259
  sid = t.take("H")
1226
1260
  s = self.streams.get(sid)
1227
1261
  if s is None:
1228
- raise Reject(m.UNAVAILABLE)
1262
+ raise Reject(m.NO_CONNECTION) # a number it does not know (core §4.3)
1229
1263
  if op == _CON.op["close"]:
1230
1264
  t.tail()
1231
1265
  s.closed = True
@@ -1268,8 +1302,8 @@ class Endpoint:
1268
1302
  pairs = [t.take("HB") for _ in range(n)]
1269
1303
  t.tail()
1270
1304
  for i, (ch, mode) in enumerate(pairs):
1271
- if ch not in mine or mode > 7:
1272
- raise Reject(m.UNAVAILABLE, bytes([i]))
1305
+ if ch not in mine or mode > 7: # the position as the gpio's TLV (fixture §1)
1306
+ raise unavailable(channel=ch, extra=m.tlv(_GPIO.tlv["unavailable_payload"]["index"], bytes([i])))
1273
1307
  for ch, mode in pairs:
1274
1308
  self.gpio_modes[ch] = mode
1275
1309
  self.gpio_log.append((ch, mode))
@@ -1280,7 +1314,7 @@ class Endpoint:
1280
1314
  _, ignored = t.tail()
1281
1315
  for i, ch in enumerate(chans):
1282
1316
  if ch not in mine:
1283
- raise Reject(m.UNAVAILABLE, bytes([i]))
1317
+ raise unavailable(channel=ch, extra=m.tlv(_GPIO.tlv["unavailable_payload"]["index"], bytes([i])))
1284
1318
  levels = []
1285
1319
  for ch in chans:
1286
1320
  mode = self.gpio_modes.get(ch, 0)
@@ -1359,12 +1393,14 @@ class Endpoint:
1359
1393
  if op == _CFG.op["save"]:
1360
1394
  t.tail()
1361
1395
  if len(b"".join(self._canonical(self.config))) > self.storage_max:
1362
- raise Reject(m.UNAVAILABLE)
1396
+ raise unavailable("storage_full")
1363
1397
  self.saved = dict(self.config)
1398
+ self.saved_ids = {fn: self.identity[fn] for fn in self._referenced(self.config) if fn in self.identity}
1399
+ self.saved_reason = 0
1364
1400
  return m.COMPLETED, m.SUCCESS, struct.pack("<I", self._hash(self.config))
1365
1401
  if op == _CFG.op["erase"]:
1366
1402
  t.tail()
1367
- self.saved = None
1403
+ self.saved, self.saved_ids, self.saved_reason = None, {}, 0
1368
1404
  return m.COMPLETED, m.SUCCESS, b""
1369
1405
  return m.REJECTED, m.UNKNOWN_OPERATION, b""
1370
1406
 
@@ -1389,6 +1425,51 @@ class Endpoint:
1389
1425
  def _hash(self, config: dict | None) -> int:
1390
1426
  return zlib.crc32(b"".join(self._canonical(config or {})))
1391
1427
 
1428
+ @staticmethod
1429
+ def _referenced(config: dict) -> set[int]:
1430
+ """The fns saved items name (probe.config §2): plan fns, slot wire_fns, fixture UARTs a bind carries."""
1431
+ fns = set()
1432
+ for (tag, key), value in config.items():
1433
+ if tag == ITEM["plan"]:
1434
+ fns.add(key)
1435
+ elif tag == ITEM["slot"]:
1436
+ fns.add(struct.unpack_from("<H", value, 1)[0])
1437
+ elif tag == ITEM["bind"]:
1438
+ for k in range(value[3]):
1439
+ if value[4 + 3 * k] == BIND_STREAM["fixture_uart"]:
1440
+ fns.add(struct.unpack_from("<H", value, 5 + 3 * k)[0])
1441
+ return fns
1442
+
1443
+ def _apply_saved(self) -> None:
1444
+ """At boot: the saved items' fns found again by (name, instance, revision) and renumbered, then applied; one
1445
+ not found (or of another revision) leaves the whole unapplied (probe.config §2)."""
1446
+ now = {ident: fn for fn, ident in self.identity.items()}
1447
+ remap = {}
1448
+ for fn, ident in self.saved_ids.items():
1449
+ if ident not in now:
1450
+ self.saved_reason = 2
1451
+ return
1452
+ remap[fn] = now[ident]
1453
+ pack = lambda fn: struct.pack("<H", remap.get(fn, fn))
1454
+ new = {}
1455
+ for (tag, key), value in self.saved.items():
1456
+ if tag == ITEM["plan"]:
1457
+ new[(tag, remap.get(key, key))] = [pack(key) + v[2:] for v in value]
1458
+ elif tag == ITEM["slot"]:
1459
+ new[(tag, key)] = value[:1] + pack(struct.unpack_from("<H", value, 1)[0]) + value[3:]
1460
+ elif tag == ITEM["bind"]:
1461
+ v = bytearray(value)
1462
+ for k in range(v[3]):
1463
+ if v[4 + 3 * k] == BIND_STREAM["fixture_uart"]:
1464
+ v[5 + 3 * k:7 + 3 * k] = pack(struct.unpack_from("<H", v, 5 + 3 * k)[0])
1465
+ new[(tag, key)] = bytes(v)
1466
+ else:
1467
+ new[(tag, key)] = value
1468
+ try:
1469
+ self._apply_config(new, boot=True)
1470
+ except Reject:
1471
+ self.saved_reason = 3
1472
+
1392
1473
  def load_config(self, items: list[bytes], saved: bool = True) -> None:
1393
1474
  """Put `items` (item TLVs) in as the config, as a set would; with `saved` they are also the saved config
1394
1475
  (a probe that booted with them)."""
@@ -1402,6 +1483,7 @@ class Endpoint:
1402
1483
  self._apply_config(new, changed_slots=None)
1403
1484
  if saved:
1404
1485
  self.saved = dict(self.config)
1486
+ self.saved_ids = {fn: self.identity[fn] for fn in self._referenced(self.config) if fn in self.identity}
1405
1487
 
1406
1488
  def _apply_config(self, new: dict, changed_slots: set[int] | None = None, boot: bool = False) -> None:
1407
1489
  """Check the whole config, then make it the current one (set is all-or-nothing up to reserving resources);
@@ -1469,8 +1551,8 @@ class Endpoint:
1469
1551
  t = Take(v)
1470
1552
  n, wire_fn, swdio, swclk, attach, retry_s, max_speed, idle_clock, mech, name_len = t.take("BHHHBHIBBB")
1471
1553
  name = t.bytes(name_len)
1472
- scheme = t.take("B")
1473
- rest = v[t.at:]
1554
+ lock_len = t.take("B") # the lock's part; 0 = none (probe.config §1.1)
1555
+ lock_part = t.bytes(lock_len) # what follows is for later fields: skipped (core §2.3)
1474
1556
  if n >= self.slots_max or attach not in SLOT_ATTACH.values():
1475
1557
  raise Reject(m.MALFORMED)
1476
1558
  if retry_s and attach != SLOT_ATTACH["at_boot"]:
@@ -1484,19 +1566,18 @@ class Endpoint:
1484
1566
  if mech not in self.mechanisms:
1485
1567
  raise Reject(m.UNSUPPORTED)
1486
1568
  lock = None
1487
- if scheme:
1488
- if not rest or len(rest) % 2:
1569
+ if lock_len:
1570
+ if lock_len < 3 or lock_len % 2 == 0 or lock_part[0] == 0:
1489
1571
  raise Reject(m.MALFORMED)
1490
- lock = (scheme, rest[:len(rest) // 2], rest[len(rest) // 2:])
1491
- elif rest:
1492
- raise Reject(m.MALFORMED)
1572
+ half = (lock_len - 1) // 2
1573
+ lock = (lock_part[0], lock_part[1:1 + half], lock_part[1 + half:])
1493
1574
  return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, max_speed, idle_clock, mech, name.decode(), lock)
1494
1575
 
1495
1576
  def _parse_bind(self, v: bytes, slots: dict[int, Slot]) -> Bind:
1496
1577
  t = Take(v)
1497
1578
  port, mode, selected, n = t.take("BBBB")
1498
1579
  streams = tuple(t.take("BH") for _ in range(n))
1499
- if t.at != len(v) or n == 0:
1580
+ if n == 0: # after the streams: later fields, skipped
1500
1581
  raise Reject(m.MALFORMED)
1501
1582
  if port not in self.serial_ports:
1502
1583
  raise Reject(m.UNAVAILABLE)
@@ -1735,5 +1816,5 @@ class Endpoint:
1735
1816
 
1736
1817
  SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
1737
1818
  "oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
1738
- "oep.probe.config": "config_op", "oep.fixture.capture": "capture", "oep.fixture.analog": "capture",
1819
+ "oep.probe.config": "config_op", "oep.fixture.logic": "capture", "oep.fixture.analog": "capture",
1739
1820
  "oep.fixture.capture-group": "group"}