Skip to content

feat: add OLED display support with DisplayRenderer trait#782

Open
gbPagano wants to merge 24 commits intoHaoboGu:mainfrom
gbPagano:feat/oled-displays
Open

feat: add OLED display support with DisplayRenderer trait#782
gbPagano wants to merge 24 commits intoHaoboGu:mainfrom
gbPagano:feat/oled-displays

Conversation

@gbPagano
Copy link
Copy Markdown
Contributor

This PR adds initial OLED display support to RMK via a new display feature flag. The goal is to get early feedback on the architecture and direction before investing in the full feature set.

What's included

  • DisplayRenderer trait — the core abstraction that allows users to fully customize what is drawn on the display:
    pub trait DisplayRenderer {
        fn render<D: DrawTarget<Color = BinaryColor>>(
            &mut self, ctx: &RenderContext, display: &mut D,
        );
    }
  • RenderContext — a snapshot of keyboard state (layer, WPM, caps/num lock, battery) passed to renderers on every redraw, decoupled from the display driver.
  • DefaultRenderer — a built-in renderer that automatically adapts between landscape and portrait layouts based on the logical display dimensions.
  • OledDisplayProcessor<DI, SIZE, R> — a processor (using the existing #[processor] macro) generic over the renderer. R defaults to DefaultRenderer, so ::new(display) works out of the box and ::with_renderer(display, my_renderer) gives full control.
  • examples/use_rust/rp2040_oled updated with a working I2C OLED setup.

Architecture

                    ┌─────────────────────────────────────┐
  use_config ─────▶│  keyboard.toml [display]            │
  (future)          │  rmk-macro generates a configured   │
                    │  renderer at compile-time           │
                    └──────────────┬──────────────────────┘
                                   │ impl DisplayRenderer
                    ┌──────────────▼──────────────────────┐
  use_rust ───────▶│  OledDisplayProcessor<DI, SIZE, R>  │
                    │  R: DisplayRenderer                 │
                    │  R = DefaultRenderer (if omitted)   │
                    └─────────────────────────────────────┘

The design mirrors the two user profiles RMK already supports:

  • use_rust users implement DisplayRenderer for full control.
  • use_config users (future) would declare widgets in keyboard.toml and the macro would generate the renderer.

Not yet included: support for displays other than SSD1306 (SH1106, SSD1309, etc.), an SPI example, keyboard.toml configuration for widget selection and orientation, and public helper functions for custom renderers to reuse.

Questions for review

  1. Does this overall architecture (trait-based renderer + processor generic over it) align with where you'd like RMK's display support to go?
  2. For the keyboard.toml integration: would a widget-slot model or a simpler theme-based approach be preferred?

Open to any thoughts on the direction, would love to hear feedback before moving forward.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Mar 29, 2026

Binary Size Report

use_config/nrf52832_ble

   text	   data	    bss	    dec	    hex	filename
 323812	   5104	  34664	 363580	  58c3c	rmk-nrf52832
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.2%    +516  [ = ]       0    .strtab
  +0.2%    +224  [ = ]       0    .symtab
  +0.0%    +217  [ = ]       0    .debug_loc
  +0.3%    +140  [ = ]       0    .debug_frame
  +0.0%    +132  +0.0%    +132    .text
  +0.1%    +104  [ = ]       0    .debug_ranges
  +0.0%     +65  [ = ]       0    .debug_line
  +0.1%     +48  [ = ]       0    .debug_aranges
  +0.0%     +41  [ = ]       0    .debug_str
  [ = ]       0  +0.0%      +8    .bss
  -1.6%      -1  [ = ]       0    [Unmapped]
  -0.0%    -250  [ = ]       0    .debug_info
  +0.0% +1.21Ki  +0.0%    +140    TOTAL

use_config/nrf52840_ble

   text	   data	    bss	    dec	    hex	filename
 361728	   5104	  48728	 415560	  65748	rmk-nrf52840
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +2.59Ki  [ = ]       0    .debug_str
  +0.3%    +785  [ = ]       0    .strtab
  +0.1%    +380  +0.1%    +380    .text
  +0.1%    +343  [ = ]       0    .debug_line
  +0.2%    +256  [ = ]       0    .symtab
  +0.0%    +235  [ = ]       0    .debug_info
  +0.4%    +176  [ = ]       0    .debug_frame
  +0.1%     +56  [ = ]       0    .debug_aranges
  [ = ]       0  +0.0%      +8    .bss
  -1.6%      -1  [ = ]       0    [Unmapped]
  -0.1%    -248  [ = ]       0    .debug_ranges
  -0.1%    -599  [ = ]       0    .debug_loc
  +0.1% +3.95Ki  +0.1%    +388    TOTAL

use_config/nrf52840_ble_split

   text	   data	    bss	    dec	    hex	filename
 441592	   6428	  46864	 494884	  78d24	central

   text	   data	    bss	    dec	    hex	filename
 280400	   5792	  26520	 312712	  4c588	peripheral
Diff

Central Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.9% +32.4Ki  [ = ]       0    .debug_str
  +1.0% +7.30Ki  [ = ]       0    .debug_loc
  +0.3% +6.08Ki  [ = ]       0    .debug_info
  +0.6% +1.37Ki  [ = ]       0    .debug_ranges
  +0.2%    +928  +0.2%    +928    .text
  +0.2%    +811  [ = ]       0    .strtab
  +0.2%    +108  +0.2%    +108    .rodata
  [ = ]       0  +0.2%     +80    .bss
   +49%     +20  [ = ]       0    [Unmapped]
  +0.0%      +8  [ = ]       0    .debug_frame
  -0.9%      -8  [ = ]       0    .defmt
  -0.0%     -76  [ = ]       0    .debug_line
  -0.2%     -80  [ = ]       0    .debug_aranges
  -0.2%    -352  [ = ]       0    .symtab
  +0.6% +48.4Ki  +0.2% +1.09Ki    TOTAL

Peripheral Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1% +2.17Ki  [ = ]       0    .debug_str
  +0.1% +1.84Ki  [ = ]       0    .debug_info
  +0.2%    +837  [ = ]       0    .debug_loc
  +0.2%    +304  [ = ]       0    .debug_ranges
  +0.1%    +221  [ = ]       0    .debug_line
  +0.1%    +128  +0.1%    +128    .text
  [ = ]       0  +0.0%      +8    .bss
  -0.0%      -1  [ = ]       0    .strtab
  -0.0%      -8  [ = ]       0    .debug_frame
  -0.0%     -16  [ = ]       0    .symtab
  -0.2%     -64  [ = ]       0    .debug_aranges
  +0.1% +5.38Ki  +0.0%    +136    TOTAL

use_rust/nrf52840_ble_split

   text	   data	    bss	    dec	    hex	filename
 445344	   6428	  52576	 504348	  7b21c	central

   text	   data	    bss	    dec	    hex	filename
 278680	   5232	  25264	 309176	  4b7b8	peripheral
Diff

Central Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.7% +26.1Ki  [ = ]       0    .debug_str
  +0.3% +6.21Ki  [ = ]       0    .debug_info
  +0.6% +4.24Ki  [ = ]       0    .debug_loc
  +0.5% +1.12Ki  [ = ]       0    .debug_ranges
  +0.3% +1.06Ki  [ = ]       0    .strtab
  +0.1%    +384  [ = ]       0    .debug_line
  +0.4%    +204  [ = ]       0    .debug_frame
  +0.2%    +108  +0.2%    +108    .rodata
  [ = ]       0  +0.2%     +80    .bss
  +0.0%      +8  [ = ]       0    .debug_aranges
   +14%      +7  [ = ]       0    [Unmapped]
  -0.9%      -8  [ = ]       0    .defmt
  -0.1%    -368  -0.1%    -368    .text
  +0.5% +39.0Ki  -0.0%    -180    TOTAL

Peripheral Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.1%    +829  [ = ]       0    .debug_info
  +0.0%    +519  [ = ]       0    .debug_str
  +0.0%     +49  [ = ]       0    .debug_line
  +0.0%     +49  [ = ]       0    .debug_loc
  +0.0%     +28  +0.0%     +28    .text
  +0.0%     +24  [ = ]       0    .debug_ranges
  [ = ]       0  +0.0%      +8    .bss
  -0.1%     -16  [ = ]       0    .debug_frame
  -0.0%     -19  [ = ]       0    .strtab
 -43.7%     -31  [ = ]       0    [Unmapped]
  -0.0%     -32  [ = ]       0    .symtab
  -0.2%     -72  [ = ]       0    .debug_aranges
  +0.0% +1.30Ki  +0.0%     +36    TOTAL

use_config/pi_pico_w_ble

   text	   data	    bss	    dec	    hex	filename
 605272	      0	  55792	 661064	  a1648	rmk-pi-pico-w
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +933  [ = ]       0    .debug_info
  +0.0%    +790  [ = ]       0    .debug_str
  +0.1%    +720  [ = ]       0    .debug_loc
  +0.2%    +448  [ = ]       0    .strtab
  +0.1%    +318  [ = ]       0    .debug_line
  +0.1%    +168  +0.1%    +168    .text
  +0.1%    +152  [ = ]       0    .debug_ranges
  +0.2%    +128  [ = ]       0    .symtab
  +0.2%     +88  [ = ]       0    .debug_frame
  +0.1%     +24  [ = ]       0    .debug_aranges
  [ = ]       0  +0.0%      +8    .bss
 -20.0%     -13  [ = ]       0    [Unmapped]
  +0.0% +3.67Ki  +0.0%    +176    TOTAL

use_config/pi_pico_w_ble_split

   text	   data	    bss	    dec	    hex	filename
 637428	      0	  62528	 699956	  aae34	central

   text	   data	    bss	    dec	    hex	filename
 495760	      0	  41796	 537556	  833d4	peripheral
Diff

Central Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.8% +32.3Ki  [ = ]       0    .debug_str
  +0.3% +6.91Ki  [ = ]       0    .debug_info
  +0.5% +1.50Ki  [ = ]       0    .strtab
  +0.1% +1.42Ki  [ = ]       0    .debug_loc
  +0.4%    +968  [ = ]       0    .debug_ranges
  +0.6%    +272  [ = ]       0    .debug_frame
  +0.0%    +108  +0.0%    +108    .rodata
  [ = ]       0  +0.1%     +80    .bss
  +0.1%     +48  [ = ]       0    .symtab
  +0.1%     +32  [ = ]       0    .debug_aranges
   +52%     +22  [ = ]       0    [Unmapped]
  -1.7%     -16  [ = ]       0    .defmt
  -0.0%     -60  -0.0%     -60    .text
  -0.0%    -101  [ = ]       0    .debug_line
  +0.5% +43.4Ki  +0.0%    +128    TOTAL

Peripheral Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +1.17Ki  [ = ]       0    .debug_str
  +0.1%    +957  [ = ]       0    .debug_info
  +0.1%    +689  [ = ]       0    .debug_loc
  +0.1%    +112  [ = ]       0    .debug_ranges
  +0.0%     +85  [ = ]       0    .debug_line
  +0.0%     +28  +0.0%     +28    .text
  [ = ]       0  +0.0%      +8    .bss
  +6.5%      +4  [ = ]       0    [Unmapped]
  -0.0%      -7  [ = ]       0    .strtab
  -0.2%     -64  [ = ]       0    .debug_aranges
  +0.0% +2.93Ki  +0.0%     +36    TOTAL

use_rust/pi_pico_w_ble_split

   text	   data	    bss	    dec	    hex	filename
 638244	      0	  62848	 701092	  ab2a4	central

   text	   data	    bss	    dec	    hex	filename
 496364	      0	  41796	 538160	  83630	peripheral
Diff

Central Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.8% +31.4Ki  [ = ]       0    .debug_str
  +0.4% +8.78Ki  [ = ]       0    .debug_info
  +0.2% +1.96Ki  [ = ]       0    .debug_loc
  +0.5% +1.48Ki  [ = ]       0    .strtab
  +0.6%    +272  [ = ]       0    .debug_frame
  +0.1%    +248  [ = ]       0    .debug_ranges
  +0.0%    +108  +0.0%    +108    .rodata
  [ = ]       0  +0.1%     +80    .bss
  +0.1%     +32  [ = ]       0    .debug_aranges
  -1.7%     -16  [ = ]       0    .defmt
 -41.9%     -31  [ = ]       0    [Unmapped]
  -0.1%     -48  [ = ]       0    .symtab
  -0.0%     -48  -0.0%     -48    .text
  -0.9%     -96  [ = ]       0    .debug_abbrev
  -0.1%    -387  [ = ]       0    .debug_line
  +0.5% +43.6Ki  +0.0%    +140    TOTAL

Peripheral Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0% +1.17Ki  [ = ]       0    .debug_str
  +0.1%    +957  [ = ]       0    .debug_info
  +0.1%    +679  [ = ]       0    .debug_loc
  +0.1%    +112  [ = ]       0    .debug_ranges
  +0.0%     +85  [ = ]       0    .debug_line
  +0.0%     +28  +0.0%     +28    .text
  [ = ]       0  +0.0%      +8    .bss
  +9.1%      +6  [ = ]       0    [Unmapped]
  -0.0%      -7  [ = ]       0    .strtab
  -0.2%     -64  [ = ]       0    .debug_aranges
  +0.0% +2.92Ki  +0.0%     +36    TOTAL

use_config/rp2040

   text	   data	    bss	    dec	    hex	filename
 134416	      0	  15664	 150080	  24a40	rmk-rp2040
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.5% +1.43Ki  [ = ]       0    .debug_loc
  +0.1%   +1004  [ = ]       0    .debug_info
  +0.1%    +766  [ = ]       0    .debug_str
  +0.3%    +438  [ = ]       0    .debug_line
  +0.3%    +344  [ = ]       0    .strtab
  +0.3%    +256  [ = ]       0    .debug_ranges
  +0.2%    +248  +0.2%    +248    .text
  +0.3%     +56  [ = ]       0    .debug_frame
  +0.1%     +48  [ = ]       0    .symtab
  +0.1%     +16  [ = ]       0    .debug_aranges
  [ = ]       0  +0.1%      +8    .bss
   +12%      +7  [ = ]       0    [Unmapped]
  +0.1% +4.54Ki  +0.2%    +256    TOTAL

use_config/rp2040_split

   text	   data	    bss	    dec	    hex	filename
 145360	      0	  16740	 162100	  27934	central

   text	   data	    bss	    dec	    hex	filename
  23884	     56	   2412	  26352	   66f0	peripheral
Diff

Central Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.1% +16.6Ki  [ = ]       0    .debug_str
  +0.3% +2.85Ki  [ = ]       0    .debug_info
  +0.8%    +152  [ = ]       0    .debug_frame
  +0.4%    +128  [ = ]       0    .symtab
  +0.5%     +88  +0.5%     +88    .rodata
  [ = ]       0  +0.4%     +56    .bss
   +83%     +30  [ = ]       0    [Unmapped]
  +0.0%      +8  [ = ]       0    .debug_aranges
  -0.1%     -91  [ = ]       0    .debug_line
  -0.2%    -173  [ = ]       0    .strtab
  -0.2%    -212  -0.2%    -212    .text
  -0.6%    -544  [ = ]       0    .debug_ranges
  -0.5% -1.52Ki  [ = ]       0    .debug_loc
  +0.5% +17.3Ki  -0.0%     -68    TOTAL

Peripheral Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.2%    +725  [ = ]       0    .debug_loc
  +0.2%    +723  [ = ]       0    .debug_info
  +1.4%    +208  [ = ]       0    .debug_ranges
  +0.3%    +170  [ = ]       0    .debug_line
  +0.6%    +108  +0.6%    +108    .text
  +0.0%     +30  [ = ]       0    .debug_str
  [ = ]       0  +0.6%      +8    .bss
 -24.1%     -14  [ = ]       0    [Unmapped]
  -0.7%     -36  [ = ]       0    .debug_frame
  -0.6%     -48  [ = ]       0    .symtab
  -0.4%     -56  [ = ]       0    .debug_aranges
  -0.6%    -106  [ = ]       0    .strtab
  +0.1% +1.66Ki  +0.4%    +116    TOTAL

use_rust/rp2040_split

   text	   data	    bss	    dec	    hex	filename
 144224	      0	  16336	 160560	  27330	central

   text	   data	    bss	    dec	    hex	filename
  24548	     56	   2676	  27280	   6a90	peripheral
Diff

Central Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.1% +17.3Ki  [ = ]       0    .debug_str
  +0.3% +3.25Ki  [ = ]       0    .debug_info
  +0.5% +1.63Ki  [ = ]       0    .debug_loc
  +0.5%    +192  [ = ]       0    .symtab
  +0.8%    +152  [ = ]       0    .debug_frame
  +0.5%     +88  +0.5%     +88    .rodata
  +0.1%     +64  [ = ]       0    .debug_ranges
  [ = ]       0  +0.2%     +32    .bss
  +0.0%      +8  [ = ]       0    .debug_aranges
  -1.8%      -8  [ = ]       0    .defmt
 -15.8%      -9  [ = ]       0    [Unmapped]
  -0.1%    -112  [ = ]       0    .debug_line
  -0.1%    -161  [ = ]       0    .strtab
  -0.2%    -240  -0.2%    -240    .text
  +0.6% +22.2Ki  -0.1%    -120    TOTAL

Peripheral Diff

    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +1.3%    +745  [ = ]       0    .debug_loc
  +0.2%    +672  [ = ]       0    .debug_info
  +1.4%    +208  [ = ]       0    .debug_ranges
  +0.3%    +182  [ = ]       0    .debug_line
  +0.5%    +104  +0.5%    +104    .text
  +0.0%     +30  [ = ]       0    .debug_str
  [ = ]       0  +0.5%      +8    .bss
 -12.1%      -7  [ = ]       0    [Unmapped]
  -0.7%     -36  [ = ]       0    .debug_frame
  -0.6%     -48  [ = ]       0    .symtab
  -0.4%     -56  [ = ]       0    .debug_aranges
  -0.6%    -106  [ = ]       0    .strtab
  +0.1% +1.65Ki  +0.4%    +112    TOTAL

use_config/stm32f1

   text	   data	    bss	    dec	    hex	filename
  56988	     24	   8048	  65060	   fe24	rmk-stm32f1
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.9% +1.07Ki  [ = ]       0    .debug_loc
  +1.1%    +364  [ = ]       0    .strtab
  +0.7%    +296  [ = ]       0    .debug_ranges
  +0.5%     +96  [ = ]       0    .symtab
  +0.2%     +88  +0.2%     +88    .text
  +0.5%     +68  [ = ]       0    .debug_frame
  +0.1%     +66  [ = ]       0    .debug_line
  +0.4%     +24  [ = ]       0    .debug_aranges
   +18%      +9  [ = ]       0    [Unmapped]
  [ = ]       0  +0.1%      +8    .bss
  -0.0%    -195  [ = ]       0    .debug_str
  -0.0%    -240  [ = ]       0    .debug_info
  +0.1% +1.64Ki  +0.1%     +96    TOTAL

use_config/stm32f4

   text	   data	    bss	    dec	    hex	filename
 129944	    320	  16192	 146456	  23c18	rmk-stm32f4
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.0%    +437  [ = ]       0    .debug_info
  +0.1%    +330  [ = ]       0    .debug_loc
  +0.2%    +262  [ = ]       0    .strtab
  +0.2%    +204  +0.2%    +204    .text
  +0.1%    +147  [ = ]       0    .debug_line
  +0.1%     +32  [ = ]       0    .symtab
   +70%     +23  [ = ]       0    [Unmapped]
  [ = ]       0  +0.1%      +8    .bss
  -0.0%      -4  [ = ]       0    .debug_frame
  -0.1%     -16  [ = ]       0    .debug_aranges
  -0.0%     -35  [ = ]       0    .debug_str
  -0.2%    -200  [ = ]       0    .debug_ranges
  +0.0% +1.15Ki  +0.1%    +212    TOTAL

use_config/stm32h7

   text	   data	    bss	    dec	    hex	filename
  92360	    264	  10456	 103080	  192a8	rmk-stm32h7
Diff
    FILE SIZE        VM SIZE    
 --------------  -------------- 
  +0.4%    +792  [ = ]       0    .debug_loc
  +0.7%    +373  [ = ]       0    .strtab
  +0.0%    +287  [ = ]       0    .debug_info
  +0.0%    +270  [ = ]       0    .debug_str
  +0.5%    +144  [ = ]       0    .symtab
  +0.2%    +112  [ = ]       0    .debug_ranges
  +0.1%    +108  +0.1%    +108    .text
  +0.1%    +100  [ = ]       0    .debug_line
  +0.4%     +64  [ = ]       0    .debug_frame
  +0.1%     +24  [ = ]       0    .debug_aranges
   +37%     +18  [ = ]       0    [Unmapped]
  [ = ]       0  +0.1%      +8    .bss
  +0.1% +2.24Ki  +0.1%    +116    TOTAL

@HaoboGu
Copy link
Copy Markdown
Owner

HaoboGu commented Mar 30, 2026

Thanks for proposing it!

The overall architecture looks really good to me. Decoupling renderer and the processor is great.

What I'm thinking about is, can we make both processor and renderer be abled to be implemented in a third-party crate? I'm imaging:

# Cargo.toml
rmk = { version = "*", feature = ["display", ] }

rmk-ssd1306-processor = { .. } # Display driver support
rmk-custom-renderer = { .. } # Display renderer support

Then in keyboard.toml, we can declare something like

# keyboard.toml
[display]
processor = "Ssd1306Processor"
renderer = "CustomRenderer"

where Ssd1306Processor and CustomRenderer are exported in rmk-ssd1306-processor and rmk-custom-renderer.

I prefer not to configure "widgets" in keyboard.toml because it's hard to define all widgets in the RMK's display interface -- I think it might limit the design of the display UI. It should be somehow defined in the renderer crate, not in RMK

@gbPagano
Copy link
Copy Markdown
Contributor Author

Thanks for the feedback! I really like the direction of decoupling the display driver from the RMK core. Here's what I'm thinking for the architecture:

1. rmk core - traits and default renderer only

The core would export only the generic abstractions, with no driver dependencies:

  • DisplayRenderer<C: PixelColor> - generic over the pixel color type so it works for both monochrome OLEDs and color LCDs
  • RenderContext - the keyboard state snapshot (unchanged)
  • DefaultRenderer - built-in renderer implementing DisplayRenderer<BinaryColor>

2. rmk-displays - a single crate with feature-gated drivers

Rather than one crate per display chip, I think a single rmk-displays crate with features makes more sense, since embedded-graphics already unifies the rendering API — the processors share the same event-handling logic and only differ in the driver init/flush layer:

# User's Cargo.toml
rmk-displays = { version = "*", features = ["ssd1306"] }

Each feature would expose a processor (Ssd1306DisplayProcessor<R>, Sh1106DisplayProcessor<R>, etc.), all generic over the renderer. This could live in the same repo as a workspace member.

3. Custom renderers via keyboard.toml

For use_config users who want a custom renderer, I'm thinking the TOML would accept an optional full path:

[display]
driver = "ssd1306"
interface = "i2c"
size = "128x64"
i2c_scl = "PIN_5"
i2c_sda = "PIN_4"
# Optional - omit for DefaultRenderer
renderer = "my_crate::MyRenderer"

The macro would generate:

  • If renderer is omitted: instantiate with DefaultRenderer
  • If renderer is set: parse it as a Rust path and call Default::default() to construct it (the custom renderer must implement Default)

The constraint that custom renderers need Default is reasonable since renderers are typically stateless. For anything more complex, use_rust gives full control.

4. use_rust - unchanged

Users who want full control keep using the processor directly, optionally with a custom renderer, exactly as it works today. They could also use #[register_processor] to wire things up manually.


I agree with your point about not defining widgets in keyboard.toml, the renderer crate is the right place for UI design decisions, not the config file.

What do you think about this approach?

@HaoboGu
Copy link
Copy Markdown
Owner

HaoboGu commented Mar 30, 2026

In RMK core, the built-in renderer(DefaultRenderer) and processor(such as Ssd1306DisplayProcessor) can be provided, that's fine. I just want to ensure that the Renderer/Processor can be implemented as a third crate.

I agree with your for other points. The only comment from me is the keyboard.toml configuration, I prefer to group the configuration for common interfaces. For example, for a display driver using i2c:

driver = "ssd1306"
interface = "i2c"
i2c = { scl = "", sda = "",  }
size = "128x64"

@gbPagano
Copy link
Copy Markdown
Contributor Author

Ok! I'll move forward with the implementation and come back with updates soon

@gbPagano gbPagano force-pushed the feat/oled-displays branch 3 times, most recently from 41ef80e to cd8d8fa Compare April 1, 2026 15:48
@gbPagano
Copy link
Copy Markdown
Contributor Author

gbPagano commented Apr 1, 2026

Hey @HaoboGu , here's an update on the progress since our last discussion.

keyboard.toml configuration

The [display] section uses the grouped interface config you suggested, reusing the existing CommunicationProtocol enum (I2c/Spi):

[display]
driver = "ssd1306"
size = "128x32"
rotation = 270
renderer = "custom_renderer::BigLayerRenderer"  # optional

[display.protocol.i2c]
instance = "I2C1"
scl = "PIN_3"
sda = "PIN_2"
address = 60

It also works in split configs via [split.central.display] and [split.peripheral.display].

Custom renderer support

As we discussed, custom renderers are referenced by path in keyboard.toml. I added an example library at examples/use_rust/custom_renderer/ that the use_config/rp2040_oled example uses as a dependency.

Driver re-exports

Driver crates (ssd1306, oled_async, display-interface-i2c) are re-exported from rmk::display::* so use_config users don't need to add them manually.

Other changes since last push

  • Render rate-limiting — configurable minimum interval (default 30ms) to prevent high-frequency events from overwhelming displays
  • More event subscriptionsKeyboardEvent, SleepStateEvent, BleStatusChangeEvent, PeripheralConnectedEvent, CentralConnectedEvent, PeripheralBatteryEvent.
  • oled_async driver support — SH1106, SH1107, SH1108, SSD1309 via feature flags

SPI interface

The config and codegen are prepared for SPI (via CommunicationProtocol::Spi), but the actual SPI initialization is not implemented yet — I don't have SPI display hardware to test against, so I preferred to leave it as a clear panic rather than ship untested code.

Next steps

If the current architecture looks good to you, my planned next steps are:

  1. Improve the custom_renderer example to better showcase the API
  2. Improve the default renderers (BLE status, split peripheral info, better layouts)
  3. Write user-facing documentation for the RMK docs site

@HaoboGu
Copy link
Copy Markdown
Owner

HaoboGu commented Apr 2, 2026

Thanks for your effort!

I just went through the code roughly, overall it's really great. Some initial comments from me:

  1. In render the DisplayProcessor checks the min interval, is it possible to use the polling based processor here? If so, all self.render().await; in on_xx_event can also be removed.
  2. There are width and height in RenderContext, I'm not sure this is the correct way, maybe associated const is better in this case? I understand that introducing many generics is verbose, but it's just kind of "strange" that display's width and height are in RenderContext. It should be a parameter/field of the display imo
  3. oled_async is a git dependency. It's fine right now but if we want to release the next version, the crates.io version should be there.

And I also found a bug that might block the DisplayProcessor, I've pushed a fix: #787

@gbPagano
Copy link
Copy Markdown
Contributor Author

gbPagano commented Apr 2, 2026

Thanks for the review!

1. Polling-based rendering

I'm actually working on this right now, the DisplayProcessor will support a configurable render_interval that controls the poll frequency, with Duration::MAX (effectively disabled) as the default. I'll push a commit soon.

That said, I don't think we should remove self.render() from the event handlers entirely, for two reasons:

  • Some state changes need to be reflected on the display practically instantly (e.g. a key press triggering a visual response).
  • Users may want to disable polling altogether to save battery and allow the keyboard to enter sleep mode. In that case, event-driven renders are the only way the display updates.

  1. You're right, these don't belong in RenderContext. The renderer already receives &mut D which implements DrawTarget, so it can call display.bounding_box().size directly. I'll remove them.

  2. The reason for the git dependency is that the SH1106 driver isn't included in the latest crates.io release yet. I'll reach out to the crate author about publishing a new version.

@HaoboGu
Copy link
Copy Markdown
Owner

HaoboGu commented Apr 2, 2026

yeah, making DisplayProcessor support both polling and event driven mode is fine. Let's keep self.render() then

@HaoboGu
Copy link
Copy Markdown
Owner

HaoboGu commented Apr 3, 2026

#788 was merged

gbPagano added 16 commits April 3, 2026 20:30
  Introduce a `display` feature flag with:
  - `DisplayRenderer` trait for user-defined rendering
  - `RenderContext` to decouple state from the display driver
  - `DefaultRenderer` with auto landscape/portrait layout
  - `OledDisplayProcessor` generic over the renderer (defaults to DefaultRenderer)
  - `examples/use_rust/rp2040_oled` with I2C SSD1306 setup
Introduce DisplayDriver trait extending DrawTarget with async init/flush,
replacing the SSD1306-specific OledDisplayProcessor. The processor is now
DisplayProcessor<D, R> generic over any DisplayDriver implementation.

Driver support is feature-gated: "display" provides base traits and
processor, "ssd1306" adds the SSD1306 impl. Clear responsibility moved
to the renderer. DefaultRenderer renamed to DefaultOledRenderer.
…s internal state

- Add KeyboardEvent, SleepStateEvent, BleStatusChangeEvent, PeripheralConnectedEvent,
CentralConnectedEvent, and PeripheralBatteryEvent subscriptions to DisplayProcessor.
- Extend RenderContext with the corresponding fields and use it directly as the
processor's internal state to avoid field duplication.
- Bump keyboard event default subs from 2 to 3 to accommodate the new subscriber.
Throttle display refreshes to prevent high-frequency events (e.g. KeyboardEvent)
from overwhelming slow I2C displays. Renders are skipped when the interval since
the last refresh is below a configurable minimum (default 30 ms); the latest
state is drawn on the next event that passes the time check.
Enable display setup via [display] section in keyboard.toml.
Supports SSD1306 and oled_async drivers (SH1106, SH1107, SH1108, SSD1309)
over I2C, with optional custom renderer via `renderer` field.

- Add DisplayConfig, DisplayDriver enum, and protocol-based I2C config
to rmk-config
- Add display codegen for all chip families (RP2040, NRF52, STM32, ESP32)
- Wire display processor and I2C interrupts into orchestrator and split
peripheral codegen
- Re-export driver crates from rmk::display for use_config compatibility
- Add use_config/rp2040_oled example
Add a standalone no_std library demonstrating how to implement a custom
DisplayRenderer, and wire it into the use_config/rp2040_oled example
via the `renderer` field in keyboard.toml.
…tom_renderer example

- Add `manual_polling` option to #[processor] macro, allowing manual PollingProcessor
implementations with dynamic interval control
- Add `render_interval` and `min_render_interval` to DisplayConfig, configurable
via keyboard.toml
- DisplayProcessor uses Duration::MAX by default (no polling), with builder methods
to enable periodic redraws for animations
- Fix overflow in PollingProcessor::polling_loop by using Timer::at with checked_add
instead of Timer::after
- Improve custom_renderer example using BongoCat Animation
- Skip redundant render on WpmUpdateEvent
Renderers can obtain display dimensions directly from the DrawTarget
via display.bounding_box().size, so there's no need to duplicate them
in RenderContext.
When the `display` feature is enabled, the central now forwards WPM,
modifier state, and sleep state to each peripheral via new SplitMessage
variants (Wpm, Modifier, SleepState), all gated behind `#[cfg(feature =
"display")]`. Peripherals republish these as local events, allowing a
peripheral-side display to render the same keyboard state as the central.
gbPagano added 2 commits April 3, 2026 20:30
…tructure

Removes renderer.rs, merging its traits directly into mod.rs.
Built-in renderers move to a new display/renderers/ submodule.
@gbPagano gbPagano force-pushed the feat/oled-displays branch from 1a4e5e0 to 6976c9f Compare April 3, 2026 23:31
@gbPagano
Copy link
Copy Markdown
Contributor Author

gbPagano commented Apr 4, 2026

Finished implementing the built-in display renderers, now living under rmk::display:

  • OledRenderer: full-featured landscape/portrait adaptive layout with modifier icons, BLE indicator, battery widget, and lock dots.
  • LogoRenderer: static RMK logo splash screen. Now the default renderer for DisplayProcessor. Logo data is embedded as a const.

Both are exported directly from rmk::display and can be referenced by bare name in keyboard.toml:

renderer = "OledRenderer"   # instead of rmk::display::OledRenderer
renderer = "LogoRenderer"

The macro resolves bare names to ::rmk::display:: automatically.

Display events forwarded to split peripherals

Under the display feature, the central now forwards three additional events to each peripheral:

WpmUpdateEvent, ModifierEvent and SleepStateEvent

All three variants and their handling on both sides are gated behind #[cfg(feature = "display")], so there is no impact on builds without the display feature.

Peripherals republish these as local events, allowing a peripheral-side display to render the same keyboard state (WPM, active modifiers, sleep) as the central.

@gbPagano
Copy link
Copy Markdown
Contributor Author

gbPagano commented Apr 4, 2026

I think the implementation is done. What do you think, @HaoboGu? If everything looks good, only the documentation is left.

@gbPagano gbPagano marked this pull request as ready for review April 4, 2026 00:07
gbPagano added 2 commits April 4, 2026 22:02
…counts

Add display to rmk-types so CARGO_FEATURE_DISPLAY is visible during build.rs,
enabling display-gated subscriber bumps in subscriber_default.toml.
Fix clear_peer count = 2 for split+BLE with 2 peripherals.
Copy link
Copy Markdown
Owner

@HaoboGu HaoboGu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I've left several comments about the polling controller, other parts are all good!

{
fn interval(&self) -> Duration {
if self.ctx.sleeping {
Duration::MAX
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to wake up the keyboard display from sleeping?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the display automatically wakes up

Since self.interval() is evaluated dynamically on each loop tick via select!(), when the keyboard receives a key event and ctx.sleeping flips to false, the next iteration returns the normal render_interval and polling resumes

The event itself is also rendered immediately via self.render().await in the event handler


/// Polling loop that processes events and calls `update()` at the specified interval.
///
/// When `interval()` is so large that `last + interval` overflows, the timer
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, the interval should not be that large. PollingController is not designed for this case. Please revert changes here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually ran into panics specifically when polling was disabled (interval() = Duration::MAX to effectively turn off polling)

Any cleaner alternative to avoid Duration::MAX in polling/sleep?

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.

2 participants