Injection enumsButton · Action

See the injection model for what each Action means; button slots on Usage IDs.

Button
MemberValue
LEFT0
RIGHT1
MIDDLE2
SIDE13
SIDE24
Action
MemberValueMeaning
SOFT_RELEASE0release unless held on the real device
PRESS1hold down
FORCE_RELEASE2release even against a hold on the real device

Lock & blanket enumsDirection · BearingMode · LockTargetKind · Blanket

See Lock for directions and blanket classes, and Catch for a direction on a traffic subscription.

Direction

Its meaning depends on what it is attached to: an axis, a usage, or a CatchFilter naming a byte-oriented catch class.

MemberValueAliasesAxis or wheelButton or keyTraffic-class filter
BOTH0-both signs; a scale sets the two fixed signs and passes WITH / AGAINST in fullpress and releaseboth directions
POSITIVE1PRESS · IN+x / +y / wheel-up onlythe press edgeIN, device to PC
NEGATIVE2RELEASE · OUT-x / -y / wheel-down onlythe release edgeOUT, PC to device
WITH3-the sign the box is injectingrefusedno meaning
AGAINST4-the sign opposing itrefusedno meaning

Aliases are the same values under call-site names: Direction.PRESS is Direction.POSITIVE. WITH and AGAINST are measured against the bearing, not a fixed sign; .is_relative tells them apart.

Only an axis has a bearing, so WITH or AGAINST on a lock of anything else raises RelativeDirectionError. A media usage has no edges: an edge named on one is sent as BOTH, and reads back that way in Locks.

BearingMode

How the box reads the direction it is injecting, which WITH and AGAINST are measured against. Set with dev.set_bearing(window_ms, mode).

MemberValueMeaning
PER_AXIS0each axis compares its sign against its bearing, independently; the default
VECTOR1the native delta is projected onto the injected direction. The relative scale weighs only the part along it, and one relative scale (the lower of X's and Y's) governs X and Y as one vector. The fixed-sign scales still apply to what is left on each axis

Under VECTOR, Locks reports that one scale.

LockTargetKind
MemberValue
X0
Y1
WHEEL2
PAN3
USAGE4

Built by LockTarget.x/y/wheel/pan/usage (and the button/key/media shortcuts).

Blanket
MemberValueClassDirection
AIM0the X and Y cursor axesa sign, on each axis
WHEEL1the wheela sign
BUTTONS2every mouse buttonan edge, on each button
KEYS3every keyboard key and modifieran edge: POSITIVE blocks presses, NEGATIVE releases, BOTH both
MEDIA4every media usagenothing; media has no edges

These are ABI-local ordinals (matching the crate's Blanket order), not the clip auto-lock scope bits.

Keycode enumsKey · MediaKey

Named subsets of the HID usage tables; full lists on Usage IDs (keys, media). Any call taking a Key or MediaKey also accepts a raw int usage.

Key
MembersValues
A .. Z4 to 29
N1 .. N9, N030 to 39
ENTER ESCAPE BACKSPACE TAB SPACE40 to 44
CAPS_LOCK57
F1 .. F1258 to 69
INSERT HOME PAGE_UP DELETE END PAGE_DOWN73 to 78
RIGHT LEFT DOWN UP (arrows)79 to 82
LEFT_CTRL LEFT_SHIFT LEFT_ALT LEFT_GUI224 to 227
RIGHT_CTRL RIGHT_SHIFT RIGHT_ALT RIGHT_GUI228 to 231
MediaKey
MemberValue
PLAY176
PAUSE177
NEXT_TRACK181
PREV_TRACK182
STOP183
PLAY_PAUSE205
MUTE226
VOLUME_UP233
VOLUME_DOWN234

LED & admin enumsLedTarget · LedMode · RebootTarget

See LED and Admin.

LedTarget
MemberValue
DEVICE0
HOST1
BOTH2
LedMode
MemberValue
AUTO0
OFF1
SOLID2
BLINK3
RebootTarget
MemberValue
DEVICE_DOWNLOAD0
HOST_DOWNLOAD1
DEVICE_RUN2
HOST_RUN3

Emit paceEmitMode · EmitPace

Passed to dev.set_emit_pace(). See Options.

EmitMode
MemberValue
LEARNED0
INTERVAL1
FIXED2
EmitPace

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).

RenderRenderMode

Passed to dev.set_render(). See Options.

RenderMode
MemberValue
OFF0
STOCK1
DESPIKED2
UNSMOOTHED3

ClipClipState · Edge · ClipAction · ClipTrigger · ClipPacketTrigger · ClipSettings · ClipStatus

Buffered-clip types. See Clip.

ClipState
MemberValueMeaning
IDLE0No clip playing.
PLAYING1Draining the ring, one entry per native frame.
PAUSED2Halted mid-clip; the cursor and any held input are retained.
FAULTED3An append was dropped or the ring overflowed; clear to recover.
UpdateTarget
MemberValueMeaning
DEVICE0The PC-facing chip, written directly over the control port.
HOST1The chip that reads the real device, relayed over the inter-chip link.
ImageState
MemberValueMeaning
NEW0Selected but not yet booted.
PENDING_VERIFY1Booted and on probation (the rollback window).
VALID2Confirmed by the image itself.
INVALID3The image asked to be rolled back.
ABORTED4Booted once and never confirmed.
UNKNOWN0xFFNo entry for this slot.
Edge
MemberValueFires on
BOTH0either edge of the trigger usage
PRESS1the native press edge
RELEASE2the native release edge

Which edge of a ClipTrigger runs its action.

ClipAction
MemberValueRuns
START0rewind and play
STOP1stop and release
PAUSE2halt mid-clip
RESUME3continue from the pause
RESTART4force a rewind and play
TOGGLE5play if idle/paused, stop if playing

The action a bound trigger of either kind runs on the box, matching the clip.start/stop/pause/resume/restart/toggle methods.

ClipTrigger

A dataclass binding a native usage's edge to a clip action, passed to clip.bind(). The box runs the action with no host round-trip.

FieldTypeMeaning
onUsagethe trigger usage (button, key, or media)
edgeEdgewhich edge fires the action
actionClipActionwhat the box runs
consumeboolsuppress the native edge so it never reaches the game PC (default False)

Construct it directly, e.g. ClipTrigger(Usage.button(Button.SIDE1), Edge.PRESS, ClipAction.TOGGLE, consume=True).

ClipPacketTrigger

A dataclass binding a matched packet to a clip action, keyed by (traffic_class, id, direction, match_bytes, mask). Passed to clip.bind_packet() and read back by clip.query_config(). See Clip.

FieldTypeMeaning
traffic_classTrafficClassthe surface the packet crosses: any member but BUS and CLIP_TRANSFER
idintthe interface number for HID_IN, the endpoint number for the rest, or ClipPacketTrigger.ANY_ID (0xFFFF)
directionDirectionIN, OUT or BOTH
actionClipActionwhat the box runs, on the frame clock's next tick
match_bytes, maskbytesthe head compare, both one length and ≤PKT_MATCH_MAX. A packet matches when head[i] & mask[i] == match_bytes[i] for each byte. The default b"" matches every packet on the address
consumebooldrop every packet this trigger matches as the top-ranked trigger, before the rewrite table sees it. The box holds a consuming trigger only under allow_imperfect_clones, and on any class but CONTROL (default False)
once_per_runboolrun the action on the first packet of a run of matching packets; needs a class other than CONTROL, a concrete id, and IN or OUT (default False)
selector_lenintwith once_per_run: how many leading match bytes pick the run's stream (a report ID, for example); the remaining bytes are the condition (default 0)
hitsintpackets matched as the top-ranked trigger since the trigger was bound or overwritten (saturating). Read back by query_config(). bind_packet() does not send it, but raises ValueError outside 0..65535
EXAMPLE
from medius import ClipAction, ClipPacketTrigger, ClipPacketTriggerError, Direction, TrafficClass

clip = dev.clip()

# Report ID 7 on interface 2 carries a button in bit 5 of its second byte. Start once per hold.
held = ClipPacketTrigger(TrafficClass.HID_IN, 2, Direction.IN, ClipAction.START,
                         match_bytes=b"\x07\x20", mask=b"\xFF\x20",
                         once_per_run=True, selector_len=1)
clip.bind_packet(held)

try:
    clip.bind_packet(ClipPacketTrigger(TrafficClass.CONTROL, 0, Direction.OUT,
                                       ClipAction.STOP, consume=True))
except ClipPacketTriggerError as e:
    print(e)                      # consume on CONTROL; nothing was sent

for t in clip.query_config().packet_triggers:
    print(t.traffic_class, t.id, t.action, t.hits)

clip.unbind_packet(held)          # by key
clip.clear_triggers()             # both kinds
ClipSettings (clip.query_config())
FieldTypeMeaning
autolockList[Blanket]the input groups auto-locked while the clip plays
loopboolplayback loops at the clip end (retained mode only)
retainboolthe loaded clip is retained so it can rewind and replay
finalizedboola retained clip's end is fixed, ready to replay and loop
rideboolthe clip's motion waits for a native move under movement riding
triggersList[ClipTrigger]the bound input triggers (up to 8)
packet_triggersList[ClipPacketTrigger]the bound packet triggers (up to 8), in the order the box holds them, each with its hits
ClipStatus (clip.query_status())
Field / methodTypeMeaning
stateClipStatethe lifecycle state
free / totalintring bytes free (pace top-ups off this) / retained clip size in bytes (streaming: bytes buffered but not yet played)
playedintbytes played from the clip start (retained progress; ~0 while streaming)
ticksintcontent frames played since the box booted (gap runs excluded)
underruns / overruns / seq_gapsintempty-ring / ring-full / dropped-append counts
xfersintclip transfers the real device completed
xfer_errsintclip transfers that ended any other way: a refusal, no reply, no room in the box's queue, or dropped behind a transfer the real device did not reply to
gatedintraw reports and transfers the box discarded because the imperfect-clone opt-in was off
heldList[Usage]the held-usage snapshot: the buttons, keys, and media the clip is holding down (one shape, like a UsageSnapshot)
is_held(usage)booltest one Usage in held
CLIP CONSTANTS
Module constantValueMeaning
CLIP_EDGES_MAX8the most edges one clip frame carries
CLIP_RAW_MAX8the most raw reports one clip frame carries
CLIP_ENTRY_MAX512the most bytes one clip frame encodes to: one CLIP_APPEND payload
CLIP_PKT_TRIG_MAX8the most packet triggers the box holds, beside its input triggers
CLIP_PKT_MATCH_POOL112the match bytes the box holds across every packet trigger
PKT_MATCH_MAX16the most match bytes one packet trigger compares

Stream enumsCatchClass · TrafficClass · Axis · CatchFilter · Capture · CatchEventKind · ClockDomain · BusEventKind · LogLevel

See Catch and Logs & counters; consuming events is on Streams.

CatchClass

The address class a CatchFilter names. Members 0 to 3 are the lock classes, unchanged; the rest are the byte-oriented traffic the box carries. id is class-specific.

MemberValueid meansAs a blanket
BUTTON0a Button slotevery button
KEY1a HID keyboard usageevery key and modifier
MEDIA2a 16-bit Consumer usageevery media usage
AXIS3an Axisevery axis
HID_IN4an interface numberevery HID interface
HID_OUT5an endpoint numberevery interrupt-OUT endpoint
VENDOR_INTERRUPT6an endpoint numberevery vendor interrupt endpoint
VENDOR_BULK7an endpoint numberevery vendor bulk endpoint
CONTROL8an endpoint number (0 = EP0; on EP0, the requests the clone relays to the real device)every control endpoint
EMIT9an endpoint numberevery emitting endpoint
BUS10unusedthe bus lifecycle
CLIP_TRANSFER11the endpoint number (0 = EP0) a clip's transfer ran onevery control endpoint

The every-class wildcard is CatchFilter.everything(), whose catch_class reads None. cls.is_input() is true for 0 to 3, cls.is_traffic() for the rest.

Where each class is tapped, and why a locked input still reports, is on Catch.

TrafficClass

The byte-oriented half of the address space, values 4 to 11 under the same names as CatchClass.

MembersValues
HID_IN HID_OUT4, 5
VENDOR_INTERRUPT VENDOR_BULK6, 7
CONTROL EMIT BUS8, 9, 10
CLIP_TRANSFER11

CatchFilter.traffic and traffic_class take a TrafficClass, so an input class can't be passed to a traffic constructor.

Axis
MemberValue
X0
Y1
WHEEL2
PAN3

One relative axis, for CatchFilter.watch_axis(axis). The values are the wire axis ids a catch or lock entry carries.

CatchFilter

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(). Instance methods return a new filter.

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, the wheel and AC Pan
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)
PropertyTypeMeaning
catch_classOptional[CatchClass]the address class, or None for the every-class wildcard
idOptional[int]the class-specific id, or None for the every-id wildcard. An id of 0 is a real address, not a wildcard.
directionDirectionfor an input class, the press/release edge, 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).
captureintbytes captured per event; 0 = the whole packet

The top-ranked entry supplies the capture; the ranking is on The table.

same_address is true across two filters that differ only in capture, and false once one is narrowed to a direction.

Arguments are checked before ctypes: with_direction, watch_axis, watch_class, traffic, and traffic_class take a member of their enum, with_capture a byte; anything else raises a ValueError naming the argument.

The box's table holds 32 entries; catch_events() raises CatchTableFullError when this process's subscriptions together exceed it. A box-side refusal (a class this firmware does not know) raises nothing: compare CatchState.entries from dev.query_catch() with what you sent.

Capture
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 capture is a ceiling you request, not a guarantee. The box holds one entry per address and cuts once, so another subscriber asking for more of that address raises your capture too.

CatchEventKind
MemberValueCatchEvent.payload typeFed by
MOTION0MotionEventCatchClass.AXIS
USAGES1UsageSnapshotBUTTON / KEY / MEDIA
TRAFFIC2TrafficEventevery class from HID_IN to CLIP_TRANSFER
ClockDomain

Which of the box's two chips stamped an event's ts_us. The two ESP32-S3s boot independently, so nothing relates their timers.

MemberValueStampedCovers
HOST_CHIP0in USB interrupt context, when the real device's transfer completedmotion, usages, HID_IN, and native IN transfers on VENDOR_INTERRUPT / VENDOR_BULK
DEVICE_CHIP1at the tap on the clone sideHID_OUT, every OUT transfer, a raw packet on a vendor IN endpoint, and CONTROL / EMIT / BUS / CLIP_TRANSFER

Compare stamps only within one domain. Both clocks are box-local, wrap every ~71.6 minutes, and restart at zero when their chip reboots. A value below the previous one is a wrap, a reboot, or a domain change.

A Timeline puts stamps on this machine's clock; to cross domains, apply the ClockEstimate offset within its error bound.

BusEventKind

What a CatchClass.BUS event describes. These also drive Health bits and Stats counters; catching them adds a timestamped ordering.

MemberValuePayload fields
RESET0-
SUSPEND1-
RESUME2-
CONFIGURED3configuration
DECONFIGURED4-
SET_INTERFACE5interface, alt
DEVICE_ATTACHED6-
DEVICE_DETACHED7-
CLONE_UP8-
CLONE_DOWN9-

BusEvent is the decoded dataclass TrafficEvent.bus_event() returns: a kind plus configuration, interface and alt, each 0 for the kinds carrying none.

LogLevel
MemberValue
ERROR0
WARN1
INFO2
DEBUG3
VERBOSE4

Wire enumsMotionKind · MoveTiming · PendingMotion · Class · FrameType

Mostly 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.

MotionKind
MemberValue
CURSOR0
WHEEL1
PAN2
MoveTiming
MemberValueMeaning
RIDE0follow movement riding: while it is on, wait for a native cursor move to carry the delta (the default)
NOW1emit on the next mouse report, native or not, whatever movement riding is set to
PendingMotion
MemberValueMeaning
KEEP0leave motion held by riding as it is (the default)
FLUSH1emit held motion now, ignoring the ride window
DISCARD2drop held motion
Class
MemberValue
BUTTON0
KEY1
MEDIA2
FrameType
MemberValueMemberValue
MOVE1LOCK10
INJECT3CATCH11
RESET4MOTION_EVENT12
QUERY5USAGE_EVENT15
RESP6OPTION17
REBOOT_DL7CLIP_APPEND18
LOG8CLIP_CTRL19
LED9CLIP_SET20
CLIP_TRIGGER21
TRAFFIC_EVENT22

Parameter buildersUsage · Motion · LockTarget

Classes that wrap a C struct. Build them with their class methods, never field by field, and pass the result to the matching call.

Usage
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 an InputEvent and a UsageSnapshot return. It compares by value, hashes, and reprs as Usage(kind=BUTTON, id=0).

EXAMPLE
# 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
Motion.cursor(dx, dy) -> Motion
Motion.wheel(delta)   -> Motion
Motion.pan(delta)     -> Motion

A relative axis drive for dev.move_axis(motion, timing, pending); pan is AC Pan (horizontal scroll), a full peer of the wheel. See Move.

LockTarget
LockTarget.x()            -> LockTarget
LockTarget.y()            -> LockTarget
LockTarget.wheel()        -> LockTarget
LockTarget.pan()          -> 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 enumsDeviceKind

The cloned device's kind, on DeviceInfo.kind, and what Device.find_mouse_box() / find_keyboard_box() select on. See DeviceKind.

DeviceKind
MemberValue
UNKNOWN0
KEYBOARD1
MOUSE2

Identity & capability typesVersion · Health · DeviceInfo · Caps

Dataclasses returned by the queries. Canonical field docs: Library structs.

Version (query_version())
Field / propertyTypeMeaning
proto_verintcontrol-protocol version
fw_majorintfirmware major
fw_minorintfirmware minor
fw_patchintfirmware patch
macbytesthe device chip's base MAC (6 bytes), a stable per-box id
mac_hexstrthe MAC as 12 lowercase hex digits
namestrthe box's human-readable name (a synthesised default when unset), set with set_name
Health (query_health())
FieldType
link_upbool
mouse_attachedbool
clone_configuredbool
injection_activebool
rate_confidentbool
lock_onbool
catch_onbool
kbd_attachedbool
rewrite_onbool
patch_onbool
transform_onbool
DeviceInfo (device_info())
FieldTypeMeaning
vidintUSB vendor id
pidintUSB product id
bcd_deviceintdevice release (BCD)
bcd_usbintUSB spec (BCD)
has_serialboolexposes a serial string
has_bosboolexposes a BOS descriptor
kindDeviceKindthe real device's primary kind, from its HID report descriptors
productstrthe product string (empty when none)
is_cloned()boola device is cloned, including one with no HID interface (n_hid 0). With nothing cloned every field is zero
Caps (caps())
Field / methodTypeMeaning
mouseMouseCapsmouse capabilities
keyboardKbdCapskeyboard capabilities
mouse_change_drivenboolmouse reports only on change
kbd_change_drivenboolkeyboard reports only on change
has_mouse()boola mouse interface is present
has_keyboard()boola keyboard interface is present
is_composite()boolthe clone has more than one HID interface (n_hid > 1)
MouseCaps
FieldTypeMeaning
n_buttonsintbutton count
has_xboolX axis present
has_yboolY axis present
has_wheelboolwheel present
panboolAC Pan (horizontal scroll) present
has_report_idboolreports carry a report id
n_hidintHID interface count
KbdCaps
FieldTypeMeaning
n_keysintrollover key count
nkrobooln-key rollover
has_consumerboolConsumer (media) page
has_systemboolSystem-control page
has_report_idboolreports carry a report id

State & telemetry typesRate · Stats · Locks · CatchState · CatchEntry · ClockEstimate · ImperfectStatus · Counters · PortInfo

More query results, plus PortInfo from find_ports(). Canonical field docs: Library structs.

ChipFirmware
FieldTypeMeaning
major, minor, patchintthe version this chip is running
slotintwhich app slot it booted, 0 or 1
stateImageStateconfirmed, on probation, or rolled back
FirmwareInfo (firmware_info())
FieldTypeMeaning
deviceChipFirmwarethe PC-facing chip
hostChipFirmware | NoneNone when the host chip has not replied over the link
slot_sizeintusable bytes in a spare slot, the same on both chips
device_stagedboolan image is written and waiting to be activated
host_stagedboolthe same, for the host chip
any_pending()booleither chip has not confirmed the image it booted, so an update is refused
Rate (query_rate())
Field / methodTypeMeaning
native_period_usintmouse report period, µs
poll_period_usintpoll period, µs
confidentboolestimate is settled
change_drivenboolreports only on change
native_hz()float | Nonerate in Hz, or None if unknown
Stats (query_stats())
FieldTypeMeaning
inject_emitsintinjected reports emitted
tx_dropsintreports the clone's IN queue could not hold
tx_mergesintcoalesced TX frames
tx_maxdepthintpeak TX queue depth
tx_wedgesintwedged-endpoint recoveries
wakeupsintremote wakeups the clone signalled to the game PC, for the real device or an injection
reset_countintresets seen
config_countintclone configures
link_rx_dropsintinput frames the device chip could not take off the link from the host chip
host_rx_dropsintthe same count on the host chip, relayed over the link
relay_dropsintrelayed traffic and commands that went no further, never native input. One count each for: a vendor IN packet whose endpoint queue was full of the control PC's RAW packets; a native zero-length packet answering a poll the PC never made (the box runs one poll ahead of a suspended PC); an OUT packet with no room in the queue, that failed three times on the real device's bus, or that a bus reset or SET_INTERFACE overtook; a RAW OUT packet with no room to wait; a relayed request the box could not queue; a flagged MOVE the motion batch could not take; a frame other than native input that either chip's link receive ring dropped; a control-PC frame its receive ring had no room for. A control-port UART overrun counts once, however many frames it cost
sessioninttimes the box released some or all of the session state a host set. 0 at boot; it wraps, so compare for inequality. The library watches it for session recovery
Locks (query_locks())
Field / methodTypeMeaning
entriesList[LockEntry]one LockEntry per weighed direction
scale_of(target, direction)intpercent of the native value kept on that target and direction, 100 when nothing weighs it. BOTH reports the scale of smallest magnitude across every direction (a negative one on a tie): a block outranks a reversal of any size, and a pass outranks a reversal past -100. A delta meets a different figure: its fixed-sign scale times its relative scale
is_locked(target, direction)boolwhether that target/direction 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 caseWhat entries holds
a blanket key lockone entry per blocked edge, never BOTH
a media lock, blanket or specificBOTH, always
a relative direction under BearingMode.VECTORthe effective scale, the lower of X's and Y's, on both axes
85 entries reachedentries past 85 are left out, and nothing marks the cut; see the native LOCKS budget
SCALE CONSTANTS
Module constantValueMeaning
LOCK_SCALE_BLOCK0keep none of the native value
LOCK_SCALE_PASS100keep all of it, untouched
LOCK_SCALE_MAX2552.55×, the ceiling
LOCK_SCALE_MIN-2552.55× reversed, the floor; a negative reverses what it keeps (-100 inverts an axis). Axes only
LockEntry
FieldTypeMeaning
targetLockTargetwhat is weighed (an axis or a usage)
is_blanketboola whole-class entry, where target names only the class
directionDirectionwhich direction of the target this entry weighs
scaleintpercent of the native value kept, signed: a negative one reverses what it keeps. A usage is one bit, so the box stores a block or a pass: never a value between, never negative
is_blockboolscale == 0: blocked outright rather than weighed
Bearing (query_bearing())

What Direction.WITH and Direction.AGAINST are measured against; see the native bearing.

Field / propertyTypeMeaning
window_msOptional[int]how long an axis holds the direction of its last injected delta; None is off, leaving both relative directions inert
modeBearingModehow the bearing is read
is_liveboolwhether 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 stored value.

CatchState (query_catch())

The live subscription table: the only view of which filters the box holds, since catch_events() gets no reply.

FieldTypeMeaning
table_fullboolan entry was refused because the 32-entry table was full
droppedintbox-wide events that could not be queued
clockClockEstimatethe measured relationship between the two chips' clocks
entriesList[CatchEntry]one entry per live subscription, up to 32
CatchEntry

One row of the box's table: the CatchFilter you sent, with its drop count.

FieldTypeMeaning
filterCatchFilterthe entry as the box stored it; a class blanket stays one entry, never expanded per id
droppedintevents this entry could not queue
ClockEstimate (CatchState.clock)
Field / memberTypeMeaning
offset_usintthe host chip's clock minus the device chip's, in µs (signed)
rate_ppbOptional[int]relative drift between the two chips, parts per billion (signed), or None when the box fitted no rate
delay_usintthe best measured round trip in the window; the offset is good to ~half of it
age_msOptional[int]age of the estimate, or None when there is no estimate yet
error_bound_usinthalf 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

The two chips' crystals drift the offset by up to 20 µs/s; extrapolate with rate_ppb.

Applying the offset is optional: each event's clock stays authoritative.

ImperfectStatus (query_imperfect())
FieldTypeMeaning
allowedboolimperfect clones opted in
over_capacityboolreal device exceeds clone capacity
clone_imperfectboolthe live clone is imperfect

See Options.

EmitPaceStatus (query_emit_pace())
FieldTypeMeaning
modeEmitPacethe pace
resolved_hzintthe ceiling in effect; 0 = learned/adaptive or no device yet, 1000 once the renderer has a profile
force_hzint | Nonethe forced wire rate requested; None leaves native
advertised_hzintwhat the clone's input endpoints advertise now, forced or native; 0 = no clone
force_activeboolwhether a forced interval is written into the descriptor being served

See Options.

RenderStatus (query_render())
FieldTypeMeaning
modeRenderModethe texture motion is rendered with; OFF is the paced fill
fullboolwhether native motion is rendered by the model rather than relayed
readyboolwhether a profile has armed for the real device; nothing is rendered until it has

See Options.

SpreadStatus (query_spread())
FieldTypeMeaning
percentintshare of the command interval a delta is released across; 0 is the whole delta on the next report
span_usintthe interval in effect, in microseconds; 0 when nothing is released across an interval

See Options.

Counters (counters())
FieldTypeMeaning
frames_txinthost-side frames sent
frames_rxinthost-side frames received
crc_dropsintframes dropped on CRC
reconnectsintlink reconnects
restartsintdevice-chip restarts the library recovered from; a session release does not count here
PortInfo (find_ports())
FieldTypeMeaning
pathstrserial path, e.g. /dev/ttyACM0 or COM3
vidintUSB vendor id
pidintUSB product id
serialOptional[str]the CH343 adapter's serial, when it serves one

Pass path to Device.open(path). Canonical: PortInfo.

BoxInfo (list_boxes())
Field / propertyTypeMeaning
portPortInfothe box's control port
versionVersionits firmware version and control protocol, with the box MAC and name
deviceOptional[DeviceInfo]the real device it clones; None for a box on another control protocol (opening that box raises BadProtoVerError)
idstrthe box identity (the MAC hex)
serialOptional[str]the CH343 serial

Pass id or serial to Device.open_by_id(id). Canonical: BoxInfo.

import medius

for b in medius.list_boxes():
    if b.device is None:
        print(f"{b.id} speaks protocol {b.version.proto_ver}: update its firmware")
    else:
        print(b.id, b.device.kind.name, b.device.product)

Event & log typesYielded by the streams

Stream payloads: dev.catch_events() yields CatchEvent, dev.input_events() yields InputEvent, and dev.logs() yields LogLine. What catch reports is on Catch.

CatchEvent
Field / memberTypeMeaning
kindCatchEventKindwhich payload is set
payloadMotionEvent | UsageSnapshot | TrafficEventthe decoded event
ts_usintWhen the event happened, in box microseconds: the report's arrival for input, the tap firing for traffic. Box-local and wraps every ~71.6 minutes, so compare it only with other box stamps. See Catch timestamps.
clockClockDomainwhich chip stamped ts_us. Two stamps are directly comparable only when this matches; across domains, apply CatchState.clock.
motionMotionEvent | Nonepayload when kind == MOTION
usagesUsageSnapshot | Nonepayload when kind == USAGES
trafficTrafficEvent | Nonepayload when kind == TRAFFIC
MotionEvent
FieldTypeMeaning
dxintX delta
dyintY delta
dzintwheel delta
panintAC Pan (horizontal scroll) delta

The enclosing CatchEvent carries the stamp and domain.

UsageSnapshot
Field / methodTypeMeaning
usagesList[Usage]every held Usage (button, key, or media; modifiers are key usages 0xE0 to 0xE7)
clsClassthe one class this snapshot covers, from the frame header
directionDirectionthe edge that produced it
is_held(usage)booltest a Usage in the snapshot

Only held usages matching your filters appear, and no event is sent when none match. A one-button subscription stays sparse 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.

TrafficEvent

The payload for every byte-oriented CatchClass from HID_IN to CLIP_TRANSFER: one packet, one control transaction, or one bus event, with whatever the entry's capture let through.

Field / methodTypeMeaning
catch_classCatchClasswhich class produced it
idintendpoint number or interface number, per the class
directionDirectionIN (device to PC) or OUT (PC to device)
flagsintclass-specific, see the table below
true_lenintthe packet's length before capture truncation
bytesbytesthe captured bytes, at most 180 of them
truncated()boollen(bytes) < true_len: bytes were cut
setup()Optional[bytes]the 8-byte setup packet of a CONTROL or CLIP_TRANSFER event; None for another class or a shorter capture
data()bytesthe data stage of a CONTROL or CLIP_TRANSFER event, the whole packet for any other class
control_status()Optional[ControlStatus]the handshake the game PC received, flags bits 0-1; None for any class but CONTROL
rule_acted()boolwhether a rewrite rule changed, dropped, answered or refused the packet: flags bit 7 on HID_IN, HID_OUT, the vendor classes, CONTROL and EMIT
transfer_status()Optional[TransferStatus | int]how the transfer ended, as a TransferStatus (NAK when no reply came) or the raw byte for a status no member names; None for any class but CLIP_TRANSFER
bus_event()Optional[BusEvent]the decoded lifecycle event; None for a class other than BUS, or an unknown kind
zlp()boolwhether the event is a zero-length packet (flags bit 1 on the packet classes). On a HID endpoint, a HID_IN or EMIT event also returns true when a ZLP ended the report after its bytes
bulk_end_of_transfer()boolwhether a VENDOR_BULK event is the last of its transfer, flags bit 0
FLAGS, BY CLASS
ClassflagsRead it with
HID_IN, EMITb1 a zero-length packet (on a HID endpoint, possibly one that ended the report after the event's bytes); b7 a rewrite rule acted on the packetzlp(), rule_acted()
HID_OUT, VENDOR_INTERRUPTb1 a zero-length packet, b7 a rule actedzlp(), rule_acted()
VENDOR_BULKb0 end-of-transfer, b1 zero-length packet, b7 a rule actedbulk_end_of_transfer(), zlp(), rule_acted()
CONTROLb0-b1 the handshake the game PC received, a ControlStatus; b7 a rule actedcontrol_status(), rule_acted()
CLIP_TRANSFERhow the transfer ended, a TransferStatus byte; 0xFE when no reply cametransfer_status()
BUSthe BusEventKind; the bytes hold its argumentsbus_event()
everything else0-

A CONTROL or CLIP_TRANSFER event is one completed transaction. bytes is [setup 8][data...] (IN data only on CLIP_TRANSFER); direction says which way the data stage went.

CONTROL dataHolds
INthe reply the game PC received, after a REPLY_PATCH or REPLY_REPLACE rule, or an ANSWER rule's payload
OUTthe data stage the game PC sent, before a PATCH or REPLACE rule rewrote it for the real device

A CONTROL event is the transaction as the game PC received it, on any control endpoint. Requests the box serves from its value cache still produce one.

ControlStatus
MemberValueMeaning
OK0the transaction completed
STALLED1the game PC got a STALL from the real device, from a rule that refused the request, or (above endpoint 0) for a failed request
NAKED2NAKed until the host gave up, on endpoint 0 only
OTHER3a handshake value this build does not know; read it off TrafficEvent.flags bits 0-1
InputEvent

One decoded input: a press edge, a release edge, or a motion report. Yielded by dev.input_events(), diffed from the box's held-usage snapshots.

Field / propertyTypeMeaning
kindInputKindwhich arm is populated
usageOptional[Usage]the usage that changed; None for MOTION
dx / dy / dz / panintright / down / wheel-up / pan-right deltas this report; 0 unless kind is MOTION
ts_usintthe box stamp, as on a CatchEvent
clockClockDomainwhich chip stamped it
is_press / is_releaseboolshorthand for the kind test
InputKind
MemberValuePopulates
PRESS0usage, a momentary usage going down
RELEASE1usage, the same coming up
MOTION2dx, dy, dz, pan
Stamped

One event placed on this machine's clock by a Timeline.

FieldTypeMeaning
host_nsintwhen the event happened, on the same monotonic scale passed as now_ns
box_usintthe event's stamp, unwrapped past the 32-bit rollover
excess_nsinthow much later than the measured floor this event arrived. Jitter, not latency.
LogLine
FieldTypeMeaning
levelLogLevelseverity
textstrthe log message
RecordedFrame (MockBox.recorded_frame(idx))
FieldTypeMeaning
typeFrameType | intframe type (raw int if unknown)
seqintframe sequence byte
payloadbytesraw frame payload

Only meaningful with the mock feature.

Advanced control layer typesSetup · TransferOutcome · RewriteRule · Patch

Value types for the imperfect-clone advanced control layer. See Raw injection, Control transfers, Rewrite rules, and Descriptor patches.

Setup
FieldTypeMeaning
request_typeintbmRequestType: direction in bit 7 (set = device-to-host).
requestintbRequest.
valueintwValue.
indexintwIndex.
lengthintwLength: the data-stage length.
TransferOutcome
FieldTypeMeaning
statusTransferStatus | intOK (0x00), REFUSED (0xFC), STALL (0xFD), NAK (0xFE), NO_DEVICE (0xFF), or a byte no constant names.
databytesThe IN data the real device returned.
is_ok()boolWhether the transfer completed.
RewriteRule / RewriteEntry / RewriteTable

RewriteRule is what dev.set_rewrite takes and query_rewrite_entry returns, keyed by (rewrite_class, id, direction, match_bytes, mask). query_rewrite returns a RewriteTable (table_full, generation, entries) of RewriteEntry summaries. table_full says the box refused the last new rule or overwrite for lack of room (32 rules, or the 2048-byte payload pool full). The next change to the table, or a clear, resets it.

FieldTypeMeaning
rewrite_classRewriteClassHID_IN 4, HID_OUT 5, VENDOR_INTERRUPT 6, VENDOR_BULK 7, CONTROL 8, EMIT 9, ANY 0xFF.
idintInterface number or endpoint number.
directionDirectionBOTH / POSITIVE / NEGATIVE.
actionRewriteActionPASS 0, DROP 1, PATCH 2, REPLACE 3, ANSWER 4, STALL 5, NAK 6, REPLY_PATCH 7, REPLY_REPLACE 8.
offsetintWhere a patching action writes.
match_bytes, mask, payloadbytesThe head compare (equal length) and the action's payload.
Patch / PatchEntry / PatchSet

Patch is what dev.set_patch takes, keyed by (section, cfg, index, offset); empty bytes removes it. query_patches returns a PatchSet (applied, pending, refused, table_full, entries) of PatchEntry summaries; the four flags read as on PatchSet.

FieldTypeMeaning
sectionPatchSectionDEVICE 0, CONFIG 1, REPORT 2, STRING 3, BOS 4.
cfgintConfiguration index, for CONFIG/REPORT. 0 is the first configuration, not bConfigurationValue.
indexintInterface or string index, for REPORT/STRING.
offsetintByte offset within the descriptor.
bytesbytesThe overwrite bytes; empty removes the patch.

Transform typesTransform · Transforms

Value types for field transforms. Build a Transform with a classmethod or from parts; dev.query_transforms returns a Transforms table. To weigh or reverse a field, use dev.scale (signed percent).

Transform.swap(a, b)              -> Transform
Transform.remap(source, dest)     -> Transform
Transform(op, source, dest)
FieldTypeMeaning
opTransformOpREMAP 0, SWAP 1.
sourceLockTargetThe field the transform reads.
destLockTargetThe field it writes. Naming the same field as source raises TransformOpFieldsError: both ops move a value.

Transforms carries table_full (bool) and entries (a list of Transform, in the order the box applies them).

ErrorsMediusError, its subclasses, and the Status codes

Every Blocks call (and any that fails on the wire) raises a MediusError or a subclass; catch the base class for all of them. Canonical mapping: Library errors.

MediusError (Exception)
AttributeTypeMeaning
statusStatusthe failure code
messagestrthe last error text
proto_verintoffending 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)
Subclass per Status
ExceptionRaised on
IoErrorERR_IO
NotFoundErrorERR_NOT_FOUND
NoReplyErrorERR_NO_REPLY
BadProtoVerErrorERR_BAD_PROTO_VER
QueryTimeoutErrorERR_QUERY_TIMEOUT
DisconnectedErrorERR_DISCONNECTED
FrameTooLongErrorERR_FRAME_TOO_LONG
UpdateErrorERR_UPDATE
InvalidArgErrorERR_INVALID_ARG
PanicErrorERR_PANIC
CatchTableFullErrorERR_CATCH_TABLE_FULL
EmptySubscriptionErrorERR_EMPTY_SUBSCRIPTION
CaptureNotApplicableErrorERR_CAPTURE_NOT_APPLICABLE
NotAnInputFilterErrorERR_NOT_AN_INPUT_FILTER
WildcardNotInputErrorERR_WILDCARD_NOT_INPUT
HalfEdgeInputFilterErrorERR_HALF_EDGE_INPUT_FILTER
ReservedIdErrorERR_RESERVED_ID
RelativeDirectionErrorERR_RELATIVE_DIRECTION
LockScaleRangeErrorERR_LOCK_SCALE_RANGE
LockScaleUsageErrorERR_LOCK_SCALE_USAGE
ImperfectRequiredErrorERR_IMPERFECT_REQUIRED
RewriteMaskLengthErrorERR_REWRITE_MASK_LENGTH
RewriteActionClassErrorERR_REWRITE_ACTION_CLASS
RewritePayloadTooLargeErrorERR_REWRITE_PAYLOAD_TOO_LARGE
RewriteTableFullErrorERR_REWRITE_TABLE_FULL
TransformOpFieldsErrorERR_TRANSFORM_OP_FIELDS
TransformTableFullErrorERR_TRANSFORM_TABLE_FULL
RawDirectionErrorERR_RAW_DIRECTION
ClipFrameCountErrorERR_CLIP_FRAME_COUNT
ClipFrameTooLongErrorERR_CLIP_FRAME_TOO_LONG
ClipTransferDataErrorERR_CLIP_TRANSFER_DATA
ClipPacketTriggerErrorERR_CLIP_PACKET_TRIGGER
RewriteMatchTooLongErrorERR_REWRITE_MATCH_TOO_LONG
RewritePoolFullErrorERR_REWRITE_POOL_FULL

The last 24 are argument refusals, raised before a frame reaches the box.

RefusalRaised on
CatchTableFullErrorthis process's subscriptions together need more than the box's 32 entries
EmptySubscriptionErrora subscription with no filters, which would never yield an event
CaptureNotApplicableErrora Capture on an input class, which carries no packet
NotAnInputFilterErrora traffic class passed to input_events, which cannot decode one
WildcardNotInputErrorCatchFilter.everything() passed to input_events; it covers traffic too
HalfEdgeInputFilterErroran input filter narrowed to one edge, which cannot be decoded into press and release
ReservedIdErroran exact id equal to the blanket sentinel, which would address the whole class instead
RelativeDirectionErrorDirection.WITH or AGAINST where only a fixed sign or edge can be addressed; they resolve against the bearing at emit time, after the call
LockScaleRangeErrora lock scale outside LOCK_SCALE_MIN to LOCK_SCALE_MAX
LockScaleUsageErrora negative (reversing) lock scale on a button, key or media usage, which carries one bit and has nothing to reverse
ImperfectRequiredErrorset_rewrite or apply_patch with the imperfect-clone opt-in off
RewriteMaskLengthErrora rewrite rule whose match and mask are different lengths
RewriteActionClassErrora rewrite action that its rule's class does not accept
RewritePayloadTooLargeErrora rewrite payload larger than the head the box holds for its class
TransformOpFieldsErrora transform op that cannot address its source and destination fields, or one field named as both
RewriteTableFullErrora rewrite rule past the table's capacity
TransformTableFullErrora transform past the table's capacity
RawDirectionErrora raw injection direction other than Direction.IN or Direction.OUT
ClipFrameCountErrora clip frame with more than CLIP_EDGES_MAX edges or CLIP_RAW_MAX raw reports
ClipFrameTooLongErrora clip frame that encodes to more than CLIP_ENTRY_MAX bytes
ClipTransferDataErrora clip transfer whose data is not what its setup packet announces: length bytes for an OUT request, none for an IN one
ClipPacketTriggerErrora ClipPacketTrigger the box would refuse; the message says why
RewriteMatchTooLongErrora rewrite rule with more than 16 match bytes
RewritePoolFullErrora rewrite payload larger than the room the held rules leave in the box's 2048-byte payload pool; an overwrite counts the bytes it replaces as free

DisconnectedError ends a stream iteration instead of propagating. OK and ERR_UNKNOWN have no subclass; ERR_UNKNOWN raises the base MediusError.

UpdateError (MediusError)
AttributeTypeMeaning
opint | Nonethe refused op: 0 begin, 1 data, 2 end, 3 abort, 4 activate; 0 after a confirm that timed out
update_statusint | Nonethe box's update status for it, such as 0x1B for a chip on probation
argint | Nonethe status argument

All three are None when the library raised it without a detail from the box.

Status
MemberValueMemberValue
OK0ERR_DISCONNECTED6
ERR_IO1ERR_FRAME_TOO_LONG7
ERR_NOT_FOUND2ERR_UPDATE8
ERR_NO_REPLY3ERR_INVALID_ARG9
ERR_BAD_PROTO_VER4ERR_PANIC10
ERR_QUERY_TIMEOUT5ERR_UNKNOWN11
ERR_CATCH_TABLE_FULL12ERR_WILDCARD_NOT_INPUT16
ERR_EMPTY_SUBSCRIPTION13ERR_HALF_EDGE_INPUT_FILTER17
ERR_CAPTURE_NOT_APPLICABLE14ERR_RESERVED_ID18
ERR_NOT_AN_INPUT_FILTER15ERR_RELATIVE_DIRECTION19
ERR_LOCK_SCALE_RANGE20ERR_TRANSFORM_OP_FIELDS27
ERR_LOCK_SCALE_USAGE21ERR_TRANSFORM_TABLE_FULL28
ERR_IMPERFECT_REQUIRED22ERR_RAW_DIRECTION29
ERR_REWRITE_MASK_LENGTH23ERR_CLIP_FRAME_COUNT30
ERR_REWRITE_ACTION_CLASS24ERR_CLIP_FRAME_TOO_LONG31
ERR_REWRITE_PAYLOAD_TOO_LARGE25ERR_CLIP_TRANSFER_DATA32
ERR_REWRITE_TABLE_FULL26ERR_CLIP_PACKET_TRIGGER33
ERR_REWRITE_MATCH_TOO_LONG34ERR_REWRITE_POOL_FULL35