oep-client-python 0.0.26__py3-none-any.whl → 0.0.28__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/__main__.py +134 -10
- oep_client/arm.py +7 -6
- oep_client/ch32_flash.py +6 -7
- oep_client/cobs.py +5 -0
- oep_client/config.py +57 -1
- oep_client/endpoint.py +355 -33
- oep_client/fake.py +55 -14
- oep_client/fixture.py +54 -5
- oep_client/link.py +508 -226
- oep_client/linktest.py +184 -0
- oep_client/pins.py +614 -0
- oep_client/registry.py +5 -5
- oep_client/riscv.py +77 -10
- oep_client/speed_record.py +104 -0
- oep_client/targets.py +97 -0
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.28.dist-info}/METADATA +97 -25
- oep_client_python-0.0.28.dist-info/RECORD +39 -0
- oep_client_python-0.0.26.dist-info/RECORD +0 -35
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.28.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.28.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.28.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
|
|
10
10
|
oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
|
|
11
11
|
oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
|
|
12
|
-
oep speed <probe>
|
|
12
|
+
oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, core §3.5 and the host
|
|
13
|
+
guide §7: try the candidates in order on a UART bridge, report)
|
|
14
|
+
oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
|
|
15
|
+
oep pins <probe> --power 5 --wire swio (find the target's debug pins and reset line: classify the channels, scan,
|
|
16
|
+
identify, hold-low + attach under reset; prints a slot, --save writes it)
|
|
13
17
|
|
|
14
18
|
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
15
19
|
ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
|
|
@@ -30,14 +34,50 @@ def main(argv=None) -> int:
|
|
|
30
34
|
parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
|
|
31
35
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
32
36
|
_config_parser(sub)
|
|
33
|
-
sp = sub.add_parser("speed", help="port_speed (core §3.5): try faster rates on a UART bridge,
|
|
37
|
+
sp = sub.add_parser("speed", help="port_speed (core §3.5, host guide §7): try faster rates on a UART bridge, "
|
|
38
|
+
"print the report")
|
|
34
39
|
sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
|
|
35
|
-
sp.add_argument("rates", help="comma-separated, in order of preference
|
|
40
|
+
sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
|
|
41
|
+
"(the first that passes is kept; default 500000). Same as --candidates")
|
|
42
|
+
sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000)")
|
|
43
|
+
sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
|
|
44
|
+
"flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
|
|
45
|
+
form = sp.add_mutually_exclusive_group()
|
|
46
|
+
form.add_argument("--verify", action="store_true", help="the full form (guide §7.3): baseline, the flows measured "
|
|
47
|
+
"at every candidate, a failed flow run again one at a time")
|
|
48
|
+
form.add_argument("--minimal", action="store_true", help="the minimal form (guide §7.2, the default): try, confirm, "
|
|
49
|
+
"commit, no measurement")
|
|
50
|
+
sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
|
|
51
|
+
sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
|
|
52
|
+
"(~/.cache/oep-client/link-speed.json, 30 days; on by default here, off in the library)")
|
|
36
53
|
sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
|
|
37
|
-
sp.add_argument("--verify-bytes", type=int, default=32768)
|
|
38
|
-
sp.add_argument("--verify-s", type=float, default=1.0)
|
|
39
|
-
sp.add_argument("--duplex-s", type=float, default=1.0, help="both ways at once for this long (default 1)")
|
|
40
54
|
sp.add_argument("--json", action="store_true")
|
|
55
|
+
lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
|
|
56
|
+
lt.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID]")
|
|
57
|
+
lt.add_argument("--rates", default="", help="comma-separated rates to switch to with port_speed and measure at "
|
|
58
|
+
"(each on its own; 0 or 'now' = the speed in force). Default: only the speed in force")
|
|
59
|
+
lt.add_argument("--patterns", default="in,out,duplex", help="in (probe->host), out (host->probe), duplex")
|
|
60
|
+
lt.add_argument("--inflight", default="1", help="comma-separated in-flight counts (above the probe's max: skipped)")
|
|
61
|
+
lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
|
|
62
|
+
lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
|
|
63
|
+
lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
|
|
64
|
+
lt.add_argument("--timeout", type=float, default=0.3, help="seconds to wait for one answer (default 0.3)")
|
|
65
|
+
lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
|
|
66
|
+
lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
|
|
67
|
+
lt.add_argument("--json", action="store_true", help="one JSON object per cell")
|
|
68
|
+
pn = sub.add_parser("pins", help="find where a target is wired: classify the channels, scan the wire, find the "
|
|
69
|
+
"reset line, suggest a slot (writes nothing without --save)")
|
|
70
|
+
pn.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
|
|
71
|
+
pn.add_argument("--power", type=int, help="the gpio channel that powers the target: switched off, then on, to "
|
|
72
|
+
"see which channels follow it (never touched without this)")
|
|
73
|
+
pn.add_argument("--exclude", default="", help="channels never to read, scan or hold, comma-separated")
|
|
74
|
+
pn.add_argument("--wire", choices=("swio", "rvswd", "swd"), default="swio")
|
|
75
|
+
pn.add_argument("--steps", default="classify,hold,scan,identify,reset,slot",
|
|
76
|
+
help="the steps to run, comma-separated (default all: classify, hold, scan, identify, reset, slot)")
|
|
77
|
+
pn.add_argument("--save", action="store_true", help="write the suggested slot (config set, then save)")
|
|
78
|
+
pn.add_argument("--slot", type=int, default=0, help="--save: the slot number (default 0)")
|
|
79
|
+
pn.add_argument("--name", help="--save: the slot's name (default: the target family, else 'target')")
|
|
80
|
+
pn.add_argument("--json", action="store_true", help="the report as JSON (the steps' lines go to stderr)")
|
|
41
81
|
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
42
82
|
src = d.add_mutually_exclusive_group(required=True)
|
|
43
83
|
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
@@ -50,6 +90,10 @@ def main(argv=None) -> int:
|
|
|
50
90
|
return _config(args)
|
|
51
91
|
if args.command == "speed":
|
|
52
92
|
return _speed(args)
|
|
93
|
+
if args.command == "linktest":
|
|
94
|
+
return _linktest(args)
|
|
95
|
+
if args.command == "pins":
|
|
96
|
+
return _pins_cmd(args)
|
|
53
97
|
|
|
54
98
|
if args.fake:
|
|
55
99
|
call = fake.PROFILES[args.fake]().call
|
|
@@ -61,17 +105,97 @@ def main(argv=None) -> int:
|
|
|
61
105
|
return 0
|
|
62
106
|
|
|
63
107
|
|
|
108
|
+
# ---- oep pins -----------------------------------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def _pins_cmd(args) -> int:
|
|
111
|
+
from . import pins
|
|
112
|
+
exclude = [int(x, 0) for x in args.exclude.replace(" ", "").split(",") if x]
|
|
113
|
+
say = (lambda t: print(t, file=sys.stderr, flush=True)) if args.json else (lambda t: print(t, flush=True)) # noqa: E731
|
|
114
|
+
hst = link.open_host(args.probe)
|
|
115
|
+
try:
|
|
116
|
+
core.take(hst, 10000, owner="oep pins")
|
|
117
|
+
finder = pins.PinFinder(hst, wire=args.wire, power=args.power, exclude=exclude, say=say, probe=args.probe,
|
|
118
|
+
save=args.save, slot=args.slot, name=args.name)
|
|
119
|
+
report = finder.run(steps=tuple(s for s in args.steps.split(",") if s))
|
|
120
|
+
say(f"done in {report.elapsed_s:.1f} s")
|
|
121
|
+
if args.json:
|
|
122
|
+
print(json.dumps(report.as_dict(), indent=2))
|
|
123
|
+
try:
|
|
124
|
+
hst.end()
|
|
125
|
+
except host.OepError:
|
|
126
|
+
pass
|
|
127
|
+
except host.Rejected as e:
|
|
128
|
+
print(f"refused: {e}", file=sys.stderr)
|
|
129
|
+
return 2
|
|
130
|
+
finally:
|
|
131
|
+
hst.link.close()
|
|
132
|
+
return 0 if report.found else 1
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---- oep linktest -------------------------------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _linktest(args) -> int:
|
|
138
|
+
"""Every parameter on the command line: no fixed numbers in the measurement."""
|
|
139
|
+
import dataclasses
|
|
140
|
+
import json
|
|
141
|
+
from . import linktest
|
|
142
|
+
ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
|
|
143
|
+
rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
|
|
144
|
+
hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
|
|
145
|
+
else link.open_host(args.probe)
|
|
146
|
+
stream = getattr(hst.link, "stream", None)
|
|
147
|
+
if hasattr(stream, "set_low_latency_mode"):
|
|
148
|
+
try:
|
|
149
|
+
stream.set_low_latency_mode(args.low_latency == "on")
|
|
150
|
+
except (OSError, ValueError, NotImplementedError):
|
|
151
|
+
pass
|
|
152
|
+
try:
|
|
153
|
+
core.take(hst, 30000, owner="oep linktest")
|
|
154
|
+
for result in linktest.matrix(hst, rates=rates, patterns=[p for p in args.patterns.split(",") if p],
|
|
155
|
+
inflight=ints(args.inflight), sizes=ints(args.sizes) or None,
|
|
156
|
+
frames=args.frames, seconds=args.seconds, timeout=args.timeout):
|
|
157
|
+
if args.json:
|
|
158
|
+
for c in result.cells:
|
|
159
|
+
print(json.dumps({**dataclasses.asdict(c), "actual": result.actual, "low_latency": args.low_latency}), flush=True)
|
|
160
|
+
if result.why:
|
|
161
|
+
print(json.dumps({"rate": result.rate, "why": result.why}), flush=True)
|
|
162
|
+
else:
|
|
163
|
+
print(result.text(), flush=True)
|
|
164
|
+
try:
|
|
165
|
+
hst.end()
|
|
166
|
+
except Exception:
|
|
167
|
+
pass
|
|
168
|
+
finally:
|
|
169
|
+
hst.link.close()
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
|
|
64
173
|
# ---- oep speed ----------------------------------------------------------------------------------------------------
|
|
65
174
|
|
|
175
|
+
def _flows(text: str) -> list[tuple[str, int]] | None:
|
|
176
|
+
"""--flows "in:2,out,duplex:1" -> [("in", 2), ("out", 0), ("duplex", 1)]; "" -> None (the default flows)."""
|
|
177
|
+
out = []
|
|
178
|
+
for item in text.replace(" ", "").split(","):
|
|
179
|
+
if not item:
|
|
180
|
+
continue
|
|
181
|
+
name, _, n = item.partition(":")
|
|
182
|
+
out.append((name, int(n) if n else 0))
|
|
183
|
+
return out or None
|
|
184
|
+
|
|
185
|
+
|
|
66
186
|
def _speed(args) -> int:
|
|
67
|
-
"""Take the lock, raise_speed, print the report; the session's end puts the port
|
|
187
|
+
"""Take the lock, raise_speed by the host guide's procedure, print the report; the session's end puts the port
|
|
188
|
+
back at its boot speed. The record of passed / failed rates is on unless --no-record."""
|
|
68
189
|
import dataclasses
|
|
69
|
-
|
|
190
|
+
text = args.candidates or args.rates
|
|
191
|
+
candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
|
|
192
|
+
flows = _flows(args.flows)
|
|
193
|
+
verify = True if args.verify or flows else (False if args.minimal else None)
|
|
70
194
|
hst = link.open_host(args.probe, baud=args.baud)
|
|
71
195
|
try:
|
|
72
196
|
core.take(hst, 5000, owner="oep speed")
|
|
73
|
-
report = link.raise_speed(hst,
|
|
74
|
-
|
|
197
|
+
report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
|
|
198
|
+
record=not args.no_record)
|
|
75
199
|
if args.json:
|
|
76
200
|
sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
|
|
77
201
|
else:
|
oep_client/arm.py
CHANGED
|
@@ -10,7 +10,7 @@ import struct
|
|
|
10
10
|
|
|
11
11
|
from . import host as h, message as m, registry as reg
|
|
12
12
|
from .core import Interface
|
|
13
|
-
from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
|
|
13
|
+
from .riscv import OK, BlockLength, TargetError, WireBase, check, ran, status_name # noqa: F401
|
|
14
14
|
|
|
15
15
|
DP_DPIDR = DP_ABORT = 0x0
|
|
16
16
|
DP_CTRL_STAT = 0x4
|
|
@@ -60,7 +60,9 @@ def transfer_reads(steps: bytes) -> list[bool]:
|
|
|
60
60
|
return out
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
class ArmAdi(Interface):
|
|
63
|
+
class ArmAdi(Interface, BlockLength):
|
|
64
|
+
"""oep.target.arm-adi on one connection. `max_length` / `max_words` bound read_block / write_block (from the
|
|
65
|
+
probe's describe, oep-if-debug §6: mandatory there)."""
|
|
64
66
|
NAME = "oep.target.arm-adi"
|
|
65
67
|
REVISION = 1
|
|
66
68
|
TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
|
|
@@ -70,6 +72,7 @@ class ArmAdi(Interface):
|
|
|
70
72
|
self.conn = conn
|
|
71
73
|
self.adiv6 = adiv6
|
|
72
74
|
self._select: int | None = None
|
|
75
|
+
self._max_length = None
|
|
73
76
|
|
|
74
77
|
# ---- raw transfers ----
|
|
75
78
|
@staticmethod
|
|
@@ -140,10 +143,8 @@ class MemAp:
|
|
|
140
143
|
csw = adi.ap_read(ap, self.base)
|
|
141
144
|
adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
|
|
142
145
|
adi._ap_select(ap, self.base) # the bank the block operations assume
|
|
143
|
-
# Words per block operation
|
|
144
|
-
|
|
145
|
-
from .core import confirm
|
|
146
|
-
self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
|
|
146
|
+
# Words per block operation: the probe's declared max_length (oep-if-debug §6), never computed from max_frame
|
|
147
|
+
self.chunk = adi.max_words
|
|
147
148
|
|
|
148
149
|
def write_many(self, pairs: list[tuple[int, int]]) -> None:
|
|
149
150
|
"""Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
|
oep_client/ch32_flash.py
CHANGED
|
@@ -17,7 +17,7 @@ import struct
|
|
|
17
17
|
import time
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
19
|
|
|
20
|
-
from . import
|
|
20
|
+
from . import host as h, riscv
|
|
21
21
|
|
|
22
22
|
# oep-spec experiments/flash-primitives/x035_loader.S, assembled at 0x20000000 (riscv32-esp-elf binutils)
|
|
23
23
|
FAST_PAGE_LOADER = bytes.fromhex(
|
|
@@ -91,11 +91,10 @@ LOADER, FAST_BUFFER, FAST_DONE = 0x20000000, 0x20000400, 0x200000B0
|
|
|
91
91
|
V003_INPUT, V003_STACK, V003_EBREAK, V003_RUN = 0x20000200, 0x20000800, 0x2000015C, 1024
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def _block_size(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return (info["max_frame"] - 5 - 6 - 4 - 1 - 4 - 2) // 4 * 4
|
|
94
|
+
def _block_size(dm: riscv.RiscvDm) -> int:
|
|
95
|
+
"""Bytes per read_block / write_block: the probe's declared max_length (oep-if-debug §4.5), never a value computed
|
|
96
|
+
from max_frame (riscv.NoMaxLength when the probe declares none)."""
|
|
97
|
+
return dm.max_words * 4
|
|
99
98
|
|
|
100
99
|
|
|
101
100
|
def _write_requests(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
|
|
@@ -142,7 +141,7 @@ def program(hst: h.Host, dm: riscv.RiscvDm, image: bytes, profile: FlashProfile)
|
|
|
142
141
|
image = image + b"\xff" * (-len(image) % profile.page)
|
|
143
142
|
if profile.size and len(image) > profile.size:
|
|
144
143
|
raise ValueError(f"image of {len(image)} bytes exceeds {profile.size}")
|
|
145
|
-
block = _block_size(
|
|
144
|
+
block = _block_size(dm)
|
|
146
145
|
t = {}
|
|
147
146
|
t0 = time.perf_counter()
|
|
148
147
|
if profile.method == "fast-page":
|
oep_client/cobs.py
CHANGED
|
@@ -71,3 +71,8 @@ def unframe(raw: bytes) -> bytes:
|
|
|
71
71
|
if crc16(body) != got:
|
|
72
72
|
raise CorruptFrame("CRC mismatch")
|
|
73
73
|
return body
|
|
74
|
+
|
|
75
|
+
def frame_max(message_len: int) -> int:
|
|
76
|
+
"""The longest frame a message of this length makes: CRC16, a COBS code per 254 bytes and one more, the delimiter."""
|
|
77
|
+
n = message_len + 2
|
|
78
|
+
return n + (n + 253) // 254 + 1 + 1
|
oep_client/config.py
CHANGED
|
@@ -73,14 +73,23 @@ class Label:
|
|
|
73
73
|
|
|
74
74
|
@dataclass(kw_only=True)
|
|
75
75
|
class Idle:
|
|
76
|
+
"""The state of a channel no plan or connection uses (probe.config §1): at boot and after every release. mode:
|
|
77
|
+
hi-z, pull-up, pull-down, output-low, output-high (output_low / output_high too). An output mode keeps driving that
|
|
78
|
+
level while the channel is free - a target's power switch kept on - and a gpio plan that takes the channel keeps
|
|
79
|
+
it until its first set (fixture §1); a probe that cannot drive the channel refuses it unsupported."""
|
|
76
80
|
channel: int
|
|
77
|
-
mode: str = "pull-up" # hi-z, pull-up, pull-down
|
|
81
|
+
mode: str = "pull-up" # hi-z, pull-up, pull-down, output-low, output-high
|
|
78
82
|
TAG = ITEM["idle"]
|
|
79
83
|
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
self.mode = self.mode.replace("_", "-")
|
|
86
|
+
|
|
80
87
|
def key(self) -> tuple:
|
|
81
88
|
return (self.channel,)
|
|
82
89
|
|
|
83
90
|
def value(self) -> bytes:
|
|
91
|
+
if self.mode not in IDLE:
|
|
92
|
+
raise ValueError(f"idle mode {self.mode!r}: one of {', '.join(IDLE)}")
|
|
84
93
|
return struct.pack("<HB", self.channel, IDLE[self.mode])
|
|
85
94
|
|
|
86
95
|
|
|
@@ -395,3 +404,50 @@ class ProbeConfig(Interface):
|
|
|
395
404
|
return st
|
|
396
405
|
first_slot += n_slots
|
|
397
406
|
first_bind += n_binds
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# The label names of a target's power and reset lines (host-development-guide §8.1): `nrst` its reset, `power_hi` high
|
|
410
|
+
# powers it, `power_lo` low powers it. On a probe with several slots they are `<slot name>.<name>`.
|
|
411
|
+
LINE_NAMES = ("nrst", "power_hi", "power_lo")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class AmbiguousLine(LookupError):
|
|
415
|
+
"""find_line could not tell which channel is meant; `candidates` holds (label text, channel)."""
|
|
416
|
+
|
|
417
|
+
def __init__(self, message: str, candidates: list[tuple[str, int]]):
|
|
418
|
+
super().__init__(message)
|
|
419
|
+
self.candidates = candidates
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def find_line(hst_or_items, name: str, slot: int | str | None = None) -> int | None:
|
|
423
|
+
"""The channel labelled for `name` (nrst, power_hi, power_lo: host-development-guide §8.1), or None when there is
|
|
424
|
+
none. hst_or_items: a Host (its settings are read: `ProbeConfig.items()`, no lock) or the decoded items. slot: the
|
|
425
|
+
slot's name or number; None picks the probe's one slot.
|
|
426
|
+
|
|
427
|
+
`<slot>.<name>` first, then the bare `name` - the bare name only when the settings hold at most one slot (a bare
|
|
428
|
+
name is for a one-slot probe). With several slots and no `slot`, or two channels with the same label, it raises
|
|
429
|
+
AmbiguousLine listing the candidates."""
|
|
430
|
+
items = hst_or_items if isinstance(hst_or_items, (list, tuple)) else ProbeConfig(hst_or_items).items()
|
|
431
|
+
labels = [(it.text, it.channel) for it in items if isinstance(it, Label)]
|
|
432
|
+
slots = [it for it in items if isinstance(it, Slot)]
|
|
433
|
+
if isinstance(slot, int):
|
|
434
|
+
named = [s.name for s in slots if s.slot == slot]
|
|
435
|
+
if not named:
|
|
436
|
+
raise LookupError(f"no slot {slot} in the probe's settings")
|
|
437
|
+
slot = named[0]
|
|
438
|
+
if slot is None and len(slots) > 1:
|
|
439
|
+
candidates = sorted((t, c) for t, c in labels if t == name or t.endswith("." + name))
|
|
440
|
+
if candidates:
|
|
441
|
+
listed = ", ".join(f"{t} (channel {c})" for t, c in candidates)
|
|
442
|
+
raise AmbiguousLine(f"{name}: {len(slots)} slots and no slot given; candidates: {listed}", candidates)
|
|
443
|
+
return None
|
|
444
|
+
if slot is None and slots:
|
|
445
|
+
slot = slots[0].name
|
|
446
|
+
texts = ([f"{slot}.{name}"] if slot is not None else []) + ([name] if len(slots) <= 1 else [])
|
|
447
|
+
for text in texts:
|
|
448
|
+
found = sorted(c for t, c in labels if t == text)
|
|
449
|
+
if len(found) > 1:
|
|
450
|
+
raise AmbiguousLine(f"{text}: on {len(found)} channels", [(text, c) for c in found])
|
|
451
|
+
if found:
|
|
452
|
+
return found[0]
|
|
453
|
+
return None
|