feat: add OLED display support with DisplayRenderer trait#782
feat: add OLED display support with DisplayRenderer trait#782gbPagano wants to merge 24 commits intoHaoboGu:mainfrom
Conversation
Binary Size Reportuse_config/nrf52832_bleDiffuse_config/nrf52840_bleDiffuse_config/nrf52840_ble_splitDiffCentral DiffPeripheral Diffuse_rust/nrf52840_ble_splitDiffCentral DiffPeripheral Diffuse_config/pi_pico_w_bleDiffuse_config/pi_pico_w_ble_splitDiffCentral DiffPeripheral Diffuse_rust/pi_pico_w_ble_splitDiffCentral DiffPeripheral Diffuse_config/rp2040Diffuse_config/rp2040_splitDiffCentral DiffPeripheral Diffuse_rust/rp2040_splitDiffCentral DiffPeripheral Diffuse_config/stm32f1Diffuse_config/stm32f4Diffuse_config/stm32h7Diff |
|
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 supportThen in # keyboard.toml
[display]
processor = "Ssd1306Processor"
renderer = "CustomRenderer"
where 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 |
|
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.
|
|
In RMK core, the built-in renderer( I agree with your for other points. The only comment from me is the driver = "ssd1306"
interface = "i2c"
i2c = { scl = "", sda = "", }
size = "128x64" |
|
Ok! I'll move forward with the implementation and come back with updates soon |
41ef80e to
cd8d8fa
Compare
|
Hey @HaoboGu , here's an update on the progress since our last discussion.
|
|
Thanks for your effort! I just went through the code roughly, overall it's really great. Some initial comments from me:
And I also found a bug that might block the |
|
Thanks for the review! 1. Polling-based rendering I'm actually working on this right now, the That said, I don't think we should remove
|
|
yeah, making |
|
#788 was merged |
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.
…tructure Removes renderer.rs, merging its traits directly into mod.rs. Built-in renderers move to a new display/renderers/ submodule.
1a4e5e0 to
6976c9f
Compare
|
I think the implementation is done. What do you think, @HaoboGu? If everything looks good, only the documentation is left. |
…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.
HaoboGu
left a comment
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Is there a way to wake up the keyboard display from sleeping?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
I don't think so, the interval should not be that large. PollingController is not designed for this case. Please revert changes here.
There was a problem hiding this comment.
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?






This PR adds initial OLED display support to RMK via a new
displayfeature flag. The goal is to get early feedback on the architecture and direction before investing in the full feature set.What's included
DisplayRenderertrait — the core abstraction that allows users to fully customize what is drawn on the display: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.Rdefaults toDefaultRenderer, so::new(display)works out of the box and::with_renderer(display, my_renderer)gives full control.examples/use_rust/rp2040_oledupdated with a working I2C OLED setup.Architecture
The design mirrors the two user profiles RMK already supports:
use_rustusers implementDisplayRendererfor full control.use_configusers (future) would declare widgets inkeyboard.tomland the macro would generate the renderer.Not yet included: support for displays other than SSD1306 (SH1106, SSD1309, etc.), an SPI example,
keyboard.tomlconfiguration for widget selection and orientation, and public helper functions for custom renderers to reuse.Questions for review
keyboard.tomlintegration: 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.