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.
@@ -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"