oep-client-python 0.0.53__py3-none-any.whl → 0.0.54__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.53"
5
+ __version__ = "0.0.54"
oep_client/__main__.py CHANGED
@@ -125,6 +125,17 @@ def main(argv=None) -> int:
125
125
  rs.add_argument("--reopen-s", type=float, default=0.0, metavar="S",
126
126
  help="when the probe is not back within its restart_max_ms, open it again for up to S more seconds "
127
127
  "(your reopen: a host where the device returns late, e.g. WSL re-attaching it through usbipd)")
128
+ ca = sub.add_parser("call", help="run any interface's op: by number with the request as hex bytes, or by the name and "
129
+ "fields its describe gives (core §7.4 op_name / op_field)")
130
+ ca.add_argument("probe", help="a probe: oep://UNIT_ID[?via=...], a serial port, tcp://HOST:PORT or usb")
131
+ ca.add_argument("interface", help="the interface's name (io.github.you.thing); its fn is looked up")
132
+ ca.add_argument("op", help="the op: a number (0x10) or its name")
133
+ ca.add_argument("args", nargs="*", help="NAME=VALUE per field (integers 10 or 0x, bools true / false, lists 1,2,3, "
134
+ "bytes hex, text as it is), or the request as hex bytes")
135
+ ca.add_argument("--text", action="store_true", help="the request is ARGS joined by spaces, as UTF-8 bytes")
136
+ ca.add_argument("--wait", type=float, metavar="S", help="the op is a job (oep::SimpleInterface::job): wait up to S "
137
+ "seconds for it by the interface's status op, then show that")
138
+ ca.add_argument("--json", action="store_true")
128
139
  fd = sub.add_parser("find", help="probes on the local network that announce _oep._tcp (DNS-SD over mDNS)")
129
140
  fd.add_argument("--timeout", type=float, default=2.0, help="seconds to listen for answers (default 2)")
130
141
  fd.add_argument("--engine", choices=("auto", "zeroconf", "minimal"), default="auto",
@@ -157,6 +168,8 @@ def main(argv=None) -> int:
157
168
  return _restart_cmd(args)
158
169
  if args.command == "find":
159
170
  return _find_cmd(args)
171
+ if args.command == "call":
172
+ return _call_cmd(args)
160
173
 
161
174
  confirm = (0, 1)
162
175
  if args.virtual:
@@ -237,6 +250,82 @@ def _restart_cmd(args) -> int:
237
250
  return 0
238
251
 
239
252
 
253
+ # ---- oep call -----------------------------------------------------------------------------------------------------
254
+
255
+ def _field_value(field, text: str):
256
+ from . import call as c
257
+ t = field.type
258
+ if t & c.LIST:
259
+ items = [x for x in text.replace(" ", "").split(",") if x]
260
+ return [(x.lower() in ("1", "true")) if (t & ~c.LIST) == c.T["bool"] else int(x, 0) for x in items]
261
+ if t == c.T["bool"]:
262
+ if text.lower() not in ("0", "1", "true", "false"):
263
+ raise SystemExit(f"oep call: {field.name} takes true or false")
264
+ return text.lower() in ("1", "true")
265
+ if t == c.T["bytes"]:
266
+ return bytes.fromhex(text)
267
+ if t == c.T["text"]:
268
+ return text
269
+ return int(text, 0)
270
+
271
+
272
+ def _call_cmd(args) -> int:
273
+ import json
274
+ from . import call as c
275
+ hst = _open_host(args.probe)
276
+ try:
277
+ forms = c.ops(hst, args.interface)
278
+ except c.NotOffered as e:
279
+ raise SystemExit(f"oep call: {e}") from None
280
+ op: int | str = int(args.op, 0) if args.op[:1].isdigit() else args.op
281
+ form = forms.get(op) if isinstance(op, int) else next((f for f in forms.values() if f.name == op), None)
282
+ payload, fields = None, {}
283
+ if args.text:
284
+ payload = " ".join(args.args).encode()
285
+ elif form is not None and form.form and (any("=" in a for a in args.args) or not args.args):
286
+ by_name = {f.name: f for f in form.request}
287
+ for item in args.args:
288
+ name, eq, value = item.partition("=")
289
+ if not eq or name not in by_name:
290
+ raise SystemExit(f"oep call: {form.signature()}: give NAME=VALUE for its fields ({item!r})")
291
+ fields[name] = _field_value(by_name[name], value)
292
+ else:
293
+ try:
294
+ payload = bytes.fromhex("".join(args.args))
295
+ except ValueError:
296
+ raise SystemExit("oep call: the request as hex bytes (its describe gives this op no fields)") from None
297
+ if not (form is not None and form.lock_free):
298
+ core.take(hst, 10000 + int((args.wait or 0) * 1000), owner="oep call") # a job's wait inside the lease
299
+ try:
300
+ if args.wait is not None:
301
+ r = c.call_wait(hst, args.interface, op, payload, timeout=args.wait, **fields)
302
+ else:
303
+ r = c.call(hst, args.interface, op, payload, **fields)
304
+ except (c.JobFailed, c.JobCancelled, c.JobTimeout, c.ProbeRestarted) as e:
305
+ st = getattr(e, "status", {})
306
+ print(json.dumps({"result": type(e).__name__, "why": str(e), "status": st}, default=str) if args.json
307
+ else f"{type(e).__name__} {e}")
308
+ return 1
309
+ except TypeError as e:
310
+ raise SystemExit(f"oep call: {e}") from None
311
+ except c.NotOffered as e:
312
+ raise SystemExit(f"oep call: {e}") from None
313
+ except c.CommandFailed as e:
314
+ out = {"result": "failed", "op": e.op, "fields": e.fields, "payload": e.payload.hex()}
315
+ print(json.dumps(out, default=str) if args.json else f"failed {e}" + ("" if e.fields else f" {e.payload.hex()}"))
316
+ return 1
317
+ except c.CommandRejected as e:
318
+ print(json.dumps({"result": "rejected", "op": e.op, "why": str(e)}) if args.json else f"rejected {e}")
319
+ return 1
320
+ shown = {k: (v.hex() if isinstance(v, bytes) else v) for k, v in r.items()}
321
+ if args.json:
322
+ print(json.dumps({"result": "completed", "revision": r.revision, "fields": shown, "payload": r.payload.hex()}))
323
+ else:
324
+ body = " ".join(f"{k}={v}" for k, v in shown.items()) if shown or (form and form.form) else r.payload.hex()
325
+ print(f"completed rev {r.revision} {body}".rstrip())
326
+ return 0
327
+
328
+
240
329
  # ---- oep pins -----------------------------------------------------------------------------------------------------
241
330
 
242
331
  def _pins_cmd(args) -> int:
oep_client/call.py ADDED
@@ -0,0 +1,284 @@
1
+ """Call any interface's op by its number, or by the name and fields its describe gives (oep-core §7.4 op_name /
2
+ op_field) - an interface this host has no module for, such as a sketch's oep::SimpleInterface.
3
+
4
+ from oep_client import link, core
5
+ from oep_client.call import call
6
+ hst = link.open_host("/dev/ttyACM0")
7
+ core.take(hst, 10000, owner="my-test")
8
+ r = call(hst, "io.github.yourname.mine", "add", a=2, b=3)
9
+ r["sum"], r.sum, r.revision # the success fields by name; the interface's revision
10
+ call(hst, "io.github.yourname.mine", 0x10, b"\\x02\\0\\0\\0\\x03\\0\\0\\0").payload # by number, the bytes as they are
11
+
12
+ Errors: InterfaceMissing / RevisionMismatch / OpMissing (all NotOffered: a test can turn them into a skip),
13
+ CommandRejected (the probe refused it: the reason), CommandFailed (completed failed: code and message when the op
14
+ gives them).
15
+
16
+ A job (oep::SimpleInterface::job: it starts work on the probe's worker and answers at once) and its status op:
17
+
18
+ st = call_wait(hst, "io.github.yourname.mine", "collect", count=3, timeout=10)
19
+ st.start_ns, st.end_ns # on the probe's clock (a capture's markers' one)
20
+ # wait=: a stream's wait (acquire.Request(during=...)) keeps the capture collecting while this waits
21
+
22
+ JobFailed (the job's Worker::fail: code, message), JobCancelled, JobTimeout (cancel sent first), ProbeRestarted (its
23
+ boot_id changed while the job ran)."""
24
+
25
+ from __future__ import annotations
26
+
27
+ import struct
28
+ import time
29
+ from typing import Any, Callable
30
+
31
+ from . import catalog, core, host as h
32
+
33
+ T = catalog.OP_FIELD_TYPES
34
+ LIST = catalog.OP_FIELD_LIST
35
+ _INT = {T["u8"]: "<B", T["u16"]: "<H", T["u32"]: "<I", T["u64"]: "<Q",
36
+ T["i8"]: "<b", T["i16"]: "<h", T["i32"]: "<i", T["i64"]: "<q", T["bool"]: "<B"}
37
+
38
+
39
+ class NotOffered(LookupError):
40
+ """The probe does not offer what was asked: no such interface, another revision, no such op."""
41
+
42
+
43
+ class InterfaceMissing(NotOffered):
44
+ pass
45
+
46
+
47
+ class RevisionMismatch(NotOffered):
48
+ def __init__(self, interface: str, revision: int, wanted: int):
49
+ super().__init__(f"{interface} is revision {revision} on this probe, {wanted} asked")
50
+ self.revision, self.wanted = revision, wanted
51
+
52
+
53
+ class OpMissing(NotOffered):
54
+ pass
55
+
56
+
57
+ class CommandRejected(h.Rejected):
58
+ """The probe refused the op (resolution rejected): `result.detail` is the reason (core §4.3)."""
59
+
60
+ def __init__(self, op: str, result):
61
+ super().__init__(result)
62
+ self.op = op
63
+ self.args = (f"{op}: {result.describe()}",)
64
+
65
+
66
+ class CommandFailed(h.Failed):
67
+ """The op ran and did not work (completed failed / partial): `fields` the failed part decoded (code, message for
68
+ an oep::SimpleInterface op), `payload` the bytes."""
69
+
70
+ def __init__(self, op: str, result, fields: dict | None):
71
+ why = f"{op}: {result.describe()}"
72
+ if fields and "code" in fields:
73
+ why += f" (code {fields['code']}" + (f": {fields['message']}" if fields.get("message") else "") + ")"
74
+ super().__init__(result, why)
75
+ self.op, self.fields, self.payload = op, fields or {}, result.payload
76
+ self.code, self.message = self.fields.get("code"), self.fields.get("message")
77
+
78
+
79
+ class JobFailed(h.OepError):
80
+ """The job ended failed: `code` and `message` from its Worker::fail(), `status` the status op's fields."""
81
+
82
+ def __init__(self, op: str, status: dict):
83
+ super().__init__(f"{op}: failed (code {status.get('code')}"
84
+ + (f": {status['message']}" if status.get("message") else "") + ")")
85
+ self.op, self.status, self.code, self.message = op, status, status.get("code"), status.get("message")
86
+
87
+
88
+ class JobCancelled(h.OepError):
89
+ """The job stopped on a cancel (`status` the status op's fields)."""
90
+
91
+ def __init__(self, op: str, status: dict):
92
+ super().__init__(f"{op}: cancelled")
93
+ self.op, self.status = op, status
94
+
95
+
96
+ class JobTimeout(h.OepError, TimeoutError):
97
+ """The job still ran at the timeout; a cancel was sent (`status` the last status op's fields)."""
98
+
99
+ def __init__(self, op: str, timeout: float, status: dict):
100
+ super().__init__(f"{op}: still running after {timeout} s (cancel sent)")
101
+ self.op, self.status = op, status
102
+
103
+
104
+ class ProbeRestarted(h.OepError):
105
+ """The probe's boot_id changed while the job ran (core §6.5): it restarted, the job with it."""
106
+
107
+
108
+ # The status op's state (oep::Worker::State): the firmware's convention, not the registry's
109
+ JOB_STATES = {"idle": 0, "running": 1, "done": 2, "failed": 3, "cancelled": 4}
110
+
111
+
112
+ class CallResult(dict):
113
+ """The success fields by name (a dict, and attributes), the raw payload, the interface's revision and the op."""
114
+
115
+ def __init__(self, fields: dict, payload: bytes, revision: int, op: catalog.OpForm | int):
116
+ super().__init__(fields)
117
+ self.payload, self.revision, self.op = payload, revision, op
118
+
119
+ def __getattr__(self, name: str) -> Any:
120
+ try:
121
+ return self[name]
122
+ except KeyError:
123
+ raise AttributeError(name) from None
124
+
125
+
126
+ def encode(fields: list[catalog.OpField], values: dict) -> bytes:
127
+ """A request's fixed part from values by field name (ints, bools, bytes, str, lists of ints / bools)."""
128
+ missing = [f.name for f in fields if f.name not in values]
129
+ extra = sorted(set(values) - {f.name for f in fields})
130
+ if missing or extra:
131
+ raise TypeError(f"fields {', '.join(f'{f.name}: {f.type_name}' for f in fields) or 'none'}"
132
+ + (f"; missing {missing}" if missing else "") + (f"; not fields {extra}" if extra else ""))
133
+ out = b""
134
+ for f in fields:
135
+ v = values[f.name]
136
+ if f.type & LIST:
137
+ items = list(v)
138
+ out += struct.pack("<H", len(items)) + b"".join(struct.pack(_INT[f.type & ~LIST], int(x)) for x in items)
139
+ elif f.type == T["bytes"]:
140
+ b = bytes(v)
141
+ out += struct.pack("<H", len(b)) + b
142
+ elif f.type == T["text"]:
143
+ b = str(v).encode()
144
+ out += struct.pack("<H", len(b)) + b
145
+ elif f.type in _INT:
146
+ out += struct.pack(_INT[f.type], int(v))
147
+ else:
148
+ raise TypeError(f"{f.name}: type {f.type} unknown to this host")
149
+ return out
150
+
151
+
152
+ def decode(fields: list[catalog.OpField], payload: bytes) -> tuple[dict, bytes]:
153
+ """A fixed part's fields by name, and what follows them (the TLVs)."""
154
+ out, at = {}, 0
155
+ for f in fields:
156
+ if f.type & LIST:
157
+ (n,) = struct.unpack_from("<H", payload, at)
158
+ fmt = _INT[f.type & ~LIST]
159
+ w = struct.calcsize(fmt)
160
+ vals = [struct.unpack_from(fmt, payload, at + 2 + i * w)[0] for i in range(n)]
161
+ out[f.name] = [bool(x) for x in vals] if (f.type & ~LIST) == T["bool"] else vals
162
+ at += 2 + n * w
163
+ elif f.type in (T["bytes"], T["text"]):
164
+ (n,) = struct.unpack_from("<H", payload, at)
165
+ if at + 2 + n > len(payload):
166
+ raise h.ShortPayload(f"{f.name}: {n} bytes, the answer ends before")
167
+ raw = payload[at + 2:at + 2 + n]
168
+ out[f.name] = raw.decode("utf-8", "replace") if f.type == T["text"] else raw
169
+ at += 2 + n
170
+ else:
171
+ fmt = _INT[f.type]
172
+ out[f.name] = struct.unpack_from(fmt, payload, at)[0]
173
+ if f.type == T["bool"]:
174
+ out[f.name] = bool(out[f.name])
175
+ at += struct.calcsize(fmt)
176
+ return out, payload[at:]
177
+
178
+
179
+ def ops(hst: h.Host, interface: str | int) -> dict[int, catalog.OpForm]:
180
+ """The op_name / op_field an interface's describe gives (by op number; empty when it gives none)."""
181
+ fn = interface if isinstance(interface, int) else _fn(hst, interface)
182
+ return catalog.decode_description(b"".join(catalog.tlv(t, v) for t, v in core.describe(hst, fn))).op_forms
183
+
184
+
185
+ def _fn(hst: h.Host, interface: str) -> int:
186
+ try:
187
+ return core.find(hst, interface)
188
+ except LookupError as e:
189
+ raise InterfaceMissing(str(e)) from None
190
+
191
+
192
+ def call(hst: h.Host, interface: str, op: int | str, payload: bytes | None = None, *, locked: bool | None = None,
193
+ revision: int | None = None, expect_ms: int = 0, **fields) -> CallResult:
194
+ """Run `op` (a number, or a name from the describe's op_name) of `interface` on the session `hst` holds.
195
+ payload: the request as bytes (then no fields); otherwise the fields by name, packed as op_field says.
196
+ locked: send it in the session (default: unless op_name says lock_free). revision: the one expected."""
197
+ fn = _fn(hst, interface)
198
+ rev = core.revision(hst, interface, fn)
199
+ if revision is not None and rev != revision:
200
+ raise RevisionMismatch(interface, rev, revision)
201
+ forms = ops(hst, fn)
202
+ if isinstance(op, str):
203
+ form = next((f for f in forms.values() if f.name == op), None)
204
+ if form is None:
205
+ raise OpMissing(f"{interface} has no op named {op!r}" + (f" (it names {', '.join(f.name for f in forms.values())})"
206
+ if forms else " (its describe names none)"))
207
+ code = form.op
208
+ else:
209
+ code, form = op, forms.get(op)
210
+ label = form.name if form else f"0x{code:02x}"
211
+ if not core.offers(hst, fn, code):
212
+ raise OpMissing(f"{interface} does not offer op {label}")
213
+ if payload is None:
214
+ if fields:
215
+ if form is None or not form.form:
216
+ raise TypeError(f"{interface} {label}: its describe gives no fields; pass payload= as bytes")
217
+ payload = encode(form.request, fields)
218
+ elif form is not None and form.form and form.request:
219
+ payload = encode(form.request, fields) # raises: the fields it needs
220
+ else:
221
+ payload = b""
222
+ elif fields:
223
+ raise TypeError("payload= or fields, not both")
224
+ if locked is None:
225
+ locked = not (form is not None and form.lock_free)
226
+ try:
227
+ r = hst.request(fn, code, payload, locked=locked, expect_ms=expect_ms)
228
+ except h.Rejected as e:
229
+ raise CommandRejected(label, e.result) from None
230
+ if r.succeeded:
231
+ got = decode(form.success, r.payload)[0] if form is not None and form.form else {}
232
+ return CallResult(got, r.payload, rev, form or code)
233
+ failed = None
234
+ if r.ran and form is not None and form.form and form.failed:
235
+ try:
236
+ failed = decode(form.failed, r.payload)[0]
237
+ except (struct.error, h.ShortPayload):
238
+ failed = None # a failed answer without its fields (a firmware bug)
239
+ raise CommandFailed(label, r, failed)
240
+
241
+
242
+ def call_wait(hst: h.Host, interface: str, op: int | str, payload: bytes | None = None, *, timeout: float,
243
+ wait: Callable[[float], None] | None = None, interval: float = 0.05, status: str = "status",
244
+ cancel: str = "cancel", revision: int | None = None, **fields) -> CallResult:
245
+ """Start the job `op` (as call()) and wait for it by its `status` op: the status's fields when it ended done.
246
+ wait(seconds) sleeps between the status reads (time.sleep by default; a stream's wait keeps a capture going)."""
247
+ boot = hst.clock().boot_id
248
+ started = call(hst, interface, op, payload, revision=revision, **fields)
249
+ code = started.op.op if isinstance(started.op, catalog.OpForm) else started.op
250
+ label = started.op.name if isinstance(started.op, catalog.OpForm) else f"0x{code:02x}"
251
+ pause = wait or time.sleep
252
+ deadline = time.monotonic() + timeout
253
+ while True:
254
+ try:
255
+ st = call(hst, interface, status)
256
+ except (h.OepError, OSError):
257
+ _restarted(hst, boot, label)
258
+ raise
259
+ if st["op"] != code or st["state"] == JOB_STATES["idle"]:
260
+ _restarted(hst, boot, label)
261
+ raise ProbeRestarted(f"{label}: the status names op {st['op']} in state {st['state']}, not this job")
262
+ if st["state"] != JOB_STATES["running"]:
263
+ break
264
+ if time.monotonic() >= deadline:
265
+ try:
266
+ call(hst, interface, cancel)
267
+ except (h.OepError, OSError):
268
+ pass # the timeout is the error to report
269
+ raise JobTimeout(label, timeout, dict(st))
270
+ pause(interval)
271
+ if st["state"] == JOB_STATES["failed"]:
272
+ raise JobFailed(label, dict(st))
273
+ if st["state"] == JOB_STATES["cancelled"]:
274
+ raise JobCancelled(label, dict(st))
275
+ return st
276
+
277
+
278
+ def _restarted(hst: h.Host, boot: int, label: str) -> None:
279
+ try:
280
+ now = hst.clock().boot_id
281
+ except (h.OepError, OSError):
282
+ return
283
+ if now != boot:
284
+ raise ProbeRestarted(f"{label}: the probe restarted while the job ran (boot_id {boot:#x} -> {now:#x})")
oep_client/catalog.py CHANGED
@@ -32,6 +32,13 @@ MIN_CLOCK_HZ = _COMMON["min_clock_hz"] # u32
32
32
  FEATURES = _COMMON["features"] # u32, bits defined by the interface
33
33
  CHANNEL_GROUP = _COMMON["channel_group"] # group(u8) n(u8) then n x (role(u8), channel(u16)): a fixed pin set
34
34
  OPS = _COMMON["ops"] # base(u8) bitmap: the ops this fn offers (core §1.2, §7.4)
35
+ OP_NAME = _COMMON["op_name"] # op(u8) flags(u8) name: an op's name (core §7.4, optional)
36
+ OP_FIELD = _COMMON["op_field"] # op(u8) part(u8) type(u8) name: one field of an op's fixed part (core §7.4)
37
+ OP_NAME_FORM = reg.CORE.enum["op_name_flag"]["form"]
38
+ OP_NAME_LOCK_FREE = reg.CORE.enum["op_name_flag"]["lock_free"]
39
+ OP_FIELD_PARTS = reg.CORE.enum["op_field_part"] # request / success / failed
40
+ OP_FIELD_TYPES = reg.CORE.enum["op_field_type"] # u8 ... text, and the list bit
41
+ OP_FIELD_LIST = OP_FIELD_TYPES["list"]
35
42
  assert OPS == m.TAG_OPS
36
43
  CRITICAL = 0x80
37
44
  INTERFACE_TAG_FIRST = 0x40
@@ -197,6 +204,50 @@ def text(tag: int, s: str) -> bytes:
197
204
  return tlv(tag, s.encode("ascii"))
198
205
 
199
206
 
207
+ class OpField:
208
+ """One field of an op's fixed part (core §7.4 op_field): its name and type (registry op_field_type)."""
209
+
210
+ def __init__(self, name: str, type: int):
211
+ self.name, self.type = name, type
212
+
213
+ @property
214
+ def type_name(self) -> str:
215
+ names = {v: k for k, v in OP_FIELD_TYPES.items() if k != "list"}
216
+ base = self.type & ~OP_FIELD_LIST
217
+ label = names.get(base, f"type{base}")
218
+ return label + "[]" if self.type & OP_FIELD_LIST else label
219
+
220
+ def __repr__(self) -> str:
221
+ return f"{self.name}: {self.type_name}"
222
+
223
+
224
+ @dataclass
225
+ class OpForm:
226
+ """An op's name and, with form, its request / success / failed fixed parts' fields (core §7.4)."""
227
+
228
+ op: int
229
+ name: str
230
+ flags: int
231
+ request: list[OpField] = field(default_factory=list)
232
+ success: list[OpField] = field(default_factory=list)
233
+ failed: list[OpField] = field(default_factory=list)
234
+
235
+ @property
236
+ def form(self) -> bool:
237
+ return bool(self.flags & OP_NAME_FORM)
238
+
239
+ @property
240
+ def lock_free(self) -> bool:
241
+ return bool(self.flags & OP_NAME_LOCK_FREE)
242
+
243
+ def signature(self) -> str:
244
+ """measure(mode: u8) -> temp: i32, hum: u32 (the name alone without form)"""
245
+ if not self.form:
246
+ return self.name
247
+ s = f"{self.name}({', '.join(map(repr, self.request))})"
248
+ return s + (" -> " + ", ".join(map(repr, self.success)) if self.success else "")
249
+
250
+
200
251
  @dataclass
201
252
  class Description:
202
253
  """Common tags decoded; interface-specific and unknown tags kept raw, in order."""
@@ -212,11 +263,13 @@ class Description:
212
263
  specific: list[tuple[int, bytes]] = field(default_factory=list)
213
264
  unknown_critical: list[int] = field(default_factory=list)
214
265
  broken: list[int] = field(default_factory=list) # common tags whose value is not their defined length (not used)
266
+ op_forms: dict[int, OpForm] = field(default_factory=dict) # op -> its op_name / op_field (core §7.4)
215
267
 
216
268
 
217
269
  def decode_description(data: bytes) -> Description:
218
270
  d = Description()
219
271
  seen: set[int] = set()
272
+ fields: list[tuple[int, int, int, str]] = [] # op_field (op, part, type, name), in the describe's order
220
273
  for tag, value in split_tlv(data):
221
274
  if tag & CRITICAL:
222
275
  continue # bit 7 in an answer: an unknown tag, skipped (core §2.2)
@@ -248,11 +301,28 @@ def decode_description(data: bytes) -> Description:
248
301
  d.ops_invalid = why
249
302
  else:
250
303
  d.ops = unpack_ops(value)
304
+ elif t == OP_NAME:
305
+ if len(value) < 3:
306
+ d.broken.append(tag)
307
+ continue
308
+ if value[0] in d.op_forms:
309
+ continue # a second op_name for an op: the first is used
310
+ d.op_forms[value[0]] = OpForm(value[0], value[2:].decode("ascii", "replace"), value[1])
311
+ elif t == OP_FIELD:
312
+ if len(value) < 4:
313
+ d.broken.append(tag)
314
+ continue
315
+ fields.append((value[0], value[1], value[2], value[3:].decode("ascii", "replace")))
251
316
  elif t >= INTERFACE_TAG_FIRST:
252
317
  d.specific.append((tag, value))
253
318
  elif tag & CRITICAL:
254
319
  d.unknown_critical.append(tag)
255
320
  # an unknown non-critical common tag is skipped
321
+ parts = {OP_FIELD_PARTS["request"]: "request", OP_FIELD_PARTS["success"]: "success", OP_FIELD_PARTS["failed"]: "failed"}
322
+ for op, part, type_, name in fields: # only an op named with form takes its fields
323
+ form = d.op_forms.get(op)
324
+ if form is not None and form.form and part in parts:
325
+ getattr(form, parts[part]).append(OpField(name, type_))
256
326
  for role in d.roles:
257
327
  d.roles[role] = sorted(set(d.roles[role]))
258
328
  if d.ops_invalid:
@@ -145,7 +145,7 @@ def checker_sources_sha256():
145
145
  for name in CHECKER_SOURCES}
146
146
 
147
147
 
148
- REVIEWED_CORE_SHA256 = '0408d232e728bb9692ab1c24cefa1fdbf109c35aeebd378034d86c0f4d19789a' # oep-spec docs/oep-core.ja.md
148
+ REVIEWED_CORE_SHA256 = '81cffc532f2626812b109d0aa5eef81636799011f410a8bc2fb9ea520fcbf4bc' # oep-spec docs/oep-core.ja.md
149
149
 
150
150
 
151
151
  def require_reviewed_contract(registry, spec=None):
@@ -8,7 +8,7 @@ import struct
8
8
  from . import ops, require, tlvs
9
9
  from .. import conformance as _helpers
10
10
 
11
- REVIEWED_CORE_SHA256 = '0408d232e728bb9692ab1c24cefa1fdbf109c35aeebd378034d86c0f4d19789a' # oep-spec docs/oep-core.ja.md
11
+ REVIEWED_CORE_SHA256 = '81cffc532f2626812b109d0aa5eef81636799011f410a8bc2fb9ea520fcbf4bc' # oep-spec docs/oep-core.ja.md
12
12
 
13
13
 
14
14
  class CoreSmokeChecks:
@@ -4,7 +4,7 @@ from pathlib import Path
4
4
  import struct
5
5
  from . import require,spec_identity
6
6
 
7
- REVIEWED_CORE_SHA256='0408d232e728bb9692ab1c24cefa1fdbf109c35aeebd378034d86c0f4d19789a'
7
+ REVIEWED_CORE_SHA256='81cffc532f2626812b109d0aa5eef81636799011f410a8bc2fb9ea520fcbf4bc'
8
8
  CORE_OPS={'confirm':1,'list':2,'describe':3,'clock':4,'open':16,'end':17,'keepalive':18,'lock_state':19}
9
9
  # the settings' ops (core §13.5 / §13.7, transports §7.3): their numbers reviewed, their results not read here
10
10
  SETTINGS_OPS={'get':32,'set':33,'unset':34,'save':35,'state':36,'network':37}
@@ -9,8 +9,8 @@ from pathlib import Path
9
9
  from . import require,spec_identity
10
10
 
11
11
 
12
- REVIEWED={'docs/oep-core.ja.md':'0408d232e728bb9692ab1c24cefa1fdbf109c35aeebd378034d86c0f4d19789a',
13
- 'docs/oep-transports.ja.md':'2e99bf8039a3d8f5b55c80b66ac97ac40ddf7ad4711079177ffc947c5790103f'}
12
+ REVIEWED={'docs/oep-core.ja.md':'81cffc532f2626812b109d0aa5eef81636799011f410a8bc2fb9ea520fcbf4bc',
13
+ 'docs/oep-transports.ja.md':'a22754ee25997058be0e698bd62ea132537627914f38a06caffd48e3d79f096e'}
14
14
  QUICK_MS=1000 # core_quick_op_max_ms
15
15
 
16
16
 
@@ -12,11 +12,11 @@ from . import require, spec_identity
12
12
  REVIEWED = {
13
13
  'interfaces/oep-if-plan.ja.md': 'da8bd60af53499573c07bc33dbd8c4df93eb61a93b91dc57907f26620ae36bbf',
14
14
  'interfaces/oep-if-fixture.ja.md': '777a972349a14d1efa6e4575d3118b4e0566b81a6f22320401d529e4dca585f1',
15
- 'docs/oep-core.ja.md': '0408d232e728bb9692ab1c24cefa1fdbf109c35aeebd378034d86c0f4d19789a',
15
+ 'docs/oep-core.ja.md': '81cffc532f2626812b109d0aa5eef81636799011f410a8bc2fb9ea520fcbf4bc',
16
16
  'interfaces/oep-if-debug.ja.md': '6f21171b494e67d50bfc78b4da9e16949033c9e24d960efb42887335da021681',
17
17
  'interfaces/oep-if-capture.ja.md': 'f1035dc14cf469d929d782dbe710fc2c1e1567006da41a001abe3c5c25761324',
18
18
  }
19
- REVIEWED_REGISTRY_SHA256='82456b04874c4d20775744d2bde3398d7a748d955d73b8f295a5e8e889a46477'
19
+ REVIEWED_REGISTRY_SHA256='183922c36d78c0173cedece530410478dc3f1de0094d1922d3a960e752863d1e'
20
20
 
21
21
 
22
22
  def reviewed_identity(spec):
@@ -6,8 +6,8 @@ from .. import registry as _reg
6
6
  SAVE=_reg.CORE.op['save'] # fn 0's save (core §13.7)
7
7
 
8
8
 
9
- REVIEWED={'docs/oep-core.ja.md':'0408d232e728bb9692ab1c24cefa1fdbf109c35aeebd378034d86c0f4d19789a',
10
- 'docs/oep-transports.ja.md':'2e99bf8039a3d8f5b55c80b66ac97ac40ddf7ad4711079177ffc947c5790103f'}
9
+ REVIEWED={'docs/oep-core.ja.md':'81cffc532f2626812b109d0aa5eef81636799011f410a8bc2fb9ea520fcbf4bc',
10
+ 'docs/oep-transports.ja.md':'a22754ee25997058be0e698bd62ea132537627914f38a06caffd48e3d79f096e'}
11
11
  QUICK_MS=1000 # core_quick_op_max_ms
12
12
 
13
13
 
oep_client/dump.py CHANGED
@@ -179,7 +179,11 @@ def describe_offer(o: Offer) -> dict:
179
179
  if getattr(d, key) is not None:
180
180
  out[key] = getattr(d, key)
181
181
  if d.ops is not None:
182
- out["ops"] = interfaces.op_names(name, d.ops)
182
+ out["ops"] = [d.op_forms[op].name if op in d.op_forms else n
183
+ for op, n in zip(sorted(d.ops), interfaces.op_names(name, d.ops))]
184
+ forms = [f.signature() for op, f in sorted(d.op_forms.items()) if f.form]
185
+ if forms:
186
+ out["op_forms"] = forms
183
187
  if d.features is not None:
184
188
  out["features"] = _features(d.features, known.features if known else {})
185
189
  specific = {}
@@ -241,6 +245,8 @@ def to_text(caps: Capabilities) -> str:
241
245
  lines.append(f"{pad} " + ", ".join(limits))
242
246
  if "ops" in r:
243
247
  lines.append(f"{pad} ops: " + (", ".join(r["ops"]) or "none"))
248
+ for sig in r.get("op_forms", []):
249
+ lines.append(f"{pad} {sig}")
244
250
  if r.get("features"):
245
251
  lines.append(f"{pad} features: " + ", ".join(r["features"]))
246
252
  for k, v in r.get("declares", {}).items():
oep_client/registry.py CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  from types import SimpleNamespace as _NS
5
5
 
6
- REGISTRY_HASH = "82456b04874c4d20"
6
+ REGISTRY_HASH = "183922c36d78c017"
7
7
  SCHEMA = 1
8
8
  PROTOCOL_REVISION = 1
9
9
  CONFIRM_REQUEST_MAGIC = 'OEP?'
@@ -19,16 +19,16 @@ RESOLUTIONS = {"rejected": 0x00, "completed": 0x01}
19
19
  OUTCOMES = {"success": 0x00, "failed": 0x01, "partial": 0x02}
20
20
  REJECT_REASONS = {"unknown_function": 0x01, "unknown_operation": 0x02, "malformed": 0x03, "unavailable": 0x04, "window_exceeded": 0x06, "no_session": 0x07, "locked": 0x08, "session_required": 0x09, "no_resource": 0x0A, "unsupported": 0x0B, "result_lost": 0x0C}
21
21
  STATUS = {"ok": 0x00, "wait": 0x01, "line": 0x02, "fault": 0x03, "timeout": 0x04, "state": 0x05}
22
- DESCRIBE_COMMON = {"role_channels": 0x01, "max_clock_hz": 0x02, "max_length": 0x03, "min_clock_hz": 0x04, "features": 0x05, "channel_group": 0x06, "ops": 0x07, "setting": 0x08, "setting_choice": 0x09}
22
+ DESCRIBE_COMMON = {"role_channels": 0x01, "max_clock_hz": 0x02, "max_length": 0x03, "min_clock_hz": 0x04, "features": 0x05, "channel_group": 0x06, "ops": 0x07, "setting": 0x08, "setting_choice": 0x09, "op_name": 0x0A, "op_field": 0x0B}
23
23
  TIMING = {"probe_frame_gap_ms": 0xC8, "uart_bridge_boot_baud": 0x1C200, "port_speed_idle_ms": 0xBB8, "host_wait_add_ms": 0x3E8, "core_quick_op_max_ms": 0x3E8, "notify_pending_max_frames": 0x02, "port_speed_switch_wait_ms": 0x14}
24
24
  USB = {"project_vid": 0x1209, "project_pid": 0x4F45, "vendor_bulk_class": 0xFF, "vendor_bulk_subclass": 0x4F, "vendor_bulk_protocol": 0x45, "hid_usage_page": 0xFF4F, "hid_usage": 0x45}
25
- LIMITS = {"request_header_bytes": 0x0A, "result_header_bytes": 0x05, "corr_max": 0xFFFF, "corr_half_range": 0x8000, "resource_id_bytes": 0x04, "resource_id_max": 0xFFFFFFFF, "lease_min_ms": 0x3E8, "lease_max_ms": 0xEA60, "owner_max_bytes": 0x20, "unit_id_max_bytes": 0x20, "wifi_ssid_max_bytes": 0x20, "wifi_min_max_frame": 0x52, "label_max_bytes": 0x20, "setting_name_max_bytes": 0x30, "setting_choice_name_max_bytes": 0x10, "channel_name_max_bytes": 0x10, "setting_get_overhead_bytes": 0x12, "interface_name_max_bytes": 0x30, "port_speed_tolerance_pct": 0x02, "uart_baud_tolerance_pct": 0x05, "max_op_ms_max": 0x927C0}
25
+ LIMITS = {"request_header_bytes": 0x0A, "result_header_bytes": 0x05, "corr_max": 0xFFFF, "corr_half_range": 0x8000, "resource_id_bytes": 0x04, "resource_id_max": 0xFFFFFFFF, "lease_min_ms": 0x3E8, "lease_max_ms": 0xEA60, "owner_max_bytes": 0x20, "unit_id_max_bytes": 0x20, "wifi_ssid_max_bytes": 0x20, "wifi_min_max_frame": 0x52, "label_max_bytes": 0x20, "setting_name_max_bytes": 0x30, "op_name_max_bytes": 0x18, "setting_choice_name_max_bytes": 0x10, "channel_name_max_bytes": 0x10, "setting_get_overhead_bytes": 0x12, "interface_name_max_bytes": 0x30, "port_speed_tolerance_pct": 0x02, "uart_baud_tolerance_pct": 0x05, "max_op_ms_max": 0x927C0}
26
26
  COMMON = _NS(enum={"target_id_scheme": {"dmi_7f": 0x01, "dpidr": 0x02}, "read_from": {"position": 0x00, "oldest": 0x01, "now": 0x02, "last_mark": 0x03}, "read_flags": {"more": 0x01, "gap": 0x02}, "mark_kind": {"lost": 0x01, "clear": 0x02, "host": 0x03}, "mark_detail_lost": {"overflow": 0x01, "framing": 0x02, "parity": 0x03}})
27
27
 
28
28
  INTERFACES = {}
29
29
  CORE = _NS(op={"confirm": 0x01, "list": 0x02, "describe": 0x03, "clock": 0x04, "open": 0x10, "end": 0x11, "keepalive": 0x12, "lock_state": 0x13, "get": 0x20, "set": 0x21, "unset": 0x22, "save": 0x23, "state": 0x24, "network": 0x25}, lock_free={0x01, 0x02, 0x03, 0x04, 0x10, 0x13, 0x20, 0x24, 0x25},
30
- tlv={"confirm_answer": {"transport": 0x01}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "unavailable_payload": {"cause": 0x01, "channel": 0x02, "item": 0x03, "fn": 0x05}, "unsupported_payload": {"supported": 0x01, "channel": 0x02, "item": 0x03, "fn": 0x05}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "channel_name": 0x44, "netif": 0x45, "transport": 0x49, "chip": 0x4C, "max_op_ms": 0x4D}}, event={}, enum={"unavailable_cause": {"pin_in_use": 0x01, "limit": 0x02, "storage_full": 0x03, "bound_in_group": 0x04, "held_by_settings": 0x05, "wrong_state": 0x06, "policy_denied": 0x07}, "transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}, "route_kind": {"bridge": 0x01, "cdc": 0x02, "usj": 0x03, "bulk": 0x04, "hid": 0x05, "tcp": 0x06}, "setting_form": {"bytes": 0x01, "text": 0x02, "u32": 0x03, "ipv4": 0x04, "bool": 0x05, "enum": 0x06, "channel": 0x07, "transport": 0x08, "fn": 0x09}, "setting_key_kind": {"none": 0x00, "channel": 0x01, "index": 0x02, "transport": 0x03, "netif": 0x04}, "setting_flag": {"secret": 0x01, "restart": 0x02}, "storage_state": {"none": 0x00, "applied": 0x01, "unreadable": 0x02, "partial": 0x03}, "storage_reason": {"none": 0x00, "form": 0x01, "item": 0x02, "capacity": 0x03}, "channel_idle": {"hi_z": 0x00, "pull_up": 0x01, "pull_down": 0x02, "low": 0x03, "high": 0x04}, "ipv4_mode": {"dhcp": 0x00, "static": 0x01}, "network_state": {"off": 0x00, "connecting": 0x01, "up": 0x02, "waiting": 0x03}, "network_reason": {"none": 0x00, "not_found": 0x01, "auth": 0x02, "no_address": 0x03, "other": 0x04}}, own={},
31
- line_names={'swdio': 'the debug data line (2 wires: rvswd, swd) (Host only)', 'swclk': 'the debug clock line (2 wires: rvswd, swd) (Host only)', 'swio': 'the one debug wire (swio) (Host only)', 'rx': 'the fixture UART receive, wired to the target TX (Host only)', 'tx': 'the fixture UART transmit, wired to the target RX (Host only)', 'nrst': 'the target reset line (Host only)', 'power_hi': 'target power on when high (Host only)', 'power_lo': 'target power on when low (Host only)'}, names={'netif': {'wifi': 'Wi-Fi station', 'ethernet': 'wired Ethernet'}}, settings={'channel.{}.label': "text 1-32, channel; the pin's label (NRST)", 'channel.{}.idle': 'enum channel_idle, channel; the idle state (core §8)', 'channel.{}.disable': 'bool, channel; true: the probe neither drives nor configures the pin', 'wifi.{}.ssid': "text 1-32, index (entry); the network's name", 'wifi.{}.passphrase': 'text 8-64, index (entry), secret; 8-63 printable passphrase or 64 hex digits', 'ipv4.{}.mode': 'enum ipv4_mode, netif; dhcp when absent', 'ipv4.{}.address': 'ipv4, netif; static address', 'ipv4.{}.prefix': 'u32 1-32, netif; static prefix length', 'ipv4.{}.gateway': 'ipv4, netif; static gateway, none when absent', 'ipv4.{}.dns': 'ipv4, netif; static DNS server, none when absent'})
30
+ tlv={"confirm_answer": {"transport": 0x01}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "unavailable_payload": {"cause": 0x01, "channel": 0x02, "item": 0x03, "fn": 0x05}, "unsupported_payload": {"supported": 0x01, "channel": 0x02, "item": 0x03, "fn": 0x05}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "channel_name": 0x44, "netif": 0x45, "transport": 0x49, "chip": 0x4C, "max_op_ms": 0x4D}}, event={}, enum={"unavailable_cause": {"pin_in_use": 0x01, "limit": 0x02, "storage_full": 0x03, "bound_in_group": 0x04, "held_by_settings": 0x05, "wrong_state": 0x06, "policy_denied": 0x07}, "transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}, "route_kind": {"bridge": 0x01, "cdc": 0x02, "usj": 0x03, "bulk": 0x04, "hid": 0x05, "tcp": 0x06}, "setting_form": {"bytes": 0x01, "text": 0x02, "u32": 0x03, "ipv4": 0x04, "bool": 0x05, "enum": 0x06, "channel": 0x07, "transport": 0x08, "fn": 0x09}, "setting_key_kind": {"none": 0x00, "channel": 0x01, "index": 0x02, "transport": 0x03, "netif": 0x04}, "setting_flag": {"secret": 0x01, "restart": 0x02}, "op_name_flag": {"form": 0x01, "lock_free": 0x02}, "op_field_part": {"request": 0x00, "success": 0x01, "failed": 0x02}, "op_field_type": {"u8": 0x01, "u16": 0x02, "u32": 0x03, "u64": 0x04, "i8": 0x05, "i16": 0x06, "i32": 0x07, "i64": 0x08, "bool": 0x09, "bytes": 0x0A, "text": 0x0B, "list": 0x80}, "storage_state": {"none": 0x00, "applied": 0x01, "unreadable": 0x02, "partial": 0x03}, "storage_reason": {"none": 0x00, "form": 0x01, "item": 0x02, "capacity": 0x03}, "channel_idle": {"hi_z": 0x00, "pull_up": 0x01, "pull_down": 0x02, "low": 0x03, "high": 0x04}, "ipv4_mode": {"dhcp": 0x00, "static": 0x01}, "network_state": {"off": 0x00, "connecting": 0x01, "up": 0x02, "waiting": 0x03}, "network_reason": {"none": 0x00, "not_found": 0x01, "auth": 0x02, "no_address": 0x03, "other": 0x04}}, own={},
31
+ line_names={'swdio': 'the debug data line (2 wires: rvswd, swd) (Host only)', 'swclk': 'the debug clock line (2 wires: rvswd, swd) (Host only)', 'swio': 'the one debug wire (swio) (Host only)', 'rx': 'the fixture UART receive, wired to the target TX (Host only)', 'tx': 'the fixture UART transmit, wired to the target RX (Host only)', 'nrst': 'the target reset line (Host only)', 'power_hi': 'target power on when high (Host only)', 'power_lo': 'target power on when low (Host only)'}, names={'netif': {'wifi': 'Wi-Fi station', 'ethernet': 'wired Ethernet'}}, settings={'channel.{}.label': "text 1-32, channel; the pin's label (NRST)", 'channel.{}.idle': 'enum channel_idle, channel; the idle state (core §8)', 'channel.{}.disable': 'bool, channel; true: the probe neither drives nor configures the pin', 'wifi.{}.ssid': "text 1-32, index (entry); the network's name", 'wifi.{}.passphrase': 'text 8-64, index (entry), secret; 8-63 printable passphrase or 64 hex digits', 'ipv4.{}.mode': 'enum ipv4_mode, netif; dhcp when absent', 'ipv4.{}.address': 'ipv4, netif; static address', 'ipv4.{}.prefix': 'u32 1-32, netif; static prefix length', 'ipv4.{}.gateway': 'ipv4, netif; static gateway, none when absent', 'ipv4.{}.dns': 'ipv4, netif; static DNS server, none when absent', 'usb.transports': "enum (the probe's choices: transport short names in describe order joined by -, a count after a repeated one: bulk-hid-cdc2), restart; the OEP transports the probe's own USB device offers after a restart, its default set when absent (transports §3.1)"})
32
32
  PROBE_PLAN = _NS(name="oep.probe.plan", revision=1, target=None, op={"plan_apply": 0x10, "plan_release": 0x11}, lock_free=set(),
33
33
  tlv={"plan_apply": {"role_assignment": 0x10}, "describe": {"plan_roles": 0x40}}, event={}, enum={}, own={},
34
34
  line_names={}, names={}, settings={})
@@ -320,7 +320,9 @@ NETWORK_ITEMS = (Item("wifi.{}.ssid", "text", "index", WIFI_ENTRIES, reg.LIMITS[
320
320
  Item("ipv4.{}.mode", "enum", "netif", choices=tuple(choices_of(reg.CORE.enum["ipv4_mode"]).items())),
321
321
  Item("ipv4.{}.address", "ipv4", "netif"), Item("ipv4.{}.prefix", "u32", "netif"),
322
322
  Item("ipv4.{}.gateway", "ipv4", "netif"), Item("ipv4.{}.dns", "ipv4", "netif"))
323
- assert {i.name for i in CORE_ITEMS + NETWORK_ITEMS} == set(_CS), "the core's items as the registry names them"
323
+ # transports §3.1: a probe whose own USB device can start more than one set of transports (none of the profiles here)
324
+ USB_ITEMS = (Item("usb.transports", "enum", flags=FLAG["restart"], choices=((0, "bulk-hid-cdc"), (1, "bulk-hid"))),)
325
+ assert {i.name for i in CORE_ITEMS + NETWORK_ITEMS + USB_ITEMS} == set(_CS), "the core's items as the registry names them"
324
326
  HOLD = Item("channel.{}.hold", "bool", "channel")
325
327
  REST = Item("channel.{}.rest", "enum", "channel", choices=tuple(choices_of(reg.WIRE_RVSWD.enum["rest"]).items()))
326
328
 
@@ -672,6 +674,16 @@ def rp2350_pins() -> VirtualProbe:
672
674
 
673
675
 
674
676
  STAND_IN = f"{NS}.stand-in"
677
+ # The stand-in ops' names and fields (core §7.4 op_name / op_field): write(value: u32), read() -> value: u32 (lock-free)
678
+ _OPN, _OPF = reg.DESCRIBE_COMMON["op_name"], reg.DESCRIBE_COMMON["op_field"]
679
+ _FLAG, _PART, _TYPE = (reg.CORE.enum[k] for k in ("op_name_flag", "op_field_part", "op_field_type"))
680
+ _WRITE, _READ = STAND_IN_OPS
681
+ STAND_IN_FORMS = (
682
+ catalog.tlv(_OPN, bytes([_WRITE, _FLAG["form"]]) + b"write"),
683
+ catalog.tlv(_OPF, bytes([_WRITE, _PART["request"], _TYPE["u32"]]) + b"value"),
684
+ catalog.tlv(_OPN, bytes([_READ, _FLAG["form"] | _FLAG["lock_free"]]) + b"read"),
685
+ catalog.tlv(_OPF, bytes([_READ, _PART["success"], _TYPE["u32"]]) + b"value"),
686
+ )
675
687
 
676
688
 
677
689
  def _again(probe: VirtualProbe, offered: list[Offered]) -> VirtualProbe:
@@ -731,7 +743,7 @@ def with_stand_in(probe: VirtualProbe) -> VirtualProbe:
731
743
  """The profile plus one fn (the next number) the endpoint does not simulate: it answers only the two stand-in
732
744
  operations (endpoint.TOY_WRITE / TOY_READ), for tests of the session rules. VIRTUAL BENCH ONLY."""
733
745
  fn = max(o.fn for o in probe.offered) + 1
734
- return _again(probe, list(probe.offered) + [Offered(fn, 0, STAND_IN)])
746
+ return _again(probe, list(probe.offered) + [Offered(fn, 0, STAND_IN, STAND_IN_FORMS)])
735
747
 
736
748
 
737
749
  ATTACH_HALT = reg.WIRE_RVSWD.enum["features"]["attach_halt"] # attach method 1 (debug §3)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.53
3
+ Version: 0.0.54
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a virtual bench
5
5
  Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
6
  Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
@@ -1,16 +1,17 @@
1
- oep_client/__init__.py,sha256=h7kpeApyfQHsMxED1_KZ6bWVGpyXHIWTutC3cmwSr7o,164
2
- oep_client/__main__.py,sha256=kbXBtEmFdhRXbVqXEbWCO-_5aDa-KyqMcW2HLZb5fjc,31510
1
+ oep_client/__init__.py,sha256=xps99W-P4elEjl5RY8jG6Pr_sjJfRFSzMeiUo3p0pco,164
2
+ oep_client/__main__.py,sha256=SMeCK8A4Ru8tvCW3u4cXV1oFI_eUyEUJnBZukCZeVNg,36319
3
3
  oep_client/acquire.py,sha256=CXjK5s-kjdCoGg9Yt_46rr4te6T2fbX4tl89PjOnURA,102541
4
4
  oep_client/arm.py,sha256=zsgq4jsGzroE_9yF5D48rJF7dAe07MR8RY4RMCgjiwc,16411
5
+ oep_client/call.py,sha256=4h8jGv_12CaTIfjOVlNXJYcu_ZCiPpPAwGdvbUv86Vc,12806
5
6
  oep_client/capture.py,sha256=92ZznVWu2i3_5mIk0bMSIeqNqJ0LpjbMi3JRb2qrYrs,75848
6
7
  oep_client/capture_blocks.py,sha256=B8T2dKozQfGTWg4noG0cZ0DZ9KrrfiFFdbTrBKWfWhY,17330
7
- oep_client/catalog.py,sha256=WkaksNn9cFvkTRnvtX4I9g1AvkFoldEiNQaKiyPQ7_E,10772
8
+ oep_client/catalog.py,sha256=aWzWWLAzeGcpNqTz8ZM2AupTNLY1jsADr3CjHrzx-_0,13960
8
9
  oep_client/ch32_flash.py,sha256=lY_HKxf2foSfhNda91lcWefgRnY9ebTljsg4zJEj-Ks,12885
9
10
  oep_client/cobs.py,sha256=RbdPrqgzD3SukxOIXX9m2dZpVTud8xAYYEhm3rcW2nA,2513
10
11
  oep_client/console.py,sha256=hDxoaXDCzk6KiOo-Y3RqC4yZuLPkCXOVi886_C4xXaE,12910
11
12
  oep_client/core.py,sha256=o7qJjZc68RS9p91TfV3aLsLcTq-jWBO1atB9kGL_tkA,22188
12
13
  oep_client/discovery.py,sha256=oES0_NkR1Hc9E_cgPcfW8qfevQcVuYmayMaHuqufdO8,20180
13
- oep_client/dump.py,sha256=9rfO07q1EOY4aLFZkm_9RO8awjxNQa75DIDeKAoF_ac,12995
14
+ oep_client/dump.py,sha256=ic3Nv_ZX1iLjgSkjyFwgXVkjCnPwClnkqZnagQY9N-c,13317
14
15
  oep_client/endpoint.py,sha256=A4fCeCwMCXOpsY4zeVy1LG_rG5SvYfImKRP153dIC5I,237768
15
16
  oep_client/fixture.py,sha256=gMc6TRAlXBEMBFg6aCBkGGJvbIJsGBSfUdk0genD_Y8,34397
16
17
  oep_client/frames.py,sha256=FM1AGn5NY_e-r1yKBsDkEEOqNrk5XTrlKA5Mq6Ycqiw,4984
@@ -28,7 +29,7 @@ oep_client/pins.py,sha256=iXswbqvze0KED1WNHLnjG5sn-_SjoJrIvcjj84LU_jU,53670
28
29
  oep_client/probe_log.py,sha256=gAamfON2M7lMH3xOn4Wpmes_Wp0H4c2EBNbpXjIvCMM,4927
29
30
  oep_client/pytest_conformance.py,sha256=-bcJU2E1UE4xjQ7HZjaIgtp0l65cIibBTNFnaW2tews,2929
30
31
  oep_client/pytest_rp2040_peer.py,sha256=9buM_lpXx9lhi-H-tIWAfS5xyDmgGUBwfHlOkBG3jiA,2873
31
- oep_client/registry.py,sha256=1oFMpnbys1Iqih592ueUn8kru56O4eTCYNhdIQfDIP0,22706
32
+ oep_client/registry.py,sha256=yzp4URjmECxBlbl6vt2KaeB4v-93hQFy-brsT7_17II,23339
32
33
  oep_client/riscv.py,sha256=1QloF51Uci2-6gonkBxQNh9XleF6yCvKG-L7XNm4sgI,60460
33
34
  oep_client/rp2040.py,sha256=m_3eOWxrZOTWB7TGYv2_OmtfHREBdwCfdoVXjP_Qq0A,3392
34
35
  oep_client/rp2040_peer.py,sha256=BQc6H79wPhjEcj0sl4O5mDPH_YZIHB2FbrZ8vMdxdg8,14505
@@ -39,7 +40,7 @@ oep_client/stimulus.py,sha256=pfX4xLjyG3UWIJRWUHnkg9c1ueL1MeozHj3CnlJ7dpA,6072
39
40
  oep_client/targets.py,sha256=vEXPyhI1UNeaZKIpbNIVf-XsmpvnV9r0pPyXYb3Ddaw,7994
40
41
  oep_client/uiapduino.py,sha256=oafP1SyKPO3cox-2RYbWUZusy4vc6NsEVC7Z-Y1sjWI,6337
41
42
  oep_client/usb_stream.py,sha256=zRGGHkvtOme6lIZ-gkrCe2xKn9hC62JGqmc7pKQDAJI,23540
42
- oep_client/virtual_bench.py,sha256=tLPjfVy19CtcRwQIhYYx4xzQ5JIHXhFnRvUVwFj_sVE,50273
43
+ oep_client/virtual_bench.py,sha256=YekAy5xxchLUOMarWMvdgshTVnFMe9VJZcib0Cpz4Y8,51203
43
44
  oep_client/virtual_bench_capture.py,sha256=KxnXoCZ71qy9WwujyMt43jDJLsU4xHicBlS16NJMwkg,101716
44
45
  oep_client/virtual_bench_mdns.py,sha256=cnJNBKg5zTduXUcH-wqN_ZBDGl7DyL-tL3CBn2jro0E,13305
45
46
  oep_client/virtual_bench_serial.py,sha256=EvbdOYeGrM6CRti2b90I0PzpmmherfoJRaAg4v7Iq9Y,13205
@@ -49,7 +50,7 @@ oep_client/virtual_bench_usb.py,sha256=cnewgbsl30b0PgwkfebwSf3xjKyPg9y8vTd5Jew5r
49
50
  oep_client/virtual_core.py,sha256=F-UxtQIld4eE04_M8j4ANMlQ12jXANzfTnfn59kUWHk,10716
50
51
  oep_client/virtual_fixture_drive.py,sha256=3i5ss_bxIJFsRDb5S-GJ0Nb9QDyOBv_SwF9CDP6nPfE,21844
51
52
  oep_client/virtual_names.py,sha256=p72ME65R9Jy56xmVYnVCtapK3Id8iFpAdI5xLe2tNZ0,1449
52
- oep_client/conformance/__init__.py,sha256=P_RyiWnULdDQMKyo0eCnE7VLv3v7Ps7lnVCY1hfOjdA,64739
53
+ oep_client/conformance/__init__.py,sha256=r3xk9VyzN4LgETYlN0E3pyA-b1uI9Km0TXra-Bl-ShY,64739
53
54
  oep_client/conformance/allocator.py,sha256=dYR5ZcCVW-LjoZVx4W9Ad76L8Ol8Pu1aOkj78A9UY1E,6251
54
55
  oep_client/conformance/allocator_model.py,sha256=j6l1E1Hlju1jNeFQ_N0a2mDaegaTeyk6iScqzMpvvTc,1563
55
56
  oep_client/conformance/allocator_sample.py,sha256=xtiXuZa3VDTilrZArIWOiRyOkuXJ98_rY2M65sIVvpo,855
@@ -84,7 +85,7 @@ oep_client/conformance/clock.py,sha256=bDqBjih71zg_mhkgCSryzKI_N8FEoGoGv9jO4PMYD
84
85
  oep_client/conformance/clock_model.py,sha256=jyB8oTVR5sqbzeJCCWxdQBnzNNl4_uITLLYQ-yx8pRU,2568
85
86
  oep_client/conformance/clock_sample.py,sha256=x24mSpQoOamrXrinmnF_Tk4NPk76LwS6ZhfuK2SCwCo,673
86
87
  oep_client/conformance/core_fixed_fields.py,sha256=LRFP9yGOjDHQM1diX1HMdwXh9jMgAOY6xIC-woCikSk,6297
87
- oep_client/conformance/core_smoke.py,sha256=np7SZ3yVdD0imiFyjBjAT_J8KK_EmtUiJAeplUNpYR8,10815
88
+ oep_client/conformance/core_smoke.py,sha256=6fvQ6JYX1PTFGKtpCTffKigzKF5ejsYLffMFXxOCVQE,10815
88
89
  oep_client/conformance/corr.py,sha256=P6ljdsXpBq-vyEXRWOB1MqACQjT6HRBXgfAxgkyPjl4,6552
89
90
  oep_client/conformance/cut.py,sha256=4B8szd6_QtcGtYMWgP86ABsXUDOFVgwWaWCpTWMDO80,7942
90
91
  oep_client/conformance/cut_model.py,sha256=bHPnK8vKoInKVslW0KlJXRRgpGK-7ktIVGUEXo60X34,4791
@@ -119,21 +120,21 @@ oep_client/conformance/gpio_lifecycle_model.py,sha256=K0nzlXf-43kj3jsfaAuzEWOftO
119
120
  oep_client/conformance/gpio_lifecycle_sample.py,sha256=Yk1WENQ-2tbPCM2yUY4Yk9dgWkjIfL42VtUSpx1qhhM,369
120
121
  oep_client/conformance/gpio_model.py,sha256=CXy42K8cdRSTBxLC44h-gTYhjh3HrduSehsgsbvmDpo,5633
121
122
  oep_client/conformance/gpio_sample.py,sha256=JHsOrjkYcM738IGSzP5q7v5vANETwKT6-csCTxH55MQ,490
122
- oep_client/conformance/host_core.py,sha256=RaCD86ehLC1w35hjXu0ekQ6hKTtoljdnReYTttll4KE,12634
123
+ oep_client/conformance/host_core.py,sha256=7yaDKXpam5qBKuAl41a0eBHq4ijspQIpQhk-JKNvFoQ,12634
123
124
  oep_client/conformance/host_core_sample.py,sha256=egU8Xw-f3eHx_CmUooN1yW1lzltOam6IsJbnD8uOx9I,3803
124
- oep_client/conformance/host_deadline.py,sha256=P0Yh3lZrq35_zQkEPhXjjX2aORXGkpPJvgxWtfNPvFM,6978
125
+ oep_client/conformance/host_deadline.py,sha256=uGm6LhWDsv-HsO7M80MROhjGYEmd12qjEKk8B-aT2Mg,6978
125
126
  oep_client/conformance/host_deadline_sample.py,sha256=G471jEc2HxDIu52Acw_aHDSxFAEC72OBCuvn2YQtAdU,5136
126
127
  oep_client/conformance/host_notifications.py,sha256=LGRJhOS66_36MYG8fHLcyHroXtoZRQVJgammDieVhQY,9307
127
128
  oep_client/conformance/host_notifications_sample.py,sha256=auze2DdWtUOdQMHnfDF8WXLAiETv3OuODR6y77VmsTQ,1788
128
129
  oep_client/conformance/host_request_sample.py,sha256=Oq60q350EhDxhe2FESnUF04YhJ6HDjDSHTe4FR55H9M,1848
129
- oep_client/conformance/host_requests.py,sha256=y6QClQaVB1Bo6G1k49Ce9iizFomqJZZJEemIiellVwA,10718
130
+ oep_client/conformance/host_requests.py,sha256=Z_gaT55iel6jGgYHytGWQpph78xbhTScvFoWpbsZyEs,10718
130
131
  oep_client/conformance/host_resend.py,sha256=P8tFWtr6TwQ33ph-JO0l8EXyJGxSjC6fBeyKQ4AhqxU,5519
131
132
  oep_client/conformance/host_resend_sample.py,sha256=nwaru9ZVcxpcTi1OEritp8PSF6Qfic_IRGuutH2aBA4,4808
132
133
  oep_client/conformance/host_tlv.py,sha256=h5SZT9r3DCsxMLcRacdyKOoZNQHSL8DzRgT1VPu_86A,6140
133
134
  oep_client/conformance/host_tlv_sample.py,sha256=V6ORb5E2i76dc8O73Bew0eO2CbjG-ZPc02JLi6w5vmY,743
134
135
  oep_client/conformance/host_values.py,sha256=VnjEB5TQbI34fuyAx0OGVbiyfI2ZH_of9I4TLT7Dyyk,4742
135
136
  oep_client/conformance/host_values_sample.py,sha256=7C58mSP9jjrkNSgSb3uNK44VrdPXOznof_oe5UCcZro,1532
136
- oep_client/conformance/host_wait.py,sha256=B9h0mjhIbo8DH9LwUwP8OXL1weeKkPOOeW_YGU4H-CA,5591
137
+ oep_client/conformance/host_wait.py,sha256=N7Aj70EIE5A8mg849kbQ192BPG1gXj6fHAHz-rF2MSw,5591
137
138
  oep_client/conformance/host_wait_sample.py,sha256=7SkJ8H_9ScMv8gLnT85xBKj_GXciPvAzmH-KxfMWYCg,3257
138
139
  oep_client/conformance/i2c.py,sha256=fNDweyCLlCsv7fCwmmQMC7di5AG1HzDCflW0miWwcjc,18041
139
140
  oep_client/conformance/i2c_model.py,sha256=HLYqRKdKMvlCGMcgYxSS3dy50Zf7aPojN_gO5fBvy6U,9759
@@ -259,8 +260,8 @@ oep_client/conformance/writer_lease_model.py,sha256=FxhDqqzdFOa_SSHSvjdQFh49Wn3Y
259
260
  oep_client/conformance/writer_lease_sample.py,sha256=oowRwYUNAE4f6rEE2HmYlSS9dDWE0TGegkcsqVH8Nak,455
260
261
  oep_client/pyserial/__init__.py,sha256=JSsbRkLYgFVN8V4H9ovdqyDr6JW3P3qi4RYoElg050o,121
261
262
  oep_client/pyserial/protocol_oep.py,sha256=3dC8nbtddNG2lfonLUkbBTKfsfJoNHFfabuedn0n7Ho,2497
262
- oep_client_python-0.0.53.dist-info/METADATA,sha256=M2NZFKa5D1kJLOC8gBnV2OxzEnq0klAORQTOKBYaFxI,55222
263
- oep_client_python-0.0.53.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
264
- oep_client_python-0.0.53.dist-info/entry_points.txt,sha256=XEt39Jz7oQyU9Hfg_tV6eDGofNxWYrjSfslxKP-fh70,135
265
- oep_client_python-0.0.53.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
266
- oep_client_python-0.0.53.dist-info/RECORD,,
263
+ oep_client_python-0.0.54.dist-info/METADATA,sha256=26-ib8waK12oL_vgWAHFD-j7voBa-hsBbAPksQg5LXE,55222
264
+ oep_client_python-0.0.54.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
265
+ oep_client_python-0.0.54.dist-info/entry_points.txt,sha256=XEt39Jz7oQyU9Hfg_tV6eDGofNxWYrjSfslxKP-fh70,135
266
+ oep_client_python-0.0.54.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
267
+ oep_client_python-0.0.54.dist-info/RECORD,,