oep-client-python 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oep_client/__init__.py +5 -0
- oep_client/v1/__init__.py +1 -0
- oep_client/v1/__main__.py +40 -0
- oep_client/v1/arm.py +269 -0
- oep_client/v1/capture.py +365 -0
- oep_client/v1/catalog.py +181 -0
- oep_client/v1/ch32_flash.py +254 -0
- oep_client/v1/cobs.py +73 -0
- oep_client/v1/console.py +165 -0
- oep_client/v1/core.py +172 -0
- oep_client/v1/decode.py +65 -0
- oep_client/v1/dump.py +157 -0
- oep_client/v1/endpoint.py +1479 -0
- oep_client/v1/esp32_targets.py +137 -0
- oep_client/v1/fake.py +233 -0
- oep_client/v1/fake_serial.py +105 -0
- oep_client/v1/fake_serve.py +299 -0
- oep_client/v1/fixture.py +121 -0
- oep_client/v1/frames.py +91 -0
- oep_client/v1/hid_stream.py +257 -0
- oep_client/v1/host.py +321 -0
- oep_client/v1/interfaces.py +119 -0
- oep_client/v1/link.py +414 -0
- oep_client/v1/message.py +235 -0
- oep_client/v1/names.py +86 -0
- oep_client/v1/registry.py +59 -0
- oep_client/v1/riscv.py +430 -0
- oep_client/v1/rp2350.py +85 -0
- oep_client/v1/target.py +16 -0
- oep_client/v1/uiapduino.py +121 -0
- oep_client/v1/usb_stream.py +209 -0
- oep_client_python-0.0.1.dist-info/METADATA +106 -0
- oep_client_python-0.0.1.dist-info/RECORD +36 -0
- oep_client_python-0.0.1.dist-info/WHEEL +4 -0
- oep_client_python-0.0.1.dist-info/entry_points.txt +2 -0
- oep_client_python-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""The ESP32 I2C / SPI slave tools of oep-probe-arduino, custom interfaces revision 1:
|
|
2
|
+
io.github.ch32-riscv-ug.esp32.i2c-target and io.github.ch32-riscv-ug.esp32.spi-target (OepP4I2cTarget.h /
|
|
3
|
+
OepP4SpiTarget.h). A DUT's I2C / SPI controller talks to them; the host arms what they answer and reads what they got.
|
|
4
|
+
Variable byte lists carry a count(u16) in front (oep-core §2.3)."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import struct
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
from . import host as h, message as m
|
|
12
|
+
from .core import Interface
|
|
13
|
+
|
|
14
|
+
NS = "io.github.ch32-riscv-ug.esp32"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class I2cStatus:
|
|
19
|
+
flags: int # bit0 started, bits 1-2 mode, bit4 armed, bits 5-7 frames queued
|
|
20
|
+
rx_frames: int
|
|
21
|
+
tx_slots: int
|
|
22
|
+
errors: int
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class I2cRegisters:
|
|
27
|
+
sr: int
|
|
28
|
+
int_raw: int
|
|
29
|
+
fifo_st: int
|
|
30
|
+
ctr: int
|
|
31
|
+
slave_addr: int
|
|
32
|
+
filter_cfg: int
|
|
33
|
+
scl_stretch_conf: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class I2cTarget(Interface):
|
|
37
|
+
"""The I2C target (E147-E150 contract): fixed rx (arm_rx with the exact length), framed rx (a 1-byte length header
|
|
38
|
+
transaction, then the payload), preloaded tx (slots the controller reads)."""
|
|
39
|
+
NAME = f"{NS}.i2c-target"
|
|
40
|
+
REVISION = 1
|
|
41
|
+
CONFIGURE, ARM_RX, READ_RX, PRELOAD_TX, STATUS, RESET, READ_HW, SET_STRETCH = 1, 2, 3, 4, 5, 6, 0x10, 0x11
|
|
42
|
+
MODE_FIXED_RX, MODE_FRAMED_RX, MODE_PRELOADED_TX = 1, 2, 3
|
|
43
|
+
ROLE_SDA, ROLE_SCL = 1, 2
|
|
44
|
+
|
|
45
|
+
def __init__(self, hst: h.Host, fn: int | None = None, name: str | None = None):
|
|
46
|
+
super().__init__(hst, name, fn=fn)
|
|
47
|
+
|
|
48
|
+
def assignments(self, sda: int, scl: int) -> list[tuple[int, int, int]]:
|
|
49
|
+
return [(self.fn, self.ROLE_SDA, sda), (self.fn, self.ROLE_SCL, scl)]
|
|
50
|
+
|
|
51
|
+
def configure(self, address: int, mode: int) -> None:
|
|
52
|
+
self._call(self.CONFIGURE, struct.pack("<BB", address, mode))
|
|
53
|
+
|
|
54
|
+
def arm_rx(self, length: int) -> None:
|
|
55
|
+
self._call(self.ARM_RX, struct.pack("<H", length))
|
|
56
|
+
|
|
57
|
+
def read_rx(self) -> tuple[int, bytes]:
|
|
58
|
+
"""-> (frames still queued after this one, the oldest frame or b"")."""
|
|
59
|
+
rd = m.Reader(self._call(self.READ_RX).payload)
|
|
60
|
+
pending, count = rd.take("BH")
|
|
61
|
+
data = rd.bytes(count)
|
|
62
|
+
rd.tail()
|
|
63
|
+
return pending, data
|
|
64
|
+
|
|
65
|
+
def preload_tx(self, data: bytes) -> int:
|
|
66
|
+
"""-> the slots preloaded so far."""
|
|
67
|
+
rd = m.Reader(self._call(self.PRELOAD_TX, struct.pack("<H", len(data)) + data).payload)
|
|
68
|
+
slots = rd.u8()
|
|
69
|
+
rd.tail()
|
|
70
|
+
return slots
|
|
71
|
+
|
|
72
|
+
def status(self) -> I2cStatus:
|
|
73
|
+
rd = m.Reader(self._call(self.STATUS, locked=False).payload)
|
|
74
|
+
s = I2cStatus(*rd.take("BIBH"))
|
|
75
|
+
rd.tail()
|
|
76
|
+
return s
|
|
77
|
+
|
|
78
|
+
def reset(self) -> None:
|
|
79
|
+
self._call(self.RESET)
|
|
80
|
+
|
|
81
|
+
def set_stretch(self, stretch_us: int) -> None:
|
|
82
|
+
"""Hold every hardware SCL stretch for stretch_us (0 = off); takes effect at the next configure()."""
|
|
83
|
+
self._call(self.SET_STRETCH, struct.pack("<I", stretch_us))
|
|
84
|
+
|
|
85
|
+
def read_hw(self) -> I2cRegisters:
|
|
86
|
+
"""Debug view of the P4 I2C0 block (zeros on other chips)."""
|
|
87
|
+
rd = m.Reader(self._call(self.READ_HW, locked=False).payload)
|
|
88
|
+
r = I2cRegisters(*rd.take("7I"))
|
|
89
|
+
rd.tail()
|
|
90
|
+
return r
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class SpiStatus:
|
|
95
|
+
flags: int # bit0 started, bit1 armed, bits 2-3 mode, bit4 LSB first, bits 5-7 queued
|
|
96
|
+
transactions: int
|
|
97
|
+
errors: int
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class SpiTarget(Interface):
|
|
101
|
+
"""The SPI target (SPI2_HOST, no DMA, <= 64 bytes): one CS-framed transaction at a time - arm() with the MISO
|
|
102
|
+
bytes, then read_rx() after the controller raised CS."""
|
|
103
|
+
NAME = f"{NS}.spi-target"
|
|
104
|
+
REVISION = 1
|
|
105
|
+
CONFIGURE, ARM, READ_RX, STATUS, RESET = 1, 2, 3, 4, 5
|
|
106
|
+
ROLE_SCK, ROLE_MOSI, ROLE_MISO, ROLE_CS = 1, 2, 3, 4
|
|
107
|
+
MSB_FIRST, LSB_FIRST = 0, 1
|
|
108
|
+
|
|
109
|
+
def __init__(self, hst: h.Host, fn: int | None = None, name: str | None = None):
|
|
110
|
+
super().__init__(hst, name, fn=fn)
|
|
111
|
+
|
|
112
|
+
def assignments(self, sck: int, mosi: int, miso: int, cs: int) -> list[tuple[int, int, int]]:
|
|
113
|
+
return [(self.fn, self.ROLE_SCK, sck), (self.fn, self.ROLE_MOSI, mosi), (self.fn, self.ROLE_MISO, miso),
|
|
114
|
+
(self.fn, self.ROLE_CS, cs)]
|
|
115
|
+
|
|
116
|
+
def configure(self, mode: int = 0, bit_order: int = 0) -> None:
|
|
117
|
+
self._call(self.CONFIGURE, struct.pack("<BB", mode, bit_order))
|
|
118
|
+
|
|
119
|
+
def arm(self, length: int, tx: bytes = b"") -> None:
|
|
120
|
+
self._call(self.ARM, struct.pack("<HH", length, len(tx)) + tx)
|
|
121
|
+
|
|
122
|
+
def read_rx(self) -> tuple[int, int, bytes]:
|
|
123
|
+
"""-> (transactions still queued, bits clocked, the MOSI bytes) of the oldest finished transaction."""
|
|
124
|
+
rd = m.Reader(self._call(self.READ_RX).payload)
|
|
125
|
+
pending, bits, count = rd.take("BIH")
|
|
126
|
+
data = rd.bytes(count)
|
|
127
|
+
rd.tail()
|
|
128
|
+
return pending, bits, data
|
|
129
|
+
|
|
130
|
+
def status(self) -> SpiStatus:
|
|
131
|
+
rd = m.Reader(self._call(self.STATUS, locked=False).payload)
|
|
132
|
+
s = SpiStatus(*rd.take("BIH"))
|
|
133
|
+
rd.tail()
|
|
134
|
+
return s
|
|
135
|
+
|
|
136
|
+
def reset(self) -> None:
|
|
137
|
+
self._call(self.RESET)
|
oep_client/v1/fake.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""In-process fake probes that declare capabilities in the draft wire forms (no hardware).
|
|
2
|
+
|
|
3
|
+
Each profile is a list of offered interfaces with their describe TLVs. The fake answers the core
|
|
4
|
+
operations - confirm, list, describe - by encoding real payloads and paging them to its max_frame,
|
|
5
|
+
so what `dump` shows is what a host would decode from a probe of that shape.
|
|
6
|
+
|
|
7
|
+
The profiles are EXAMPLES of declarations, not a decision about which capabilities are standard.
|
|
8
|
+
Wire forms: oep-core §7 (confirm with a revision range, list first / total u16 with oep.core as the first
|
|
9
|
+
entry, describe first u16).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import struct
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
|
|
17
|
+
from . import catalog, message as m, names, registry as reg
|
|
18
|
+
from .catalog import (CHANNEL_GROUP, FEATURES, IMPLEMENTATION, MAX_CLOCK_HZ, MAX_LENGTH,
|
|
19
|
+
MIN_CLOCK_HZ, ListEntry)
|
|
20
|
+
|
|
21
|
+
CORE_FN = 0
|
|
22
|
+
OP_CONFIRM, OP_LIST, OP_DESCRIBE = 0x01, 0x02, 0x03
|
|
23
|
+
RESULT_HEADER = 5 # role(1) correlation(2) resolution(1) detail(1) in front of every payload
|
|
24
|
+
REVISION = 1 # the protocol revision confirm reports
|
|
25
|
+
WINDOW, MAX_INFLIGHT = 4096, 4
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Offered:
|
|
30
|
+
fn: int
|
|
31
|
+
instance: int
|
|
32
|
+
name: str
|
|
33
|
+
tlvs: tuple[bytes, ...] = ()
|
|
34
|
+
revision: int = 1
|
|
35
|
+
flags: int = 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class FakeProbe:
|
|
39
|
+
def __init__(self, label: str, max_frame: int, offered: list[Offered]):
|
|
40
|
+
self.label = label
|
|
41
|
+
self.max_frame = max_frame
|
|
42
|
+
self.offered = sorted(offered, key=lambda o: o.fn)
|
|
43
|
+
self.requests = 0
|
|
44
|
+
for o in self.offered:
|
|
45
|
+
names.validate(o.name)
|
|
46
|
+
|
|
47
|
+
# The one entry point a transport would call: (fn, op, payload) -> result payload.
|
|
48
|
+
def call(self, fn: int, op: int, payload: bytes = b"") -> bytes:
|
|
49
|
+
self.requests += 1
|
|
50
|
+
if fn != CORE_FN:
|
|
51
|
+
raise ValueError(f"fake: fn {fn} has no operations here")
|
|
52
|
+
if op == OP_CONFIRM:
|
|
53
|
+
if len(payload) < 6 or payload[:4] != m.CONFIRM_REQUEST:
|
|
54
|
+
raise ValueError("fake: confirm needs \"OEP?\" min_rev max_rev")
|
|
55
|
+
if not payload[4] <= REVISION <= payload[5]:
|
|
56
|
+
raise LookupError(f"fake: no revision in {payload[4]}..{payload[5]}")
|
|
57
|
+
return struct.pack("<4sBBHIB", m.CONFIRM_RESULT, REVISION, 0, self.max_frame, WINDOW, MAX_INFLIGHT)
|
|
58
|
+
if op == OP_LIST:
|
|
59
|
+
return self._list(*catalog.unpack_list_request(payload)[:3])
|
|
60
|
+
if op == OP_DESCRIBE:
|
|
61
|
+
if len(payload) < 4:
|
|
62
|
+
raise ValueError("fake: describe needs fn(u16) first(u16)")
|
|
63
|
+
target, first = struct.unpack_from("<HH", payload)
|
|
64
|
+
return self._describe(target, first)
|
|
65
|
+
raise ValueError(f"fake: core op 0x{op:02x} unknown")
|
|
66
|
+
|
|
67
|
+
def _list(self, prefix: str, exact: bool, first: int) -> bytes:
|
|
68
|
+
hits = [o for o in self.offered if names.matches(o.name, prefix, exact)]
|
|
69
|
+
budget = self.max_frame - RESULT_HEADER - 3
|
|
70
|
+
page, used = [], 0
|
|
71
|
+
for o in hits[first:]:
|
|
72
|
+
size = len(catalog.pack_entry(self._entry(o)))
|
|
73
|
+
if page and used + size > budget:
|
|
74
|
+
break
|
|
75
|
+
page.append(self._entry(o))
|
|
76
|
+
used += size
|
|
77
|
+
return catalog.pack_list_result(len(hits), page)
|
|
78
|
+
|
|
79
|
+
def _describe(self, fn: int, first: int) -> bytes:
|
|
80
|
+
o = next((x for x in self.offered if x.fn == fn), None)
|
|
81
|
+
if o is None:
|
|
82
|
+
raise ValueError(f"fake: fn {fn} not offered")
|
|
83
|
+
budget = self.max_frame - RESULT_HEADER - 1
|
|
84
|
+
out, sent = b"", 0
|
|
85
|
+
for t in o.tlvs[first:]:
|
|
86
|
+
if out and len(out) + len(t) > budget:
|
|
87
|
+
break
|
|
88
|
+
out += t
|
|
89
|
+
sent += 1
|
|
90
|
+
more = 1 if first + sent < len(o.tlvs) else 0
|
|
91
|
+
return bytes([more]) + out
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _entry(o: Offered) -> ListEntry:
|
|
95
|
+
return ListEntry(o.fn, o.instance, o.revision, o.flags, o.name)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---- example profiles ----------------------------------------------------
|
|
99
|
+
# Names follow oep-spec docs/capability-name-hierarchy.ja.md (provisional, 2026-09-24): probe-wide
|
|
100
|
+
# declarations in oep.core's describe, oep.wire.<link> to scan and attach, oep.target.riscv-dm and
|
|
101
|
+
# oep.target.console on the connection, fixtures gpio / uart / capture, the ESP-IDF I2C and SPI
|
|
102
|
+
# targets under the project's own name. Probe-wide tags (oep.core, interface-specific 0x40..):
|
|
103
|
+
_CORE_TAGS = reg.CORE.tlv["describe"]
|
|
104
|
+
CORE_FIRMWARE, CORE_MODEL, CORE_UNIT_ID, CORE_CHANNELS = (_CORE_TAGS[k] for k in ("firmware", "model", "unit_id", "channels"))
|
|
105
|
+
CORE_RESERVED, CORE_PROFILE, CORE_LABEL = _CORE_TAGS["reserved"], _CORE_TAGS["profile"], _CORE_TAGS["label"]
|
|
106
|
+
CORE_RESETS_ON_OPEN, CORE_TRANSPORT, CORE_OEP_PID = _CORE_TAGS["resets_on_open"], _CORE_TAGS["transport"], _CORE_TAGS["oep_pid"]
|
|
107
|
+
TRANSPORT = reg.CORE.enum["transport_kind"]
|
|
108
|
+
SERIAL_KINDS = {TRANSPORT["uart_bridge"], TRANSPORT["usb_cdc"], TRANSPORT["usb_serial_jtag"]}
|
|
109
|
+
NS = "io.github.ch32-riscv-ug"
|
|
110
|
+
MECHANISMS = reg.TARGET_CONSOLE.tlv["describe"]["mechanisms"]
|
|
111
|
+
MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
|
|
112
|
+
_CFG = reg.PROBE_CONFIG.tlv["describe"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _transports(kinds: list[tuple[int, int]]) -> tuple[bytes, ...]:
|
|
116
|
+
"""(kind, USB interface or 0xFF) per transport, index = position (core §7.5)."""
|
|
117
|
+
return tuple(catalog.tlv(CORE_TRANSPORT, bytes([i, k, itf])) for i, (k, itf) in enumerate(kinds))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _config(fn: int, instance: int, slots_max: int, modes: int = 0b111, storage: int = 4096) -> Offered:
|
|
121
|
+
"""oep.probe.config's static declarations; slot_state / bind_state are added live by the endpoint."""
|
|
122
|
+
return Offered(fn, instance, "oep.probe.config", (
|
|
123
|
+
catalog.tlv(_CFG["storage"], struct.pack("<IBII", storage, 0, 0, 20)),
|
|
124
|
+
catalog.tlv(_CFG["items"], bytes(reg.PROBE_CONFIG.tlv["item"].values())),
|
|
125
|
+
catalog.u8(_CFG["slots_max"], slots_max), catalog.u8(_CFG["bind_modes"], modes)))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _roles(assign: dict[int, list[int]]) -> tuple[bytes, ...]:
|
|
129
|
+
return tuple(catalog.role_channels(r, ch) for r, ch in assign.items())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _label(channel: int, name: str) -> bytes:
|
|
133
|
+
return catalog.tlv(CORE_LABEL, struct.pack("<H", channel) + name.encode("ascii"))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _core(firmware: str, model: str, unit_id: bytes, channels: int, reserved: list[int], profile: str,
|
|
137
|
+
labels: dict[int, str], extra: tuple[bytes, ...] = ()) -> Offered:
|
|
138
|
+
base, bits = catalog.channels_to_bitmap(reserved)
|
|
139
|
+
return Offered(0, 0, "oep.core", (
|
|
140
|
+
catalog.text(CORE_FIRMWARE, firmware), catalog.text(CORE_MODEL, model), catalog.tlv(CORE_UNIT_ID, unit_id),
|
|
141
|
+
catalog.u16(CORE_CHANNELS, channels), catalog.tlv(CORE_RESERVED, struct.pack("<H", base) + bits),
|
|
142
|
+
catalog.text(CORE_PROFILE, profile)) + tuple(_label(c, n) for c, n in labels.items()) + extra)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def p4_x035() -> FakeProbe:
|
|
146
|
+
"""ESP32-P4 development probe on the CH32X035F8U6 jig (as wired on 2026-09-24), in the recommended USB shape
|
|
147
|
+
(probe guide §3.8): USB-Serial/JTAG (serial port 0), and on the HS port vendor bulk, HID and a CDC (serial port 3)
|
|
148
|
+
under the OEP VID:PID."""
|
|
149
|
+
reserved = [2, 24, 25, 54] # RVSWD SWDIO/SWCLK, USB-Serial/JTAG
|
|
150
|
+
pins = [p for p in range(55) if p not in reserved]
|
|
151
|
+
return FakeProbe("p4-x035", 1024, [
|
|
152
|
+
_core("3.0.0", "esp32-p4-devkit", bytes.fromhex("30eda0e31108"), 55, reserved, f"{NS}.p4-x035",
|
|
153
|
+
{2: "SWDIO", 54: "SWCLK", 51: "LED"},
|
|
154
|
+
_transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
|
|
155
|
+
(TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)]) + (catalog.u8(CORE_OEP_PID, 1),)),
|
|
156
|
+
Offered(1, 1, "oep.wire.rvswd", (
|
|
157
|
+
catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
|
|
158
|
+
catalog.u8(MAX_CONNECTIONS, 1))),
|
|
159
|
+
Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
|
|
160
|
+
catalog.u16(MAX_LENGTH, 1000))),
|
|
161
|
+
Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
162
|
+
Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
|
|
163
|
+
Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
164
|
+
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
165
|
+
Offered(6, 4, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
166
|
+
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
167
|
+
Offered(7, 5, "oep.fixture.capture", _roles({k: pins for k in range(8)}) + (
|
|
168
|
+
catalog.u32(MAX_CLOCK_HZ, 20_000_000), catalog.u32(MIN_CLOCK_HZ, 1_000),
|
|
169
|
+
catalog.u16(MAX_LENGTH, 65000), catalog.u8(IMPLEMENTATION, 3))),
|
|
170
|
+
Offered(8, 6, f"{NS}.esp32.i2c-target", _roles({1: pins, 2: pins}) + (
|
|
171
|
+
catalog.u16(MAX_LENGTH, 128), catalog.u32(MAX_CLOCK_HZ, 1_000_000),
|
|
172
|
+
catalog.u32(FEATURES, 0b11), catalog.u8(IMPLEMENTATION, 2))),
|
|
173
|
+
Offered(9, 7, f"{NS}.esp32.spi-target", _roles({1: pins, 2: pins, 3: pins, 4: pins}) + (
|
|
174
|
+
catalog.u16(MAX_LENGTH, 64), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
175
|
+
_config(10, 8, slots_max=1),
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def esp32_v003() -> FakeProbe:
|
|
180
|
+
"""A small probe with 64-byte frames over a 115200 bps UART bridge (its only transport, serial port 0):
|
|
181
|
+
classic ESP32 on a CH32V003 (SWIO) jig."""
|
|
182
|
+
reserved = [0, 1, 2, 3, 6, 7, 8, 9, 10, 11, 12, 15, 16]
|
|
183
|
+
wired = [4, 5, 13, 14, 17, 18, 19, 21, 22, 25, 26, 27, 32, 33]
|
|
184
|
+
return FakeProbe("esp32-v003", 64, [
|
|
185
|
+
_core("3.0.0", "esp32-d0wd", bytes.fromhex("0070070d9394"), 40, reserved, f"{NS}.esp32-v003",
|
|
186
|
+
{16: "SWIO", 23: "NRST", 22: "DUT TX", 21: "DUT RX"},
|
|
187
|
+
_transports([(TRANSPORT["uart_bridge"], 0xFF)])),
|
|
188
|
+
Offered(1, 1, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.u8(IMPLEMENTATION, 1),
|
|
189
|
+
catalog.u8(MAX_CONNECTIONS, 1))),
|
|
190
|
+
Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
|
|
191
|
+
catalog.u16(MAX_LENGTH, 40))),
|
|
192
|
+
Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
193
|
+
Offered(4, 2, "oep.fixture.gpio", _roles({1: wired + [23]})),
|
|
194
|
+
Offered(5, 3, "oep.fixture.uart", _roles({1: wired, 2: wired}) + (
|
|
195
|
+
catalog.u32(MAX_CLOCK_HZ, 115_200), catalog.u8(IMPLEMENTATION, 2))),
|
|
196
|
+
Offered(6, 4, "oep.fixture.capture", _roles({k: wired for k in range(4)}) + (
|
|
197
|
+
catalog.u32(MAX_CLOCK_HZ, 2_000_000), catalog.u32(MIN_CLOCK_HZ, 400_000), catalog.u8(IMPLEMENTATION, 1))),
|
|
198
|
+
Offered(7, 5, f"{NS}.esp32.i2c-target", _roles({1: wired, 2: wired}) + (
|
|
199
|
+
catalog.u16(MAX_LENGTH, 16), catalog.u32(MAX_CLOCK_HZ, 100_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
200
|
+
# Two fixed pin sets (an example of channel_group; GPIO23 is the DUT's NRST on this jig).
|
|
201
|
+
Offered(8, 6, f"{NS}.esp32.spi-target", (
|
|
202
|
+
catalog.channel_group(1, [(1, 18), (2, 19), (3, 5), (4, 4)]),
|
|
203
|
+
catalog.channel_group(2, [(1, 14), (2, 13), (3, 27), (4, 26)]),
|
|
204
|
+
catalog.u16(MAX_LENGTH, 32), catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
205
|
+
_config(9, 7, slots_max=1, modes=0b011, storage=1024),
|
|
206
|
+
])
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def p4_bench() -> FakeProbe:
|
|
210
|
+
"""A made-up bench probe with three RVSWD places and two seats (slots, the seat rule and the bind modes can be
|
|
211
|
+
exercised): USB-Serial/JTAG (serial port 0), vendor bulk, HID and a CDC (serial port 3) under the OEP VID:PID."""
|
|
212
|
+
reserved = [2, 3, 4, 5, 6, 7, 24, 25]
|
|
213
|
+
pins = [p for p in range(55) if p not in reserved]
|
|
214
|
+
return FakeProbe("p4-bench", 1024, [
|
|
215
|
+
_core("3.0.0", "esp32-p4-devkit", bytes.fromhex("30eda0e3b001"), 55, reserved, f"{NS}.p4-bench",
|
|
216
|
+
{2: "A SWDIO", 3: "A SWCLK", 4: "B SWDIO", 5: "B SWCLK", 6: "C SWDIO", 7: "C SWCLK"},
|
|
217
|
+
_transports([(TRANSPORT["usb_serial_jtag"], 0xFF), (TRANSPORT["vendor_bulk"], 0),
|
|
218
|
+
(TRANSPORT["hid"], 1), (TRANSPORT["usb_cdc"], 2)]) + (catalog.u8(CORE_OEP_PID, 1),)),
|
|
219
|
+
Offered(1, 1, "oep.wire.rvswd", (
|
|
220
|
+
catalog.channel_group(1, [(1, 2), (2, 3)]), catalog.channel_group(2, [(1, 4), (2, 5)]),
|
|
221
|
+
catalog.channel_group(3, [(1, 6), (2, 7)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000),
|
|
222
|
+
catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 2))),
|
|
223
|
+
Offered(2, 1, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
|
|
224
|
+
catalog.u16(MAX_LENGTH, 1000))),
|
|
225
|
+
Offered(3, 1, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
226
|
+
Offered(4, 2, "oep.fixture.gpio", _roles({1: pins})),
|
|
227
|
+
Offered(5, 3, "oep.fixture.uart", _roles({1: pins, 2: pins}) + (
|
|
228
|
+
catalog.u32(MAX_CLOCK_HZ, 3_000_000), catalog.u8(IMPLEMENTATION, 2))),
|
|
229
|
+
_config(6, 4, slots_max=4),
|
|
230
|
+
])
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
PROFILES = {"p4-x035": p4_x035, "esp32-v003": esp32_v003, "p4-bench": p4_bench}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""The byte side of a fake probe's serial port (oep-core §3.1, §3.4): COBS frames and raw bytes on one port.
|
|
2
|
+
|
|
3
|
+
`FakeSerialPort(endpoint, index)` is serial port `index` of an `endpoint.Endpoint`. Bytes from the host go to
|
|
4
|
+
`feed`; what the probe sends comes from `output`. It does what a probe does:
|
|
5
|
+
|
|
6
|
+
- a candidate runs from a 0x00 to the next 0x00; if it decodes and its CRC matches it is a request, otherwise it
|
|
7
|
+
(with its leading 0x00) is raw bytes, and the closing 0x00 starts the next candidate; bytes outside a candidate
|
|
8
|
+
are raw at once; a candidate that stops for 200 ms (`tick`) is raw; an empty candidate (0x00 0x00) is nothing
|
|
9
|
+
- raw bytes go to the endpoint's bind for this port (`Endpoint.port_input`)
|
|
10
|
+
- answers go out as 0x00 <COBS> 0x00, ahead of raw chunks (`Endpoint.port_output`); one writer, never a raw byte
|
|
11
|
+
inside a frame
|
|
12
|
+
|
|
13
|
+
`answer_filter(n, message) -> bytes | None` may change how the n-th answer (1-based) goes on the wire: the framed
|
|
14
|
+
bytes to send, or None to send nothing (fault injection for tests).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Callable
|
|
20
|
+
|
|
21
|
+
from . import cobs, registry as reg
|
|
22
|
+
|
|
23
|
+
GAP_MS = reg.TIMING["probe_frame_gap_ms"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class FakeSerialPort:
|
|
27
|
+
def __init__(self, ep, index: int, answer_filter: Callable[[int, bytes], bytes | None] | None = None):
|
|
28
|
+
self.ep, self.index = ep, index
|
|
29
|
+
self.answer_filter = answer_filter
|
|
30
|
+
self.cand: bytearray | None = None # the candidate so far, its leading 0x00 included
|
|
31
|
+
self.last_ms = 0
|
|
32
|
+
self.limit = 2 * ep.probe.max_frame + 16 # longer than any COBS frame of max_frame: raw
|
|
33
|
+
self.frames: list[bytes] = [] # framed answers waiting to go out
|
|
34
|
+
self.answers = 0
|
|
35
|
+
|
|
36
|
+
def feed(self, data: bytes) -> None:
|
|
37
|
+
now = self.ep.now()
|
|
38
|
+
self._gap(now)
|
|
39
|
+
raw = bytearray()
|
|
40
|
+
for b in data:
|
|
41
|
+
if b == 0:
|
|
42
|
+
if self.cand is not None:
|
|
43
|
+
self._close(raw)
|
|
44
|
+
self.cand = bytearray(b"\x00")
|
|
45
|
+
elif self.cand is not None:
|
|
46
|
+
self.cand.append(b)
|
|
47
|
+
if len(self.cand) > self.limit:
|
|
48
|
+
raw += self.cand
|
|
49
|
+
self.cand = None
|
|
50
|
+
else:
|
|
51
|
+
raw.append(b)
|
|
52
|
+
self.last_ms = now
|
|
53
|
+
self._raw(raw)
|
|
54
|
+
|
|
55
|
+
def _close(self, raw: bytearray) -> None:
|
|
56
|
+
body = bytes(self.cand[1:])
|
|
57
|
+
self.cand = None
|
|
58
|
+
if not body:
|
|
59
|
+
return # 0x00 0x00: an empty frame
|
|
60
|
+
try:
|
|
61
|
+
msg = cobs.unframe(body)
|
|
62
|
+
except cobs.CorruptFrame:
|
|
63
|
+
raw += b"\x00" + body # not a frame: raw, the leading 0x00 too
|
|
64
|
+
return
|
|
65
|
+
self._raw(raw) # the raw bytes before the frame go first
|
|
66
|
+
raw.clear()
|
|
67
|
+
try:
|
|
68
|
+
result = self.ep.handle(msg, self.index)
|
|
69
|
+
except ValueError:
|
|
70
|
+
return # not a request (a role the probe does not take): dropped
|
|
71
|
+
if result is None:
|
|
72
|
+
return
|
|
73
|
+
self.answers += 1
|
|
74
|
+
wire = cobs.frame(result)
|
|
75
|
+
if self.answer_filter is not None:
|
|
76
|
+
wire = self.answer_filter(self.answers, result)
|
|
77
|
+
if wire:
|
|
78
|
+
self.frames.append(wire)
|
|
79
|
+
|
|
80
|
+
def _gap(self, now: int) -> None:
|
|
81
|
+
if self.cand is not None and now - self.last_ms >= GAP_MS:
|
|
82
|
+
raw, self.cand = bytes(self.cand), None
|
|
83
|
+
self._raw(bytearray(raw))
|
|
84
|
+
|
|
85
|
+
def _raw(self, raw: bytearray) -> None:
|
|
86
|
+
if raw:
|
|
87
|
+
self.ep.port_input(self.index, bytes(raw))
|
|
88
|
+
|
|
89
|
+
def tick(self) -> None:
|
|
90
|
+
self._gap(self.ep.now())
|
|
91
|
+
self.ep.tick()
|
|
92
|
+
|
|
93
|
+
def output(self, room: int = 4096) -> bytes:
|
|
94
|
+
"""What the probe sends now: waiting answers first, then raw bytes by the bind (up to `room` in all)."""
|
|
95
|
+
out = bytearray()
|
|
96
|
+
while self.frames and len(out) + len(self.frames[0]) <= max(room, len(self.frames[0])):
|
|
97
|
+
out += self.frames.pop(0)
|
|
98
|
+
if len(out) >= room:
|
|
99
|
+
return bytes(out)
|
|
100
|
+
while len(out) < room:
|
|
101
|
+
chunk = self.ep.port_output(self.index, min(self.ep.CHUNK, room - len(out)))
|
|
102
|
+
if not chunk:
|
|
103
|
+
break
|
|
104
|
+
out += chunk
|
|
105
|
+
return bytes(out)
|