Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
a54dc3c
split: add a bounded application-message side channel
colonelpanic8 Jul 19, 2026
a1ce157
feat(lighting): add topology-aware lighting system
colonelpanic8 Jul 20, 2026
6dd5b85
feat(rynk): add native lighting control
colonelpanic8 Jul 20, 2026
66ed93a
feat(vial): add RGB Matrix lighting compatibility
colonelpanic8 Jul 20, 2026
424213e
fix(lighting): reserve processor event subscriptions
colonelpanic8 Jul 20, 2026
1354e66
fix(lighting): use const-safe slot assertion
colonelpanic8 Jul 20, 2026
21731f4
style(lighting): apply repository formatting
colonelpanic8 Jul 20, 2026
2a59f6b
feat(lighting): support split renderer replicas
colonelpanic8 Jul 21, 2026
9d91e5a
fix(rynk): use HID for reliable USB transport
colonelpanic8 Jul 20, 2026
ddf9509
feat(rynk): add application build info
colonelpanic8 Jul 20, 2026
757ceb9
rynk: route split peripheral bootloader entry
colonelpanic8 Jul 21, 2026
087ff94
feat(rynk): make bootloader unlock policy configurable
colonelpanic8 Jul 21, 2026
ef94863
feat(rynk): define the lighting scene wire surface
colonelpanic8 Jul 21, 2026
2e701ed
feat(lighting): add runtime-configurable per-layer scenes over Rynk
colonelpanic8 Jul 21, 2026
73eba27
feat(rynk): mirror the scene endpoints in the host client and wasm bi…
colonelpanic8 Jul 21, 2026
b424445
feat(split): size application queue for scene snapshots
colonelpanic8 Jul 21, 2026
831b099
rynk: stop minting protocol versions downstream
colonelpanic8 Jul 21, 2026
3f45cb6
feat(rynk): add complete lighting and layer readback
colonelpanic8 Jul 21, 2026
e4a4d76
feat(lighting): support board snapshot invalidation
colonelpanic8 Jul 21, 2026
8a6aad7
feat(rynk): expose resolved modifier state
colonelpanic8 Jul 21, 2026
64c06fd
feat(split): make central latency runtime configurable
colonelpanic8 Jul 21, 2026
017ac30
feat(lighting): add declarative conditional scenes
colonelpanic8 Jul 21, 2026
9ccf189
feat(lighting): configure board controls
colonelpanic8 Jul 21, 2026
8b20e31
feat(lighting): add declarative output modes
colonelpanic8 Jul 21, 2026
31b0008
feat(lighting): support local powered-only scope
colonelpanic8 Jul 21, 2026
02f0a01
feat(lighting): let extension sources claim light actions and add sta…
colonelpanic8 Jul 21, 2026
861b336
Validate standalone lighting expansion against the Voyager config
colonelpanic8 Jul 21, 2026
52f9e3c
feat(lighting): expose extension-source introspection to the engine
colonelpanic8 Jul 22, 2026
474b62c
feat(rynk): expose extension-effects discovery and control
colonelpanic8 Jul 22, 2026
0a59edf
Make lighting extension replication atomic
colonelpanic8 Jul 22, 2026
f5c3a18
feat(lighting): add runtime conditional scenes
colonelpanic8 Jul 22, 2026
610d885
fix(rynk): gate output mode dispatch on lighting
colonelpanic8 Jul 22, 2026
d5ddf8a
fix(split): detect a dead peripheral link within two seconds
colonelpanic8 Jul 22, 2026
defece4
test: regenerate rynk wire snapshots and protocol reference
colonelpanic8 Jul 28, 2026
a8d58b8
fix: adapt glove80 rynk/lighting stack to COBS framing
colonelpanic8 Jul 28, 2026
54c3b4b
feat(lighting): generic per-effect extension parameters over rynk
colonelpanic8 Jul 29, 2026
a14dec4
feat(lighting): host wrappers and replica carry extension parameters
colonelpanic8 Jul 29, 2026
2e56797
feat(lighting): persist the animated extension selection
colonelpanic8 Jul 29, 2026
fb96f89
feat(lighting): runtime conditional cells override compiled rules
colonelpanic8 Jul 30, 2026
8eb8911
feat(lighting): dense scene storage keyed by a style pool
colonelpanic8 Jul 30, 2026
771d219
feat(lighting): free scene styles by scanning, not counting
colonelpanic8 Jul 30, 2026
3528eb9
feat(lighting): intern scene effects to shrink the cell to 4 bytes
colonelpanic8 Jul 30, 2026
e35cad7
refactor(lighting): split standard.rs into a module directory
colonelpanic8 Jul 30, 2026
4cf2341
feat(lighting): gate conditional rules on the output mode
colonelpanic8 Jul 30, 2026
d4c370b
fix(lighting): tie the engine's conditional page size to the wire chunk
colonelpanic8 Jul 30, 2026
d6cf74c
fix(lighting): use core::assert in the const chunk-size check
colonelpanic8 Jul 30, 2026
747c642
feat(lighting): wake layers become a settable mask
colonelpanic8 Jul 30, 2026
bd2f328
feat(config): wake layers are a list resolving to a mask
colonelpanic8 Jul 30, 2026
32c3afb
fix(lighting): advertise output-mode support from the engine
colonelpanic8 Jul 30, 2026
45bb0e1
fix(rynk): gate wake-layer dispatch on lighting
colonelpanic8 Jul 30, 2026
aedbd87
feat(lighting): intern conditional rule styles
colonelpanic8 Jul 30, 2026
5a520fa
feat(config): support output-mode lighting conditions
colonelpanic8 Jul 30, 2026
3926168
refactor(config): derive lighting output default
colonelpanic8 Jul 30, 2026
a283147
fix(lighting): satisfy current clippy
colonelpanic8 Jul 30, 2026
b1aa796
fix(rynk): avoid cloning conditional cells
colonelpanic8 Jul 30, 2026
db23f54
fix(rynk): adapt the lighting stack to upstream's rebased rynk
colonelpanic8 Jul 31, 2026
85cb09a
test(rynk): restore the lighting loopback cases on the new harness
colonelpanic8 Jul 31, 2026
2401589
feat(lighting): support an extension overlay layer
colonelpanic8 Aug 3, 2026
13bb512
perf(lighting): group scene cells by layer
colonelpanic8 Aug 3, 2026
ed95206
feat(lighting): match conditional scenes on connection state
colonelpanic8 Aug 4, 2026
278838c
feat(rynk): add connection-aware lighting conditions
colonelpanic8 Aug 4, 2026
7716c7b
fix(config): account for lighting connection subscriber
colonelpanic8 Aug 4, 2026
e2a1c03
fix(rynk): bound connection profiles and guard storage downgrade
colonelpanic8 Aug 4, 2026
b539e12
fix(macro): emit the connection field in compiled condition sets
colonelpanic8 Aug 4, 2026
7506bb4
feat(lighting): expose last-rule condition amendment on the runtime t…
colonelpanic8 Aug 4, 2026
7e7bc68
feat(lighting): gate conditional rules on the extension being on
colonelpanic8 Aug 4, 2026
3deec12
feat(ble): track which profile slots hold a bond
colonelpanic8 Aug 4, 2026
802b3cf
feat(lighting): bonded-slot conditions on extended cells
colonelpanic8 Aug 4, 2026
082149a
feat(lighting): usb-connected condition on extended cells
colonelpanic8 Aug 4, 2026
4fd9048
test(rynk): cover the new connection condition fields
colonelpanic8 Aug 5, 2026
34217cf
fix(rynk): silence unused session outside lighting builds
colonelpanic8 Aug 5, 2026
f8bd809
feat(lighting): read back the frame the output presented
colonelpanic8 Aug 6, 2026
624f2c1
feat(rynk): add lighting frame and replica-status readback
colonelpanic8 Aug 6, 2026
6077ff4
feat(rynk): serve the frame and replica-status endpoints
colonelpanic8 Aug 6, 2026
da04121
feat(rynk): expose lighting replica attestation status
colonelpanic8 Aug 7, 2026
f5202f3
fix(split): satisfy collapsible_match on the app inbox arm
colonelpanic8 Aug 5, 2026
4fe4754
Merge pull request #9 from colonelpanic8/work/lighting-observability
colonelpanic8 Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/ci/_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,16 @@ RMK_FEATURESETS=(
"rynk,_ble,split,storage,async_matrix"
"rynk,storage"
"rynk"
"rynk,lighting"
"rynk,storage,lighting"
)

# Behavioral coverage only; RMK_FEATURESETS remains the compile/clippy matrix.
RMK_TEST_FEATURESETS=(
""
"vial,host_lock,_no_usb,steno,passkey_entry"
"rynk,_ble,split,async_matrix,storage"
"rynk,storage,lighting"
)

# Examples auto-discovery skiplist. Reasons:
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/main/docs/configuration/appendix.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,9 @@ split_peripherals_num = 0
ble_profiles_num = 3
# BLE Split Central sleep timeout in seconds (0 = disabled)
split_central_sleep_timeout_seconds = 0
# Maximum skipped split connection events while active (0 = lowest latency)
split_central_max_latency_powered = 30
split_central_max_latency_battery = 30
# Maximum macro data bytes in one Rynk macro request or response
protocol_macro_chunk_size = 64
# Rynk RX/TX buffer size in bytes. 488 bytes = 2*BLE maximum packet size
Expand Down Expand Up @@ -487,6 +490,8 @@ unlock_keys = [[0, 0], [0, 1]]
insecure = false
# Rynk only: move config writes into the locked tier (default: false).
write_requires_unlock = false
# Require physical unlock before central or split-peripheral bootloader entry.
bootloader_requires_unlock = true

# Chip-specific configuration
# To use the default configuration, ignore this section completely
Expand Down
5 changes: 5 additions & 0 deletions docs/docs/main/docs/configuration/host_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ insecure = false
# Rynk only: move config writes (SetKeyAction, SetMacro, …) into the locked
# tier so they also require unlock (default: false).
write_requires_unlock = false

# Rynk only: require physical unlock before central or split-peripheral
# bootloader entry (default: true). Set false for host-managed deployment;
# other dangerous operations remain gated.
bootloader_requires_unlock = true
```

## Common Setups
Expand Down
4 changes: 4 additions & 0 deletions docs/docs/main/docs/configuration/rmk_config.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ split_peripherals_num = 0
ble_profiles_num = 3
# BLE Split Central sleep timeout in seconds (0 = disabled)
split_central_sleep_timeout_seconds = 0
# Maximum skipped 7.5 ms split connection events while active
split_central_max_latency_powered = 30
split_central_max_latency_battery = 30
# Maximum macro data bytes in one Rynk macro request or response
protocol_macro_chunk_size = 64
# Maximum number of auto mouse layer entries (auto-derived from [[behavior.auto_mouse_layer]] if unset)
Expand Down Expand Up @@ -98,6 +101,7 @@ These tune the [Rynk](../features/rynk) protocol and rarely need changing.

- `ble_profiles_num`: The number of available Bluetooth profiles, default value is 3. This parameter defines how many Bluetooth paired devices the keyboard can store.
- `split_central_sleep_timeout_seconds`: Sleep timeout for BLE split central in seconds, default value is 0 (disabled). When set to a non-zero value, the split central will enter sleep mode after this many seconds of inactivity to save power. Set to 0 to disable automatic sleep.
- `split_central_max_latency_powered` / `split_central_max_latency_battery`: Maximum number of BLE connection events the split peripheral may skip while active, selected by whether USB power is present. Both default to 30. Lower values reduce worst-case split input and central-to-peripheral update latency at the cost of more radio wakeups and peripheral battery use. Set to 0 for the lowest latency. Sleep mode uses its own low-power connection parameters. Rynk can replace these volatile values or force/clear a runtime override with `GetSplitCentralLatency` and `SetSplitCentralLatency`; reboot restores the build-time values.

### Auto Mouse Layer Configuration

Expand Down
173 changes: 119 additions & 54 deletions docs/docs/main/docs/development/rynk_protocol.md

Large diffs are not rendered by default.

19 changes: 15 additions & 4 deletions docs/docs/main/docs/features/rynk.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,10 @@ There are three tiers:
- **Open** — everything you normally reach for: read the keymap, change keys,
layers, combos, macros, switch BLE profiles, reboot. These stay available so
on-the-fly configuration is friction-free.
- **Locked** — the dangerous ones, always gated: entering the bootloader,
resetting stored settings and bonds, reading the live key matrix (a keylogger
if left open), and clearing a BLE bond. A host tool gets a "locked" error
until you unlock.
- **Locked** — the dangerous ones: entering the bootloader, resetting stored
settings and bonds, reading the live key matrix (a keylogger if left open),
and clearing a BLE bond. A host tool gets a "locked" error until you unlock.
Bootloader entry may be opted out separately for managed deployment setups.
- **Config writes** — open by default, because on-the-fly configuration is the
point. Set `write_requires_unlock = true` to move every write (keymap, macros,
…) into the locked tier as well.
Expand All @@ -127,6 +127,17 @@ If you leave `unlock_keys` unset, the locked operations can never be unlocked
a safe default, but it means a fresh config can't enter the bootloader over Rynk
or use the matrix tester until you add the keys.

For a keyboard whose host is trusted to manage firmware deployment, allow only
central and split-peripheral bootloader entry without the physical challenge:

```toml title="keyboard.toml"
[host]
bootloader_requires_unlock = false
```

Storage reset, matrix-state reads, and BLE bond clearing remain gated. This is
more narrowly scoped than `insecure = true`.

For local development you can bypass the gate entirely:

```toml title="keyboard.toml"
Expand Down
1 change: 1 addition & 0 deletions examples/use_rust/nrf54lm20_ble/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ async fn main(spawner: Spawner) {
unlock_keys: RYNK_UNLOCK_KEYS,
insecure: false,
write_requires_unlock: false,
bootloader_requires_unlock: true,
},
storage_config,
..Default::default()
Expand Down
1 change: 1 addition & 0 deletions examples/use_rust/qemu-riscv-rynk/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ async fn main(_spawner: Spawner) {
let rmk_config = RMK_CONFIG.init(RmkConfig {
lock_config: LockConfig {
insecure: true,
bootloader_requires_unlock: true,
..Default::default()
},
..Default::default()
Expand Down
6 changes: 6 additions & 0 deletions rmk-config/src/default_config/event_default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ channel_size = 1
pubs = 1
subs = 1

# Unit invalidation published after authoritative lighting state changes.
[event.lighting_changed]
channel_size = 1
pubs = 1
subs = 1

# Power events
[event.battery_status]
channel_size = 1
Expand Down
26 changes: 26 additions & 0 deletions rmk-config/src/default_config/subscriber_default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,13 @@ events = [
{ name = "connection_status_change" },
{ name = "sleep_state" },
{ name = "led_indicator" },
{ name = "modifier" },
]

[[subscriber]]
features = ["rynk", "lighting"]
events = [
{ name = "lighting_changed" },
]

[[subscriber]]
Expand All @@ -80,6 +87,25 @@ events = [
{ name = "connection_status_change" },
{ name = "sleep_state" },
{ name = "led_indicator" },
{ name = "modifier" },
]

[[subscriber]]
features = ["rynk", "lighting", "_ble"]
events = [
# Second Rynk session slot on dual-transport boards.
{ name = "lighting_changed" },
]

[[subscriber]]
features = ["lighting"]
events = [
# lighting/processor.rs: LightingProcessor subscribes to authoritative
# keyboard-state invalidations on every lighting-enabled board.
{ name = "layer_change" },
{ name = "connection_status_change" },
{ name = "led_indicator" },
{ name = "sleep_state" },
]

# --- Split-gated internal subscribers ---
Expand Down
101 changes: 101 additions & 0 deletions rmk-config/src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ impl LayoutInfo {
}
}

pub(crate) struct ResolvedLayout {
pub blob: Vec<u8>,
pub keys: Vec<[u8; 2]>,
pub physical: crate::resolved::PhysicalLayout,
}

/// A resolved shape: every default applied. `rect2` is the L-key's second
/// rectangle stored as center-relative offsets — its `x`/`y` are offsets from
/// the primary center, not absolute positions (the walk resolves them).
Expand Down Expand Up @@ -668,6 +674,101 @@ pub(crate) fn build_layout_blob(
Ok(miniz_oxide::deflate::compress_to_vec(&bytes, 10))
}

/// Resolve both host-facing KLE data and the compact firmware geometry from
/// one parse/walk. This keeps `[layout].map` the only board-layout authority.
pub(crate) fn build_resolved_layout(
layout: &LayoutTomlConfig,
expected_encoders: Option<usize>,
) -> Result<ResolvedLayout, String> {
let Some(info) = build_layout_info(layout, expected_encoders)? else {
return Ok(ResolvedLayout {
blob: Vec::new(),
keys: Vec::new(),
physical: crate::resolved::PhysicalLayout::default(),
});
};

let bytes =
postcard::to_allocvec(&info).map_err(|e| format!("keyboard.toml: layout blob serialize failed: {e}"))?;
let blob = miniz_oxide::deflate::compress_to_vec(&bytes, 10);
let keys = parse_map(layout.map.as_deref().unwrap_or_default(), layout.rows, layout.cols)?
.into_iter()
.filter_map(|token| match token {
MapToken::Key { row, col, .. } => Some([row, col]),
_ => None,
})
.collect();
let variant = &info.variants[info.default_variant as usize];
let physical = crate::resolved::PhysicalLayout {
keys: variant
.keys
.iter()
.map(|key| {
let center = fixed_point(key.rect.x, key.rect.y)
.map_err(|reason| format!("layout.map key ({},{}) center {reason}", key.row, key.col))?;
let size = fixed_size(key.rect.w, key.rect.h)
.map_err(|reason| format!("layout.map key ({},{}) size {reason}", key.row, key.col))?;
let rotation_centidegrees = centidegrees(key.r)
.map_err(|reason| format!("layout.map key ({},{}) rotation {reason}", key.row, key.col))?;
Ok(crate::resolved::PhysicalKey {
matrix: [key.row, key.col],
center,
size,
rotation_centidegrees,
})
})
.collect::<Result<Vec<_>, String>>()?,
};

Ok(ResolvedLayout { blob, keys, physical })
}

fn fixed_point(x: f32, y: f32) -> Result<crate::resolved::FixedPoint3, &'static str> {
fn axis(value: f32) -> Result<i16, &'static str> {
if !value.is_finite() {
return Err("must be finite");
}
let raw = (value * 256.0).round();
if raw < i16::MIN as f32 || raw > i16::MAX as f32 {
return Err("does not fit signed Q8.8 key-pitch units");
}
Ok(raw as i16)
}
Ok(crate::resolved::FixedPoint3 {
x: axis(x)?,
y: axis(y)?,
z: 0,
})
}

fn fixed_size(width: f32, height: f32) -> Result<crate::resolved::FixedSize2, &'static str> {
fn axis(value: f32) -> Result<u16, &'static str> {
if !value.is_finite() || value <= 0.0 {
return Err("must be finite and positive");
}
let raw = (value * 256.0).round();
if raw < 1.0 || raw > u16::MAX as f32 {
return Err("does not fit unsigned Q8.8 key-pitch units");
}
Ok(raw as u16)
}
Ok(crate::resolved::FixedSize2 {
width: axis(width)?,
height: axis(height)?,
})
}

fn centidegrees(degrees: f32) -> Result<i16, &'static str> {
if !degrees.is_finite() {
return Err("must be finite");
}
let raw = (degrees * 100.0).round();
if raw < i16::MIN as f32 || raw > i16::MAX as f32 {
return Err("does not fit signed centidegrees");
}
Ok(raw as i16)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading
Loading