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 +1 -1
- oep_client/arm.py +13 -0
- oep_client/pyserial/__init__.py +1 -0
- oep_client/pyserial/protocol_oep.py +92 -0
- oep_client/pytest_rp2040_peer.py +68 -0
- oep_client/rp2040.py +68 -0
- oep_client/rp2040_peer.py +358 -0
- oep_client/rp2350.py +11 -8
- {oep_client_python-0.0.52.dist-info → oep_client_python-0.0.53.dist-info}/METADATA +1 -1
- {oep_client_python-0.0.52.dist-info → oep_client_python-0.0.53.dist-info}/RECORD +13 -8
- {oep_client_python-0.0.52.dist-info → oep_client_python-0.0.53.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.52.dist-info → oep_client_python-0.0.53.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.52.dist-info → oep_client_python-0.0.53.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
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
|
-
|
|
52
|
-
|
|
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),
|
|
68
|
-
chunk = data[at:at +
|
|
69
|
-
self.core.mem.write_block(
|
|
70
|
-
self.core.call(fn["RP"], (offset + at,
|
|
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.
|
|
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=
|
|
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=
|
|
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/
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
oep_client_python-0.0.
|
|
260
|
-
oep_client_python-0.0.
|
|
261
|
-
oep_client_python-0.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|