Types & errors
Every enum, dataclass, and exception the package exposesReference for the values the API takes and returns. Field meanings live with each command, so this page links to the Library types and Native API. Raw HID id meanings (keycodes, button slots, Consumer usages) are on Usage IDs.
Every enum subclasses enum.IntEnum. A member is its wire byte: int(Button.LEFT) == 0, and anywhere an enum is accepted a bare int works too, for a raw HID id, an endpoint address, or an interface number with no named member.
Injection enums
Button · ActionSee the injection model for what each Action means; button slots on Usage IDs.
| Member | Value | Meaning |
|---|---|---|
SOFT_RELEASE | 0 | release unless the user is physically holding it |
PRESS | 1 | hold down |
FORCE_RELEASE | 2 | release even against a physical hold |
Lock & blanket enums
Direction · BearingMode · LockTargetKind · BlanketSee Lock for what a direction and a blanket class mean, and Catch for the third reading a direction has on a traffic subscription.
One enum with three readings, picked by what it is attached to: an axis, a usage, or a CatchFilter naming one of the byte-oriented catch classes.
| Member | Value | Aliases | On an axis or wheel | On a button or key | On a traffic-class filter |
|---|---|---|---|---|---|
BOTH | 0 | - | both signs; on a scale, a full pass to the relative pair | press and release | both directions |
POSITIVE | 1 | PRESS · IN | +x / +y / wheel-up only | the press edge | IN, device to PC |
NEGATIVE | 2 | RELEASE · OUT | -x / -y / wheel-down only | the release edge | OUT, PC to device |
WITH | 3 | - | the sign the box is injecting | refused | no meaning |
AGAINST | 4 | - | the sign opposing it | refused | no meaning |
The aliases are the same values under names that read at the call site: Direction.PRESS is Direction.POSITIVE. WITH and AGAINST are measured against the bearing rather than a fixed sign; .is_relative tells them apart.
Only an axis has a bearing, so WITH or AGAINST on a lock anywhere else raises RelativeDirectionError. A media usage has no edges: an edge named on one goes out as BOTH, which is what Locks reports it as.
How the box reads the direction it is injecting, which is what WITH and AGAINST resolve by. Set with dev.set_bearing(window_ms, mode).
| Member | Value | Meaning |
|---|---|---|
PER_AXIS | 0 | each axis compares its own sign against its own bearing, independently; the default |
VECTOR | 1 | the delta is projected onto the injected direction, and the relative scale weighs only the part along it; one relative scale, the lower of X's and Y's, governs the whole aim, and the fixed-sign scales still reach what the projection leaves on each axis |
What Locks reports back under VECTOR is there.
| Member | Value |
|---|---|
X | 0 |
Y | 1 |
WHEEL | 2 |
USAGE | 3 |
Built for you by LockTarget.x/y/wheel/usage (and the button/key/media shortcuts); you rarely name it directly.
| Member | Value | Class | What direction picks |
|---|---|---|---|
AIM | 0 | the X and Y cursor axes | a sign, on each axis |
WHEEL | 1 | the wheel | a sign |
BUTTONS | 2 | every mouse button | an edge, on each button |
KEYS | 3 | every keyboard key and modifier | an edge: POSITIVE blocks presses, NEGATIVE releases, BOTH both |
MEDIA | 4 | every media usage | nothing; media has no edges |
These are ABI-local ordinals (matching the crate's Blanket order), not the clip auto-lock scope bits.
Keycode enums
Key · MediaKeyNamed subsets of the HID usage tables. The full list of ids and what they do is on Usage IDs (keys) and Usage IDs (media). Any call that takes a Key or MediaKey also accepts a raw int usage.
| Members | Values |
|---|---|
A … Z | 4 to 29 |
N1 … N9, N0 | 30 to 39 |
ENTER ESCAPE BACKSPACE TAB SPACE | 40 to 44 |
CAPS_LOCK | 57 |
F1 … F12 | 58 to 69 |
INSERT HOME PAGE_UP DELETE END PAGE_DOWN | 73 to 78 |
RIGHT LEFT DOWN UP (arrows) | 79 to 82 |
LEFT_CTRL LEFT_SHIFT LEFT_ALT LEFT_GUI | 224 to 227 |
RIGHT_CTRL RIGHT_SHIFT RIGHT_ALT RIGHT_GUI | 228 to 231 |
| Member | Value |
|---|---|
PLAY | 176 |
PAUSE | 177 |
NEXT_TRACK | 181 |
PREV_TRACK | 182 |
STOP | 183 |
PLAY_PAUSE | 205 |
MUTE | 226 |
VOLUME_UP | 233 |
VOLUME_DOWN | 234 |
Emit pace
EmitMode · EmitPacePassed to dev.set_emit_pace(). See Options.
| Member | Value |
|---|---|
LEARNED | 0 |
INTERVAL | 1 |
FIXED | 2 |
A frozen dataclass carrying mode and hz. Build it with EmitPace.learned(), EmitPace.interval(), or EmitPace.fixed(hz) (the rate snaps to 1000/n and caps at 1 kHz).
Clip
ClipState · Edge · ClipAction · ClipTrigger · ClipSettings · ClipStatusThe buffered-clip types. Concept on Clip.
| Member | Value | Meaning |
|---|---|---|
IDLE | 0 | No clip playing. |
PLAYING | 1 | Draining the ring, one entry per native frame. |
PAUSED | 2 | Halted mid-clip; the cursor and any held input are retained. |
FAULTED | 3 | An append was dropped or the ring overflowed; clear to recover. |
| Member | Value | Meaning |
|---|---|---|
DEVICE | 0 | The PC-facing chip, written directly over the control port. |
HOST | 1 | The chip that reads the real device, relayed over the inter-chip link. |
| Member | Value | Meaning |
|---|---|---|
NEW | 0 | Selected but not yet booted. |
PENDING_VERIFY | 1 | Booted and on probation; the window rollback lives in. |
VALID | 2 | Confirmed by the image itself. |
INVALID | 3 | The image asked to be rolled back. |
ABORTED | 4 | Booted once and never confirmed. |
UNKNOWN | 0xFF | No entry for this slot. |
| Member | Value | Fires on |
|---|---|---|
BOTH | 0 | either edge of the trigger usage |
PRESS | 1 | the physical press edge |
RELEASE | 2 | the physical release edge |
Which edge of a ClipTrigger runs its action.
| Member | Value | Runs |
|---|---|---|
START | 0 | rewind and play |
STOP | 1 | stop and release |
PAUSE | 2 | halt mid-clip |
RESUME | 3 | continue from the pause |
RESTART | 4 | force a rewind and play |
TOGGLE | 5 | play if idle/paused, stop if playing |
The action a bound trigger runs on the box, matching the clip.start/stop/pause/resume/restart/toggle methods.
A dataclass binding a physical usage's edge to a clip action, passed to clip.bind(). The box runs the action itself with no host round-trip.
| Field | Type | Meaning |
|---|---|---|
on | Usage | the trigger usage (button, key, or media) |
edge | Edge | which edge fires the action |
action | ClipAction | what the box runs |
consume | bool | suppress the physical edge so it does not reach the PC (default False) |
Construct it directly, e.g. ClipTrigger(Usage.button(Button.SIDE1), Edge.PRESS, ClipAction.TOGGLE, consume=True).
| Field | Type | Meaning |
|---|---|---|
autolock | List[Blanket] | the input groups auto-locked while the clip plays |
loop | bool | playback loops at the clip end (retained mode only) |
retain | bool | the loaded clip is retained so it can rewind and replay |
finalized | bool | a retained clip's end is fixed, ready to replay and loop |
ride | bool | the clip's motion waits for a real move under movement riding |
triggers | List[ClipTrigger] | the bound trigger set (up to 8) |
| Field / method | Type | Meaning |
|---|---|---|
state | ClipState | the lifecycle state |
free / total | int | ring bytes free (pace top-ups off this) / retained clip size in bytes (streaming: buffered-but-undrained) |
played | int | bytes played from the clip start (retained progress; ~0 while streaming) |
ticks | int | content frames emitted since the last start (gap runs excluded) |
underruns / overruns / seq_gaps | int | empty-ring / ring-full / dropped-append counts |
held | List[Usage] | the held-usage snapshot: the buttons, keys, and media the clip is holding down (one shape, like a UsageSnapshot) |
is_held(usage) | bool | test one Usage in held |
Stream enums
CatchClass · TrafficClass · Axis · CatchFilter · Capture · CatchEventKind · ClockDomain · BusEventKind · LogLevelSee Catch and Logs & counters; consuming events is on Streams.
The address class a CatchFilter names. It is the same address vocabulary lock uses, with members 0 to 3 being the lock classes unchanged, extended with the byte-oriented traffic the box carries. id is class-specific.
| Member | Value | id means | As a blanket |
|---|---|---|---|
BUTTON | 0 | a Button slot | every button |
KEY | 1 | a HID keyboard usage | every key and modifier |
MEDIA | 2 | a 16-bit Consumer usage | every media usage |
AXIS | 3 | an Axis | every axis |
HID_IN | 4 | an interface number | every HID interface |
HID_OUT | 5 | an endpoint address | every interrupt-OUT endpoint |
VENDOR_INTERRUPT | 6 | an endpoint address | every vendor interrupt endpoint |
VENDOR_BULK | 7 | an endpoint address | every vendor bulk endpoint |
CONTROL | 8 | an endpoint number (0 = EP0) | every control endpoint |
EMIT | 9 | an endpoint address | every emitting endpoint |
BUS | 10 | unused | the bus lifecycle |
There is no every-class member. The wildcard is CatchFilter.everything(), whose catch_class reads None. cls.is_input() is true for 0 to 3, cls.is_traffic() for the rest.
The input classes are tapped before lock suppression and injection, so an input you have locked still reports here. EMIT is the opposite end, what the clone put on the wire afterwards.
The byte-oriented half of the address space, values 4 to 10 under the same names as CatchClass.
| Members | Values |
|---|---|
HID_IN HID_OUT | 4, 5 |
VENDOR_INTERRUPT VENDOR_BULK | 6, 7 |
CONTROL EMIT BUS | 8, 9, 10 |
It is what CatchFilter.traffic and traffic_class take, so an input class cannot reach a traffic constructor at all.
| Member | Value |
|---|---|
X | 0 |
Y | 1 |
WHEEL | 2 |
One relative axis, for CatchFilter.watch_axis(axis). The values are the wire axis ids a catch or lock entry carries.
One subscription entry: a class, an id inside it, a direction, and how many bytes to keep per event. Pass one or an iterable to dev.catch_events() or dev.input_events(). The instance methods return a new filter rather than mutating in place.
CatchFilter.watch(usage) -> CatchFilter # a Usage, or a Button/Key/MediaKey CatchFilter.watch_axis(axis) -> CatchFilter # one Axis CatchFilter.watch_class(input_class) -> CatchFilter # every usage in one Class CatchFilter.watch_axes() -> CatchFilter # X, Y and the wheel CatchFilter.all_input() -> List[CatchFilter] # all four input classes CatchFilter.traffic(traffic_class, id) -> CatchFilter # one endpoint, interface, or EP number CatchFilter.traffic_class(tc) -> CatchFilter # every id in one TrafficClass CatchFilter.everything() -> CatchFilter # every class, every id, one entry .with_direction(direction) -> CatchFilter # a Direction, default BOTH .with_capture(n) -> CatchFilter # bytes kept per event, default 0 = all .on_press() / .on_release() -> CatchFilter # one edge of an input filter .inbound() / .outbound() -> CatchFilter # one flow of a traffic filter .same_address(other) -> bool # same table entry, whatever the capture CatchFilter.traffic(TrafficClass.VENDOR_INTERRUPT, 0x83).with_capture(16)
| Property | Type | Meaning |
|---|---|---|
catch_class | Optional[CatchClass] | the address class, or None for the every-class wildcard |
id | Optional[int] | the class-specific id, or None for the every-id wildcard. An id of 0 is a real address, not a wildcard. |
direction | Direction | for an input class, the press/release edge, exactly as for a lock; for a traffic class, the transfer flow, where POSITIVE is IN (device to PC) and NEGATIVE is OUT (PC to device). |
capture | int | bytes captured per event; 0 = the whole packet |
Matching is most-specific-first: an exact (class, id) is matched before a class blanket, that before everything(), and a named direction before BOTH. The winning entry supplies the capture.
same_address is true across two filters that differ only in capture, and false once one is narrowed to a direction.
The arguments are checked here, before they reach ctypes. with_direction, watch_axis, watch_class, traffic, and traffic_class want a member of their enum, and with_capture a byte; anything else is a ValueError naming the argument.
The box's table holds 32 entries. catch_events() raises CatchTableFullError when the union of every subscription in this process exceeds it. What the box itself refuses (a class this firmware does not know) raises nothing: compare CatchState.entries from dev.query_catch() against what you sent.
Capture.WHOLE # 0, keep the whole packet Capture.first(n) # keep the first n bytes; first(0) is WHOLE
What with_capture takes. Traffic classes only: an input class carries no packet, so naming one with a capture raises CaptureNotApplicableError.
A ceiling request, not a guarantee. The box holds one entry per address and cuts once, so another subscriber naming that address more widely raises yours too.
| Member | Value | CatchEvent.payload type | Fed by |
|---|---|---|---|
MOTION | 0 | MotionEvent | CatchClass.AXIS |
USAGES | 1 | UsageSnapshot | BUTTON / KEY / MEDIA |
TRAFFIC | 2 | TrafficEvent | every class from HID_IN to BUS |
Which of the box's two chips stamped an event's ts_us. The two ESP32-S3s boot independently, so nothing relates their timers.
| Member | Value | Stamped | Covers |
|---|---|---|---|
HOST_CHIP | 0 | in USB interrupt context, when the real device's transfer completed | motion, usages, HID_IN, and IN transfers on VENDOR_INTERRUPT / VENDOR_BULK |
DEVICE_CHIP | 1 | at the tap on the clone side | HID_OUT, every OUT transfer, and CONTROL / EMIT / BUS |
A stamp is only meaningful against another from the same domain. Both clocks are box-local, wrap every ~71.6 minutes, and restart at zero when that chip reboots, so a value below the previous one is a wrap, a reboot, or a domain change.
To put stamps on this machine's clock, feed them to a Timeline; to cross the two domains, apply the offset in ClockEstimate and respect its error bound.
What a CatchClass.BUS event describes. These also drive Health bits and Stats counters; catching them adds a timestamped ordering.
| Member | Value | Payload fields |
|---|---|---|
RESET | 0 | - |
SUSPEND | 1 | - |
RESUME | 2 | - |
CONFIGURED | 3 | configuration |
DECONFIGURED | 4 | - |
SET_INTERFACE | 5 | interface, alt |
DEVICE_ATTACHED | 6 | - |
DEVICE_DETACHED | 7 | - |
CLONE_UP | 8 | - |
CLONE_DOWN | 9 | - |
BusEvent is the decoded dataclass TrafficEvent.bus_event() returns: a kind plus configuration, interface and alt, each 0 for the kinds carrying none.
| Member | Value |
|---|---|
ERROR | 0 |
WARN | 1 |
INFO | 2 |
DEBUG | 3 |
VERBOSE | 4 |
Wire enums
MotionKind · MoveTiming · PendingMotion · Class · FrameTypeMostly internal. MotionKind and Class tag the structs the Usage and Motion builders produce; FrameType names a wire frame for MockBox.saw() and RecordedFrame.type. Frame semantics are on Frames and Library frames.
| Member | Value |
|---|---|
CURSOR | 0 |
WHEEL | 1 |
| Member | Value | Meaning |
|---|---|---|
RIDE | 0 | wait for a real cursor move to carry the delta (the default) |
NOW | 1 | emit on the box's own clock, whatever movement riding is set to |
| Member | Value | Meaning |
|---|---|---|
KEEP | 0 | leave motion held for a ride alone (the default) |
FLUSH | 1 | emit it now, ignoring the ride window |
DISCARD | 2 | drop it |
| Member | Value |
|---|---|
BUTTON | 0 |
KEY | 1 |
MEDIA | 2 |
| Member | Value | Member | Value |
|---|---|---|---|
MOVE | 1 | LOCK | 10 |
INJECT | 3 | CATCH | 11 |
RESET | 4 | MOTION_EVENT | 12 |
QUERY | 5 | USAGE_EVENT | 15 |
RESP | 6 | OPTION | 17 |
REBOOT_DL | 7 | CLIP_APPEND | 18 |
LOG | 8 | CLIP_CTRL | 19 |
LED | 9 | CLIP_SET | 20 |
CLIP_TRIGGER | 21 | ||
TRAFFIC_EVENT | 22 |
Parameter builders
Usage · Motion · LockTargetSmall classes that wrap a native struct. Build them with their class methods and pass the result to the matching call. Never construct one field by field.
Usage.button(button) -> Usage # build Usage.key(key) -> Usage Usage.media(media) -> Usage usage.kind -> Class # read one back usage.id -> int
An injection target for dev.inject(input, action), and what a InputEvent and a UsageSnapshot hand back. It compares by value, hashes, and reprs as Usage(kind=BUTTON, id=0).
# Naming a button off the stream: id is the Button value, kind says which class it is.
if ev.usage is not None and ev.usage.kind is Class.BUTTON:
print(Button(ev.usage.id).name)
# Or compare whole usages.
if ev.usage == Usage.button(Button.SIDE1):
print("side button")Motion.cursor(dx, dy) -> Motion Motion.wheel(delta) -> Motion
A relative axis drive for dev.move_axis(motion, timing, pending). See Move.
LockTarget.x() -> LockTarget LockTarget.y() -> LockTarget LockTarget.wheel() -> LockTarget LockTarget.usage(usage) -> LockTarget LockTarget.button(button) -> LockTarget LockTarget.key(key) -> LockTarget LockTarget.media(media) -> LockTarget
An axis or usage to lock for dev.lock(target, direction); the button/key/media shortcuts wrap usage(). See Lock.
Device enums
DeviceKindThe cloned device's kind, on DeviceInfo.kind, and what Device.find_mouse_box() / find_keyboard_box() select on. See DeviceKind.
| Member | Value |
|---|---|
UNKNOWN | 0 |
KEYBOARD | 1 |
MOUSE | 2 |
Identity & capability types
Version · Health · DeviceInfo · CapsDataclasses returned by the queries. Canonical field docs: Library structs.
| Field / property | Type | Meaning |
|---|---|---|
proto_ver | int | control-protocol version |
fw_major | int | firmware major |
fw_minor | int | firmware minor |
fw_patch | int | firmware patch |
mac | bytes | the device chip's base MAC (6 bytes), a stable per-box id |
mac_hex | str | the MAC as 12 lowercase hex digits |
name | str | the box's human-readable name (a synthesized default when unset), set with set_name |
| Field | Type |
|---|---|
link_up | bool |
mouse_attached | bool |
clone_configured | bool |
injection_active | bool |
rate_confident | bool |
lock_on | bool |
catch_on | bool |
kbd_attached | bool |
| Field | Type | Meaning |
|---|---|---|
vid | int | USB vendor id |
pid | int | USB product id |
bcd_device | int | device release (BCD) |
bcd_usb | int | USB spec (BCD) |
has_serial | bool | exposes a serial string |
has_bos | bool | exposes a BOS descriptor |
kind | DeviceKind | the device's primary kind (Boot-interface protocol) |
product | str | the product string (empty when none) |
| Field / method | Type | Meaning |
|---|---|---|
mouse | MouseCaps | mouse capabilities |
keyboard | KbdCaps | keyboard capabilities |
mouse_change_driven | bool | mouse reports only on change |
kbd_change_driven | bool | keyboard reports only on change |
has_mouse() | bool | a mouse interface is present |
has_keyboard() | bool | a keyboard interface is present |
is_composite() | bool | the clone has more than one HID interface (n_hid > 1) |
| Field | Type | Meaning |
|---|---|---|
n_buttons | int | button count |
has_x | bool | X axis present |
has_y | bool | Y axis present |
has_wheel | bool | wheel present |
has_report_id | bool | reports carry a report id |
n_hid | int | HID interface count |
| Field | Type | Meaning |
|---|---|---|
n_keys | int | rollover key count |
nkro | bool | n-key rollover |
has_consumer | bool | Consumer (media) page |
has_system | bool | System-control page |
has_report_id | bool | reports carry a report id |
State & telemetry types
Rate · Stats · Locks · CatchState · CatchEntry · ClockEstimate · ImperfectStatus · Counters · PortInfoMore query results, plus PortInfo from find_ports(). Canonical field docs: Library structs.
| Field | Type | Meaning |
|---|---|---|
major, minor, patch | int | the version this chip is running |
slot | int | which app slot it booted, 0 or 1 |
state | ImageState | confirmed, on probation, or rolled back |
| Field | Type | Meaning |
|---|---|---|
device | ChipFirmware | the PC-facing chip |
host | ChipFirmware | None | None when the host chip has not answered over the link |
slot_size | int | usable bytes in a spare slot, the same on both chips |
device_staged | bool | an image is written and waiting to be activated |
host_staged | bool | the same, for the host chip |
| Field / method | Type | Meaning |
|---|---|---|
native_period_us | int | mouse report period, µs |
poll_period_us | int | poll period, µs |
confident | bool | estimate is settled |
change_driven | bool | reports only on change |
native_hz() | float | None | rate in Hz, or None if unknown |
| Field | Type | Meaning |
|---|---|---|
inject_emits | int | injected reports emitted |
tx_drops | int | dropped TX frames |
tx_merges | int | coalesced TX frames |
tx_maxdepth | int | peak TX queue depth |
tx_wedges | int | TX stalls |
wakeups | int | scheduler wakeups |
reset_count | int | resets seen |
config_count | int | clone configures |
| Field / method | Type | Meaning |
|---|---|---|
entries | List[LockEntry] | one LockEntry per weighed direction |
scale_of(target, direction) | int | percent of the physical value kept there, 100 when nothing weighs it; BOTH reports the lowest across every direction, which is not the figure a delta meets (it picks up one from each pair, multiplied) |
is_locked(target, direction) | bool | whether it is blocked outright; a direction merely weighed is not locked. Also true when a whole-class blanket covers it. BOTH asks about the two fixed signs only, so name WITH or AGAINST to ask about one of those |
| Readback case | What entries holds |
|---|---|
| a blanket key lock | one entry per blocked edge, never BOTH |
| a media lock, blanket or specific | BOTH, always |
a relative direction under BearingMode.VECTOR | the effective scale, the lower of X's and Y's, on both axes |
| 96 entries reached | the rest is absent, with nothing marking it; see the native LOCKS budget |
| Module constant | Value | Meaning |
|---|---|---|
LOCK_SCALE_BLOCK | 0 | keep none of the physical value |
LOCK_SCALE_PASS | 100 | keep all of it, untouched |
LOCK_SCALE_MAX | 255 | 2.55x, the ceiling |
| Field | Type | Meaning |
|---|---|---|
target | LockTarget | what is weighed (an axis or a usage) |
is_blanket | bool | a whole-class entry, where target names only the class |
direction | Direction | which direction of the target this entry weighs |
scale | int | percent of the physical value kept; a usage carries one bit, so the box stores the block or pass it renders and this never reads between them |
is_block | bool | scale == 0: blocked outright rather than weighed |
What Direction.WITH and Direction.AGAINST are measured against; see the native bearing.
| Field / property | Type | Meaning |
|---|---|---|
window_ms | Optional[int] | how long an axis holds the direction of its last injected delta; None is off, leaving both relative directions inert |
mode | BearingMode | how the bearing is read |
is_live | bool | whether a bearing is held at all |
Module constant BEARING_WINDOW_DEFAULT_MS (20) is the factory window. A box that has been set boots at its own value.
The live subscription table read back from the box. Since catch_events() gets no reply, this is the only way to see which filters the box holds.
| Field | Type | Meaning |
|---|---|---|
table_full | bool | an entry was refused because the 32-entry table was full |
dropped | int | box-wide events that could not be queued |
clock | ClockEstimate | the measured relationship between the two chips' clocks |
entries | List[CatchEntry] | one entry per live subscription, up to 32 |
One row of the box's table: the CatchFilter you sent, with a drop count attached.
| Field | Type | Meaning |
|---|---|---|
filter | CatchFilter | the entry as the box stored it; a class blanket stays one entry, never expanded per id |
dropped | int | events this entry could not queue |
| Field / member | Type | Meaning |
|---|---|---|
offset_us | int | the host chip's clock minus the device chip's, in µs (signed) |
rate_ppb | Optional[int] | relative drift between the two chips, parts per billion (signed), or None when the box fitted no rate |
delay_us | int | the best measured round trip in the window; the offset is good to about half of it |
age_ms | Optional[int] | age of the estimate, or None when there is no estimate yet |
error_bound_us | int | half delay_us: the bound on how wrong offset_us can be |
to_host_domain(device_us) | Optional[int] | a device-chip stamp on the host chip's timeline, or None when there is no estimate to apply |
Two independent crystals make an offset stale at up to 20 µs per second, so extrapolate with rate_ppb rather than trusting it.
Applying the offset is optional: each event's clock stays authoritative.
| Field | Type | Meaning |
|---|---|---|
allowed | bool | imperfect clones opted in |
over_capacity | bool | mouse exceeds clone capacity |
clone_imperfect | bool | the live clone is imperfect |
See Options.
| Field | Type | Meaning |
|---|---|---|
mode | EmitPace | the selected mode |
resolved_hz | int | the ceiling in effect; 0 = learned/adaptive or no device yet |
force_hz | int | None | the forced wire rate requested; None leaves the device's own |
advertised_hz | int | what the clone's input endpoints advertise now, forced or native; 0 = no clone |
force_active | bool | whether a forced interval is written into the descriptor being served |
See Options.
| Field | Type | Meaning |
|---|---|---|
frames_tx | int | host-side frames sent |
frames_rx | int | host-side frames received |
crc_drops | int | frames dropped on CRC |
reconnects | int | link reconnects |
| Field | Type | Meaning |
|---|---|---|
path | str | serial path, e.g. /dev/ttyACM0 or COM3 |
vid | int | USB vendor id |
pid | int | USB product id |
serial | Optional[str] | the CH343 adapter's serial, when it serves one |
Pass path to Device.open(path). Canonical: PortInfo.
| Field / property | Type | Meaning |
|---|---|---|
port | PortInfo | the box's control port |
version | Version | its firmware version, with the box MAC and name |
device | DeviceInfo | the device it clones |
id | str | the box identity (the MAC hex) |
serial | Optional[str] | the CH343 serial |
Pass id or serial to Device.open_by_id(id). Canonical: BoxInfo.
Event & log types
Yielded by the streamsPayloads from streams. dev.catch_events() yields CatchEvent, dev.input_events() yields InputEvent, and dev.logs() yields LogLine. What catch reports lives on Catch.
| Field / member | Type | Meaning |
|---|---|---|
kind | CatchEventKind | which payload is set |
payload | MotionEvent | UsageSnapshot | TrafficEvent | the decoded event |
ts_us | int | When the event happened, in box microseconds: the report's arrival for input, the tap firing for traffic. Box-local and wrapping every ~71.6 minutes, so compare stamps only against each other. See Catch timestamps. |
clock | ClockDomain | which chip stamped ts_us. Two stamps are directly comparable only when this matches; across domains, apply CatchState.clock. |
motion | MotionEvent | None | payload when kind == MOTION |
usages | UsageSnapshot | None | payload when kind == USAGES |
traffic | TrafficEvent | None | payload when kind == TRAFFIC |
| Field | Type | Meaning |
|---|---|---|
dx | int | X delta |
dy | int | Y delta |
dz | int | wheel delta |
The stamp and its domain stay on the CatchEvent around it.
| Field / method | Type | Meaning |
|---|---|---|
usages | List[Usage] | every held Usage (button, key, or media; modifiers are key usages 0xE0 to 0xE7) |
cls | Class | the one class this snapshot covers, from the frame header |
direction | Direction | the edge that produced it |
is_held(usage) | bool | test a Usage in the snapshot |
Only held usages that resolve against your filters appear, and no event is emitted when none do, so a subscription to one button stays sparse even while the mouse reports at 1 kHz.
An empty snapshot still names its class: cls and direction come from the frame header, not the entries.
The payload for every byte-oriented CatchClass from HID_IN to BUS: one packet, one control transaction, or one bus event, with whatever the entry's capture let through.
| Field / method | Type | Meaning |
|---|---|---|
catch_class | CatchClass | which class produced it |
id | int | endpoint address, interface number, or endpoint number, per the class |
direction | Direction | IN (device to PC) or OUT (PC to device) |
flags | int | class-specific, see the table below |
true_len | int | the packet's length before capture truncation |
bytes | bytes | the captured bytes, at most 180 of them |
truncated() | bool | len(bytes) < true_len: bytes were cut |
setup() | Optional[bytes] | the 8-byte setup packet of a CONTROL event; None for another class or a shorter capture |
data() | bytes | the data stage of a CONTROL event, the whole packet for any other class |
control_status() | Optional[ControlStatus] | what the real device answered; None for any class but CONTROL |
bus_event() | Optional[BusEvent] | the decoded lifecycle event; None for any class but BUS or an unknown kind |
bulk_end_of_transfer() / bulk_zlp() | bool | the two VENDOR_BULK framing bits, read off flags |
| Class | flags | Read it with |
|---|---|---|
VENDOR_BULK | b0 end-of-transfer, b1 zero-length packet | bulk_end_of_transfer(), bulk_zlp() |
CONTROL | the real device's answer: 0 OK, 0xFD it STALLed, 0xFE it NAKed to timeout | control_status() |
BUS | the BusEventKind; the bytes hold its arguments | bus_event() |
| everything else | 0 | - |
A CONTROL event is one completed transaction, not one stage: bytes is [setup 8][data…] and direction says which way the data stage went. Requests the box serves from its own descriptor cache still produce an event.
| Member | Value | Meaning |
|---|---|---|
OK | 0 | the real device answered |
STALLED | 1 | it STALLed the request |
NAKED | 2 | it NAKed to timeout |
OTHER | 3 | a status byte this build does not know; the raw byte stays on TrafficEvent.flags |
One decoded input: a press edge, a release edge, or a motion report. Yielded by dev.input_events(), which diffs the box's held-usage snapshots so you do not have to.
| Field / property | Type | Meaning |
|---|---|---|
kind | InputKind | which arm is populated |
usage | Optional[Usage] | the usage this is an edge on; None for MOTION |
dx / dy / dz | int | right / down / wheel-up deltas this report; 0 unless kind is MOTION |
ts_us | int | the box stamp, as on a CatchEvent |
clock | ClockDomain | which chip stamped it |
is_press / is_release | bool | shorthand for the kind test |
| Member | Value | Populates |
|---|---|---|
PRESS | 0 | usage, a momentary usage going down |
RELEASE | 1 | usage, the same coming up |
MOTION | 2 | dx, dy, dz |
One event placed on this machine's clock by a Timeline.
| Field | Type | Meaning |
|---|---|---|
host_ns | int | when the event happened, on the same monotonic scale passed as now_ns |
box_us | int | the event's own stamp, unwrapped past the 32-bit rollover |
excess_ns | int | how much later than the measured floor this event arrived. Jitter, not latency. |
| Field | Type | Meaning |
|---|---|---|
level | LogLevel | severity |
text | str | the log message |
Errors
MediusError, its subclasses, and the Status codesEvery Blocks call (and any that fails on the wire) raises a MediusError or one of its subclasses. Catch the base class to catch them all. Canonical mapping: Library errors.
| Attribute | Type | Meaning |
|---|---|---|
status | Status | the failure code |
message | str | the box's last error text |
proto_ver | int | offending version byte (bad-proto-version only) |
from medius import Device, MediusError, NotFoundError
try:
dev = Device.find()
except NotFoundError:
... # no box plugged in
except MediusError as e: # any other failure
print(e.status, e.message)| Exception | Raised on |
|---|---|
IoError | ERR_IO |
NotFoundError | ERR_NOT_FOUND |
NoReplyError | ERR_NO_REPLY |
BadProtoVerError | ERR_BAD_PROTO_VER |
QueryTimeoutError | ERR_QUERY_TIMEOUT |
DisconnectedError | ERR_DISCONNECTED |
FrameTooLongError | ERR_FRAME_TOO_LONG |
UpdateError | ERR_UPDATE |
InvalidArgError | ERR_INVALID_ARG |
PanicError | ERR_PANIC |
CatchTableFullError | ERR_CATCH_TABLE_FULL |
EmptySubscriptionError | ERR_EMPTY_SUBSCRIPTION |
CaptureNotApplicableError | ERR_CAPTURE_NOT_APPLICABLE |
NotAnInputFilterError | ERR_NOT_AN_INPUT_FILTER |
WildcardNotInputError | ERR_WILDCARD_NOT_INPUT |
HalfEdgeInputFilterError | ERR_HALF_EDGE_INPUT_FILTER |
ReservedIdError | ERR_RESERVED_ID |
RelativeDirectionError | ERR_RELATIVE_DIRECTION |
The last eight are argument refusals, raised before a frame reaches the box.
| Refusal | Raised on |
|---|---|
CatchTableFullError | the union of every subscription in this process needs more than the box's 32 entries |
EmptySubscriptionError | a subscription with no filters, which would never yield an event |
CaptureNotApplicableError | a Capture on an input class, which carries no packet |
NotAnInputFilterError | a traffic class passed to input_events, which cannot decode one |
WildcardNotInputError | CatchFilter.everything() passed to input_events; it covers traffic too |
HalfEdgeInputFilterError | an input filter narrowed to one edge, which cannot be decoded into press and release |
ReservedIdError | an exact id equal to the blanket sentinel, which would address the whole class instead |
RelativeDirectionError | Direction.WITH or AGAINST where only a fixed sign or edge can be addressed; they resolve against the bearing at emit time, after the call is made |
DisconnectedError ends a stream iteration cleanly rather than propagating. OK and ERR_UNKNOWN have no dedicated subclass; ERR_UNKNOWN raises the base MediusError.
| Member | Value | Member | Value |
|---|---|---|---|
OK | 0 | ERR_DISCONNECTED | 6 |
ERR_IO | 1 | ERR_FRAME_TOO_LONG | 7 |
ERR_NOT_FOUND | 2 | ERR_UPDATE | 8 |
ERR_NO_REPLY | 3 | ERR_INVALID_ARG | 9 |
ERR_BAD_PROTO_VER | 4 | ERR_PANIC | 10 |
ERR_QUERY_TIMEOUT | 5 | ERR_UNKNOWN | 11 |
ERR_CATCH_TABLE_FULL | 12 | ERR_WILDCARD_NOT_INPUT | 16 |
ERR_EMPTY_SUBSCRIPTION | 13 | ERR_HALF_EDGE_INPUT_FILTER | 17 |
ERR_CAPTURE_NOT_APPLICABLE | 14 | ERR_RESERVED_ID | 18 |
ERR_NOT_AN_INPUT_FILTER | 15 | ERR_RELATIVE_DIRECTION | 19 |