oep-client-python 0.0.52__py3-none-any.whl → 0.0.53__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 CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  __all__ = ["__version__"]
4
4
 
5
- __version__ = "0.0.52"
5
+ __version__ = "0.0.53"
oep_client/arm.py CHANGED
@@ -315,6 +315,19 @@ class CortexM:
315
315
  raise h.OepError(f"stopped at {pc:#010x}, not at the return breakpoint")
316
316
  return self.reg(0)
317
317
 
318
+ DEMCR, VC_CORERESET = 0xE000EDFC, 1
319
+
320
+ def reset_halt(self, timeout: float = 1.0) -> int:
321
+ """Reset the core and stop it on its first instruction (DEMCR.VC_CORERESET through AIRCR.SYSRESETREQ), the
322
+ catch cleared again: a clean core to call ROM functions on (a core left locked up in a fault handler is not
323
+ one). C_MASKINTS is cleared first (it survives the reset). -> PC"""
324
+ self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
325
+ self.mem.write32(self.DEMCR, self.mem.read32(self.DEMCR) | self.VC_CORERESET)
326
+ self.mem.write32(self.AIRCR, 0x05FA0004)
327
+ self._wait(self.S_HALT, timeout)
328
+ self.mem.write32(self.DEMCR, self.mem.read32(self.DEMCR) & ~self.VC_CORERESET)
329
+ return self.reg(self.PC)
330
+
318
331
  def sys_reset(self) -> None:
319
332
  """AIRCR.SYSRESETREQ: the core restarts; debug-domain state (DHCSR) survives, so clear the mask first."""
320
333
  self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
@@ -0,0 +1 @@
1
+ """pyserial URL handlers (serial.protocol_handler_packages.append("oep_client.pyserial")): oep:// - see protocol_oep."""
@@ -0,0 +1,92 @@
1
+ """pyserial's handler for oep://<probe>/<port>: an RP2040 peer's console over the probe's SWD (SEGGER RTT through the
2
+ MEM-AP, oep_client.rp2040_peer.Console). Baud rate, parity and the control lines mean nothing here and are taken as
3
+ given. A console that stops (the session lost, the target gone) raises on the next read."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import time
8
+
9
+ from serial.serialutil import PortNotOpenError, SerialBase, SerialException
10
+
11
+ from ..rp2040_peer import Console
12
+
13
+
14
+ class Serial(SerialBase):
15
+ def open(self):
16
+ if self._port is None:
17
+ raise SerialException("port must be configured before it can be used")
18
+ if self.is_open:
19
+ raise SerialException("port already open")
20
+ try:
21
+ self._console = Console(self.portstr)
22
+ except Exception as e:
23
+ raise SerialException(f"{self.portstr}: {e}") from e
24
+ self.is_open = True
25
+
26
+ def close(self):
27
+ if self.is_open:
28
+ self._console.close()
29
+ self.is_open = False
30
+
31
+ def from_url(self, url):
32
+ return url
33
+
34
+ def _reconfigure_port(self):
35
+ pass
36
+
37
+ @property
38
+ def in_waiting(self):
39
+ if not self.is_open:
40
+ raise PortNotOpenError()
41
+ return self._console.in_waiting()
42
+
43
+ def read(self, size=1):
44
+ if not self.is_open:
45
+ raise PortNotOpenError()
46
+ deadline = None if self._timeout is None else time.monotonic() + self._timeout
47
+ out = bytearray()
48
+ while len(out) < size:
49
+ if self._console.error:
50
+ raise SerialException(f"{self.portstr}: {self._console.error}")
51
+ out += self._console.read(size - len(out))
52
+ if len(out) >= size or (deadline is not None and time.monotonic() >= deadline):
53
+ break
54
+ time.sleep(0.005)
55
+ return bytes(out)
56
+
57
+ def write(self, data):
58
+ if not self.is_open:
59
+ raise PortNotOpenError()
60
+ return self._console.write(bytes(data))
61
+
62
+ def reset_input_buffer(self):
63
+ if self.is_open:
64
+ self._console.read()
65
+
66
+ def reset_output_buffer(self):
67
+ pass
68
+
69
+ def _update_break_state(self):
70
+ pass
71
+
72
+ def _update_rts_state(self):
73
+ pass
74
+
75
+ def _update_dtr_state(self):
76
+ pass
77
+
78
+ @property
79
+ def cts(self):
80
+ return True
81
+
82
+ @property
83
+ def dsr(self):
84
+ return True
85
+
86
+ @property
87
+ def ri(self):
88
+ return False
89
+
90
+ @property
91
+ def cd(self):
92
+ return True
@@ -0,0 +1,68 @@
1
+ """pytest-embedded-arduino-cli peers that are RP2040 boards on an OEP probe's SWD (oep_client.rp2040_peer), enabled by
2
+ `pytest_plugins = ["oep_client.pytest_rp2040_peer"]` in a conftest.py.
3
+
4
+ A peer `peer_<name>/` is such a peer when its address, the environment variable `TEST_SERIAL_PORT_PEER_<NAME>` (the
5
+ plugin's own), is `oep://<probe>/<port>`, and its profile uploads with arduino-pico's `uploadmethod=picoprobe_cmsis_dap`.
6
+ Then:
7
+
8
+ - its console: pyserial opens `oep://...` as the RTT console over SWD (oep_client.pyserial is registered);
9
+ - its upload: `interface/cmsis-dap.cfg` and `target/rp2040.cfg` are generated in the peer's directory (keep them out of
10
+ git), so arduino-pico's OpenOCD hands the ELF to `python -m oep_client.rp2040_peer flash` instead of a CMSIS-DAP;
11
+ - its device lock: its own (`rp2040-swd:<address>`), not the probe's - it shares the probe with the DUT through ch32rv's
12
+ broker;
13
+ - the broker: when the DUT is ch32rv's (pytest-embedded-arduino-cli-ch32rv's `ch32rv` fixture), its ch32rv goes to the
14
+ writer as `CH32RV`, which finds the broker the DUT's monitor started and shares its lock.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import pathlib
21
+
22
+ import pytest
23
+ import serial
24
+
25
+ from . import rp2040_peer
26
+
27
+ if "oep_client.pyserial" not in serial.protocol_handler_packages:
28
+ serial.protocol_handler_packages.append("oep_client.pyserial")
29
+
30
+
31
+ def _env_name(peer: str) -> str:
32
+ return "TEST_SERIAL_PORT_PEER_" + peer.upper().replace("-", "_")
33
+
34
+
35
+ def _peer_addresses() -> set[str]:
36
+ return {v for k, v in os.environ.items() if k.startswith("TEST_SERIAL_PORT_PEER_") and v.startswith("oep://")}
37
+
38
+
39
+ def pytest_collection_modifyitems(session, config, items):
40
+ """The OpenOCD hook in every RP2040 peer directory beside the collected tests."""
41
+ seen = set()
42
+ for item in items:
43
+ for peer_dir in pathlib.Path(item.path).parent.glob("peer_*"):
44
+ if peer_dir in seen or not peer_dir.is_dir():
45
+ continue
46
+ seen.add(peer_dir)
47
+ name = peer_dir.name[len("peer_"):]
48
+ address = os.environ.get(_env_name(name), "")
49
+ yaml = peer_dir / "sketch.yaml"
50
+ if address.startswith("oep://") and yaml.exists() and "picoprobe_cmsis_dap" in yaml.read_text():
51
+ rp2040_peer.write_openocd_hook(str(peer_dir), _env_name(name))
52
+
53
+
54
+ def pytest_arduino_cli_device_lock_key(config, port):
55
+ if port and port in _peer_addresses():
56
+ return f"rp2040-swd:{port}"
57
+ return None
58
+
59
+
60
+ @pytest.fixture(autouse=True)
61
+ def _oep_rp2040_peer_broker(request, monkeypatch):
62
+ """The DUT's ch32rv (when there is one) for the peer's writer, which runs under OpenOCD apart from pytest."""
63
+ if not _peer_addresses():
64
+ return
65
+ try:
66
+ monkeypatch.setenv("CH32RV", str(request.getfixturevalue("ch32rv")))
67
+ except pytest.FixtureLookupError:
68
+ pass
oep_client/rp2040.py ADDED
@@ -0,0 +1,68 @@
1
+ """RP2040 target knowledge for the host: its boot ROM's function lookup, the RAM a host-driven call can use, and a reboot
2
+ - the flash sequence itself is RP2350's (rp2350.Flash: the pico-sdk's hardware_flash order through the same ROM function
3
+ codes). The probe knows none of this.
4
+
5
+ Facts from the pico-sdk (boot_bootrom_headers/bootrom_constants.h, pico_bootrom/bootrom.h, regs/addressmap.h): the
6
+ ROM keeps a halfword pointer to its public function table at 0x14 and to rom_table_lookup(table, code) at 0x18; the
7
+ codes are two characters ('I', 'F') = 'I' | 'F' << 8; SRAM is 0x20000000 - 0x20042000. The RP2040 ROM has no reboot
8
+ function: the watchdog's trigger restarts the chip (USB included).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from .arm import CortexM
14
+ from . import rp2350
15
+
16
+ FUNC_TABLE_PTR, TABLE_LOOKUP_PTR = 0x14, 0x18
17
+ # RAM the target can spare while the host drives it (SRAM ends at 0x20042000; SRAM4 / 5 above 0x20040000 kept free):
18
+ # the return breakpoint, the data buffer for programming, and the stack the ROM functions run on.
19
+ BKPT_AT, BUF, BUF_BYTES, STACK_TOP = 0x20030000, 0x20031000, 16 * 1024, 0x20040000
20
+ # the watchdog (WATCHDOG_BASE 0x40058000: CTRL, SCRATCH4) and what its reset takes (PSM_BASE 0x40010000: WDSEL), as
21
+ # the pico-sdk's watchdog_reboot sets them: everything but ROSC / XOSC, SCRATCH4 0 for a normal boot
22
+ WATCHDOG_CTRL, WATCHDOG_SCRATCH4, WATCHDOG_TRIGGER = 0x40058000, 0x4005801C, 1 << 31
23
+ PSM_WDSEL, PSM_WDSEL_ALL_BUT_OSCS = 0x40010008, 0x0001FFFF & ~0x3
24
+ # TARGETSEL of the multidrop SWD (RP2040 datasheet: core 0, core 1, the rescue DP)
25
+ TARGETSEL_CORE0, TARGETSEL_CORE1, TARGETSEL_RESCUE = 0x01002927, 0x11002927, 0xF1002927
26
+
27
+
28
+ def _hword(core: CortexM, at: int) -> int:
29
+ word = core.mem.read32(at & ~3)
30
+ return (word >> (8 * (at & 3))) & 0xFFFF
31
+
32
+
33
+ class Rom:
34
+ def __init__(self, core: CortexM):
35
+ self.core = core
36
+ self._fn: dict[str, int] = {}
37
+
38
+ def lookup(self, c: str) -> int:
39
+ if c not in self._fn:
40
+ table, lookup = _hword(self.core, FUNC_TABLE_PTR), _hword(self.core, TABLE_LOOKUP_PTR)
41
+ addr = self.core.call(lookup, (table, rp2350.code(*c)))
42
+ if not 0 < addr < 0x4000:
43
+ raise LookupError(f"ROM has no function {c!r} (lookup returned {addr:#x})")
44
+ self._fn[c] = addr
45
+ return self._fn[c]
46
+
47
+ def reboot(self) -> None:
48
+ """The whole chip restarted by the watchdog's trigger, as the pico-sdk's watchdog_reboot(0, 0, 0): WDSEL set to
49
+ everything but the oscillators (left as it comes, the trigger restarted nothing: the core stayed locked up on
50
+ the return breakpoint, 2026-11-10), SCRATCH4 0 (a normal boot), not paused for debug. The core is let go
51
+ first (C_MASKINTS off: it survives every reset but power-on); the USB device re-enumerates."""
52
+ mem = self.core.mem
53
+ mem.write32(WATCHDOG_SCRATCH4, 0)
54
+ mem.write32(PSM_WDSEL, PSM_WDSEL_ALL_BUT_OSCS)
55
+ self.core.release()
56
+ mem.write32(WATCHDOG_CTRL, WATCHDOG_TRIGGER)
57
+
58
+
59
+ def flash(core: CortexM) -> rp2350.Flash:
60
+ return rp2350.Flash(core, Rom(core), BUF, BUF_BYTES)
61
+
62
+
63
+ def core_of(mem) -> CortexM:
64
+ return CortexM(mem, bkpt_at=BKPT_AT, stack_top=STACK_TOP)
65
+
66
+
67
+ def program_and_verify(core: CortexM, image: bytes, offset: int = 0, log=lambda s: None) -> bool:
68
+ return rp2350.program_and_verify(core, image, offset, log, flash(core))
@@ -0,0 +1,358 @@
1
+ """An RP2040 board as a test's peer over an OEP probe's SWD alone: written over SWD (rp2040), its console the SEGGER RTT
2
+ rings read and written through the MEM-AP while the core runs. For a peer whose native USB goes to the device under test,
3
+ so neither the USB writing nor a USB console is there.
4
+
5
+ The address is `oep://<probe>/<port>`: the probe's channels labelled `<port>.swdio` / `<port>.swclk` are the wire. When
6
+ ch32rv's broker serves that probe (the DUT's monitor started it), the session shares its lock (ch32rv's named extension
7
+ io.github.ch32-riscv-ug.share, as its own tools and pytest's oep_host do); else the probe is opened directly.
8
+
9
+ - `python -m oep_client.rp2040_peer flash oep://<probe>/<port> <image.elf|.bin>`: write, verify, reboot.
10
+ - `python -m oep_client.rp2040_peer console oep://<probe>/<port> [seconds]`: print the RTT output.
11
+ - pyserial: `serial.protocol_handler_packages.append("oep_client.pyserial")` makes `serial_for_url("oep://...")` this
12
+ console (the handler is oep_client.pyserial.protocol_oep).
13
+ - `write_openocd_hook(sketch_dir, env)`: files for arduino-pico's OpenOCD upload (uploadmethod=picoprobe_cmsis_dap)
14
+ that hand the ELF to `flash` with the address from the environment variable `env` instead of driving a CMSIS-DAP.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import os
21
+ import shutil
22
+ import struct
23
+ import subprocess
24
+ import sys
25
+ import threading
26
+ import time
27
+ import urllib.parse
28
+
29
+ from . import acquire, arm, core, link, rp2040
30
+
31
+ SHARE = "io.github.ch32-riscv-ug.share" # ch32rv's broker: share the lock its tools hold
32
+ LEASE_MS = 60000
33
+ RAM, RAM_END = 0x20000000, 0x20042000
34
+ RTT_ID = b"SEGGER RTT\0\0\0\0\0\0"
35
+ FLASH_BASE, FLASH_END = 0x10000000, 0x11000000
36
+
37
+
38
+ def parse(address: str) -> tuple[str, str]:
39
+ """oep://<probe>/<port>[?...] -> (unit id, port)."""
40
+ u = urllib.parse.urlparse(address)
41
+ port = u.path.strip("/").split("/")[0] if u.path.strip("/") else ""
42
+ if u.scheme != "oep" or not u.netloc or not port:
43
+ raise ValueError(f"{address}: want oep://<probe>/<port> (the port's labels <port>.swdio / <port>.swclk)")
44
+ return u.netloc, port
45
+
46
+
47
+ def _broker_endpoint(unit: str) -> str | None:
48
+ """ch32rv's broker for this probe, when one runs (`ch32rv broker endpoint --json`; ch32rv from $CH32RV or PATH)."""
49
+ exe = os.environ.get("CH32RV") or shutil.which("ch32rv")
50
+ if not exe:
51
+ return None
52
+ try:
53
+ r = subprocess.run([exe, "broker", "endpoint", "--probe", f"oep://{unit}", "--json"], capture_output=True,
54
+ text=True, timeout=20)
55
+ return (json.loads(r.stdout).get("result") or {}).get("endpoint")
56
+ except (OSError, ValueError, subprocess.TimeoutExpired):
57
+ return None
58
+
59
+
60
+ def open_host(unit: str, owner: str):
61
+ """A Host in a session on the probe: through ch32rv's broker sharing its lock when it runs, else directly."""
62
+ from . import host as oh, message as m
63
+ endpoint = _broker_endpoint(unit)
64
+ if endpoint:
65
+ host_, port = endpoint.rsplit(":", 1)
66
+ lk = link.SerialLink.on_stream(link.TcpStream(host_, int(port)), "length", 3.0)
67
+ h = oh.Host(lk.send)
68
+ lk.attach_host(h)
69
+ h.link = lk # closed with the session, as a direct one's
70
+ try:
71
+ h.open(LEASE_MS, owner=owner, extensions=[SHARE])
72
+ except oh.Unsupported as e:
73
+ if e.tag != m.TAG_NAMED_EXTENSION | m.TAG_CRITICAL:
74
+ raise
75
+ h.open(LEASE_MS, owner=owner)
76
+ return h
77
+ h = link.open_host(f"oep://{unit}?via=usb")
78
+ h.open(LEASE_MS, owner=owner)
79
+ return h
80
+
81
+
82
+ class Target:
83
+ """The RP2040's core 0 on the port's SWD pair: wire, connection, MEM-AP, CortexM."""
84
+
85
+ def __init__(self, h, port: str):
86
+ swdio, swclk = acquire._channels(core, h, [f"{port}.swdio", f"{port}.swclk"])
87
+ self.host, self.wire = h, arm.SwdWire(h)
88
+ self.conn, self.dpidr, _ = self.wire.attach(targetsel=rp2040.TARGETSEL_CORE0, pins=(swdio, swclk))
89
+ adi = arm.ArmAdi(h, self.conn)
90
+ adi.power_up()
91
+ self.mem = arm.MemAp(adi, 0)
92
+ self.core = rp2040.core_of(self.mem)
93
+
94
+ def close(self) -> None:
95
+ try:
96
+ self.wire.detach(self.conn)
97
+ except Exception:
98
+ pass
99
+
100
+
101
+ def image_of(path: str) -> bytes:
102
+ """A flash image from 0x10000000: a .bin as it is, an ELF's loadable segments in flash laid out by address."""
103
+ data = open(path, "rb").read()
104
+ if data[:4] != b"\x7fELF":
105
+ return data
106
+ if data[4] != 1 or data[5] != 1:
107
+ raise ValueError(f"{path}: want a 32-bit little-endian ELF")
108
+ phoff, = struct.unpack_from("<I", data, 28)
109
+ phentsize, phnum = struct.unpack_from("<HH", data, 42)
110
+ parts = []
111
+ for i in range(phnum):
112
+ p_type, p_offset, _vaddr, p_paddr, p_filesz = struct.unpack_from("<IIIII", data, phoff + i * phentsize)
113
+ if p_type == 1 and p_filesz and FLASH_BASE <= p_paddr < FLASH_END:
114
+ parts.append((p_paddr, data[p_offset:p_offset + p_filesz]))
115
+ if not parts:
116
+ raise ValueError(f"{path}: no loadable segment in flash")
117
+ end = max(a + len(b) for a, b in parts)
118
+ image = bytearray(b"\xff" * (end - FLASH_BASE))
119
+ for a, b in parts:
120
+ image[a - FLASH_BASE:a - FLASH_BASE + len(b)] = b
121
+ return bytes(image)
122
+
123
+
124
+ def clear_rtt_ids(mem) -> int:
125
+ """Wipe every SEGGER RTT control block's ID left in RAM (RAM outlives a reboot): a console opened after the reboot
126
+ then finds only the block the new image writes, never the old one - whose offsets, written back while the new
127
+ image set them up again, lost its first lines. The core must be halted. -> how many were wiped."""
128
+ n, chunk = 0, 16 * 1024
129
+ for at in range(RAM, RAM_END, chunk):
130
+ words = mem.read_block(at, min(chunk, RAM_END - at) // 4)
131
+ raw = struct.pack(f"<{len(words)}I", *words)
132
+ i = raw.find(RTT_ID)
133
+ while i >= 0:
134
+ if i % 4 == 0:
135
+ mem.write32(at + i, 0)
136
+ n += 1
137
+ i = raw.find(RTT_ID, i + 1)
138
+ return n
139
+
140
+
141
+ def flash(address: str, path: str, log=print) -> None:
142
+ """Write the image over SWD, verify it, reboot the chip. Raises on a mismatch."""
143
+ unit, port = parse(address)
144
+ image = image_of(path)
145
+ h = open_host(unit, f"rp2040 flash {port}")
146
+ try:
147
+ t = Target(h, port)
148
+ try:
149
+ t.core.halt()
150
+ t.core.reset_halt() # a clean core to call the ROM from
151
+ if not rp2040.program_and_verify(t.core, image, 0, log=log):
152
+ raise RuntimeError(f"{address}: the flash did not read back as written ({len(image)} B)")
153
+ log(f"{address}: {len(image)} B written and verified")
154
+ clear_rtt_ids(t.mem)
155
+ rp2040.Rom(t.core).reboot()
156
+ finally:
157
+ t.close()
158
+ finally:
159
+ try:
160
+ h.end()
161
+ finally:
162
+ h.link.close()
163
+
164
+
165
+ class Rtt:
166
+ """SEGGER RTT's up buffer 0 (target -> host) and down buffer 0 (host -> target) through the MEM-AP, the core
167
+ running. The control block is found by its ID in RAM (it is written at run time; `wait` seconds for it)."""
168
+
169
+ def __init__(self, mem, wait: float = 5.0):
170
+ self.mem = mem
171
+ self.cb = self._find(wait)
172
+ nup, ndown = mem.read_block(self.cb + 16, 2)
173
+ self.up = self.cb + 24 # aUp[0]: sName pBuffer SizeOfBuffer WrOff RdOff Flags
174
+ self.down = self.cb + 24 + 24 * nup # aDown[0]
175
+ _, self.up_buf, self.up_size, _, _, _ = mem.read_block(self.up, 6)
176
+ _, self.down_buf, self.down_size, _, _, _ = mem.read_block(self.down, 6) if ndown else (0, 0, 0, 0, 0, 0)
177
+
178
+ def _find(self, wait: float) -> int:
179
+ deadline = time.monotonic() + wait
180
+ chunk = 16 * 1024
181
+ while True:
182
+ for at in range(RAM, RAM_END, chunk):
183
+ words = self.mem.read_block(at, min(chunk, RAM_END - at) // 4)
184
+ raw = struct.pack(f"<{len(words)}I", *words)
185
+ i = raw.find(RTT_ID)
186
+ while i >= 0:
187
+ if i % 4 == 0:
188
+ return at + i
189
+ i = raw.find(RTT_ID, i + 1)
190
+ if time.monotonic() > deadline:
191
+ raise RuntimeError("no SEGGER RTT control block in RAM (is the sketch using RTT and running?)")
192
+ time.sleep(0.2)
193
+
194
+ def read(self) -> bytes:
195
+ wr, rd = self.mem.read_block(self.up + 12, 2)
196
+ if wr == rd or not self.up_size:
197
+ return b""
198
+ base = self.up_buf & ~3 # the buffer need not be word aligned; block reads are
199
+ skew = self.up_buf - base
200
+ words = self.mem.read_block(base, (skew + self.up_size + 3) // 4)
201
+ buf = struct.pack(f"<{len(words)}I", *words)[skew:skew + self.up_size]
202
+ out = buf[rd:wr] if wr > rd else buf[rd:self.up_size] + buf[:wr]
203
+ self.mem.write32(self.up + 16, wr)
204
+ return out
205
+
206
+ def write(self, data: bytes) -> int:
207
+ if not self.down_size:
208
+ return 0
209
+ wr, rd = self.mem.read_block(self.down + 12, 2)
210
+ room = (rd - wr - 1) % self.down_size
211
+ data = data[:room]
212
+ for b in data:
213
+ at = self.down_buf + wr
214
+ word = self.mem.read32(at & ~3)
215
+ sh = 8 * (at & 3)
216
+ self.mem.write32(at & ~3, (word & ~(0xFF << sh)) | (b << sh))
217
+ wr = (wr + 1) % self.down_size
218
+ self.mem.write32(self.down + 12, wr)
219
+ return len(data)
220
+
221
+
222
+ class Console:
223
+ """The peer's console: a session, the SWD connection and the RTT rings, polled in a thread into a buffer."""
224
+
225
+ POLL_S = 0.01
226
+
227
+ def __init__(self, address: str, wait: float = 10.0):
228
+ unit, port = parse(address)
229
+ self.address = address
230
+ self.host = open_host(unit, f"rp2040 console {port}")
231
+ self.target = Target(self.host, port)
232
+ self.rtt = Rtt(self.target.mem, wait)
233
+ self._buf = bytearray()
234
+ self._out = bytearray()
235
+ self._lock = threading.Lock()
236
+ self._stop = False
237
+ self.error: Exception | None = None
238
+ self._thread = threading.Thread(target=self._run, name="rp2040-rtt", daemon=True)
239
+ self._thread.start()
240
+
241
+ def _run(self) -> None:
242
+ last = time.monotonic()
243
+ while not self._stop:
244
+ try:
245
+ with self._lock:
246
+ out, self._out = bytes(self._out), bytearray()
247
+ while out:
248
+ n = self.rtt.write(out)
249
+ out = out[n:]
250
+ if out:
251
+ time.sleep(self.POLL_S)
252
+ data = self.rtt.read()
253
+ if data:
254
+ with self._lock:
255
+ self._buf += data
256
+ if time.monotonic() - last > 5: # the lease: polling is requests, but keep it explicit
257
+ last = time.monotonic()
258
+ except Exception as e: # the console's end: kept for the reader to see
259
+ self.error = e
260
+ return
261
+ time.sleep(self.POLL_S)
262
+
263
+ def read(self, size: int = -1) -> bytes:
264
+ with self._lock:
265
+ n = len(self._buf) if size < 0 else min(size, len(self._buf))
266
+ data = bytes(self._buf[:n])
267
+ del self._buf[:n]
268
+ return data
269
+
270
+ def in_waiting(self) -> int:
271
+ with self._lock:
272
+ return len(self._buf)
273
+
274
+ def write(self, data: bytes) -> int:
275
+ with self._lock:
276
+ self._out += data
277
+ return len(data)
278
+
279
+ def close(self) -> None:
280
+ self._stop = True
281
+ self._thread.join(2)
282
+ self.target.close()
283
+ try:
284
+ self.host.end()
285
+ except Exception:
286
+ pass
287
+ close = getattr(getattr(self.host, "link", None), "close", None)
288
+ if close:
289
+ close()
290
+
291
+
292
+ OPENOCD_TARGET = """# generated by oep_client.rp2040_peer.write_openocd_hook: arduino-pico's OpenOCD upload hands the ELF to the OEP
293
+ # probe's SWD instead of a CMSIS-DAP (the address from the environment variable {env})
294
+ rename init _oep_orig_init
295
+ proc init {{}} {{}}
296
+ rename adapter _oep_orig_adapter
297
+ proc adapter {{args}} {{}}
298
+ rename program _oep_orig_program
299
+ proc program {{file args}} {{
300
+ if {{![info exists ::env({env})]}} {{ error "set {env} to the peer's oep://<probe>/<port>" }}
301
+ echo [exec {python} -m oep_client.rp2040_peer flash $::env({env}) $file 2>@1]
302
+ }}
303
+ proc reset {{args}} {{}}
304
+ """
305
+
306
+
307
+ def write_openocd_hook(sketch_dir: str, env: str, python: str | None = None) -> list[str]:
308
+ """interface/cmsis-dap.cfg and target/rp2040.cfg in the peer's sketch directory (OpenOCD looks in its cwd, the
309
+ sketch directory, before its own scripts): they turn arduino-pico's `program` into `flash` here. Generated
310
+ files - keep them out of git. -> the paths written."""
311
+ python = python or sys.executable
312
+ out = []
313
+ for rel, text in (("interface/cmsis-dap.cfg", "# generated by oep_client.rp2040_peer: no adapter (target/rp2040.cfg flashes)\n"),
314
+ ("target/rp2040.cfg", OPENOCD_TARGET.format(env=env, python=python))):
315
+ path = os.path.join(sketch_dir, rel)
316
+ os.makedirs(os.path.dirname(path), exist_ok=True)
317
+ with open(path, "w") as f:
318
+ f.write(text)
319
+ out.append(path)
320
+ return out
321
+
322
+
323
+ def main(argv=None) -> int:
324
+ import argparse
325
+ ap = argparse.ArgumentParser(prog="python -m oep_client.rp2040_peer", description=__doc__.split("\n\n")[0])
326
+ sub = ap.add_subparsers(dest="cmd", required=True)
327
+ f = sub.add_parser("flash", help="write an image over SWD, verify, reboot")
328
+ f.add_argument("address")
329
+ f.add_argument("image")
330
+ c = sub.add_parser("console", help="print the RTT output")
331
+ c.add_argument("address")
332
+ c.add_argument("seconds", type=float, nargs="?", default=5.0)
333
+ a = ap.parse_args(argv)
334
+ if a.cmd == "flash":
335
+ try:
336
+ flash(a.address, a.image)
337
+ except Exception as e:
338
+ print(f"rp2040_peer flash: {e}", file=sys.stderr)
339
+ return 1
340
+ return 0
341
+ con = Console(a.address)
342
+ try:
343
+ t = time.monotonic()
344
+ while time.monotonic() - t < a.seconds:
345
+ data = con.read()
346
+ if data:
347
+ sys.stdout.write(data.decode(errors="replace"))
348
+ sys.stdout.flush()
349
+ if con.error:
350
+ raise con.error
351
+ time.sleep(0.05)
352
+ finally:
353
+ con.close()
354
+ return 0
355
+
356
+
357
+ if __name__ == "__main__":
358
+ sys.exit(main())
oep_client/rp2350.py CHANGED
@@ -48,8 +48,11 @@ class Rom:
48
48
 
49
49
 
50
50
  class Flash:
51
- def __init__(self, core: CortexM):
52
- self.core, self.rom = core, Rom(core)
51
+ """The pico-sdk's hardware_flash order through the ROM. `rom`, `buf` / `buf_bytes`: another chip's ROM lookup and
52
+ the RAM it can spare (rp2040 hands in its own); RP2350's by default."""
53
+ def __init__(self, core: CortexM, rom=None, buf: int = BUF, buf_bytes: int = BUF_BYTES):
54
+ self.core, self.rom = core, rom or Rom(core)
55
+ self.buf, self.buf_bytes = buf, buf_bytes
53
56
 
54
57
  def program(self, offset: int, data: bytes, log=lambda s: None) -> bytes:
55
58
  """Erase and program at flash `offset` (sector aligned). Leaves the flash in command-XIP mode: reads through
@@ -64,10 +67,10 @@ class Flash:
64
67
  self.core.call(fn["EX"]) # flash_exit_xip
65
68
  self.core.call(fn["RE"], (offset, erase, BLOCK, BLOCK_ERASE_CMD), timeout=120) # flash_range_erase
66
69
  t1 = time.monotonic()
67
- for at in range(0, len(data), BUF_BYTES):
68
- chunk = data[at:at + BUF_BYTES]
69
- self.core.mem.write_block(BUF, list(struct.unpack(f"<{len(chunk) // 4}I", chunk)))
70
- self.core.call(fn["RP"], (offset + at, BUF, len(chunk)), timeout=30) # flash_range_program
70
+ for at in range(0, len(data), self.buf_bytes):
71
+ chunk = data[at:at + self.buf_bytes]
72
+ self.core.mem.write_block(self.buf, list(struct.unpack(f"<{len(chunk) // 4}I", chunk)))
73
+ self.core.call(fn["RP"], (offset + at, self.buf, len(chunk)), timeout=30) # flash_range_program
71
74
  t2 = time.monotonic()
72
75
  self.core.call(fn["FC"]) # flash_flush_cache
73
76
  self.core.call(fn["CX"]) # flash_enter_cmd_xip
@@ -79,7 +82,7 @@ class Flash:
79
82
  return struct.pack(f"<{len(words)}I", *words)[:length]
80
83
 
81
84
 
82
- def program_and_verify(core: CortexM, image: bytes, offset: int = 0, log=lambda s: None) -> bool:
83
- flash = Flash(core)
85
+ def program_and_verify(core: CortexM, image: bytes, offset: int = 0, log=lambda s: None, flash: Flash | None = None) -> bool:
86
+ flash = flash or Flash(core)
84
87
  padded = flash.program(offset, image, log)
85
88
  return flash.read(offset, len(padded)) == padded
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oep-client-python
3
- Version: 0.0.52
3
+ Version: 0.0.53
4
4
  Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a virtual bench
5
5
  Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
6
6
  Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
@@ -1,7 +1,7 @@
1
- oep_client/__init__.py,sha256=oESQSHu9kyVEvWFQpmYspr8nOwcnd5kfwBCCvSasDzI,164
1
+ oep_client/__init__.py,sha256=h7kpeApyfQHsMxED1_KZ6bWVGpyXHIWTutC3cmwSr7o,164
2
2
  oep_client/__main__.py,sha256=kbXBtEmFdhRXbVqXEbWCO-_5aDa-KyqMcW2HLZb5fjc,31510
3
3
  oep_client/acquire.py,sha256=CXjK5s-kjdCoGg9Yt_46rr4te6T2fbX4tl89PjOnURA,102541
4
- oep_client/arm.py,sha256=SszSLae8zErD4T-a1uQ6s6o3Lz7pe2SR8d3Qy-wi5rY,15633
4
+ oep_client/arm.py,sha256=zsgq4jsGzroE_9yF5D48rJF7dAe07MR8RY4RMCgjiwc,16411
5
5
  oep_client/capture.py,sha256=92ZznVWu2i3_5mIk0bMSIeqNqJ0LpjbMi3JRb2qrYrs,75848
6
6
  oep_client/capture_blocks.py,sha256=B8T2dKozQfGTWg4noG0cZ0DZ9KrrfiFFdbTrBKWfWhY,17330
7
7
  oep_client/catalog.py,sha256=WkaksNn9cFvkTRnvtX4I9g1AvkFoldEiNQaKiyPQ7_E,10772
@@ -27,9 +27,12 @@ oep_client/names.py,sha256=ohrRvQNURraYyJ-BtjHRwxGt_GLq-t8IO0aOji9Dhe0,3531
27
27
  oep_client/pins.py,sha256=iXswbqvze0KED1WNHLnjG5sn-_SjoJrIvcjj84LU_jU,53670
28
28
  oep_client/probe_log.py,sha256=gAamfON2M7lMH3xOn4Wpmes_Wp0H4c2EBNbpXjIvCMM,4927
29
29
  oep_client/pytest_conformance.py,sha256=-bcJU2E1UE4xjQ7HZjaIgtp0l65cIibBTNFnaW2tews,2929
30
+ oep_client/pytest_rp2040_peer.py,sha256=9buM_lpXx9lhi-H-tIWAfS5xyDmgGUBwfHlOkBG3jiA,2873
30
31
  oep_client/registry.py,sha256=1oFMpnbys1Iqih592ueUn8kru56O4eTCYNhdIQfDIP0,22706
31
32
  oep_client/riscv.py,sha256=1QloF51Uci2-6gonkBxQNh9XleF6yCvKG-L7XNm4sgI,60460
32
- oep_client/rp2350.py,sha256=7OMyj270k3AY1e3-ljicPwOjf9jOlokbT6QNkA0b0yY,4227
33
+ oep_client/rp2040.py,sha256=m_3eOWxrZOTWB7TGYv2_OmtfHREBdwCfdoVXjP_Qq0A,3392
34
+ oep_client/rp2040_peer.py,sha256=BQc6H79wPhjEcj0sl4O5mDPH_YZIHB2FbrZ8vMdxdg8,14505
35
+ oep_client/rp2350.py,sha256=DaiJcbAsIh22y7Xt6zdWxMA9V7dLzXkNkt68a0UDs8E,4589
33
36
  oep_client/settings.py,sha256=wVRpvAmcvDygVE9PCbo4-7k-tq6pIzKh7VRlmtLO1Fk,26391
34
37
  oep_client/speed_record.py,sha256=w24GH3gZVh-TbjKrgo2yfTJSdx0ene-VNEwHmZMSwXY,7260
35
38
  oep_client/stimulus.py,sha256=pfX4xLjyG3UWIJRWUHnkg9c1ueL1MeozHj3CnlJ7dpA,6072
@@ -254,8 +257,10 @@ oep_client/conformance/wire_options_sample.py,sha256=dog-yvUcu693M8b51tA5TnhAnKr
254
257
  oep_client/conformance/writer_lease.py,sha256=5wDd4XIJWFPFlRY91wHlJs5U7ANp3xPpGgU2Eki7e8g,7331
255
258
  oep_client/conformance/writer_lease_model.py,sha256=FxhDqqzdFOa_SSHSvjdQFh49Wn3YuZ98ISSlRRxrq9E,1721
256
259
  oep_client/conformance/writer_lease_sample.py,sha256=oowRwYUNAE4f6rEE2HmYlSS9dDWE0TGegkcsqVH8Nak,455
257
- oep_client_python-0.0.52.dist-info/METADATA,sha256=1YkxWYHWTjB64j5HBr46QiZ0-1O9Hxl6LVHZQD8b1h4,55222
258
- oep_client_python-0.0.52.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
259
- oep_client_python-0.0.52.dist-info/entry_points.txt,sha256=XEt39Jz7oQyU9Hfg_tV6eDGofNxWYrjSfslxKP-fh70,135
260
- oep_client_python-0.0.52.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
261
- oep_client_python-0.0.52.dist-info/RECORD,,
260
+ oep_client/pyserial/__init__.py,sha256=JSsbRkLYgFVN8V4H9ovdqyDr6JW3P3qi4RYoElg050o,121
261
+ oep_client/pyserial/protocol_oep.py,sha256=3dC8nbtddNG2lfonLUkbBTKfsfJoNHFfabuedn0n7Ho,2497
262
+ oep_client_python-0.0.53.dist-info/METADATA,sha256=M2NZFKa5D1kJLOC8gBnV2OxzEnq0klAORQTOKBYaFxI,55222
263
+ oep_client_python-0.0.53.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
264
+ oep_client_python-0.0.53.dist-info/entry_points.txt,sha256=XEt39Jz7oQyU9Hfg_tV6eDGofNxWYrjSfslxKP-fh70,135
265
+ oep_client_python-0.0.53.dist-info/licenses/LICENSE,sha256=ntlalztb3L1OaNAxmQdHlubu5fVhjMc9oRZi3PeQhXg,1076
266
+ oep_client_python-0.0.53.dist-info/RECORD,,