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
oep_client/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Draft: capability discovery by name (oep-spec capability-*.ja.md). No hardware yet."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Draft OEP capability discovery by name, on an in-process fake or a v1 draft probe.
|
|
2
|
+
|
|
3
|
+
uv run python -m oep_client.v1 dump --port /run/board-identify/by-id/<probe>
|
|
4
|
+
uv run python -m oep_client.v1 dump --fake p4-x035
|
|
5
|
+
uv run python -m oep_client.v1 dump --fake esp32-v003 --prefix oep.fixture
|
|
6
|
+
uv run python -m oep_client.v1 dump --fake p4-x035 --prefix oep.target --json
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from . import dump, fake, host, link
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv=None) -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(description="OEP capability discovery (draft)")
|
|
19
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
20
|
+
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
21
|
+
src = d.add_mutually_exclusive_group(required=True)
|
|
22
|
+
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
23
|
+
src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
|
|
24
|
+
d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
|
|
25
|
+
d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
|
|
26
|
+
d.add_argument("--json", action="store_true", help="machine-readable output")
|
|
27
|
+
args = parser.parse_args(argv)
|
|
28
|
+
|
|
29
|
+
if args.fake:
|
|
30
|
+
call = fake.PROFILES[args.fake]().call
|
|
31
|
+
else:
|
|
32
|
+
hst = link.open_host(args.port)
|
|
33
|
+
call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
|
|
34
|
+
caps = dump.collect(call, args.prefix, args.exact)
|
|
35
|
+
sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|
oep_client/v1/arm.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""oep.wire.swd and oep.target.arm-adi, revision 1 (oep-spec oep-if-debug §1, §5-§6).
|
|
2
|
+
|
|
3
|
+
The probe moves raw DP / AP transfers and MEM-AP blocks; everything above - power-up, SELECT (ADIv5 APSEL/APBANKSEL or
|
|
4
|
+
ADIv6 AP addresses), CSW, the Cortex-M debug registers - is here, as target knowledge belongs to the host.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
from . import host as h, message as m, registry as reg
|
|
12
|
+
from .core import Interface
|
|
13
|
+
from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
|
|
14
|
+
|
|
15
|
+
DP_DPIDR = DP_ABORT = 0x0
|
|
16
|
+
DP_CTRL_STAT = 0x4
|
|
17
|
+
DP_SELECT = 0x8
|
|
18
|
+
DP_RDBUFF = 0xC
|
|
19
|
+
_ADI = reg.TARGET_ARM_ADI
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SwdWire(WireBase):
|
|
23
|
+
NAME = "oep.wire.swd"
|
|
24
|
+
TAG_TARGETSEL = reg.WIRE_SWD.tlv["attach"]["targetsel"]
|
|
25
|
+
|
|
26
|
+
def __init__(self, hst: h.Host):
|
|
27
|
+
super().__init__(hst)
|
|
28
|
+
self.speed_hz = 0
|
|
29
|
+
self.existing = False
|
|
30
|
+
|
|
31
|
+
def attach(self, targetsel: int | None = None, max_speed: int | None = None,
|
|
32
|
+
pins: tuple[int, int] | None = None) -> tuple[int, int, bool]:
|
|
33
|
+
"""-> (connection, DPIDR, woke from dormant). targetsel (multidrop) and max_speed go as critical TLVs: a probe
|
|
34
|
+
that cannot honour them refuses. self.existing: the wire was attached already (its connection returned)."""
|
|
35
|
+
body = self._speed_tlv(max_speed) + self._pins_tlv(pins)
|
|
36
|
+
if targetsel is not None:
|
|
37
|
+
body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
|
|
38
|
+
rd = m.Reader(self._call(self.ATTACH, body).payload)
|
|
39
|
+
conn, dpidr, flags, self.speed_hz = rd.take("HIBI")
|
|
40
|
+
self.existing = bool(flags & 2)
|
|
41
|
+
rd.tail()
|
|
42
|
+
return conn, dpidr, bool(flags & 1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AdiError(TargetError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def transfer_reads(steps: bytes) -> list[bool]:
|
|
50
|
+
"""Per transfer in a packed list: True for a read (1 byte), False for a write (1 + 4 bytes)."""
|
|
51
|
+
out, at = [], 0
|
|
52
|
+
while at < len(steps):
|
|
53
|
+
read = bool(steps[at] & 2)
|
|
54
|
+
out.append(read)
|
|
55
|
+
at += 1 if read else 5
|
|
56
|
+
if at != len(steps):
|
|
57
|
+
raise ValueError("the transfer list ends inside a write")
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ArmAdi(Interface):
|
|
62
|
+
NAME = "oep.target.arm-adi"
|
|
63
|
+
REVISION = 1
|
|
64
|
+
TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
|
|
65
|
+
|
|
66
|
+
def __init__(self, hst: h.Host, conn: int, adiv6: bool = False):
|
|
67
|
+
super().__init__(hst, prefix=struct.pack("<H", conn))
|
|
68
|
+
self.conn = conn
|
|
69
|
+
self.adiv6 = adiv6
|
|
70
|
+
self._select: int | None = None
|
|
71
|
+
|
|
72
|
+
# ---- raw transfers ----
|
|
73
|
+
@staticmethod
|
|
74
|
+
def req(ap: bool, read: bool, addr: int, value: int = 0) -> bytes:
|
|
75
|
+
b = bytes([int(ap) | (int(read) << 1) | (((addr >> 2) & 3) << 2)])
|
|
76
|
+
return b if read else b + struct.pack("<I", value)
|
|
77
|
+
|
|
78
|
+
def transfer(self, steps: bytes) -> list[int]:
|
|
79
|
+
"""A packed transfer list (req() concatenated). -> the values read, in order (an AP read's value arrives one
|
|
80
|
+
transfer late, as on the wire). A list that stopped raises AdiError (status, done, the values it read, and
|
|
81
|
+
self.last_ack = the raw ACK of the last transfer)."""
|
|
82
|
+
reads = transfer_reads(steps)
|
|
83
|
+
r = self._request(self.TRANSFER, struct.pack("<H", len(reads)) + steps)
|
|
84
|
+
rd = ran(r)
|
|
85
|
+
done, status, self.last_ack = rd.take("HBB")
|
|
86
|
+
values = rd.words(sum(reads[:done]))
|
|
87
|
+
rd.tail()
|
|
88
|
+
if status != OK or not r.succeeded or done != len(reads):
|
|
89
|
+
raise AdiError(f"transfer (ack {self.last_ack:#x})", status, r, done=done, values=values)
|
|
90
|
+
return values
|
|
91
|
+
def dp_read(self, addr: int) -> int:
|
|
92
|
+
return self.transfer(self.req(False, True, addr))[0]
|
|
93
|
+
|
|
94
|
+
def dp_write(self, addr: int, value: int) -> None:
|
|
95
|
+
self.transfer(self.req(False, False, addr, value))
|
|
96
|
+
|
|
97
|
+
def select(self, value: int) -> None:
|
|
98
|
+
if value != self._select:
|
|
99
|
+
self.dp_write(DP_SELECT, value)
|
|
100
|
+
self._select = value
|
|
101
|
+
|
|
102
|
+
def _ap_select(self, ap: int, reg: int) -> None:
|
|
103
|
+
"""ADIv5: ap = APSEL (0..255), reg = register offset in the AP. ADIv6: ap = the AP's base address."""
|
|
104
|
+
if self.adiv6:
|
|
105
|
+
self.select((ap + reg) & ~0xF)
|
|
106
|
+
else:
|
|
107
|
+
self.select((ap << 24) | (reg & 0xF0))
|
|
108
|
+
|
|
109
|
+
def ap_read(self, ap: int, reg: int) -> int:
|
|
110
|
+
self._ap_select(ap, reg)
|
|
111
|
+
return self.transfer(self.req(True, True, reg) + self.req(False, True, DP_RDBUFF))[1] # posted
|
|
112
|
+
|
|
113
|
+
def ap_write(self, ap: int, reg: int, value: int) -> None:
|
|
114
|
+
self._ap_select(ap, reg)
|
|
115
|
+
self.transfer(self.req(True, False, reg, value))
|
|
116
|
+
|
|
117
|
+
def power_up(self) -> int:
|
|
118
|
+
"""Clear sticky errors, request debug + system power, wait for both acks. -> CTRL/STAT"""
|
|
119
|
+
self.dp_write(DP_ABORT, 0x1E)
|
|
120
|
+
self._select = None
|
|
121
|
+
self.select(0)
|
|
122
|
+
self.dp_write(DP_CTRL_STAT, 0x50000000)
|
|
123
|
+
for _ in range(100):
|
|
124
|
+
cs = self.dp_read(DP_CTRL_STAT)
|
|
125
|
+
if (cs >> 29) & 1 and (cs >> 31) & 1:
|
|
126
|
+
return cs
|
|
127
|
+
raise h.OepError(f"no power-up ack: CTRL/STAT {cs:#010x}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MemAp:
|
|
131
|
+
"""One MEM-AP (ADIv5 APSEL or ADIv6 base address) with 32-bit, auto-incrementing access."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, adi: ArmAdi, ap: int, csw_set: int = 0, csw_clear: int = 0):
|
|
134
|
+
"""csw_set / csw_clear: target-specific CSW bits (protection, security). The RP2350's AHB-APs come up
|
|
135
|
+
non-secure (CSW bit 30), and its SRAM then faults: pass csw_clear=1 << 30 there (2026-09-24)."""
|
|
136
|
+
self.adi, self.ap = adi, ap
|
|
137
|
+
self.base = 0xD00 if adi.adiv6 else 0x00 # CSW, TAR, DRW at base + 0x0 / 0x4 / 0xC
|
|
138
|
+
csw = adi.ap_read(ap, self.base)
|
|
139
|
+
adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
|
|
140
|
+
adi._ap_select(ap, self.base) # the bank the block operations assume
|
|
141
|
+
# Words per block operation, from the probe's frame limit: request header 6 + session 4 + connection 2 +
|
|
142
|
+
# address 4 + count 2 on the way in (the answer's 5 + done 2 + status 1 is smaller).
|
|
143
|
+
from .core import confirm
|
|
144
|
+
self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
|
|
145
|
+
|
|
146
|
+
def write_many(self, pairs: list[tuple[int, int]]) -> None:
|
|
147
|
+
"""Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
|
|
148
|
+
has landed): one round trip instead of one per word - what a debug-register sequence needs."""
|
|
149
|
+
self.adi._ap_select(self.ap, self.base)
|
|
150
|
+
steps = b"".join(self.adi.req(True, False, self.base + 0x4, a) + self.adi.req(True, False, self.base + 0xC, v)
|
|
151
|
+
for a, v in pairs)
|
|
152
|
+
self.adi.transfer(steps + self.adi.req(False, True, DP_RDBUFF))
|
|
153
|
+
|
|
154
|
+
def read_block(self, address: int, words: int) -> list[int]:
|
|
155
|
+
out, chunk = [], self.chunk
|
|
156
|
+
for off in range(0, words, chunk):
|
|
157
|
+
self.adi._ap_select(self.ap, self.base)
|
|
158
|
+
n = min(chunk, words - off)
|
|
159
|
+
r = self.adi._request(ArmAdi.READ_BLOCK, struct.pack("<IH", address + off * 4, n))
|
|
160
|
+
rd = ran(r)
|
|
161
|
+
done, status = rd.take("HB")
|
|
162
|
+
got = rd.words(done)
|
|
163
|
+
rd.tail()
|
|
164
|
+
if status != OK or not r.succeeded or done != n:
|
|
165
|
+
raise AdiError("read_block", status, r, done=off + done, values=out + got)
|
|
166
|
+
out += got
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
def write_block(self, address: int, values: list[int]) -> None:
|
|
170
|
+
chunk = self.chunk
|
|
171
|
+
for off in range(0, len(values), chunk):
|
|
172
|
+
self.adi._ap_select(self.ap, self.base)
|
|
173
|
+
part = values[off:off + chunk]
|
|
174
|
+
r = self.adi._request(ArmAdi.WRITE_BLOCK, struct.pack("<IH", address + off * 4, len(part))
|
|
175
|
+
+ struct.pack(f"<{len(part)}I", *part))
|
|
176
|
+
rd = ran(r)
|
|
177
|
+
done, status = rd.take("HB")
|
|
178
|
+
rd.tail()
|
|
179
|
+
if status != OK or not r.succeeded:
|
|
180
|
+
raise AdiError("write_block", status, r, done=off + done)
|
|
181
|
+
|
|
182
|
+
def read32(self, address: int) -> int:
|
|
183
|
+
return self.read_block(address, 1)[0]
|
|
184
|
+
|
|
185
|
+
def write32(self, address: int, value: int) -> None:
|
|
186
|
+
self.write_block(address, [value])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class CortexM:
|
|
190
|
+
"""Armv7-M / Armv8-M core debug through a MEM-AP: halt, resume, core registers through DCRSR / DCRDR, and running
|
|
191
|
+
a function on the target (arguments in r0-r3, LR at a BKPT in RAM, run until the core halts on it) - the way a
|
|
192
|
+
host-side flash algorithm drives the target's own ROM or a RAM loader."""
|
|
193
|
+
|
|
194
|
+
DHCSR, DCRSR, DCRDR, AIRCR = 0xE000EDF0, 0xE000EDF4, 0xE000EDF8, 0xE000ED0C
|
|
195
|
+
KEY = 0xA05F0000
|
|
196
|
+
C_DEBUGEN, C_HALT, C_MASKINTS = 1, 2, 8
|
|
197
|
+
S_REGRDY, S_HALT = 1 << 16, 1 << 17
|
|
198
|
+
SP, LR, PC, XPSR = 13, 14, 15, 16
|
|
199
|
+
|
|
200
|
+
def __init__(self, mem: MemAp, bkpt_at: int, stack_top: int):
|
|
201
|
+
"""bkpt_at: a word of RAM the target does not need (the return breakpoint goes there); stack_top: where the
|
|
202
|
+
called function's stack starts (its RAM below is clobbered)."""
|
|
203
|
+
self.mem, self.bkpt_at, self.stack_top = mem, bkpt_at, stack_top
|
|
204
|
+
|
|
205
|
+
def _wait(self, mask: int, timeout: float):
|
|
206
|
+
import time
|
|
207
|
+
deadline = time.monotonic() + timeout
|
|
208
|
+
while True:
|
|
209
|
+
v = self.mem.read32(self.DHCSR)
|
|
210
|
+
if v & mask:
|
|
211
|
+
return v
|
|
212
|
+
if time.monotonic() > deadline:
|
|
213
|
+
raise TimeoutError(f"DHCSR {v:#010x}: waiting for {mask:#x}")
|
|
214
|
+
|
|
215
|
+
def halted(self) -> bool:
|
|
216
|
+
return bool(self.mem.read32(self.DHCSR) & self.S_HALT)
|
|
217
|
+
|
|
218
|
+
def halt(self) -> None:
|
|
219
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
|
|
220
|
+
self._wait(self.S_HALT, 1.0)
|
|
221
|
+
|
|
222
|
+
def resume(self, mask_ints: bool = False) -> None:
|
|
223
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | (self.C_MASKINTS if mask_ints else 0))
|
|
224
|
+
|
|
225
|
+
def release(self) -> None:
|
|
226
|
+
"""Run, debug off. C_MASKINTS is cleared first: it lives in the debug domain and survives every reset but
|
|
227
|
+
power-on, and firmware left with it set runs without SysTick / USB interrupts (RP2350, 2026-09-24)."""
|
|
228
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
|
|
229
|
+
self.mem.write32(self.DHCSR, self.KEY)
|
|
230
|
+
|
|
231
|
+
def reg(self, n: int) -> int:
|
|
232
|
+
self.mem.write32(self.DCRSR, n)
|
|
233
|
+
self._wait(self.S_REGRDY, 1.0)
|
|
234
|
+
return self.mem.read32(self.DCRDR)
|
|
235
|
+
|
|
236
|
+
def set_reg(self, n: int, value: int) -> None:
|
|
237
|
+
self.mem.write32(self.DCRDR, value)
|
|
238
|
+
self.mem.write32(self.DCRSR, (1 << 16) | n)
|
|
239
|
+
self._wait(self.S_REGRDY, 1.0)
|
|
240
|
+
|
|
241
|
+
def prepare_call(self, fn: int, args=()) -> None:
|
|
242
|
+
"""Registers for fn(args...): r0-r3, SP, LR to the breakpoint, PC, Thumb bit, no active exception. The
|
|
243
|
+
writes go out as one transfer list; a register write takes the core a few cycles and each SWD transfer
|
|
244
|
+
takes microseconds, so S_REGRDY is checked once at the end rather than after each."""
|
|
245
|
+
xpsr = (self.reg(self.XPSR) | 1 << 24) & ~0x1FF
|
|
246
|
+
regs = [*enumerate(args), (self.SP, self.stack_top), (self.LR, self.bkpt_at | 1), (self.PC, fn & ~1),
|
|
247
|
+
(self.XPSR, xpsr)]
|
|
248
|
+
pairs = [(self.bkpt_at, 0xBE00BE00)] # bkpt #0, twice
|
|
249
|
+
for n, value in regs:
|
|
250
|
+
pairs += [(self.DCRDR, value), (self.DCRSR, (1 << 16) | n)]
|
|
251
|
+
self.mem.write_many(pairs)
|
|
252
|
+
self._wait(self.S_REGRDY, 1.0)
|
|
253
|
+
|
|
254
|
+
def call(self, fn: int, args=(), timeout: float = 10.0) -> int:
|
|
255
|
+
"""Run fn(args...) on the halted core with interrupts masked (their handlers may live in flash that the call
|
|
256
|
+
makes unreadable), wait for the breakpoint, clear the mask, return r0."""
|
|
257
|
+
self.prepare_call(fn, args)
|
|
258
|
+
self.resume(mask_ints=True)
|
|
259
|
+
self._wait(self.S_HALT, timeout)
|
|
260
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT) # MASKINTS off while halted
|
|
261
|
+
pc = self.reg(self.PC)
|
|
262
|
+
if pc & ~3 != self.bkpt_at:
|
|
263
|
+
raise h.OepError(f"stopped at {pc:#010x}, not at the return breakpoint")
|
|
264
|
+
return self.reg(0)
|
|
265
|
+
|
|
266
|
+
def sys_reset(self) -> None:
|
|
267
|
+
"""AIRCR.SYSRESETREQ: the core restarts; debug-domain state (DHCSR) survives, so clear the mask first."""
|
|
268
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
|
|
269
|
+
self.mem.write32(self.AIRCR, 0x05FA0004)
|