oep-client-python 0.0.8__py3-none-any.whl → 0.0.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oep_client/__init__.py +1 -1
- oep_client/capture.py +147 -17
- oep_client/endpoint.py +180 -6
- oep_client/fake.py +51 -4
- oep_client/fake_capture.py +437 -0
- oep_client/fake_serial.py +2 -0
- oep_client/fake_serve.py +8 -1
- oep_client/registry.py +8 -5
- {oep_client_python-0.0.8.dist-info → oep_client_python-0.0.10.dist-info}/METADATA +16 -4
- {oep_client_python-0.0.8.dist-info → oep_client_python-0.0.10.dist-info}/RECORD +13 -12
- {oep_client_python-0.0.8.dist-info → oep_client_python-0.0.10.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.8.dist-info → oep_client_python-0.0.10.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.8.dist-info → oep_client_python-0.0.10.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/capture.py
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
"""oep.fixture.capture / oep.fixture.analog revision 1 (oep-spec docs/oep-if-capture.ja.md:
|
|
2
|
-
§3 operations). Numbers from `registry`.
|
|
1
|
+
"""oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group revision 1 (oep-spec docs/oep-if-capture.ja.md:
|
|
2
|
+
§1 layouts, §2 segments, §3 operations, §3.8 calibration, §4 groups). Numbers from `registry`.
|
|
3
|
+
|
|
4
|
+
Times are the probe's one clock (ns since its boot, comparable within one boot_id): estimates with an uncertainty, the
|
|
5
|
+
probe's known corrections applied. Analog values are always raw; the probe's 1st-order scale, its calibration data and
|
|
6
|
+
its reference are for the host to choose from.
|
|
3
7
|
|
|
4
8
|
configure: a TLV the probe cannot honour is refused (rejected unsupported, 0x0B, payload = the tag) when the host marked
|
|
5
9
|
it critical, and otherwise ignored and listed in the answer's ignored TLV (0x7F, oep-core §2.3)."""
|
|
@@ -17,12 +21,17 @@ from .frames import FramingLost
|
|
|
17
21
|
from .core import Interface, confirm
|
|
18
22
|
|
|
19
23
|
_CAP = reg.FIXTURE_CAPTURE
|
|
24
|
+
_ANA = reg.FIXTURE_ANALOG
|
|
25
|
+
_GRP = reg.FIXTURE_CAPTURE_GROUP
|
|
20
26
|
# configure TLVs; bit 7 of a tag = critical (the probe must reject what it cannot do)
|
|
21
27
|
MODE, RATE, SAMPLES, SEGMENTS, TRIGGER, PRETRIGGER, FRONTEND = (
|
|
22
28
|
_CAP.tlv["configure"][k] for k in ("mode", "rate", "samples", "segments", "trigger", "pretrigger", "frontend"))
|
|
23
|
-
ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
ACTUAL_RATE, LAYOUT, ACTUAL_SAMPLES, ACTUAL_SEGMENTS, TIMING, SCALE, BLOCKING, SKEW, FRONTEND_USED, REFERENCE, \
|
|
30
|
+
RATE_ACCURACY = (_ANA.tlv["configure_answer"][k] for k in (
|
|
31
|
+
"actual_rate", "layout", "actual_samples", "actual_segments", "timing", "scale", "blocking_ms", "skew",
|
|
32
|
+
"frontend_used", "reference", "rate_accuracy"))
|
|
33
|
+
FACTORY, VREFINT = _ANA.tlv["calibration_answer"]["factory"], _ANA.tlv["calibration_answer"]["vrefint"]
|
|
34
|
+
REFERENCE_SOURCE = {v: k for k, v in _ANA.enum["reference_source"].items()}
|
|
26
35
|
IGNORED = m.TAG_IGNORED
|
|
27
36
|
CRITICAL = m.TAG_CRITICAL
|
|
28
37
|
ONE_SHOT, REPEAT, STREAMING = (_CAP.enum["mode"][k] for k in ("one_shot", "repeat", "streaming"))
|
|
@@ -37,7 +46,8 @@ def tlvs(payload: bytes) -> list[tuple[int, bytes]]:
|
|
|
37
46
|
return m.split_tlvs(payload)
|
|
38
47
|
|
|
39
48
|
|
|
40
|
-
SEGMENT_BYTES =
|
|
49
|
+
SEGMENT_BYTES = 33 # serial u32, position u64, samples u32, start_ns u64, start_uncertainty_ns u32, trigger_index u32,
|
|
50
|
+
# flags u8
|
|
41
51
|
|
|
42
52
|
|
|
43
53
|
@dataclass
|
|
@@ -45,7 +55,8 @@ class Segment:
|
|
|
45
55
|
serial: int
|
|
46
56
|
position: int
|
|
47
57
|
samples: int
|
|
48
|
-
|
|
58
|
+
start_ns: int # the first sample's time on the probe's clock (an estimate)
|
|
59
|
+
start_uncertainty_ns: int # +- of start_ns (a guide, not a promise)
|
|
49
60
|
trigger_index: int | None
|
|
50
61
|
flags: int
|
|
51
62
|
|
|
@@ -56,8 +67,8 @@ class Segment:
|
|
|
56
67
|
|
|
57
68
|
@classmethod
|
|
58
69
|
def unpack(cls, b: bytes) -> "Segment":
|
|
59
|
-
serial, position, samples,
|
|
60
|
-
return cls(serial, position, samples,
|
|
70
|
+
serial, position, samples, start_ns, uncertainty, trig, flags = struct.unpack_from("<IQIQIIB", b)
|
|
71
|
+
return cls(serial, position, samples, start_ns, uncertainty, None if trig == 0xFFFFFFFF else trig, flags)
|
|
61
72
|
|
|
62
73
|
|
|
63
74
|
@dataclass
|
|
@@ -74,9 +85,13 @@ class Config:
|
|
|
74
85
|
segments: int = 0
|
|
75
86
|
jitter_kind: int = 0
|
|
76
87
|
jitter_ns: int = 0
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
rate_measured: bool = False # rate_accuracy: the rate was measured (else computed from a divider)
|
|
89
|
+
rate_ppm: int = 0 # its uncertainty (0: unknown)
|
|
90
|
+
skew_ns: dict[int, int] = field(default_factory=dict) # analog, per channel (role)
|
|
91
|
+
zero: dict[int, int] = field(default_factory=dict) # analog, per channel
|
|
92
|
+
scale_nv: dict[int, int] = field(default_factory=dict) # analog, nV per value, per channel
|
|
93
|
+
frontend: dict[int, int] = field(default_factory=dict) # analog: the frontend each channel took
|
|
94
|
+
reference: tuple[str, int, bool] | None = None # analog: (source, mV, measured)
|
|
80
95
|
blocking_ms: int = 0
|
|
81
96
|
ignored: list[int] = field(default_factory=list)
|
|
82
97
|
|
|
@@ -140,9 +155,19 @@ def _config(payload: bytes, analog: bool) -> Config:
|
|
|
140
155
|
c.segments = struct.unpack("<I", v)[0]
|
|
141
156
|
elif tag == TIMING:
|
|
142
157
|
c.jitter_kind, c.jitter_ns = v[0], struct.unpack_from("<I", v, 1)[0]
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
elif tag == RATE_ACCURACY:
|
|
159
|
+
c.rate_measured, c.rate_ppm = v[0] == 1, struct.unpack_from("<I", v, 1)[0]
|
|
160
|
+
elif tag == SCALE and analog:
|
|
161
|
+
role, zero, scale = struct.unpack("<BII", v)
|
|
162
|
+
c.zero[role], c.scale_nv[role] = zero, scale
|
|
163
|
+
elif tag == SKEW and analog:
|
|
164
|
+
role, ns = struct.unpack("<BI", v)
|
|
165
|
+
c.skew_ns[role] = ns
|
|
166
|
+
elif tag == FRONTEND_USED and analog:
|
|
167
|
+
c.frontend[v[0]] = v[1]
|
|
168
|
+
elif tag == REFERENCE and analog:
|
|
169
|
+
source, mv, how = struct.unpack("<BIB", v)
|
|
170
|
+
c.reference = (REFERENCE_SOURCE.get(source, str(source)), mv, how == 1)
|
|
146
171
|
elif tag == BLOCKING:
|
|
147
172
|
c.blocking_ms = struct.unpack("<I", v)[0]
|
|
148
173
|
elif tag == IGNORED:
|
|
@@ -165,7 +190,7 @@ class LogicCapture(Interface):
|
|
|
165
190
|
|
|
166
191
|
def configure(self, *, rate: int, mode: int = ONE_SHOT, samples: int | None = None, segments: int | None = None,
|
|
167
192
|
trigger: tuple[int, int, int] | None = None, pretrigger: int | None = None, query: bool = False,
|
|
168
|
-
critical: set[int] = frozenset()) -> Config:
|
|
193
|
+
critical: set[int] = frozenset(), frontends: dict[int, int] | None = None) -> Config:
|
|
169
194
|
"""-> the probe's actual values (Config.ignored: tags the probe ignored). `critical`: tags the probe must honour
|
|
170
195
|
or reject (host.Unsupported, .tag = the one it cannot)."""
|
|
171
196
|
def tlv(tag: int, value: bytes) -> bytes:
|
|
@@ -179,6 +204,8 @@ class LogicCapture(Interface):
|
|
|
179
204
|
body += tlv(TRIGGER, struct.pack("<BBH", *trigger))
|
|
180
205
|
if pretrigger is not None:
|
|
181
206
|
body += tlv(PRETRIGGER, struct.pack("<I", pretrigger))
|
|
207
|
+
for role, fe in sorted((frontends or {}).items()): # analog: the input range per channel (describe frontend)
|
|
208
|
+
body += tlv(FRONTEND, bytes([role, fe]))
|
|
182
209
|
# query is its own operation: the lock is decided per operation, before the payload is looked at
|
|
183
210
|
op = self.QUERY_OP if query else self.CONFIGURE
|
|
184
211
|
c = _config(self._call(op, body, locked=not query).payload, self.ANALOG)
|
|
@@ -331,7 +358,8 @@ class LogicCapture(Interface):
|
|
|
331
358
|
"""The segment's bytes; every Host.on_capture callback gets them as a CaptureRecord (a run recorder, e.g.
|
|
332
359
|
pytest-embedded-wireskein, without this package knowing it)."""
|
|
333
360
|
c = self.config
|
|
334
|
-
|
|
361
|
+
n = (segment.samples * c.width + 7) // 8 if c.width else segment.samples * len(c.order) * c.slot // 8
|
|
362
|
+
data = self.read(segment.position, n)
|
|
335
363
|
if self.host.on_capture:
|
|
336
364
|
record = CaptureRecord(self.fn, self.name, c, segment, data, self.armed_s, time.monotonic())
|
|
337
365
|
for callback in list(self.host.on_capture):
|
|
@@ -365,3 +393,105 @@ class LogicCapture(Interface):
|
|
|
365
393
|
z.writestr("version", "2")
|
|
366
394
|
z.writestr("metadata", "\n".join(meta))
|
|
367
395
|
z.writestr("logic-1-1", out)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
@dataclass
|
|
399
|
+
class Calibration:
|
|
400
|
+
"""What the probe knows for turning an analog value into a voltage (oep-if-capture §3.8), raw: the probe applies
|
|
401
|
+
none of it. factory: (frontend or None, scheme, raw bytes) - scheme names how to read raw; vrefint: (raw, ns), the
|
|
402
|
+
internal reference measured after the last start."""
|
|
403
|
+
factory: list[tuple[int | None, str, bytes]] = field(default_factory=list)
|
|
404
|
+
vrefint: tuple[int, int] | None = None
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
class AnalogCapture(LogicCapture):
|
|
408
|
+
"""Basic analog capture (oep.fixture.analog): the same operations as the logic one; values are raw (§1.2)."""
|
|
409
|
+
NAME = "oep.fixture.analog"
|
|
410
|
+
ANALOG = True
|
|
411
|
+
CALIBRATION = _ANA.op["calibration"]
|
|
412
|
+
|
|
413
|
+
def values(self, data: bytes, k: int, samples: int | None = None) -> list[int]:
|
|
414
|
+
"""Channel k's raw values (§1.2: slot s bits little endian, the value in bits o .. o+b-1, frames in `order`)."""
|
|
415
|
+
c = self.config
|
|
416
|
+
width = c.slot // 8
|
|
417
|
+
frame = width * len(c.order)
|
|
418
|
+
n = samples if samples is not None else len(data) // frame
|
|
419
|
+
m_ = c.order.index(k)
|
|
420
|
+
mask = (1 << c.bits) - 1
|
|
421
|
+
return [(int.from_bytes(data[i * frame + m_ * width:i * frame + (m_ + 1) * width], "little") >> c.offset) & mask
|
|
422
|
+
for i in range(n)]
|
|
423
|
+
|
|
424
|
+
def millivolts(self, k: int, value: int) -> float:
|
|
425
|
+
"""The probe's own 1st-order reading of a raw value of channel k, (value - zero) x scale_nv (a nominal
|
|
426
|
+
reference: see reference and calibration() for others)."""
|
|
427
|
+
c = self.config
|
|
428
|
+
return (value - c.zero.get(k, 0)) * c.scale_nv.get(k, 0) / 1_000_000
|
|
429
|
+
|
|
430
|
+
def calibration(self) -> Calibration:
|
|
431
|
+
out = Calibration()
|
|
432
|
+
for tag, v in tlvs(self._call(self.CALIBRATION, locked=False).payload):
|
|
433
|
+
if tag == FACTORY:
|
|
434
|
+
n = v[1]
|
|
435
|
+
out.factory.append((None if v[0] == 0xFF else v[0], v[2:2 + n].decode("utf-8", "replace"), bytes(v[2 + n:])))
|
|
436
|
+
elif tag == VREFINT:
|
|
437
|
+
out.vrefint = struct.unpack("<IQ", v)
|
|
438
|
+
return out
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@dataclass
|
|
442
|
+
class GroupStatus:
|
|
443
|
+
state: int
|
|
444
|
+
start_ns: int | None
|
|
445
|
+
trigger_ns: int | None
|
|
446
|
+
trigger_fn: int | None
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class CaptureGroup(Interface):
|
|
450
|
+
"""oep.fixture.capture-group (§4): tracks (LogicCapture / AnalogCapture, each configured as usual) started together,
|
|
451
|
+
one of them the trigger. Each track is read as usual; a track's offset is its first segment's start_ns minus the
|
|
452
|
+
group's start_ns, and every track's segment marks the trigger's instant (trigger_index)."""
|
|
453
|
+
NAME = "oep.fixture.capture-group"
|
|
454
|
+
REVISION = 1
|
|
455
|
+
BIND, START, STOP, FORCE, STATUS = (_GRP.op[k] for k in ("bind", "start", "stop", "force", "status"))
|
|
456
|
+
TAG_TRIGGER_TRACK = _GRP.tlv["bind"]["trigger_track"]
|
|
457
|
+
EVENT_TRIGGERED, EVENT_STOPPED = _GRP.event["triggered"], _GRP.event["stopped"]
|
|
458
|
+
NO_TIME = 0xFFFFFFFFFFFFFFFF
|
|
459
|
+
|
|
460
|
+
def bind(self, tracks: list[LogicCapture], trigger: LogicCapture | None = None) -> None:
|
|
461
|
+
"""Bind these (configured) tracks; [] unbinds. `trigger`: the track whose configure trigger starts them all."""
|
|
462
|
+
body = struct.pack(f"<B{len(tracks)}H", len(tracks), *(t.fn for t in tracks))
|
|
463
|
+
if trigger is not None:
|
|
464
|
+
body += m.tlv(self.TAG_TRIGGER_TRACK, struct.pack("<H", trigger.fn), critical=True)
|
|
465
|
+
self._call(self.BIND, body)
|
|
466
|
+
|
|
467
|
+
def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
|
|
468
|
+
"""-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s to set (for records)."""
|
|
469
|
+
blocking, start_ns = m.Reader(self._call(self.START).payload).take("IQ")
|
|
470
|
+
now = time.monotonic()
|
|
471
|
+
for t in tracks:
|
|
472
|
+
t.armed_s = now
|
|
473
|
+
return blocking, start_ns
|
|
474
|
+
|
|
475
|
+
def stop(self) -> None:
|
|
476
|
+
self._call(self.STOP)
|
|
477
|
+
|
|
478
|
+
def force(self) -> None:
|
|
479
|
+
self._call(self.FORCE)
|
|
480
|
+
|
|
481
|
+
def status(self) -> GroupStatus:
|
|
482
|
+
state, start, trig, fn = m.Reader(self._call(self.STATUS, locked=False).payload).take("BQQH")
|
|
483
|
+
none = self.NO_TIME
|
|
484
|
+
return GroupStatus(state, None if start == none else start, None if trig == none else trig, fn or None)
|
|
485
|
+
|
|
486
|
+
def wait(self, timeout: float = 5.0) -> GroupStatus:
|
|
487
|
+
"""Poll until every track is done (one-shot)."""
|
|
488
|
+
deadline = time.monotonic() + timeout
|
|
489
|
+
while time.monotonic() < deadline:
|
|
490
|
+
st = self.status()
|
|
491
|
+
if st.state == STATE["done"]:
|
|
492
|
+
return st
|
|
493
|
+
if st.state == STATE["error"]:
|
|
494
|
+
raise h.Failed(None, "the group's capture stopped with an error")
|
|
495
|
+
time.sleep(0.002)
|
|
496
|
+
raise TimeoutError("the group's capture did not finish")
|
|
497
|
+
|
oep_client/endpoint.py
CHANGED
|
@@ -39,7 +39,7 @@ from collections import OrderedDict
|
|
|
39
39
|
from dataclasses import dataclass, field
|
|
40
40
|
from typing import Callable
|
|
41
41
|
|
|
42
|
-
from . import catalog, fake, message as m, registry as reg
|
|
42
|
+
from . import catalog, fake, fake_capture, message as m, registry as reg
|
|
43
43
|
|
|
44
44
|
TOY_WRITE, TOY_READ = 0x01, 0x02
|
|
45
45
|
OK, WAIT, LINE, FAULT, TIMEOUT, STATE = (reg.STATUS[k] for k in ("ok", "wait", "line", "fault", "timeout", "state"))
|
|
@@ -275,6 +275,11 @@ class Endpoint:
|
|
|
275
275
|
first = self._role_pairs(fn)[0]
|
|
276
276
|
self.targets[(fn, first)] = FakeTarget()
|
|
277
277
|
self.target = next(iter(self.targets.values()), FakeTarget())
|
|
278
|
+
self.captures: dict[int, fake_capture.FakeCapture] = {
|
|
279
|
+
fn: self._capture_from(self.static[fn]) for fn, name in self.names.items()
|
|
280
|
+
if name in ("oep.fixture.capture", "oep.fixture.analog")}
|
|
281
|
+
self.groups: dict[int, fake_capture.FakeGroup] = {
|
|
282
|
+
fn: self._group_from(self.static[fn]) for fn, name in self.names.items() if name == "oep.fixture.capture-group"}
|
|
278
283
|
self.mechanisms = set()
|
|
279
284
|
for fn, name in self.names.items():
|
|
280
285
|
if name == "oep.target.console":
|
|
@@ -302,6 +307,9 @@ class Endpoint:
|
|
|
302
307
|
self.dropped = 0 # requests a v0 endpoint dropped (role 0x81)
|
|
303
308
|
self.requests: list[m.Request] = []
|
|
304
309
|
self.subscribed: set[int] = set()
|
|
310
|
+
self.push_seq: dict[int, int] = {} # fn -> the next event / data seq (core §11.2)
|
|
311
|
+
self.outbox: list[bytes] = [] # events and data frames waiting to go out (pushes())
|
|
312
|
+
self.capture_slipped = False # every capture segment says flags bit2 (a pace that fell behind)
|
|
305
313
|
self.plan: set[tuple[int, int, int]] = set() # (fn, role, channel), from plan_apply and the config
|
|
306
314
|
self.plan_from_config: set[int] = set() # fns whose plan came from the config (not a session's)
|
|
307
315
|
self.resend: OrderedDict[int, tuple[int, int, int, bytes | None]] = OrderedDict()
|
|
@@ -340,6 +348,155 @@ class Endpoint:
|
|
|
340
348
|
def target_id(self, value: int | None) -> None:
|
|
341
349
|
self.target.target_id = value
|
|
342
350
|
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
|
|
353
|
+
"""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"]
|
|
355
|
+
modes, widths, lo, hi, ring, most, fronts = set(), {8}, 1, 1_000_000, 1, 1024, {}
|
|
356
|
+
for t in tlvs:
|
|
357
|
+
tag, v = t[0], t[2:2 + t[1]]
|
|
358
|
+
if tag == d["frontend"]: # analog: frontend min_mv max_mv attenuation_mdb
|
|
359
|
+
fe, lo_mv, hi_mv, mdb = struct.unpack("<BiiI", v)
|
|
360
|
+
fronts[fe] = (lo_mv, hi_mv, mdb)
|
|
361
|
+
elif tag == d["mode"]:
|
|
362
|
+
modes.add(v[0])
|
|
363
|
+
elif tag == d["channels"]:
|
|
364
|
+
widths = {1 << i for i in range(6) if v[1] >> i & 1}
|
|
365
|
+
elif tag == catalog.MIN_CLOCK_HZ:
|
|
366
|
+
lo = struct.unpack("<I", v)[0]
|
|
367
|
+
elif tag == catalog.MAX_CLOCK_HZ:
|
|
368
|
+
hi = struct.unpack("<I", v)[0]
|
|
369
|
+
elif tag == d["segment_ring"]:
|
|
370
|
+
ring = struct.unpack("<H", v)[0]
|
|
371
|
+
elif tag == d["max_read"]:
|
|
372
|
+
most = struct.unpack("<I", v)[0]
|
|
373
|
+
return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most, fronts)
|
|
374
|
+
|
|
375
|
+
@staticmethod
|
|
376
|
+
def _group_from(tlvs: list[bytes]) -> fake_capture.FakeGroup:
|
|
377
|
+
d = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
|
|
378
|
+
tracks, most, budgets = [], 1, []
|
|
379
|
+
for t in tlvs:
|
|
380
|
+
tag, v = t[0], t[2:2 + t[1]]
|
|
381
|
+
if tag == d["tracks"]:
|
|
382
|
+
tracks = list(struct.unpack(f"<{len(v) // 2}H", v))
|
|
383
|
+
elif tag == d["max_tracks"]:
|
|
384
|
+
most = v[0]
|
|
385
|
+
elif tag == d["budget"]:
|
|
386
|
+
budgets.append((struct.unpack_from("<I", v)[0], list(struct.unpack_from(f"<{(len(v) - 4) // 2}H", v, 4))))
|
|
387
|
+
return fake_capture.FakeGroup(tracks, most, budgets)
|
|
388
|
+
|
|
389
|
+
def _next_seq(self, fn: int) -> int:
|
|
390
|
+
seq = self.push_seq.get(fn, 0)
|
|
391
|
+
self.push_seq[fn] = (seq + 1) & 0xFFFF
|
|
392
|
+
return seq
|
|
393
|
+
|
|
394
|
+
def _events(self, fn: int, events: list[bytes]) -> None:
|
|
395
|
+
"""Events (kind(u8) payload) of `fn` to go out while it is subscribed; unsubscribed, they are not sent."""
|
|
396
|
+
for e in events:
|
|
397
|
+
if fn in self.subscribed:
|
|
398
|
+
self.outbox.append(bytes([m.ROLE_EVENT]) + struct.pack("<HH", fn, self._next_seq(fn)) + e)
|
|
399
|
+
|
|
400
|
+
def pushes(self) -> list[bytes]:
|
|
401
|
+
"""The frames the probe sends by itself now (core §11): events, and a streaming capture's data. A serving loop
|
|
402
|
+
frames and sends them; a test takes them from here."""
|
|
403
|
+
self.tick()
|
|
404
|
+
for fn, cap in self.captures.items():
|
|
405
|
+
if fn in self.subscribed:
|
|
406
|
+
self.outbox += cap.pushes(fn, lambda fn=fn: self._next_seq(fn), self.probe.max_frame)
|
|
407
|
+
out, self.outbox = self.outbox, []
|
|
408
|
+
return out
|
|
409
|
+
|
|
410
|
+
def _capture(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
411
|
+
cap, O = self.captures[fn], fake_capture.OP
|
|
412
|
+
self._events(fn, cap.tick(self.now())) # what the clock captured up to this request
|
|
413
|
+
roles = sorted(a[1] for a in self.plan if a[0] == fn)
|
|
414
|
+
budget = self.probe.max_frame - m.RESULT_HEADER
|
|
415
|
+
try:
|
|
416
|
+
if op in (O["configure"], O["query"]):
|
|
417
|
+
rest = t.data[t.at:]
|
|
418
|
+
analog = self.names[fn] == "oep.fixture.analog"
|
|
419
|
+
known = set(fake_capture.TLV.values()) - (set() if analog else {fake_capture.TLV["frontend"]})
|
|
420
|
+
got, ignored = t.tail(known)
|
|
421
|
+
# the frontend TLV comes once per channel: read them all
|
|
422
|
+
fronts = [tuple(v[:2]) for tag, v in m.split_tlvs(rest) if tag & 0x7F == fake_capture.TLV["frontend"]] \
|
|
423
|
+
if analog else []
|
|
424
|
+
settled = cap.settle(got, t.critical, len(roles), fronts)
|
|
425
|
+
if op == O["configure"]:
|
|
426
|
+
cap.apply(settled)
|
|
427
|
+
cap.slipped = self.capture_slipped
|
|
428
|
+
return self._answer(cap.answer(settled), ignored)
|
|
429
|
+
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
|
|
431
|
+
if op == O["start"]:
|
|
432
|
+
t.tail()
|
|
433
|
+
self._events(fn, cap.start(self.now()))
|
|
434
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<I", 0)
|
|
435
|
+
if op == O["stop"]:
|
|
436
|
+
t.tail()
|
|
437
|
+
self._events(fn, cap.stop())
|
|
438
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
439
|
+
if op == O["force"]:
|
|
440
|
+
t.tail()
|
|
441
|
+
return m.COMPLETED, m.SUCCESS, b"" # nothing waits: a trigger is found at start
|
|
442
|
+
if op == O["status"]:
|
|
443
|
+
t.tail()
|
|
444
|
+
return m.COMPLETED, m.SUCCESS, cap.status()
|
|
445
|
+
if op == O["read"]:
|
|
446
|
+
position, most = t.take("QI")
|
|
447
|
+
return m.COMPLETED, m.SUCCESS, cap.read(position, most, budget)
|
|
448
|
+
if op == O["segments"]:
|
|
449
|
+
first = t.take("I")
|
|
450
|
+
t.tail()
|
|
451
|
+
return m.COMPLETED, m.SUCCESS, cap.segment_list(first, budget)
|
|
452
|
+
if op == O["release"]:
|
|
453
|
+
serial = t.take("I")
|
|
454
|
+
t.tail()
|
|
455
|
+
cap.release(serial, self.now())
|
|
456
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
457
|
+
if op == fake_capture.ANA.op["calibration"] and cap.analog:
|
|
458
|
+
t.tail()
|
|
459
|
+
return m.COMPLETED, m.SUCCESS, cap.calibration()
|
|
460
|
+
except fake_capture.Reject as e:
|
|
461
|
+
raise Reject(e.reason, e.payload)
|
|
462
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
463
|
+
|
|
464
|
+
def _group(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
465
|
+
grp, O = self.groups[fn], fake_capture.GRP.op
|
|
466
|
+
try:
|
|
467
|
+
if op == O["bind"]:
|
|
468
|
+
n = t.take("B")
|
|
469
|
+
fns = [t.take("H") for _ in range(n)]
|
|
470
|
+
got, ignored = t.tail({fake_capture.GRP.tlv["bind"]["trigger_track"]})
|
|
471
|
+
src = got.get(fake_capture.GRP.tlv["bind"]["trigger_track"])
|
|
472
|
+
grp.bind(self.captures, fns, struct.unpack("<H", src)[0] if src else 0)
|
|
473
|
+
return self._answer(b"", ignored)
|
|
474
|
+
if op == O["start"]:
|
|
475
|
+
t.tail()
|
|
476
|
+
per, own = grp.start(self.captures, self.now())
|
|
477
|
+
for track, events in per:
|
|
478
|
+
self._events(track, events)
|
|
479
|
+
self._events(fn, own)
|
|
480
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<IQ", 0, grp.start_ns)
|
|
481
|
+
if op == O["stop"]:
|
|
482
|
+
t.tail()
|
|
483
|
+
per, own = grp.stop(self.captures)
|
|
484
|
+
for track, events in per:
|
|
485
|
+
self._events(track, events)
|
|
486
|
+
self._events(fn, own)
|
|
487
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
488
|
+
if op == O["force"]:
|
|
489
|
+
t.tail()
|
|
490
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
491
|
+
if op == O["status"]:
|
|
492
|
+
t.tail()
|
|
493
|
+
for track in grp.tracks:
|
|
494
|
+
self._events(track, self.captures[track].tick(self.now()))
|
|
495
|
+
return m.COMPLETED, m.SUCCESS, grp.status(self.captures)
|
|
496
|
+
except fake_capture.Reject as e:
|
|
497
|
+
raise Reject(e.reason, e.payload)
|
|
498
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
499
|
+
|
|
343
500
|
def _role_pairs(self, fn: int) -> list[tuple[int, int]]:
|
|
344
501
|
"""A role_channels wire's pairs in the count = 0 order (oep-if-debug §1): swdio ascending, then swclk."""
|
|
345
502
|
roles = self.pin_roles[fn]
|
|
@@ -361,7 +518,7 @@ class Endpoint:
|
|
|
361
518
|
def _held(self, fn: int | None = None, pair: tuple[int, int] | None = None) -> set[int]:
|
|
362
519
|
"""Channels something holds (core §8.1): the plan, the slots' pairs, the live connections' pairs - except
|
|
363
520
|
wire `fn`'s own connection on `pair` (attaching there again, or scanning through it)."""
|
|
364
|
-
held = {a[2] for a in self.plan}
|
|
521
|
+
held = {a[2] for a in self.plan if a[0] not in self.captures} # a capture only listens
|
|
365
522
|
held |= {p for s in self.slots.values() for p in s.pair if p != 0xFFFF and not (s.wire_fn == fn and s.pair == pair)}
|
|
366
523
|
held |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF and not (c.fn == fn and c.pair == pair)}
|
|
367
524
|
return held
|
|
@@ -613,6 +770,7 @@ class Endpoint:
|
|
|
613
770
|
if fn != 0 and fn not in self.names:
|
|
614
771
|
return m.REJECTED, m.UNAVAILABLE, b""
|
|
615
772
|
self.subscribed.add(fn)
|
|
773
|
+
self.push_seq[fn] = 0 # seq from 0 at every subscribe (core §11.2)
|
|
616
774
|
return m.COMPLETED, m.SUCCESS, b""
|
|
617
775
|
if op == m.OP_UNSUBSCRIBE:
|
|
618
776
|
fn = t.take("H")
|
|
@@ -644,14 +802,24 @@ class Endpoint:
|
|
|
644
802
|
def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
|
|
645
803
|
"""plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
|
|
646
804
|
named = {fn for fn, _, _ in got}
|
|
647
|
-
|
|
805
|
+
# a capture only listens: it shares pins with anything, and nothing is kept from a pin by it (core §8.1)
|
|
806
|
+
kept = {a for a in self.plan if a[0] not in named and a[0] not in self.captures}
|
|
648
807
|
slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
|
|
649
808
|
slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
|
|
650
809
|
for fn, role, ch in got:
|
|
810
|
+
if fn in self.captures:
|
|
811
|
+
if role not in self._declared_roles(fn, ch):
|
|
812
|
+
raise Reject(m.UNAVAILABLE)
|
|
813
|
+
continue
|
|
651
814
|
roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
|
|
652
815
|
if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
|
|
653
816
|
raise Reject(m.UNAVAILABLE)
|
|
654
817
|
|
|
818
|
+
def _declared_roles(self, fn: int, channel: int) -> set[int]:
|
|
819
|
+
"""The roles fn's describe offers on `channel` (role_channels)."""
|
|
820
|
+
return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS
|
|
821
|
+
and channel in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
|
|
822
|
+
|
|
655
823
|
def _drop_plan(self, fn: int) -> None:
|
|
656
824
|
for a in [a for a in self.plan if a[0] == fn]:
|
|
657
825
|
self.gpio_modes.pop(a[2], None)
|
|
@@ -787,8 +955,11 @@ class Endpoint:
|
|
|
787
955
|
if not self._allows(fn, pair) or set(pair) & self._held(fn, pair):
|
|
788
956
|
raise Reject(m.UNAVAILABLE) # not allowed here, or its pins are held (§8.1)
|
|
789
957
|
return pair
|
|
958
|
+
live = [c.pair for c in self.conns.values() if c.fn == fn]
|
|
959
|
+
if len(live) == 1:
|
|
960
|
+
return live[0] # no pins: the wire's one live connection
|
|
790
961
|
allowed = self._allowed_pairs(fn)
|
|
791
|
-
if len(allowed) != 1:
|
|
962
|
+
if live or len(allowed) != 1:
|
|
792
963
|
raise Reject(m.UNAVAILABLE) # the host chooses among several
|
|
793
964
|
return allowed[0]
|
|
794
965
|
|
|
@@ -1399,9 +1570,11 @@ class Endpoint:
|
|
|
1399
1570
|
self.flows[(port, key)] = Flow(sid, 0)
|
|
1400
1571
|
|
|
1401
1572
|
def tick(self) -> None:
|
|
1402
|
-
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet."""
|
|
1573
|
+
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
|
|
1403
1574
|
self._lapse()
|
|
1404
1575
|
now = self.now()
|
|
1576
|
+
for fn, cap in self.captures.items():
|
|
1577
|
+
self._events(fn, cap.tick(now))
|
|
1405
1578
|
for n, s in self.slots.items():
|
|
1406
1579
|
rt = self.slot_rt[n]
|
|
1407
1580
|
if (s.attach == SLOT_ATTACH["at_boot"] and s.retry_s and not rt.evicted
|
|
@@ -1549,4 +1722,5 @@ class Endpoint:
|
|
|
1549
1722
|
|
|
1550
1723
|
SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
|
|
1551
1724
|
"oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
|
|
1552
|
-
"oep.probe.config": "config_op"
|
|
1725
|
+
"oep.probe.config": "config_op", "oep.fixture.capture": "capture", "oep.fixture.analog": "capture",
|
|
1726
|
+
"oep.fixture.capture-group": "group"}
|
oep_client/fake.py
CHANGED
|
@@ -108,6 +108,42 @@ TRANSPORT = reg.CORE.enum["transport_kind"]
|
|
|
108
108
|
SERIAL_KINDS = {TRANSPORT["uart_bridge"], TRANSPORT["usb_cdc"], TRANSPORT["usb_serial_jtag"]}
|
|
109
109
|
NS = "io.github.ch32-riscv-ug"
|
|
110
110
|
MECHANISMS = reg.TARGET_CONSOLE.tlv["describe"]["mechanisms"]
|
|
111
|
+
_CAPD = reg.FIXTURE_CAPTURE.tlv["describe"]
|
|
112
|
+
_ANAD = reg.FIXTURE_ANALOG.tlv["describe"]
|
|
113
|
+
_GRPD = reg.FIXTURE_CAPTURE_GROUP.tlv["describe"]
|
|
114
|
+
CORE_CHIP = reg.CORE.tlv["describe"]["chip"]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _analog_decl(frontends: list[tuple[int, int, int, int]], max_samples: int) -> tuple[bytes, ...]:
|
|
118
|
+
"""oep.fixture.analog's declarations: one-shot / repeat / streaming, 4 channels in 16-bit slots, the frontends
|
|
119
|
+
(number, min_mv, max_mv, attenuation_mdb), triggers cross up / down."""
|
|
120
|
+
out = tuple(catalog.tlv(_ANAD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, 8 if k == "repeat" else 1))
|
|
121
|
+
for k in ("one_shot", "repeat", "streaming"))
|
|
122
|
+
out += tuple(catalog.tlv(_ANAD["frontend"], struct.pack("<BiiI", *f)) for f in frontends)
|
|
123
|
+
return out + (catalog.tlv(_ANAD["channels"], bytes([4, 0b100])), # s 16
|
|
124
|
+
catalog.tlv(_ANAD["trigger"], struct.pack("<BI", 0b11001, max_samples - 1)), # immediate, cross up / down
|
|
125
|
+
catalog.u32(_ANAD["max_read"], 4096), catalog.u16(_ANAD["segment_ring"], 8),
|
|
126
|
+
catalog.u32(FEATURES, 0b001))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _group_decl(tracks: list[int], budgets: list[tuple[int, list[int]]], skews: dict[int, int]) -> tuple[bytes, ...]:
|
|
130
|
+
return ((catalog.tlv(_GRPD["tracks"], struct.pack(f"<{len(tracks)}H", *tracks)), catalog.u8(_GRPD["max_tracks"], len(tracks)))
|
|
131
|
+
+ tuple(catalog.tlv(_GRPD["budget"], struct.pack(f"<I{len(f)}H", most, *f)) for most, f in budgets)
|
|
132
|
+
+ tuple(catalog.tlv(_GRPD["start_skew"], struct.pack("<HI", fn, ns)) for fn, ns in skews.items()))
|
|
133
|
+
_CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _capture_decl(modes: list[str], max_channels: int, widths: list[int], max_samples: int, ring: int,
|
|
137
|
+
max_read: int) -> tuple[bytes, ...]:
|
|
138
|
+
"""oep.fixture.capture's declarations (oep-if-capture §3.5): a mode entry per mode, channels (max, the w allowed as
|
|
139
|
+
bits: bit i = w 2^i), the trigger types (immediate, level, edge) with the most pretrigger, max_read, segment_ring."""
|
|
140
|
+
out = tuple(catalog.tlv(_CAPD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, ring if k == "repeat" else 1))
|
|
141
|
+
for k in modes)
|
|
142
|
+
bits = sum(1 << (w.bit_length() - 1) for w in widths)
|
|
143
|
+
return out + (catalog.tlv(_CAPD["channels"], bytes([max_channels, bits])),
|
|
144
|
+
catalog.tlv(_CAPD["trigger"], struct.pack("<BI", 0b111, max_samples - 1)),
|
|
145
|
+
catalog.u32(_CAPD["max_read"], max_read), catalog.u16(_CAPD["segment_ring"], ring),
|
|
146
|
+
catalog.u32(FEATURES, 0b001))
|
|
111
147
|
MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
|
|
112
148
|
_CFG = reg.PROBE_CONFIG.tlv["describe"]
|
|
113
149
|
|
|
@@ -153,7 +189,8 @@ def p4_x035() -> FakeProbe:
|
|
|
153
189
|
_core("3.0.0", "esp32-p4-devkit", bytes.fromhex("30eda0e31108"), 55, reserved, f"{NS}.p4-x035",
|
|
154
190
|
{2: "SWDIO", 54: "SWCLK", 51: "LED"},
|
|
155
191
|
_transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
|
|
156
|
-
(TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)])
|
|
192
|
+
(TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)])
|
|
193
|
+
+ (catalog.u8(CORE_OEP_PID, 1), catalog.text(CORE_CHIP, "esp32p4 v1.0"))),
|
|
157
194
|
Offered(1, 1, "oep.wire.rvswd", (
|
|
158
195
|
catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
|
|
159
196
|
catalog.u8(MAX_CONNECTIONS, 1))),
|
|
@@ -165,15 +202,23 @@ def p4_x035() -> FakeProbe:
|
|
|
165
202
|
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
166
203
|
Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
167
204
|
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
168
|
-
|
|
205
|
+
# the P4's PARLIO: w 1-16, one-shot / repeat / streaming
|
|
206
|
+
Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(16)}) + (
|
|
169
207
|
catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
|
|
170
|
-
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
|
|
208
|
+
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
|
|
209
|
+
+ _capture_decl(["one_shot", "repeat", "streaming"], 16, [1, 2, 4, 8, 16], 1 << 20, 8, 4096)),
|
|
171
210
|
Offered(8, 6, f"{NS}.esp32.i2c-target", _roles({1: pins, 2: pins}) + (
|
|
172
211
|
catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
|
|
173
212
|
catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
|
|
174
213
|
Offered(9, 7, f"{NS}.esp32.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
|
|
175
214
|
catalog.u16(MAX_LENGTH, 64), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
176
215
|
_config(10, 8, slots_max=1),
|
|
216
|
+
# the P4's ADC1 (GPIO16-23): one ADC for all its channels, 611 Hz - 83.3 kHz in all; ESP32-style attenuations
|
|
217
|
+
Offered(11, 1, "oep.fixture.analog", _roles({k: list(range(16, 24)) for k in range(4)}) + (
|
|
218
|
+
catalog.u32(MAX_CLOCK_HZ, 83_333), catalog.u32(MIN_CLOCK_HZ, 611), catalog.u8(IMPLEMENTATION, 3))
|
|
219
|
+
+ _analog_decl([(0, 0, 950, 0), (1, 0, 1250, 2500), (2, 0, 1750, 6000), (3, 0, 3100, 12000)], 65536)),
|
|
220
|
+
# the logic (fn 7) and the analog (fn 11) together; the ADC's 83.3 kHz is shared by its channels
|
|
221
|
+
Offered(12, 1, "oep.fixture.capture-group", _group_decl([7, 11], [(83_333, [11])], {11: 5000})),
|
|
177
222
|
])
|
|
178
223
|
|
|
179
224
|
|
|
@@ -194,8 +239,10 @@ def esp32_v003() -> FakeProbe:
|
|
|
194
239
|
Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
|
|
195
240
|
Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
|
|
196
241
|
catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
|
|
242
|
+
# the classic ESP32's GPIO sampler: a byte a sample (w 8), one-shot
|
|
197
243
|
Offered(6, 4, "oep.fixture.capture", _roles({k: wired for k in range(4)}) + (
|
|
198
|
-
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
|
|
244
|
+
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
|
|
245
|
+
+ _capture_decl(["one_shot"], 8, [8], 65536, 1, 480)),
|
|
199
246
|
Offered(7, 5, f"{NS}.esp32.i2c-target", _roles({1: wired, 2: wired}) + (
|
|
200
247
|
catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
201
248
|
# Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""oep.fixture.capture / oep.fixture.analog / oep.fixture.capture-group in the fake probe (oep-spec
|
|
3
|
+
docs/oep-if-capture.ja.md, revision 1).
|
|
4
|
+
|
|
5
|
+
What it captures is known in advance, so a receiver can check it (sample i counted from the track's start, across
|
|
6
|
+
segments):
|
|
7
|
+
|
|
8
|
+
- logic: sample i is the counter i, channel k bit k of it - a square wave of period 2^(k+1) samples. The layout is the
|
|
9
|
+
probe's (§1.1): w is the smallest width the describe allows (channels tag 0x44) that holds the channels, pos[k] = k.
|
|
10
|
+
- analog: 12-bit values in 16-bit slots (s 16, o 0, b 12), channels in role order. Channel k's period is
|
|
11
|
+
P = 64 (k // 2 + 1) samples: an even k is a square wave (4095 for the first half of the period, then 0), an odd k a sine
|
|
12
|
+
round(2047.5 + 2047 sin(2 pi i / P)). zero 0, scale_nv = the frontend's range / 4095.
|
|
13
|
+
|
|
14
|
+
Time (the probe's one clock, ns): a track's first sample is at its start (the group's start, or the endpoint's clock
|
|
15
|
+
at start) plus the track's start offset - logic 0 (+-50 ns), analog 5 us (+-2 us), as a probe that corrected what it
|
|
16
|
+
knows and says how sure it is. Later segments follow at the actual rate.
|
|
17
|
+
|
|
18
|
+
Modes: one-shot (the segment is there as soon as start answers), repeat (segments come with the clock at the actual
|
|
19
|
+
rate, up to the ring; a full ring stops the capture, release frees it) and streaming (the bytes come with the clock and
|
|
20
|
+
go out as data pushes while subscribed). Triggers: logic level / edge, analog cross up / down, with a pretrigger.
|
|
21
|
+
`slipped` sets flags bit2 on every segment (a probe whose software pace fell behind).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
import struct
|
|
28
|
+
from dataclasses import dataclass, field
|
|
29
|
+
from fractions import Fraction
|
|
30
|
+
|
|
31
|
+
from . import message as m, registry as reg
|
|
32
|
+
|
|
33
|
+
CAP, ANA, GRP = reg.FIXTURE_CAPTURE, reg.FIXTURE_ANALOG, reg.FIXTURE_CAPTURE_GROUP
|
|
34
|
+
OP = CAP.op
|
|
35
|
+
TLV, ANSWER = CAP.tlv["configure"], ANA.tlv["configure_answer"]
|
|
36
|
+
MODE, STATE, TRIGGER = CAP.enum["mode"], CAP.enum["state"], CAP.enum["trigger"]
|
|
37
|
+
STOPPED, FLAG = CAP.enum["stopped_reason"], CAP.enum["segment_flag"]
|
|
38
|
+
EVENT = CAP.event
|
|
39
|
+
CALIBRATION = ANA.tlv["calibration_answer"]
|
|
40
|
+
NONE = 0xFFFFFFFF
|
|
41
|
+
NO_TIME = 0xFFFFFFFFFFFFFFFF
|
|
42
|
+
MAX_SAMPLES = 1 << 20 # a fake keeps its captures in memory
|
|
43
|
+
SEGMENT = struct.Struct("<IQIQIIB") # serial position samples start_ns start_uncertainty_ns trigger_index flags (§2)
|
|
44
|
+
FULL = 4095 # the analog value's top (12 bits)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Reject(Exception):
|
|
48
|
+
def __init__(self, reason: int, payload: bytes = b""):
|
|
49
|
+
self.reason, self.payload = reason, payload
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Segment:
|
|
54
|
+
serial: int
|
|
55
|
+
position: int
|
|
56
|
+
samples: int
|
|
57
|
+
start_ns: int
|
|
58
|
+
start_uncertainty_ns: int
|
|
59
|
+
trigger_index: int = NONE
|
|
60
|
+
flags: int = 0
|
|
61
|
+
|
|
62
|
+
def pack(self) -> bytes:
|
|
63
|
+
return SEGMENT.pack(self.serial, self.position, self.samples, self.start_ns, self.start_uncertainty_ns,
|
|
64
|
+
self.trigger_index, self.flags)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def analog_value(k: int, i: int) -> int:
|
|
68
|
+
"""Channel k's value at sample i of the fake analog waveform."""
|
|
69
|
+
period = 64 * (k // 2 + 1)
|
|
70
|
+
if k % 2 == 0:
|
|
71
|
+
return FULL if i % period < period // 2 else 0
|
|
72
|
+
return round(2047.5 + 2047 * math.sin(2 * math.pi * i / period))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class FakeCapture:
|
|
77
|
+
"""One capture track (logic, or analog when `frontends` is given). Times: the endpoint's clock in ms, the probe's in
|
|
78
|
+
ns."""
|
|
79
|
+
modes: set[int] # the modes the describe declares
|
|
80
|
+
widths: set[int] # logic: the w the describe allows
|
|
81
|
+
min_hz: int
|
|
82
|
+
max_hz: int
|
|
83
|
+
ring: int = 8 # segment_ring
|
|
84
|
+
max_read: int = 4096
|
|
85
|
+
frontends: dict[int, tuple[int, int, int]] = field(default_factory=dict) # analog: n -> (min_mv, max_mv, mdb)
|
|
86
|
+
slipped: bool = False
|
|
87
|
+
state: int = STATE["unconfigured"]
|
|
88
|
+
mode: int = MODE["one_shot"]
|
|
89
|
+
rate: Fraction = Fraction(0)
|
|
90
|
+
samples: int = 0
|
|
91
|
+
segments_max: int = 0
|
|
92
|
+
trigger: tuple[int, int, int] | None = None
|
|
93
|
+
pretrigger: int = 0
|
|
94
|
+
width: int = 0 # logic: w; analog: the bytes of one sample (C x 2)
|
|
95
|
+
channels: int = 0
|
|
96
|
+
frontend_of: dict[int, int] = field(default_factory=dict) # analog: channel -> frontend
|
|
97
|
+
data: bytearray = field(default_factory=bytearray) # the stream from `base` on
|
|
98
|
+
base: int = 0 # the byte position data[0] is at
|
|
99
|
+
segs: list[Segment] = field(default_factory=list)
|
|
100
|
+
serial_done: int = 0
|
|
101
|
+
started_ms: int = 0
|
|
102
|
+
t0_ns: int = 0 # the probe-clock time of the track's first sample
|
|
103
|
+
produced: int = 0 # samples produced since start
|
|
104
|
+
sent: int = 0 # streaming: the byte position pushed so far
|
|
105
|
+
gap_next: bool = False # repeat: the capture stopped for want of a segment; the next one says so
|
|
106
|
+
group: object | None = None # the FakeGroup that binds it
|
|
107
|
+
vrefint_ns: int = NO_TIME
|
|
108
|
+
|
|
109
|
+
@property
|
|
110
|
+
def analog(self) -> bool:
|
|
111
|
+
return bool(self.frontends)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def start_offset_ns(self) -> int:
|
|
115
|
+
return 5000 if self.analog else 0
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def uncertainty_ns(self) -> int:
|
|
119
|
+
return 2000 if self.analog else 50
|
|
120
|
+
|
|
121
|
+
# ---- configure ------------------------------------------------------------------------------------------------
|
|
122
|
+
def settle(self, got: dict[int, bytes], critical: set[int], channels: int, frontends: list[tuple[int, int]]) -> dict:
|
|
123
|
+
"""The actual values for a configure / query request (nothing changed). `frontends`: (role, frontend) asked."""
|
|
124
|
+
if self.group is not None:
|
|
125
|
+
raise Reject(m.UNAVAILABLE) # bound: configure again after unbinding
|
|
126
|
+
mode = got[TLV["mode"]][0] if TLV["mode"] in got else MODE["one_shot"]
|
|
127
|
+
if mode not in self.modes:
|
|
128
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["mode"] | (m.TAG_CRITICAL if TLV["mode"] in critical else 0)]))
|
|
129
|
+
if TLV["rate"] not in got or len(got[TLV["rate"]]) != 4:
|
|
130
|
+
raise Reject(m.MALFORMED)
|
|
131
|
+
asked = struct.unpack("<I", got[TLV["rate"]])[0]
|
|
132
|
+
if not asked or not channels:
|
|
133
|
+
raise Reject(m.MALFORMED if not asked else m.UNAVAILABLE)
|
|
134
|
+
top = self.max_hz // channels if self.analog else self.max_hz # an ADC's rate is shared by its channels
|
|
135
|
+
div = max(1, -(-self.max_hz // min(max(asked, self.min_hz), top)))
|
|
136
|
+
rate = Fraction(self.max_hz, div)
|
|
137
|
+
samples = struct.unpack("<I", got[TLV["samples"]])[0] if TLV["samples"] in got else 4096
|
|
138
|
+
samples = max(1, min(samples, MAX_SAMPLES))
|
|
139
|
+
segs = struct.unpack("<I", got[TLV["segments"]])[0] if TLV["segments"] in got else self.ring
|
|
140
|
+
segs = max(1, min(segs, self.ring)) if mode == MODE["repeat"] else 1
|
|
141
|
+
trigger = None
|
|
142
|
+
if TLV["trigger"] in got:
|
|
143
|
+
kind, role, value = struct.unpack("<BBH", got[TLV["trigger"]])
|
|
144
|
+
allowed = (TRIGGER["cross_up"], TRIGGER["cross_down"]) if self.analog else (TRIGGER["level"], TRIGGER["edge"])
|
|
145
|
+
if (kind and kind not in allowed) or (kind and role >= channels):
|
|
146
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["trigger"] | m.TAG_CRITICAL]))
|
|
147
|
+
trigger = (kind, role, value) if kind else None
|
|
148
|
+
pre = struct.unpack("<I", got[TLV["pretrigger"]])[0] if TLV["pretrigger"] in got else 0
|
|
149
|
+
chosen = {}
|
|
150
|
+
if self.analog:
|
|
151
|
+
chosen = {k: max(self.frontends) for k in range(channels)} # the widest range unless asked
|
|
152
|
+
for role, fe in frontends:
|
|
153
|
+
if role >= channels or fe not in self.frontends:
|
|
154
|
+
raise Reject(m.UNAVAILABLE)
|
|
155
|
+
chosen[role] = fe
|
|
156
|
+
width = 2 * channels
|
|
157
|
+
else:
|
|
158
|
+
if frontends:
|
|
159
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["frontend"] | m.TAG_CRITICAL]))
|
|
160
|
+
width = min((w for w in self.widths if w >= channels), default=None)
|
|
161
|
+
if width is None:
|
|
162
|
+
raise Reject(m.UNAVAILABLE) # more than a sample holds
|
|
163
|
+
if mode != MODE["one_shot"] and width < 8:
|
|
164
|
+
samples = -(-samples // (8 // width)) * (8 // width) # segments end on a byte
|
|
165
|
+
return {"mode": mode, "rate": rate, "samples": samples, "segments": segs, "trigger": trigger,
|
|
166
|
+
"pretrigger": min(pre, samples - 1), "width": width, "channels": channels, "frontends": chosen}
|
|
167
|
+
|
|
168
|
+
def apply(self, s: dict) -> None:
|
|
169
|
+
self.mode, self.rate, self.samples, self.segments_max = s["mode"], s["rate"], s["samples"], s["segments"]
|
|
170
|
+
self.trigger, self.pretrigger, self.width, self.channels = s["trigger"], s["pretrigger"], s["width"], s["channels"]
|
|
171
|
+
self.frontend_of = s["frontends"]
|
|
172
|
+
self.state = STATE["configured"]
|
|
173
|
+
self._clear()
|
|
174
|
+
|
|
175
|
+
def answer(self, s: dict) -> bytes:
|
|
176
|
+
def tlv(tag: int, v: bytes) -> bytes:
|
|
177
|
+
return bytes([tag, len(v)]) + v
|
|
178
|
+
c = s["channels"]
|
|
179
|
+
out = tlv(ANSWER["actual_rate"], struct.pack("<II", s["rate"].numerator, s["rate"].denominator))
|
|
180
|
+
if self.analog:
|
|
181
|
+
out += tlv(ANSWER["layout"], bytes([16, 0, 12, c]) + bytes(range(c)))
|
|
182
|
+
else:
|
|
183
|
+
out += tlv(ANSWER["layout"], bytes([s["width"], c]) + bytes(range(c)))
|
|
184
|
+
out += (tlv(ANSWER["actual_samples"], struct.pack("<I", s["samples"]))
|
|
185
|
+
+ tlv(ANSWER["actual_segments"], struct.pack("<I", s["segments"]))
|
|
186
|
+
+ tlv(ANSWER["timing"], struct.pack("<BI", 1 if s["rate"].denominator != 1 else 0, 0))
|
|
187
|
+
+ tlv(ANSWER["blocking_ms"], struct.pack("<I", 0))
|
|
188
|
+
# the logic's rate is the divider's; the ADC's is off by some per mille, as the P4's (+0.15 %)
|
|
189
|
+
+ tlv(ANSWER["rate_accuracy"], struct.pack("<BI", 1, 1500) if self.analog else struct.pack("<BI", 0, 0)))
|
|
190
|
+
if self.analog:
|
|
191
|
+
step_ns = int(1e9 / (s["rate"] * c)) # one ADC, the channels in turn
|
|
192
|
+
for k in range(c):
|
|
193
|
+
lo, hi, _ = self.frontends[s["frontends"][k]]
|
|
194
|
+
out += tlv(ANSWER["scale"], struct.pack("<BII", k, 0, (hi - lo) * 1_000_000 // FULL))
|
|
195
|
+
out += tlv(ANSWER["skew"], struct.pack("<BI", k, k * step_ns))
|
|
196
|
+
out += tlv(ANSWER["frontend_used"], bytes([k, s["frontends"][k]]))
|
|
197
|
+
out += tlv(ANSWER["reference"], struct.pack("<BIB", ANA.enum["reference_source"]["internal"], 1100, 0))
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
def calibration(self) -> bytes:
|
|
201
|
+
"""The analog's calibration (§3.8): a made-up two-point value per frontend, and the reference measured at start."""
|
|
202
|
+
if not self.analog:
|
|
203
|
+
raise Reject(m.UNKNOWN_OPERATION)
|
|
204
|
+
scheme = b"org.example.fake.two-point"
|
|
205
|
+
out = b""
|
|
206
|
+
for fe in sorted(self.frontends):
|
|
207
|
+
v = bytes([fe, len(scheme)]) + scheme + struct.pack("<HH", 150, 3950) # raw at the range's 5 % / 95 %
|
|
208
|
+
out += bytes([CALIBRATION["factory"], len(v)]) + v
|
|
209
|
+
if self.vrefint_ns != NO_TIME:
|
|
210
|
+
v = struct.pack("<IQ", 1365, self.vrefint_ns) # 1100 mV against the 3300 mV full scale
|
|
211
|
+
out += bytes([CALIBRATION["vrefint"], len(v)]) + v
|
|
212
|
+
return out
|
|
213
|
+
|
|
214
|
+
# ---- the data ---------------------------------------------------------------------------------------------------
|
|
215
|
+
def _clear(self) -> None:
|
|
216
|
+
self.data, self.base, self.segs, self.serial_done = bytearray(), 0, [], 0
|
|
217
|
+
self.produced, self.sent, self.gap_next = 0, 0, False
|
|
218
|
+
|
|
219
|
+
def _pack(self, first: int, n: int) -> bytes:
|
|
220
|
+
if self.analog:
|
|
221
|
+
return b"".join(struct.pack(f"<{self.channels}H", *(analog_value(k, first + i) for k in range(self.channels)))
|
|
222
|
+
for i in range(n))
|
|
223
|
+
w = self.width # logic: the counter, w bits a sample
|
|
224
|
+
mask = (1 << w) - 1
|
|
225
|
+
if w >= 8:
|
|
226
|
+
return b"".join(((first + i) & mask).to_bytes(w // 8, "little") for i in range(n))
|
|
227
|
+
out = bytearray((n * w + 7) // 8)
|
|
228
|
+
for i in range(n):
|
|
229
|
+
bit = i * w
|
|
230
|
+
out[bit >> 3] |= ((first + i) & mask) << (bit & 7)
|
|
231
|
+
return bytes(out)
|
|
232
|
+
|
|
233
|
+
def value(self, k: int, i: int) -> int:
|
|
234
|
+
return analog_value(k, i) if self.analog else (i >> k) & 1
|
|
235
|
+
|
|
236
|
+
def trigger_at(self) -> int:
|
|
237
|
+
"""The first sample, at or after the pretrigger, where the trigger holds (within a few periods)."""
|
|
238
|
+
kind, role, value = self.trigger
|
|
239
|
+
span = (1 << (role + 1)) if not self.analog else 64 * (role // 2 + 1)
|
|
240
|
+
for i in range(max(self.pretrigger, 1), self.pretrigger + 3 * span + 2):
|
|
241
|
+
now, before = self.value(role, i), self.value(role, i - 1)
|
|
242
|
+
if kind == TRIGGER["level"] and now == (value & 1):
|
|
243
|
+
return i
|
|
244
|
+
if kind == TRIGGER["edge"] and now != before and (value == 2 or (value == 0) == (now == 1)):
|
|
245
|
+
return i
|
|
246
|
+
if kind == TRIGGER["cross_up"] and before < value <= now:
|
|
247
|
+
return i
|
|
248
|
+
if kind == TRIGGER["cross_down"] and before > value >= now:
|
|
249
|
+
return i
|
|
250
|
+
return self.pretrigger
|
|
251
|
+
|
|
252
|
+
def time_of(self, i: int) -> int:
|
|
253
|
+
"""The probe-clock time of the track's sample i."""
|
|
254
|
+
return self.t0_ns + int(i * 1_000_000_000 / self.rate)
|
|
255
|
+
|
|
256
|
+
def index_at(self, t_ns: int) -> int:
|
|
257
|
+
"""The track's sample nearest the probe-clock time t (NONE when outside the one-shot segment)."""
|
|
258
|
+
i = round((t_ns - self.t0_ns) * self.rate / 1_000_000_000)
|
|
259
|
+
return i if 0 <= i < self.samples else NONE
|
|
260
|
+
|
|
261
|
+
def _segment(self, n: int, trigger_index: int = NONE, flags: int = 0) -> Segment:
|
|
262
|
+
if self.gap_next:
|
|
263
|
+
flags, self.gap_next = flags | FLAG["gap"], False
|
|
264
|
+
seg = Segment(self.serial_done, self.base + len(self.data), n, self.time_of(self.produced), self.uncertainty_ns,
|
|
265
|
+
trigger_index, flags | (FLAG["slipped"] if self.slipped else 0))
|
|
266
|
+
self.data += self._pack(self.produced, n)
|
|
267
|
+
self.produced += n
|
|
268
|
+
self.segs.append(seg)
|
|
269
|
+
self.serial_done += 1
|
|
270
|
+
return seg
|
|
271
|
+
|
|
272
|
+
# ---- the operations ---------------------------------------------------------------------------------------------
|
|
273
|
+
def start(self, now_ms: int, group_ns: int | None = None, trigger_ns: int | None = None) -> list[bytes]:
|
|
274
|
+
"""-> the events it raises (kind(u8) payload). In a group: its start and, from the trigger track, the trigger's
|
|
275
|
+
time to mark on this track."""
|
|
276
|
+
if self.state == STATE["unconfigured"]:
|
|
277
|
+
raise Reject(m.UNAVAILABLE)
|
|
278
|
+
self._clear()
|
|
279
|
+
self.started_ms = now_ms
|
|
280
|
+
self.t0_ns = (group_ns if group_ns is not None else now_ms * 1_000_000) + self.start_offset_ns
|
|
281
|
+
self.vrefint_ns = self.t0_ns if self.analog else NO_TIME
|
|
282
|
+
events = []
|
|
283
|
+
if self.mode == MODE["one_shot"]:
|
|
284
|
+
if trigger_ns is not None:
|
|
285
|
+
index = self.index_at(trigger_ns)
|
|
286
|
+
elif self.trigger:
|
|
287
|
+
index = self.trigger_at()
|
|
288
|
+
events.append(bytes([EVENT["triggered"]]) + struct.pack("<IIQ", 0, index, self.time_of(index)))
|
|
289
|
+
else:
|
|
290
|
+
index = NONE
|
|
291
|
+
seg = self._segment(self.samples, index)
|
|
292
|
+
self.state = STATE["done"]
|
|
293
|
+
events += [bytes([EVENT["segment"]]) + seg.pack(), bytes([EVENT["stopped"], STOPPED["complete"]])]
|
|
294
|
+
else:
|
|
295
|
+
self.state = STATE["capturing"]
|
|
296
|
+
return events
|
|
297
|
+
|
|
298
|
+
def tick(self, now_ms: int) -> list[bytes]:
|
|
299
|
+
"""Time passes: repeat's segments, streaming's bytes. -> events."""
|
|
300
|
+
if self.state != STATE["capturing"]:
|
|
301
|
+
return []
|
|
302
|
+
due = int((now_ms - self.started_ms) * self.rate / 1000) # samples the clock has taken since start
|
|
303
|
+
events = []
|
|
304
|
+
if self.mode == MODE["repeat"]:
|
|
305
|
+
while due - self.produced >= self.samples:
|
|
306
|
+
if len(self.segs) >= self.segments_max: # no free segment: the capture stops
|
|
307
|
+
self.state = STATE["paused"]
|
|
308
|
+
events.append(bytes([EVENT["stopped"], STOPPED["no_free_segment"]]))
|
|
309
|
+
break
|
|
310
|
+
seg = self._segment(self.samples)
|
|
311
|
+
events.append(bytes([EVENT["segment"]]) + seg.pack())
|
|
312
|
+
elif self.mode == MODE["streaming"]:
|
|
313
|
+
per_byte = 8 // self.width if not self.analog and self.width < 8 else 1 # whole bytes only
|
|
314
|
+
n = min((due - self.produced) // per_byte * per_byte, MAX_SAMPLES)
|
|
315
|
+
if n > 0:
|
|
316
|
+
self.data += self._pack(self.produced, n)
|
|
317
|
+
self.produced += n
|
|
318
|
+
return events
|
|
319
|
+
|
|
320
|
+
def stop(self) -> list[bytes]:
|
|
321
|
+
if self.state not in (STATE["capturing"], STATE["waiting"], STATE["paused"]):
|
|
322
|
+
return []
|
|
323
|
+
self.state = STATE["done"]
|
|
324
|
+
return [bytes([EVENT["stopped"], STOPPED["host"]])]
|
|
325
|
+
|
|
326
|
+
def status(self) -> bytes:
|
|
327
|
+
return struct.pack("<BIQB", self.state, self.serial_done, self.base + len(self.data), 0)
|
|
328
|
+
|
|
329
|
+
def read(self, position: int, most: int, budget: int) -> bytes:
|
|
330
|
+
flags = 0
|
|
331
|
+
if position < self.base:
|
|
332
|
+
position, flags = self.base, flags | 0x02 # re-used: from what is kept (gap)
|
|
333
|
+
end = self.base + len(self.data)
|
|
334
|
+
take = max(0, min(most, self.max_read, budget - 9, end - position))
|
|
335
|
+
data = bytes(self.data[position - self.base:position - self.base + take])
|
|
336
|
+
if position + take < end:
|
|
337
|
+
flags |= 0x01 # more
|
|
338
|
+
return struct.pack("<QB", position, flags) + data
|
|
339
|
+
|
|
340
|
+
def segment_list(self, first: int, budget: int) -> bytes:
|
|
341
|
+
out = [s.pack() for s in self.segs if s.serial >= first]
|
|
342
|
+
out = out[:max(0, (budget - 1) // SEGMENT.size)][:255]
|
|
343
|
+
return bytes([len(out)]) + b"".join(out)
|
|
344
|
+
|
|
345
|
+
def release(self, serial: int, now_ms: int) -> None:
|
|
346
|
+
if self.mode != MODE["repeat"]:
|
|
347
|
+
return # one-shot: the next start clears; streaming: pushed
|
|
348
|
+
keep = [s for s in self.segs if s.serial > serial]
|
|
349
|
+
drop_to = keep[0].position if keep else self.base + len(self.data)
|
|
350
|
+
del self.data[:drop_to - self.base]
|
|
351
|
+
self.base = drop_to
|
|
352
|
+
self.segs = keep
|
|
353
|
+
if self.state == STATE["paused"]: # it goes on from now; the gap shows (flags bit0)
|
|
354
|
+
self.state, self.gap_next = STATE["capturing"], True
|
|
355
|
+
self.started_ms = now_ms - int(self.produced * 1000 / self.rate)
|
|
356
|
+
|
|
357
|
+
def pushes(self, fn: int, next_seq, budget: int) -> list[bytes]:
|
|
358
|
+
"""Streaming: the bytes not pushed yet, as data frames (position(u64) then data, common §1.5)."""
|
|
359
|
+
if self.mode != MODE["streaming"]:
|
|
360
|
+
return []
|
|
361
|
+
out = []
|
|
362
|
+
end = self.base + len(self.data)
|
|
363
|
+
while self.sent < end:
|
|
364
|
+
n = min(end - self.sent, budget - 13)
|
|
365
|
+
chunk = bytes(self.data[self.sent - self.base:self.sent - self.base + n])
|
|
366
|
+
out.append(bytes([m.ROLE_DATA]) + struct.pack("<HHQ", fn, next_seq(), self.sent) + chunk)
|
|
367
|
+
self.sent += n
|
|
368
|
+
del self.data[:self.sent - self.base] # pushed: the probe re-uses it
|
|
369
|
+
self.base = self.sent
|
|
370
|
+
return out
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@dataclass
|
|
374
|
+
class FakeGroup:
|
|
375
|
+
"""oep.fixture.capture-group (§4): tracks started together, one of them the trigger."""
|
|
376
|
+
tracks_allowed: list[int] # the fns it may bind (describe tracks)
|
|
377
|
+
max_tracks: int
|
|
378
|
+
budgets: list[tuple[int, list[int]]] # (max channel-samples / s, the fns sharing it)
|
|
379
|
+
tracks: list[int] = field(default_factory=list)
|
|
380
|
+
trigger_fn: int = 0
|
|
381
|
+
start_ns: int = NO_TIME
|
|
382
|
+
trigger_ns: int = NO_TIME
|
|
383
|
+
|
|
384
|
+
def bind(self, caps: dict[int, FakeCapture], fns: list[int], trigger_fn: int) -> None:
|
|
385
|
+
for fn in self.tracks:
|
|
386
|
+
caps[fn].group = None
|
|
387
|
+
self.tracks, self.trigger_fn, self.start_ns, self.trigger_ns = [], 0, NO_TIME, NO_TIME
|
|
388
|
+
if not fns:
|
|
389
|
+
return
|
|
390
|
+
chosen = [caps.get(fn) for fn in fns]
|
|
391
|
+
if (len(set(fns)) != len(fns) or len(fns) > self.max_tracks or any(fn not in self.tracks_allowed for fn in fns)
|
|
392
|
+
or any(c is None or c.state == STATE["unconfigured"] for c in chosen)
|
|
393
|
+
or len({c.mode for c in chosen}) != 1 or (trigger_fn and trigger_fn not in fns)
|
|
394
|
+
or any(c.trigger for fn, c in zip(fns, chosen) if fn != trigger_fn)):
|
|
395
|
+
raise Reject(m.UNAVAILABLE)
|
|
396
|
+
for most, shared in self.budgets:
|
|
397
|
+
if sum(caps[fn].channels * caps[fn].rate for fn in fns if fn in shared) > most:
|
|
398
|
+
raise Reject(m.UNAVAILABLE)
|
|
399
|
+
self.tracks, self.trigger_fn = list(fns), trigger_fn
|
|
400
|
+
for c in chosen:
|
|
401
|
+
c.group = self
|
|
402
|
+
|
|
403
|
+
def start(self, caps: dict[int, FakeCapture], now_ms: int) -> tuple[list[tuple[int, list[bytes]]], list[bytes]]:
|
|
404
|
+
"""-> (each track's events, the group's events)."""
|
|
405
|
+
if not self.tracks:
|
|
406
|
+
raise Reject(m.UNAVAILABLE)
|
|
407
|
+
self.start_ns, self.trigger_ns = now_ms * 1_000_000, NO_TIME
|
|
408
|
+
per, own = [], []
|
|
409
|
+
trigger_ns = None
|
|
410
|
+
src = caps.get(self.trigger_fn)
|
|
411
|
+
if src is not None and src.trigger and src.mode == MODE["one_shot"]:
|
|
412
|
+
per.append((self.trigger_fn, src.start(now_ms, self.start_ns)))
|
|
413
|
+
trigger_ns = self.trigger_ns = src.time_of(src.segs[0].trigger_index)
|
|
414
|
+
own.append(bytes([GRP.event["triggered"]]) + struct.pack("<HQ", self.trigger_fn, trigger_ns))
|
|
415
|
+
for fn in self.tracks:
|
|
416
|
+
if fn != self.trigger_fn or trigger_ns is None:
|
|
417
|
+
per.append((fn, caps[fn].start(now_ms, self.start_ns, trigger_ns)))
|
|
418
|
+
if all(caps[fn].state == STATE["done"] for fn in self.tracks):
|
|
419
|
+
own.append(bytes([GRP.event["stopped"], STOPPED["complete"]]))
|
|
420
|
+
return per, own
|
|
421
|
+
|
|
422
|
+
def stop(self, caps: dict[int, FakeCapture]) -> tuple[list[tuple[int, list[bytes]]], list[bytes]]:
|
|
423
|
+
per = [(fn, caps[fn].stop()) for fn in self.tracks]
|
|
424
|
+
return per, [bytes([GRP.event["stopped"], STOPPED["host"]])] if any(e for _, e in per) else []
|
|
425
|
+
|
|
426
|
+
def status(self, caps: dict[int, FakeCapture]) -> bytes:
|
|
427
|
+
states = [caps[fn].state for fn in self.tracks]
|
|
428
|
+
if not states:
|
|
429
|
+
state = STATE["unconfigured"]
|
|
430
|
+
elif all(s == STATE["done"] for s in states):
|
|
431
|
+
state = STATE["done"]
|
|
432
|
+
elif any(s in (STATE["capturing"], STATE["paused"]) for s in states):
|
|
433
|
+
state = STATE["capturing"]
|
|
434
|
+
else:
|
|
435
|
+
state = STATE["configured"]
|
|
436
|
+
return struct.pack("<BQQH", state, self.start_ns, self.trigger_ns,
|
|
437
|
+
self.trigger_fn if self.trigger_ns != NO_TIME else 0)
|
oep_client/fake_serial.py
CHANGED
|
@@ -91,6 +91,8 @@ class FakeSerialPort:
|
|
|
91
91
|
def tick(self) -> None:
|
|
92
92
|
self._gap(self.ep.now())
|
|
93
93
|
self.ep.tick()
|
|
94
|
+
for f in self.ep.pushes(): # events and data the probe sends by itself (core §11)
|
|
95
|
+
self.frames.append(cobs.frame(f))
|
|
94
96
|
|
|
95
97
|
def output(self, room: int = 4096) -> bytes:
|
|
96
98
|
"""What the probe sends now: waiting answers first, then raw bytes by the bind (up to `room` in all)."""
|
oep_client/fake_serve.py
CHANGED
|
@@ -12,7 +12,7 @@ The first line on stdout says where to open: `PTY /dev/pts/N` or `PORT n`. The p
|
|
|
12
12
|
(so a test's child never stays behind), or with --once when the first TCP connection closes.
|
|
13
13
|
|
|
14
14
|
Options:
|
|
15
|
-
--profile NAME p4-x035 (default), esp32-v003
|
|
15
|
+
--profile NAME p4-x035 (default), esp32-v003, p4-bench or rp2350-pins (p4_x035 style names work too)
|
|
16
16
|
--port-index N which serial port of the profile the pty / cobs TCP is (default: the first one)
|
|
17
17
|
--noise TEXT raw bytes written in front of every answer (the host must skip them)
|
|
18
18
|
--drop N the N-th answer (1-based) is not sent, once (the request did run: a resend gets the
|
|
@@ -25,6 +25,7 @@ Options:
|
|
|
25
25
|
--bind MODE bind the serial port to every --slot: last-reset, manual or mixed
|
|
26
26
|
--target-id HEX the target_id every target's attach reports (wch_dmi_7f)
|
|
27
27
|
--absent N the N-th pin pair of the first wire has no target (repeatable)
|
|
28
|
+
--capture-slipped every oep.fixture.capture segment says flags bit2 (slipped: a pace that fell behind)
|
|
28
29
|
--uart-plan the first oep.fixture.uart gets its RX / TX plan at boot, as if saved (the jig's "DUT TX" /
|
|
29
30
|
"DUT RX" labels when the profile has them, else the first free channels); configure then works
|
|
30
31
|
--uart-rx TEXT what arrives on that UART's RX every --every ms once it is configured (%d = a counter)
|
|
@@ -70,6 +71,7 @@ def build(a: argparse.Namespace) -> endpoint.Endpoint:
|
|
|
70
71
|
hook = _load_hook(a.run_hook)
|
|
71
72
|
for tg in ep.targets.values():
|
|
72
73
|
tg.run_hook = (lambda t: lambda pc, regs: hook(t, pc, regs))(tg)
|
|
74
|
+
ep.capture_slipped = getattr(a, "capture_slipped", False)
|
|
73
75
|
for n in a.absent:
|
|
74
76
|
ep.targets[(wire, ep.pairs[wire][n])].present = False
|
|
75
77
|
items = []
|
|
@@ -294,6 +296,10 @@ def _serve_conn(a, ep, console, conn, watch_stdin) -> bool:
|
|
|
294
296
|
conn.setblocking(False)
|
|
295
297
|
else:
|
|
296
298
|
ep.tick()
|
|
299
|
+
for f in ep.pushes(): # events and data pushes (core §11)
|
|
300
|
+
conn.setblocking(True)
|
|
301
|
+
conn.sendall(struct.pack("<H", len(f)) + f)
|
|
302
|
+
conn.setblocking(False)
|
|
297
303
|
|
|
298
304
|
|
|
299
305
|
def main(argv: list[str] | None = None) -> None:
|
|
@@ -317,6 +323,7 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
317
323
|
ap.add_argument("--uart-rx")
|
|
318
324
|
ap.add_argument("--run-hook")
|
|
319
325
|
ap.add_argument("--once", action="store_true")
|
|
326
|
+
ap.add_argument("--capture-slipped", action="store_true")
|
|
320
327
|
a = ap.parse_args(argv)
|
|
321
328
|
if a.tcp is None and a.framing == "length":
|
|
322
329
|
ap.error("--framing length is for --tcp")
|
oep_client/registry.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from types import SimpleNamespace as _NS
|
|
4
4
|
|
|
5
|
-
REGISTRY_HASH = "
|
|
5
|
+
REGISTRY_HASH = "27f1876a8dfdabb7"
|
|
6
6
|
SCHEMA = 1
|
|
7
7
|
PROTOCOL_REVISION = 1
|
|
8
8
|
CONFIRM_REQUEST_MAGIC = 'OEP?'
|
|
@@ -22,7 +22,7 @@ TIMING = {"resync_quiet_ms": 0x32, "probe_frame_gap_ms": 0xC8, "host_frame_pause
|
|
|
22
22
|
|
|
23
23
|
INTERFACES = {}
|
|
24
24
|
CORE = _NS(name="oep.core", revision=1, op={"confirm": 0x01, "list": 0x02, "describe": 0x03, "plan_apply": 0x04, "plan_release": 0x05, "open": 0x10, "end": 0x11, "keepalive": 0x12, "lock_state": 0x13, "subscribe": 0x30, "unsubscribe": 0x32, "link_source": 0x40, "link_sink": 0x41}, lock_free={0x01, 0x02, 0x03, 0x10, 0x13, 0x40, 0x41},
|
|
25
|
-
closed_tail={0x40}, tlv={"plan_apply": {"role_assignment": 0x90}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "reserved": 0x44, "profile": 0x45, "label": 0x46, "resets_on_open": 0x47, "transport": 0x49, "oep_pid": 0x4A, "plan_roles": 0x4B}}, event={"heartbeat": 0x01}, enum={"transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}}, own={})
|
|
25
|
+
closed_tail={0x40}, tlv={"plan_apply": {"role_assignment": 0x90}, "open": {"owner": 0x01}, "lock_state_answer": {"owner": 0x01}, "locked_payload": {"owner": 0x01}, "describe": {"firmware": 0x40, "model": 0x41, "unit_id": 0x42, "channels": 0x43, "reserved": 0x44, "profile": 0x45, "label": 0x46, "resets_on_open": 0x47, "transport": 0x49, "oep_pid": 0x4A, "plan_roles": 0x4B, "chip": 0x4C}}, event={"heartbeat": 0x01}, enum={"transport_kind": {"uart_bridge": 0x01, "usb_cdc": 0x02, "usb_serial_jtag": 0x03, "vendor_bulk": 0x04, "hid": 0x05, "tcp": 0x06}}, own={})
|
|
26
26
|
INTERFACES["oep.core"] = CORE
|
|
27
27
|
WIRE_RVSWD = _NS(name="oep.wire.rvswd", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "attach_under_reset": 0x04, "connections": 0x05}, lock_free={0x05},
|
|
28
28
|
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "scan": {"skip": 0x01}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03, "idle_clock": 0x04}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03, "idle_clock": 0x04}, "attach_answer": {"target_id": 0x10}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "target_id_scheme": {"wch_dmi_7f": 0x01}, "attach_method": {"run": 0x00, "halt": 0x01}, "pin_role": {"swdio": 0x01, "swclk": 0x02, "reset": 0x03}, "idle_clock": {"high": 0x00, "low": 0x01}}, own={})
|
|
@@ -49,11 +49,14 @@ FIXTURE_UART = _NS(name="oep.fixture.uart", revision=1, op={"configure": 0x01, "
|
|
|
49
49
|
closed_tail={0x02}, tlv={"configure": {"format": 0x01}, "describe": {"formats": 0x40}}, event={}, enum={"role": {"rx": 0x01, "tx": 0x02}}, own={})
|
|
50
50
|
INTERFACES["oep.fixture.uart"] = FIXTURE_UART
|
|
51
51
|
FIXTURE_CAPTURE = _NS(name="oep.fixture.capture", revision=1, op={"configure": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05, "read": 0x06, "segments": 0x07, "release": 0x08, "query": 0x09}, lock_free={0x05, 0x06, 0x07, 0x09},
|
|
52
|
-
closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "skew": 0x57}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
|
|
52
|
+
closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "rate_accuracy": 0x5A, "skew": 0x57}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
|
|
53
53
|
INTERFACES["oep.fixture.capture"] = FIXTURE_CAPTURE
|
|
54
|
-
FIXTURE_ANALOG = _NS(name="oep.fixture.analog", revision=1, op={"configure": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05, "read": 0x06, "segments": 0x07, "release": 0x08, "query": 0x09}, lock_free={0x05, 0x06, 0x07, 0x09},
|
|
55
|
-
closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "skew": 0x57}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
|
|
54
|
+
FIXTURE_ANALOG = _NS(name="oep.fixture.analog", revision=1, op={"configure": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05, "read": 0x06, "segments": 0x07, "release": 0x08, "query": 0x09, "calibration": 0x0A}, lock_free={0x05, 0x06, 0x07, 0x09, 0x0A},
|
|
55
|
+
closed_tail={0x06}, tlv={"configure": {"mode": 0x40, "rate": 0x42, "samples": 0x43, "segments": 0x44, "trigger": 0x45, "pretrigger": 0x46, "frontend": 0x47}, "configure_answer": {"actual_rate": 0x50, "layout": 0x51, "actual_samples": 0x52, "actual_segments": 0x53, "timing": 0x54, "scale": 0x55, "blocking_ms": 0x56, "skew": 0x57, "rate_accuracy": 0x5A, "frontend_used": 0x58, "reference": 0x59}, "calibration_answer": {"factory": 0x01, "vrefint": 0x02}, "describe": {"mode": 0x40, "rate_range": 0x41, "rate_list": 0x42, "rate_limit": 0x43, "channels": 0x44, "trigger": 0x45, "frontend": 0x46, "max_read": 0x47, "segment_ring": 0x48, "frontend_shared": 0x49}}, event={"segment": 0x01, "stopped": 0x02, "triggered": 0x03}, enum={"mode": {"one_shot": 0x01, "repeat": 0x02, "streaming": 0x03}, "trigger": {"immediate": 0x00, "level": 0x01, "edge": 0x02, "cross_up": 0x03, "cross_down": 0x04}, "reference_source": {"supply": 0x00, "internal": 0x01, "external": 0x02}, "state": {"unconfigured": 0x00, "configured": 0x01, "waiting": 0x02, "capturing": 0x03, "done": 0x04, "paused": 0x05, "error": 0x06}, "stopped_reason": {"complete": 0x00, "host": 0x01, "no_free_segment": 0x02, "error": 0x03}, "segment_flag": {"gap": 0x01, "short": 0x02, "slipped": 0x04}}, own={})
|
|
56
56
|
INTERFACES["oep.fixture.analog"] = FIXTURE_ANALOG
|
|
57
|
+
FIXTURE_CAPTURE_GROUP = _NS(name="oep.fixture.capture-group", revision=1, op={"bind": 0x01, "start": 0x02, "stop": 0x03, "force": 0x04, "status": 0x05}, lock_free={0x05},
|
|
58
|
+
closed_tail={}, tlv={"bind": {"trigger_track": 0x01}, "describe": {"tracks": 0x40, "max_tracks": 0x41, "budget": 0x42, "start_skew": 0x43}}, event={"triggered": 0x01, "stopped": 0x02}, enum={}, own={})
|
|
59
|
+
INTERFACES["oep.fixture.capture-group"] = FIXTURE_CAPTURE_GROUP
|
|
57
60
|
PROBE_CONFIG = _NS(name="oep.probe.config", revision=1, op={"get": 0x01, "set": 0x02, "save": 0x03, "erase": 0x04}, lock_free={0x01},
|
|
58
61
|
closed_tail={}, tlv={"item": {"plan": 0x01, "label": 0x02, "idle": 0x03, "slot": 0x04, "bind": 0x05}, "describe": {"storage": 0x40, "items": 0x41, "slots_max": 0x42, "bind_modes": 0x43, "slot_state": 0x44, "bind_state": 0x45}}, event={}, enum={"idle_mode": {"hi_z": 0x00, "pull_up": 0x01, "pull_down": 0x02}, "slot_attach": {"host": 0x00, "at_boot": 0x01}, "slot_state": {"connected": 0x00, "absent": 0x01, "lock_mismatch": 0x02, "no_target_id": 0x03}, "bind_mode": {"last_reset": 0x00, "manual": 0x01, "mixed": 0x02}, "bind_stream": {"slot_console": 0x01, "fixture_uart": 0x02}, "bind_flow": {"idle": 0x00, "streaming": 0x01, "held": 0x02}, "storage_state": {"none": 0x00, "applied": 0x01, "unreadable": 0x02}}, own={})
|
|
59
62
|
INTERFACES["oep.probe.config"] = PROBE_CONFIG
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
|
|
5
5
|
Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
|
|
6
6
|
Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
|
|
@@ -107,8 +107,19 @@ A run on hardware: ArduinoCore-CH32's `tests/manual/oep_smoke/` (`oep_smoke.py`,
|
|
|
107
107
|
|
|
108
108
|
`endpoint.Endpoint` is a fake probe that answers as oep-spec says; ch32rv, this client and the probe firmware are checked
|
|
109
109
|
against it (when the spec changes, this is brought in line before the firmware). `fake` holds example declarations (profiles
|
|
110
|
-
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats
|
|
111
|
-
port (COBS candidates, raw bytes and binds, held during a session and
|
|
110
|
+
`p4-x035`, `esp32-v003`, `p4-bench` = a made-up jig with three slots and two seats, `rp2350-pins` = a wire whose pins the host
|
|
111
|
+
chooses), `fake_serial` the byte side of a serial port (COBS candidates, raw bytes and binds, held during a session and
|
|
112
|
+
resumed after it).
|
|
113
|
+
|
|
114
|
+
`fake_capture` is `oep.fixture.capture` (logic): one-shot, repeat (segments with the clock at the actual rate, a ring,
|
|
115
|
+
release) and streaming (data pushes while subscribed), level / edge triggers with a pretrigger, and events. What it
|
|
116
|
+
captures is known: sample i is the counter i, channel k its bit k (a square wave of period 2^(k+1) samples), in the layout
|
|
117
|
+
the profile allows (`p4-x035`: w 1-16 as the P4's PARLIO, three channels in w 4; `esp32-v003`: w 8 as the classic ESP32's
|
|
118
|
+
sampler, one-shot only). A capture only listens, so it may be planned on pins other interfaces hold. `p4-x035` also has
|
|
119
|
+
`oep.fixture.analog` (4 channels of the P4's ADC1 on GPIO16-23: an even channel k a square wave, an odd one a sine, of
|
|
120
|
+
period 64 (k // 2 + 1) samples, 12-bit values in 16-bit slots; ESP32-style frontends; a made-up two-point calibration and a
|
|
121
|
+
Vrefint) and `oep.fixture.capture-group` binding the logic and the analog: started together, the analog 5 us later
|
|
122
|
+
(+-2 us), the trigger of one marked on both. Times are ns on the probe's one clock.
|
|
112
123
|
|
|
113
124
|
Other programs' tests run `fake_serve` as a child process:
|
|
114
125
|
|
|
@@ -122,4 +133,5 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
|
|
|
122
133
|
`--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
|
|
123
134
|
so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
|
|
124
135
|
broken once). `--uart-plan` / `--uart-rx` give the first fixture UART a plan and RX bytes, `--run-hook` a host's own model of
|
|
125
|
-
riscv-dm run.
|
|
136
|
+
riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
|
|
137
|
+
(both framings). The rest: `--help`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
oep_client/__init__.py,sha256=
|
|
1
|
+
oep_client/__init__.py,sha256=W4Jmi9TS_xd7VSEtxOeaXMp33zr8gXCFqH9a0VwBDy0,164
|
|
2
2
|
oep_client/__main__.py,sha256=qtVGdpAHULBtLY2SedabJf-uwDZZhMf_YmrbqRzM57U,14669
|
|
3
3
|
oep_client/arm.py,sha256=JLyUhLSPcmsk73schZcaJYoOKuHzrvnU8kPmfrEvVuo,12331
|
|
4
|
-
oep_client/capture.py,sha256=
|
|
4
|
+
oep_client/capture.py,sha256=mL_C2_5hK9D9nR_F4f4YNN-uaRVegMN-8vnwHR7rQkg,24962
|
|
5
5
|
oep_client/catalog.py,sha256=EqAQnSgHFxGbCO2GQ_Ee-uheJKbBBUUpL1-0qHfNMXU,6574
|
|
6
6
|
oep_client/ch32_flash.py,sha256=Fc-6jEj15cQtMSKmfC5dmDetMXJ8hAdAuJ9d9odwrsA,12651
|
|
7
7
|
oep_client/cobs.py,sha256=hqddQI_X7rsBvBvcP_MBE3bXPyB13-Azjqp_9JfdQ1g,2288
|
|
@@ -10,11 +10,12 @@ oep_client/console.py,sha256=YYcuc9Qxe3kihla8wynYbDaLVr7wBLEvswrM-cMK3Tk,6859
|
|
|
10
10
|
oep_client/core.py,sha256=Cyyaknjipkg6tS76dZe5GMqjmOoQjlfeP0WFtMHkJNs,10403
|
|
11
11
|
oep_client/decode.py,sha256=M-bRARrA2qGZLoJn0BdXeTb4WjL_2Hb2nk_g1IGXIw4,2382
|
|
12
12
|
oep_client/dump.py,sha256=GAnmu2abuDmGmbWcbbEfC3EUuPp5Hady9vQVNcQ-724,6743
|
|
13
|
-
oep_client/endpoint.py,sha256=
|
|
13
|
+
oep_client/endpoint.py,sha256=aa9znMaedjEnMDNcGsV-4gxTZJSd6IxNyPpcJM2PEzk,87244
|
|
14
14
|
oep_client/esp32_targets.py,sha256=b8tNH-nb2KC-XgNfWg5l3TO0cyhZOF-gJajaVxNVSGQ,4988
|
|
15
|
-
oep_client/fake.py,sha256=
|
|
16
|
-
oep_client/
|
|
17
|
-
oep_client/
|
|
15
|
+
oep_client/fake.py,sha256=jTaiii8DtByI_CyUYVRoUnGT05k5OdIerSdQig4HKrk,18096
|
|
16
|
+
oep_client/fake_capture.py,sha256=6BadaybnZ0jRDfxjnkLXttw65rkoPgsIY5FvAWKxMg8,22812
|
|
17
|
+
oep_client/fake_serial.py,sha256=nXpbBuPl9sAh5BdoWSsa5EDYvPC7VLOvaBYrslGlniA,4659
|
|
18
|
+
oep_client/fake_serve.py,sha256=CzJ_TO_gWVNPP0r5ggQ57rgwAgBapunGI0YtyW8MrRM,15133
|
|
18
19
|
oep_client/fixture.py,sha256=653hCD-Iz0Lfs8dT4Hdg3QyubbbOGs1_xjq_3nb8foU,5564
|
|
19
20
|
oep_client/frames.py,sha256=xjgjnXmXUqNJZ5m0h91r4Cy48WoVK1mEMW4gRg-mlMI,3991
|
|
20
21
|
oep_client/hid_stream.py,sha256=99Dsm4Io1keukuETG_UNqjBGiVYZZeb__Pv4ytaKg9k,10695
|
|
@@ -23,14 +24,14 @@ oep_client/interfaces.py,sha256=TNvof5blyLjO1Mc0OentKcMVBD_5e4riRG18UgEh2ZE,5380
|
|
|
23
24
|
oep_client/link.py,sha256=J9VtBWfTgNoIgUQuj8SsH9hHesQM2uEN545IoxKVS8E,19406
|
|
24
25
|
oep_client/message.py,sha256=MEM1UT6KFM7AsT56jvBLt_FJK_6SJ7O_nGAiipW0_zo,9912
|
|
25
26
|
oep_client/names.py,sha256=ztCZFWs__aR-a5G14TnJP-Q4YfLhbyrzvOVOMJuG93k,3653
|
|
26
|
-
oep_client/registry.py,sha256=
|
|
27
|
+
oep_client/registry.py,sha256=g6TfTWVPer9M46we6qSjMZqnqDbL6A49rdxM0v67Hx0,10758
|
|
27
28
|
oep_client/riscv.py,sha256=H9kE_oUsnaBzqdTXDAVgDQkDvsrn5ZwsVi8dF1W_BDM,23117
|
|
28
29
|
oep_client/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
|
|
29
30
|
oep_client/target.py,sha256=R8BvYfFfYNhZF0MysJkNb8b69YgBlmeGXNX8ReZIe5k,1068
|
|
30
31
|
oep_client/uiapduino.py,sha256=IEPG3LbZPksi8g3wmAf6T0CRWSfYKW1_qrH_GBVQdxY,6090
|
|
31
32
|
oep_client/usb_stream.py,sha256=VJMCxsOlQCk005FcS92pqTh33fRSFBizoL__L0VbUMA,10242
|
|
32
|
-
oep_client_python-0.0.
|
|
33
|
-
oep_client_python-0.0.
|
|
34
|
-
oep_client_python-0.0.
|
|
35
|
-
oep_client_python-0.0.
|
|
36
|
-
oep_client_python-0.0.
|
|
33
|
+
oep_client_python-0.0.10.dist-info/METADATA,sha256=K29Cy_nfBC_19RrkZ_hZQtD9nzLoW1oTWw9rinSp8Hc,8590
|
|
34
|
+
oep_client_python-0.0.10.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
35
|
+
oep_client_python-0.0.10.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
|
|
36
|
+
oep_client_python-0.0.10.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
|
|
37
|
+
oep_client_python-0.0.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|