medius 3.4.1

Host control library for the medius transparent mouse passthrough box
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
//! Frame opcodes and wire constants, pinned to `ctrl_proto.h` / `control-protocol.md`.

use core::fmt;

/// Start-of-frame byte; the receiver resyncs by scanning for it (§2).
pub const SOF: u8 = 0xA5;

/// Maximum payload length (§2); a larger `LEN` is rejected as bogus.
pub const MAX_PAYLOAD: usize = 512;

/// Protocol version in `RESP(VERSION)` (§4.1); the handshake requires this exact value. Bumped to 8
/// for v3.4.1, which reshapes `RESP(CLIP)` and `CLIP_TRIGGER` and matches an interrupt OUT packet on
/// a vendor interface as `VendorInterrupt`, which protocol 7 matched as `HidOut`. Protocol 7 is
/// v3.4.0: the advanced control layer (`RAW`/`TRANSFER`/`REWRITE`/`PATCH`) and the `u16` `HEALTH`
/// flags. The handshake refuses a v3.4.0 box with [`Error::BadProtoVer`](crate::Error::BadProtoVer);
/// update its firmware from the dashboard at <https://medius.k4tech.net/dashboard> before opening it
/// here.
pub const PROTO_VER: u8 = 8;

/// `INJECT` class byte: the momentary-usage field kind.
pub const INJ_BTN: u8 = 0;
pub const INJ_KEY: u8 = 1;
pub const INJ_MEDIA: u8 = 2;
/// `MOVE` motion byte: the relative-axis field kind.
pub const INJ_MOTION_CURSOR: u8 = 0;
pub const INJ_MOTION_WHEEL: u8 = 1;
pub const INJ_MOTION_PAN: u8 = 2;
/// `MOVE` flags byte: the per-command movement-riding override (§3.1).
pub const MV_F_NOW: u8 = 0x01;
pub const MV_F_FLUSH: u8 = 0x02;
pub const MV_F_DISCARD: u8 = 0x04;

pub const Q_VERSION: u8 = 0;
pub const Q_HEALTH: u8 = 1;
/// Cloned device identity: vid/pid/bcd + serial/bos flags + primary kind + product (§4.3).
pub const Q_DEVICE_INFO: u8 = 2;
/// Unified device capabilities (mouse, keyboard, per-class change_driven) for the whole cloned device (§4.4).
pub const Q_CAPS: u8 = 3;
/// Live native report rate + clone poll period + confidence (§4.5).
pub const Q_RATE: u8 = 4;
/// Delivery/telemetry counters (§4.6).
pub const Q_STATS: u8 = 5;
/// Active lock bitmask (§4.8, v1.5.0).
pub const Q_LOCKS: u8 = 6;
/// Active catch subscription mask + dropped-event count (§4.9, v1.6.0).
pub const Q_CATCH: u8 = 7;
// selector 8 retired (was Q_KBD_CAPS; keyboard caps folded into the unified Q_CAPS = 3).
/// Persistent box options, read one at a time by id: `QUERY [Q_OPTIONS][id]` → `RESP [Q_OPTIONS][id][value..]` (§4.14).
pub const Q_OPTIONS: u8 = 9;

/// `OPTION` id: imperfect-clone opt-in. Set value `[allow u8]`; readback adds `over_capacity`/`clone_imperfect`.
pub const OPT_IMPERFECT: u8 = 0;
/// `OPTION` id: movement riding. Value `[timeout u16 LE ms]`; 0 = off, N = ride window in ms.
pub const OPT_MOVE_RIDE: u8 = 1;
/// `OPTION` id: emit-rate pacing. Value `[mode u8][rate_hz u16 LE][force_hz u16 LE]`; mode 0 learnt / 1 bInterval / 2 fixed.
pub const OPT_EMIT: u8 = 2;
/// `OPTION` id: box name. Value `[name ascii]` 1..32 printable ASCII (0 bytes clears to default); set-only, read off `RESP(VERSION)`.
pub const OPT_NAME: u8 = 3;
/// `OPTION` id: the bearing. Value `[window u16 LE ms][mode u8]`; what `LOCK_DIR_WITH`/`AGAINST` are measured against (§3.12).
pub const OPT_BEARING: u8 = 4;
/// `OPTION` id: what the box renders motion with. Value `[mode u8][full u8]`; mode 0 off / 1 stock / 2 de-spiked / 3 unsmoothed.
pub const OPT_RENDER: u8 = 5;
/// `OPTION` id: how far an injected delta is spread across the host's command interval. Value `[percent u16 LE]`.
pub const OPT_SPREAD: u8 = 6;
/// `OPTION(BEARING)` mode: each axis reads its own sign against its own bearing.
pub const BEARING_PER_AXIS: u8 = 0;
/// `OPTION(BEARING)` mode: the physical delta is projected onto the injected XY vector; motion across it is untouched.
pub const BEARING_VECTOR: u8 = 1;

/// Buffered-clip status selector: `QUERY [Q_CLIP]` → `RESP(CLIP)` (§4.15).
pub const Q_CLIP: u8 = 10;
/// Both chips' firmware versions and slot state: `QUERY [Q_FIRMWARE]` → `RESP(FIRMWARE)` (§4.16).
pub const Q_FIRMWARE: u8 = 11;
/// Rewrite-rule table summary: `QUERY [Q_REWRITE]` → `RESP(REWRITE)` (flags + gen + list) (§3.14, §4.17).
pub const Q_REWRITE: u8 = 12;
/// One rewrite rule in full, in the `REWRITE` command's own shape: `QUERY [Q_REWRITE_ENTRY][index]` (§4.17).
pub const Q_REWRITE_ENTRY: u8 = 13;
/// Descriptor-patch set summary: `QUERY [Q_PATCHES]` → `RESP(PATCHES)` (flags + list) (§3.14, §4.17).
pub const Q_PATCHES: u8 = 14;
/// One descriptor patch in full, in the `PATCH` command's own shape: `QUERY [Q_PATCH_ENTRY][index]` (§4.17).
pub const Q_PATCH_ENTRY: u8 = 15;

/// `UPDATE` sub-ops (§3.13).
pub const OTA_OP_BEGIN: u8 = 0;
pub const OTA_OP_DATA: u8 = 1;
pub const OTA_OP_END: u8 = 2;
pub const OTA_OP_ABORT: u8 = 3;
pub const OTA_OP_ACTIVATE: u8 = 4;
/// Image bytes per `DATA` frame: the frame's 512 minus op, target and a 2-byte chunk index, rounded
/// down to a multiple of four so every flash write on the box is aligned.
pub const OTA_CHUNK: usize = 504;
/// `DATA` frames the box accepts before it must acknowledge, when it does not say otherwise.
pub const OTA_CREDIT: usize = 16;
/// `UPDATE_RESP` payload length: op, target, status, arg.
pub const UPD_RESP_LEN: usize = 7;
/// `CLIP_CTRL` engine verbs. Ops 0..5 are the shared action space (also a trigger `action` byte).
pub const CLIP_OP_START: u8 = 0;
pub const CLIP_OP_STOP: u8 = 1;
pub const CLIP_OP_PAUSE: u8 = 2;
pub const CLIP_OP_RESUME: u8 = 3;
pub const CLIP_OP_RESTART: u8 = 4;
pub const CLIP_OP_TOGGLE: u8 = 5;
pub const CLIP_OP_CLEAR: u8 = 6;
pub const CLIP_OP_FINALIZE: u8 = 7;
/// `CLIP_SET` scalar setting ids (OPTION-shaped `[id][value]`).
pub const CLIP_SET_AUTOLOCK: u8 = 0;
pub const CLIP_SET_LOOP: u8 = 1;
pub const CLIP_SET_RETAIN: u8 = 2;
pub const CLIP_SET_RIDE: u8 = 3;
/// `CLIP_TRIGGER` set: max bindings and the flags byte bits.
pub const CLIP_TRIG_MAX: usize = 8;
pub const CLIP_TRIG_F_PRESENT: u8 = 0x01;
pub const CLIP_TRIG_F_CONSUME: u8 = 0x02;
/// Packet trigger: the verb runs on the first packet of a run of matching ones.
pub const CLIP_TRIG_F_RUN: u8 = 0x04;
/// Packet triggers the box holds beside its input bindings (`CLIP_PKT_TRIG_MAX`).
pub const CLIP_PKT_TRIG_MAX: usize = 8;
/// Match bytes the box holds across every packet trigger (`CLIP_PKT_MATCH_POOL`); it bounds
/// `RESP(CLIP)` to one frame.
pub const CLIP_PKT_MATCH_POOL: usize = 112;
/// `CLIP_TRIGGER` bytes ahead of a packet trigger's match: `[class][id u16][dir][action][flags][slen][mlen]`.
pub const CLIP_PKT_TRIG_HDR: usize = 8;
/// `RESP(CLIP)` bytes ahead of a packet trigger entry's match: the command's eight and `hits u16`.
pub const CLIP_PKT_TRIG_ENTRY: usize = 10;
/// `RESP(CLIP)` config-section flags byte.
pub const CLIP_CFG_F_LOOP: u8 = 0x01;
pub const CLIP_CFG_F_RETAIN: u8 = 0x02;
pub const CLIP_CFG_F_FINALIZED: u8 = 0x04;
pub const CLIP_CFG_F_RIDE: u8 = 0x08;
/// Autolock scope bits (`CLIP_SET(AUTOLOCK)` value): which physical-input classes the clip auto-locks (0 = none).
pub const CLIP_LOCK_AIM: u8 = 0x01;
pub const CLIP_LOCK_WHEEL: u8 = 0x02;
pub const CLIP_LOCK_BUTTONS: u8 = 0x04;
pub const CLIP_LOCK_KEYS: u8 = 0x08;
pub const CLIP_LOCK_MEDIA: u8 = 0x10;
/// Trigger binding class/id wildcards: any class, any usage within a class.
pub const CLIP_COND_ANY_CLASS: u8 = 0xFF;
pub const CLIP_COND_ANY_ID: u16 = 0xFFFF;
/// Clip entry tags/flags (see [`ClipBuilder`](crate::ClipBuilder)).
pub const CLIP_TAG_GAP: u8 = 0x00;
pub const CLIP_F_XY: u8 = 0x01;
pub const CLIP_F_WHEEL: u8 = 0x02;
pub const CLIP_F_EDGES: u8 = 0x04;
pub const CLIP_F_PAN: u8 = 0x08;
pub const CLIP_F_RAW: u8 = 0x10;
pub const CLIP_F_XFER: u8 = 0x20;
/// `RESP(CLIP)`: the scalar prefix length, whose last byte is the held-usage count (§4.15).
pub const RESP_CLIP_HDR: usize = 31;

pub const BTN_LEFT: u8 = 0;
pub const BTN_RIGHT: u8 = 1;
pub const BTN_MIDDLE: u8 = 2;
pub const BTN_SIDE1: u8 = 3;
pub const BTN_SIDE2: u8 = 4;
/// Count of *named* buttons; ids past it are numeric and drive up to the clone's declared count.
pub const BTN_COUNT: u8 = 5;
/// Ceiling on injectable/lockable/catchable buttons: the widest owned devices declare sixteen in one
/// run, and a device past this caps here. Mirrors `MAX_BUTTONS` in the firmware.
pub const MAX_BUTTONS: u8 = 16;

/// Clear our injected press; defer to physical state.
pub const ACT_SOFTREL: u8 = 0;
/// Force the button down regardless of physical state.
pub const ACT_PRESS: u8 = 1;
/// Force the button up, masking a physical hold.
pub const ACT_FORCEREL: u8 = 2;

/// Inter-chip link to the host chip is up.
pub const H_LINK_UP: u8 = 0x01;
/// A real mouse is attached on the host chip.
pub const H_MOUSE_ATT: u8 = 0x02;
/// The clone has been configured by the game PC.
pub const H_CLONE_CFG: u8 = 0x04;
/// Injection is currently active.
pub const H_INJECT_ON: u8 = 0x08;
/// The native-rate estimator window is full, so the `RATE` value is trustworthy (§4.2, v1.4.0).
pub const H_RATE_CONFIDENT: u8 = 0x10;
/// At least one lock is active (§4.2, v1.5.0).
pub const H_LOCK_ON: u8 = 0x20;
/// A catch subscription is active; physical-input events are streaming (§4.2, v1.6.0).
pub const H_CATCH_ON: u8 = 0x40;
/// A keyboard is attached on the host chip, cloned and injectable (§4.2, v2.0.0).
pub const H_KBD_ATT: u8 = 0x80;

// `HEALTH` became a `u16` LE in `CTRL_PROTO_VER 7` (§4.2). Bits 0-7 kept their meaning; the v3.4.0
// advanced control layer opened the high byte, so these three are `u16`. [`Health`](crate::Health) decodes
// the whole word.
/// The rewrite-rule table (§3.14) is non-empty (v3.4.0).
pub const H_REWRITE_ON: u16 = 0x0100;
/// A descriptor-patch set (§3.14) is applied to the clone (v3.4.0).
pub const H_PATCH_ON: u16 = 0x0200;
/// A field transform is active (reserved; the transforms feature owns this bit) (v3.4.0).
pub const H_TRANSFORM_ON: u16 = 0x0400;

/// `CATCH` class: a mouse button (§3.9). Classes 0..3 are `LOCK`'s classes unchanged.
pub const CATCH_CLS_BTN: u8 = 0;
/// `CATCH` class: a keyboard key or modifier (§3.9).
pub const CATCH_CLS_KEY: u8 = 1;
/// `CATCH` class: a media (Consumer) usage (§3.9).
pub const CATCH_CLS_MEDIA: u8 = 2;
/// `CATCH` class: a relative axis (§3.9).
pub const CATCH_CLS_AXIS: u8 = 3;
/// `CATCH` class: raw HID input report bytes, keyed by interface number (§3.9).
pub const CATCH_CLS_HID_IN: u8 = 4;
/// `CATCH` class: interrupt-OUT report bytes, keyed by endpoint number, direction OUT (§3.9).
pub const CATCH_CLS_HID_OUT: u8 = 5;
/// `CATCH` class: vendor-interface interrupt traffic, keyed by endpoint number and direction (§3.9).
pub const CATCH_CLS_VEND_INTR: u8 = 6;
/// `CATCH` class: vendor-interface bulk traffic, keyed by endpoint number and direction (§3.9).
pub const CATCH_CLS_VEND_BULK: u8 = 7;
/// `CATCH` class: a proxied control transaction, keyed by endpoint number (§3.9).
pub const CATCH_CLS_CONTROL: u8 = 8;
/// `CATCH` class: the bytes the clone emitted, keyed by endpoint number, direction IN (§3.9).
pub const CATCH_CLS_EMIT: u8 = 9;
/// `CATCH` class: bus lifecycle events (§3.9).
pub const CATCH_CLS_BUS: u8 = 10;
/// `CATCH` class: a control transfer a clip ran against the device (§3.9).
pub const CATCH_CLS_CLIP_XFER: u8 = 11;
/// `CATCH` class wildcard: every class (§3.9).
pub const CATCH_CLS_ANY: u8 = 0xFF;
/// `CATCH` id wildcard: every id within the class (§3.9), the same sentinel `LOCK` uses.
pub const CATCH_ID_ANY: u16 = 0xFFFF;

/// Entries the box's subscription table holds (`CATCH_TAB_MAX`). Past this the box refuses the entry
/// and says so only in `RESP(CATCH).table_full`, so a caller that never reads that flag would get a
/// silently incomplete subscription.
pub const CATCH_MAX_ENTRIES: usize = 32;

/// `TRAFFIC_EVENT.flags` for a `CATCH_CLS_CONTROL` event: what the real device answered.
pub const CATCH_CTRL_OK: u8 = 0x00;
/// The device STALLed the transfer.
pub const CATCH_CTRL_STALL: u8 = 0xFD;
/// The device NAKed to timeout, or never answered.
pub const CATCH_CTRL_NAK: u8 = 0xFE;

/// `RESP(CATCH).clk_rate_ppb` sentinel: the box has fitted no drift rate. Distinct from a fitted 0,
/// which says the two crystals are matched.
pub const CLK_RATE_NONE: i32 = i32::MIN;

// `REWRITE` action byte (§3.14): what the winning rule does to a matched packet. A report class can
// `DROP`; only the control class may `ANSWER`/`STALL`/`NAK` or rewrite the device's reply. Shared
// wire values with the firmware `rewrite_tab.h` and the dashboard.
/// The rule matched but leaves the packet untouched (a shadow over a broader rule).
pub const RW_PASS: u8 = 0;
/// Report class: the packet is not delivered.
pub const RW_DROP: u8 = 1;
/// Overwrite `plen` payload bytes at `off`, length preserved.
pub const RW_PATCH: u8 = 2;
/// The packet becomes the payload.
pub const RW_REPLACE: u8 = 3;
/// Control: answer from the payload without asking the device.
pub const RW_ANSWER: u8 = 4;
/// Control: protocol STALL.
pub const RW_STALL: u8 = 5;
/// Control: NAK to a timeout.
pub const RW_NAK: u8 = 6;
/// Control IN: overwrite the device's reply at `off`.
pub const RW_REPLY_PATCH: u8 = 7;
/// Control IN: replace the device's reply with the payload.
pub const RW_REPLY_REPLACE: u8 = 8;

// `PATCH` section byte (§3.14): which descriptor a patch overwrites. `APPLY`/`CLEAR` are engine verbs
// carried in the same byte, handled before the store rather than kept as section keys.
/// The 18-byte device descriptor (`cfg`/`index` ignored).
pub const PATCH_SEC_DEVICE: u8 = 0;
/// A configuration's descriptor (`cfg` = configuration index).
pub const PATCH_SEC_CONFIG: u8 = 1;
/// An interface's report descriptor (`cfg` + `index` = interface number).
pub const PATCH_SEC_REPORT: u8 = 2;
/// A string descriptor (`index` = string index), the whole string.
pub const PATCH_SEC_STRING: u8 = 3;
/// The BOS descriptor (`cfg`/`index` ignored).
pub const PATCH_SEC_BOS: u8 = 4;
/// `PATCH` engine verb: re-present the clone with the stored set (one replug to the game PC).
pub const PATCH_APPLY: u8 = 0xFE;
/// `PATCH` engine verb: drop every patch for this device and re-present unpatched.
pub const PATCH_CLEAR: u8 = 0xFF;

/// Entries the box's rewrite table holds (`REWRITE_TAB_MAX`); past it a rule is refused and `RESP(REWRITE).table_full` says so.
pub const REWRITE_MAX_ENTRIES: usize = 32;
/// Entries the box's descriptor-patch store holds (`PATCH_MAX`); past it a patch is refused and `RESP(PATCHES).table_full` says so.
pub const PATCH_MAX_ENTRIES: usize = 16;
/// The most `match`/`mask` bytes the box compares against a packet head (`PKT_MATCH_MAX`), for a
/// rewrite rule and a clip packet trigger alike.
pub const PKT_MATCH_MAX: usize = 16;
/// The most `match`/`mask` bytes one rewrite rule compares (`REWRITE_MATCH_MAX`).
pub const REWRITE_MATCH_MAX: usize = PKT_MATCH_MAX;

// `TRANSFORM` op byte (§3.15, `CTRL_XF_*`): a field operation on the semantic path. `sclass`/`dclass`
// reuse the input classes ([`CATCH_CLS_AXIS`]/`_BTN`/`_KEY`/`_MEDIA`). Shared wire values with the
// firmware `transform_tab.h`.
/// Move a source field into a destination (axis→axis or button→button in one report, or button→key /
/// button→media across classes).
pub const TF_REMAP: u8 = 0;
/// Exchange two axes (read both, write both).
pub const TF_SWAP: u8 = 1;
/// Ops the box admits (`CTRL_XF_OP_COUNT`): a byte at or above it is refused.
pub const TF_OP_COUNT: u8 = 2;

/// Field-transform table summary: `QUERY [Q_TRANSFORMS]` → `RESP(TRANSFORMS)` (flags + list) (§3.15).
pub const Q_TRANSFORMS: u8 = 16;
/// Entries the box's transform table holds (`CTRL_TRANSFORM_MAXN`); past it an entry is refused and `RESP(TRANSFORMS).table_full` says so.
pub const TRANSFORM_MAX_ENTRIES: usize = 32;
/// `RESP(TRANSFORMS).flags` bit 0 (`CTRL_TRANSFORM_F_FULL`): the table is full.
pub const TF_F_FULL: u8 = 0x01;

/// `LOCK` class byte (§3.8): momentary usages share `INJECT`'s space, plus a relative-axis class.
pub const LOCK_CLS_BTN: u8 = 0;
pub const LOCK_CLS_KEY: u8 = 1;
pub const LOCK_CLS_MEDIA: u8 = 2;
pub const LOCK_CLS_AXIS: u8 = 3;
/// `LOCK` id sentinel: the whole class (a blanket lock).
pub const LOCK_ID_ALL: u16 = 0xFFFF;
/// `LOCK` axis ids (for `LOCK_CLS_AXIS`).
pub const LOCK_AXIS_X: u16 = 0;
pub const LOCK_AXIS_Y: u16 = 1;
pub const LOCK_AXIS_WHEEL: u16 = 2;
/// AC Pan (horizontal scroll): a first-class relative axis, peer of the wheel.
pub const LOCK_AXIS_PAN: u16 = 3;
/// `LOCK` direction byte: both / positive-or-press / negative-or-release, then the two measured
/// against the bearing rather than a fixed sign (§3.12).
pub const LOCK_DIR_BOTH: u8 = 0;
pub const LOCK_DIR_POS: u8 = 1;
pub const LOCK_DIR_NEG: u8 = 2;
pub const LOCK_DIR_WITH: u8 = 3;
pub const LOCK_DIR_AGAINST: u8 = 4;
/// `LOCK` scale (§3.8), an `i16` on the wire: percent of the physical value kept. 0 blocks, 100 passes
/// it untouched, above 100 amplifies, to a ceiling of 255 (2.55x).
pub const LOCK_SCALE_BLOCK: i16 = 0;
pub const LOCK_SCALE_PASS: i16 = 100;
pub const LOCK_SCALE_MAX: i16 = 255;
/// The most a `LOCK` scale can REVERSE by: a negative scale weighs the physical value and reverses what
/// it keeps, so `-100` is a plain inversion and `-50` keeps half of it the other way round. The slot is
/// picked from the sign of the delta before the weigh, so a directional negative is well defined. A
/// momentary usage carries one bit and has nothing to reverse, so it takes `0 ..= LOCK_SCALE_MAX`.
pub const LOCK_SCALE_MIN: i16 = -LOCK_SCALE_MAX;

/// `CAPS` kbd_flags: keys are an NKRO bitmap (`n_keys` = 0xFF), else a keycode array (§4.4).
pub const KBC_NKRO: u8 = 0x01;
/// `CAPS` kbd_flags: a Consumer (media-key) collection is present and injectable/catchable.
pub const KBC_CONSUMER: u8 = 0x02;
/// `CAPS` kbd_flags: a System-control collection is present (passthrough-only, not injectable).
pub const KBC_SYSTEM: u8 = 0x04;
/// `CAPS` kbd_flags: the keyboard report sits behind a HID report ID.
pub const KBC_REPORT_ID: u8 = 0x08;

/// `CAPS` change_driven flag: the mouse class is change-driven (never set, mouse motion is continuous).
pub const CAPS_CD_MOUSE: u8 = 0x01;
/// `CAPS` change_driven flag: the keyboard/media class is change-driven (set when a keyboard is bound).
pub const CAPS_CD_KBD: u8 = 0x02;

/// `DEVICE_INFO` flag: the clone serves a serial string (§4.3).
pub const DI_HAS_SERIAL: u8 = 0x01;
/// `DEVICE_INFO` flag: the clone serves a BOS descriptor (§4.3).
pub const DI_HAS_BOS: u8 = 0x02;

/// `CAPS` axis flag: relative X present (§4.4).
pub const CAP_X: u8 = 0x01;
/// `CAPS` axis flag: relative Y present (§4.4).
pub const CAP_Y: u8 = 0x02;
/// `CAPS` axis flag: wheel present (§4.4).
pub const CAP_WHEEL: u8 = 0x04;
/// `CAPS` axis flag: the mouse report sits behind a HID report ID (§4.4).
pub const CAP_REPORT_ID: u8 = 0x08;
/// `CAPS` axis flag: AC Pan (horizontal scroll) present (§4.4).
pub const CAP_PAN: u8 = 0x10;

/// `RATE` flag: estimator window full (same source as [`H_RATE_CONFIDENT`], §4.5).
pub const RATE_CONFIDENT: u8 = 0x01;
/// `RATE` flag: the active input is change-driven (keyboard/media); no continuous cadence, poll floor only.
pub const RATE_CHANGE_DRIVEN: u8 = 0x02;

pub const LOG_ERROR: u8 = 0;
pub const LOG_WARN: u8 = 1;
pub const LOG_INFO: u8 = 2;
pub const LOG_DEBUG: u8 = 3;
pub const LOG_VERBOSE: u8 = 4;

/// A frame opcode (the `TYPE` byte, §3 / §4).
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FrameType {
    /// `MOVE`: relative-axis movement, motion-tagged (cursor dx/dy or wheel dz) (PC→box).
    Move = 0x01,
    /// `INJECT`: set a momentary-usage override (button/key/media), class-tagged (PC→box).
    Inject = 0x03,
    /// `RESET`: clear all injection (PC→box).
    Reset = 0x04,
    /// `QUERY`: request a state snapshot, elicits `RESP` (PC→box).
    Query = 0x05,
    /// `RESP`: reply to a `QUERY`, `SEQ` echoes the request (box→PC).
    Resp = 0x06,
    /// `REBOOT_DL`: reboot a chip to ROM download or to run (PC→box).
    RebootDl = 0x07,
    /// `LOG`: unsolicited device diagnostics (box→PC).
    Log = 0x08,
    /// `LED`: status LED override (PC→box).
    Led = 0x09,
    /// `LOCK`: lock/unlock an axis or button edge (PC→box).
    Lock = 0x0A,
    /// `CATCH`: subscribe to the physical-input event stream (PC→box).
    Catch = 0x0B,
    /// `MOTION_EVENT`: one unsolicited relative-axis catch event (dx/dy/dz/dpan); `SEQ` rolling (box→PC).
    MotionEvent = 0x0C,
    /// `USAGE_EVENT`: one unsolicited held-usage snapshot (class-tagged button/key/media); box→PC.
    UsageEvent = 0x0F,
    /// `TRAFFIC_EVENT`: one unsolicited byte-oriented catch event (HID / vendor / control / emit / bus); box→PC.
    TrafficEvent = 0x16,
    /// `OPTION`: set a persistent box option by id (imperfect-clone, movement riding, emit pacing, box name) (PC→box).
    Option = 0x11,
    /// `CLIP_APPEND`: append buffered-clip entries to the device ring; `SEQ` = append seq (PC→box).
    ClipAppend = 0x12,
    /// `CLIP_CTRL`: a clip engine verb (start/stop/pause/resume/restart/toggle/clear/finalize) (PC→box).
    ClipCtrl = 0x13,
    /// `CLIP_SET`: a clip scalar setting `[id][value]` (autolock/loop/retain) (PC→box).
    ClipSet = 0x14,
    /// `CLIP_TRIGGER`: add/remove a clip trigger. An input binding is `[class][id u16][edge][action][flags]`;
    /// a packet trigger names a traffic class and carries `[slen][mlen][match][mask]` after those six bytes (PC→box).
    ClipTrigger = 0x15,
    /// `UPDATE`: stage and activate firmware on either chip (PC→box) (§3.13).
    Update = 0x17,
    /// `UPDATE_RESP`: the answer to one `UPDATE` op (box→PC) (§4.16).
    UpdateResp = 0x18,
    /// `RAW`: put raw bytes on a cloned endpoint, fire-and-forget (PC→box, §3.14, `OPTION(IMPERFECT)`).
    Raw = 0x19,
    /// `TRANSFER`: run one control transfer against the real device, answered by `TransferResp` (PC→box, §3.14).
    Transfer = 0x1A,
    /// `TRANSFER_RESP`: the device's answer to a `TRANSFER`, `SEQ` echoes the request (box→PC, §3.14).
    TransferResp = 0x1B,
    /// `REWRITE`: add/overwrite/remove one on-box rewrite rule, fire-and-forget (PC→box, §3.14).
    Rewrite = 0x1C,
    /// `PATCH`: store/apply/clear a descriptor patch, fire-and-forget (PC→box, §3.14).
    Patch = 0x1D,
    /// `TRANSFORM`: add/overwrite/remove one field transform, fire-and-forget (PC→box, §3.15).
    Transform = 0x1E,
}

/// Error returned when a byte does not name a known [`FrameType`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct UnknownFrameType(pub u8);

impl fmt::Display for UnknownFrameType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "unknown frame type 0x{:02X}", self.0)
    }
}

impl core::error::Error for UnknownFrameType {}

impl TryFrom<u8> for FrameType {
    type Error = UnknownFrameType;

    fn try_from(value: u8) -> Result<Self, Self::Error> {
        Ok(match value {
            0x01 => FrameType::Move,
            0x03 => FrameType::Inject,
            0x04 => FrameType::Reset,
            0x05 => FrameType::Query,
            0x06 => FrameType::Resp,
            0x07 => FrameType::RebootDl,
            0x08 => FrameType::Log,
            0x09 => FrameType::Led,
            0x0A => FrameType::Lock,
            0x0B => FrameType::Catch,
            0x0C => FrameType::MotionEvent,
            0x0F => FrameType::UsageEvent,
            0x16 => FrameType::TrafficEvent,
            0x11 => FrameType::Option,
            0x12 => FrameType::ClipAppend,
            0x13 => FrameType::ClipCtrl,
            0x14 => FrameType::ClipSet,
            0x15 => FrameType::ClipTrigger,
            0x17 => FrameType::Update,
            0x18 => FrameType::UpdateResp,
            0x19 => FrameType::Raw,
            0x1A => FrameType::Transfer,
            0x1B => FrameType::TransferResp,
            0x1C => FrameType::Rewrite,
            0x1D => FrameType::Patch,
            0x1E => FrameType::Transform,
            other => return Err(UnknownFrameType(other)),
        })
    }
}

impl From<FrameType> for u8 {
    fn from(t: FrameType) -> u8 {
        t as u8
    }
}