Transform
Swap or remap a field on the wireTRANSFORM moves a field the clone's descriptor declares into another one, clamped to the destination's declared range, so the clone still emits only values the real device could. It carries no imperfect-clone opt-in, unlike the rewrite/raw/patch layer.
parsed report --> LOCK --> [ TRANSFORM ] --> render --> inject --> emit
|
+-- swap two axes, read both then write both
+-- remap source -> destination, source cleared
|
+-- button -> key / media, held on the
destination's own interface| op | Name | Effect |
|---|---|---|
0 | REMAP | move the source field into the destination and clear the source |
1 | SWAP | exchange two axes: read both, then write both |
To weigh a field, or reverse it, use LOCK, whose percent is signed. An op above 1 is refused.
A field is a (class, id) in the same space INJECT and LOCK use: 0 button, 1 key, 2 media, 3 axis (id 0=X, 1=Y, 2=wheel, 3=pan).
QUERY(TRANSFORMS) reads the installed table back.
TRANSFORM
Install, overwrite, or remove one entryAn entry is keyed by its (source, dest); setting one whose key exists overwrites its op. Opcode 0x1E.
TRANSFORM 0x1E · payload 8 bytes
Fire-and-forget
| Offset | Field | Type | Notes |
|---|---|---|---|
| 0 | op | u8 | the operation, as the table above |
| 1 | sclass | u8 | source class: 0 button, 1 key, 2 media, 3 axis |
| 2 | sid | u16 | source id within the class, little-endian |
| 4 | dclass | u8 | destination class |
| 5 | did | u16 | destination id, little-endian |
| 7 | state | u8 | 1 set (add or overwrite), 0 remove the keyed entry |
The weigh runs first and the transform moves what it kept. The rounding remainder is banked once, per axis and sign, by the lock.
LOCK(X, both, -50) keep half of X, reversed TRANSFORM(swap, X, Y) and put what is left on Y physical X = +10 -> weighed -5 -> emitted on Y
| Refused when | Why |
|---|---|
op is above 1 | remap and swap are the whole set |
| the op does not admit that class pair | each op names the shapes it can read and write |
| the source and the destination are the same field | a move needs two; to weigh a field in place, use the lock |
| a field this clone does not declare | the box will not store an address it cannot reach; re-send the entry after a re-clone |
| the table already holds 32 entries | nothing is evicted; the readback's full flag says an entry was turned away |
An entry takes effect on the next report the device sends. The box walks the table in the order entries were installed, so a swap installed after a remap exchanges what the remap wrote.
TRANSFORM has no reply, so a refused entry shows up as its absence from RESP(TRANSFORMS). That reply also carries the full flag, and the TRANSFORM_ON health bit follows the table.
Swap X and Y: op = 1, source (axis 3, id 0), dest (axis 3, id 1), state = 1:
+--------+--------+--------+--------+--------+--------+ | A5 | 1E | 00 | 08 00 | 01 | 03 | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | op | sclass | +--------+--------+--------+--------+--------+--------+ +--------+--------+--------+--------+--------+ | 00 00 | 03 | 01 00 | 01 | lo hi | +--------+--------+--------+--------+--------+ | sid | dclass | did | state | CRC16 | +--------+--------+--------+--------+--------+
The wheel drives vertical motion: op = 0 (remap), source (axis 3, id 2), dest (axis 3, id 1):
+--------+--------+--------+--------+--------+--------+ | A5 | 1E | 01 | 08 00 | 00 | 03 | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | op | sclass | +--------+--------+--------+--------+--------+--------+ +--------+--------+--------+--------+--------+ | 02 00 | 03 | 01 00 | 01 | lo hi | +--------+--------+--------+--------+--------+ | sid | dclass | did | state | CRC16 | +--------+--------+--------+--------+--------+
Library bindings: transform, transform_swap, and transform_remap.
Field pairs
Which source and destination each op admitsEach op reads and writes a fixed set of shapes. Anything else is refused.
swap axis a <---------> axis b two axes
remap axis a ----------> axis b one report, source zeroed
button i ----------> button j one report, source bit cleared
button i ----------> key the keyboard collection
button i ----------> media the consumer collection
every pair is two different fields| Name | What the box does |
|---|---|
swap | Reads both axes, then writes both. Two remaps would read the second after the first had overwritten it and leave the pair equal. |
remap | Adds the source onto the destination's own value, then zeroes the source. A button destination is OR'd the press instead, and the source bit is cleared. |
An axis remap adds, so a destination that was already moving keeps its own motion and picks up the source's on top. Only the source is zeroed. The sum is clamped to the destination's declared range like any other result.
Button to key or media
The one remap that crosses collectionsA button source can drive a keyboard or Consumer destination. The key or media usage is emitted through that collection's own interface, exactly as INJECT emits one.
physical button ____----------________-----____ key on the clone ____----------________-----____ driven from the button mask the device just reported, re-read every report, with no edge latch to strand
Because it follows the level, a configuration switch, a missed release, or a destination that was briefly unbound all resolve on the next report instead of leaving a key held with nothing pressed.
A device may split its buttons across two mouse collections. The destination is held while any collection the box reports on has that button down, and a collection that does not declare the id holds nothing for it.
An entry naming a destination collection the box has not bound is refused outright. One whose collection goes away with a configuration switch turns inert: it does nothing and does not clear its source, so the button keeps reaching the game PC until that collection binds again.
Side button 3 drives the a key: op = 0 (remap), source (button 0, id 3), dest (key 1, id 0x04):
+--------+--------+--------+--------+--------+--------+ | A5 | 1E | 02 | 08 00 | 00 | 00 | +--------+--------+--------+--------+--------+--------+ | SOF | TYPE | SEQ | LEN | op | sclass | +--------+--------+--------+--------+--------+--------+ +--------+--------+--------+--------+--------+ | 03 00 | 01 | 04 00 | 01 | lo hi | +--------+--------+--------+--------+--------+ | sid | dclass | did | state | CRC16 | +--------+--------+--------+--------+--------+
The keycode is a HID keyboard usage; a media destination takes a 16-bit Consumer usage.
Where the pass sits
Against locks, rendering, and injectionThe weigh comes first and the field pass moves what it left, so a transform carries a weighed value and everything downstream reads the field where the table put it.
physical report
|
+-- 1 LOCK weighs each field on its own sign and bearing
|
+-- 2 TRANSFORM moves the weighed fields, in table order
|
+-- 3 render the model is handed the transformed, weighed delta
|
+-- 4 inject drains onto the field a remap just cleared
|
v
emitted report| Stage | Reads | Acts on |
|---|---|---|
LOCK | the physical field | a lock bites on the sign the device reported, not on where the value ends up, so a swap never moves a lock with it |
| rendering | the transformed, weighed cursor delta | the model is fed the same numbers the wire would have carried, so a scale changes what it renders rather than what it corrects |
| injection | nothing the table wrote | injected motion drains into the axis after the pass; a remap that zeroed that axis does not take it with it |
The input classes of CATCH tap the physical report before either pass, so they still report the value the device sent. Its EMIT class is the mirror and carries what the clone actually put on the wire.
Lifecycle
A transform is PC-owned session stateremove a TRANSFORM with state = 0 and the entry's (source, dest) clear state 0 with both classes 0xFF and both ids 0xFFFF, a blanket silence ~1 s with no control-PC frame RESET a RESET command link loss the inter-chip link drops detach the real device goes away re-clone the box binds a device again
Any valid frame resets the silence timer, so a keepalive holds the table open. The host library re-asserts the whole table after a device-side blip and across a control-link reconnect, exactly as it does a LOCK.
The cross-class holds go before the entries do, so no key or media usage is left down once the entry that drove it is gone. Removing one entry releases only what that entry was holding.
RESET, a detach and a link drop take the table through the same release, alongside injection and locks.