oep-client-python 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oep_client/__init__.py +5 -0
- oep_client/v1/__init__.py +1 -0
- oep_client/v1/__main__.py +40 -0
- oep_client/v1/arm.py +269 -0
- oep_client/v1/capture.py +365 -0
- oep_client/v1/catalog.py +181 -0
- oep_client/v1/ch32_flash.py +254 -0
- oep_client/v1/cobs.py +73 -0
- oep_client/v1/console.py +165 -0
- oep_client/v1/core.py +172 -0
- oep_client/v1/decode.py +65 -0
- oep_client/v1/dump.py +157 -0
- oep_client/v1/endpoint.py +1479 -0
- oep_client/v1/esp32_targets.py +137 -0
- oep_client/v1/fake.py +233 -0
- oep_client/v1/fake_serial.py +105 -0
- oep_client/v1/fake_serve.py +299 -0
- oep_client/v1/fixture.py +121 -0
- oep_client/v1/frames.py +91 -0
- oep_client/v1/hid_stream.py +257 -0
- oep_client/v1/host.py +321 -0
- oep_client/v1/interfaces.py +119 -0
- oep_client/v1/link.py +414 -0
- oep_client/v1/message.py +235 -0
- oep_client/v1/names.py +86 -0
- oep_client/v1/registry.py +59 -0
- oep_client/v1/riscv.py +430 -0
- oep_client/v1/rp2350.py +85 -0
- oep_client/v1/target.py +16 -0
- oep_client/v1/uiapduino.py +121 -0
- oep_client/v1/usb_stream.py +209 -0
- oep_client_python-0.0.1.dist-info/METADATA +106 -0
- oep_client_python-0.0.1.dist-info/RECORD +36 -0
- oep_client_python-0.0.1.dist-info/WHEEL +4 -0
- oep_client_python-0.0.1.dist-info/entry_points.txt +2 -0
- oep_client_python-0.0.1.dist-info/licenses/LICENSE +21 -0
oep_client/v1/decode.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Host-side decoders for capture channels (oep.fixture.capture: `LogicCapture.channel(data, k)` gives one value
|
|
2
|
+
per sample)."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class I2cEvent:
|
|
11
|
+
kind: str # "start", "byte", "stop"
|
|
12
|
+
sample: int # sample index where the event was recognised
|
|
13
|
+
value: int = 0 # byte value for "byte"
|
|
14
|
+
ack: bool | None = None # True = ACK (SDA low on the 9th clock)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class I2cTrace:
|
|
19
|
+
events: list[I2cEvent] = field(default_factory=list)
|
|
20
|
+
scl_periods: list[int] = field(default_factory=list) # samples between SCL rising edges
|
|
21
|
+
|
|
22
|
+
def bytes(self) -> list[tuple[int, bool]]:
|
|
23
|
+
return [(e.value, bool(e.ack)) for e in self.events if e.kind == "byte"]
|
|
24
|
+
|
|
25
|
+
def summary(self) -> str:
|
|
26
|
+
parts = []
|
|
27
|
+
for e in self.events:
|
|
28
|
+
if e.kind == "byte":
|
|
29
|
+
parts.append(f"{e.value:02x}{'A' if e.ack else 'N'}")
|
|
30
|
+
else:
|
|
31
|
+
parts.append("S" if e.kind == "start" else "P")
|
|
32
|
+
return " ".join(parts)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def decode_i2c(scl: list[int], sda: list[int]) -> I2cTrace:
|
|
36
|
+
"""Edge-based I2C decode of two channels: START = SDA falling while SCL high, STOP = SDA rising while SCL high,
|
|
37
|
+
data sampled on SCL rising edges, every 9th bit is the ACK."""
|
|
38
|
+
trace = I2cTrace()
|
|
39
|
+
n = min(len(scl), len(sda))
|
|
40
|
+
if not n:
|
|
41
|
+
return trace
|
|
42
|
+
bits: list[int] = []
|
|
43
|
+
in_frame = False
|
|
44
|
+
last_rise = None
|
|
45
|
+
for i in range(1, n):
|
|
46
|
+
if scl[i] and scl[i - 1]:
|
|
47
|
+
if sda[i - 1] and not sda[i]:
|
|
48
|
+
trace.events.append(I2cEvent("start", i))
|
|
49
|
+
in_frame, bits = True, []
|
|
50
|
+
elif not sda[i - 1] and sda[i]:
|
|
51
|
+
trace.events.append(I2cEvent("stop", i))
|
|
52
|
+
in_frame, bits = False, []
|
|
53
|
+
if scl[i] and not scl[i - 1]: # SCL rising edge: sample SDA
|
|
54
|
+
if last_rise is not None:
|
|
55
|
+
trace.scl_periods.append(i - last_rise)
|
|
56
|
+
last_rise = i
|
|
57
|
+
if in_frame:
|
|
58
|
+
bits.append(sda[i])
|
|
59
|
+
if len(bits) == 9:
|
|
60
|
+
value = 0
|
|
61
|
+
for b in bits[:8]:
|
|
62
|
+
value = (value << 1) | b
|
|
63
|
+
trace.events.append(I2cEvent("byte", i, value, ack=bits[8] == 0))
|
|
64
|
+
bits = []
|
|
65
|
+
return trace
|
oep_client/v1/dump.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Collect a probe's declared capabilities (list + describe, paged) and render them.
|
|
2
|
+
|
|
3
|
+
Talks only through `call(fn, op, payload) -> payload`, so the same code runs against the in-process
|
|
4
|
+
fake now and a real transport later.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import struct
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
|
|
13
|
+
from . import catalog, interfaces, message as m, names
|
|
14
|
+
|
|
15
|
+
CORE_FN, OP_CONFIRM, OP_LIST, OP_DESCRIBE = 0, 0x01, 0x02, 0x03
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Offer:
|
|
20
|
+
entry: catalog.ListEntry
|
|
21
|
+
description: catalog.Description
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Capabilities:
|
|
26
|
+
revision: int
|
|
27
|
+
max_frame: int
|
|
28
|
+
offers: list[Offer] = field(default_factory=list)
|
|
29
|
+
requests: dict[str, int] = field(default_factory=dict)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def collect(call, prefix: str = "", exact: bool = False) -> Capabilities:
|
|
33
|
+
p = call(CORE_FN, OP_CONFIRM, m.CONFIRM_REQUEST + bytes([0, 1])) # v0 and v1 both answer
|
|
34
|
+
magic, revision = struct.unpack_from("<4sB", p)
|
|
35
|
+
if magic != m.CONFIRM_RESULT:
|
|
36
|
+
raise ValueError("not an OEP endpoint")
|
|
37
|
+
# revision 0: max_frame(u16) follows; revision 1: flags(u8), then max_frame(u16)
|
|
38
|
+
max_frame = struct.unpack_from("<H", p, 5 if revision == 0 else 6)[0]
|
|
39
|
+
caps = Capabilities(revision, max_frame, requests={"confirm": 1, "list": 0, "describe": 0})
|
|
40
|
+
entries: list[catalog.ListEntry] = []
|
|
41
|
+
while True:
|
|
42
|
+
total, page = catalog.unpack_list_result(call(CORE_FN, OP_LIST, catalog.pack_list_request(prefix, exact, len(entries))))
|
|
43
|
+
caps.requests["list"] += 1
|
|
44
|
+
entries += page
|
|
45
|
+
if len(entries) >= total or not page:
|
|
46
|
+
break
|
|
47
|
+
for e in entries:
|
|
48
|
+
data, first = b"", 0
|
|
49
|
+
while True:
|
|
50
|
+
result = call(CORE_FN, OP_DESCRIBE, catalog.pack_describe_request(e.fn, first))
|
|
51
|
+
caps.requests["describe"] += 1
|
|
52
|
+
more, chunk = result[0], result[1:]
|
|
53
|
+
data += chunk
|
|
54
|
+
first += len(catalog.split_tlv(chunk))
|
|
55
|
+
if not more or not chunk:
|
|
56
|
+
break
|
|
57
|
+
caps.offers.append(Offer(e, catalog.decode_description(data)))
|
|
58
|
+
return caps
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---- rendering -----------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def _hz(v: int) -> str:
|
|
64
|
+
for unit, div in (("MHz", 1_000_000), ("kHz", 1_000)):
|
|
65
|
+
if v >= div and v % (div // 1000 or 1) == 0:
|
|
66
|
+
return f"{v / div:g} {unit}"
|
|
67
|
+
return f"{v} Hz"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _features(bits: int, known: dict[int, str]) -> list[str]:
|
|
71
|
+
return [known.get(b, f"bit{b}") for b in range(32) if bits >> b & 1]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def describe_offer(o: Offer) -> dict:
|
|
75
|
+
"""One offer as plain data: what JSON carries and what text renders."""
|
|
76
|
+
name = o.entry.name
|
|
77
|
+
known = interfaces.KNOWN.get(name)
|
|
78
|
+
d = o.description
|
|
79
|
+
out = {"fn": o.entry.fn, "instance": o.entry.instance, "name": name, "revision": o.entry.revision,
|
|
80
|
+
"namespace": names.kind(name), "known": known is not None}
|
|
81
|
+
if known:
|
|
82
|
+
out["summary"] = known.summary
|
|
83
|
+
if d.roles:
|
|
84
|
+
out["roles"] = {(known.roles.get(r, f"role{r}") if known else f"role{r}"): interfaces.ranges(ch)
|
|
85
|
+
for r, ch in sorted(d.roles.items())}
|
|
86
|
+
if d.groups:
|
|
87
|
+
out["pin_groups"] = {str(g): {(known.roles.get(r, f"role{r}") if known else f"role{r}"): c for r, c in pins}
|
|
88
|
+
for g, pins in sorted(d.groups.items())}
|
|
89
|
+
for key in ("max_clock_hz", "min_clock_hz", "max_length"):
|
|
90
|
+
if getattr(d, key) is not None:
|
|
91
|
+
out[key] = getattr(d, key)
|
|
92
|
+
if d.features is not None:
|
|
93
|
+
out["features"] = _features(d.features, known.features if known else {})
|
|
94
|
+
if d.implementation is not None:
|
|
95
|
+
out["implementation"] = catalog.IMPLEMENTATIONS.get(d.implementation, str(d.implementation))
|
|
96
|
+
specific = {}
|
|
97
|
+
for tag, value in d.specific:
|
|
98
|
+
label, decode = (known.tags.get(tag & 0x7F) if known else None) or (f"tag 0x{tag:02x}", lambda v: v.hex())
|
|
99
|
+
# a tag may repeat (one label per channel): keep every value
|
|
100
|
+
specific[label] = f"{specific[label]}; {decode(value)}" if label in specific else decode(value)
|
|
101
|
+
if specific:
|
|
102
|
+
out["declares"] = specific
|
|
103
|
+
if d.unknown_critical:
|
|
104
|
+
out["unusable"] = f"unknown critical tags {[hex(t) for t in d.unknown_critical]}"
|
|
105
|
+
return out
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def to_json(caps: Capabilities) -> str:
|
|
109
|
+
return json.dumps({"revision": caps.revision, "max_frame": caps.max_frame, "requests": caps.requests,
|
|
110
|
+
"interfaces": [describe_offer(o) for o in caps.offers]}, indent=2)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def to_text(caps: Capabilities) -> str:
|
|
114
|
+
rows = [describe_offer(o) for o in caps.offers]
|
|
115
|
+
lines = [f"OEP revision {caps.revision}, max frame {caps.max_frame} bytes; "
|
|
116
|
+
f"{len(rows)} interfaces in {caps.requests['list']} list and "
|
|
117
|
+
f"{caps.requests['describe']} describe requests", ""]
|
|
118
|
+
by_instance: dict[int, list[dict]] = {}
|
|
119
|
+
for r in rows:
|
|
120
|
+
by_instance.setdefault(r["instance"], []).append(r)
|
|
121
|
+
for inst, group in by_instance.items():
|
|
122
|
+
for i, r in enumerate(group):
|
|
123
|
+
head = f"instance {inst:<3}" if i == 0 else " " * 12
|
|
124
|
+
tag = "" if r["known"] else " (not known to this host)"
|
|
125
|
+
lines.append(f"{head} fn {r['fn']:<3} {r['name']} rev {r['revision']}{tag}")
|
|
126
|
+
for r in group:
|
|
127
|
+
pad = " " * 14
|
|
128
|
+
if len(group) > 1:
|
|
129
|
+
lines.append(f"{pad}[{r['name']}]")
|
|
130
|
+
if "summary" in r:
|
|
131
|
+
lines.append(f"{pad}{r['summary']}")
|
|
132
|
+
if "roles" in r:
|
|
133
|
+
width = max(len(k) for k in r["roles"])
|
|
134
|
+
for role, chans in r["roles"].items():
|
|
135
|
+
lines.append(f"{pad} {role:<{width}} channels {chans}")
|
|
136
|
+
if "pin_groups" in r:
|
|
137
|
+
for g, pins in r["pin_groups"].items():
|
|
138
|
+
lines.append(f"{pad} pin set {g}: " + ", ".join(f"{k}={v}" for k, v in pins.items()))
|
|
139
|
+
limits = []
|
|
140
|
+
if "max_clock_hz" in r:
|
|
141
|
+
limits.append(f"max {_hz(r['max_clock_hz'])}")
|
|
142
|
+
if "min_clock_hz" in r:
|
|
143
|
+
limits.append(f"min {_hz(r['min_clock_hz'])}")
|
|
144
|
+
if "max_length" in r:
|
|
145
|
+
limits.append(f"max length {r['max_length']}")
|
|
146
|
+
if limits:
|
|
147
|
+
lines.append(f"{pad} " + ", ".join(limits))
|
|
148
|
+
if r.get("features"):
|
|
149
|
+
lines.append(f"{pad} features: " + ", ".join(r["features"]))
|
|
150
|
+
if "implementation" in r:
|
|
151
|
+
lines.append(f"{pad} implementation: {r['implementation']}")
|
|
152
|
+
for k, v in r.get("declares", {}).items():
|
|
153
|
+
lines.append(f"{pad} {k}: {v}")
|
|
154
|
+
if "unusable" in r:
|
|
155
|
+
lines.append(f"{pad} UNUSABLE: {r['unusable']}")
|
|
156
|
+
lines.append("")
|
|
157
|
+
return "\n".join(lines).rstrip() + "\n"
|