LYNXcat Firmware
Keyboard half firmware. Scans matrices, reads sensors, transmits InputActions to tower via ESP-NOW.
Architecture Overview
┌─────────────────────────────────────────────────────────────┐
│ LYNXcat (LeftCat / RightCat) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Finger Slot │ │ Thumb Slot │ │ Bottom Slot │ │
│ │ │ │ │ │ │ │
│ │ Matrix 4×6 │ │ Matrix 4×3 │ │ MouseSensor │ │
│ │ ScrollWheel* │ │ Joystick* │ │ Gyro* │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └─────────────────┴─────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ EventGenerator │ │
│ │ (InputAction) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ TransmitterTask │ │
│ │ (ESP-NOW TX) │ │
│ └────────┬────────┘ │
│ │ │
└───────────────────────────┼────────────────────────────────┘
│ ESP-NOW (~0.5ms)
▼
┌──────────────┐
│ LYNXtower │
└──────────────┘
* Variant-dependent components
Firmware Layers
HAL Layer (esp-hal)
Direct hardware access.
- GPIO: Matrix scanning (input/output pins)
- Encoder: Quadrature decoder for scroll wheel
- SPI: ADNS5050 mouse sensor (bit-bang)
- ADC: Joystick analog input (future)
Driver Layer
Component-specific drivers. Reusable across slots.
MatrixDriver<ROWS, COLS>
- Generic const-generic scanner
- 5ms debounce per key
- 1kHz scan rate
- Outputs: KeyPress, KeyRelease events with matrix indices
ScrollWheelDriver
- Quadrature encoder (2-phase Gray code)
- Hardware encoder peripheral
- Outputs: ScrollIncrement(delta) per detent
MouseSensor Trait
pub trait MouseSensor {
fn init(&mut self) -> Result<(), &'static str>;
fn read_motion(&mut self) -> MotionData; // Returns struct with delta_x, delta_y
fn sensor_name(&self) -> &'static str;
}
ADNS5050 Driver
- Implements MouseSensor
- Bit-bang SPI (~500kHz)
- 100Hz polling rate
- 16-bit register protocol
- Outputs: MouseMotion(dx, dy)
Event Generation Layer
Converts driver outputs to InputActions.
EventGenerator (event_gen.rs)
- Wraps driver events in InputAction enum (slot-local row/col)
- Adds FrameHeader (sender, slot, sequence_id)
- Enqueues InputFrame for transmission
- Key index calculation happens on tower side (not here)
Communication Layer
ESP-NOW wireless transmission.
ESP-NOW TX (comm.rs)
- Broadcast mode (no pairing)
- Peer: tower MAC address (hardcoded)
- Queue: 10-slot channel (StaticCell)
- Transmitter task dequeues + sends
- No ACK required (fire-and-forget)
Source Files
firmware/cat/
├── bin/
│ └── main.rs # Entry point, task spawning
├── config.rs # Feature flag validation, pin definitions
├── event_gen.rs # InputAction generation, frame wrapping
├── comm.rs # ESP-NOW peer setup, TX task
└── drivers/
├── matrix.rs # MatrixDriver<ROWS, COLS>
├── scrollwheel.rs # ScrollWheelDriver (encoder)
└── mouse/
├── mod.rs # MouseSensor trait
└── adns5050.rs # ADNS5050 driver (bit-bang SPI)
Embassy Tasks
All tasks are Embassy async tasks. Spawn order: drivers first, transmitter last.
finger_matrix_task
- Rate: 1kHz (1ms loop)
- Driver: MatrixDriver<4, 6>
- Outputs: KeyPress, KeyRelease (24 keys)
- Slot: Finger (0)
thumb_matrix_task
- Rate: 1kHz (1ms loop)
- Driver: MatrixDriver<4, 3>
- Outputs: KeyPress, KeyRelease (12 keys)
- Slot: Thumb (1)
scroll_wheel_task
- Rate: 1kHz (1ms loop)
- Feature:
fkwonly - Driver: ScrollWheelDriver
- Outputs: ScrollIncrement(delta)
- Slot: Finger (0)
mouse_sensor_task
- Rate: 100Hz (10ms loop)
- Feature:
bm-*only - Driver: ADNS5050 (MouseSensor trait)
- Outputs: MouseMotion(dx, dy)
- Slot: Bottom (2)
transmitter_task
- Rate: Event-driven (awaits queue)
- Input: InputFrame queue (32 slots)
- Operation: Dequeue → ESP-NOW send
- Latency: ~0.5ms per frame
Task Spawn Sequence
// main.rs (simplified)
let (tx_queue, rx_queue) = make_channel::<InputFrame>();
// Spawn driver tasks (all send to tx_queue)
spawner.spawn(finger_matrix_task(peripherals, tx_queue.clone())).ok();
spawner.spawn(thumb_matrix_task(peripherals, tx_queue.clone())).ok();
#[cfg(feature = "fkw")]
spawner.spawn(scroll_wheel_task(peripherals, tx_queue.clone())).ok();
#[cfg(feature = "bm-adns5050")]
spawner.spawn(mouse_sensor_task(peripherals, tx_queue.clone())).ok();
// Spawn transmitter (receives from rx_queue)
spawner.spawn(transmitter_task(esp_now, rx_queue)).ok();
Feature Flags & Variants
Cat firmware builds use Cargo features to select hardware variants.
Build command syntax:
./f cat-{l|r} <finger> <thumb> <bottom> <port_num>
Example:
./f cat-l fkw tk bn 0 # Linux: /dev/ttyACM0, macOS: first usbmodem
# LeftCat with FingerKeysWheel, ThumbKeys, BottomNone
Feature validation: config.rs ensures exactly one variant per slot is active at compile time.
Pin Configuration
Pin assignments vary by slot and variant. All defined in config.rs.
Example (Finger slot):
// Matrix: rows = inputs (pull-up), cols = outputs
// Rows: GPIO38, GPIO37, GPIO36, GPIO35
// Cols: GPIO1, GPIO2, GPIO42, GPIO41, GPIO40, GPIO39
// Scroll wheel (fkw only)
// Channel A: GPIO45, Channel B: GPIO48
See hardware/docs/pins-connections.md for complete pin tables.
Debounce Strategy
Matrix keys: 5ms software debounce
- State change detected → start 5ms timer
- Confirm state after timer expires
- Prevents spurious events from mechanical bounce
Scroll wheel: Hardware encoder peripheral (no debounce needed)
Mouse sensor: Optical (no debounce needed)
Scan Rates & Latency
| Component | Scan Rate | Latency (typ) | Latency (max) |
|---|---|---|---|
| Matrix | 1kHz | 1ms | 6ms (with debounce) |
| Scroll Wheel | 1kHz | 1ms | 2ms |
| Mouse Sensor | 100Hz | 10ms | 20ms |
| ESP-NOW TX | Event-driven | 0.5ms | 1ms |
Total latency (key press): 1–7ms (matrix) + 0.5ms (TX) = 1.5–7.5ms
Memory Usage
Approximate (cat-l-fkw-tk-bm-adns5050 build):
- Flash: ~250KB (ESP32-S3 has 8MB)
- RAM: ~60KB (ESP32-S3 has 512KB)
- Queue: 32 × 24 bytes = 768 bytes (InputFrame queue)
Next Steps
Testing:
- Flash cat firmware with desired features
- Flash tower in
testmode for logging - Verify InputActions in tower logs
- Switch tower to
rmkmode for USB HID
See also:
- Tower firmware:
firmware/docs/tower.md - Input frames:
/home/ad/dev/lynx-v4/docs/architecture/input-frames.md - Input actions:
/home/ad/dev/lynx-v4/docs/architecture/input-actions.md