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