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:
| DeviceRole | device_base | Notes |
|---|---|---|
| LeftCat | 0 | Indices 0-35 |
| RightCat | 36 | Indices 36-71 |
| Center | 72 | Reserved for future expansion |
| LeftAux | 108 | Reserved for future expansion |
| RightAux | 132 | Reserved for future expansion |
| Extra1 | 156 | Reserved for future expansion |
| Extra2 | 180 | Reserved for future expansion |
Slot Offsets (per device):
| ModuleSlot | slot_offset | cols | Notes |
|---|---|---|---|
| Finger (0) | 0 | 6 | Always present |
| Thumb (1) | 24 | 3 | Always 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:
| Property | Supported | Example |
|---|---|---|
"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:
- Check for
"t","a", or explicit"y": 0properties - REMOVE THEM - Verify matrix dimensions match code (
VIRT_ROWS,VIRT_COLS) - Check build output for compression size warnings
- 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:
- On different physical rows (different row coordinate)
- Easy to press simultaneously (e.g., same column on different rows)
- 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:
| Device | Finger Indices | Thumb Indices | Total |
|---|---|---|---|
| LeftCat | 0-23 | 24-35 | 36 |
| RightCat | 36-59 | 60-71 | 36 |
| Center | 72-95 | 96-107 | 36 |
| LeftAux | 108-131 | (Finger only) | 24 |
| RightAux | 132-155 | (Finger only) | 24 |
| Extra1 | 156-179 | (Finger only) | 24 |
| Extra2 | 180-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:
- Check cat logs for matrix coordinate → should see (2, 1)
- Check tower logs for received index → should see 13
- Check vial.json layout → index 13 should be in Left Finger island
- 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
Related Documentation
- 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/