oep-client-python 0.0.1__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 +5 -0
- oep_client/v1/__init__.py +1 -0
- oep_client/v1/__main__.py +40 -0
- oep_client/v1/arm.py +269 -0
- oep_client/v1/capture.py +365 -0
- oep_client/v1/catalog.py +181 -0
- oep_client/v1/ch32_flash.py +254 -0
- oep_client/v1/cobs.py +73 -0
- oep_client/v1/console.py +165 -0
- oep_client/v1/core.py +172 -0
- oep_client/v1/decode.py +65 -0
- oep_client/v1/dump.py +157 -0
- oep_client/v1/endpoint.py +1479 -0
- oep_client/v1/esp32_targets.py +137 -0
- oep_client/v1/fake.py +233 -0
- oep_client/v1/fake_serial.py +105 -0
- oep_client/v1/fake_serve.py +299 -0
- oep_client/v1/fixture.py +121 -0
- oep_client/v1/frames.py +91 -0
- oep_client/v1/hid_stream.py +257 -0
- oep_client/v1/host.py +321 -0
- oep_client/v1/interfaces.py +119 -0
- oep_client/v1/link.py +414 -0
- oep_client/v1/message.py +235 -0
- oep_client/v1/names.py +86 -0
- oep_client/v1/registry.py +59 -0
- oep_client/v1/riscv.py +430 -0
- oep_client/v1/rp2350.py +85 -0
- oep_client/v1/target.py +16 -0
- oep_client/v1/uiapduino.py +121 -0
- oep_client/v1/usb_stream.py +209 -0
- oep_client_python-0.0.1.dist-info/METADATA +106 -0
- oep_client_python-0.0.1.dist-info/RECORD +36 -0
- oep_client_python-0.0.1.dist-info/WHEEL +4 -0
- oep_client_python-0.0.1.dist-info/entry_points.txt +2 -0
- oep_client_python-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1479 @@
|
|
|
1
|
+
"""A fake probe endpoint that speaks OEP v1, answering whole messages (no hardware).
|
|
2
|
+
|
|
3
|
+
This is the spec side's "working spec": ch32rv, this client and the probe firmware are checked against it. It wraps
|
|
4
|
+
a `fake.FakeProbe` (the static declarations) and does what oep-spec docs/oep-core.ja.md and docs/oep-if-*.ja.md
|
|
5
|
+
define:
|
|
6
|
+
|
|
7
|
+
- confirm with a revision range; `revision=0` makes a v0 probe that answers in the v0 shape and drops role 0x81
|
|
8
|
+
requests unanswered
|
|
9
|
+
- the lock (core §6): a host-chosen session id, extended by every request of its holder and counted from when that
|
|
10
|
+
request completed; when it lapses or ends the last id is remembered and may resume; lease 0 = the probe default,
|
|
11
|
+
1000-60000 ms taken as asked, longer ones cut to `lease_max_ms`; the open's owner TLV shown by lock_state and by
|
|
12
|
+
rejected locked (never the session id)
|
|
13
|
+
- the resend table (core §5.2): the last session's recent requests with their results (results longer than
|
|
14
|
+
`remember_max` bytes are not kept -> result_lost), corr_reused, result_lost for old requests, emptied by open
|
|
15
|
+
- rejects: no session, locked (+ remaining ms), session required, no connection, unsupported (+ the critical tag),
|
|
16
|
+
malformed (short fixed part, tag 0xFF)
|
|
17
|
+
- request tails: unknown critical -> rejected unsupported, unknown non-critical -> listed in the result's ignored TLV
|
|
18
|
+
(0x7F); `tail=` appends TLVs to every result that may carry them, so hosts can be checked to skip what they do not
|
|
19
|
+
know
|
|
20
|
+
- the plan (plan_apply / plan_release; the session's plan goes when the lease lapses), and simulations of the
|
|
21
|
+
interfaces the profiles offer: oep.wire.rvswd / swio (scan, attach on declared pin pairs, several connections up
|
|
22
|
+
to max_connections, the seat rule, connections), oep.target.riscv-dm on one `FakeTarget` per pin pair,
|
|
23
|
+
oep.target.console streams, oep.fixture.gpio, oep.fixture.uart, and oep.probe.config (plan / label / idle / slot /
|
|
24
|
+
bind items, get / set / save / erase, slot_state and bind_state)
|
|
25
|
+
- the serial ports' raw side (core §3.4, probe.config §1.2): `port_input` / `port_output` carry the bytes outside
|
|
26
|
+
the frames for each serial port by its bind; a port the lock holder's requests came in on is held until the
|
|
27
|
+
session ends, then resumes from the session's last host reset. The byte framing itself is `fake_serial`.
|
|
28
|
+
|
|
29
|
+
Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
|
|
30
|
+
nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import re
|
|
36
|
+
import struct
|
|
37
|
+
import zlib
|
|
38
|
+
from collections import OrderedDict
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import Callable
|
|
41
|
+
|
|
42
|
+
from . import catalog, fake, message as m, registry as reg
|
|
43
|
+
|
|
44
|
+
TOY_WRITE, TOY_READ = 0x01, 0x02
|
|
45
|
+
OK, WAIT, LINE, FAULT, TIMEOUT, STATE = (reg.STATUS[k] for k in ("ok", "wait", "line", "fault", "timeout", "state"))
|
|
46
|
+
_RV, _CON, _GPIO, _UART, _CFG = (reg.TARGET_RISCV_DM, reg.TARGET_CONSOLE, reg.FIXTURE_GPIO, reg.FIXTURE_UART,
|
|
47
|
+
reg.PROBE_CONFIG)
|
|
48
|
+
STEP = _RV.enum["dmi_step"]
|
|
49
|
+
STEP_ARGS = {STEP["write"]: "BI", STEP["read"]: "B", STEP["poll_reads"]: "BIIH", STEP["wait_us"]: "I",
|
|
50
|
+
STEP["poll_us"]: "BIII"}
|
|
51
|
+
MARK = _CON.enum["mark_kind"]
|
|
52
|
+
ITEM = _CFG.tlv["item"]
|
|
53
|
+
CFG_DESCRIBE = _CFG.tlv["describe"]
|
|
54
|
+
SLOT_ATTACH = _CFG.enum["slot_attach"]
|
|
55
|
+
SLOT_STATE = _CFG.enum["slot_state"]
|
|
56
|
+
BIND_MODE = _CFG.enum["bind_mode"]
|
|
57
|
+
BIND_STREAM = _CFG.enum["bind_stream"]
|
|
58
|
+
BIND_FLOW = _CFG.enum["bind_flow"]
|
|
59
|
+
WIRES = ("oep.wire.rvswd", "oep.wire.swio")
|
|
60
|
+
OWNER = reg.CORE.tlv["open"]["owner"]
|
|
61
|
+
SLOT_NAME = re.compile(r"[a-z0-9_-]{1,32}")
|
|
62
|
+
NO_SLOT, NEVER = 0xFF, 0xFFFFFFFF
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Reject(Exception):
|
|
66
|
+
def __init__(self, reason: int, payload: bytes = b""):
|
|
67
|
+
self.reason, self.payload = reason, payload
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Take:
|
|
71
|
+
"""Reads a request's fixed part; too short -> rejected malformed. `tail(known)` applies the request-tail rule."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, payload: bytes):
|
|
74
|
+
self.data, self.at = payload, 0
|
|
75
|
+
|
|
76
|
+
def take(self, fmt: str):
|
|
77
|
+
size = struct.calcsize("<" + fmt)
|
|
78
|
+
if self.at + size > len(self.data):
|
|
79
|
+
raise Reject(m.MALFORMED)
|
|
80
|
+
v = struct.unpack_from("<" + fmt, self.data, self.at)
|
|
81
|
+
self.at += size
|
|
82
|
+
return v if len(v) > 1 else v[0]
|
|
83
|
+
|
|
84
|
+
def bytes(self, n: int) -> bytes:
|
|
85
|
+
if self.at + n > len(self.data):
|
|
86
|
+
raise Reject(m.MALFORMED)
|
|
87
|
+
out = self.data[self.at:self.at + n]
|
|
88
|
+
self.at += n
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
def tail(self, known: set[int] = frozenset()) -> tuple[dict[int, bytes], list[int]]:
|
|
92
|
+
"""-> (known tags without the critical bit -> value, ignored non-critical tags)."""
|
|
93
|
+
rest, at, got, ignored = self.data[self.at:], 0, {}, []
|
|
94
|
+
self.critical = set() # known tags that came with the critical bit
|
|
95
|
+
while at < len(rest):
|
|
96
|
+
if at + 2 > len(rest) or at + 2 + rest[at + 1] > len(rest):
|
|
97
|
+
raise Reject(m.MALFORMED)
|
|
98
|
+
tag, value = rest[at], rest[at + 2:at + 2 + rest[at + 1]]
|
|
99
|
+
at += 2 + rest[at + 1]
|
|
100
|
+
if tag == m.TAG_INVALID or tag == m.TAG_IGNORED:
|
|
101
|
+
raise Reject(m.MALFORMED)
|
|
102
|
+
if tag & 0x7F in known:
|
|
103
|
+
got[tag & 0x7F] = value
|
|
104
|
+
if tag & m.TAG_CRITICAL:
|
|
105
|
+
self.critical.add(tag & 0x7F)
|
|
106
|
+
elif tag & m.TAG_CRITICAL:
|
|
107
|
+
raise Reject(m.UNSUPPORTED, bytes([tag]))
|
|
108
|
+
else:
|
|
109
|
+
ignored.append(tag)
|
|
110
|
+
self.at = len(self.data)
|
|
111
|
+
return got, ignored
|
|
112
|
+
|
|
113
|
+
def refuse(self, tag: int, got: dict[int, bytes], ignored: list[int]) -> None:
|
|
114
|
+
"""A known TLV whose value cannot be honoured: critical -> unsupported with the tag as received, else it is
|
|
115
|
+
dropped and listed as ignored."""
|
|
116
|
+
if tag in self.critical:
|
|
117
|
+
raise Reject(m.UNSUPPORTED, bytes([tag | m.TAG_CRITICAL]))
|
|
118
|
+
got.pop(tag, None)
|
|
119
|
+
ignored.append(tag)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class FakeTarget:
|
|
124
|
+
"""A RISC-V hart behind a debug module, as far as the riscv-dm operations see it."""
|
|
125
|
+
halted: bool = False
|
|
126
|
+
dpc: int = 0x100
|
|
127
|
+
reset_vector: int = 0
|
|
128
|
+
mem: dict = field(default_factory=dict) # word address -> value
|
|
129
|
+
dmi: dict = field(default_factory=dict) # DMI address -> value (what a read returns)
|
|
130
|
+
dmi_reads: dict = field(default_factory=dict) # DMI address -> list of values the next reads return
|
|
131
|
+
fail_write: set = field(default_factory=set) # DMI addresses whose write fails on the line
|
|
132
|
+
fault_at: set = field(default_factory=set) # word addresses a block access faults on
|
|
133
|
+
regs: dict = field(default_factory=dict) # regno -> value
|
|
134
|
+
havereset: bool = True
|
|
135
|
+
resume_misses: int = 0 # resumes that do not take (status state, dpc unchanged)
|
|
136
|
+
present: bool = True # something answers on this pin pair (scan, attach)
|
|
137
|
+
target_id: int | None = None # the wch_dmi_7f target_id attach reports (None: none)
|
|
138
|
+
# run(pc, regs) -> (stopped, dpc, elapsed_us); default: halts 0x10 past the start
|
|
139
|
+
run_hook: Callable | None = None
|
|
140
|
+
|
|
141
|
+
def dmstatus(self) -> int:
|
|
142
|
+
return 0x82 | ((0x300 if self.halted else 0xC00)) | (0xC0000 if self.havereset else 0)
|
|
143
|
+
|
|
144
|
+
def read_dmi(self, address: int) -> int:
|
|
145
|
+
queue = self.dmi_reads.get(address)
|
|
146
|
+
if queue:
|
|
147
|
+
self.dmi[address] = queue.pop(0)
|
|
148
|
+
return self.dmi.get(address, 0)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class Stream:
|
|
153
|
+
"""A position stream (common §1): bytes from position `base`, marks with serials."""
|
|
154
|
+
data: bytearray = field(default_factory=bytearray)
|
|
155
|
+
base: int = 0
|
|
156
|
+
marks: list = field(default_factory=list) # (serial, position, kind, time_ms, detail)
|
|
157
|
+
serial: int = 0
|
|
158
|
+
closed: bool = False
|
|
159
|
+
written: bytearray = field(default_factory=bytearray)
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def end(self) -> int:
|
|
163
|
+
return self.base + len(self.data)
|
|
164
|
+
|
|
165
|
+
def add_mark(self, kind: int, time_ms: int, detail: int = 0) -> None:
|
|
166
|
+
self.marks.append((self.serial, self.end, kind, time_ms, detail))
|
|
167
|
+
self.serial += 1
|
|
168
|
+
|
|
169
|
+
def drop_oldest(self, n: int) -> None:
|
|
170
|
+
del self.data[:n]
|
|
171
|
+
self.base += n
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@dataclass
|
|
175
|
+
class Connection:
|
|
176
|
+
"""A debug connection (common §2): made by a wire's attach, open while anything uses it."""
|
|
177
|
+
fn: int
|
|
178
|
+
pair: tuple[int, int]
|
|
179
|
+
order: int # creation order (the seat rule closes the oldest)
|
|
180
|
+
speed: int = 4_000_000
|
|
181
|
+
tid: int | None = None
|
|
182
|
+
users: set = field(default_factory=set) # "host" and/or ("slot", n)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class Slot:
|
|
187
|
+
slot: int
|
|
188
|
+
wire_fn: int
|
|
189
|
+
pair: tuple[int, int]
|
|
190
|
+
attach: int
|
|
191
|
+
retry_s: int
|
|
192
|
+
mechanism: int
|
|
193
|
+
name: str
|
|
194
|
+
lock: tuple[int, bytes, bytes] | None # (scheme, mask, value)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True)
|
|
198
|
+
class Bind:
|
|
199
|
+
port: int
|
|
200
|
+
mode: int
|
|
201
|
+
selected: int
|
|
202
|
+
streams: tuple[tuple[int, int], ...] # (kind, id)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@dataclass
|
|
206
|
+
class SlotRuntime:
|
|
207
|
+
last_try_ms: int | None = None
|
|
208
|
+
evicted: bool = False # the seat rule closed its connection: no retry until a new cue
|
|
209
|
+
mismatch_tid: int | None = None # what the last automatic attach saw when the lock did not match
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class Flow:
|
|
214
|
+
"""One stream as a serial port's bind carries it: the stream id and the port's position in it."""
|
|
215
|
+
sid: object = None
|
|
216
|
+
pos: int = 0
|
|
217
|
+
line: bytearray = field(default_factory=bytearray)
|
|
218
|
+
last_ms: int = 0
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class Endpoint:
|
|
222
|
+
MARKS_PER_ANSWER = 3 # small, so hosts must follow `more`
|
|
223
|
+
CHUNK = 64 # raw bytes a serial port takes at a time (probe guide §3.6)
|
|
224
|
+
MIXED_LINE_MAX, MIXED_QUIET_MS = 128, 100
|
|
225
|
+
|
|
226
|
+
def __init__(self, probe: fake.FakeProbe, now_ms: Callable[[], int], boot_id: int = 0x1234ABCD,
|
|
227
|
+
lease_default_ms: int = 3000, lease_max_ms: int = 600000, revision: int = 1, tail: bytes = b"",
|
|
228
|
+
window: int = 1 << 18, max_inflight: int = 4, remember_max: int = 72):
|
|
229
|
+
self.probe = probe
|
|
230
|
+
self.now = now_ms
|
|
231
|
+
self.boot_id = boot_id
|
|
232
|
+
self.lease_default_ms = lease_default_ms
|
|
233
|
+
self.lease_max_ms = lease_max_ms
|
|
234
|
+
self.revision = revision
|
|
235
|
+
self.tail = tail
|
|
236
|
+
self.window, self.max_inflight = window, max_inflight
|
|
237
|
+
self.remember_max = remember_max
|
|
238
|
+
self.names = {o.fn: o.name for o in probe.offered}
|
|
239
|
+
self.fns = {name: fn for fn, name in sorted(self.names.items(), reverse=True)} # first fn of each name
|
|
240
|
+
self.static = {o.fn: o.tlvs for o in probe.offered}
|
|
241
|
+
self.static_labels: dict[int, str] = {}
|
|
242
|
+
self.transports: list[int] = []
|
|
243
|
+
for t in self.static.get(0, ()):
|
|
244
|
+
if t[0] == fake.CORE_LABEL:
|
|
245
|
+
self.static_labels[struct.unpack_from("<H", t, 2)[0]] = t[4:2 + t[1]].decode()
|
|
246
|
+
if t[0] == fake.CORE_TRANSPORT:
|
|
247
|
+
self.transports.append(t[3])
|
|
248
|
+
self.serial_ports = {i for i, k in enumerate(self.transports) if k in fake.SERIAL_KINDS}
|
|
249
|
+
self.pairs: dict[int, list[tuple[int, int]]] = {} # wire fn -> allowed (swdio, swclk), declared order
|
|
250
|
+
self.max_connections: dict[int, int] = {}
|
|
251
|
+
for fn, name in self.names.items():
|
|
252
|
+
if name in WIRES:
|
|
253
|
+
self.pairs[fn] = [self._group_pair(name, t) for t in self.static[fn] if t[0] == catalog.CHANNEL_GROUP]
|
|
254
|
+
self.max_connections[fn] = next((t[2] for t in self.static[fn] if t[0] == fake.MAX_CONNECTIONS), 1)
|
|
255
|
+
self.targets: dict[tuple[int, tuple[int, int]], FakeTarget] = {
|
|
256
|
+
(fn, p): FakeTarget() for fn in sorted(self.pairs) for p in self.pairs[fn]}
|
|
257
|
+
self.target = next(iter(self.targets.values()), FakeTarget())
|
|
258
|
+
self.mechanisms = set()
|
|
259
|
+
for fn, name in self.names.items():
|
|
260
|
+
if name == "oep.target.console":
|
|
261
|
+
self.mechanisms |= {b for t in self.static[fn] if t[0] == fake.MECHANISMS for b in t[2:2 + t[1]]}
|
|
262
|
+
self.block_max = {fn: next((struct.unpack_from("<H", t, 2)[0] for t in self.static[fn]
|
|
263
|
+
if t[0] == catalog.MAX_LENGTH), 1 << 16)
|
|
264
|
+
for fn, name in self.names.items() if name == "oep.target.riscv-dm"}
|
|
265
|
+
cfg_fn = self.fns.get("oep.probe.config")
|
|
266
|
+
cfg = {t[0]: t[2:2 + t[1]] for t in self.static.get(cfg_fn, ())}
|
|
267
|
+
self.slots_max = cfg[CFG_DESCRIBE["slots_max"]][0] if CFG_DESCRIBE["slots_max"] in cfg else 0
|
|
268
|
+
self.bind_modes = cfg[CFG_DESCRIBE["bind_modes"]][0] if CFG_DESCRIBE["bind_modes"] in cfg else 0
|
|
269
|
+
self.items = set(cfg.get(CFG_DESCRIBE["items"], b""))
|
|
270
|
+
self.storage_max = struct.unpack_from("<I", cfg[CFG_DESCRIBE["storage"]])[0] if CFG_DESCRIBE["storage"] in cfg else 0
|
|
271
|
+
self.console_accept = 64
|
|
272
|
+
self.uart_accept = 256
|
|
273
|
+
self._boot()
|
|
274
|
+
|
|
275
|
+
def _boot(self) -> None:
|
|
276
|
+
self.holder: int | None = None
|
|
277
|
+
self.last: int | None = None
|
|
278
|
+
self.owner: bytes | None = None
|
|
279
|
+
self.lease_ms = self.lease_default_ms
|
|
280
|
+
self.expires_ms = 0
|
|
281
|
+
self.values: dict[int, int] = {}
|
|
282
|
+
self.dropped = 0 # requests a v0 endpoint dropped (role 0x81)
|
|
283
|
+
self.requests: list[m.Request] = []
|
|
284
|
+
self.subscribed: set[int] = set()
|
|
285
|
+
self.plan: set[tuple[int, int, int]] = set() # (fn, role, channel), from plan_apply and the config
|
|
286
|
+
self.plan_from_config: set[int] = set() # fns whose plan came from the config (not a session's)
|
|
287
|
+
self.resend: OrderedDict[int, tuple[int, int, int, bytes | None]] = OrderedDict()
|
|
288
|
+
self.newest_corr: int | None = None
|
|
289
|
+
self.conns: dict[int, Connection] = {}
|
|
290
|
+
self._next_conn = 1
|
|
291
|
+
self._order = 0
|
|
292
|
+
self.streams: dict[int, Stream] = {} # console stream id -> stream
|
|
293
|
+
self.stream_keys: dict[tuple[int, int], int] = {} # (connection, mechanism) -> stream id
|
|
294
|
+
self.stream_places: dict[int, tuple[int, tuple[int, int]]] = {} # stream id -> (wire fn, pin pair) it was on
|
|
295
|
+
self._next_stream = 1
|
|
296
|
+
self.gpio_modes: dict[int, int] = {}
|
|
297
|
+
self.gpio_inputs: dict[int, int] = {}
|
|
298
|
+
self.gpio_log: list[tuple[int, int]] = []
|
|
299
|
+
self.uarts: dict[int, Stream] = {} # fn -> stream (configured)
|
|
300
|
+
self.uart_baud: dict[int, tuple[int, int]] = {}
|
|
301
|
+
self.uart_tx: dict[int, bytearray] = {} # what a serial port's raw bytes sent out on a fixture UART
|
|
302
|
+
self.config: dict[tuple[int, int], bytes | list[bytes]] = {} # (item tag, key) -> value (plan: list)
|
|
303
|
+
self.saved: dict | None = getattr(self, "saved", None)
|
|
304
|
+
self.slots: dict[int, Slot] = {}
|
|
305
|
+
self.binds: dict[int, Bind] = {}
|
|
306
|
+
self.slot_rt: dict[int, SlotRuntime] = {}
|
|
307
|
+
self.selected: dict[int, int] = {} # port -> selected index (last-reset / manual)
|
|
308
|
+
self.flows: dict[tuple[int, tuple[int, int]], Flow] = {}
|
|
309
|
+
self.mixed_out: dict[int, bytearray] = {}
|
|
310
|
+
self.held_ports: set[int] = set()
|
|
311
|
+
self.session_resets: dict[tuple[int, int], tuple[object, int]] = {} # stream key -> (sid, position)
|
|
312
|
+
if self.saved is not None:
|
|
313
|
+
self._apply_config(dict(self.saved), boot=True)
|
|
314
|
+
|
|
315
|
+
@property
|
|
316
|
+
def target_id(self) -> int | None:
|
|
317
|
+
return self.target.target_id
|
|
318
|
+
|
|
319
|
+
@target_id.setter
|
|
320
|
+
def target_id(self, value: int | None) -> None:
|
|
321
|
+
self.target.target_id = value
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def _group_pair(name: str, t: bytes) -> tuple[int, int]:
|
|
325
|
+
roles = {t[3 + 3 * i]: struct.unpack_from("<H", t, 4 + 3 * i)[0] for i in range((t[1] - 1) // 3)}
|
|
326
|
+
return roles.get(1, 0xFFFF), roles.get(2, 0xFFFF) if name != "oep.wire.swio" else 0xFFFF
|
|
327
|
+
|
|
328
|
+
# ---- the one entry point: a request message in, a result message out ------------------------
|
|
329
|
+
def handle(self, data: bytes, transport: int = 0) -> bytes | None:
|
|
330
|
+
"""A request from transport `transport` (the index in the describe's transport list) -> its result."""
|
|
331
|
+
if self.revision == 0 and data and data[0] & m.ROLE_SESSION:
|
|
332
|
+
self.dropped += 1 # a v0 probe: unknown role, no answer
|
|
333
|
+
return None
|
|
334
|
+
req = m.Request.unpack(data)
|
|
335
|
+
self.requests.append(req)
|
|
336
|
+
remembered = self._resent(req)
|
|
337
|
+
if remembered is not None:
|
|
338
|
+
return remembered
|
|
339
|
+
try:
|
|
340
|
+
res, detail, payload = self._dispatch(req)
|
|
341
|
+
except Reject as r:
|
|
342
|
+
res, detail, payload = m.REJECTED, r.reason, r.payload
|
|
343
|
+
if res != m.REJECTED and not self._closed_tail(req.fn, req.op) and self.revision >= 1:
|
|
344
|
+
payload += self.tail
|
|
345
|
+
if req.session is not None and req.session == self.holder:
|
|
346
|
+
self.expires_ms = self.now() + self.lease_ms # watchdog, counted from completion
|
|
347
|
+
took_lock = req.fn == m.CORE_FN and req.op == m.OP_OPEN and res == m.COMPLETED
|
|
348
|
+
if self.holder is not None and transport in self.serial_ports and (
|
|
349
|
+
took_lock or (req.session is not None and req.session == self.holder)):
|
|
350
|
+
self.held_ports.add(transport) # core §3.4: the raw transfer holds here
|
|
351
|
+
out = m.Result(req.corr, res, detail, payload).pack()
|
|
352
|
+
if req.session is not None and req.session == self.last:
|
|
353
|
+
self._remember(req, out)
|
|
354
|
+
return out
|
|
355
|
+
|
|
356
|
+
def _interface(self, fn: int):
|
|
357
|
+
return reg.INTERFACES.get(self.names.get(fn, ""))
|
|
358
|
+
|
|
359
|
+
def _closed_tail(self, fn: int, op: int) -> bool:
|
|
360
|
+
i = self._interface(fn)
|
|
361
|
+
return bool(i and op in i.closed_tail)
|
|
362
|
+
|
|
363
|
+
def _lock_free(self, fn: int, op: int) -> bool:
|
|
364
|
+
i = self._interface(fn)
|
|
365
|
+
if fn == m.CORE_FN:
|
|
366
|
+
return op in reg.CORE.lock_free
|
|
367
|
+
if i is None or self.names.get(fn) not in SIMS:
|
|
368
|
+
return op == TOY_READ
|
|
369
|
+
return op in i.lock_free
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _answer(payload: bytes, ignored: list[int], detail: int = m.SUCCESS) -> tuple[int, int, bytes]:
|
|
373
|
+
"""A completed result; the request's ignored non-critical tags follow in TLV 0x7F."""
|
|
374
|
+
return m.COMPLETED, detail, payload + (bytes([m.TAG_IGNORED, len(ignored)]) + bytes(ignored) if ignored else b"")
|
|
375
|
+
|
|
376
|
+
def _dispatch(self, req: m.Request) -> tuple[int, int, bytes]:
|
|
377
|
+
self._lapse()
|
|
378
|
+
if req.fn == m.CORE_FN and req.op == m.OP_CONFIRM:
|
|
379
|
+
return self._confirm(req.payload)
|
|
380
|
+
if req.fn == m.CORE_FN and req.op == m.OP_OPEN:
|
|
381
|
+
return self._open(req.payload)
|
|
382
|
+
if req.fn != m.CORE_FN and req.fn not in self.names:
|
|
383
|
+
return m.REJECTED, m.UNKNOWN_FUNCTION, b""
|
|
384
|
+
if not self._lock_free(req.fn, req.op):
|
|
385
|
+
refused = self._check(req.session)
|
|
386
|
+
if refused:
|
|
387
|
+
return refused
|
|
388
|
+
if req.fn == m.CORE_FN:
|
|
389
|
+
return self._core(req)
|
|
390
|
+
sim = SIMS.get(self.names[req.fn])
|
|
391
|
+
if sim is not None:
|
|
392
|
+
return getattr(self, f"_{sim}")(req.fn, req.op, Take(req.payload))
|
|
393
|
+
return self._toy(req)
|
|
394
|
+
|
|
395
|
+
# ---- the resend table (core §5.2) -----------------------------------------------------------
|
|
396
|
+
def _resent(self, req: m.Request) -> bytes | None:
|
|
397
|
+
"""A request of the last session seen before: its remembered result, corr_reused or result_lost; None = new."""
|
|
398
|
+
if req.session is None or req.session != self.last:
|
|
399
|
+
return None
|
|
400
|
+
entry = self.resend.get(req.corr)
|
|
401
|
+
if entry is not None:
|
|
402
|
+
fn, op, crc, result = entry
|
|
403
|
+
if (fn, op, crc) != (req.fn, req.op, zlib.crc32(req.payload)):
|
|
404
|
+
return m.Result(req.corr, m.REJECTED, m.CORR_REUSED).pack()
|
|
405
|
+
return result if result is not None else m.Result(req.corr, m.REJECTED, m.RESULT_LOST).pack()
|
|
406
|
+
if self.newest_corr is not None and m.serial_diff(req.corr, self.newest_corr, 16) <= 0:
|
|
407
|
+
return m.Result(req.corr, m.REJECTED, m.RESULT_LOST).pack()
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def _remember(self, req: m.Request, result: bytes) -> None:
|
|
411
|
+
self.resend[req.corr] = (req.fn, req.op, zlib.crc32(req.payload),
|
|
412
|
+
result if len(result) <= self.remember_max else None)
|
|
413
|
+
self.resend.move_to_end(req.corr)
|
|
414
|
+
while len(self.resend) > 2 * max(self.max_inflight, 4):
|
|
415
|
+
self.resend.popitem(last=False)
|
|
416
|
+
if self.newest_corr is None or m.serial_diff(req.corr, self.newest_corr, 16) > 0:
|
|
417
|
+
self.newest_corr = req.corr
|
|
418
|
+
|
|
419
|
+
# ---- the stand-in operations ----------------------------------------------------------------
|
|
420
|
+
def _toy(self, req: m.Request) -> tuple[int, int, bytes]:
|
|
421
|
+
t = Take(req.payload)
|
|
422
|
+
if req.op == TOY_READ:
|
|
423
|
+
_, ignored = t.tail()
|
|
424
|
+
return self._answer(struct.pack("<I", self.values.get(req.fn, 0)), ignored)
|
|
425
|
+
if req.op == TOY_WRITE:
|
|
426
|
+
value = t.take("I")
|
|
427
|
+
_, ignored = t.tail()
|
|
428
|
+
self.values[req.fn] = value
|
|
429
|
+
return self._answer(b"", ignored)
|
|
430
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
431
|
+
|
|
432
|
+
# ---- the lock -------------------------------------------------------------------------------
|
|
433
|
+
def _lapse(self) -> None:
|
|
434
|
+
if self.holder is not None and self.now() >= self.expires_ms:
|
|
435
|
+
self._release_lock(taken=True) # the lock goes, the last id stays
|
|
436
|
+
|
|
437
|
+
def _release_lock(self, taken: bool) -> None:
|
|
438
|
+
"""end (taken False) keeps the session's resources for the next open; a lapse or force (taken True) drops
|
|
439
|
+
them (core §9)."""
|
|
440
|
+
self.holder = None
|
|
441
|
+
self.subscribed.clear() # subscriptions end with the lock
|
|
442
|
+
if taken:
|
|
443
|
+
for fn in {a[0] for a in self.plan} - self.plan_from_config:
|
|
444
|
+
self._drop_plan(fn)
|
|
445
|
+
for cid, c in list(self.conns.items()):
|
|
446
|
+
c.users.discard("host")
|
|
447
|
+
if not c.users:
|
|
448
|
+
self._close_conn(cid, MARK["detach"])
|
|
449
|
+
self._refresh()
|
|
450
|
+
self._session_over()
|
|
451
|
+
|
|
452
|
+
def _remaining(self) -> int:
|
|
453
|
+
return max(0, self.expires_ms - self.now()) if self.holder is not None else 0
|
|
454
|
+
|
|
455
|
+
def _locked(self) -> tuple[int, int, bytes]:
|
|
456
|
+
owner = m.tlv(reg.CORE.tlv["locked_payload"]["owner"], self.owner) if self.owner else b""
|
|
457
|
+
return m.REJECTED, m.LOCKED, struct.pack("<I", self._remaining()) + owner
|
|
458
|
+
|
|
459
|
+
def _check(self, session: int | None) -> tuple[int, int, bytes] | None:
|
|
460
|
+
if session is None:
|
|
461
|
+
return m.REJECTED, m.SESSION_REQUIRED, b""
|
|
462
|
+
if self.holder is None:
|
|
463
|
+
if session == self.last:
|
|
464
|
+
self.holder = session # resume: nobody else came in between
|
|
465
|
+
return None
|
|
466
|
+
return m.REJECTED, m.NO_SESSION, b""
|
|
467
|
+
if session == self.holder:
|
|
468
|
+
return None
|
|
469
|
+
return self._locked()
|
|
470
|
+
|
|
471
|
+
def _open(self, payload: bytes) -> tuple[int, int, bytes]:
|
|
472
|
+
t = Take(payload)
|
|
473
|
+
session, lease, force = t.take("IIB")
|
|
474
|
+
got, ignored = t.tail({OWNER})
|
|
475
|
+
owner = got.get(OWNER)
|
|
476
|
+
if owner is not None and not 1 <= len(owner) <= 32:
|
|
477
|
+
t.refuse(OWNER, got, ignored)
|
|
478
|
+
owner = None
|
|
479
|
+
if self.holder is not None and self.holder != session:
|
|
480
|
+
if not force:
|
|
481
|
+
return self._locked()
|
|
482
|
+
self._release_lock(taken=True) # force: the old session is cleaned up first
|
|
483
|
+
resumed = session in (self.holder, self.last)
|
|
484
|
+
if session != self.holder:
|
|
485
|
+
self.subscribed.clear()
|
|
486
|
+
if session != self.last:
|
|
487
|
+
self.owner = None
|
|
488
|
+
if owner is not None:
|
|
489
|
+
self.owner = owner
|
|
490
|
+
self.holder = self.last = session
|
|
491
|
+
self.resend.clear()
|
|
492
|
+
self.newest_corr = None
|
|
493
|
+
if lease == 0:
|
|
494
|
+
self.lease_ms = self.lease_default_ms
|
|
495
|
+
else:
|
|
496
|
+
self.lease_ms = min(lease, max(self.lease_max_ms, 60000))
|
|
497
|
+
self.expires_ms = self.now() + self.lease_ms
|
|
498
|
+
return self._answer(struct.pack("<IIB", self.lease_ms, self.boot_id, int(resumed)), ignored)
|
|
499
|
+
|
|
500
|
+
def reboot(self, boot_id: int) -> None:
|
|
501
|
+
"""The probe restarts: lock, last id, connections, streams, the unsaved config and the plan are gone; the
|
|
502
|
+
saved config comes back."""
|
|
503
|
+
self.boot_id = boot_id
|
|
504
|
+
self._boot()
|
|
505
|
+
|
|
506
|
+
def lose_connections(self) -> None:
|
|
507
|
+
"""A wire drops every connection; their console streams close with a link-lost mark."""
|
|
508
|
+
for cid in list(self.conns):
|
|
509
|
+
self._close_conn(cid, MARK["link_lost"])
|
|
510
|
+
self._refresh()
|
|
511
|
+
|
|
512
|
+
# ---- core -----------------------------------------------------------------------------------
|
|
513
|
+
def _confirm(self, payload: bytes) -> tuple[int, int, bytes]:
|
|
514
|
+
t = Take(payload)
|
|
515
|
+
magic, lo, hi = t.bytes(4), *t.take("BB")
|
|
516
|
+
if magic != m.CONFIRM_REQUEST:
|
|
517
|
+
return m.REJECTED, m.MALFORMED, b""
|
|
518
|
+
if self.revision == 0: # v0 shape: max_frame(16) window(16) inflight flags
|
|
519
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<4sBHHBB", m.CONFIRM_RESULT, 0, self.probe.max_frame,
|
|
520
|
+
min(self.window, 0xFFFF), self.max_inflight, 0)
|
|
521
|
+
_, ignored = t.tail()
|
|
522
|
+
if not lo <= self.revision <= hi:
|
|
523
|
+
return m.REJECTED, m.UNSUPPORTED, b""
|
|
524
|
+
return self._answer(struct.pack("<4sBBHIB", m.CONFIRM_RESULT, self.revision, 0, self.probe.max_frame,
|
|
525
|
+
self.window, self.max_inflight), ignored)
|
|
526
|
+
|
|
527
|
+
def _core(self, req: m.Request) -> tuple[int, int, bytes]:
|
|
528
|
+
op, t = req.op, Take(req.payload)
|
|
529
|
+
if op == m.OP_LIST:
|
|
530
|
+
try:
|
|
531
|
+
return m.COMPLETED, m.SUCCESS, self.probe.call(m.CORE_FN, op, req.payload)
|
|
532
|
+
except ValueError:
|
|
533
|
+
return m.REJECTED, m.MALFORMED, b""
|
|
534
|
+
if op == m.OP_DESCRIBE:
|
|
535
|
+
fn, first = t.take("HH")
|
|
536
|
+
if fn not in self.names:
|
|
537
|
+
return m.REJECTED, m.MALFORMED, b""
|
|
538
|
+
return m.COMPLETED, m.SUCCESS, self._page(self._declarations(fn), first)
|
|
539
|
+
if op == m.OP_LOCK_STATE:
|
|
540
|
+
_, ignored = t.tail()
|
|
541
|
+
owner = (m.tlv(reg.CORE.tlv["lock_state_answer"]["owner"], self.owner)
|
|
542
|
+
if self.owner and self.holder is not None else b"")
|
|
543
|
+
return self._answer(struct.pack("<BI", int(self.holder is not None), self._remaining()) + owner, ignored)
|
|
544
|
+
if op == m.OP_LINK_SOURCE:
|
|
545
|
+
n = min(t.take("I"), self.probe.max_frame - m.RESULT_HEADER)
|
|
546
|
+
return m.COMPLETED, m.SUCCESS, bytes(k & 0xFF for k in range(n))
|
|
547
|
+
if op == m.OP_LINK_SINK:
|
|
548
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<I", len(req.payload))
|
|
549
|
+
if op == m.OP_END:
|
|
550
|
+
t.tail()
|
|
551
|
+
self._release_lock(taken=False)
|
|
552
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
553
|
+
if op == m.OP_KEEPALIVE:
|
|
554
|
+
_, ignored = t.tail()
|
|
555
|
+
return self._answer(b"", ignored)
|
|
556
|
+
if op == m.OP_SUBSCRIBE:
|
|
557
|
+
fn = t.take("H")
|
|
558
|
+
if t.at < len(t.data):
|
|
559
|
+
t.take("HH")
|
|
560
|
+
t.tail()
|
|
561
|
+
if fn != 0 and fn not in self.names:
|
|
562
|
+
return m.REJECTED, m.UNAVAILABLE, b""
|
|
563
|
+
self.subscribed.add(fn)
|
|
564
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
565
|
+
if op == m.OP_UNSUBSCRIBE:
|
|
566
|
+
fn = t.take("H")
|
|
567
|
+
t.tail()
|
|
568
|
+
self.subscribed.discard(fn)
|
|
569
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
570
|
+
if op == m.OP_PLAN_APPLY:
|
|
571
|
+
got = []
|
|
572
|
+
for tag, value in m.split_tlvs(req.payload) if req.payload else []:
|
|
573
|
+
if tag == reg.CORE.tlv["plan_apply"]["role_assignment"] and len(value) >= 5:
|
|
574
|
+
got.append(struct.unpack_from("<HBH", value))
|
|
575
|
+
elif tag & m.TAG_CRITICAL:
|
|
576
|
+
return m.REJECTED, m.UNSUPPORTED, bytes([tag])
|
|
577
|
+
self._check_plan(got)
|
|
578
|
+
named = {fn for fn, _, _ in got}
|
|
579
|
+
self.plan = {a for a in self.plan if a[0] not in named} | set(got)
|
|
580
|
+
self.plan_from_config -= named
|
|
581
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
582
|
+
if op == m.OP_PLAN_RELEASE: # n(u8) n x fn(u16); n = 0: every fn
|
|
583
|
+
n = t.take("B")
|
|
584
|
+
fns = {t.take("H") for _ in range(n)}
|
|
585
|
+
t.tail()
|
|
586
|
+
for fn in {a[0] for a in self.plan if not fns or a[0] in fns}:
|
|
587
|
+
self._drop_plan(fn)
|
|
588
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
589
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
590
|
+
|
|
591
|
+
def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
|
|
592
|
+
"""plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
|
|
593
|
+
named = {fn for fn, _, _ in got}
|
|
594
|
+
kept = {a for a in self.plan if a[0] not in named}
|
|
595
|
+
slot_pins = {p for s in self.slots.values() for p in s.pair if p != 0xFFFF}
|
|
596
|
+
for fn, role, ch in got:
|
|
597
|
+
roles = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}}.get(self.names.get(fn, ""), set())
|
|
598
|
+
if role not in roles or any(k[2] == ch for k in kept) or ch in slot_pins:
|
|
599
|
+
raise Reject(m.UNAVAILABLE)
|
|
600
|
+
|
|
601
|
+
def _drop_plan(self, fn: int) -> None:
|
|
602
|
+
for a in [a for a in self.plan if a[0] == fn]:
|
|
603
|
+
self.gpio_modes.pop(a[2], None)
|
|
604
|
+
self.plan.discard(a)
|
|
605
|
+
self.uarts.pop(fn, None)
|
|
606
|
+
self.plan_from_config.discard(fn)
|
|
607
|
+
|
|
608
|
+
# ---- describe: the static declarations plus the live ones -----------------------------------
|
|
609
|
+
def _declarations(self, fn: int) -> list[bytes]:
|
|
610
|
+
tlvs = list(self.static[fn])
|
|
611
|
+
if fn == m.CORE_FN:
|
|
612
|
+
labels = dict(self.static_labels)
|
|
613
|
+
for (tag, key), value in self.config.items():
|
|
614
|
+
if tag == ITEM["label"]:
|
|
615
|
+
labels[key] = value[2:].decode("utf-8", "replace")
|
|
616
|
+
tlvs = [t for t in tlvs if t[0] != fake.CORE_LABEL]
|
|
617
|
+
tlvs += [catalog.tlv(fake.CORE_LABEL, struct.pack("<H", ch) + name.encode()) for ch, name in sorted(labels.items())]
|
|
618
|
+
elif self.names[fn] == "oep.probe.config":
|
|
619
|
+
tlvs = [t for t in tlvs if t[0] != CFG_DESCRIBE["storage"]]
|
|
620
|
+
saved_hash = self._hash(self.saved) if self.saved is not None else 0
|
|
621
|
+
tlvs.insert(0, catalog.tlv(CFG_DESCRIBE["storage"], struct.pack(
|
|
622
|
+
"<IBII", self.storage_max, 0 if self.saved is None else 1, saved_hash, 20)))
|
|
623
|
+
tlvs += [catalog.tlv(CFG_DESCRIBE["slot_state"], self._slot_state(n)) for n in sorted(self.slots)]
|
|
624
|
+
tlvs += [catalog.tlv(CFG_DESCRIBE["bind_state"], self._bind_state(p)) for p in sorted(self.binds)]
|
|
625
|
+
return tlvs
|
|
626
|
+
|
|
627
|
+
def _page(self, tlvs: list[bytes], first: int) -> bytes:
|
|
628
|
+
budget = self.probe.max_frame - m.RESULT_HEADER - 1
|
|
629
|
+
out, sent = b"", 0
|
|
630
|
+
for t in tlvs[first:]:
|
|
631
|
+
if out and len(out) + len(t) > budget:
|
|
632
|
+
break
|
|
633
|
+
out += t
|
|
634
|
+
sent += 1
|
|
635
|
+
return bytes([1 if first + sent < len(tlvs) else 0]) + out
|
|
636
|
+
|
|
637
|
+
# ---- oep.wire.rvswd / swio ------------------------------------------------------------------
|
|
638
|
+
def _wire(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
639
|
+
allowed = self.pairs.get(fn, [])
|
|
640
|
+
if op == 0x01: # scan: count(u8) pairs -> tried count found...
|
|
641
|
+
count = t.take("B")
|
|
642
|
+
pairs = [t.take("HH") for _ in range(count)]
|
|
643
|
+
t.tail()
|
|
644
|
+
if any(p not in allowed for p in pairs):
|
|
645
|
+
raise Reject(m.UNAVAILABLE)
|
|
646
|
+
pairs = pairs or allowed
|
|
647
|
+
found = []
|
|
648
|
+
for p in pairs:
|
|
649
|
+
tg = self.targets[(fn, p)]
|
|
650
|
+
if tg.present: # a live connection's pair: read over it, no restart
|
|
651
|
+
found.append(struct.pack("<BHHI", 1, *p, tg.dmstatus()))
|
|
652
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<BB", len(pairs), len(found)) + b"".join(found)
|
|
653
|
+
if op in (0x02, 0x04): # attach, attach_under_reset
|
|
654
|
+
if op == 0x02:
|
|
655
|
+
method = t.take("B")
|
|
656
|
+
if method > 1:
|
|
657
|
+
raise Reject(m.UNSUPPORTED)
|
|
658
|
+
else:
|
|
659
|
+
channel, _hold = t.take("HH")
|
|
660
|
+
if channel not in (0xFFFF, self._channel_named("NRST")):
|
|
661
|
+
raise Reject(m.UNAVAILABLE)
|
|
662
|
+
got, ignored = t.tail({0x01, 0x03})
|
|
663
|
+
pair = self._pick_pair(fn, got)
|
|
664
|
+
tg = self.targets[(fn, pair)]
|
|
665
|
+
speed = min(4_000_000, struct.unpack("<I", got[0x01])[0]) if 0x01 in got else 4_000_000
|
|
666
|
+
cid = self._conn_at(fn, pair)
|
|
667
|
+
flags = 0
|
|
668
|
+
if cid is None:
|
|
669
|
+
if not tg.present:
|
|
670
|
+
return m.COMPLETED, m.FAILED, bytes([LINE])
|
|
671
|
+
cid = self._seat(fn, pair, tg, speed)
|
|
672
|
+
if tg.havereset and op == 0x02:
|
|
673
|
+
tg.havereset, flags = False, flags | 1
|
|
674
|
+
else:
|
|
675
|
+
flags |= 2
|
|
676
|
+
self.conns[cid].speed = min(self.conns[cid].speed, speed)
|
|
677
|
+
c = self.conns[cid]
|
|
678
|
+
c.users.add("host")
|
|
679
|
+
for n, s in self.slots.items(): # a new connection for an evicted slot: a new cue
|
|
680
|
+
if s.wire_fn == fn and s.pair == pair:
|
|
681
|
+
self.slot_rt[n].evicted = False
|
|
682
|
+
tid = b"" if c.tid is None else m.tlv(0x10, bytes([1]) + struct.pack("<I", c.tid))
|
|
683
|
+
if op == 0x04:
|
|
684
|
+
tg.halted, tg.dpc = True, tg.reset_vector
|
|
685
|
+
self._host_reset(cid)
|
|
686
|
+
self._refresh()
|
|
687
|
+
return self._answer(struct.pack("<HII", cid, tg.dpc, c.speed) + tid, ignored)
|
|
688
|
+
if method == 1:
|
|
689
|
+
tg.halted = True
|
|
690
|
+
self._refresh()
|
|
691
|
+
return self._answer(struct.pack("<HIBI", cid, tg.dmstatus(), flags, c.speed) + tid, ignored)
|
|
692
|
+
if op == 0x03: # detach
|
|
693
|
+
cid = t.take("H")
|
|
694
|
+
got, _ = t.tail({0x01})
|
|
695
|
+
c = self.conns.get(cid)
|
|
696
|
+
if c is None or c.fn != fn:
|
|
697
|
+
raise Reject(m.NO_CONNECTION)
|
|
698
|
+
c.users.discard("host")
|
|
699
|
+
if 0x01 in got or not c.users:
|
|
700
|
+
self._close_conn(cid, MARK["detach"])
|
|
701
|
+
self._refresh()
|
|
702
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
703
|
+
if op == 0x05: # connections (lock-free)
|
|
704
|
+
t.tail()
|
|
705
|
+
mine = sorted((c.order, cid) for cid, c in self.conns.items() if c.fn == fn)
|
|
706
|
+
out = bytearray([len(mine)])
|
|
707
|
+
for _, cid in mine:
|
|
708
|
+
c = self.conns[cid]
|
|
709
|
+
users = (1 if "host" in c.users else 0) | (2 if any(u != "host" for u in c.users) else 0)
|
|
710
|
+
slot = next((n for n, s in self.slots.items() if s.wire_fn == fn and s.pair == c.pair), NO_SLOT)
|
|
711
|
+
tid = b"" if c.tid is None else struct.pack("<I", c.tid)
|
|
712
|
+
out += struct.pack("<HHHIBBBB", cid, *c.pair, c.speed, users, slot, 1 if tid else 0, len(tid)) + tid
|
|
713
|
+
return m.COMPLETED, m.SUCCESS, bytes(out)
|
|
714
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
715
|
+
|
|
716
|
+
def _pick_pair(self, fn: int, got: dict[int, bytes]) -> tuple[int, int]:
|
|
717
|
+
allowed = self.pairs.get(fn, [])
|
|
718
|
+
if 0x03 in got:
|
|
719
|
+
if len(got[0x03]) != 4:
|
|
720
|
+
raise Reject(m.MALFORMED)
|
|
721
|
+
pair = struct.unpack("<HH", got[0x03])
|
|
722
|
+
if pair not in allowed:
|
|
723
|
+
raise Reject(m.UNAVAILABLE) # a pair this probe does not allow
|
|
724
|
+
return pair
|
|
725
|
+
if len(allowed) != 1:
|
|
726
|
+
raise Reject(m.UNAVAILABLE) # the host chooses among several
|
|
727
|
+
return allowed[0]
|
|
728
|
+
|
|
729
|
+
def _channel_named(self, name: str) -> int | None:
|
|
730
|
+
labels = dict(self.static_labels)
|
|
731
|
+
for (tag, key), value in self.config.items():
|
|
732
|
+
if tag == ITEM["label"]:
|
|
733
|
+
labels[key] = value[2:].decode("utf-8", "replace")
|
|
734
|
+
return next((ch for ch, n in labels.items() if n == name), None)
|
|
735
|
+
|
|
736
|
+
def _conn_at(self, fn: int, pair: tuple[int, int]) -> int | None:
|
|
737
|
+
return next((cid for cid, c in self.conns.items() if c.fn == fn and c.pair == pair), None)
|
|
738
|
+
|
|
739
|
+
def _seat(self, fn: int, pair: tuple[int, int], tg: FakeTarget, speed: int, evict: bool = True) -> int:
|
|
740
|
+
"""A new connection; a full wire gives up its oldest slot-only connection (debug §1), if `evict`."""
|
|
741
|
+
mine = [(c.order, cid) for cid, c in self.conns.items() if c.fn == fn]
|
|
742
|
+
if len(mine) >= self.max_connections.get(fn, 1):
|
|
743
|
+
slot_only = sorted((o, cid) for o, cid in mine if "host" not in self.conns[cid].users)
|
|
744
|
+
if not evict or not slot_only:
|
|
745
|
+
raise Reject(m.UNAVAILABLE)
|
|
746
|
+
gone = self.conns[slot_only[0][1]]
|
|
747
|
+
for u in gone.users:
|
|
748
|
+
if u != "host":
|
|
749
|
+
self.slot_rt[u[1]].evicted = True
|
|
750
|
+
self._close_conn(slot_only[0][1], MARK["link_lost"])
|
|
751
|
+
if self._next_conn > 0xFFFF:
|
|
752
|
+
raise Reject(m.UNAVAILABLE) # every number used this boot (core §9)
|
|
753
|
+
cid, self._next_conn = self._next_conn, self._next_conn + 1
|
|
754
|
+
self._order += 1
|
|
755
|
+
self.conns[cid] = Connection(fn, pair, self._order, speed, tg.target_id)
|
|
756
|
+
return cid
|
|
757
|
+
|
|
758
|
+
def _close_conn(self, cid: int, mark: int) -> None:
|
|
759
|
+
self.conns.pop(cid, None)
|
|
760
|
+
for (c, _), sid in list(self.stream_keys.items()):
|
|
761
|
+
if c == cid and not self.streams[sid].closed:
|
|
762
|
+
self.streams[sid].add_mark(mark, self.now())
|
|
763
|
+
self.streams[sid].closed = True
|
|
764
|
+
|
|
765
|
+
def _target_of(self, cid: int) -> FakeTarget:
|
|
766
|
+
c = self.conns[cid]
|
|
767
|
+
return self.targets[(c.fn, c.pair)]
|
|
768
|
+
|
|
769
|
+
# ---- oep.target.riscv-dm --------------------------------------------------------------------
|
|
770
|
+
@staticmethod
|
|
771
|
+
def _outcome(status: int, done: int) -> int:
|
|
772
|
+
return m.SUCCESS if status == OK else (m.PARTIAL if done else m.FAILED)
|
|
773
|
+
|
|
774
|
+
def _dm(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
775
|
+
conn = t.take("H")
|
|
776
|
+
if conn not in self.conns:
|
|
777
|
+
raise Reject(m.NO_CONNECTION)
|
|
778
|
+
tg = self._target_of(conn)
|
|
779
|
+
if op == _RV.op["dmi"]:
|
|
780
|
+
n = t.take("H")
|
|
781
|
+
steps = []
|
|
782
|
+
for _ in range(n):
|
|
783
|
+
kind = t.take("B")
|
|
784
|
+
if kind not in STEP_ARGS:
|
|
785
|
+
raise Reject(m.MALFORMED)
|
|
786
|
+
steps.append((kind, t.take(STEP_ARGS[kind])))
|
|
787
|
+
t.tail()
|
|
788
|
+
done, status, values = 0, OK, []
|
|
789
|
+
for kind, args in steps:
|
|
790
|
+
if kind == STEP["write"]:
|
|
791
|
+
address, value = args
|
|
792
|
+
if address in tg.fail_write:
|
|
793
|
+
status = LINE
|
|
794
|
+
break
|
|
795
|
+
tg.dmi[address] = value
|
|
796
|
+
if address == 0x17 and value & 0xFFFF == 0x07B1: # access register dpc: into DATA0, not busy
|
|
797
|
+
tg.dmi[0x04], tg.dmi[0x16] = tg.dpc, 0
|
|
798
|
+
elif kind == STEP["read"]:
|
|
799
|
+
values.append(tg.read_dmi(args))
|
|
800
|
+
elif kind in (STEP["poll_reads"], STEP["poll_us"]):
|
|
801
|
+
address, mask, want, limit = args
|
|
802
|
+
tries = limit if kind == STEP["poll_reads"] else max(1, limit // 100)
|
|
803
|
+
for _ in range(max(1, tries)):
|
|
804
|
+
v = tg.read_dmi(address)
|
|
805
|
+
if v & mask == want:
|
|
806
|
+
break
|
|
807
|
+
values.append(v)
|
|
808
|
+
if v & mask != want:
|
|
809
|
+
status = TIMEOUT
|
|
810
|
+
break
|
|
811
|
+
done += 1
|
|
812
|
+
return m.COMPLETED, self._outcome(status, done), struct.pack(f"<HB{len(values)}I", done, status, *values)
|
|
813
|
+
if op == _RV.op["halt"]:
|
|
814
|
+
t.tail()
|
|
815
|
+
tg.halted = True
|
|
816
|
+
return m.COMPLETED, m.SUCCESS, bytes([OK])
|
|
817
|
+
if op == _RV.op["resume"]:
|
|
818
|
+
t.tail()
|
|
819
|
+
if tg.resume_misses: # a CH32V006 now and then: the request does not take
|
|
820
|
+
tg.resume_misses -= 1
|
|
821
|
+
return m.COMPLETED, m.FAILED, bytes([STATE])
|
|
822
|
+
if tg.halted:
|
|
823
|
+
tg.halted, tg.dpc = False, tg.dpc + 0x40
|
|
824
|
+
return m.COMPLETED, m.SUCCESS, bytes([OK])
|
|
825
|
+
if op == _RV.op["reset"]:
|
|
826
|
+
mode = t.take("B")
|
|
827
|
+
got, ignored = t.tail({_RV.tlv["reset"]["method"]})
|
|
828
|
+
if mode > 2:
|
|
829
|
+
raise Reject(m.UNSUPPORTED)
|
|
830
|
+
method = got.get(_RV.tlv["reset"]["method"])
|
|
831
|
+
if method is not None and (len(method) != 1 or method[0] > 2):
|
|
832
|
+
t.refuse(_RV.tlv["reset"]["method"], got, ignored)
|
|
833
|
+
tg.havereset = True
|
|
834
|
+
tg.halted = mode == 2
|
|
835
|
+
tg.dpc = tg.reset_vector if mode == 2 else tg.reset_vector + 0x200
|
|
836
|
+
# debug §4.3: bit0 reached the mode's state, bit1 confirmed by the pc (mode 1), attempts 1
|
|
837
|
+
flags, pc = (0b01, 0) if mode == 0 else ((0b11, tg.dpc) if mode == 1 else (0b01, tg.dpc))
|
|
838
|
+
self._host_reset(conn)
|
|
839
|
+
return self._answer(struct.pack("<BBBI", OK, flags, 1, pc), ignored)
|
|
840
|
+
if op == _RV.op["step"]:
|
|
841
|
+
t.tail()
|
|
842
|
+
if not tg.halted:
|
|
843
|
+
return m.COMPLETED, m.FAILED, struct.pack("<BBII", STATE, 0, tg.dpc, tg.dpc)
|
|
844
|
+
before = tg.dpc
|
|
845
|
+
tg.dpc += 4
|
|
846
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<BBII", OK, 1, before, tg.dpc)
|
|
847
|
+
if op == _RV.op["read_block"]:
|
|
848
|
+
address, count = t.take("IH")
|
|
849
|
+
t.tail()
|
|
850
|
+
if 4 * count > self.block_max.get(fn, 1 << 16):
|
|
851
|
+
raise Reject(m.MALFORMED) # past the declared max_length (bytes)
|
|
852
|
+
words, status = [], OK
|
|
853
|
+
for i in range(count):
|
|
854
|
+
if address + 4 * i in tg.fault_at:
|
|
855
|
+
status = FAULT
|
|
856
|
+
break
|
|
857
|
+
words.append(tg.mem.get(address + 4 * i, 0))
|
|
858
|
+
return (m.COMPLETED, self._outcome(status, len(words)),
|
|
859
|
+
struct.pack(f"<HB{len(words)}I", len(words), status, *words))
|
|
860
|
+
if op == _RV.op["write_block"]:
|
|
861
|
+
address, count = t.take("IH")
|
|
862
|
+
if 4 * count > self.block_max.get(fn, 1 << 16):
|
|
863
|
+
raise Reject(m.MALFORMED)
|
|
864
|
+
words = struct.unpack(f"<{count}I", t.bytes(4 * count))
|
|
865
|
+
t.tail()
|
|
866
|
+
done, status = 0, OK
|
|
867
|
+
for i, w in enumerate(words):
|
|
868
|
+
if address + 4 * i in tg.fault_at:
|
|
869
|
+
status = FAULT
|
|
870
|
+
break
|
|
871
|
+
tg.mem[address + 4 * i] = w
|
|
872
|
+
done += 1
|
|
873
|
+
return m.COMPLETED, self._outcome(status, done), struct.pack("<HB", done, status)
|
|
874
|
+
if op == _RV.op["run"]:
|
|
875
|
+
pc, timeout_ms, n = t.take("IIB")
|
|
876
|
+
regs = dict(t.take("HI") for _ in range(n))
|
|
877
|
+
n_out = t.take("B")
|
|
878
|
+
outs = [t.take("H") for _ in range(n_out)]
|
|
879
|
+
t.tail()
|
|
880
|
+
if not tg.halted:
|
|
881
|
+
return m.COMPLETED, m.FAILED, struct.pack(f"<BBII{n_out}I", STATE, 0, tg.dpc, 0, *[0] * n_out)
|
|
882
|
+
tg.regs.update(regs)
|
|
883
|
+
stopped, dpc, us = tg.run_hook(pc, tg.regs) if tg.run_hook else (True, pc + 0x10, 50)
|
|
884
|
+
tg.dpc = dpc
|
|
885
|
+
status = OK if stopped else TIMEOUT
|
|
886
|
+
values = [tg.regs.get(r, 0) for r in outs]
|
|
887
|
+
return (m.COMPLETED, m.SUCCESS if stopped else m.FAILED,
|
|
888
|
+
struct.pack(f"<BBII{n_out}I", status, int(stopped), dpc, us, *values))
|
|
889
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
890
|
+
|
|
891
|
+
def _host_reset(self, cid: int) -> None:
|
|
892
|
+
"""A reset last-reset counts (probe.config §1.2): riscv-dm reset or attach_under_reset on connection `cid`."""
|
|
893
|
+
now = self.now()
|
|
894
|
+
slots = [n for n, s in self.slots.items() if (s.wire_fn, s.pair) == (self.conns[cid].fn, self.conns[cid].pair)]
|
|
895
|
+
for (c, _), sid in self.stream_keys.items():
|
|
896
|
+
if c == cid and not self.streams[sid].closed:
|
|
897
|
+
self.streams[sid].add_mark(MARK["reset"], now)
|
|
898
|
+
for port, b in self.binds.items():
|
|
899
|
+
if b.mode == BIND_MODE["last_reset"]:
|
|
900
|
+
for i, key in enumerate(b.streams):
|
|
901
|
+
if key[0] == BIND_STREAM["slot_console"] and key[1] in slots:
|
|
902
|
+
self.selected[port] = i
|
|
903
|
+
if self.holder is not None:
|
|
904
|
+
for n in slots:
|
|
905
|
+
key = (BIND_STREAM["slot_console"], n)
|
|
906
|
+
sid, s = self._stream_for(key)
|
|
907
|
+
if s is not None:
|
|
908
|
+
self.session_resets[key] = (sid, s.end)
|
|
909
|
+
for fn, s in self.uarts.items():
|
|
910
|
+
self.session_resets[(BIND_STREAM["fixture_uart"], fn)] = (("uart", fn), s.end)
|
|
911
|
+
|
|
912
|
+
# ---- position streams (console, fixture.uart) -----------------------------------------------
|
|
913
|
+
def _stream_op(self, s: Stream, op: int, t: Take, accept: int) -> tuple[int, int, bytes]:
|
|
914
|
+
if op == _CON.op["read"]:
|
|
915
|
+
frm, arg, mx = t.take("BQH")
|
|
916
|
+
t.tail()
|
|
917
|
+
if frm == 0:
|
|
918
|
+
pos = arg
|
|
919
|
+
elif frm == 1:
|
|
920
|
+
pos = s.base
|
|
921
|
+
elif frm == 2:
|
|
922
|
+
pos = s.end
|
|
923
|
+
elif frm == 3:
|
|
924
|
+
hits = [mk for mk in s.marks if arg == 0 or mk[2] == arg]
|
|
925
|
+
pos = hits[-1][1] if hits else s.base
|
|
926
|
+
else:
|
|
927
|
+
raise Reject(m.UNSUPPORTED)
|
|
928
|
+
flags = 0
|
|
929
|
+
if pos < s.base:
|
|
930
|
+
pos, flags = s.base, 2
|
|
931
|
+
data = bytes(s.data[pos - s.base:pos - s.base + mx])
|
|
932
|
+
if pos + len(data) < s.end:
|
|
933
|
+
flags |= 1
|
|
934
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<QB", pos, flags) + data
|
|
935
|
+
if op == _CON.op["marks"]:
|
|
936
|
+
frm = t.take("I")
|
|
937
|
+
_, ignored = t.tail()
|
|
938
|
+
hits = [mk for mk in s.marks if m.serial_diff(mk[0], frm) >= 0]
|
|
939
|
+
page = hits[:self.MARKS_PER_ANSWER]
|
|
940
|
+
body = struct.pack("<BB", int(len(hits) > len(page)), len(page))
|
|
941
|
+
body += b"".join(struct.pack("<IQBIB", *mk) for mk in page)
|
|
942
|
+
return self._answer(body, ignored)
|
|
943
|
+
if s.closed:
|
|
944
|
+
raise Reject(m.UNAVAILABLE)
|
|
945
|
+
if op == _CON.op["clear"]:
|
|
946
|
+
t.tail()
|
|
947
|
+
s.drop_oldest(len(s.data))
|
|
948
|
+
s.add_mark(MARK["clear"], self.now())
|
|
949
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
950
|
+
if op == _CON.op["mark"]:
|
|
951
|
+
value = t.take("B")
|
|
952
|
+
t.tail()
|
|
953
|
+
s.add_mark(MARK["host"], self.now(), value)
|
|
954
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
955
|
+
if op == _CON.op["write"]:
|
|
956
|
+
count = t.take("H")
|
|
957
|
+
data = t.bytes(count)
|
|
958
|
+
_, ignored = t.tail()
|
|
959
|
+
took = min(count, accept)
|
|
960
|
+
s.written += data[:took]
|
|
961
|
+
return self._answer(struct.pack("<H", took), ignored, m.SUCCESS if took == count else m.PARTIAL)
|
|
962
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
963
|
+
|
|
964
|
+
# ---- oep.target.console ---------------------------------------------------------------------
|
|
965
|
+
def _console(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
966
|
+
if op == _CON.op["open"]:
|
|
967
|
+
conn, mech = t.take("HB")
|
|
968
|
+
_, ignored = t.tail()
|
|
969
|
+
if conn not in self.conns:
|
|
970
|
+
raise Reject(m.NO_CONNECTION)
|
|
971
|
+
if mech not in self.mechanisms:
|
|
972
|
+
raise Reject(m.UNSUPPORTED)
|
|
973
|
+
sid, existing = self._open_stream(conn, mech)
|
|
974
|
+
return self._answer(struct.pack("<HB", sid, int(existing)), ignored)
|
|
975
|
+
sid = t.take("H")
|
|
976
|
+
s = self.streams.get(sid)
|
|
977
|
+
if s is None:
|
|
978
|
+
raise Reject(m.UNAVAILABLE)
|
|
979
|
+
if op == _CON.op["close"]:
|
|
980
|
+
t.tail()
|
|
981
|
+
s.closed = True
|
|
982
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
983
|
+
return self._stream_op(s, op, t, self.console_accept)
|
|
984
|
+
|
|
985
|
+
def _open_stream(self, conn: int, mech: int) -> tuple[int, bool]:
|
|
986
|
+
sid = self.stream_keys.get((conn, mech))
|
|
987
|
+
if sid is not None and not self.streams[sid].closed:
|
|
988
|
+
return sid, True
|
|
989
|
+
place = (self.conns[conn].fn, self.conns[conn].pair)
|
|
990
|
+
for key in [k for k, v in self.stream_keys.items() # a closed one of this mechanism on this place goes
|
|
991
|
+
if k[1] == mech and self.streams[v].closed and self.stream_places.get(v) == place]:
|
|
992
|
+
sid = self.stream_keys.pop(key)
|
|
993
|
+
self.streams.pop(sid, None)
|
|
994
|
+
self.stream_places.pop(sid, None)
|
|
995
|
+
sid = self._next_stream
|
|
996
|
+
self._next_stream += 1 # never reused within a boot (core §9)
|
|
997
|
+
self.streams[sid], self.stream_keys[(conn, mech)] = Stream(), sid
|
|
998
|
+
self.stream_places[sid] = place
|
|
999
|
+
self.streams[sid].add_mark(MARK["attach"], self.now())
|
|
1000
|
+
return sid, False
|
|
1001
|
+
|
|
1002
|
+
def emit(self, sid: int, data: bytes) -> None:
|
|
1003
|
+
"""The target writes to its console stream `sid`."""
|
|
1004
|
+
self.streams[sid].data += data
|
|
1005
|
+
|
|
1006
|
+
def target_says(self, data: bytes, target: FakeTarget | None = None) -> None:
|
|
1007
|
+
"""The target (the first one by default) writes to its console: every open stream on a connection to it."""
|
|
1008
|
+
target = target or self.target
|
|
1009
|
+
for (cid, _), sid in self.stream_keys.items():
|
|
1010
|
+
if cid in self.conns and self._target_of(cid) is target and not self.streams[sid].closed:
|
|
1011
|
+
self.streams[sid].data += data
|
|
1012
|
+
|
|
1013
|
+
# ---- oep.fixture.gpio -----------------------------------------------------------------------
|
|
1014
|
+
def _gpio(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
1015
|
+
mine = {ch for f, role, ch in self.plan if f == fn and role == 1}
|
|
1016
|
+
if op == _GPIO.op["set"]:
|
|
1017
|
+
n = t.take("B")
|
|
1018
|
+
pairs = [t.take("HB") for _ in range(n)]
|
|
1019
|
+
t.tail()
|
|
1020
|
+
for i, (ch, mode) in enumerate(pairs):
|
|
1021
|
+
if ch not in mine or mode > 7:
|
|
1022
|
+
raise Reject(m.UNAVAILABLE, bytes([i]))
|
|
1023
|
+
for ch, mode in pairs:
|
|
1024
|
+
self.gpio_modes[ch] = mode
|
|
1025
|
+
self.gpio_log.append((ch, mode))
|
|
1026
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
1027
|
+
if op == _GPIO.op["read"]:
|
|
1028
|
+
n = t.take("B")
|
|
1029
|
+
chans = [t.take("H") for _ in range(n)]
|
|
1030
|
+
_, ignored = t.tail()
|
|
1031
|
+
for i, ch in enumerate(chans):
|
|
1032
|
+
if ch not in mine:
|
|
1033
|
+
raise Reject(m.UNAVAILABLE, bytes([i]))
|
|
1034
|
+
levels = []
|
|
1035
|
+
for ch in chans:
|
|
1036
|
+
mode = self.gpio_modes.get(ch, 0)
|
|
1037
|
+
levels.append({1: 1, 3: 0, 4: 1, 5: 0, 6: 1}.get(mode, self.gpio_inputs.get(ch, 0)))
|
|
1038
|
+
return self._answer(bytes(levels), ignored)
|
|
1039
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
1040
|
+
|
|
1041
|
+
# ---- oep.fixture.uart -----------------------------------------------------------------------
|
|
1042
|
+
def _uart(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
1043
|
+
if op == _UART.op["configure"]:
|
|
1044
|
+
baud = t.take("I")
|
|
1045
|
+
got, ignored = t.tail({_UART.tlv["configure"]["format"]})
|
|
1046
|
+
if not any(f == fn for f, _, _ in self.plan) or baud == 0:
|
|
1047
|
+
raise Reject(m.UNAVAILABLE)
|
|
1048
|
+
fmt = got.get(_UART.tlv["configure"]["format"], b"\0")
|
|
1049
|
+
if len(fmt) != 1 or fmt[0] & ~0x1F or fmt[0] & 3 > 1 or (fmt[0] >> 2) & 3 > 2:
|
|
1050
|
+
t.refuse(_UART.tlv["configure"]["format"], got, ignored)
|
|
1051
|
+
fmt = bytes(1)
|
|
1052
|
+
actual = 80_000_000 // (80_000_000 // baud)
|
|
1053
|
+
self.uart_baud[fn] = (actual, fmt[0])
|
|
1054
|
+
self.uarts.setdefault(fn, Stream())
|
|
1055
|
+
return self._answer(struct.pack("<I", actual), ignored)
|
|
1056
|
+
s = self.uarts.get(fn)
|
|
1057
|
+
if s is None:
|
|
1058
|
+
raise Reject(m.UNAVAILABLE)
|
|
1059
|
+
return self._stream_op(s, op, t, self.uart_accept)
|
|
1060
|
+
|
|
1061
|
+
def uart_rx(self, fn: int, data: bytes) -> None:
|
|
1062
|
+
"""Bytes arrive on fixture UART `fn`'s RX."""
|
|
1063
|
+
self.uarts[fn].data += data
|
|
1064
|
+
|
|
1065
|
+
# ---- oep.probe.config -----------------------------------------------------------------------
|
|
1066
|
+
def _config_op(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
1067
|
+
if op == _CFG.op["get"]:
|
|
1068
|
+
first = t.take("H")
|
|
1069
|
+
t.tail()
|
|
1070
|
+
items = self._canonical(self.config)
|
|
1071
|
+
budget = self.probe.max_frame - m.RESULT_HEADER - 5
|
|
1072
|
+
out, sent = b"", 0
|
|
1073
|
+
for item in items[first:]:
|
|
1074
|
+
if out and len(out) + len(item) > budget:
|
|
1075
|
+
break
|
|
1076
|
+
out += item
|
|
1077
|
+
sent += 1
|
|
1078
|
+
more = 1 if first + sent < len(items) else 0
|
|
1079
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<BI", more, self._hash(self.config)) + out
|
|
1080
|
+
if op == _CFG.op["set"]:
|
|
1081
|
+
new = dict(self.config)
|
|
1082
|
+
seen, plan_roles, plans = set(), set(), {}
|
|
1083
|
+
for tag, value in m.split_tlvs(t.data) if t.data else []:
|
|
1084
|
+
tag &= 0x7F # kept without the critical bit
|
|
1085
|
+
if tag not in self.items:
|
|
1086
|
+
raise Reject(m.UNSUPPORTED, bytes([tag]))
|
|
1087
|
+
key = self._item_key(tag, value)
|
|
1088
|
+
if tag == ITEM["plan"]: # one item per assignment; fn alone = no plan
|
|
1089
|
+
plans.setdefault(key, [])
|
|
1090
|
+
if len(value) != 2:
|
|
1091
|
+
if len(value) != 5 or (key, value[2]) in plan_roles:
|
|
1092
|
+
raise Reject(m.MALFORMED)
|
|
1093
|
+
plan_roles.add((key, value[2]))
|
|
1094
|
+
plans[key].append(value)
|
|
1095
|
+
continue
|
|
1096
|
+
if (tag, key) in seen:
|
|
1097
|
+
raise Reject(m.MALFORMED) # the same key twice in one set
|
|
1098
|
+
seen.add((tag, key))
|
|
1099
|
+
if len(value) == self._key_len(tag):
|
|
1100
|
+
new.pop((tag, key), None) # the key alone removes the item
|
|
1101
|
+
else:
|
|
1102
|
+
new[(tag, key)] = value
|
|
1103
|
+
for fn, values in plans.items():
|
|
1104
|
+
new.pop((ITEM["plan"], fn), None)
|
|
1105
|
+
if values:
|
|
1106
|
+
new[(ITEM["plan"], fn)] = values
|
|
1107
|
+
self._apply_config(new, changed_slots={k for t_, k in seen if t_ == ITEM["slot"]})
|
|
1108
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<I", self._hash(self.config))
|
|
1109
|
+
if op == _CFG.op["save"]:
|
|
1110
|
+
t.tail()
|
|
1111
|
+
if len(b"".join(self._canonical(self.config))) > self.storage_max:
|
|
1112
|
+
raise Reject(m.UNAVAILABLE)
|
|
1113
|
+
self.saved = dict(self.config)
|
|
1114
|
+
return m.COMPLETED, m.SUCCESS, struct.pack("<I", self._hash(self.config))
|
|
1115
|
+
if op == _CFG.op["erase"]:
|
|
1116
|
+
t.tail()
|
|
1117
|
+
self.saved = None
|
|
1118
|
+
return m.COMPLETED, m.SUCCESS, b""
|
|
1119
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
1120
|
+
|
|
1121
|
+
@staticmethod
|
|
1122
|
+
def _key_len(tag: int) -> int:
|
|
1123
|
+
return 1 if tag in (ITEM["slot"], ITEM["bind"]) else 2
|
|
1124
|
+
|
|
1125
|
+
def _item_key(self, tag: int, value: bytes) -> int:
|
|
1126
|
+
if len(value) < self._key_len(tag):
|
|
1127
|
+
raise Reject(m.MALFORMED)
|
|
1128
|
+
return value[0] if self._key_len(tag) == 1 else struct.unpack_from("<H", value)[0]
|
|
1129
|
+
|
|
1130
|
+
@staticmethod
|
|
1131
|
+
def _canonical(config: dict) -> list[bytes]:
|
|
1132
|
+
"""probe.config §2: tag order, then key order (plan by (fn, role)); TLVs without the critical bit."""
|
|
1133
|
+
rows = []
|
|
1134
|
+
for (tag, key), value in config.items():
|
|
1135
|
+
for v in (value if isinstance(value, list) else [value]):
|
|
1136
|
+
rows.append((tag, key, v[2] if isinstance(value, list) else 0, bytes([tag, len(v)]) + v))
|
|
1137
|
+
return [r[3] for r in sorted(rows)]
|
|
1138
|
+
|
|
1139
|
+
def _hash(self, config: dict | None) -> int:
|
|
1140
|
+
return zlib.crc32(b"".join(self._canonical(config or {})))
|
|
1141
|
+
|
|
1142
|
+
def load_config(self, items: list[bytes], saved: bool = True) -> None:
|
|
1143
|
+
"""Put `items` (item TLVs) in as the config, as a set would; with `saved` they are also the saved config
|
|
1144
|
+
(a probe that booted with them)."""
|
|
1145
|
+
new: dict = {}
|
|
1146
|
+
for tag, value in m.split_tlvs(b"".join(items)):
|
|
1147
|
+
key = self._item_key(tag, value)
|
|
1148
|
+
if tag == ITEM["plan"]:
|
|
1149
|
+
new.setdefault((tag, key), []).append(value)
|
|
1150
|
+
else:
|
|
1151
|
+
new[(tag, key)] = value
|
|
1152
|
+
self._apply_config(new, changed_slots=None)
|
|
1153
|
+
if saved:
|
|
1154
|
+
self.saved = dict(self.config)
|
|
1155
|
+
|
|
1156
|
+
def _apply_config(self, new: dict, changed_slots: set[int] | None = None, boot: bool = False) -> None:
|
|
1157
|
+
"""Check the whole config, then make it the current one (set is all-or-nothing up to reserving resources);
|
|
1158
|
+
automatic attaches and console opens follow (and are not rolled back)."""
|
|
1159
|
+
slots, binds = {}, {}
|
|
1160
|
+
for (tag, key), value in new.items():
|
|
1161
|
+
if tag == ITEM["slot"]:
|
|
1162
|
+
slots[key] = self._parse_slot(value)
|
|
1163
|
+
names = [s.name for s in slots.values()]
|
|
1164
|
+
if len(set(names)) != len(names):
|
|
1165
|
+
raise Reject(m.MALFORMED)
|
|
1166
|
+
places = [(s.wire_fn, s.pair) for s in slots.values()]
|
|
1167
|
+
if len(set(places)) != len(places):
|
|
1168
|
+
raise Reject(m.UNAVAILABLE)
|
|
1169
|
+
for fn in self.pairs:
|
|
1170
|
+
if sum(1 for s in slots.values() if s.wire_fn == fn and s.attach == SLOT_ATTACH["at_boot"]) > \
|
|
1171
|
+
self.max_connections.get(fn, 1):
|
|
1172
|
+
raise Reject(m.UNAVAILABLE)
|
|
1173
|
+
for (tag, key), value in new.items():
|
|
1174
|
+
if tag == ITEM["bind"]:
|
|
1175
|
+
binds[key] = self._parse_bind(value, slots)
|
|
1176
|
+
plans: dict[int, list[tuple[int, int, int]]] = {}
|
|
1177
|
+
for (tag, key), value in new.items():
|
|
1178
|
+
if tag == ITEM["plan"]:
|
|
1179
|
+
plans[key] = [struct.unpack_from("<HBH", v) for v in value if len(v) >= 5]
|
|
1180
|
+
if any(len(v) != 5 for v in value):
|
|
1181
|
+
raise Reject(m.MALFORMED)
|
|
1182
|
+
elif tag in (ITEM["label"], ITEM["idle"]):
|
|
1183
|
+
if tag == ITEM["idle"] and (len(value) != 3 or value[2] > 2):
|
|
1184
|
+
raise Reject(m.MALFORMED)
|
|
1185
|
+
old_plan_fns = {k[1] for k in self.config if k[0] == ITEM["plan"]}
|
|
1186
|
+
want = [a for fn in plans for a in plans[fn]]
|
|
1187
|
+
self.slots = slots # the pin check below sees the new slots
|
|
1188
|
+
try:
|
|
1189
|
+
self._check_plan(want)
|
|
1190
|
+
except Reject:
|
|
1191
|
+
self.slots = {k: self._parse_slot(v) for (t, k), v in self.config.items() if t == ITEM["slot"]}
|
|
1192
|
+
raise
|
|
1193
|
+
# accepted: make it current
|
|
1194
|
+
for fn in old_plan_fns - set(plans):
|
|
1195
|
+
self._drop_plan(fn)
|
|
1196
|
+
for fn, assigned in plans.items():
|
|
1197
|
+
self.plan = {a for a in self.plan if a[0] != fn} | set(assigned)
|
|
1198
|
+
self.plan_from_config.add(fn)
|
|
1199
|
+
self.config = new
|
|
1200
|
+
for n in list(self.slot_rt):
|
|
1201
|
+
if n not in slots:
|
|
1202
|
+
del self.slot_rt[n]
|
|
1203
|
+
for n in slots:
|
|
1204
|
+
self.slot_rt.setdefault(n, SlotRuntime())
|
|
1205
|
+
for port, b in binds.items():
|
|
1206
|
+
if port not in self.binds or self.binds[port] != b:
|
|
1207
|
+
self.selected[port] = b.selected if b.mode == BIND_MODE["manual"] else 0
|
|
1208
|
+
for key in b.streams:
|
|
1209
|
+
self.flows.pop((port, key), None)
|
|
1210
|
+
self.mixed_out.pop(port, None)
|
|
1211
|
+
self.binds = binds
|
|
1212
|
+
for n, s in slots.items():
|
|
1213
|
+
if s.attach == SLOT_ATTACH["at_boot"] and (boot or changed_slots is None or n in changed_slots):
|
|
1214
|
+
self.slot_rt[n].evicted = False
|
|
1215
|
+
self._auto_attach(n)
|
|
1216
|
+
self._refresh()
|
|
1217
|
+
|
|
1218
|
+
def _parse_slot(self, v: bytes) -> Slot:
|
|
1219
|
+
t = Take(v)
|
|
1220
|
+
n, wire_fn, swdio, swclk, attach, retry_s, mech, name_len = t.take("BHHHBHBB")
|
|
1221
|
+
name = t.bytes(name_len)
|
|
1222
|
+
scheme = t.take("B")
|
|
1223
|
+
rest = v[t.at:]
|
|
1224
|
+
if n >= self.slots_max or attach not in SLOT_ATTACH.values():
|
|
1225
|
+
raise Reject(m.MALFORMED)
|
|
1226
|
+
if retry_s and attach != SLOT_ATTACH["at_boot"]:
|
|
1227
|
+
raise Reject(m.MALFORMED)
|
|
1228
|
+
if not SLOT_NAME.fullmatch(name.decode("ascii", "replace")):
|
|
1229
|
+
raise Reject(m.MALFORMED)
|
|
1230
|
+
if self.names.get(wire_fn) not in WIRES or (swdio, swclk) not in self.pairs.get(wire_fn, []):
|
|
1231
|
+
raise Reject(m.UNAVAILABLE)
|
|
1232
|
+
if mech not in self.mechanisms:
|
|
1233
|
+
raise Reject(m.UNSUPPORTED)
|
|
1234
|
+
lock = None
|
|
1235
|
+
if scheme:
|
|
1236
|
+
if not rest or len(rest) % 2:
|
|
1237
|
+
raise Reject(m.MALFORMED)
|
|
1238
|
+
lock = (scheme, rest[:len(rest) // 2], rest[len(rest) // 2:])
|
|
1239
|
+
elif rest:
|
|
1240
|
+
raise Reject(m.MALFORMED)
|
|
1241
|
+
return Slot(n, wire_fn, (swdio, swclk), attach, retry_s, mech, name.decode(), lock)
|
|
1242
|
+
|
|
1243
|
+
def _parse_bind(self, v: bytes, slots: dict[int, Slot]) -> Bind:
|
|
1244
|
+
t = Take(v)
|
|
1245
|
+
port, mode, selected, n = t.take("BBBB")
|
|
1246
|
+
streams = tuple(t.take("BH") for _ in range(n))
|
|
1247
|
+
if t.at != len(v) or n == 0:
|
|
1248
|
+
raise Reject(m.MALFORMED)
|
|
1249
|
+
if port not in self.serial_ports:
|
|
1250
|
+
raise Reject(m.UNAVAILABLE)
|
|
1251
|
+
if mode not in BIND_MODE.values() or not self.bind_modes & (1 << mode):
|
|
1252
|
+
raise Reject(m.UNSUPPORTED)
|
|
1253
|
+
if mode == BIND_MODE["manual"] and selected >= n:
|
|
1254
|
+
raise Reject(m.MALFORMED)
|
|
1255
|
+
for kind, i in streams:
|
|
1256
|
+
if kind == BIND_STREAM["slot_console"]:
|
|
1257
|
+
if i not in slots:
|
|
1258
|
+
raise Reject(m.UNAVAILABLE)
|
|
1259
|
+
elif kind == BIND_STREAM["fixture_uart"]:
|
|
1260
|
+
if self.names.get(i) != "oep.fixture.uart":
|
|
1261
|
+
raise Reject(m.UNAVAILABLE)
|
|
1262
|
+
else:
|
|
1263
|
+
raise Reject(m.MALFORMED)
|
|
1264
|
+
return Bind(port, mode, selected if mode == BIND_MODE["manual"] else 0, streams)
|
|
1265
|
+
|
|
1266
|
+
# ---- slots: automatic attach, the lock check, what uses a connection ------------------------
|
|
1267
|
+
@staticmethod
|
|
1268
|
+
def _lock_ok(s: Slot, tid: int | None) -> bool | None:
|
|
1269
|
+
"""True / False; None = the slot has a lock and there is no target_id to check."""
|
|
1270
|
+
if s.lock is None:
|
|
1271
|
+
return True
|
|
1272
|
+
if tid is None:
|
|
1273
|
+
return None
|
|
1274
|
+
scheme, mask, value = s.lock
|
|
1275
|
+
raw = struct.pack("<I", tid)
|
|
1276
|
+
if scheme != 1 or len(mask) != len(raw):
|
|
1277
|
+
return False
|
|
1278
|
+
return bytes(a & b for a, b in zip(raw, mask)) == value
|
|
1279
|
+
|
|
1280
|
+
def _bound(self, n: int) -> bool:
|
|
1281
|
+
return any((BIND_STREAM["slot_console"], n) in b.streams for b in self.binds.values())
|
|
1282
|
+
|
|
1283
|
+
def _auto_attach(self, n: int) -> None:
|
|
1284
|
+
s = self.slots[n]
|
|
1285
|
+
rt = self.slot_rt[n]
|
|
1286
|
+
rt.last_try_ms = self.now()
|
|
1287
|
+
tg = self.targets[(s.wire_fn, s.pair)]
|
|
1288
|
+
cid = self._conn_at(s.wire_fn, s.pair)
|
|
1289
|
+
if cid is None:
|
|
1290
|
+
if not tg.present:
|
|
1291
|
+
return
|
|
1292
|
+
try:
|
|
1293
|
+
cid = self._seat(s.wire_fn, s.pair, tg, 4_000_000, evict=False) # automatic: never evicts
|
|
1294
|
+
except Reject:
|
|
1295
|
+
return
|
|
1296
|
+
tg.havereset = False
|
|
1297
|
+
c = self.conns[cid]
|
|
1298
|
+
if self._lock_ok(s, c.tid) is True:
|
|
1299
|
+
c.users.add(("slot", n))
|
|
1300
|
+
rt.mismatch_tid = None
|
|
1301
|
+
else:
|
|
1302
|
+
rt.mismatch_tid = c.tid # found, wrong chip: let go of it
|
|
1303
|
+
if not c.users:
|
|
1304
|
+
self._close_conn(cid, MARK["detach"])
|
|
1305
|
+
|
|
1306
|
+
def _refresh(self) -> None:
|
|
1307
|
+
"""Make what the slots use match the config: a bound slot rides any connection on its place (lock
|
|
1308
|
+
permitting) with its console open; an at-boot slot keeps its automatic connection; nothing else."""
|
|
1309
|
+
for cid, c in list(self.conns.items()):
|
|
1310
|
+
for u in [u for u in c.users if u != "host"]:
|
|
1311
|
+
s = self.slots.get(u[1])
|
|
1312
|
+
if (s is None or (s.wire_fn, s.pair) != (c.fn, c.pair) or self._lock_ok(s, c.tid) is not True
|
|
1313
|
+
or not (s.attach == SLOT_ATTACH["at_boot"] or self._bound(u[1]))):
|
|
1314
|
+
c.users.discard(u)
|
|
1315
|
+
if not c.users:
|
|
1316
|
+
self._close_conn(cid, MARK["detach"])
|
|
1317
|
+
for n, s in self.slots.items():
|
|
1318
|
+
cid = self._conn_at(s.wire_fn, s.pair)
|
|
1319
|
+
if cid is None or self._lock_ok(s, self.conns[cid].tid) is not True or not self._bound(n):
|
|
1320
|
+
continue
|
|
1321
|
+
self.conns[cid].users.add(("slot", n))
|
|
1322
|
+
sid, existing = self._open_stream(cid, s.mechanism)
|
|
1323
|
+
if not existing:
|
|
1324
|
+
for port, b in self.binds.items():
|
|
1325
|
+
key = (BIND_STREAM["slot_console"], n)
|
|
1326
|
+
if key in b.streams:
|
|
1327
|
+
self.flows[(port, key)] = Flow(sid, 0)
|
|
1328
|
+
|
|
1329
|
+
def tick(self) -> None:
|
|
1330
|
+
"""Time passes: the lease, at-boot retries, mixed lines closed by quiet."""
|
|
1331
|
+
self._lapse()
|
|
1332
|
+
now = self.now()
|
|
1333
|
+
for n, s in self.slots.items():
|
|
1334
|
+
rt = self.slot_rt[n]
|
|
1335
|
+
if (s.attach == SLOT_ATTACH["at_boot"] and s.retry_s and not rt.evicted
|
|
1336
|
+
and self._conn_at(s.wire_fn, s.pair) is None
|
|
1337
|
+
and (rt.last_try_ms is None or now - rt.last_try_ms >= 1000 * s.retry_s)):
|
|
1338
|
+
self._auto_attach(n)
|
|
1339
|
+
self._refresh()
|
|
1340
|
+
|
|
1341
|
+
def _slot_state(self, n: int) -> bytes:
|
|
1342
|
+
s, rt = self.slots[n], self.slot_rt[n]
|
|
1343
|
+
cid = self._conn_at(s.wire_fn, s.pair)
|
|
1344
|
+
tid = self.conns[cid].tid if cid is not None else rt.mismatch_tid
|
|
1345
|
+
if cid is not None:
|
|
1346
|
+
ok = self._lock_ok(s, tid)
|
|
1347
|
+
state = SLOT_STATE["connected"] if ok else (SLOT_STATE["no_target_id"] if ok is None
|
|
1348
|
+
else SLOT_STATE["lock_mismatch"])
|
|
1349
|
+
else:
|
|
1350
|
+
state = SLOT_STATE["lock_mismatch"] if rt.mismatch_tid is not None else SLOT_STATE["absent"]
|
|
1351
|
+
age = NEVER if rt.last_try_ms is None else min(NEVER - 1, self.now() - rt.last_try_ms)
|
|
1352
|
+
raw = b"" if tid is None else struct.pack("<I", tid)
|
|
1353
|
+
return struct.pack("<BBHIBB", n, state, cid or 0, age, 1 if raw else 0, len(raw)) + raw
|
|
1354
|
+
|
|
1355
|
+
def _bind_state(self, port: int) -> bytes:
|
|
1356
|
+
b = self.binds[port]
|
|
1357
|
+
mixed = b.mode == BIND_MODE["mixed"]
|
|
1358
|
+
selected = 0xFF if mixed else self.selected.get(port, 0)
|
|
1359
|
+
if port in self.held_ports and self.holder is not None:
|
|
1360
|
+
flow = BIND_FLOW["held"]
|
|
1361
|
+
else:
|
|
1362
|
+
keys = b.streams if mixed else (b.streams[selected],)
|
|
1363
|
+
flow = BIND_FLOW["streaming"] if any(self._stream_for(k)[1] is not None for k in keys) else BIND_FLOW["idle"]
|
|
1364
|
+
return struct.pack("<BBBB", port, b.mode, selected, flow)
|
|
1365
|
+
|
|
1366
|
+
# ---- serial ports: the raw bytes outside the frames (core §3.4, probe.config §1.2) ----------
|
|
1367
|
+
def _stream_for(self, key: tuple[int, int]) -> tuple[object, Stream | None]:
|
|
1368
|
+
kind, i = key
|
|
1369
|
+
if kind == BIND_STREAM["fixture_uart"]:
|
|
1370
|
+
return ("uart", i), self.uarts.get(i)
|
|
1371
|
+
s = self.slots.get(i)
|
|
1372
|
+
if s is None:
|
|
1373
|
+
return None, None
|
|
1374
|
+
cid = self._conn_at(s.wire_fn, s.pair)
|
|
1375
|
+
sid = self.stream_keys.get((cid, s.mechanism)) if cid is not None else None
|
|
1376
|
+
if sid is None or self.streams[sid].closed:
|
|
1377
|
+
return None, None
|
|
1378
|
+
return sid, self.streams[sid]
|
|
1379
|
+
|
|
1380
|
+
def _flow(self, port: int, key: tuple[int, int]) -> tuple[Flow, Stream | None]:
|
|
1381
|
+
sid, s = self._stream_for(key)
|
|
1382
|
+
f = self.flows.get((port, key))
|
|
1383
|
+
if s is None:
|
|
1384
|
+
return f or Flow(), None
|
|
1385
|
+
if f is None or f.sid != sid:
|
|
1386
|
+
f = self.flows[(port, key)] = Flow(sid, s.end, last_ms=self.now()) # first seen: from now
|
|
1387
|
+
if f.pos < s.base:
|
|
1388
|
+
f.pos = s.base # overflowed past the port: the oldest left
|
|
1389
|
+
return f, s
|
|
1390
|
+
|
|
1391
|
+
def port_held(self, port: int) -> bool:
|
|
1392
|
+
return self.holder is not None and port in self.held_ports
|
|
1393
|
+
|
|
1394
|
+
def port_output(self, port: int, room: int | None = None) -> bytes:
|
|
1395
|
+
"""The raw bytes serial port `port` sends now by its bind (empty while a session holds it)."""
|
|
1396
|
+
room = self.CHUNK if room is None else room
|
|
1397
|
+
b = self.binds.get(port)
|
|
1398
|
+
if b is None or self.port_held(port) or room <= 0:
|
|
1399
|
+
return b""
|
|
1400
|
+
if b.mode != BIND_MODE["mixed"]:
|
|
1401
|
+
f, s = self._flow(port, b.streams[self.selected.get(port, 0)])
|
|
1402
|
+
if s is None:
|
|
1403
|
+
return b""
|
|
1404
|
+
out = bytes(s.data[f.pos - s.base:f.pos - s.base + room])
|
|
1405
|
+
f.pos += len(out)
|
|
1406
|
+
return out
|
|
1407
|
+
pending = self.mixed_out.setdefault(port, bytearray())
|
|
1408
|
+
now = self.now()
|
|
1409
|
+
for key in b.streams:
|
|
1410
|
+
f, s = self._flow(port, key)
|
|
1411
|
+
if s is None:
|
|
1412
|
+
continue
|
|
1413
|
+
new = bytes(s.data[f.pos - s.base:])
|
|
1414
|
+
f.pos += len(new)
|
|
1415
|
+
if new:
|
|
1416
|
+
f.line += new
|
|
1417
|
+
f.last_ms = now
|
|
1418
|
+
while True:
|
|
1419
|
+
cut = f.line.find(b"\n")
|
|
1420
|
+
if cut < 0 and len(f.line) < self.MIXED_LINE_MAX and not (f.line and now - f.last_ms >= self.MIXED_QUIET_MS):
|
|
1421
|
+
break
|
|
1422
|
+
if not f.line:
|
|
1423
|
+
break
|
|
1424
|
+
end = cut + 1 if cut >= 0 else min(len(f.line), self.MIXED_LINE_MAX)
|
|
1425
|
+
line = bytes(f.line[:end])
|
|
1426
|
+
del f.line[:end]
|
|
1427
|
+
pending += b"[" + self._mixed_name(key).encode() + b"] " + line + (b"" if line.endswith(b"\n") else b"\n")
|
|
1428
|
+
out = bytes(pending[:room])
|
|
1429
|
+
del pending[:room]
|
|
1430
|
+
return out
|
|
1431
|
+
|
|
1432
|
+
def _mixed_name(self, key: tuple[int, int]) -> str:
|
|
1433
|
+
kind, i = key
|
|
1434
|
+
if kind == BIND_STREAM["slot_console"]:
|
|
1435
|
+
return self.slots[i].name
|
|
1436
|
+
rx = next((ch for fn, role, ch in self.plan if fn == i and role == 1), None)
|
|
1437
|
+
label = None
|
|
1438
|
+
if rx is not None:
|
|
1439
|
+
label = self.static_labels.get(rx)
|
|
1440
|
+
v = self.config.get((ITEM["label"], rx))
|
|
1441
|
+
if v is not None:
|
|
1442
|
+
label = v[2:].decode("utf-8", "replace")
|
|
1443
|
+
if not label:
|
|
1444
|
+
return f"uart{i}"
|
|
1445
|
+
return "".join("_" if c == "]" or ord(c) < 0x20 else c for c in label)
|
|
1446
|
+
|
|
1447
|
+
def port_input(self, port: int, data: bytes) -> None:
|
|
1448
|
+
"""Raw bytes that came in on serial port `port` outside any frame."""
|
|
1449
|
+
b = self.binds.get(port)
|
|
1450
|
+
if b is None or self.port_held(port) or b.mode == BIND_MODE["mixed"] or not data:
|
|
1451
|
+
return
|
|
1452
|
+
key = b.streams[self.selected.get(port, 0)]
|
|
1453
|
+
sid, s = self._stream_for(key)
|
|
1454
|
+
if s is None:
|
|
1455
|
+
return
|
|
1456
|
+
if key[0] == BIND_STREAM["fixture_uart"]:
|
|
1457
|
+
self.uart_tx.setdefault(key[1], bytearray()).extend(data)
|
|
1458
|
+
else:
|
|
1459
|
+
s.written += data
|
|
1460
|
+
|
|
1461
|
+
def _session_over(self) -> None:
|
|
1462
|
+
"""The session ended (end, lapse, force): the ports it held resume from its last host reset (or now)."""
|
|
1463
|
+
for port in self.held_ports:
|
|
1464
|
+
b = self.binds.get(port)
|
|
1465
|
+
if b is None:
|
|
1466
|
+
continue
|
|
1467
|
+
for key in b.streams:
|
|
1468
|
+
sid, s = self._stream_for(key)
|
|
1469
|
+
if s is None:
|
|
1470
|
+
continue
|
|
1471
|
+
rsid, pos = self.session_resets.get(key, (sid, s.end))
|
|
1472
|
+
self.flows[(port, key)] = Flow(sid, pos if rsid == sid else s.end, last_ms=self.now())
|
|
1473
|
+
self.held_ports.clear()
|
|
1474
|
+
self.session_resets.clear()
|
|
1475
|
+
|
|
1476
|
+
|
|
1477
|
+
SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
|
|
1478
|
+
"oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
|
|
1479
|
+
"oep.probe.config": "config_op"}
|