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: fkw only
  • 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

ComponentScan RateLatency (typ)Latency (max)
Matrix1kHz1ms6ms (with debounce)
Scroll Wheel1kHz1ms2ms
Mouse Sensor100Hz10ms20ms
ESP-NOW TXEvent-driven0.5ms1ms

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:

  1. Flash cat firmware with desired features
  2. Flash tower in test mode for logging
  3. Verify InputActions in tower logs
  4. Switch tower to rmk mode 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