Skip to content

Add topology-aware lighting with Rynk control - #1031

Open
colonelpanic8 wants to merge 77 commits into
rmk-rs:mainfrom
colonelpanic8:glove80-rmk/lighting-v2
Open

Add topology-aware lighting with Rynk control#1031
colonelpanic8 wants to merge 77 commits into
rmk-rs:mainfrom
colonelpanic8:glove80-rmk/lighting-v2

Conversation

@colonelpanic8

Copy link
Copy Markdown
Contributor

What

  • add physical key topology and generated lighting configuration
  • add a composable lighting engine with selectors, sources, effects, layers, output mapping, and RMK state integration
  • add native Rynk lighting discovery, query, mutation, and topic support
  • add Vial RGB Matrix compatibility backed by the same authoritative lighting state
  • add declarative and runtime conditional scenes, keyed on layer, output mode, and connection state
  • add protocol snapshots, loopback coverage, and focused unit tests

Why

RMK currently has no general model for per-key lighting that can span irregular and split layouts while keeping firmware, Rynk, and Vial state consistent. This implementation makes one lighting service authoritative, so changes made by either host protocol are reflected by later state queries and rendered output.

Relationship to earlier PRs

This replaces #987, which was opened against feat/rynk before that branch was merged into main (9448f7a) and so could no longer be reviewed against a live base. This branch is the same work rebased onto main and carried forward, and it merges cleanly.

It is self-contained: it includes the bounded split application-message side channel from #984, since split keyboards use that channel to propagate lighting state. It also supersedes the smaller abstraction draft in #982.

Checks

  • repository formatter
  • full RMK feature-matrix tests, including Rynk lighting loopback and Vial RGB Matrix tests
  • Rynk native tests and doctests
  • WASM target checks and generated package TypeScript validation
  • workspace and WASM clippy with warnings denied
  • physical Glove80 lighting and host-state round trips exercised on the composed integration branch

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

Size Report

Example main PR Diff .text .data .bss
use_config/nrf52832_ble 397.6 KiB 398.3 KiB +0.19% ⬆️ +792 0 0
use_config/nrf52840_ble 439.6 KiB 440.4 KiB +0.17% ⬆️ +804 0 0
use_config/nrf52840_ble_split (central) 500.8 KiB 507.4 KiB +1.31% ⬆️ +3248 +56 +3440
use_config/nrf52840_ble_split (peripheral) 318.8 KiB 321.5 KiB +0.83% ⬆️ +2128 +56 +528
use_config/pi_pico_w_ble 672.7 KiB 673.4 KiB +0.10% ⬆️ +724 0 +8
use_config/rp2040 146.3 KiB 147.0 KiB +0.49% ⬆️ +736 0 +8
use_config/rp2040_split (central) 159.9 KiB 165.6 KiB +3.55% ⬆️ +2452 0 +3372
use_config/rp2040_split (peripheral) 27.9 KiB 29.9 KiB +7.24% ⬆️ +1640 +52 +376
use_config/stm32f1 62.6 KiB 62.7 KiB +0.09% ⬆️ +60 0 0
use_config/stm32h7 100.4 KiB 101.2 KiB +0.72% ⬆️ +748 0 0
use_rust/nrf52832_ble 385.2 KiB 385.8 KiB +0.15% ⬆️ +604 0 0
use_rust/nrf52840_ble 435.2 KiB 435.8 KiB +0.13% ⬆️ +600 0 0
use_rust/nrf52840_ble_split (central) 504.3 KiB 510.5 KiB +1.24% ⬆️ +2908 +56 +3440
use_rust/nrf52840_ble_split (peripheral) 316.4 KiB 319.1 KiB +0.86% ⬆️ +2212 +56 +528
use_rust/pi_pico_w_ble 672.1 KiB 672.8 KiB +0.10% ⬆️ +712 0 +8
use_rust/rp2040 145.6 KiB 146.3 KiB +0.50% ⬆️ +740 0 +8
use_rust/rp2040_split (central) 158.4 KiB 164.1 KiB +3.58% ⬆️ +2464 0 +3352
use_rust/rp2040_split (peripheral) 28.2 KiB 30.3 KiB +7.12% ⬆️ +1632 +52 +376
use_rust/stm32f1 61.9 KiB 62.0 KiB +0.10% ⬆️ +68 0 0
use_rust/stm32h7 119.7 KiB 120.8 KiB +0.95% ⬆️ +1164 0 +4
use_config/nrf52832_ble — 397.6 KiB → 398.3 KiB (+0.19% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 365664	   8620	  33624	 407908	  63964	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 364872	   8620	  33624	 407116	  6364c	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +11.4Ki  [ = ]       0    .debug_str
  +0.4% +8.06Ki  [ = ]       0    .debug_info
  +0.4%    +920  [ = ]       0    .debug_ranges
  +0.1%    +843  [ = ]       0    .debug_loc
  +0.2%    +664  +0.2%    +664    .text
  +0.1%    +311  [ = ]       0    .debug_line
  +0.3%    +128  +0.3%    +128    .rodata
  +0.0%     +16  [ = ]       0    .symtab
   +20%     +12  [ = ]       0    [Unmapped]
  -0.0%     -16  [ = ]       0    .debug_aranges
  -0.0%     -16  [ = ]       0    .debug_frame
  -0.1%    -204  [ = ]       0    .strtab
  +0.3% +22.0Ki  +0.2%    +792    TOTAL
use_config/nrf52840_ble — 439.6 KiB → 440.4 KiB (+0.17% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 398704	   8628	  43664	 450996	  6e1b4	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 397900	   8628	  43664	 450192	  6de90	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +11.7Ki  [ = ]       0    .debug_str
  +0.4% +8.20Ki  [ = ]       0    .debug_info
  +0.1%    +954  [ = ]       0    .debug_loc
  +0.4%    +888  [ = ]       0    .debug_ranges
  +0.2%    +676  +0.2%    +676    .text
  +0.2%    +624  [ = ]       0    .debug_line
  +0.3%    +128  +0.3%    +128    .rodata
  +0.1%     +80  [ = ]       0    .symtab
  +0.5%     +39  [ = ]       0    .debug_abbrev
  +0.0%     +16  [ = ]       0    .debug_frame
  -8.3%      -4  [ = ]       0    [Unmapped]
  -0.9%      -8  [ = ]       0    .defmt
  -0.0%    -154  [ = ]       0    .strtab
  +0.3% +23.1Ki  +0.2%    +804    TOTAL
use_config/nrf52840_ble_split (central) — 500.8 KiB → 507.4 KiB (+1.31% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 458508	   8740	  52296	 519544	  7ed78	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 455260	   8684	  48856	 512800	  7d320	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.0% +94.3Ki  [ = ]       0    .debug_str
  +1.5% +35.9Ki  [ = ]       0    .debug_info
  +0.6% +4.99Ki  [ = ]       0    .debug_loc
  [ = ]       0  +7.2% +3.36Ki    .bss
  +0.8% +3.34Ki  [ = ]       0    .strtab
  +0.8% +2.90Ki  [ = ]       0    .debug_line
  +1.1% +2.87Ki  [ = ]       0    .debug_ranges
  +0.7% +2.71Ki  +0.7% +2.71Ki    .text
  +0.5%    +832  [ = ]       0    .symtab
  +1.1%    +472  +1.1%    +472    .rodata
  +0.8%    +448  [ = ]       0    .debug_frame
  +0.4%    +200  [ = ]       0    .debug_aranges
  +0.6%     +56  +0.6%     +56    .data
  +0.1%      +1  [ = ]       0    .defmt
  -0.0%      -4  [ = ]       0    .debug_abbrev
  -9.5%      -7  [ = ]       0    [Unmapped]
  +1.5%  +148Ki  +1.3% +6.59Ki    TOTAL
use_config/nrf52840_ble_split (peripheral) — 318.8 KiB → 321.5 KiB (+0.83% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 294932	   8556	  25712	 329200	  505f0	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 292804	   8500	  25184	 326488	  4fb58	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.1% +69.7Ki  [ = ]       0    .debug_str
  +1.4% +22.0Ki  [ = ]       0    .debug_info
  +1.2% +3.17Ki  [ = ]       0    .strtab
  +0.6% +3.10Ki  [ = ]       0    .debug_loc
  +1.5% +2.38Ki  [ = ]       0    .debug_ranges
  +1.0% +2.32Ki  [ = ]       0    .debug_line
  +0.7% +1.87Ki  +0.7% +1.87Ki    .text
  +0.7%    +736  [ = ]       0    .symtab
  [ = ]       0  +2.2%    +528    .bss
  +1.2%    +392  [ = ]       0    .debug_frame
  +0.7%    +216  +0.7%    +216    .rodata
  +0.4%    +176  [ = ]       0    .debug_aranges
  +0.7%     +56  +0.7%     +56    .data
  +0.2%      +1  [ = ]       0    .defmt
 -15.8%      -9  [ = ]       0    [Unmapped]
  +1.6%  +106Ki  +0.8% +2.65Ki    TOTAL
use_config/pi_pico_w_ble — 672.7 KiB → 673.4 KiB (+0.10% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 635828	      0	  53712	 689540	  a8584	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 635104	      0	  53704	 688808	  a82a8	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +12.7Ki  [ = ]       0    .debug_str
  +0.3% +8.22Ki  [ = ]       0    .debug_info
  +0.2% +2.18Ki  [ = ]       0    .debug_loc
  +0.4%    +928  [ = ]       0    .debug_ranges
  +0.2%    +746  [ = ]       0    .debug_line
  +0.2%    +600  +0.2%    +600    .text
  +0.0%    +124  +0.0%    +124    .rodata
  +0.2%     +20  [ = ]       0    .debug_abbrev
   +20%     +10  [ = ]       0    [Unmapped]
  [ = ]       0  +0.0%      +8    .bss
  -0.0%      -4  [ = ]       0    .debug_frame
  -0.0%      -8  [ = ]       0    .debug_aranges
  -0.0%     -16  [ = ]       0    .symtab
  -0.0%     -42  [ = ]       0    .strtab
  +0.3% +25.4Ki  +0.1%    +732    TOTAL
use_config/rp2040 — 146.3 KiB → 147.0 KiB (+0.49% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 136616	      0	  13916	 150532	  24c04	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135880	      0	  13908	 149788	  2491c	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +8.22Ki  [ = ]       0    .debug_str
  +0.7% +6.96Ki  [ = ]       0    .debug_info
  +0.8% +2.56Ki  [ = ]       0    .debug_loc
  +1.1%    +928  [ = ]       0    .debug_ranges
  +0.4%    +666  [ = ]       0    .debug_line
  +0.5%    +612  +0.5%    +612    .text
  +0.8%    +124  +0.8%    +124    .rodata
  +0.0%     +16  [ = ]       0    .symtab
  +0.2%     +14  [ = ]       0    .debug_abbrev
  [ = ]       0  +0.1%      +8    .bss
  +2.6%      +1  [ = ]       0    [Unmapped]
  -0.1%     -16  [ = ]       0    .debug_aranges
  -0.2%     -32  [ = ]       0    .debug_frame
  -0.3%    -413  [ = ]       0    .strtab
  +0.6% +19.6Ki  +0.5%    +744    TOTAL
use_config/rp2040_split (central) — 159.9 KiB → 165.6 KiB (+3.55% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 151360	      0	  18232	 169592	  29678	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 148908	      0	  14860	 163768	  27fb8	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.8% +55.6Ki  [ = ]       0    .debug_str
  +2.1% +23.4Ki  [ = ]       0    .debug_info
  +1.7% +5.92Ki  [ = ]       0    .debug_loc
  [ = ]       0   +24% +3.29Ki    .bss
  +2.4% +2.23Ki  [ = ]       0    .debug_ranges
  +1.6% +2.04Ki  +1.6% +2.04Ki    .text
  +1.1% +1.94Ki  [ = ]       0    .debug_line
  +1.0% +1.32Ki  [ = ]       0    .strtab
  +0.9%    +320  [ = ]       0    .symtab
  +1.7%    +316  +1.7%    +316    .rodata
  +0.8%    +156  [ = ]       0    .debug_frame
  +0.4%     +80  [ = ]       0    .debug_aranges
   +29%     +52   +37%     +52    .data
  +0.2%     +14  [ = ]       0    .debug_abbrev
   +34%     +12  [ = ]       0    [Unmapped]
  +0.2%      +1  [ = ]       0    .defmt
  +2.3% +93.4Ki  +3.6% +5.69Ki    TOTAL
use_config/rp2040_split (peripheral) — 27.9 KiB → 29.9 KiB (+7.24% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  27336	    112	   3180	  30628	   77a4	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25696	     60	   2804	  28560	   6f90	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +6.5% +55.0Ki  [ = ]       0    .debug_str
  +5.1% +19.8Ki  [ = ]       0    .debug_info
   +12% +1.93Ki  [ = ]       0    .debug_ranges
  +3.1% +1.79Ki  [ = ]       0    .debug_line
  +2.4% +1.66Ki  [ = ]       0    .debug_loc
  +6.2% +1.51Ki  [ = ]       0    .strtab
  +7.1% +1.43Ki  +7.1% +1.43Ki    .text
  [ = ]       0   +21%    +376    .bss
  +3.3%    +288  [ = ]       0    .symtab
  +3.9%    +180  +3.9%    +180    .rodata
  +2.9%    +160  [ = ]       0    .debug_frame
  +0.6%     +88  [ = ]       0    .debug_aranges
   +52%     +52   +87%     +52    .data
  +0.4%     +26  [ = ]       0    .debug_abbrev
   +19%      +9  [ = ]       0    [Unmapped]
  +0.5%      +1  [ = ]       0    .defmt
  +5.7% +83.8Ki  +7.2% +2.02Ki    TOTAL
use_config/stm32f1 — 62.6 KiB → 62.7 KiB (+0.09% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  57808	     28	   6336	  64172	   faac	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  57748	     28	   6336	  64112	   fa70	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +5.21Ki  [ = ]       0    .debug_str
  +0.4% +2.53Ki  [ = ]       0    .debug_info
  +0.4%    +152  [ = ]       0    .debug_ranges
  +0.1%     +60  [ = ]       0    .debug_line
  +0.1%     +60  +0.1%     +60    .text
  +0.2%     +10  [ = ]       0    .debug_abbrev
  +6.1%      +4  [ = ]       0    [Unmapped]
  -0.1%      -8  [ = ]       0    .debug_aranges
  -0.2%     -28  [ = ]       0    .debug_frame
  -0.2%     -32  [ = ]       0    .symtab
  -0.1%     -36  [ = ]       0    .strtab
  -0.2%    -195  [ = ]       0    .debug_loc
  +0.4% +7.72Ki  +0.1%     +60    TOTAL
use_config/stm32h7 — 100.4 KiB → 101.2 KiB (+0.72% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  95188	    264	   8128	 103580	  1949c	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  94440	    264	   8128	 102832	  191b0	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.4% +8.52Ki  [ = ]       0    .debug_str
  +0.8% +7.75Ki  [ = ]       0    .debug_info
  +2.3% +1.52Ki  [ = ]       0    .debug_ranges
  +0.6%    +723  [ = ]       0    .debug_line
  +0.8%    +628  +0.8%    +628    .text
  +0.2%    +339  [ = ]       0    .debug_loc
  +0.9%    +120  +0.9%    +120    .rodata
  +0.2%     +64  [ = ]       0    .symtab
  +0.2%     +10  [ = ]       0    .debug_abbrev
  +0.0%      +8  [ = ]       0    .debug_aranges
  +0.0%      +4  [ = ]       0    .debug_frame
 -21.0%     -13  [ = ]       0    [Unmapped]
  -0.0%     -20  [ = ]       0    .strtab
  +0.6% +19.6Ki  +0.7%    +748    TOTAL
use_rust/nrf52832_ble — 385.2 KiB → 385.8 KiB (+0.15% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 352836	   8620	  33560	 395016	  60708	rmk-nrf52832

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 352232	   8620	  33560	 394412	  604ac	rmk-nrf52832

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2% +6.95Ki  [ = ]       0    .debug_str
  +0.3% +5.79Ki  [ = ]       0    .debug_info
  +0.4% +2.63Ki  [ = ]       0    .debug_loc
  +0.3%    +616  [ = ]       0    .debug_ranges
  +0.2%    +480  +0.2%    +480    .text
  +0.1%    +368  [ = ]       0    .debug_line
  +0.1%    +220  [ = ]       0    .strtab
  +0.3%    +124  +0.3%    +124    .rodata
  +0.1%     +64  [ = ]       0    .symtab
  +4.8%      +3  [ = ]       0    [Unmapped]
  -0.0%     -20  [ = ]       0    .debug_frame
  +0.2% +17.2Ki  +0.2%    +604    TOTAL
use_rust/nrf52840_ble — 435.2 KiB → 435.8 KiB (+0.13% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 396736	   8628	  40904	 446268	  6cf3c	rmk-nrf52840

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 396136	   8628	  40904	 445668	  6cce4	rmk-nrf52840

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.4% +14.9Ki  [ = ]       0    .debug_str
  +0.4% +7.63Ki  [ = ]       0    .debug_info
  +0.3% +2.21Ki  [ = ]       0    .debug_loc
  +0.1%    +480  +0.1%    +480    .text
  +0.2%    +408  [ = ]       0    .debug_ranges
  +0.1%    +268  [ = ]       0    .debug_line
  +0.3%    +120  +0.3%    +120    .rodata
  +0.2%     +16  [ = ]       0    .debug_abbrev
   +16%      +8  [ = ]       0    [Unmapped]
  -0.9%      -8  [ = ]       0    .defmt
  -0.1%     -40  [ = ]       0    .debug_aranges
  -0.0%     -44  [ = ]       0    .strtab
  -0.1%     -52  [ = ]       0    .debug_frame
  -0.1%     -80  [ = ]       0    .symtab
  +0.3% +25.8Ki  +0.1%    +600    TOTAL
use_rust/nrf52840_ble_split (central) — 504.3 KiB → 510.5 KiB (+1.24% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 459780	   8740	  54264	 522784	  7fa20	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 456872	   8684	  50824	 516380	  7e11c	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.1%  +101Ki  [ = ]       0    .debug_str
  +1.4% +34.4Ki  [ = ]       0    .debug_info
  +0.5% +4.35Ki  [ = ]       0    .debug_loc
  +1.0% +4.04Ki  [ = ]       0    .strtab
  [ = ]       0  +6.9% +3.36Ki    .bss
  +0.7% +2.64Ki  [ = ]       0    .debug_line
  +0.6% +2.38Ki  +0.6% +2.38Ki    .text
  +0.7% +1.73Ki  [ = ]       0    .debug_ranges
  +0.4%    +624  [ = ]       0    .symtab
  +1.1%    +472  +1.1%    +472    .rodata
  +0.7%    +400  [ = ]       0    .debug_frame
  +0.4%    +176  [ = ]       0    .debug_aranges
  +0.6%     +56  +0.6%     +56    .data
  +0.1%      +1  [ = ]       0    .defmt
  -0.0%      -3  [ = ]       0    .debug_abbrev
 -29.5%     -18  [ = ]       0    [Unmapped]
  +1.6%  +152Ki  +1.2% +6.25Ki    TOTAL
use_rust/nrf52840_ble_split (peripheral) — 316.4 KiB → 319.1 KiB (+0.86% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 292756	   8556	  25480	 326792	  4fc88	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 290544	   8500	  24952	 323996	  4f19c	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.2% +69.9Ki  [ = ]       0    .debug_str
  +1.5% +23.2Ki  [ = ]       0    .debug_info
  +1.2% +3.12Ki  [ = ]       0    .strtab
  +0.5% +2.77Ki  [ = ]       0    .debug_loc
  +1.0% +2.49Ki  [ = ]       0    .debug_line
  +1.2% +1.95Ki  [ = ]       0    .debug_ranges
  +0.8% +1.95Ki  +0.8% +1.95Ki    .text
  +0.7%    +752  [ = ]       0    .symtab
  [ = ]       0  +2.2%    +528    .bss
  +1.1%    +376  [ = ]       0    .debug_frame
  +0.7%    +220  +0.7%    +220    .rodata
  +0.4%    +176  [ = ]       0    .debug_aranges
  +0.7%     +56  +0.7%     +56    .data
  +1.4%      +9  [ = ]       0    .defmt
   +16%      +7  [ = ]       0    [Unmapped]
  +1.6%  +106Ki  +0.9% +2.73Ki    TOTAL
use_rust/pi_pico_w_ble — 672.1 KiB → 672.8 KiB (+0.10% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 635564	      0	  53392	 688956	  a833c	rmk-pi-pico-w

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 634852	      0	  53384	 688236	  a806c	rmk-pi-pico-w

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +12.5Ki  [ = ]       0    .debug_str
  +0.3% +7.98Ki  [ = ]       0    .debug_info
  +0.2% +2.31Ki  [ = ]       0    .debug_loc
  +0.4%    +896  [ = ]       0    .debug_ranges
  +0.2%    +733  [ = ]       0    .debug_line
  +0.2%    +588  +0.2%    +588    .text
  +0.0%    +124  +0.0%    +124    .rodata
  +0.4%     +40  [ = ]       0    .debug_abbrev
   +67%     +28  [ = ]       0    [Unmapped]
  +0.0%     +16  [ = ]       0    .symtab
  [ = ]       0  +0.0%      +8    .bss
  -0.0%      -4  [ = ]       0    .debug_frame
  -0.0%      -8  [ = ]       0    .debug_aranges
  -0.0%     -58  [ = ]       0    .strtab
  +0.3% +25.1Ki  +0.1%    +720    TOTAL
use_rust/rp2040 — 145.6 KiB → 146.3 KiB (+0.50% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 136052	      0	  13756	 149808	  24930	rmk-rp2040

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 135312	      0	  13748	 149060	  24644	rmk-rp2040

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +8.06Ki  [ = ]       0    .debug_str
  +0.7% +6.71Ki  [ = ]       0    .debug_info
  +0.8% +2.49Ki  [ = ]       0    .debug_loc
  +1.1%    +928  [ = ]       0    .debug_ranges
  +0.5%    +616  +0.5%    +616    .text
  +0.4%    +609  [ = ]       0    .debug_line
  +0.8%    +124  +0.8%    +124    .rodata
  +0.0%     +16  [ = ]       0    .symtab
  +0.2%     +14  [ = ]       0    .debug_abbrev
  [ = ]       0  +0.1%      +8    .bss
  -6.3%      -4  [ = ]       0    [Unmapped]
  -0.1%     -16  [ = ]       0    .debug_aranges
  -0.2%     -32  [ = ]       0    .debug_frame
  -0.3%    -413  [ = ]       0    .strtab
  +0.5% +19.1Ki  +0.5%    +748    TOTAL
use_rust/rp2040_split (central) — 158.4 KiB → 164.1 KiB (+3.58% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 150072	      0	  17956	 168028	  2905c	central

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 147608	      0	  14604	 162212	  279a4	central

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +2.8% +55.8Ki  [ = ]       0    .debug_str
  +2.1% +23.5Ki  [ = ]       0    .debug_info
  +2.1% +7.30Ki  [ = ]       0    .debug_loc
  [ = ]       0   +25% +3.27Ki    .bss
  +2.6% +2.37Ki  [ = ]       0    .debug_ranges
  +1.6% +2.05Ki  +1.6% +2.05Ki    .text
  +1.1% +2.04Ki  [ = ]       0    .debug_line
  +1.0% +1.29Ki  [ = ]       0    .strtab
  +1.5%    +544  [ = ]       0    .symtab
  +1.7%    +316  +1.7%    +316    .rodata
  +0.8%    +156  [ = ]       0    .debug_frame
  +0.5%     +88  [ = ]       0    .debug_aranges
   +29%     +52   +37%     +52    .data
  +0.2%     +14  [ = ]       0    .debug_abbrev
  +7.3%      +4  [ = ]       0    [Unmapped]
  +0.2%      +1  [ = ]       0    .defmt
  +2.3% +95.5Ki  +3.6% +5.68Ki    TOTAL
use_rust/rp2040_split (peripheral) — 28.2 KiB → 30.3 KiB (+7.12% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  27428	    112	   3444	  30984	   7908	peripheral

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  25796	     60	   3068	  28924	   70fc	peripheral

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +6.4% +54.9Ki  [ = ]       0    .debug_str
  +5.3% +20.3Ki  [ = ]       0    .debug_info
   +11% +7.68Ki  [ = ]       0    .debug_loc
  +3.1% +1.75Ki  [ = ]       0    .debug_line
  +6.4% +1.54Ki  [ = ]       0    .strtab
  +7.0% +1.41Ki  +7.0% +1.41Ki    .text
  +8.6% +1.41Ki  [ = ]       0    .debug_ranges
  +4.8%    +416  [ = ]       0    .symtab
  [ = ]       0   +18%    +376    .bss
  +3.8%    +184  +3.8%    +184    .rodata
  +3.0%    +160  [ = ]       0    .debug_frame
  +0.6%     +88  [ = ]       0    .debug_aranges
   +52%     +52   +87%     +52    .data
   +27%     +13  [ = ]       0    [Unmapped]
  +0.5%      +1  [ = ]       0    .defmt
  +6.1% +89.9Ki  +7.1% +2.01Ki    TOTAL
use_rust/stm32f1 — 61.9 KiB → 62.0 KiB (+0.10% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
  57152	     28	   6272	  63452	   f7dc	rmk-stm32f1

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
  57084	     28	   6272	  63384	   f798	rmk-stm32f1

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.6% +5.11Ki  [ = ]       0    .debug_str
  +0.4% +2.06Ki  [ = ]       0    .debug_info
  +0.4%    +152  [ = ]       0    .debug_ranges
  +0.1%     +68  +0.1%     +68    .text
  +0.0%     +36  [ = ]       0    .debug_line
  +0.2%     +10  [ = ]       0    .debug_abbrev
  -3.5%      -2  [ = ]       0    [Unmapped]
  -0.2%      -8  [ = ]       0    .debug_aranges
  -0.1%     -27  [ = ]       0    .strtab
  -0.2%     -28  [ = ]       0    .debug_frame
  -0.2%     -32  [ = ]       0    .symtab
  -0.2%    -195  [ = ]       0    .debug_loc
  +0.4% +7.14Ki  +0.1%     +68    TOTAL
use_rust/stm32h7 — 119.7 KiB → 120.8 KiB (+0.95% ⬆️)

cargo size (PR):

   text	   data	    bss	    dec	    hex	filename
 109352	    320	  14032	 123704	  1e338	rmk-stm32h7

cargo size (main):

   text	   data	    bss	    dec	    hex	filename
 108188	    320	  14028	 122536	  1dea8	rmk-stm32h7

Bloaty diff (PR vs main):

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.3% +7.53Ki  [ = ]       0    .debug_str
  +0.6% +6.54Ki  [ = ]       0    .debug_info
  +1.9% +1.46Ki  [ = ]       0    .debug_ranges
  +1.1% +1.13Ki  +1.1% +1.13Ki    .text
  +0.4% +1.00Ki  [ = ]       0    .debug_loc
  +0.3%    +539  [ = ]       0    .debug_line
  [ = ]       0  +0.0%      +4    .bss
  +0.1%      +4  +0.1%      +4    .rodata
 -21.4%     -12  [ = ]       0    [Unmapped]
  -0.1%    -148  [ = ]       0    .strtab
  -0.4%    -168  [ = ]       0    .debug_aranges
  -1.5%    -304  [ = ]       0    .debug_frame
  -1.1%    -400  [ = ]       0    .symtab
  +0.4% +17.2Ki  +1.0% +1.14Ki    TOTAL

colonelpanic8 and others added 27 commits August 6, 2026 22:56
Add an opaque, bounded application payload that firmware can exchange
between the split central and peripheral alongside the normal split
traffic, without ever taking priority over key events.

* `split_app` module: `SplitAppData` (a small, `MaxSize`, postcard
  length-prefixed payload) plus four statics — `SPLIT_APP_TX`
  (central -> peripheral), `SPLIT_APP_PERIPH_TX` (peripheral ->
  central), the symmetric `SPLIT_APP_RX` inbox, and the `SPLIT_APP_LINK`
  watch that reports split-link state to the application.
* Producers use `try_send` only (bounded, drop-on-full) so the split
  read/write loops never block or get starved by application traffic;
  key events are always polled first.
* The split driver and peripheral drain the application queues as the
  lowest-priority arm of their outgoing selects and forward received
  `SplitMessage::Application` payloads into the inbox.
* `SPLIT_APP_LINK` is state-based (a `Watch`), so a late-subscribing
  application still observes the current link state; the `false -> true`
  edge is a resync trigger. Link-down edges are emitted from a drop
  guard so they survive async cancellation of the split session.
* On the peripheral the link is raised on the FIRST inbound message from
  the central rather than on bare connection: over BLE, notifications to
  a central that has not yet subscribed are silently dropped, so the
  connection alone is not proof the application channel is usable.

Developed for a split keyboard port.
Add the protocol types for runtime-configurable per-layer scenes:
LightingSceneCell (layer + stable LED id + effect), LightingLayerPolicy,
a revision-pinned scenes page, single-cell set/unset requests, an atomic
Begin/Put/Commit/Abort replacement transaction, and SetLightingLayerPolicy.

Discovery deliberately leaves LightingCapabilities and LightingState
byte-identical: postcard is positional, so appending fields would break
new-host/old-firmware decode. Scenes are advertised through a new
LAYER_SCENES bit in the existing LightingFeatureFlags plus a dedicated
GetLightingSceneStatus endpoint carrying capacity, occupancy, and policy.
New LightingError variants (UnknownLayer, SceneFull) are appended, so
existing encodings are unchanged. Protocol version bumps to v0.3.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Give the standard engine an owned, fixed-capacity SceneTable (new
SCENE_CAP const generic, default 0) holding per-layer, per-slot effects
with the same EffectiveOnly/ActiveStack composition semantics as the
static LayerScenes. The table composes in the layer band immediately
after the board's static scenes — a runtime cell overrides a static
default for the same slot — while the TTL overlay stays highest-priority.

Scene mutations are revision-checked commands; whole-table replacement
stages inside the engine via bounded chunks (Begin/Put/Commit/Abort)
with inactivity expiry and an idempotent commit. Staging engine-side
instead of reusing the host-side overlay staging keeps kilobyte-sized
scene batches off the bounded mailbox channels, whose payloads are
copied by value. Scene reads and transaction reservation need non-state
readback, so the engine's reply type becomes the StandardReply enum and
StandardState gains scene_len/scene_policy.

The Rynk service exposes the table through nine endpoints for status,
revision-pinned paged reads, single-cell set/unset, layer policy, and
the chunked replacement transaction. It validates effects, layer bounds
(against the live keymap), and stable LED ids (against the topology)
before anything reaches the engine; boards opt in by advertising a
capacity via RynkLightingController::with_scene_capacity, and scene
endpoints reject with Unsupported when none is wired. Scene writes
honor the same unlock gate as the other lighting writes.

Scene configuration is durable config like the keymap: after every
successful mutation the adapter reads the table back out of the engine
and persists it as one header record (len + policy) plus chunk-sized
shard records in wire (stable LED id) form. The storage task compares
before writing so unchanged shards cost no flash traffic; a storage
reset clears them. Boards load persisted scenes at boot with
Storage::read_lighting_scenes + install_lighting_scenes, which skips
cells whose LED id no longer resolves. Lighting featuresets join the
CI test/clippy matrix.

Composed with the split renderer replicas underneath: the runtime
scene table travels inside StandardReplicaState, so replica renderers
draw runtime scenes exactly like the authority without ever seeing the
incremental scene mutation commands. StandardReplicaState/-Slot and
StandardCommand gain a trailing SCENE_CAP const generic (default 0),
ExportReplica snapshots the table and ApplyReplica installs it, and the
replica round-trip test now covers a runtime scene cell.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndings

Typed client methods for every scene endpoint, plus two alloc
conveniences: read_all_lighting_scenes pages the whole table under one
pinned revision and restarts on a concurrent-mutation conflict, and
replace_all_lighting_scenes drives the begin/put/commit transaction
with a best-effort abort on staging failure. The wasm client re-exports
the same surface, so the generated TypeScript picks up the scene types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ProtocolVersion numbers belong to upstream (HaoboGu/rmk feat/rynk); a
fork that bumps CURRENT will eventually collide with upstream reusing
the same minor for a different protocol. Revert CURRENT to the upstream
base's v0.1 and drop every fork-minted claim (v0.2 native lighting,
v0.3 build info, v0.4 routed split bootloader entry, v0.5 layer
scenes).

Downstream features are discoverable without version numbers, using the
protocol's existing negotiation machinery:

- lighting endpoints: `DeviceCapabilities.lighting_enabled` (declared
  by upstream v0.1), then `GetLightingCapabilities.features`
- layer scenes: `LightingFeatureFlags::LAYER_SCENES` (set iff the board
  advertises a scene capacity) plus `GetLightingSceneStatus.capacity`
- `GetBuildInfo` / `PeripheralBootloaderJump`: per-command probing —
  firmware without them answers `UnknownCmd`, which hosts already
  receive as `Rejected(UnknownCmd)`

No code ever gated on the minted minors (the handshake only rejects
`major` mismatches), so firmware already in the field reporting
v0.2–v0.5 stays compatible with reverted hosts and vice versa.

Regenerate the wire snapshots and the generated protocol reference, and
state the rule in the reference's Compatibility section and the
changelog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ndalone config macro

Boards whose hardware cannot use the generated main (custom matrix
drivers, nonstandard USB bring-up) previously had no way to consume
`[lighting]` from keyboard.toml, and animated extension sources could
not own the RGB mode/value/speed keys: apply_light_action routed every
RGB action to the uniform background.

- LightingSource::handle_light_action (default: decline) lets the
  standard engine offer each LightAction to its extension source before
  the built-in background handling.
- rmk_lighting_config! emits the same flash-resident physical-layout
  and lighting statics as #[rmk_keyboard], resolved standalone so no
  [matrix]/[split] section is required; [keymap].layers alone provides
  the layer count for scene validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Exercise layout_standalone/lighting_standalone plus the geometry and
topology expansion on an exact copy of the rmk-zsa-voyager keyboard.toml:
52 emitters over two outputs, with { all = true } layer scenes expanding
to every slot.

The test caught that expand_standalone_lighting_config loaded the toml
through new_from_toml_path, which panics without [keyboard].chip — a
section hand-written-main firmware intentionally omits. Load with event
defaults only instead, as rmk-types' build.rs already does; the skipped
chip-default layer never touches [layout], [lighting], or [keymap].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
colonelpanic8 and others added 28 commits August 6, 2026 22:58
The capability was set only when a board bound a key to cycling the mode,
so replacing that binding with a real keycode withdrew the whole capability
-- hosts then stopped reading or writing the mode, and a config asking for
powered-only silently read back as always-on. The standard engine always
owns a policy, so the flag reflects that rather than the binding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream rebased feat/rynk before merging it, so this topic replays onto a
main whose rynk internals differ from the ones it was written against:

- `Serve::<E, _>::serve(x, msg)` became the free `serve::<E, _>(x, msg)`, and
  the lock endpoints take the session's `HostLock` rather than the session.
- `RynkSession` lost its `locker` and `topics` fields, which are now locals in
  `run_session`. It keeps only the lighting overlay transaction, which has to
  outlive a single dispatch because it spans Begin/Put/Commit.
- `RMK_VERSION` moved into `handlers::system`; it lives here again so it can be
  public alongside `RMK_VERSION_STRING` and the build-label helpers.
- `rynk::endpoint` folded into `rynk::command`, and `TopicEvent::cmd()` is gone.
- The new simulator codegen predates `bootloader_requires_unlock`, so the
  `run_tests!` scenarios could not build a `LockConfig`.

The lighting handler tests derive their gate from the service under test, the
way `run_session` does, now that the session no longer carries one.
Upstream replaced the hand-written loopback suite with a TOML-scenario
simulator, which these cases cannot use: the topology a lighting board
advertises is a compile-time fixture rather than a keymap, and the stateful
endpoints answer from an engine that has to be driven concurrently with the
request. Both are things a timeline has no vocabulary for, so they live beside
the other cases a scenario cannot express and play the host over the same
in-memory duplex `run_session` gets from a real transport.

Restores discovery with revision-pinned paging, the extension endpoints across
the full mailbox/adapter/engine path, and the lighting readback topic. The
Unsupported case is split out, since it is the one that needs no engine.

The host half is reimplemented rather than pulled from `rynk`: as a
dev-dependency it would unify `rmk-types/host` into every rmk test build, so a
lean feature row would compile `Cmd` variants whose dispatch arms are cfg'd
out. Both ends share `rmk-types` and decode with the same strictness instead.
Scene lookup scanned the whole table for every cell it resolved, so
composition cost grew with the total number of cells rather than with
the cells the active layers actually contribute. Keep cells grouped by
layer with a `layer_offsets` index instead, so a layer's run is a slice
and `included_len` is arithmetic over the active layers rather than a
filtered count. Insertion and removal shift within the array to hold
the grouping; layers past `LayerState::CAPACITY` fall back to a
partition point.

Also stop publishing a LightingChangedEvent when a board signals
`snapshot_changed`. That signal means "re-render from fresh context",
which every board raises on ordinary battery and sensor movement; the
event is a host readback invalidation and does not belong on that path.

Tests: 649 pass on the split,rynk,storage,async_matrix,_ble,lighting
row under nextest, including a new differential test that checks the
grouped table resolves identically to a naive scan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reject extended conditional-scene cells whose connection profile is past
NUM_BLE_PROFILE, empty the legacy conditional table whenever the V2 table
is written so a firmware downgrade cannot resurrect stale rules, and
document that legacy read-modify-write cycles drop connection predicates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…able

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An extension is enabled while its ExtensionState value is non-zero, which
is exactly what RgbTog flips. Nothing could observe that, so a key bound
to the toggle had no way to show its own state: its cell painted the same
colour whether effects were rendering or blanked.

Add an `effects` predicate to ConditionSet, answered from the extension
source the engine already holds. Compiled rules pass None and so stay
unsatisfiable, on the same grounds as output_mode: a source that cannot
observe the state must not let either polarity fire.

The predicate rides the extended conditional cell, which changes that
cell's encoding rather than merely adding a field to it. Firmware
advertising only RUNTIME_CONNECTION_CONDITIONS speaks the earlier cell,
so RUNTIME_EFFECTS_CONDITIONS describes the new encoding and a host that
cannot see it must fall back to the legacy endpoints. The added predicate
also costs a cell one byte, so an extended page now holds five.

Persisted V2 tables written by the earlier extended cell no longer parse
-- the shrunk chunk alone would have done that -- and fetch_data reports
a failed record as absent, so such a table boots empty and is restored by
the host's next apply rather than misparsed into stale rules.
Maintain a bitmap alongside the profile manager's bonded-device list so
subscribers outside the BLE task can see every slot's paired-versus-empty
state at once, not just the active slot's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Conditional rules gain connection.bonded = { slot, bonded }, matched
against the bond bitmap independently of the active profile, so one rule
per slot key can light paired and empty slots differently. The extended
wire cell grows accordingly (pre-release surface iterated in lockstep
with the in-tree hosts); pages now carry five cells. Stale stored V2
tables from the previous cell layout decode as absent and are restored
by the next host apply.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rules can gate on USB being plugged and routable independently of the
active transport — the difference between a USB indicator shown ready
(plugged, typing elsewhere) and active (carrying typing). Wire cell
grows by the trailing option; five-cell pages still fit the payload
bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extended conditional exemplars still built `LightingConnectionCondition`
without `bonded` and `usb_connected`, so the `host` feature build of
rmk-types failed to compile. Populate both and repin the frame snapshot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only the lighting overlay handlers take the session, so `rynk` without
`lighting` tripped -D unused-variables in example builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The engine could report what it holds but never what the LEDs show. On a
split renderer replica those are different questions: the replica renders
from a snapshot that can go stale, and nothing observable distinguished a
stale replica from a correct one.

Record each render's revision and lighting context, promote it when the
output acknowledges the write, and page the committed frame back through
`ReadFrame`. The committed frame is post-`OutputTransform`, so what comes
back is what the driver was handed rather than the pre-brightness
composition.
Two new lighting endpoints for diagnosing a split renderer replica from
the host: `GetLightingFrame` pages back what one node last presented, and
`GetLightingReplicaStatus` returns both sides of the replication
handshake in one read.

`LightingFeatureFlags` is out of bits, so these are discovered by
probing — firmware without them answers `UnknownCmd`, which is the
documented downstream path anyway. Existing layouts and the upstream
protocol version are untouched.

A remote frame page has to be reassembled from split application packets,
whose queues are shallow and lossy, so the page holds 24 cells rather
than the ~80 the payload budget would allow. Availability is encoded
explicitly: `Option` for never-observed values, and two appended
`LightingError` variants separating "no such node" from "that node could
not answer".
Wires GetLightingFrame and GetLightingReplicaStatus through the host
service: the local half answers from its own engine, the peripheral
through the board-provided remote-frame and replication-status ports,
and the loopback integration tests drive both endpoints end to end.
Native rmk-types and rynk_lighting suites pass; the cross-feature
matrix and no_std target checks have not run yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@colonelpanic8
colonelpanic8 force-pushed the glove80-rmk/lighting-v2 branch from dfdafb1 to f5202f3 Compare August 7, 2026 19:58
Expose split lighting frame and replica observability
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant