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 ADDED
@@ -0,0 +1,5 @@
1
+ """Open Embedded Probe (OEP) host client. `oep_client.v1` speaks the v1 protocol (oep-spec docs/oep-core.ja.md)."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.0.1"
@@ -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)