oep-client-python 0.0.7__py3-none-any.whl → 0.0.9__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/endpoint.py +176 -19
- oep_client/fake.py +45 -5
- oep_client/fake_capture.py +267 -0
- oep_client/fake_serial.py +2 -0
- oep_client/fake_serve.py +8 -1
- oep_client/registry.py +4 -4
- oep_client/riscv.py +20 -7
- {oep_client_python-0.0.7.dist-info → oep_client_python-0.0.9.dist-info}/METADATA +12 -4
- {oep_client_python-0.0.7.dist-info → oep_client_python-0.0.9.dist-info}/RECORD +13 -12
- {oep_client_python-0.0.7.dist-info → oep_client_python-0.0.9.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.7.dist-info → oep_client_python-0.0.9.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.7.dist-info → oep_client_python-0.0.9.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
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"))
|
|
@@ -254,16 +254,29 @@ class Endpoint:
|
|
|
254
254
|
self.pairs: dict[int, list[tuple[int, int]]] = {} # wire fn -> allowed (swdio, swclk), declared order
|
|
255
255
|
self.max_connections: dict[int, int] = {}
|
|
256
256
|
self.reset_channels: dict[int, set[int]] = {} # wire fn -> channels attach_under_reset may take (role 3)
|
|
257
|
+
self.pin_roles: dict[int, dict[int, set[int]]] = {} # wire fn -> role -> candidates (role_channels wires)
|
|
257
258
|
for fn, name in self.names.items():
|
|
258
259
|
if name in WIRES:
|
|
259
260
|
self.pairs[fn] = [self._group_pair(name, t) for t in self.static[fn] if t[0] == catalog.CHANNEL_GROUP]
|
|
261
|
+
roles: dict[int, set[int]] = {}
|
|
262
|
+
for t in self.static[fn]:
|
|
263
|
+
if t[0] == catalog.ROLE_CHANNELS and t[2] in (1, 2):
|
|
264
|
+
roles.setdefault(t[2], set()).update(
|
|
265
|
+
catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]]))
|
|
266
|
+
if roles:
|
|
267
|
+
self.pin_roles[fn] = roles
|
|
260
268
|
self.reset_channels[fn] = {
|
|
261
269
|
c for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS and t[2] == PIN_ROLE_RESET
|
|
262
270
|
for c in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
|
|
263
271
|
self.max_connections[fn] = next((t[2] for t in self.static[fn] if t[0] == fake.MAX_CONNECTIONS), 1)
|
|
264
272
|
self.targets: dict[tuple[int, tuple[int, int]], FakeTarget] = {
|
|
265
273
|
(fn, p): FakeTarget() for fn in sorted(self.pairs) for p in self.pairs[fn]}
|
|
274
|
+
for fn in sorted(self.pin_roles): # any pair: one target, on the first pair
|
|
275
|
+
first = self._role_pairs(fn)[0]
|
|
276
|
+
self.targets[(fn, first)] = FakeTarget()
|
|
266
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() if name == "oep.fixture.capture"}
|
|
267
280
|
self.mechanisms = set()
|
|
268
281
|
for fn, name in self.names.items():
|
|
269
282
|
if name == "oep.target.console":
|
|
@@ -291,6 +304,9 @@ class Endpoint:
|
|
|
291
304
|
self.dropped = 0 # requests a v0 endpoint dropped (role 0x81)
|
|
292
305
|
self.requests: list[m.Request] = []
|
|
293
306
|
self.subscribed: set[int] = set()
|
|
307
|
+
self.push_seq: dict[int, int] = {} # fn -> the next event / data seq (core §11.2)
|
|
308
|
+
self.outbox: list[bytes] = [] # events and data frames waiting to go out (pushes())
|
|
309
|
+
self.capture_slipped = False # every capture segment says flags bit2 (a pace that fell behind)
|
|
294
310
|
self.plan: set[tuple[int, int, int]] = set() # (fn, role, channel), from plan_apply and the config
|
|
295
311
|
self.plan_from_config: set[int] = set() # fns whose plan came from the config (not a session's)
|
|
296
312
|
self.resend: OrderedDict[int, tuple[int, int, int, bytes | None]] = OrderedDict()
|
|
@@ -329,6 +345,123 @@ class Endpoint:
|
|
|
329
345
|
def target_id(self, value: int | None) -> None:
|
|
330
346
|
self.target.target_id = value
|
|
331
347
|
|
|
348
|
+
@staticmethod
|
|
349
|
+
def _capture_from(tlvs: list[bytes]) -> fake_capture.FakeCapture:
|
|
350
|
+
"""A capture as its describe declares it (modes, the w allowed, the rate range, max_read, the ring)."""
|
|
351
|
+
d = reg.FIXTURE_CAPTURE.tlv["describe"]
|
|
352
|
+
modes, widths, lo, hi, ring, most = set(), {8}, 1, 1_000_000, 1, 1024
|
|
353
|
+
for t in tlvs:
|
|
354
|
+
tag, v = t[0], t[2:2 + t[1]]
|
|
355
|
+
if tag == d["mode"]:
|
|
356
|
+
modes.add(v[0])
|
|
357
|
+
elif tag == d["channels"]:
|
|
358
|
+
widths = {1 << i for i in range(6) if v[1] >> i & 1}
|
|
359
|
+
elif tag == catalog.MIN_CLOCK_HZ:
|
|
360
|
+
lo = struct.unpack("<I", v)[0]
|
|
361
|
+
elif tag == catalog.MAX_CLOCK_HZ:
|
|
362
|
+
hi = struct.unpack("<I", v)[0]
|
|
363
|
+
elif tag == d["segment_ring"]:
|
|
364
|
+
ring = struct.unpack("<H", v)[0]
|
|
365
|
+
elif tag == d["max_read"]:
|
|
366
|
+
most = struct.unpack("<I", v)[0]
|
|
367
|
+
return fake_capture.FakeCapture(modes or {fake_capture.MODE["one_shot"]}, widths, lo, hi, ring, most)
|
|
368
|
+
|
|
369
|
+
def _next_seq(self, fn: int) -> int:
|
|
370
|
+
seq = self.push_seq.get(fn, 0)
|
|
371
|
+
self.push_seq[fn] = (seq + 1) & 0xFFFF
|
|
372
|
+
return seq
|
|
373
|
+
|
|
374
|
+
def _events(self, fn: int, events: list[bytes]) -> None:
|
|
375
|
+
"""Events (kind(u8) payload) of `fn` to go out while it is subscribed; unsubscribed, they are not sent."""
|
|
376
|
+
for e in events:
|
|
377
|
+
if fn in self.subscribed:
|
|
378
|
+
self.outbox.append(bytes([m.ROLE_EVENT]) + struct.pack("<HH", fn, self._next_seq(fn)) + e)
|
|
379
|
+
|
|
380
|
+
def pushes(self) -> list[bytes]:
|
|
381
|
+
"""The frames the probe sends by itself now (core §11): events, and a streaming capture's data. A serving loop
|
|
382
|
+
frames and sends them; a test takes them from here."""
|
|
383
|
+
self.tick()
|
|
384
|
+
for fn, cap in self.captures.items():
|
|
385
|
+
if fn in self.subscribed:
|
|
386
|
+
self.outbox += cap.pushes(fn, lambda fn=fn: self._next_seq(fn), self.probe.max_frame)
|
|
387
|
+
out, self.outbox = self.outbox, []
|
|
388
|
+
return out
|
|
389
|
+
|
|
390
|
+
def _capture(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
391
|
+
cap, O = self.captures[fn], fake_capture.OP
|
|
392
|
+
self._events(fn, cap.tick(self.now())) # what the clock captured up to this request
|
|
393
|
+
roles = sorted(a[1] for a in self.plan if a[0] == fn)
|
|
394
|
+
budget = self.probe.max_frame - m.RESULT_HEADER
|
|
395
|
+
try:
|
|
396
|
+
if op in (O["configure"], O["query"]):
|
|
397
|
+
got, ignored = t.tail(set(fake_capture.TLV.values()) - {fake_capture.TLV["frontend"]})
|
|
398
|
+
settled = cap.settle(got, t.critical, len(roles))
|
|
399
|
+
if op == O["configure"]:
|
|
400
|
+
cap.apply(settled)
|
|
401
|
+
cap.slipped = self.capture_slipped
|
|
402
|
+
return self._answer(cap.answer(settled), ignored)
|
|
403
|
+
if op == O["start"]:
|
|
404
|
+
t.tail()
|
|
405
|
+
self._events(fn, cap.start(self.now()))
|
|
406
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<I", 0)
|
|
407
|
+
if op == O["stop"]:
|
|
408
|
+
t.tail()
|
|
409
|
+
self._events(fn, cap.stop())
|
|
410
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
411
|
+
if op == O["force"]:
|
|
412
|
+
t.tail()
|
|
413
|
+
return m.COMPLETED, m.SUCCESS, b"" # nothing waits: a trigger is found at start
|
|
414
|
+
if op == O["status"]:
|
|
415
|
+
t.tail()
|
|
416
|
+
return m.COMPLETED, m.SUCCESS, cap.status()
|
|
417
|
+
if op == O["read"]:
|
|
418
|
+
position, most = t.take("QI")
|
|
419
|
+
return m.COMPLETED, m.SUCCESS, cap.read(position, most, budget)
|
|
420
|
+
if op == O["segments"]:
|
|
421
|
+
first = t.take("I")
|
|
422
|
+
t.tail()
|
|
423
|
+
return m.COMPLETED, m.SUCCESS, cap.segment_list(first, budget)
|
|
424
|
+
if op == O["release"]:
|
|
425
|
+
serial = t.take("I")
|
|
426
|
+
t.tail()
|
|
427
|
+
cap.release(serial, self.now())
|
|
428
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
429
|
+
except fake_capture.Reject as e:
|
|
430
|
+
raise Reject(e.reason, e.payload)
|
|
431
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
432
|
+
|
|
433
|
+
def _role_pairs(self, fn: int) -> list[tuple[int, int]]:
|
|
434
|
+
"""A role_channels wire's pairs in the count = 0 order (oep-if-debug §1): swdio ascending, then swclk."""
|
|
435
|
+
roles = self.pin_roles[fn]
|
|
436
|
+
if self.names[fn] == "oep.wire.swio":
|
|
437
|
+
return [(d, 0xFFFF) for d in sorted(roles.get(1, ()))]
|
|
438
|
+
return [(d, c) for d in sorted(roles.get(1, ())) for c in sorted(roles.get(2, ())) if d != c]
|
|
439
|
+
|
|
440
|
+
def _allowed_pairs(self, fn: int) -> list[tuple[int, int]]:
|
|
441
|
+
return self._role_pairs(fn) if fn in self.pin_roles else self.pairs.get(fn, [])
|
|
442
|
+
|
|
443
|
+
def _allows(self, fn: int, pair: tuple[int, int]) -> bool:
|
|
444
|
+
if fn in self.pin_roles:
|
|
445
|
+
roles = self.pin_roles[fn]
|
|
446
|
+
swio = self.names[fn] == "oep.wire.swio"
|
|
447
|
+
return pair[0] in roles.get(1, ()) and (pair[1] == 0xFFFF if swio else
|
|
448
|
+
pair[1] in roles.get(2, ()) and pair[1] != pair[0])
|
|
449
|
+
return pair in self.pairs.get(fn, [])
|
|
450
|
+
|
|
451
|
+
def _held(self, fn: int | None = None, pair: tuple[int, int] | None = None) -> set[int]:
|
|
452
|
+
"""Channels something holds (core §8.1): the plan, the slots' pairs, the live connections' pairs - except
|
|
453
|
+
wire `fn`'s own connection on `pair` (attaching there again, or scanning through it)."""
|
|
454
|
+
held = {a[2] for a in self.plan if a[0] not in self.captures} # a capture only listens
|
|
455
|
+
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)}
|
|
456
|
+
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)}
|
|
457
|
+
return held
|
|
458
|
+
|
|
459
|
+
def _target(self, fn: int, pair: tuple[int, int]) -> FakeTarget:
|
|
460
|
+
tg = self.targets.get((fn, pair))
|
|
461
|
+
if tg is None: # a pair nothing is wired to
|
|
462
|
+
tg = self.targets[(fn, pair)] = FakeTarget(present=False)
|
|
463
|
+
return tg
|
|
464
|
+
|
|
332
465
|
@staticmethod
|
|
333
466
|
def _group_pair(name: str, t: bytes) -> tuple[int, int]:
|
|
334
467
|
roles = {t[3 + 3 * i]: struct.unpack_from("<H", t, 4 + 3 * i)[0] for i in range((t[1] - 1) // 3)}
|
|
@@ -570,6 +703,7 @@ class Endpoint:
|
|
|
570
703
|
if fn != 0 and fn not in self.names:
|
|
571
704
|
return m.REJECTED, m.UNAVAILABLE, b""
|
|
572
705
|
self.subscribed.add(fn)
|
|
706
|
+
self.push_seq[fn] = 0 # seq from 0 at every subscribe (core §11.2)
|
|
573
707
|
return m.COMPLETED, m.SUCCESS, b""
|
|
574
708
|
if op == m.OP_UNSUBSCRIBE:
|
|
575
709
|
fn = t.take("H")
|
|
@@ -601,13 +735,24 @@ class Endpoint:
|
|
|
601
735
|
def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
|
|
602
736
|
"""plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
|
|
603
737
|
named = {fn for fn, _, _ in got}
|
|
604
|
-
|
|
738
|
+
# a capture only listens: it shares pins with anything, and nothing is kept from a pin by it (core §8.1)
|
|
739
|
+
kept = {a for a in self.plan if a[0] not in named and a[0] not in self.captures}
|
|
605
740
|
slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
|
|
741
|
+
slot_pins |= {p for c in self.conns.values() for p in c.pair if p != 0xFFFF} # a live connection's pins too
|
|
606
742
|
for fn, role, ch in got:
|
|
743
|
+
if fn in self.captures:
|
|
744
|
+
if role not in self._declared_roles(fn, ch):
|
|
745
|
+
raise Reject(m.UNAVAILABLE)
|
|
746
|
+
continue
|
|
607
747
|
roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
|
|
608
748
|
if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
|
|
609
749
|
raise Reject(m.UNAVAILABLE)
|
|
610
750
|
|
|
751
|
+
def _declared_roles(self, fn: int, channel: int) -> set[int]:
|
|
752
|
+
"""The roles fn's describe offers on `channel` (role_channels)."""
|
|
753
|
+
return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS
|
|
754
|
+
and channel in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
|
|
755
|
+
|
|
611
756
|
def _drop_plan(self, fn: int) -> None:
|
|
612
757
|
for a in [a for a in self.plan if a[0] == fn]:
|
|
613
758
|
self.gpio_modes.pop(a[2], None)
|
|
@@ -646,17 +791,24 @@ class Endpoint:
|
|
|
646
791
|
|
|
647
792
|
# ---- oep.wire.rvswd / swio ------------------------------------------------------------------
|
|
648
793
|
def _wire(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
649
|
-
allowed = self.pairs.get(fn, [])
|
|
650
794
|
if op == 0x01: # scan: count(u8) pairs -> tried count found...
|
|
651
795
|
count = t.take("B")
|
|
652
796
|
pairs = [t.take("HH") for _ in range(count)]
|
|
653
|
-
t.tail()
|
|
654
|
-
if
|
|
655
|
-
raise Reject(m.
|
|
656
|
-
|
|
797
|
+
got, _ = t.tail({0x01})
|
|
798
|
+
if 0x01 in got and (count or len(got[0x01]) != 2):
|
|
799
|
+
raise Reject(m.MALFORMED) # skip goes with count 0 only
|
|
800
|
+
live = [c.pair for c in self.conns.values() if c.fn == fn]
|
|
801
|
+
full = len(live) >= self.max_connections.get(fn, 1) # every seat taken: the live pairs only
|
|
802
|
+
if any(not self._allows(fn, p) or set(p) & self._held(fn, p) or (full and p not in live) for p in pairs):
|
|
803
|
+
raise Reject(m.UNAVAILABLE) # not allowed, held (§8.1), or no seat to try it on
|
|
804
|
+
if not pairs: # the count-0 list, from `skip` on (oep-if-debug §1)
|
|
805
|
+
skip = struct.unpack("<H", got[0x01])[0] if 0x01 in got else 0
|
|
806
|
+
pairs = [p for p in self._allowed_pairs(fn)
|
|
807
|
+
if not set(p) & self._held(fn, p) and (not full or p in live)][skip:]
|
|
808
|
+
pairs = pairs[:255] # tried is a u8
|
|
657
809
|
found = []
|
|
658
810
|
for p in pairs:
|
|
659
|
-
tg = self.
|
|
811
|
+
tg = self._target(fn, p)
|
|
660
812
|
if tg.present: # a live connection's pair: read over it, no restart
|
|
661
813
|
found.append(struct.pack("<BHHI", 1, *p, tg.dmstatus()))
|
|
662
814
|
return m.COMPLETED, m.SUCCESS, struct.pack("<BB", len(pairs), len(found)) + b"".join(found)
|
|
@@ -675,7 +827,7 @@ class Endpoint:
|
|
|
675
827
|
raise Reject(m.MALFORMED)
|
|
676
828
|
idle_clock = got[0x04][0] if 0x04 in got else 0
|
|
677
829
|
pair = self._pick_pair(fn, got)
|
|
678
|
-
tg = self.
|
|
830
|
+
tg = self._target(fn, pair)
|
|
679
831
|
speed = min(4_000_000, struct.unpack("<I", got[0x01])[0]) if 0x01 in got else 4_000_000
|
|
680
832
|
cid = self._conn_at(fn, pair)
|
|
681
833
|
flags = 0
|
|
@@ -729,15 +881,18 @@ class Endpoint:
|
|
|
729
881
|
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
730
882
|
|
|
731
883
|
def _pick_pair(self, fn: int, got: dict[int, bytes]) -> tuple[int, int]:
|
|
732
|
-
allowed = self.pairs.get(fn, [])
|
|
733
884
|
if 0x03 in got:
|
|
734
885
|
if len(got[0x03]) != 4:
|
|
735
886
|
raise Reject(m.MALFORMED)
|
|
736
887
|
pair = struct.unpack("<HH", got[0x03])
|
|
737
|
-
if pair
|
|
738
|
-
raise Reject(m.UNAVAILABLE) #
|
|
888
|
+
if not self._allows(fn, pair) or set(pair) & self._held(fn, pair):
|
|
889
|
+
raise Reject(m.UNAVAILABLE) # not allowed here, or its pins are held (§8.1)
|
|
739
890
|
return pair
|
|
740
|
-
|
|
891
|
+
live = [c.pair for c in self.conns.values() if c.fn == fn]
|
|
892
|
+
if len(live) == 1:
|
|
893
|
+
return live[0] # no pins: the wire's one live connection
|
|
894
|
+
allowed = self._allowed_pairs(fn)
|
|
895
|
+
if live or len(allowed) != 1:
|
|
741
896
|
raise Reject(m.UNAVAILABLE) # the host chooses among several
|
|
742
897
|
return allowed[0]
|
|
743
898
|
|
|
@@ -779,7 +934,7 @@ class Endpoint:
|
|
|
779
934
|
|
|
780
935
|
def _target_of(self, cid: int) -> FakeTarget:
|
|
781
936
|
c = self.conns[cid]
|
|
782
|
-
return self.
|
|
937
|
+
return self._target(c.fn, c.pair)
|
|
783
938
|
|
|
784
939
|
# ---- oep.target.riscv-dm --------------------------------------------------------------------
|
|
785
940
|
@staticmethod
|
|
@@ -1181,7 +1336,7 @@ class Endpoint:
|
|
|
1181
1336
|
places = [(s.wire_fn, s.pair) for s in slots.values()]
|
|
1182
1337
|
if len(set(places)) != len(places):
|
|
1183
1338
|
raise Reject(m.UNAVAILABLE)
|
|
1184
|
-
for fn in self.pairs:
|
|
1339
|
+
for fn in self.pairs: # every wire (pin_roles wires have an empty list)
|
|
1185
1340
|
if sum(1 for s in slots.values() if s.wire_fn == fn and s.attach == SLOT_ATTACH["at_boot"]) > \
|
|
1186
1341
|
self.max_connections.get(fn, 1):
|
|
1187
1342
|
raise Reject(m.UNAVAILABLE)
|
|
@@ -1244,7 +1399,7 @@ class Endpoint:
|
|
|
1244
1399
|
raise Reject(m.MALFORMED)
|
|
1245
1400
|
if not SLOT_NAME.fullmatch(name.decode("ascii", "replace")):
|
|
1246
1401
|
raise Reject(m.MALFORMED)
|
|
1247
|
-
if self.names.get(wire_fn) not in WIRES or
|
|
1402
|
+
if self.names.get(wire_fn) not in WIRES or not self._allows(wire_fn, (swdio, swclk)):
|
|
1248
1403
|
raise Reject(m.UNAVAILABLE)
|
|
1249
1404
|
if mech not in self.mechanisms:
|
|
1250
1405
|
raise Reject(m.UNSUPPORTED)
|
|
@@ -1301,7 +1456,7 @@ class Endpoint:
|
|
|
1301
1456
|
s = self.slots[n]
|
|
1302
1457
|
rt = self.slot_rt[n]
|
|
1303
1458
|
rt.last_try_ms = self.now()
|
|
1304
|
-
tg = self.
|
|
1459
|
+
tg = self._target(s.wire_fn, s.pair)
|
|
1305
1460
|
cid = self._conn_at(s.wire_fn, s.pair)
|
|
1306
1461
|
if cid is None:
|
|
1307
1462
|
if not tg.present:
|
|
@@ -1348,9 +1503,11 @@ class Endpoint:
|
|
|
1348
1503
|
self.flows[(port, key)] = Flow(sid, 0)
|
|
1349
1504
|
|
|
1350
1505
|
def tick(self) -> None:
|
|
1351
|
-
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet."""
|
|
1506
|
+
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet, the captures."""
|
|
1352
1507
|
self._lapse()
|
|
1353
1508
|
now = self.now()
|
|
1509
|
+
for fn, cap in self.captures.items():
|
|
1510
|
+
self._events(fn, cap.tick(now))
|
|
1354
1511
|
for n, s in self.slots.items():
|
|
1355
1512
|
rt = self.slot_rt[n]
|
|
1356
1513
|
if (s.attach == SLOT_ATTACH["at_boot"] and s.retry_s and not rt.evicted
|
|
@@ -1498,4 +1655,4 @@ class Endpoint:
|
|
|
1498
1655
|
|
|
1499
1656
|
SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
|
|
1500
1657
|
"oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
|
|
1501
|
-
"oep.probe.config": "config_op"}
|
|
1658
|
+
"oep.probe.config": "config_op", "oep.fixture.capture": "capture"}
|
oep_client/fake.py
CHANGED
|
@@ -108,6 +108,21 @@ 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
|
+
_CAPM = reg.FIXTURE_CAPTURE.enum["mode"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _capture_decl(modes: list[str], max_channels: int, widths: list[int], max_samples: int, ring: int,
|
|
116
|
+
max_read: int) -> tuple[bytes, ...]:
|
|
117
|
+
"""oep.fixture.capture's declarations (oep-if-capture §3.5): a mode entry per mode, channels (max, the w allowed as
|
|
118
|
+
bits: bit i = w 2^i), the trigger types (immediate, level, edge) with the most pretrigger, max_read, segment_ring."""
|
|
119
|
+
out = tuple(catalog.tlv(_CAPD["mode"], struct.pack("<BBII", _CAPM[k], 1, max_samples, ring if k == "repeat" else 1))
|
|
120
|
+
for k in modes)
|
|
121
|
+
bits = sum(1 << (w.bit_length() - 1) for w in widths)
|
|
122
|
+
return out + (catalog.tlv(_CAPD["channels"], bytes([max_channels, bits])),
|
|
123
|
+
catalog.tlv(_CAPD["trigger"], struct.pack("<BI", 0b111, max_samples - 1)),
|
|
124
|
+
catalog.u32(_CAPD["max_read"], max_read), catalog.u16(_CAPD["segment_ring"], ring),
|
|
125
|
+
catalog.u32(FEATURES, 0b001))
|
|
111
126
|
MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
|
|
112
127
|
_CFG = reg.PROBE_CONFIG.tlv["describe"]
|
|
113
128
|
|
|
@@ -139,7 +154,8 @@ def _core(firmware: str, model: str, unit_id: bytes, channels: int, reserved: li
|
|
|
139
154
|
return Offered(0, 0, "oep.core", (
|
|
140
155
|
catalog.text(CORE_FIRMWARE, firmware), catalog.text(CORE_MODEL, model), catalog.tlv(CORE_UNIT_ID, unit_id),
|
|
141
156
|
catalog.u16(CORE_CHANNELS, channels), catalog.tlv(CORE_RESERVED, struct.pack("<H", base) + bits),
|
|
142
|
-
catalog.text(CORE_PROFILE, profile)) + tuple(_label(c, n) for c, n in labels.items())
|
|
157
|
+
) + ((catalog.text(CORE_PROFILE, profile),) if profile else ()) + tuple(_label(c, n) for c, n in labels.items())
|
|
158
|
+
+ extra)
|
|
143
159
|
|
|
144
160
|
|
|
145
161
|
def p4_x035() -> FakeProbe:
|
|
@@ -164,9 +180,11 @@ def p4_x035() -> FakeProbe:
|
|
|
164
180
|
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
165
181
|
Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
166
182
|
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
167
|
-
|
|
183
|
+
# the P4's PARLIO: w 1-16, one-shot / repeat / streaming
|
|
184
|
+
Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(16)}) + (
|
|
168
185
|
catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
|
|
169
|
-
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
|
|
186
|
+
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))
|
|
187
|
+
+ _capture_decl(["one_shot", "repeat", "streaming"], 16, [1, 2, 4, 8, 16], 1 << 20, 8, 4096)),
|
|
170
188
|
Offered(8, 6, f"{NS}.esp32.i2c-target", _roles({1: pins, 2: pins}) + (
|
|
171
189
|
catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
|
|
172
190
|
catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
|
|
@@ -193,8 +211,10 @@ def esp32_v003() -> FakeProbe:
|
|
|
193
211
|
Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
|
|
194
212
|
Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
|
|
195
213
|
catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
|
|
214
|
+
# the classic ESP32's GPIO sampler: a byte a sample (w 8), one-shot
|
|
196
215
|
Offered(6, 4, "oep.fixture.capture", _roles({k: wired for k in range(4)}) + (
|
|
197
|
-
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
|
|
216
|
+
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))
|
|
217
|
+
+ _capture_decl(["one_shot"], 8, [8], 65536, 1, 480)),
|
|
198
218
|
Offered(7, 5, f"{NS}.esp32.i2c-target", _roles({1: wired, 2: wired}) + (
|
|
199
219
|
catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
200
220
|
# Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
|
|
@@ -230,4 +250,24 @@ def p4_bench() -> FakeProbe:
|
|
|
230
250
|
])
|
|
231
251
|
|
|
232
252
|
|
|
233
|
-
|
|
253
|
+
def rp2350_pins() -> FakeProbe:
|
|
254
|
+
"""A board firmware whose wire takes its pins from the host (oep-if-debug §1, role_channels): any two of GP0-GP29
|
|
255
|
+
but GP19 (the Pro Micro RP2350's PSRAM CS) are an RVSWD pair, a reset line or fixture pins. USB CDC (serial port 0).
|
|
256
|
+
The fake target answers on (0, 1), the first pair."""
|
|
257
|
+
reserved = [19]
|
|
258
|
+
pins = [p for p in range(30) if p not in reserved]
|
|
259
|
+
return FakeProbe("rp2350-pins", 1024, [
|
|
260
|
+
_core("3.0.0", "sparkfun-promicro-rp2350", bytes.fromhex("e66138935f2b1f2c"), 30, reserved, "",
|
|
261
|
+
{}, _transports([(TRANSPORT["usb_cdc"], 0)])),
|
|
262
|
+
Offered(1, 1, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
|
|
263
|
+
catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
|
|
264
|
+
Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
|
|
265
|
+
catalog.u16(MAX_LENGTH, 1000))),
|
|
266
|
+
Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
267
|
+
Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
|
|
268
|
+
Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
269
|
+
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
270
|
+
])
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
PROFILES = {"p4-x035": p4_x035, "esp32-v003": esp32_v003, "p4-bench": p4_bench, "rp2350-pins": rp2350_pins}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
"""oep.fixture.capture in the fake probe (oep-spec docs/oep-if-capture.ja.md, revision 1, logic).
|
|
3
|
+
|
|
4
|
+
What it captures is known in advance, so a receiver can check it: sample i of a capture (counted from its start, across
|
|
5
|
+
segments) is the counter i, and channel k is bit k of it - a square wave of period 2^(k+1) samples. The layout is the
|
|
6
|
+
probe's (§1.1): w is the smallest width the describe allows (channels tag 0x44) that holds the channels, pos[k] = k.
|
|
7
|
+
|
|
8
|
+
Modes: one-shot (the segment is there as soon as start answers), repeat (segments come with the clock at the actual
|
|
9
|
+
rate, up to the ring; a full ring stops the capture, release frees it) and streaming (the bytes come with the clock and
|
|
10
|
+
go out as data pushes while subscribed). Triggers: level and edge on a channel, with a pretrigger. `slipped` sets flags
|
|
11
|
+
bit2 on every segment (a probe whose software pace fell behind).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import struct
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from fractions import Fraction
|
|
19
|
+
|
|
20
|
+
from . import message as m, registry as reg
|
|
21
|
+
|
|
22
|
+
CAP = reg.FIXTURE_CAPTURE
|
|
23
|
+
OP = CAP.op
|
|
24
|
+
TLV, ANSWER = CAP.tlv["configure"], CAP.tlv["configure_answer"]
|
|
25
|
+
MODE, STATE, TRIGGER = CAP.enum["mode"], CAP.enum["state"], CAP.enum["trigger"]
|
|
26
|
+
STOPPED, FLAG = CAP.enum["stopped_reason"], CAP.enum["segment_flag"]
|
|
27
|
+
EVENT = CAP.event
|
|
28
|
+
NONE = 0xFFFFFFFF
|
|
29
|
+
MAX_SAMPLES = 1 << 20 # a fake keeps its captures in memory
|
|
30
|
+
SEGMENT = struct.Struct("<IQIQIB") # serial position samples start_us trigger_index flags (§2)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Reject(Exception):
|
|
34
|
+
def __init__(self, reason: int, payload: bytes = b""):
|
|
35
|
+
self.reason, self.payload = reason, payload
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Segment:
|
|
40
|
+
serial: int
|
|
41
|
+
position: int
|
|
42
|
+
samples: int
|
|
43
|
+
start_us: int
|
|
44
|
+
trigger_index: int = NONE
|
|
45
|
+
flags: int = 0
|
|
46
|
+
|
|
47
|
+
def pack(self) -> bytes:
|
|
48
|
+
return SEGMENT.pack(self.serial, self.position, self.samples, self.start_us, self.trigger_index, self.flags)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class FakeCapture:
|
|
53
|
+
"""One capture interface. `now_ms` is the endpoint's clock; `roles()` the channels the plan gives it (role order)."""
|
|
54
|
+
modes: set[int] # the modes the describe declares
|
|
55
|
+
widths: set[int] # the w the describe allows
|
|
56
|
+
min_hz: int
|
|
57
|
+
max_hz: int
|
|
58
|
+
ring: int = 8 # segment_ring
|
|
59
|
+
max_read: int = 4096
|
|
60
|
+
slipped: bool = False
|
|
61
|
+
state: int = STATE["unconfigured"]
|
|
62
|
+
mode: int = MODE["one_shot"]
|
|
63
|
+
rate: Fraction = Fraction(0)
|
|
64
|
+
samples: int = 0
|
|
65
|
+
segments_max: int = 0
|
|
66
|
+
trigger: tuple[int, int, int] | None = None
|
|
67
|
+
pretrigger: int = 0
|
|
68
|
+
width: int = 0
|
|
69
|
+
channels: int = 0
|
|
70
|
+
data: bytearray = field(default_factory=bytearray) # the stream from `base` on
|
|
71
|
+
base: int = 0 # the byte position data[0] is at
|
|
72
|
+
segs: list[Segment] = field(default_factory=list)
|
|
73
|
+
serial_done: int = 0
|
|
74
|
+
started_ms: int = 0
|
|
75
|
+
produced: int = 0 # samples produced since start (repeat / streaming)
|
|
76
|
+
sent: int = 0 # streaming: the byte position pushed so far
|
|
77
|
+
first_sample: int = 0 # the counter value of the capture's first sample
|
|
78
|
+
gap_next: bool = False # repeat: the capture stopped for want of a segment; the next one says so
|
|
79
|
+
|
|
80
|
+
# ---- configure ------------------------------------------------------------------------------------------------
|
|
81
|
+
def settle(self, got: dict[int, bytes], critical: set[int], channels: int) -> dict:
|
|
82
|
+
"""The actual values for a configure / query request (nothing changed)."""
|
|
83
|
+
mode = got[TLV["mode"]][0] if TLV["mode"] in got else MODE["one_shot"]
|
|
84
|
+
if mode not in self.modes:
|
|
85
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["mode"] | (m.TAG_CRITICAL if TLV["mode"] in critical else 0)]))
|
|
86
|
+
if TLV["rate"] not in got or len(got[TLV["rate"]]) != 4:
|
|
87
|
+
raise Reject(m.MALFORMED)
|
|
88
|
+
asked = struct.unpack("<I", got[TLV["rate"]])[0]
|
|
89
|
+
if not asked:
|
|
90
|
+
raise Reject(m.MALFORMED)
|
|
91
|
+
# the source clock divided by a whole number: the nearest rate at or under the one asked, inside the range
|
|
92
|
+
div = max(1, -(-self.max_hz // min(max(asked, self.min_hz), self.max_hz)))
|
|
93
|
+
rate = Fraction(self.max_hz, div)
|
|
94
|
+
samples = struct.unpack("<I", got[TLV["samples"]])[0] if TLV["samples"] in got else 4096
|
|
95
|
+
samples = max(1, min(samples, MAX_SAMPLES))
|
|
96
|
+
segs = struct.unpack("<I", got[TLV["segments"]])[0] if TLV["segments"] in got else self.ring
|
|
97
|
+
segs = max(1, min(segs, self.ring)) if mode == MODE["repeat"] else 1
|
|
98
|
+
trigger = None
|
|
99
|
+
if TLV["trigger"] in got:
|
|
100
|
+
kind, role, value = struct.unpack("<BBH", got[TLV["trigger"]])
|
|
101
|
+
if kind not in (TRIGGER["immediate"], TRIGGER["level"], TRIGGER["edge"]) or (kind and role >= channels):
|
|
102
|
+
raise Reject(m.UNSUPPORTED, bytes([TLV["trigger"] | m.TAG_CRITICAL]))
|
|
103
|
+
trigger = (kind, role, value) if kind else None
|
|
104
|
+
pre = struct.unpack("<I", got[TLV["pretrigger"]])[0] if TLV["pretrigger"] in got else 0
|
|
105
|
+
width = min((w for w in self.widths if w >= max(channels, 1)), default=None)
|
|
106
|
+
if width is None or not channels:
|
|
107
|
+
raise Reject(m.UNAVAILABLE) # no channels planned, or more than a sample holds
|
|
108
|
+
if mode != MODE["one_shot"] and width < 8:
|
|
109
|
+
samples = -(-samples // (8 // width)) * (8 // width) # segments end on a byte
|
|
110
|
+
return {"mode": mode, "rate": rate, "samples": samples, "segments": segs, "trigger": trigger,
|
|
111
|
+
"pretrigger": min(pre, samples - 1), "width": width, "channels": channels}
|
|
112
|
+
|
|
113
|
+
def apply(self, s: dict) -> None:
|
|
114
|
+
self.mode, self.rate, self.samples, self.segments_max = s["mode"], s["rate"], s["samples"], s["segments"]
|
|
115
|
+
self.trigger, self.pretrigger, self.width, self.channels = s["trigger"], s["pretrigger"], s["width"], s["channels"]
|
|
116
|
+
self.state = STATE["configured"]
|
|
117
|
+
self._clear()
|
|
118
|
+
|
|
119
|
+
@staticmethod
|
|
120
|
+
def answer(s: dict) -> bytes:
|
|
121
|
+
def tlv(tag: int, v: bytes) -> bytes:
|
|
122
|
+
return bytes([tag, len(v)]) + v
|
|
123
|
+
return (tlv(ANSWER["actual_rate"], struct.pack("<II", s["rate"].numerator, s["rate"].denominator))
|
|
124
|
+
+ tlv(ANSWER["layout"], bytes([s["width"], s["channels"]]) + bytes(range(s["channels"])))
|
|
125
|
+
+ tlv(ANSWER["actual_samples"], struct.pack("<I", s["samples"]))
|
|
126
|
+
+ tlv(ANSWER["actual_segments"], struct.pack("<I", s["segments"]))
|
|
127
|
+
+ tlv(ANSWER["timing"], struct.pack("<BI", 1 if s["rate"].denominator != 1 else 0, 0))
|
|
128
|
+
+ tlv(ANSWER["blocking_ms"], struct.pack("<I", 0)))
|
|
129
|
+
|
|
130
|
+
# ---- the data ---------------------------------------------------------------------------------------------------
|
|
131
|
+
def _clear(self) -> None:
|
|
132
|
+
self.data, self.base, self.segs, self.serial_done = bytearray(), 0, [], 0
|
|
133
|
+
self.produced, self.sent, self.first_sample, self.gap_next = 0, 0, 0, False
|
|
134
|
+
|
|
135
|
+
def _pack(self, first: int, n: int) -> bytes:
|
|
136
|
+
"""Samples first .. first+n-1 of the counter in the layout (w bits each, little endian, low samples first)."""
|
|
137
|
+
w = self.width
|
|
138
|
+
if w >= 8:
|
|
139
|
+
per = w // 8
|
|
140
|
+
mask = (1 << w) - 1
|
|
141
|
+
return b"".join(((first + i) & mask).to_bytes(per, "little") for i in range(n))
|
|
142
|
+
out = bytearray((n * w + 7) // 8)
|
|
143
|
+
mask = (1 << w) - 1
|
|
144
|
+
for i in range(n):
|
|
145
|
+
bit = i * w
|
|
146
|
+
out[bit >> 3] |= ((first + i) & mask) << (bit & 7)
|
|
147
|
+
return bytes(out)
|
|
148
|
+
|
|
149
|
+
def _trigger_at(self) -> int:
|
|
150
|
+
"""The first sample, at or after the pretrigger, where the trigger holds (the counter's bit `role`)."""
|
|
151
|
+
kind, role, value = self.trigger
|
|
152
|
+
period = 1 << (role + 1)
|
|
153
|
+
i = self.pretrigger
|
|
154
|
+
while True: # within two periods of the channel
|
|
155
|
+
now, before = (i >> role) & 1, ((i - 1) >> role) & 1 if i else None
|
|
156
|
+
if kind == TRIGGER["level"] and now == (value & 1):
|
|
157
|
+
return i
|
|
158
|
+
if kind == TRIGGER["edge"] and before is not None and now != before:
|
|
159
|
+
if value == 2 or (value == 0 and now == 1) or (value == 1 and now == 0):
|
|
160
|
+
return i
|
|
161
|
+
i += 1
|
|
162
|
+
if i > self.pretrigger + 2 * period + 1:
|
|
163
|
+
return self.pretrigger
|
|
164
|
+
|
|
165
|
+
def _segment(self, n: int, now_ms: int, trigger_index: int = NONE, flags: int = 0) -> Segment:
|
|
166
|
+
if self.gap_next:
|
|
167
|
+
flags, self.gap_next = flags | FLAG["gap"], False
|
|
168
|
+
seg = Segment(self.serial_done, self.base + len(self.data), n, now_ms * 1000, trigger_index,
|
|
169
|
+
flags | (FLAG["slipped"] if self.slipped else 0))
|
|
170
|
+
self.data += self._pack(self.first_sample + self.produced, n)
|
|
171
|
+
self.produced += n
|
|
172
|
+
self.segs.append(seg)
|
|
173
|
+
self.serial_done += 1
|
|
174
|
+
return seg
|
|
175
|
+
|
|
176
|
+
# ---- the operations ---------------------------------------------------------------------------------------------
|
|
177
|
+
def start(self, now_ms: int) -> list[bytes]:
|
|
178
|
+
"""-> the events it raises (kind(u8) payload), for the endpoint to send when subscribed."""
|
|
179
|
+
if self.state == STATE["unconfigured"]:
|
|
180
|
+
raise Reject(m.UNAVAILABLE)
|
|
181
|
+
self._clear()
|
|
182
|
+
self.started_ms = now_ms
|
|
183
|
+
events = []
|
|
184
|
+
if self.mode == MODE["one_shot"]:
|
|
185
|
+
index = self._trigger_at() if self.trigger else NONE
|
|
186
|
+
if self.trigger:
|
|
187
|
+
events.append(bytes([EVENT["triggered"]]) + struct.pack("<II", 0, index))
|
|
188
|
+
seg = self._segment(self.samples, now_ms, index)
|
|
189
|
+
self.state = STATE["done"]
|
|
190
|
+
events += [bytes([EVENT["segment"]]) + seg.pack(), bytes([EVENT["stopped"], STOPPED["complete"]])]
|
|
191
|
+
else:
|
|
192
|
+
self.state = STATE["capturing"]
|
|
193
|
+
return events
|
|
194
|
+
|
|
195
|
+
def tick(self, now_ms: int) -> list[bytes]:
|
|
196
|
+
"""Time passes: repeat's segments, streaming's bytes. -> events."""
|
|
197
|
+
if self.state != STATE["capturing"]:
|
|
198
|
+
return []
|
|
199
|
+
due = int((now_ms - self.started_ms) * self.rate / 1000) # samples the clock has taken since start
|
|
200
|
+
events = []
|
|
201
|
+
if self.mode == MODE["repeat"]:
|
|
202
|
+
while due - self.produced >= self.samples:
|
|
203
|
+
if len(self.segs) >= self.segments_max: # no free segment: the capture stops
|
|
204
|
+
self.state = STATE["paused"]
|
|
205
|
+
events.append(bytes([EVENT["stopped"], STOPPED["no_free_segment"]]))
|
|
206
|
+
break
|
|
207
|
+
seg = self._segment(self.samples, now_ms)
|
|
208
|
+
events.append(bytes([EVENT["segment"]]) + seg.pack())
|
|
209
|
+
elif self.mode == MODE["streaming"]:
|
|
210
|
+
per_byte = 8 // self.width if self.width < 8 else 1 # whole bytes only
|
|
211
|
+
n = (due - self.produced) // per_byte * per_byte
|
|
212
|
+
if n > 0:
|
|
213
|
+
self.data += self._pack(self.first_sample + self.produced, min(n, MAX_SAMPLES))
|
|
214
|
+
self.produced += min(n, MAX_SAMPLES)
|
|
215
|
+
return events
|
|
216
|
+
|
|
217
|
+
def stop(self) -> list[bytes]:
|
|
218
|
+
if self.state not in (STATE["capturing"], STATE["waiting"], STATE["paused"]):
|
|
219
|
+
return []
|
|
220
|
+
self.state = STATE["done"]
|
|
221
|
+
return [bytes([EVENT["stopped"], STOPPED["host"]])]
|
|
222
|
+
|
|
223
|
+
def status(self) -> bytes:
|
|
224
|
+
return struct.pack("<BIQB", self.state, self.serial_done, self.base + len(self.data), 0)
|
|
225
|
+
|
|
226
|
+
def read(self, position: int, most: int, budget: int) -> bytes:
|
|
227
|
+
flags = 0
|
|
228
|
+
if position < self.base:
|
|
229
|
+
position, flags = self.base, flags | 0x02 # re-used: from what is kept (gap)
|
|
230
|
+
end = self.base + len(self.data)
|
|
231
|
+
take = max(0, min(most, self.max_read, budget - 9, end - position))
|
|
232
|
+
data = bytes(self.data[position - self.base:position - self.base + take])
|
|
233
|
+
if position + take < end:
|
|
234
|
+
flags |= 0x01 # more
|
|
235
|
+
return struct.pack("<QB", position, flags) + data
|
|
236
|
+
|
|
237
|
+
def segment_list(self, first: int, budget: int) -> bytes:
|
|
238
|
+
out = [s.pack() for s in self.segs if s.serial >= first]
|
|
239
|
+
out = out[:max(0, (budget - 1) // SEGMENT.size)][:255]
|
|
240
|
+
return bytes([len(out)]) + b"".join(out)
|
|
241
|
+
|
|
242
|
+
def release(self, serial: int, now_ms: int) -> None:
|
|
243
|
+
if self.mode != MODE["repeat"]:
|
|
244
|
+
return # one-shot: the next start clears; streaming: pushed
|
|
245
|
+
keep = [s for s in self.segs if s.serial > serial]
|
|
246
|
+
drop_to = keep[0].position if keep else self.base + len(self.data)
|
|
247
|
+
del self.data[:drop_to - self.base]
|
|
248
|
+
self.base = drop_to
|
|
249
|
+
self.segs = keep
|
|
250
|
+
if self.state == STATE["paused"]: # it goes on from now; the gap shows (flags bit0)
|
|
251
|
+
self.state, self.gap_next = STATE["capturing"], True
|
|
252
|
+
self.started_ms = now_ms - int(self.produced * 1000 / self.rate)
|
|
253
|
+
|
|
254
|
+
def pushes(self, fn: int, next_seq, budget: int) -> list[bytes]:
|
|
255
|
+
"""Streaming: the bytes not pushed yet, as data frames (position(u64) then data, common §1.5)."""
|
|
256
|
+
if self.mode != MODE["streaming"]:
|
|
257
|
+
return []
|
|
258
|
+
out = []
|
|
259
|
+
end = self.base + len(self.data)
|
|
260
|
+
while self.sent < end:
|
|
261
|
+
n = min(end - self.sent, budget - 13)
|
|
262
|
+
chunk = bytes(self.data[self.sent - self.base:self.sent - self.base + n])
|
|
263
|
+
out.append(bytes([m.ROLE_DATA]) + struct.pack("<HHQ", fn, next_seq(), self.sent) + chunk)
|
|
264
|
+
self.sent += n
|
|
265
|
+
del self.data[:self.sent - self.base] # pushed: the probe re-uses it
|
|
266
|
+
self.base = self.sent
|
|
267
|
+
return out
|
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 = "08b7d1656bf59269"
|
|
6
6
|
SCHEMA = 1
|
|
7
7
|
PROTOCOL_REVISION = 1
|
|
8
8
|
CONFIRM_REQUEST_MAGIC = 'OEP?'
|
|
@@ -25,13 +25,13 @@ CORE = _NS(name="oep.core", revision=1, op={"confirm": 0x01, "list": 0x02, "desc
|
|
|
25
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={})
|
|
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
|
-
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "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={})
|
|
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={})
|
|
29
29
|
INTERFACES["oep.wire.rvswd"] = WIRE_RVSWD
|
|
30
30
|
WIRE_SWIO = _NS(name="oep.wire.swio", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "attach_under_reset": 0x04, "connections": 0x05}, lock_free={0x05},
|
|
31
|
-
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "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, "reset": 0x03}}, own={})
|
|
31
|
+
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "scan": {"skip": 0x01}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "pins": 0x03}, "attach_under_reset": {"max_speed": 0x01, "pins": 0x03}, "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, "reset": 0x03}}, own={})
|
|
32
32
|
INTERFACES["oep.wire.swio"] = WIRE_SWIO
|
|
33
33
|
WIRE_SWD = _NS(name="oep.wire.swd", revision=1, op={"scan": 0x01, "attach": 0x02, "detach": 0x03, "connections": 0x05}, lock_free={0x05},
|
|
34
|
-
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "targetsel": 0x02, "pins": 0x03}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "pin_role": {"swdio": 0x01, "swclk": 0x02}}, own={})
|
|
34
|
+
closed_tail={}, tlv={"describe": {"max_connections": 0x40}, "scan": {"skip": 0x01}, "detach": {"force": 0x01}, "attach": {"max_speed": 0x01, "targetsel": 0x02, "pins": 0x03}}, event={}, enum={"connection_users": {"host_session": 0x01, "slot": 0x02}, "pin_role": {"swdio": 0x01, "swclk": 0x02}}, own={})
|
|
35
35
|
INTERFACES["oep.wire.swd"] = WIRE_SWD
|
|
36
36
|
TARGET_RISCV_DM = _NS(name="oep.target.riscv-dm", revision=1, op={"dmi": 0x01, "halt": 0x02, "resume": 0x03, "reset": 0x04, "read_block": 0x05, "write_block": 0x06, "run": 0x07, "step": 0x08}, lock_free={},
|
|
37
37
|
closed_tail={}, tlv={"describe": {}, "reset": {"method": 0x01}}, event={}, enum={"dmi_step": {"write": 0x01, "read": 0x02, "poll_reads": 0x03, "wait_us": 0x04, "poll_us": 0x05}, "reset_mode": {"run": 0x00, "run_verified": 0x01, "halt_at_reset": 0x02}, "reset_method": {"probe_default": 0x00, "ndmreset": 0x01, "system_reset": 0x02}}, own={})
|
oep_client/riscv.py
CHANGED
|
@@ -76,23 +76,36 @@ class WireBase(Interface):
|
|
|
76
76
|
TAG_MAX_SPEED = 0x01
|
|
77
77
|
TAG_PINS = 0x03 # swdio(u16) swclk(u16, 0xFFFF on one wire), critical (oep-if-debug §1)
|
|
78
78
|
|
|
79
|
+
TAG_SKIP = 0x01 # scan, count 0 only: pairs of the count-0 list to skip (oep-if-debug §1)
|
|
80
|
+
|
|
79
81
|
def scan(self, pairs: list[tuple[int, int]] | None = None) -> list[Found]:
|
|
80
|
-
"""Try `pairs` of (swdio, swclk); None = every pair the probe allows (describe's
|
|
81
|
-
role_channels). A pair the probe does not allow
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
"""Try `pairs` of (swdio, swclk); None = every pair the probe allows and nothing holds (describe's
|
|
83
|
+
channel_group / role_channels, oep-if-debug §1). A pair the probe does not allow, or one whose pins something
|
|
84
|
+
holds, refuses the whole scan (rejected unavailable). The probe tries at most 255 pairs a request and stops early
|
|
85
|
+
when its answer would not fit one frame; this goes on until every pair is tried (count 0: with skip until
|
|
86
|
+
tried = 0)."""
|
|
84
87
|
out = []
|
|
88
|
+
pairs = list(pairs or [])
|
|
89
|
+
skip = 0
|
|
85
90
|
while True:
|
|
86
|
-
|
|
91
|
+
chunk = pairs[:255]
|
|
92
|
+
body = bytes([len(chunk)]) + b"".join(struct.pack("<HH", d, c) for d, c in chunk)
|
|
93
|
+
if not pairs and skip:
|
|
94
|
+
body += m.tlv(self.TAG_SKIP, struct.pack("<H", skip))
|
|
87
95
|
rd = m.Reader(self._call(self.SCAN, body).payload)
|
|
88
96
|
tried, count = rd.take("BB")
|
|
89
97
|
for _ in range(count):
|
|
90
98
|
kind, dio, clk, status = rd.take("BHHI")
|
|
91
99
|
out.append(Found(kind, (dio, clk), status))
|
|
92
100
|
rd.tail()
|
|
93
|
-
if
|
|
101
|
+
if tried == 0:
|
|
94
102
|
return out
|
|
95
|
-
|
|
103
|
+
if pairs:
|
|
104
|
+
pairs = pairs[tried:]
|
|
105
|
+
if not pairs:
|
|
106
|
+
return out
|
|
107
|
+
else:
|
|
108
|
+
skip += tried
|
|
96
109
|
|
|
97
110
|
def detach(self, conn: int) -> None:
|
|
98
111
|
self._call(self.DETACH, struct.pack("<H", conn))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: oep-client-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
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,15 @@ 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.
|
|
112
119
|
|
|
113
120
|
Other programs' tests run `fake_serve` as a child process:
|
|
114
121
|
|
|
@@ -122,4 +129,5 @@ The pty is a serial port (the host opens it with TIOCEXCL); `--tcp PORT` is `--f
|
|
|
122
129
|
`--framing length` (the vendor bulk / TCP form). Faults: `--drop N` (the N-th answer is not sent, once; the request did run,
|
|
123
130
|
so a resend gets the remembered result), `--noise TEXT` (noise before every answer), `--corrupt N` (the N-th answer's CRC
|
|
124
131
|
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.
|
|
132
|
+
riscv-dm run, `--capture-slipped` flags bit2 on every capture segment. Events and data pushes go out on the pty and on TCP
|
|
133
|
+
(both framings). The rest: `--help`.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
oep_client/__init__.py,sha256=
|
|
1
|
+
oep_client/__init__.py,sha256=lMaMtVlHAJo6QrVhc8eg4Rho6TvB_sVCUg-_MvgWC0s,163
|
|
2
2
|
oep_client/__main__.py,sha256=qtVGdpAHULBtLY2SedabJf-uwDZZhMf_YmrbqRzM57U,14669
|
|
3
3
|
oep_client/arm.py,sha256=JLyUhLSPcmsk73schZcaJYoOKuHzrvnU8kPmfrEvVuo,12331
|
|
4
4
|
oep_client/capture.py,sha256=hs8mQU1gE1P67GOivxw2C2Ur8wimWqCdhtk1nqGN71Q,18092
|
|
@@ -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=Qp3oXoQ1K6WBzh7oEzeE72QD3I45oeZC0mgpHPQrPFY,83625
|
|
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=SXTQSySxTDWAmUBGMb3jEuaH-O23aT-42NzrwMWBgjw,15846
|
|
16
|
+
oep_client/fake_capture.py,sha256=ZgRZm83ViJir5BD8s4nfG-6uc_UPoOuJuuh54vvQuAo,13789
|
|
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/riscv.py,sha256=
|
|
27
|
+
oep_client/registry.py,sha256=6FVVEs1b6krGygH21zma84mmMB_B65dh8S7PrjYrME0,10056
|
|
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.9.dist-info/METADATA,sha256=VFcyTZyeXECe3xA8I1CRUnJwog_iXVZQIFM9QHHgcH0,8128
|
|
34
|
+
oep_client_python-0.0.9.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
35
|
+
oep_client_python-0.0.9.dist-info/entry_points.txt,sha256=h6_8QX89Ww1ogALEGG5g4kspT3IOV6O51lwKqf5BWco,49
|
|
36
|
+
oep_client_python-0.0.9.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
|
|
37
|
+
oep_client_python-0.0.9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|