You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
(EN) Follow oep-spec d801f02 .. c6ab5d9 (capture: the probe's and the clients' questions). Registry and vectors synced;
README names c6ab5d9. configure sends the §3.3 table's TLVs and trigger_track without the critical bit, only multirate
critical; rate / samples / segments of 0 and a type 0 trigger's role and value (sent 0) are handled before sending;
nothing relies on the order of refusals. After an error stop the bytes received at or past status's write_pos are
dropped (Received drop_from / dropFrom, finish). A capture-group's pretrigger is the trigger_track's alone (bind refuses a
track with its own before sending; P_k helper; bind's unavailable cause 2 names the fn). A broken multirate declaration
is not used. Virtual bench: bind computes P_k for every other track and refuses one that cannot keep it (cause 2,
TLV fn); a streaming error stop moves write_pos back to the start of the segment not kept.
§1.1: w is any integer 1-128 - the client already read bit i*w + pos[k]; the virtual bench packs any w
(virtual_bench_capture.pack_samples) and ends segments on a whole byte for any w.
§2.2: a segment the probe cannot keep seamless is not handed out; the track stops with state 6, stopped reason 3,
error 2 (1 for DMA / peripheral). The virtual bench models it: Endpoint.capture_overflow(fn, error) and
virtual_bench_serve's stdin command capture-overflow FN [ERROR]; its status now carries the TLV error in state 6;
a bound track's group stops the others.
§5 multirate: new module multirate (Multirate, Declared, Layout with decode / encode, check, the policy
enum); LogicCapture.configure(multirate=[Multirate(role, policy, d, param), ...]) sends TLV 0xE0 per role after
checking describe (multirate_declared(); d 1 sample always accepted, min_d / max_d bound d >= 2, dd5a886), Config.block / multirate_layout() / segment_bytes(), read_segment and decode_multirate(data, samples).
The virtual bench serves it on p4-x035's logic (describe 0x60, §5.2's refusals, block L, rounding, a lower base
rate for heavy combinations, one-shot / repeat / streaming as blocks, rate_range exact). oep dump shows it.
(EN) Follow oep-spec 0098b56 .. 2c6d18d (the external review's interface re-check of 0991759). Registry and
vectors synced; README names 2c6d18d. Breaking (wire and API):
Capture §3.3 contract: LogicCapture.configure always sends mode and rate, requires samples in one-shot and
repeat, refuses it in streaming, sends segments in repeat only and pretrigger only with a trigger (ValueError
before sending; a pretrigger of 0 without a trigger is left out). One-shot's answer has no actual_segments
(Config.segments is 1).
Generations (§3.4): 1 after 0xFFFFFFFF (message.next_generation), compared for equality. Every capture event
carries its generation: capture.unpack_event / Event, LogicCapture.events(link) and CaptureGroup.events(link)
take a fn's events off the link and drop those of an earlier start (stale_events).
capture-group (§4): start's answer is blocking_ms, start_ns, the group's generation, n, n x (fn, generation) - the
TLV 0x01 and capture.GROUP_GENERATIONS are gone. CaptureGroup.start still returns (blocking_ms, start_ns) and
fills each track's generation; CaptureGroup.generation / GroupStatus.generation are the group's.
Serial paging (common §1.3): marks and capture segments answer from from_serial inclusive, nothing and more 0 from
the next serial, the oldest kept for a serial not kept; Console.marks / LogicCapture.segments continue from the
last serial + 1 (mod 2^32) until more 0 (message.serial_page_start in the virtual bench). Console streams takes
first(u16).
Debug: a step that is not status ok raises StepError with dpc_before / dpc_after None (the probe sends 0); RunResult.dpc_valid / where() never show an invalid dpc (ch32_flash uses them); arm-adi transfer of n = 0
returns [].
spi-target: fixture.wire_bits / pack_wire_bits (bit k in byte k / 8, MSB or LSB first, a partial last byte's
missing bits 0); read_rx's ns is when CS went inactive (I2C: the STOP or next START).
Virtual bench: the configure contract and answer rows, wrapping generations and serials, generations on every
event and in the group's start answer / status, marks and segments paging, release by core §2.6, step's zeros,
spi-target masking and an optional ns (Endpoint.spi_ns). New tests/test_spec_2c6d18d.py; the new vectors and
ops.json events run both ways (arm-adi's probe side skipped: the virtual bench has no arm-adi).
(EN) oep find and discovery.find_unit verify what they find (oep-spec 0991759, host guide §4.1). The DNS-SD
service name oep is not registered, so another service may advertise _oep._tcp. discovery.verify(found) checks
every instance at once (one thread each, 1 s per answer): a TCP connection, confirm (an OEP! answer, the probing
rule) and fn 0's describe, whose unit_id must be the TXT unit_id; no session is opened. discovery.check(f) is one
instance's check (None, or what it said). oep find lists only verified instances and warns on stderr for each
dropped one, naming its endpoint and what it said (none left: "no verified probe", exit 1); --no-verify lists them
raw, --verify-timeout S sets the wait. find_unit (and so tcp:UNIT_ID and its reopen) takes the first verified instance with that TXT unit_id and passes
over the others (verified=False: the first announced one); open_host still checks confirm and describe's unit_id on the link it opens (checked: unchanged).
tests/test_virtual_bench_announce.py adds in-process responders announcing a non-OEP TCP service (under its own
unit_id and under the bench's) and the bench's port under a wrong TXT unit_id; tests/test_wifi_and_tcp_discovery.py
follows (its made-up instances use --no-verify / verified=False, a wrong TXT is now dropped by find_unit, and
open_host's own unit_id check is still tested).
(EN) virtual_bench_serve --tcp speaks length frames by default (as a probe's TCP transport, transports §1;
ch32rv's tests had to pass --framing length). --framing cobs stays as an explicit option that emulates a serial
port over the socket (COBS frames and the port's raw bytes, one connection at a time, as before); the pty is
unchanged. Breaking for a caller that relied on COBS from a bare --tcp: pass --framing cobs. Help text and
README (EN / JA) follow; tests that named the old default updated (oep-client-js's test/virtual-bench.js already
names its framing).
probe.config §1.4: a probe with the wifi item answers max_frame 112 or more on every transport
(wifi_min_max_frame: one set of the longest wifi item, 10 + 3 + 3 + 32 + 64). The virtual bench complies: esp32-v003 keeps wifi (max_frame 512), esp32-v003-64 has no wifi item, and a VirtualProbe that declares wifi
below 112 raises ValueError (VirtualProbe.has_wifi).
ProbeConfig.set refuses a request longer than the transport's max_frame before sending (ValueError naming the size
and max_frame; for a wifi set on a probe below 112 it names the §1.4 rule; no passphrase in the message). config.WIFI_MIN_MAX_FRAME.
The new vector (the longest wifi set, 112 bytes, corr 0x7C) passes both ways; the wifi vector count is 9.
transports §3: a TCP probe need not advertise; the not-found messages of tcp:UNIT_ID and tcp://HOST say to name
it as tcp://HOST:PORT.
tests: test_config's paging test sets its slots one per request (the 64-byte frame); new tests for the refusal and
the profiles.
(EN) virtual_bench_serve --tcp serves several connections at once (length framing).
Up to --tcp-connections N (default 3, as the reference probe, oep-probe-arduino implementation-limits §6);
one more is accepted and closed at once. Each connection is a transport of its own (transports §1): its own confirm
and revision in use, answers on it, notifications only on the connection their fn's subscribe came on (core §11.4).
All share one probe (one Endpoint, transports §3): the session, lock and resend table - an open from another
connection while a session holds the lock is refused locked, lock_state shows it from every connection; a closed
connection leaves them, and an open with the same session id from another connection takes the session back and
its notifications with it. ch32rv found a second connection waiting behind the first.
--once ends the program when no connection is left after one was served (with one connection: as before). --framing cobs (a serial port) still serves one connection at a time; --tcp-connections with it is an error.
pty mode unchanged. A connection that takes nothing for 2 s while answers wait is closed; notifications past
2 x max_frame waiting on one are dropped (core §11.4). The reboot / lose / wifi-air stdin commands and --announce work as before; the TCP transport is in describe from the start.
Endpoint.handle(..., link=) names the connection a request came on (default: the transport index); Endpoint.pushes_to() gives each notification with its connection; revision_in_use is kept per connection.
New tests/test_virtual_bench_serve_tcp_connections.py: two clients at once (lock, locked, lock_state, end, the
other opens), a fourth connection closed (and --tcp-connections 1), one client dropping while another goes on, a
session resumed from a new connection, notifications routed and moved, --once with two, stdin commands.
(EN) virtual_bench_serve announces itself (DNS-SD), discovery queries every interface, all 8 wifi vectors checked.
virtual_bench_serve --announce (with --tcp; length framing, now the default with it) answers mDNS / DNS-SD
queries for _oep._tcp (transports §3): PTR -> instance OEP virtual <unit_id> <port>, SRV (the port, host oep-virtual-<unit_id>-<port>.local.), TXT unit_id=<unit_id>, A (the --listen address, 127.0.0.1 by default;
0.0.0.0: every announcing interface's). python-zeroconf with the mdns extra, else new module virtual_bench_mdns's
minimal responder: legacy unicast queries (source port not 5353) answered to the sender with their ID and question,
TTL 10; queries from 5353 answered on the group; one announcement at start, a goodbye at the end. New options --listen ADDR, --unit-id ID (virtual_bench.unit_id_of), --announce-on ADDR (default every interface,
loopback where it can join), --announce-engine. PORT n comes once the announcement is up. README: how a CI on one
host uses it (the querier and the responder on one machine).
discovery's minimal engine sends its query out of every IPv4 interface (IP_MULTICAST_IF per address, discovery.interface_addresses, send_query) and its 5353 socket joins the group on each: a query sent once left by
one adapter only (ch32rv on Windows: WSL's vEthernet), and a probe on the Wi-Fi adapter never heard it. The zeroconf
engine names InterfaceChoice.All (its default).
tests/test_vectors.py routes the wifi vectors by the state's "items has wifi", not the name: "pass_len 0xFF for an
index with no entry" and "a 7-byte passphrase" are now checked (the latter: ValueError before sending, the answer
malformed); a test asserts the 8 vectors, corr 0x74-0x7B.
New tests/test_virtual_bench_announce.py: oep find and discovery.find_unit (both engines) and tcp:UNIT_ID find
the announcing virtual bench on this host (each responder engine), the raw legacy unicast and multicast answers, a
responder on loopback only found by the per-interface query and not by one send by the default route; skipped where
multicast does not loop back.
tests/test_vectors.py は wifi のベクタを名前でなく state の「items has wifi」で振り分ける: 「pass_len 0xFF for an index with no
entry」と「a 7-byte passphrase」も確かめる(後者は送る前に ValueError、答えは malformed)。8 本(corr 0x74〜0x7B)あることを試験で確かめる。
(EN) oep-spec c2b8007 .. 62c1988: the wifi item and TCP discovery. README names 62c1988. Registry and vectors
synced (tools/sync_registry.sh; the wifi vectors, corr 0x74-0x7B, pass both ways).
Breaking (wire): unset's len is the key's length (probe.config §2, as the new vector "unset: wifi entry 0" has
it: 01 01 08 00); it counted the tag too. config.Removal.encoded() and the virtual bench (a len other than the
tag's key length is malformed) follow; oep-probe-arduino still counts the tag and key (OepConfig.cpp unset).
probe.config §1.4 / §3.3: config.Wifi(index, ssid, passphrase) - passphrase None (open), config.KEEP (pass_len
0xFF: the entry's own; get's form) or 8-63 printable / 64 hex (else ValueError, without the passphrase); its repr and shown() say set / none, never the passphrase. Declared.wifi_max, State.wifi = WifiState(state, entry, reason, rssi, ipv4) (off / connecting / connected / waiting; reason none / not-found / auth / no-address / other). same_items / apply compare a wifi item as get shows it (host guide §15.1). config.wifi_from_env() reads OEP_WIFI_SSID_<n> / OEP_WIFI_PASS_<n>.
oep config wifi <probe> --index N --ssid S [--pass-prompt | --pass-env VAR | --open] [--save] (neither: keep the
entry's passphrase), --from-env [--force], oep config wifi-unset <probe> --index N; show / state print the
entries (passphrase set / none) and the link (state, entry, reason, rssi, ip), --json too. Nothing prints a
passphrase.
The virtual bench: esp32-v003 declares the wifi item (wifi_max 4); the passphrase write-only (get 0xFF, a set
with 0xFF keeps it, malformed without the entry), an index at or past wifi_max unsupported with the tag as
received, an SSID with 0x00 unsupported, the hash from get's form and a token that changes with a passphrase; a
simulated link (Endpoint.wifi_set_air, wifi_join_ms, wifi_rssi, wifi_ip): entries in index order against
the networks in range, the entry in use changing joins again, another change keeps the link. virtual_bench_serve --wifi-air SSID[=PASS] --wifi-join-ms --wifi-ip and the stdin command wifi-air. Endpoint.hash_fn: a test's own hash (the vectors name the probe's).
transports §3 / host guide §4.1: new module discovery (DNS-SD _oep._tcp over mDNS: browse, find_unit, port_of; python-zeroconf with the new mdns extra, else a minimal query of its own), oep find [--json]
(unit_id, host, port, address). open_host("tcp://HOST") without a port takes the SRV port DNS-SD finds for that
host (none: LookupError naming tcp://HOST:PORT - no port is fixed); open_host("tcp:UNIT_ID") opens the probe found
by its TXT unit_id, checks describe's (UnitIdMismatch) and browses again when it reopens.
oep linktest --timeout defaults to 3 s over TCP (Wi-Fi retransmissions stall answers 1-4 s), 0.3 s elsewhere
(linktest.default_timeout); tests/hw's port_speed uses the same.
KeptSession.__del__ no longer raises ImportError at interpreter exit (fcntl / msvcrt imported with the module, __del__ swallows what a shutdown leaves).
tests/hw: TCP boards - esp32-pico-d4-50029191fe34-tcp (the ATOM, tcp:50029191fe34 or OEP_HW_ATOM_TCP) and
any tcp:<unit_id> / tcp://<host>[:<port>] in OEP_HW_BOARDS (kind tcp: never flashed, no model check). A new wifi test sets and saves the entries from OEP_WIFI_SSID_<n> / OEP_WIFI_PASS_<n> (only those that differ),
waits for the link (OEP_HW_WIFI_WAIT_S) and opens tcp:<unit_id> when DNS-SD finds it; results record OEP_WIFI_* as (set) and no SSID, passphrase or address. The virtual board gets the same networks in range
through the serve's stdin. New tests/test_wifi_and_tcp_discovery.py. README / tests/hw README (EN / JA).
(EN) oep-spec 283e5b5 .. f8bb2de (the external review re-check of 2026-10-07 and the fixes after it). README names f8bb2de. Registry and vectors synced (tools/sync_registry.sh): resend_max is gone (the host decides its resends:
this client's - once after the wait, at once up to 3 times after a broken frame on a held serial port - stay), interface_name_max_bytes 64 -> 48 (names.MAX_NAME, a list entry fits the smallest max_frame).
transports §2: a frame may be split over writes, no sender pauses probe_frame_gap_ms inside one off TCP. Checked: the
client writes each batch of frames whole in one write (serial, vendor bulk; HID reports back to back; TCP sendall) and
its length-frame reader drops a frame stalled for probe_frame_gap_ms only off TCP; the virtual bench's serial port
takes a frame in any pieces and writes its answers whole. Nothing changed.
transports §3: a closed transport does not end the session. The virtual bench's TCP (virtual_bench_serve --tcp)
already kept it; now also documented and tested (the session, lock and resend table stay until the lease runs out,
the next connection's open with the same id takes it back) and the notifications made while no connection is open
are dropped instead of going to the next connection.
A host run again ends the session its previous run left (host guide §5, f8bb2de): link.open_host(..., keep_session=True) (the default, so the oep command and tests/hw) keeps the session id in <dir>/<unit_id>.session ($OEP_SESSION_DIR, else $XDG_RUNTIME_DIR/oep-client, else the user's cache directory),
locked while the host runs; the first open opens the kept id and ends it at once (Host.end_previous), then opens
as usual. A file another running host holds is left alone; an x- unit_id keeps nothing. New module kept_session (KeptSession, Host.kept).
core §7.5 / §1.2: a probe with channels declares channels, numbers 0 .. channels - 1, absent = none. The virtual
bench's endpoint reads an absent channels as 0 (it took every channel an interface named); every profile declares
it and names no channel past it (tested).
fixture §3 (4a4631a): the virtual bench's i2c-target counts at most 1 error a write (over max_length into a full
queue: 1, was 2).
oep-if-link / host guide §17 (44aa045): the sink count is max_frame - 12, which this client already sends
(core.link_sink_size); nothing changed.
The tests run with OEP_SESSION_DIR in a temporary directory (tests/conftest.py; tests/hw keeps the real one).
New tests/test_spec_283e5b5.py. README / tests/hw README (EN / JA).
(EN) Breaking (API, CLI): the fake probe is the virtual bench, with no aliases (pre-freeze). The virtual bench is a
probe, the targets behind it and the fixture wiring, modelled after the real jigs: the environment host software is
tested in. "Bench" alone still means the real HIL jigs. Renamed: module oep_client.fake -> oep_client.virtual_bench, fake_capture -> virtual_bench_capture, fake_serial -> virtual_bench_serial, fake_serve -> virtual_bench_serve (python -m oep_client.virtual_bench_serve, its stderr lines start virtual_bench_serve:); FakeProbe -> VirtualProbe, FakeTarget -> VirtualTarget, FakeCapture -> VirtualCapture, FakeGroup -> VirtualGroup, FakeSerialPort -> VirtualSerialPort, FakeSerialStream -> VirtualSerialStream; oep dump --fake <profile> -> oep dump --virtual <profile>; tests/hw's board kind fake -> virtual, its board fake-esp32-v003 -> virtual-esp32-v003 (Board.fake_profile -> virtual_profile, Run.fake -> Run.virtual_bench);
test files tests/test_fake_*.py -> tests/test_virtual_bench_*.py. README / tests/hw README (EN / JA) say virtual
bench / 仮想ベンチ. The entries below name the old names as they were then.
(EN) Breaking (wire and API): oep-spec 7688c49 .. 0f455a0, the rule review of 2026-10-07 (§2 applied, §7).
README names 0f455a0. Registry and vectors synced (tools/sync_registry.sh; probe_config_hash.json gone).
The fake (endpoint, fake, fake_capture) answers as the new text says - other clients test against it:
core: no ignored TLV - an unknown non-critical request TLV is ignored silently, an unknown critical one refused
unsupported (its tag as received); a TLV the probe implements is checked the same with or without bit 7 (another
length, longer too, or an excluded value: malformed; an unused or unhandled value: unsupported with the tag as
received); a non-repeating tag twice: the first is used; tags 0x00 / 0x7F are no tags; booleans read non-zero as
true (open's force); request text is not checked (owner keeps its 1-32 bytes). Refusals: the header, the resend
table, the session, then every check before any change and any one reason that applies. The resend table is
(corr, answer): no corr_reused, no CRC-32 (m.CORR_REUSED, m.TAG_IGNORED, m.TAG_INVALID, Tail.ignored / more_ignored / may_have_ignored, endpoint.ignored_tlv gone). list takes first(u16) alone
(catalog.pack_list_request(first); core.list_entries(hst, name, exact) filters on the host); fn 0's describe
loses implementation, reserved, profile, resets_on_open and discoverable (chip / model free text; catalog.IMPLEMENTATION, Description.implementation gone); unavailable's payload is cause, channel, fn
(Unavailable.holder_fn / holder_kind gone, Unavailable.fn new); an ops value is base and a bitmap of 1 byte or
more not past op 0xFF; resource numbers +1, skipping those in use; link source len max_frame - 7
(core.link_size), a sink request at most max_frame - 12 (core.link_sink_size).
debug: attach, scan and riscv-dm's reset answer within max_op_ms, which the host counts as their argument time
(Wire.attach_ms(), scan_ms(), RiscvDm.reset_ms(); the 13 debug limits are gone, core.FALLBACK_MAX_OP_MS for a
probe that declares none); riscv-dm reset answers status flags pc (no method, no attempts: RiscvDm.reset(confirm) -> (flags, pc), reset_halt() -> dpc); run: a preparation that fails answers stopped 3 not_run
(RunResult.not_run; the fake: FakeTarget.fail_regs, a running hart), timeout_ms 0 is not refused; DATA0 / DATA1
restore is riscv-dm's: read_register / write_register save DATA0 first (RiscvDm.data_saved) and resume / step / run write it back first (restore_data); target_id scheme dmi_7f (Wire.SCHEME_DMI_7F, targets.SCHEME_DMI_7F); a count > 0 scan's skip is not looked at. common: read from 3 with arg > 0xFF and write
count 0 are not refused. console: no send_queue (the queue is the probe's own size; Console.send_queue gone, ConsoleIO writes frame-sized chunks); the console's reads follow the order rule (no 20 ms).
capture: configure's TLVs follow core §2.3 alone; the answer loses timing and rate_accuracy (Config.jitter_kind, jitter_ns, rate_measured, rate_ppm, ignored gone); describe's mode is mode max_samples max_segments,
channels max alone, no rate_list / rate_limit / max_read / segment_ring / frontend_shared, capture-group tracks
alone (the fake keeps widths, ring, max_read, max_tracks and budgets in Offered.inner).
fixture: gpio mode 7 gone; drive is a u8 level (0xFF the default; Drive.level(n) / Drive.default(), DriveLevels.at_most(ma); Drive.max_ma, Gpio.read_state, GpioRead, INPUT_PULLUP_PULLDOWN gone; Gpio.set
returns None and sends the drive critical): a level past drive_levels, or any drive without them, is refused
unsupported; uart status is baud and format (UartStatus(baud, format)); i2c-target has one form - I2cTarget.configure(address), a write with data is one frame (cut at max_length, errors + 1), reads from preload_tx(data) slots (it answers nothing) or 0xFF, status() -> I2cStatus(state, queued, rx_frames, tx_slots, errors), internal_pullups (arm_rx, reset, the modes, pullup_ohms gone; stretch stays optional);
spi-target has no reset (SpiTarget.reset gone) and counts errors once a transfer.
probe.config: the idle item is 4 bytes, channel mode drive(u8); an item of another length is malformed whether
critical or not; label's rule is its length; slot without lock and boot_reset (config.Slot loses lock / boot_reset; retry_ms not looked at on a host slot), slot_state 0 connected / 1 absent (SlotState(slot, state, connection, last_try_at_ns)), a host checks the target with connections' tid (oep config show prints it); bind is
one stream - config.Bind(port=, stream=("slot", n) | ("uart", fn)), port kind id - whose port position stays
during a session and carries on after it (BindState(port, flow); modes, mixed lines and Declared.bind_modes
gone); the hash is the probe's own (config.hash_of / canonical gone: config.same_items, ProbeConfig.needs_save(), ProbeConfig.apply(wanted, save) - host guide §15); storage_hash is get's hash when the
saved settings became current; at boot disable and idle before any other item. oep config bind --stream takes
one stream (no --mode / --select), oep config slot loses --lock / --boot-reset, --drive-ma picks a
level from the probe's drive_levels; fake_serve --bind N (the N-th --slot), --boot-reset gone.
link: port_speed is the handshake, baud step verify_ms (no port, no idle_ms: raise_speed loses idle_ms / port, linktest.matrix loses port); the probe goes back after verify_ms, after port_speed_idle_ms with no good
frame, or at the session's end (the fake: no broken-candidate reverts; port_speed_tolerance_pct). Host guide §17.3.2
item 4 (Q8): at a raised rate in use the first wait ends well inside port_speed_idle_ms (link.RAISED_FIRST_WAIT_S,
never under core §4.4's floor) and the request goes again at the raised rate before the link falls back to the
boot speed (confirms up to port_speed_idle_ms + host_wait_add_ms). Host guide §8 (Q6): on a held serial port a
broken frame while an answer is awaited is resent at once with the same corr, up to link.BROKEN_RESENDS (3)
times while frames keep arriving; a wait with nothing allows the one resend. plan_apply sends role_assignment
plain. restart's host procedure is the host guide's §5.2 (behaviour kept; host.RESTART_AFTER_ANSWER_S = 0.1).
oep pins reads under pull-up and pull-down only (host guide §19.1: pins.PULLED_UP gone, a weak pull-up is
floating). tests/hw: the config test compares items (same_items, needs_save) instead of computing the hash;
i2c / spi target tests configure again instead of reset; capture tests read the new declarations.
The vectors are oep-spec 85170c4's (the registry is 0f455a0's, unchanged): "rvswd scan: count > 0 with skip" is
answered as the listed scan (debug §1). tests/test_vectors.py keeps TEXT_OVER_VECTOR (empty) for a vector the
text corrects. The fake also has Endpoint.lose(connection=None) and fake_serve's stdin line lose [CONNECTION]
(for ch32rv): the connection's line lost for good - closed, its streams marked link-lost and closed with detail 4,
no_connection afterwards; an at-boot slot attaches again at its next retry, its console back under the same
stream number. A new bind starts its port's position when it is set (a stream that comes later: from its start);
a refused capture-group bind changes nothing; SwdWire.FLAGS is the swd wire's.
(EN) Host.restart_probe(wait_s=None, reopen_s=None): oep-if-restart §3 lets a host retry a restarted probe only
until its restart_max_ms has passed - a wait_s longer than a declared restart_max_ms is now cut to it, and the link
(reopen_after_restart) is closed when the window passes (the probe is gone). reopen_s is the user's reopen of a
probe given up that way, asked for beforehand: once the window has passed it is opened again as a new open (confirm
first, the boot_id checked as ever) for up to that much more; Host.restart_reopened says it was needed. For a host on
which the device comes back later than the probe can know - WSL, where usbipd attaches the re-enumerated device again
(bench: an RP2350's restart worked, but its 2.0 s restart_max_ms passed before usbipd attached it, and the rest of
tests/hw failed). oep restart --reopen-s S. tests/hw: OEP_HW_REOPEN_S passes it to the config test's restart (the
record says reopen_s / reopened, or the error); a probe that did not come back is marked gone - later tests skip naming
it, each first trying once to open it again. tests/hw puts back every setting it changes whatever fails
(Run.restore_settings: the config test's label / disable items and its saves, the capture test's idle items; each
item removed or set back to its value from before the run, the storage saved again or erased; the probe opened again
when it went away, once more at the run's end) - the bench's run had left channel 28's disable on the probe. What
cannot be put back is recorded (_settings.left_on_probe) and printed in the summary with the oep config commands
that remove it. The hw conftest skips by the hw marker (not the package's keyword), so tests/hw/test_harness.py
(the restore on the in-process fake) runs in the ordinary suite. Tests: a long wait_s cut, reopen_s on a link and on
a bare send, the restore after a failed test, nothing saved before (erased), a probe that went away and came back.
README, tests/hw README (EN / JA: the WSL / usbipd note, where tests/hw changes probe state and how each is put back).
(EN) port_speed's default ceiling is 500000 (oep-spec 32260e5, oep-if-link §3 host obligation 7, host guide
§17.3.3): the default candidate stays 500000 alone (link.DEFAULT_CANDIDATES), and a rate above link.DEFAULT_CEILING
(500000) is the user's explicit choice - named in oep speed <probe> 921600,500000 / --candidates, or passed by a
caller only when its user named it. Such a rate, in the minimal and the full form alike, is committed only after its
1 s verify in the try state: full frames (max_frame - 26) of oep.probe.link source (in) and sink (out), and duplex
when the full form verifies it, each for at least link.FAST_VERIFY_S (1 s) at its in-flight n (the caller's, else
the most the link keeps), judged as a flow is (broken + lost >= 3 over max(2 x baseline, 5 %)), no second run at
n = 1 (_flow_run takes min_s). Its try asks verify_ms link.fast_verify_ms(n) (4000 for in and out, 6000 with
duplex); a session whose lease is shorter than that + 1 s skips the candidate (trial.why says so), and link.lease_for(candidates, flows, verify) gives the lease to open with - open_host(port_speed=...) and oep speed
take at least it. Committed, the rate has the probation and in-use judging of any rate. Why: on a bridge, 921600
passed a verify of 16 full frames each way and still broke 1-4 answers in every 9 KiB upload; 500000 was clean on
every bridge measured. Tests: test_port_speed runs its procedure tests without the ceiling (an autouse fixture) and
the ceiling tests with it - the default (500000, no flows, verify_ms 2000), a faster rate's in / out for 1 s with
verify_ms 4000 before the commit, a rate that breaks past the 16 frames failing its 1 s verify (500000 then taken
with the quick verify), duplex and verify_ms 6000 in the full form, lease_for and a short lease skipping the rate, oep speed on a pty (500000 by default, 921600 only when named, in and out verified). linktest and tests/hw's board
rates are unchanged (measurements at rates the caller names). README (EN / JA), oep speed's help.
(EN) The fake's esp32-v003 profile has the classic ESP32 firmware's frame limits: max_frame 512 and
oep.target.riscv-dm max_length 488 (block_max_length(512)), as that firmware declares (oep-probe-arduino
0.0.29-dev+526a881's describe on the V003 jig) - it had 64 and 40, and a host reproducing an upload on it split its
block ops as no real classic ESP32 does (ch32rv's upload of a 9168-byte CH32V003 image: 244 write_blocks and 245
read_blocks of up to 10 words, where the real probe takes 42 and 24 of up to 122). The smallest max_frame a probe may declare (64, reg.MIN_MAX_FRAME) keeps its profile: esp32-v003-64 (fake.esp32_v003_64(), the same probe; max_length 40, list / describe paged). fake.esp32_v003
takes max_frame. Tests follow: paging, C-04's room for ignored, stream I/O in a 64-byte frame and port_speed's
probation cases (a rate that breaks after 3000 bytes) on esp32-v003-64; confirm and dump read 512 on esp32-v003. README (EN / JA), fake_serve's help.
(EN) riscv-dm read_register / write_register take cmderr 6 for a failed try, not an error: QingKe's "parity bit error"
the module took one of the group's frames for a bad one and ignored it, a missed access with the link up again at
once (bench, oep-probe-arduino 0.0.29-dev+f594f04, tests/hw test_wire on a CH32L103 through an RVSWD probe:
"read_register 0x100a / 0x1009 / 0x100b failed (cmderr 6)", 3 of 8 runs; a probe's own console write can set it
too). The group (which clears cmderr first) is run again within the held tries (RiscvDm._held_abstract; read twice
over the sentinels as before); still cmderr 6 after every try: cmderr cleared, LinkNotHeld. Any other cmderr is
cleared and raised (RuntimeError) as before. riscv.CMDERR_PARITY. The fake: FakeTarget.drop_at mode
"glitch_parity" (a write missed with cmderr 6 set). Tests (test_link_drop): a glitch_parity at every access of
read_register and write_register - the right value, no cmderr left, never raised (before: RuntimeError); two at every
pair of read_register's accesses - never a wrong value nor an error; cmderr 6 at every try - LinkNotHeld, cmderr
cleared. tests/hw test_wire: when an op raises, the record keeps the loop, the error, DMSTATUS read twice and DMCONTROL
(raw dmi) and dpc when halted (printed too). README (EN / JA).
(EN) tests/hw: the config test's reboot through oep.probe.restart is opt-in on a USB probe (the P4s, the RP2s: Board.usb): only with OEP_HW_RESTART=1; without it the reboot is skipped and the record says why ("oep.probe.restart
on a USB probe needs OEP_HW_RESTART=1 ..."), the rest of the config test runs (bench: with oep-probe-arduino
0.0.29-dev+3c0cd99 an RP2350 and an ESP32-P4 came back from a restart failing enumeration until a replug). A classic
ESP32 behind a bridge still reboots through EN; the fake through its oep.probe.restart. README (EN / JA).
(JA) tests/hw: config の試験の oep.probe.restart による再起動は、USB の probe(P4、RP2: Board.usb)では選んだときだけ: OEP_HW_RESTART=1 のときだけ行い、無ければ再起動を飛ばして記録に理由を書き("oep.probe.restart on a USB probe needs
OEP_HW_RESTART=1 ...")、config の試験の残りは行う(bench: oep-probe-arduino 0.0.29-dev+3c0cd99 では RP2350 と ESP32-P4 が
再起動の後に列挙に失敗し、抜き差しが要った)。bridge の classic ESP32 は今までどおり EN で、偽の probe はその oep.probe.restart
で再起動する。README(EN / JA)。
(EN) tests/hw: test_wire prints the whole change record when a register changes over a read_block - every change's before, after, the read again, the two words past the block and dpc, one JSON line each, in the failure message (pytest.fail, no repr cut) and on stdout - instead of an assert whose repr pytest cut. Each run keeps its own results file: tests/hw/results/---.json (the run's start, e.g. 20261006T203121), so a later run no longer overwrites an earlier one; README (EN / JA) says so
(EN) Breaking (wire and API): oep-spec 289bde0 .. 498ae95, the 2026-10-06 structure. README names 498ae95. Registry and vectors synced (tools/sync_registry.sh; ops_encoding.json new).
The core has no name (core §0, §7.2): fn 0 is never a list entry (core.list_entries leaves out an fn 0 a probe lists anyway); registry.CORE has no name and INTERFACES["oep.core"] is gone. fn 0's ops are the eight mandatory ones: confirm, list, describe, clock, open, end, keepalive, lock_state (message.CORE_OPS). Removed: message.OP_PLAN_APPLY / OP_PLAN_RELEASE / OP_RESTART, host.Host.heartbeat_seen / uptime_ns, the link's on_heartbeat / heartbeats.
fn 0's clock (core §7.7, 0x04: lock-free, session-free; answer boot_id(u32) uptime_ns(u64)): Host.clock(now=time.monotonic_ns) -> host.ClockReading(before_ns, after_ns, uptime_ns, boot_id, round_trip_ns) (.host_ns the midpoint, .uncertainty_ns half the round trip, host guide §12), Host.clock_best(n=8) keeps the shortest round trip; the boot_id is watched like confirm's. CLI oep clock <probe> [-n N] [--json].
oep.probe.plan (oep-if-plan: plan_apply 0x01, plan_release 0x02, describe 0x40 plan_roles): core.plan_apply / plan_release find it by name (core.plan_fn, core.PLAN_NAME, core.OP_PLAN_APPLY / OP_PLAN_RELEASE / TAG_PLAN_ROLES, core.plan_roles(hst)); fn 0's describe no longer has plan_roles.
oep.probe.restart (oep-if-restart: restart 0x01, describe 0x40 restart_max_ms): Host.restart_probe() / request_restart() find it by name (LookupError and nothing sent when the probe lists none; core.restart_fn, core.RESTART_NAME, core.OP_RESTART, host.OP_RESTART); core.restart_max_ms reads its describe; the link takes a completed restart on that fn (SerialLink.restart_fn) back to the boot speed. CLI oep restart <probe>.
oep.link is oep.probe.link (registry.PROBE_LINK; core.link_fn and the rest unchanged).
Notifications (core §11.3): subscribe / unsubscribe are ops 0x30 / 0x32 of the emitting interface, with no target fn (Host.subscribe(fn, min_bytes, max_delay_ms) sends min_bytes(u16) max_delay_ms(u32) to that fn, unsubscribe(fn) nothing; the blind stops too); message.OP_SUBSCRIBE / OP_UNSUBSCRIBE are registry.OP_SUBSCRIBE / OP_UNSUBSCRIBE. No heartbeat; the captures' features bit2 notify is gone.
The ops encoding (core §7.4): catalog.check_ops(value) (why it breaks: 2-33 bytes, base + 8 x bitmap <= 256, bit 0 set, last byte not 0), catalog.InvalidOps from unpack_ops, pack_ops of an empty set raises, Description.ops_invalid. A host does not use an fn whose ops break it (core.ops and Interface() raise core.UnusableFunction) and does not use a probe whose fn 0 ops do (core.describe -> NotUsable).
dump: fn 0 is described first as (core) (its ops named), then the listed interfaces (the text counts those); MISSING names fn 0's ops lacking a core op or breaking the encoding, an fn whose ops break it (shown UNUSABLE), a list entry with fn 0, and oep.probe.restart's restart_max_ms; restart max ms and plan roles are named under their interfaces. names.kind says oep instead of standard (no standard-interface category, core §13).
The fake: no fn 0 in list; fn 0's ops exactly the eight; every profile lists oep.probe.plan and oep.probe.restart after its existing fns (p4-x035 14 / 15, esp32-v003 11 / 12, p4-bench 8 / 9, rp2350-pins 7 / 8; no earlier fn moved; fake._plan, fake._restart, fake.PLAN / RESTART / LINK, PLAN_ROLES_TAG, RESTART_MAX_MS_TAG), a probe with plan roles and no plan gets one at the next fn, fake.without(probe, name); fn 0's clock; logic / analog / capture-group set subscribe / unsubscribe in their ops (others: unknown_operation), Endpoint.subscribed is fn -> (min_bytes, max_delay_ms) and min_bytes / max_delay_ms batch a streaming capture's data only, events go at once; no heartbeat; fake_serve --no-restart lists no oep.probe.restart (Endpoint.restart_offered removed; Endpoint.restart_fn); a test's broken ops value is served as given. Removed: fake.CORE_OPTIONAL, CORE_PLAN_ROLES, CORE_RESTART_MAX_MS.
tests/hw: identity checks that no list entry is fn 0 and records clock (boot_id as confirm's, the shortest of 4 round trips); config's reboot step uses oep.probe.restart when the probe lists it (else the EN line, else skipped); probe_info records plan_fn / restart_fn / restart_max_ms.
(EN) riscv-dm read_register reads the register twice in its held group and takes it only when both agree: DATA0 set to 0 before the first access-register command and to all ones before the second (riscv.READ_SENTINELS), DMSTATUS read before the second DATA0 read. Bench (oep-probe-arduino 0.0.29-dev+bd19b00, tests/hw test_wire on a CH32L103 through an RVSWD probe): 5 of 8 runs read s1 or a0 wrong after a read_block with the group's looks passing (a0 read as s1's value: the command lost, DATA0 still the read before). A lost command leaves its sentinel and a DATA0 read that gives the value read before it gives ABSTRACTCS the first time and DMSTATUS the second, so one missed access cannot make the two agree on anything but the register's value. RiscvDm.held takes agree (a try whose values it refuses is run again, as one whose looks fail) and its docs say what the looks cover: a probe that brings the link up again between them is one with the per-request check (oep-probe-arduino 4c310a2: such a request answers line, no values). The fake: FakeTarget.drop_at mode "glitch" (that one access missed, the link up again at once). tests/test_link_drop.py: a glitch at every access - af3789a's one-command group returned DATA0's old word, the read twice never a wrong value, also with two glitches at every pair of accesses. tests/hw test_wire: on a change the registers are read again and the record keeps them, the two words past the block (what the probe's reader loads last) and dpc - a value that stays says the target changed, one back as before that a read was wrong.
(EN) Raw DMI groups over a debug link that drops (oep-probe-arduino ec57b23 / f594f04: a CH32L103 behind an RVSWD probe drops its link for about 0.7 - 2 ms after a change of hart state; writes are lost and reads give the last value read or all ones, and nothing in the answer says so). RiscvDm.read_register took such a read as the register (a lost command left DATA0's old word; tests/hw test_wire's before / after could report a clobber that was not there, or miss one). New RiscvDm.held(steps, what, tries=HELD_TRIES): one dmi request of look + steps + look, a look being DMSTATUS (a found version, authenticated) and DMCONTROL (dmactive, hart 0) - riscv.link_held; a dropped link reads one value for both and cannot pass. A try whose looks fail, or that stopped line / timeout / wait, is run again after HELD_RETRY_S (5 ms), 4 tries at most, then riscv.LinkNotHeld; nothing unconfirmed is returned. read_register and the new write_register are held groups that clear cmderr first (so a redo, or a cmderr left behind, does not have the command ignored); riscv.DATA0 / DMCONTROL / DMSTATUS / ABSTRACTCS / COMMAND. uiapduino.run_payload writes mstatus / dpc with write_register and reads them back (MIE clear, dpc the payload) before the resume writes, which stay unheld (a change of hart state; uiapduino._write_register, ABSTRACTCS, COMMAND, DATA0 removed). ch32_flash.resume and tests/hw test_wire get it through read_register. Not affected: the probe's block ops, run, step, halt, resume and reset (the probe holds them since ec57b23), targets / ch32_flash.program (block ops and run), arm (SWD transfers have an ACK). The fake: FakeTarget reads DMSTATUS / DMCONTROL as the module has them, runs access-register commands on regs / dpc (cmderr ignored-while-set, 4 on a running hart, W1C), haltreq / resumereq move the hart, and drops the link at a chosen access (drop_at: "stale", "ones", "ones_line"; drop_requests, dmi_accesses, lost_writes). tests/test_link_drop.py: every access of read_register, the payload's register writes and ch32_flash.resume dropped in turn in each mode - the old code returned DATA0's old word or all ones, left the hart off the payload, or said a resume went; the held groups return the right value (drop over by the next request) or raise LinkNotHeld (a drop that lasts).
(EN) tests/hw on the new wire: port_speed is read from oep.link's ops (oep-if-link §1), no longer from fn 0's describe (the run raised KeyError 'port_speed' in flash / identity); the record keeps link_fn and port_speed only when the probe offers them. The V003 jig's ESP32 wires every output-capable channel but SWIO / NRST to the V003, so its table entry gives only the input-only 34 / 35 (config's disable / label, the analog capture): gpio / capture / capture_group skip there, uart runs on the jig's settings plan or skips, i2c / spi target find too few channels and skip. A table entry's 0 channels make a fixture test skip; Board.label names config's label channel. The X035 P4 and RP2350 entries were checked against their bench wiring: unchanged.
(EN) oep-spec 3c96daf (core §1.2, §6.6, §7.5; transports §1): fn 0 describe restart_max_ms (tag 0x4F, u32, required with restart): the longest from restart's answer until the probe answers confirm again on the same transport. Registry and vectors synced (ops.json's restart states name it). README adds 3c96daf beside ecd1ab9. The fake declares restart_max_ms 2000 with restart (fake.RESTART_MAX_MS, fake.CORE_RESTART_MAX_MS; FakeProbe adds it to fn 0 whose ops set restart; Endpoint.restart_max_ms; describe carries it exactly while restart_offered, so --no-restart drops both). Host.restart_probe(wait_s=None) waits the probe's restart_max_ms (read before the restart, core.restart_max_ms), or RESTART_WAIT_S (10 s) when it declares none; past it the probe is gone (ConnectionError / TimeoutError). dump names restart_max_ms missing when restart is in ops. Tests: tests/test_restart.py (the describe value, the default wait, giving up after it), tests/test_capabilities.py.
(EN) oep-spec ecd1ab9 (core §6.6, §12; link §3 host obligation 6): fn 0's optional restart (op 0x14, needs the lock, limits.restart_after_answer_ms 100). Registry and vectors synced (ops.json: the answer, session_required, no_session, unknown_operation). README names 59dd028 with the restart from ecd1ab9. The fake: every profile offers it (fake.default_ops for fn 0 adds fake.CORE_OPTIONAL; fake.CORE_REQUIRED is public; Endpoint.restart_offered, fake_serve --no-restart); it answers completed success with no payload and, once the answer is out (Endpoint.after_answer, which also does what speed_after_answer did), reboots (Endpoint.reboots counts); in between nothing is processed or pushed; a serial port keeps the queued answer and drops the bytes it read behind the request, fake_serve's TCP length framing drops its buffer; the pty or TCP connection stays. The client: Host.request_restart() (the request alone; the session and the fn names are gone after it), Host.restart_probe(wait_s=10) (restart, then SerialLink.reopen_after_restart: close, wait, open again as a new open - confirm first, the boot speed, USB found again, TCP reconnected; or, on a bare send, wait and confirm - and return the new boot_id; host.NotRestarted when it stayed the same; a lost answer refused no_session on its resend counts as done), message.OP_RESTART, host.RESTART_AFTER_ANSWER_S; a completed restart puts a raised serial link back at its boot speed like end. SerialLink.reopener (open_host sets it for a serial path, TCP and USB). tests/test_restart.py.
(EN) oep-spec 59dd028 (debug §1, §3; probe-config §1.1): an attach that joins an existing connection keeps the connection's setting for every setting TLV it does not carry - idle_clock absent keeps the current rest (absent = high is for a new connection only), max_speed is always carried and only lowers the speed - and a scan never changes a live connection's settings. README names 59dd028; the registry and vectors did not change since 4bd3a87. The fake already behaved so; its docstrings say it and tests/test_spec_59dd028.py checks it (a join without idle_clock keeps low, one with high changes it, a slot's low rest survives a host join, a new connection rests high, a scan over a live pair leaves its speed and rest). The client: Wire.attach sends idle_clock whenever it is given, high included, and its docstring says a caller that knows the target's rest (a slot's idle_clock) passes it; no client helper attaches to a slot with the slot's settings (the pin finder attaches to pins it found, before it knows the family; uiapduino targets swio, which has no idle_clock).
(EN) Breaking: oep-spec 4621eab .. 4bd3a87. README names 4bd3a87. Registry and vectors synced again (ops.json: console cases on stream 2, link source past max_frame - 26; discovery.json: fn 0 sets every required op).
Console send queue (f0c68bf, R-A; console §1, §2, §3, common §1.4): a write goes into the stream's send queue, accepted = min(count, its free space), 0 (completed failed) only when it is full; SDI takes nothing. Console.send_queue reads the describe's tag 0x41 (u16, at least console_send_queue_min_bytes 64), Console.mechanisms(); ConsoleIO writes a send_queue at a time (ConsoleIO.send_queue) and goes on from each accepted. The fake: every console with DMDATA or dmseq declares send_queue 256 (fake._console, fake.CONSOLE_SEND_QUEUE); each stream's queue (Stream.queue; Endpoint.send_queue) is handed to the target 3 (DMDATA) / 2 (dmseq) bytes a poll, one poll per ms while the hart runs and answers, console_take hands all of it; it stays over resets and restarts, clear leaves it, it goes when the stream closes (console_slot, Stream.slot removed).
Reset settle wait (f0c68bf, R-B; debug §3, §4.3, core §4.4): Wire.attach_ms(reset) is attach_budget_ms + hold_ms + reset_settle_ms (700) with the reset TLV, RiscvDm.reset_ms() reset_settle_ms (both at most max_op_ms) - what the host's wait counts as argument time. The fake: FakeTarget.restart_ms (the DM silent after a reset's release); riscv-dm reset and attach with the reset TLV wait for it up to reset_settle_ms, then answer status line (reset: no flags bit2, the connection kept; attach: an existing connection kept and marked reset detail 3); Endpoint.settle_log.
d34dafa: a probe.config item longer than its definition is a longer request TLV (core §2.3) - unsupported when critical, else not applied and listed in ignored, the other items applied; a label's text runs to the end (the fake already did; tests added). 4bd3a87: oep.link source's len is at most max_frame - 26 (link_source_overhead_bytes), the ignored room kept always; core.link_size(max_frame); raise_speed's flows, linktest's default size and core.link_speed use it (was max_frame - 16).
(EN) Breaking (wire and API): oep-spec b69ec26 .. 4621eab, the 2026-10-06 simplification (docs/v1-simplification-proposal-2026-10-06.ja.md, every item applied; until the freeze the Japanese text is the working text). The README names the spec commit this implements (4621eab; no v0.x tag yet, versioning §6). Registry and vectors synced (sessions.json and ops.json new).
Codec (core §2.2, §2.3, §4.1): one 10-byte request header role 0x01, corr(u16), fn(u16), op(u8), session_id(u32) on every request, 0 = no session (role 0x81 is gone; message.Request.session is an int, None taken as 0; REQUEST_HEADER 10, ROLE_SESSION / SESSION_BYTES removed). One TLV form tag(u8) len(u16) value (TLV_LEN_LONG, TLV_SHORT_MAX, BadTlv removed). Sequences are count x element with no element length (list, connections, scan, marks, streams, segments, probe.config state; Reader.element and message.element removed). Fixed forms are closed: probe.config's idle is 6 bytes (drive_kind 2 = the default level, value 0; an input idle carries kind 2), a slot carries boot_reset right after attach and ends with the lock, slot_state carries reset_at_ns before the tid, a bind's streams are 3 bytes each; fixture.Drive.default().
ops (core §1.2, §7.4): every fn's describe carries the ops tag (0x09, base + bitmap), the one declaration of the ops an fn offers; it replaces the features bits that declared optional ops (riscv-dm block / run / reset / step, i2c-target stretch, logic / analog query and force, capture-group force) and fn 0's describe 0x4E. New: core.ops(hst, fn), core.offers(hst, fn, op), Interface.ops() / .offers(op), core.require / core.not_offered (raise, without sending, the same Rejected with detail unknown_operation the probe answers; the clients themselves still send and let the probe answer); catalog.pack_ops / ops_tlv / unpack_ops, Description.ops; RiscvDm.declared() reads the ops tag; dump shows every fn's ops by name and names a describe without them as missing; fixture.I2cTarget.FEATURE_STRETCH removed.
No resume (D1, core §6.2, §6.4, §9): end, lease expiry and force release everything the session created; only open takes the lock. Host.open(lease_ms, force=, owner=) always opens a new session under a new random id (the session= argument is gone) and returns Opened(lease_ms, boot_id) (resumed / swept and host.RESUMED gone). host.Expired and message.EXPIRED are gone: a request of an ended session raises NoSession, which leaves Host.session None and moves Host.epoch; end() does the same. A changed boot_id still drops the fn names; an open answering resumed 0 no longer exists. host.rejection(result) takes no lease.
oep.link (D3, oep-if-link): the link test and port_speed moved out of fn 0 into the optional interface oep.link (source 0x01: length(u32) -> len(u16) data; sink 0x02: count(u16) data -> empty; port_speed 0x03, offered when its ops set it). message.OP_LINK_SOURCE / OP_LINK_SINK removed; core.LINK_SOURCE / LINK_SINK / LINK_PORT_SPEED / link_fn / link_source_request / link_sink_request / link_source_data; core.link_speed, linktest and link.raise_speed find oep.link by name. link.raise_speed(hst, rates, record=, flows=, max_tries=) and SpeedReport (chosen, rate, trials with flows, skipped, step_downs, retried, capped) are unchanged; a probe without oep.link, or whose oep.link does not offer port_speed, reports supported False with the reason. link._speed_port returns (fn, port, why).
The fake: every profile's fn carries its ops (fake.FakeProbe fills in every op of the interface's table when a profile gives none, fill_ops=False leaves it out; fake.ops_of(name, *without); esp32-v003's riscv-dm has no step and its i2c-target no stretch, as before), and offers reads them; every profile has oep.link as its last fn (esp32-v003's with port_speed). End / lapse / force release the session's plan, its shares of streams (a stream it was the last user of closes with detail session_ended, before the connection) and connections, subscriptions and capture-group bind; console streams stay the probe's per place and mechanism (readable until the next open there, which returns the number with its position and marks); an attach on a live combination returns the slot's connection; the resend table answers a resent end, never looks up an open. An item or request TLV longer than its form is unsupported when critical, else ignored. fake_serve and its reboot line work as before.
Console writes (oep-spec 4621eab, console §2 / §3): the fake takes at most the mechanism's send slot (DMDATA 3 bytes, dmseq 2, SDI 0) and nothing while earlier bytes are still in it (Endpoint.console_slot, Stream.slot; the target takes them at the probe's next poll or at console_take(sid); console_accept removed). ConsoleIO.write goes on from each answer's accepted and raises TimeoutError after stall_s (default 2 s) with nothing accepted (an SDI console never accepts).
Spec references in code follow the split (core §3.x / §5.1 / §16 -> transports §x, core §3.5 -> oep-if-link §3). tests/test_vectors.py runs sessions.json step by step and every ops.json case on the fake in the state it names, and the client's own request and reading where it has the op; tests/test_no_resume.py; the hw session test checks no_session instead of expired / resumed.
(EN) Fix: a bare usb target looks at every device on the project's VID:PID, not the first one libusb lists. With several probes attached, a CDC-only one (an RP2350) listed first made it give up the vendor / HID path at that device without trying the others. Now link.usb_probes() lists each device once, whatever ways in it has (vendor bulk and HID from libusb / hidapi, CDC ports from pyserial, merged by USB serial or location), as link.UsbProbe (unit_id, ways, ports, location). Exactly one probe: it is opened (vendor, then HID, else its CDC port; a probe with several CDC ports asks for one). More than one: nothing is opened and link.SeveralProbes (a ValueError, .probes) lists each with its unit id and ways in and asks to name one (usb:<unit_id>, or its serial port); the oep command prints that list in its one line. None: the "no way in" error as before. usb:VID:PID[:SERIAL] and usb:<unit_id> are unchanged.
(EN) fake_serve: a line reboot on stdin reboots the fake probe mid-session with a new random boot_id (the session table, the resend table, connections, streams, subscriptions, the plan and the unsaved settings go; the saved settings apply again; the pty / TCP connection stays open, a serial port's half-read frame and unsent answers are lost). stderr says fake_serve: rebooted, boot_id 0x........; any other line is ignored with a message on stderr. stdin is read without blocking the serving. --keep-on-eof: the end of stdin no longer ends the program (by default it still does). Endpoint.reboot now also forgets the revision in use, and keeps the simulation's own knobs (capture_slipped, uart_clock_hz) that it used to reset.
(EN) The oep command (dump, config, speed, linktest, pins): a probe that cannot be opened (no device on the project's VID:PID, no port, not an OEP probe, a value the host does not use) ends it with one line on stderr instead of a traceback; for a usb target the line adds a hint: install the usb / hid extras, or name the port. A bare usb whose CDC fallback finds several ports lists them (name one), and says so when it finds none.
(EN) What every probe must give (core §1.2, §7.1, §7.5; oep-spec docs/conformance.md section 1) as one list: dump.required_missing also names describe's discoverable of fn 0 (every probe sends it, 0 when it is not on the project's VID:PID) and a confirm transport index that names no transport of fn 0's describe (0xFF, a relaying broker, names none and passes); dump.required_of(call, confirm) runs it on a probe lock-free. tests/hw has a required test after identity that fails naming each item missing (a probe without confirm's transport TLV or discoverable passed tests/hw before). The fake sends discoverable 0 on a probe not on the project's VID:PID (esp32-v003), as §7.5 says, instead of leaving it out.
(EN) Breaking: clipped analog values (oep-spec ab2045c, capture §1.2 rule 6). A raw value of 0 or 2^b - 1 (the converter's minimum or maximum code) means the input was at or beyond that end of the frontend's range, so AnalogCapture.millivolts(k, value) now returns None for it instead of a voltage (its type is float | None; a caller that did arithmetic on it gets a TypeError there, not a wrong number). values() still returns the raw codes unchanged. New: AnalogCapture.clipped(k, value) (0, CLIP_LOW = -1 at or below the low end, CLIP_HIGH = +1 at or above the high end; with a negative scale_nv code 0 is the high end), ends_millivolts(k) ((low, high) in mV, what a clipped value is shown against), clip_mask(k, values) and clip_counts(k, values) ((low, high)). The hardware test's analog record gains clipped and its mean_mv is None when every value is clipped at one end. The fake still sends raw codes (its square wave and the sine's troughs are at the codes' ends).
(EN) Breaking: the project's own USB VID:PID, 1209:4F45 (oep-spec d50a84e: registry usb.project_vid / project_pid, core §3.3, §7.5). The registry synced (USB["project_vid"], USB["project_pid"]). link.PROJECT_VID_PIDS is ((0x1209, 0x4F45),), taken from the registry, and link.is_project_device is true for it alone. link.USB_VID / USB_PID, what a bare usb target opens, are that VID:PID. A bare usb with no vendor bulk or HID way in on it opens the one CDC port carrying it (link.project_serial_ports(): pyserial's list filtered by the VID:PID, as (port, USB serial); several ports: name one) - a CDC-only probe (RP2040 / RP2350) is found so. The temporary clues are gone: link.temporary_clue and TEMPORARY_IPRODUCT_PREFIX / TEMPORARY_VENDOR_INTERFACE / TEMPORARY_HID_USAGE_PAGE are removed and nothing looks at iProduct. A probe on any other VID:PID is opened only by its unit id (usb:<unit id>), usb:VID:PID or its port. The fake's USB profiles (p4-x035, p4-bench, rp2350-pins) say discoverable = 1; esp32-v003 (a UART bridge alone) still sends none. New: udev/70-oep-probe.rules (the USB device and hidraw of 1209:4F45 for the logged-in user and plugdev; installed with administrator rights, README).
(EN) The rules since the 2026-10-02 changes (oep-spec 2e70f40, 975d88c, 598bb26, 8d91db0, 73a0c37, b4b08f1, 40291a4; docs/v1-rule-change-proposal-2026-10-06.md). Registry and test vectors synced (oep-spec fb7777e: LIMITS["max_op_ms_max"] 600000, mark_detail_reset 2 reserved, rvswd / swio scan_kind riscv-dm only, spi-target cs_setup_ns); tests/vectors/discovery.json is checked like the other vectors (list, describe of fn 0 and past the end, the order-1 refusals of the smallest probe: the fake's answers byte for byte, the host's reading). Fake: an op the interface does not define, or an optional op the probe does not declare (riscv-dm read_block / write_block / run / reset / step by features bits 0-3, capture query / force and the group's force, i2c-target stretch, probe.config save / erase without storage), is unknown_operation at core §4.3 order 1, before session_required (Endpoint.offers; it answered session_required, and served undeclared riscv-dm ops); plan_apply and probe.config set / unset check every malformed case, then the fns inside the payload (unsubscribe too), then unsupported for the whole request (C-21), and capture-group bind and a bound track's start check their form before the state; a plan item's unsupported names the item's tag; non-request roles and short requests are discarded (C-36); the clock is ns since boot at the resolution given (Endpoint(now_ns=...); fake_serve: time.monotonic_ns), never back, from 0 at reboot(), which now draws a boot_id when given none (C-31, C-19; fake_serve draws its own); confirm's bounds and max_op_ms 1..600000 are checked when an Endpoint is built (C-20, C-47); a UART bridge / TCP transport names interface 0xFF (C-41); every channel not reserved is parked at boot (△12); a riscv-dm op whose target does not answer fails with status line and rests the lines undriven until one succeeds (pin_statewire-free; debug §2); the attach reset TLV on an output-idle channel is unavailable cause 5 holder_kind 7; search_retries only when a bring-up ran; i2c-target refuses 0x00-0x07 / 0x78-0x7F (△5); a uart write without TX is unavailable cause 6 (△6); esp32-v003's spi-target declares cs_setup_ns 4000. Client: host.NotUsable for a confirm outside max_frame >= 64, window >= max_frame, max_inflight >= 1 (C-20) and a max_op_ms outside 1..600000 (C-47), nothing more sent; the transfer time counts min_max_frame until a confirm answer, then the latest one's max_frame, the link's own confirms included (N-1); an open with the id used last answered resumed = 0 drops the name -> fn cache and the describes (C-19); fn 0 heartbeats are read (Host.heartbeat_seen, uptime_ns) and their boot_id watched, as the comments had claimed; short answers / events / data are broken frames (C-36); a resend that gets no answer raises link.TransportFailed and the next request first recovers with a §5.1 confirm on every kind of frame, COBS included, its boot_id checked (recover_transport; C-38); RiscvDm.declared(); SpiTarget.cs_setup_ns (shown by oep dump); I2cTarget.configure refuses the reserved addresses before sending; ProbeConfig.state() documents the last page's storage (PC-9). The host and probe guide section numbers in the code follow oep-spec's renumbering (host §2, §4, §6, §9, §17, §18.1, §19; probe §6, §8); tests/hw/README points to release-testing.md.
(EN) The 2026-10-02 rule changes (oep-spec 09622ef..e9cd891, docs/v1-rule-change-proposal-2026-10-02.md). Registry synced (role_assignment is the number 0x10 and core.plan_apply sends it critical; the target_id schemes in registry.COMMON; registry.REFERENCE["max_op_ms"]; the analog trigger enum without level / edge; new limits, timing values, TLVs and enums). Fake: a value a later revision may define is unsupported (list flags, attach method, reset mode, read from, gpio mode, i2c-target mode, uart format, port_speed step, probe.config idle mode / drive_kind / slot enums / bind stream kind / lock scheme; an undefined gpio drive kind ignored or unsupported when critical); a request's form, then its values, then the state (core §4.3), order-1 refusals before the resend table; a non-repeating request TLV twice or short is malformed, a longer one unsupported (critical) or ignored; ignored at most 16 entries with 0x00 as the 16th, room kept; confirm's transport TLV, the revision per transport, min > max malformed, the supported range in the refusal; rejected answers remembered and the lease restarted by them, lease 1000-60000; session_id 0, open as 0x81, booleans and text malformed; plan ops unknown_operation without plan roles; verify_ms 0 in a try malformed; count = 0 leaves out idle-item channels, a named output idle is unavailable cause 5 holder_kind 7; undeclared scan combinations unsupported with their index, attach's pins tag as received, a swio swclk other than 0xFFFF unsupported; found = version >= 2 and != 15; halt / step failures (step_left); search_retries; pin_state (MISO only while CS is active, i2c-target open-drain and with_i2c_pullups, plans change no pin, an analog plan on an output idle refused); capture samples rounded down, mode / rate / trigger refused as received, capture-group state and start checks, a read past the write position; probe.config's line search with the firmware labels as step (c), no_pull, an item's channel, label text, a saved bind on a non-serial port (reason 2), an item's unsupported naming its tag; the profiles reserve only the probe's own channels; fake_serve's TCP listener is a TCP transport, an over-long length closes it. Client: every wait at least core §4.4's floor (argument time incl. the attach / scan budgets + 1000 ms + a serial port's transfer time); serial ports 8N1, DTR / RTS asserted; later confirms (the link's own, dump's) ask for the revision in use; limits["transport"] and port_speed on the transport this host came on; an x- unit_id keys no speed record and names no USB device; no stall resync on TCP, 250 ms after the last write before a resync's confirm; find_line with firmware labels; answer text shown without control characters, the owner cut on a character; Tail.more_ignored; dump names what core §1.2 requires and is missing; capture's critical TLVs and blocking_ms; Wire.search_retries, riscv.StepError, I2cTarget.pullup_ohms, describe's capture mode with background. tests/vectors (the spec's vectors, synced by tools/sync_registry.sh) checked by tests/test_vectors.py.
(EN) Fake: the transport list of fn 0's describe follows each TLV's index byte (core §7.5: index is the number designating the transport), not the TLV order; port_speed's port and a bind's port (probe.config §1.2) are checked against that index, so a probe declaring its transports out of order or with gaps is handled. Link: the link's own confirms (the §5.1 resync on length-prefixed ports, confirm_raw) ask for the revisions this client handles (1..1, core §7.1: the host sends the range it can handle), not 0..0xFF. Tests: transports out of TLV order and with gaps (port_speed accepted on the UART bridge's index, refused on the vendor bulk; a bind on the serial port's index, unsupported on the other), the range of the resync's and confirm_raw's confirms.
(JA) fake: fn 0 の describe の経路の並びは TLV の順ではなく各 TLV の index バイトに従う(core §7.5: index は経路を指す番号)。port_speed の port と bind の port(probe.config §1.2)はその index で調べるので、経路を順不同や飛び番で宣言する probe も扱える。link: link 自身の confirm(長さ前置きの経路での §5.1 の再同期、confirm_raw)は 0..0xFF ではなくこのクライアントが扱える revision(1..1、core §7.1: host は扱える範囲を送る)を求める。試験: TLV の順と違う経路と飛び番の経路(UART bridge の index での port_speed は受け付け、vendor bulk では拒否。serial port の index での bind は受け付け、もう一方は unsupported)、再同期と confirm_raw の confirm の範囲。
(EN) Fake, as oep-spec 9ed53e7 / 3cc50c8: describe of an fn the probe does not offer is rejected unknown_function (core §4.3), not malformed; the retry with reset (probe.config §3.1) counts in slot_state's last_try_at_ns (the attempt after the hold, so the next plain retry is retry_ms after it); a slot's pins without a connection are free for the idle state (probe.config §1: the state of a pin neither a plan nor a connection uses), so an idle change there applies at once (it used to wait for a release that, with the slot gone, never came). Tests: unset of an undeclared tag is unsupported with the tag as received; an idle set / unset applies at once on a free channel and at the next release on one a plan or a connection holds (mode and drive).
(EN) fake_serve options for what other hosts' tests could not reach over TCP / a pty: --no-drive-levels (the profile's oep.fixture.gpio without drive_levels, fake.without_drive_levels), --silent-until-reset N (the N-th pin pair of the first wire has a target that answers nothing until a reset through its line, FakeTarget.silent_until_reset), --boot-reset (every --slot asks for the at-boot retry with reset, boot_reset 1) and --label CH=TEXT (a label item at boot, as if saved, e.g. 23=v003.nrst): the saved items go in at once, so the retry with reset happens at start. fake_serve.parse(argv) gives the options build takes. Tests: tests/test_drive_and_boot_reset.py. A new test checks that the fake lists ignored request TLVs on failed and partial answers too (core §2.3, oep-spec 9ed53e7; it already did): wire attach, riscv-dm step / read_block / write_block / run / resume / dmi, console write.
(EN) oep pins: the dead --reset-channel / reset_channel slot-field path is gone (a slot has no reset field; the reset line is the label <slot>.nrst, probe.config §1.3, oep-spec 6489c8e). The suggestion prints the slot line and the <slot>.nrst / <slot>.power_hi labels, and --save writes the slot and those labels; Report.reset_channel stays as the channel found. The README example shows the label lines instead of the old # reset_channel comment.
(EN) The fake listed unknown non-critical request TLVs in the answer's ignored (core §2.3) only at some ops; about 50 ops (end, list, plan_apply, plan_release, subscribe / unsubscribe, the wires' scan / detach / connections, riscv-dm's dmi / halt / resume / read_block / write_block / run / step, console and uart read / mark / clear and more, logic / analog / capture-group start / stop / force / status / read / segments / release, probe.config save / erase / unset / state) dropped them. endpoint.Take now keeps the ignored tags and Endpoint._dispatch appends them to every completed answer in one place (no handler attaches them). A TLV in a describe or probe.config get request is now rejected malformed (core §7.3; get used to list it as ignored), plan_apply refuses tags 0x00 / 0x7F / 0xFF as malformed, and probe.config set's unsupported names an unknown item's tag as received (with its critical bit). tests/test_request_tails.py sends an unknown TLV, non-critical and critical, with every op of every interface the fake serves.
(JA) fake は、要求の未知の non-critical の TLV を応答の ignored に載せる(core §2.3)のを一部の op でしかしておらず、約 50 の op(end、list、plan_apply、plan_release、subscribe / unsubscribe、wire の scan / detach / connections、riscv-dm の dmi / halt / resume / read_block / write_block / run / step、console と uart の read / mark / clear ほか、logic / analog / capture-group の start / stop / force / status / read / segments / release、probe.config の save / erase / unset / state)で落としていた。endpoint.Take が ignored の tag を持ち、Endpoint._dispatch が完了したすべての応答に 1 か所で付ける(handler は付けない)。describe と probe.config の get の要求に TLV があれば rejected malformed にした(core §7.3。get は ignored に載せていた)。plan_apply は tag 0x00 / 0x7F / 0xFF を malformed で断り、probe.config の set は未知の項目の unsupported に受け取ったままの tag(critical の bit 付き)を載せる。tests/test_request_tails.py は fake が持つすべての interface のすべての op に未知の TLV を non-critical と critical で付けて送る。
(EN) oep-spec ee562c3: gpio output strength and the at-boot retry with reset. Registry synced (hash 29f3ea526760a418). Client: fixture.Drive (Drive.level(n) / `Drive.max_ma(m