Vial Islands

How physical keyboard slots map to visual groupings in Vial GUI.

Introduction

Island = Slot

Each physical hardware slot on a LYNXcat appears as a visual "island" in the Vial keyboard layout. The island metaphor makes the hardware-to-GUI relationship clear:

  • Left Finger Island → Slot 0 on LeftCat
  • Left Thumb Island → Slot 1 on LeftCat
  • Right Finger Island → Slot 0 on RightCat
  • Right Thumb Island → Slot 1 on RightCat

Understanding islands is critical for:

  • Debugging key index mismatches
  • Understanding how physical keypresses reach the host
  • Configuring Vial layouts correctly
  • Reasoning about the coordinate transformations

The Bridge: Hardware → RMK → Vial

Data flows through multiple transformations:

┌──────────────┐
│ HARDWARE     │  Physical key matrix with slot-local (row, col)
│ (LYNXcat)    │  Example: Finger slot key at position (1, 2)
└──────┬───────┘
       │
       ↓ Matrix scanning
┌──────────────┐
│ CAT FIRMWARE │  InputAction with slot-local coordinates
│              │  KeyPress { row: 1, col: 2 }
└──────┬───────┘
       │
       ↓ Wrap in InputFrame
┌──────────────┐
│ ESP-NOW WIRE │  InputFrame with (device, slot, row, col)
│              │  Header { sender: LeftCat, slot: Finger }
│              │  Payload { KeyPress { row: 1, col: 2 } }
└──────┬───────┘
       │
       ↓ ~0.5ms transmission
┌──────────────┐
│ TOWER / RMK  │  Converts to linear key index using DirectPin mode
│              │  index = 0 + 0 + (1 × 6) + 2 = 8
└──────┬───────┘
       │
       ↓ Keymap lookup
┌──────────────┐
│ VIAL GUI     │  Displays key at index 8 in the visual layout
│              │  Shows it in the Left Finger Island
└──────────────┘

Key concept: DirectPin mode flattens the matrix. RMK treats each key as a direct pin with a unique index [0-71], rather than preserving the 2D matrix structure.

Basic Setup Islands (72 matrix positions)

The basic setup uses 4 islands with 72 total matrix positions. Not all positions have physical keys—approximately 62 are actual switches; the rest are empty matrix slots marked as "no key" in vial.json.

Left Finger Island (Indices 0-23)

Slot: Finger (0), Device: LeftCat Matrix: 4 rows × 6 cols Index Range: 0-23

     Row 0 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 00 │ 01 │ 02 │ 03 │ 04 │ 05 │
                │0,0 │0,1 │0,2 │0,3 │0,4 │0,5 │
                │ r1 │ m1 │ i1 │    │    │    │
                └────┴────┴────┴────┴────┴────┘

     Row 1 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 06 │ 07 │ 08 │ 09 │ 10 │ 11 │
                │1,0 │1,1 │1,2 │1,3 │1,4 │1,5 │
                │ i5 │ i2 │ m2 │ r2 │ p1 │    │
                └────┴────┴────┴────┴────┴────┘

     Row 2 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 12 │ 13 │ 14 │ 15 │ 16 │ 17 │
                │2,0 │2,1 │2,2 │2,3 │2,4 │2,5 │
                │ i6 │ i3 │ m3 │ r3 │ p2 │ p4 │
                └────┴────┴────┴────┴────┴────┘

     Row 3 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 18 │ 19 │ 20 │ 21 │ 22 │ 23 │
                │3,0 │3,1 │3,2 │3,3 │3,4 │3,5 │
                │ i7 │ i4 │ m4 │ r4 │ p3 │ p5 │
                └────┴────┴────┴────┴────┴────┘

Legend:
  Top:    Vial key index (0-23)
  Middle: Matrix coordinate (row, col)
  Bottom: Physical label from PCB

Physical Stagger (in Vial GUI):

          r1  m1  i1
       p1 r2  m2  i2  i5
    p4 p2 r3  m3  i3  i6
    p5 p3 r4  m4  i4  i7

Left Thumb Island (Indices 24-35)

Slot: Thumb (1), Device: LeftCat Matrix: 4 rows × 3 cols Index Range: 24-35

     Row 0 ──→  ┌────┬────┬────┐
                │ 24 │ 25 │ 26 │
                │0,0 │0,1 │0,2 │
                │    │t33 │t25 │
                └────┴────┴────┘

     Row 1 ──→  ┌────┬────┬────┐
                │ 27 │ 28 │ 29 │
                │1,0 │1,1 │1,2 │
                │t11 │t23 │t15 │
                └────┴────┴────┘

     Row 2 ──→  ┌────┬────┬────┐
                │ 30 │ 31 │ 32 │
                │2,0 │2,1 │2,2 │
                │t22 │t13 │t24 │
                └────┴────┴────┘

     Row 3 ──→  ┌────┬────┬────┐
                │ 33 │ 34 │ 35 │
                │3,0 │3,1 │3,2 │
                │t12 │    │t14 │
                └────┴────┴────┘

Physical Cluster (in Vial GUI):

       t21 t11
          t22 t12
    t33 t23 t13
        t24 t14
        t25 t15

Note: The matrix is rectangular (4×3), but not all positions have physical keys in the actual hardware. Some indices may be marked as "no key" in vial.json.

Right Finger Island (Indices 36-59)

Slot: Finger (0), Device: RightCat Matrix: 4 rows × 6 cols Index Range: 36-59

     Row 0 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 36 │ 37 │ 38 │ 39 │ 40 │ 41 │
                │0,0 │0,1 │0,2 │0,3 │0,4 │0,5 │
                │    │    │    │ i1 │ m1 │ r1 │
                └────┴────┴────┴────┴────┴────┘

     Row 1 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 42 │ 43 │ 44 │ 45 │ 46 │ 47 │
                │1,0 │1,1 │1,2 │1,3 │1,4 │1,5 │
                │    │ i2 │ m2 │ r2 │ p1 │    │
                └────┴────┴────┴────┴────┴────┘

     Row 2 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 48 │ 49 │ 50 │ 51 │ 52 │ 53 │
                │2,0 │2,1 │2,2 │2,3 │2,4 │2,5 │
                │    │ i3 │ m3 │ r3 │ p2 │ p4 │
                └────┴────┴────┴────┴────┴────┘

     Row 3 ──→  ┌────┬────┬────┬────┬────┬────┐
                │ 54 │ 55 │ 56 │ 57 │ 58 │ 59 │
                │3,0 │3,1 │3,2 │3,3 │3,4 │3,5 │
                │    │ i4 │ m4 │ r4 │ p3 │ p5 │
                └────┴────┴────┴────┴────┴────┘

Physical Stagger (in Vial GUI):

    i1  m1  r1
    i2  m2  r2  p1
    i3  m3  r3  p2  p4
    i4  m4  r4  p3  p5

Note: Right finger uses fkw variant (scroll wheel), so i5/i6/i7 columns are not populated with keys. The scroll wheel generates ScrollIncrement actions, not key indices.

Right Thumb Island (Indices 60-71)

Slot: Thumb (1), Device: RightCat Matrix: 4 rows × 3 cols Index Range: 60-71

     Row 0 ──→  ┌────┬────┬────┐
                │ 60 │ 61 │ 62 │
                │0,0 │0,1 │0,2 │
                │t21 │t33 │t25 │
                └────┴────┴────┘

     Row 1 ──→  ┌────┬────┬────┐
                │ 63 │ 64 │ 65 │
                │1,0 │1,1 │1,2 │
                │t11 │t23 │t15 │
                └────┴────┴────┘

     Row 2 ──→  ┌────┬────┬────┐
                │ 66 │ 67 │ 68 │
                │2,0 │2,1 │2,2 │
                │t22 │t13 │t24 │
                └────┴────┴────┘

     Row 3 ──→  ┌────┬────┬────┐
                │ 69 │ 70 │ 71 │
                │3,0 │3,1 │3,2 │
                │t12 │    │t14 │
                └────┴────┴────┘

Physical Cluster (in Vial GUI):

    t11 t21
    t12 t22
    t13 t23 t33
    t14 t24
    t15 t25

Combined Archipelago View

All 4 islands together showing approximate physical key positions. Islands arranged horizontally as they appear in Vial:

┌──────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│                                      LYNX-V4 VIAL ARCHIPELAGO (Basic Setup)                              │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────┘

 LEFT FINGER (0-23)      LEFT THUMB (24-35)     RIGHT THUMB (60-71)     RIGHT FINGER (36-59)
 ╔═════════════════╗     ╔══════════════╗       ╔══════════════╗       ╔═════════════════╗
 ║ 03 02 01 00     ║     ║  27  24      ║       ║  60  63      ║       ║     39 38 37    ║
 ║ r1 m1 i1        ║     ║  t11 t21     ║       ║  t21 t11     ║       ║     i1 m1 r1    ║
 ║                 ║     ║              ║       ║              ║       ║                 ║
 ║ 09 08 07 06  10 ║     ║  33  30      ║       ║  66  69      ║       ║  43 44 45 46    ║
 ║ i2 m2 r2 p1  i5 ║     ║  t12 t22     ║       ║  t22 t12     ║       ║  i2 m2 r2 p1    ║
 ║                 ║     ║              ║       ║              ║       ║                 ║
 ║ 13 14 15 16  17 ║     ║  25  28  31  ║       ║  61  64  67  ║       ║  49 50 51 52 53 ║
 ║ i3 m3 r3 p2  p4 ║     ║  t33 t23 t13 ║       ║  t33 t23 t13 ║       ║  i3 m3 r3 p2 p4 ║
 ║                 ║     ║              ║       ║              ║       ║                 ║
 ║ 19 20 21 22  23 ║     ║  32  29      ║       ║  68  71      ║       ║  55 56 57 58 59 ║
 ║ i4 m4 r4 p3  p5 ║     ║  t24 t14     ║       ║  t24 t14     ║       ║  i4 m4 r4 p3 p5 ║
 ║                 ║     ║              ║       ║              ║       ║                 ║
 ║ 18              ║     ║  26  35      ║       ║  62  65      ║       ║                 ║
 ║ i7              ║     ║  t25 t15     ║       ║  t25 t15     ║       ║                 ║
 ╚═════════════════╝     ╚══════════════╝       ╚══════════════╝       ╚═════════════════╝

Index Calculation Formula

Converting a 4-tuple (device, slot, row, col) to a linear Vial key index:

index = device_base + slot_offset + (row × cols) + col

Lookup Tables

Device Base Offsets:

DeviceRoledevice_baseNotes
LeftCat0Indices 0-35
RightCat36Indices 36-71
Center72Reserved for future expansion
LeftAux108Reserved for future expansion
RightAux132Reserved for future expansion
Extra1156Reserved for future expansion
Extra2180Reserved for future expansion

Slot Offsets (per device):

ModuleSlotslot_offsetcolsNotes
Finger (0)06Always present
Thumb (1)243Always present
Bottom (2)--Sensors only, no key indices

Calculation Examples

Example 1: Left p1 key (physical label)

  • Device: LeftCat → device_base = 0
  • Slot: Finger → slot_offset = 0, cols = 6
  • Matrix: (row=1, col=4) from electrical scanning
  • Calculation: 0 + 0 + (1 × 6) + 4 = 10
  • Vial Index: 10

Example 2: Right t13 key

  • Device: RightCat → device_base = 36
  • Slot: Thumb → slot_offset = 24, cols = 3
  • Matrix: (row=2, col=1)
  • Calculation: 36 + 24 + (2 × 3) + 1 = 67
  • Vial Index: 67

Example 3: Left i3 key

  • Device: LeftCat → device_base = 0
  • Slot: Finger → slot_offset = 0, cols = 6
  • Matrix: (row=2, col=1)
  • Calculation: 0 + 0 + (2 × 6) + 1 = 13
  • Vial Index: 13

Vial JSON Connection

The vial.json configuration uses a 12×6 VirtualMatrix for RMK:

{
  "matrix": {
    "rows": 12,
    "cols": 6
  }
}

This maps the 4-island archipelago into a 12×6 virtual matrix:

  • Rows 0-3: LeftCat Finger (4×6 = 24 keys)
  • Rows 4-5: LeftCat Thumb (4×3 compacted to 2×6)
  • Rows 6-9: RightCat Finger (4×6 = 24 keys)
  • Rows 10-11: RightCat Thumb (4×3 compacted to 2×6)

Why VirtualMatrix?

Traditional keyboard firmware scans a single physical matrix. RMK on the tower aggregates input from multiple wireless devices. VirtualMatrix maps physical coordinates from each device/slot to a unified 12×6 matrix that Vial can render properly.

Consequences:

  • Each key has coordinates (row, col) in the 12×6 virtual space
  • No matrix ghosting concerns (each key is independent)
  • Island structure is logical, not electrical
  • Firmware computes virtual coordinates via physical_to_virtual()

CRITICAL: vial.json Format Requirements

LZMA Compression: The vial.json is compressed using LZMA (Lempel-Ziv-Markov chain Algorithm) at build time. This allows the ~2KB JSON to fit in firmware flash as ~200 bytes. The Vial GUI decompresses it to render the layout.

IMPORTANT: Use Only Minimal KLE Properties

The vial.json uses KLE (Keyboard Layout Editor) format. Only these properties are reliably supported:

PropertySupportedExample
"c"YES{"c": "#6b7fd4"} (background color)
"x"YES{"x": 2} (horizontal offset)
"t"NO{"t": "#ffffff"} (text color) - BREAKS LAYOUT
"a"NO{"a": 7} (alignment) - BREAKS LAYOUT
"y"AVOID{"y": 0} (vertical offset) - use implicit positioning

What Breaks Vial:

// BAD - These extra properties cause empty layout in Vial!
[{"y": 0, "c": "#6b7fd4", "t": "#ffffff", "a": 7}, "0,0", ...]

// GOOD - Minimal properties only
[{"c": "#6b7fd4"}, "0,0", "0,1", "0,2", ...]

Key String Format:

// GOOD - Simple row,col format
"0,0", "0,1", "0,2"

// ALSO OK - With label (but adds file size)
"0,0\n\n\n\n\n\nLF00"

Debugging Vial Issues:

If Vial shows empty layout:

  1. Check for "t", "a", or explicit "y": 0 properties - REMOVE THEM
  2. Verify matrix dimensions match code (VIRT_ROWS, VIRT_COLS)
  3. Check build output for compression size warnings
  4. Compare with working test-examples/vial-extended.json

CRITICAL: Matrix Tester Requires vial_lock Feature

Symptom: Keys work (text appears in editor) but Matrix Tester shows nothing.

Root Cause: RMK only updates internal matrix state when vial_lock feature is enabled:

// Only compiled when vial_lock is enabled:
#[cfg(feature = "vial_lock")]
self.keymap.borrow_mut().matrix_state.update(&event);

Without this, Matrix Tester cannot see which keys are pressed.

Fix: Enable vial_lock in Cargo.toml:

# WRONG - Matrix Tester won't work!
rmk = { ..., features = ["storage", "vial"] }

# CORRECT - Matrix Tester works!
rmk = { ..., features = ["storage", "vial", "vial_lock"] }

Why it's separate: The vial_lock feature adds security (unlock keys required) and the matrix state tracking overhead. Projects not using Matrix Tester can disable it to save memory.

Reference: RMK Issue #131 - Matrix Tester was implemented in v0.8.0.

CRITICAL: Unlock Keys Must Be On Different Rows

Symptom: Pressing each unlock key individually shows 50% progress, but pressing both together still shows only 50%.

Root Cause: When two unlock keys are on the same physical matrix row, simultaneous key presses can cause matrix scanning conflicts (ghosting). The matrix scanner may only report one key even when both are physically pressed.

Fix: Configure unlock keys on different rows:

// WRONG - Both keys on same row (row 0) - causes 50% stuck!
VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 1), (0, 2)])

// CORRECT - Keys on different rows - unlock works!
VialConfig::new(VIAL_KEYBOARD_ID, VIAL_KEYBOARD_DEF, &[(0, 1), (3, 1)])

Best Practice: Choose unlock keys that are:

  1. On different physical rows (different row coordinate)
  2. Easy to press simultaneously (e.g., same column on different rows)
  3. Not frequently used during normal typing

Example (LYNXtower):

  • (0, 1) = Li4 (bottom row, index finger)
  • (3, 1) = Li1 (top row, index finger)

Same column, different rows - easy to press with one finger sliding vertically.

Island Expansion (Future)

The architecture supports up to 204 keys across 7 devices:

DeviceFinger IndicesThumb IndicesTotal
LeftCat0-2324-3536
RightCat36-5960-7136
Center72-9596-10736
LeftAux108-131(Finger only)24
RightAux132-155(Finger only)24
Extra1156-179(Finger only)24
Extra2180-203(Finger only)24

Each new device adds 1-2 islands to the archipelago.

Debugging with Islands

Symptom: Key press on Left i3 sends wrong keycode Diagnosis Steps:

  1. Check cat logs for matrix coordinate → should see (2, 1)
  2. Check tower logs for received index → should see 13
  3. Check vial.json layout → index 13 should be in Left Finger island
  4. Check keymap configuration in Vial GUI → verify index 13 assignment

Common Issues:

  • Off-by-one: Check if row/col are 0-indexed vs 1-indexed
  • Wrong island: Verify device_base and slot_offset in calculation
  • Matrix mismatch: Ensure matrix dimensions (rows × cols) match hardware
  • Key Mapping: /home/ad/dev/lynx-v4/firmware/docs/key-mapping.md
  • Slots & Variants: /home/ad/dev/lynx-v4/firmware/docs/slots-&-variants.md
  • Input Frames: /home/ad/dev/lynx-v4/docs/architecture/input-frames.md
  • Vial Config: /home/ad/dev/lynx-v4/firmware/tower/vial.json
  • RMK Docs: https://rmk.rs/