Non-Key Mapping
How non-keyboard inputs (mouse, scroll, joystick, gyroscope) flow through the system.
Overview
Unlike keys, non-key inputs bypass the keymap entirely. They use direct HID field mapping instead of index→keymap lookup.
Physical Sensor → Driver → InputAction → ESP-NOW → Accumulate/Process → HID Report Field
[Cat] [Cat] [Cat] [Wire] [Tower] [Host]
Key Differences from Keys:
- Keys: Binary state (press/release), on-change only, RMK keymap lookup
- Non-keys: Continuous stream, polling-based, direct HID field population
Input Types
| Input | Slot/Variant | Data Type | InputAction | Cat Poll Rate |
|---|---|---|---|---|
| Mouse Sensor | Bottom (bm) | Delta X/Y (i8, i8) | MouseMotion { delta } | ~500Hz |
| Scroll Wheel | Finger (fkw) | Delta (i8) | ScrollIncrement { delta } | On-detent |
| Joystick | Thumb (tkj) | Position X/Y (i16, i16) | JoystickPosition { position } | ~100Hz |
| Gyroscope | Bottom (bg) | Angular velocity (i16, i16, i16) | GyroMotion { angular_velocity } | ~100Hz |
Processing Model
Keys vs Non-Keys
| Aspect | Keys | Non-Keys |
|---|---|---|
| State | Binary (press/release) | Continuous stream |
| Lookup | Index → Keymap → Keycode | Direct HID field |
| Rate | On-change only | Polling-based |
| Accumulation | N/A | Deltas sum between USB polls |
| Layer interaction | Full layer support | Limited/None |
| Wire format | KeyPress/Release { row, col } | MouseMotion/ScrollIncrement/etc. |
Cat-Side vs Tower-Side
Cat (LYNXcat):
- Sensor polling at hardware rate
- Driver-level conversion (raw → digital)
- InputAction generation
- ESP-NOW transmission
Tower (LYNXtower):
- Channel routing by InputAction type
- Accumulation (mouse/scroll deltas)
- Scaling/sensitivity adjustment
- Mode switching (joystick)
- HID report field population
Data Flow: Mouse Sensor
ADNS-5050 SPI → MouseDriver → MouseMotion{delta:(i8,i8)} → ESP-NOW → Tower Accumulator → Mouse HID Report
[Cat] [Cat] [Cat] [Wire] [Tower] [Host]
Cat side:
- SPI read motion registers at ~500Hz
- Extract dx, dy as i8 (-127 to +127)
- Generate
InputAction::MouseMotion { delta: (dx, dy) } - Create InputFrame with
slot=Bottom, sender=RightCat - Transmit via ESP-NOW
Tower side:
- Receive InputFrame, route to mouse channel
- Accumulate deltas in i16 (prevent overflow)
- Apply sensitivity scaling (future: DPI setting)
- On USB poll (125Hz), send HID mouse report
- Reset accumulator
USB HID Mouse Report Fields:
struct MouseReport {
buttons: u8, // Bits 0-7 for mouse buttons
x: i8, // Horizontal movement (-127 to +127)
y: i8, // Vertical movement (-127 to +127)
wheel: i8, // Scroll wheel (-127 to +127)
pan: i8, // Horizontal scroll (optional)
}
Mouse sensor populates x and y fields.
Button State (Cross-Pipeline Coordination):
Mouse buttons use keyboard keycodes (MouseBtn1-5) mapped in keymap. VirtualMatrix (keyboard pipeline) detects these by querying keymap on each key event. On detection, updates MOUSE_BUTTON_STATE: AtomicU8 shared with pointer_processor. PointerProcessor reads this atomic at 125Hz when building reports. This ensures button state and motion appear in same USB report, enabling drag operations. Button state persists until key release (unlike motion which resets each report).
Data Flow: Scroll Wheel
Encoder Rotation → ScrollWheelDriver → ScrollIncrement{delta:i8} → ESP-NOW → Tower Accumulator → Mouse HID Wheel
[Cat] [Cat] [Cat] [Wire] [Tower] [Host]
Cat side:
- GPIO encoder detects rotation direction
- On each detent (physical click), emit
ScrollIncrement { delta: ±1 } - Create InputFrame with
slot=Finger, sender=RightCat - Transmit via ESP-NOW
Tower side:
- Receive InputFrame, route to pointer channel
- Accumulate scroll delta with motion deltas
- On USB poll (125Hz), send HID mouse report
- Reset accumulator
Notes:
- Scroll wheel shares pointer channel with MouseMotion
- Populates
wheelfield in Mouse HID Report - Direction: positive = scroll up, negative = scroll down
- Right finger variant
fkwreplaces i5/i6/i7 key columns with encoder - Scroll-only setups work correctly (scroll wakes processor from idle)
Data Flow: Joystick
ADC X/Y → JoystickDriver → JoystickPosition{position:(i16,i16)} → ESP-NOW → Mode Handler → HID Report
[Cat] [Cat] [Cat] [Wire] [Tower] [Host]
Cat side:
- ADC reads X/Y axes at ~100Hz
- Convert to centered range: 0 = center, ±32767 = max deflection
- Generate
InputAction::JoystickPosition { position: (x, y) } - Create InputFrame with
slot=Thumb, sender=LeftCat/RightCat - Transmit via ESP-NOW
Tower side (mode-dependent):
Joystick can operate in three modes:
| Mode | Processing | HID Report | Use Case |
|---|---|---|---|
| Mouse | Position → velocity → cumulative motion | Mouse Report (x, y) | Cursor control |
| Scroll | Position → scroll speed | Mouse Report (wheel) | Fast document navigation |
| Key | Threshold detection (4 directions) | Keyboard Report (arrow keys) | Menu navigation |
Mouse Mode (default):
- Convert stick position to velocity (distance from center)
- Apply dead zone (ignore small movements)
- Generate motion deltas based on velocity
- Accumulate into mouse x/y
- Send Mouse HID Report
Scroll Mode:
- Y-axis position → scroll speed
- Faster deflection = more scroll per frame
- Send Mouse HID Report (wheel field)
Key Mode:
- Apply threshold to each axis (e.g., ±50%)
- Generate discrete key presses for 4 directions
- Inject KeyPress/KeyRelease as virtual events
- Send Keyboard HID Report
Configuration (conceptual):
- Dead zone threshold (e.g., 10% center)
- Sensitivity scaling
- Mode selection (compile-time or runtime)
Data Flow: Gyroscope
IMU I2C → GyroDriver → GyroMotion{angular_velocity:(i16,i16,i16)} → ESP-NOW → Mode Handler → HID Report
[Cat] [Cat] [Cat] [Wire] [Tower] [Host]
Cat side:
- I2C read angular velocity (pitch, yaw, roll) at ~100Hz
- Units: degrees/second scaled to i16
- Generate
InputAction::GyroMotion { angular_velocity: (pitch, yaw, roll) } - Create InputFrame with
slot=Bottom, sender=LeftCat - Transmit via ESP-NOW
Tower side:
- Receive InputFrame, route to sensor channel
- Apply mode logic (implementation-dependent):
- Tilt mode: Pitch/roll → mouse X/Y (continuous motion)
- Gesture mode: Sharp rotation → key injection (flick to switch layers)
- Camera mode: Yaw/pitch → mouse X/Y (FPS games)
- Generate appropriate HID report
Coordinate System:
- Pitch: rotation around X-axis (tilt forward/back)
- Yaw: rotation around Y-axis (turn left/right)
- Roll: rotation around Z-axis (tilt sideways)
Current Status: MVP logs only, no HID generation yet.
USB HID Reports
Keyboard Report (Keys Only)
struct KeyboardReport {
modifiers: u8, // Ctrl, Shift, Alt, GUI bits
reserved: u8, // Always 0
keycodes: [u8; 6], // 6-key rollover (NKRO optional)
}
- Populated by RMK keymap lookup
- Non-key inputs do NOT use this report (except joystick key mode)
Mouse Report (Non-Keys)
struct MouseReport {
buttons: u8, // Mouse button bits
x: i8, // Horizontal motion (-127 to +127)
y: i8, // Vertical motion (-127 to +127)
wheel: i8, // Scroll wheel (-127 to +127)
pan: i8, // Horizontal scroll (optional)
}
x, y: Mouse sensor, joystick (mouse mode), gyro (tilt mode)wheel: Scroll wheel, joystick (scroll mode)buttons: Mouse button keycodes from keymap (cross-pipeline coordination via atomic)
Example Trace: Mouse Motion
Scenario: ADNS-5050 detects rightward movement on RightCat.
- Cat: SPI read returns dx=+5, dy=-3 (5 counts right, 3 counts up)
- Cat: Generate
InputAction::MouseMotion { delta: (5, -3) } - Cat: Create
InputFramewithsender=RightCat, slot=Bottom, sequence=1234 - Wire: Serialize with postcard (~12 bytes), transmit via ESP-NOW (~0.5ms)
- Tower: Receive frame, deserialize, route to mouse channel
- Tower: Accumulate: dx_total += 5, dy_total += -3
- Tower: Wait for USB poll interval (8ms at 125Hz)
- Tower: Additional frames arrive, accumulate dx=+3, dy=-2
- Tower: USB poll triggers: dx_total=8, dy_total=-5
- Tower: Apply sensitivity scaling (e.g., 1.5x): dx=12, dy=-7
- Tower: Generate Mouse HID Report with x=12, y=-7, wheel=0
- Tower: Reset accumulator to zero
- Host: OS receives report, moves cursor 12px right, 7px up
Coordinate Systems
| Layer | Mouse | Scroll | Joystick | Gyroscope |
|---|---|---|---|---|
| Physical | Sensor orientation | Rotation CW/CCW | Stick deflection | Board tilt |
| Electrical | SPI motion registers | Encoder pulses | ADC voltage (0-3.3V) | I2C registers |
| Digital (Cat) | MouseMotion{delta:(i8,i8)} | ScrollIncrement{delta:i8} | JoystickPosition{position:(i16,i16)} | GyroMotion{angular_velocity:(i16,i16,i16)} |
| Wire | InputFrame + FrameHeader | InputFrame + FrameHeader | InputFrame + FrameHeader | InputFrame + FrameHeader |
| Digital (Tower) | Accumulated i16, scaled | Accumulated i8 | Mode-processed | Mode-processed |
| Host | HID mouse x,y (-127 to +127) | HID wheel (-127 to +127) | HID mouse/keys (mode-dependent) | HID mouse/keys (mode-dependent) |
Key Observations:
- Cat uses fixed-width integer types matched to sensor ranges
- Wire protocol preserves exact values (no loss)
- Tower performs accumulation in wider types (i8→i16 for mouse)
- Host sees final 8-bit HID values (clamped if needed)
Timing & Accumulation
Mouse Sensor
Cat Poll Rate: ~500Hz (2ms intervals)
- High-frequency polling for responsive cursor
- Each poll generates InputAction (even if dx=0, dy=0)
Tower Accumulation Window: 8ms (125Hz USB poll)
- Up to 4 motion frames can accumulate per report
- Example: 4 frames of (dx=2, dy=1) → HID report (x=8, y=4)
Latency Budget:
| Stage | Time | Cumulative |
|---|---|---|
| Sensor poll | 2ms | 2ms |
| InputAction gen | <0.1ms | 2.1ms |
| ESP-NOW | 0.5ms | 2.6ms |
| Tower routing | 0.5ms | 3.1ms |
| USB poll (worst) | 8ms | 11.1ms |
| USB poll (best) | 1ms | 4.1ms |
Observed: 4-11ms sensor-to-cursor.
Scroll Wheel
Cat Event Rate: On-detent only
- Physical encoder has ~24 detents per rotation
- Only generates InputAction when detent clicks
Tower Accumulation: Same 8ms window
- Multiple rapid scrolls within 8ms sum together
- Fast scroll: 3 detents in 8ms → HID wheel=3
Joystick
Cat Poll Rate: ~100Hz (10ms intervals)
- Lower rate acceptable for analog control
- Position reported continuously (even if not moving)
Tower Processing: Mode-dependent
- Mouse mode: generates motion every USB poll
- Scroll mode: generates scroll every USB poll
- Key mode: generates KeyPress on threshold cross
Gyroscope
Cat Poll Rate: ~100Hz (10ms intervals)
- IMU reports angular velocity
- Only transmit if velocity exceeds threshold (reduce wireless traffic)
Tower Processing: MVP logs only
- Future: Apply sensitivity, dead zone, mode logic
Configuration Parameters
Current implementation is hardcoded. Future enhancements:
| Parameter | Input | Purpose | Default |
|---|---|---|---|
| DPI/Sensitivity | Mouse | Scale motion magnitude | 1.0x |
| Dead zone | Joystick, Gyro | Ignore noise near zero | 10% |
| Scroll direction | Scroll | Invert natural/reverse | Normal |
| Joystick mode | Joystick | Mouse/Scroll/Key | Mouse |
| Gyro axis map | Gyro | Which axes control X/Y | Yaw→X, Pitch→Y |
| Poll rate | All | Cat-side sampling freq | Per-sensor |
Configuration Storage:
- Compile-time: Cargo features (current)
- Runtime: Vial GUI settings (future)
Relationship to RMK
RMK is keyboard-focused. Non-key inputs bypass most RMK logic:
RMK Handles:
- Keyboard HID report generation
- Keymap lookups (keys only)
- Layer switching (triggered by keys)
- Macros (triggered by keys)
Tower Custom Handlers:
- Pointer processing (motion + scroll accumulation) (
mouse_processor.rs) - Sensor processing (
sensor_handler.rs)
Hybrid Case: Joystick Key Mode
- Joystick generates virtual KeyPress/KeyRelease
- Injected into RMK's event queue
- RMK processes like normal keys (supports layers, macros)
Related Documentation
- Key mapping (complementary):
firmware/docs/key-mapping.md - Event pipeline (complete flow):
firmware/docs/event-pipeline.md - InputAction definitions:
firmware/common/src/input_actions.rs - Pointer processor implementation:
firmware/tower/src/pipelines/mouse_processor.rs - Sensor handler implementation:
firmware/tower/src/pipelines/sensor_handler.rs - Cat event generation:
firmware/cat/src/event_gen.rs