Skip to main content

rmk_q1_pro_iso/backlight/
init.rs

1//! Backlight task initialization and runtime loop.
2
3use crate::backlight::{
4    gamma_correction::gamma_correction,
5    lock_indicator::{BACKLIGHT_CH, BacklightCmd},
6    mapping::iso_knob::LED_LAYOUT,
7};
8use embassy_stm32::{i2c, i2c::I2c, mode::Async};
9use embassy_time::{Duration, Ticker, Timer};
10use embedded_hal_async::i2c::ErrorType;
11use rmk::embassy_futures::select::{Either, select};
12use snled27351_driver::{
13    driver::Driver,
14    transport::i2c::{I2cTransport, I2cTransportError},
15};
16
17/// Concrete I2C transport type for this keyboard.
18type Transport = I2cTransport<I2c<'static, Async, i2c::Master>, 2>;
19/// Concrete driver type for this keyboard.
20type BacklightDriver = Driver<Transport, 2>;
21
22/// LED index for the Caps Lock indicator key.
23const CAPS_LOCK_LED_INDEX: usize = 45;
24/// Brightness percentage applied to indicator LEDs (0–100).
25const INDICATOR_BRIGHTNESS: u8 = 100;
26/// RGB colour for the active Caps Lock indicator state (red).
27const INDICATOR_RED: (u8, u8, u8) = (255, 0, 0);
28/// RGB colour for the inactive Caps Lock indicator state (white).
29const INDICATOR_WHITE: (u8, u8, u8) = (255, 255, 255);
30/// Number of brightness steps in the soft-start ramp.
31const SOFTSTART_STEPS: u8 = 50;
32/// Total duration of the soft-start ramp in milliseconds.
33const SOFTSTART_RAMP_MS: u32 = 1000;
34/// Brightness percentage applied when a driver chip reports temperature ≥ 70
35/// °C.
36const THERMAL_THROTTLE_BRIGHTNESS: u8 = 50;
37/// Interval between thermal register polls.
38const THERMAL_POLL: Duration = Duration::from_secs(5);
39
40#[inline]
41const fn scale(value: u8, brightness_percent: u8) -> u8 {
42    let v = u16::from(value);
43    let p = u16::from(brightness_percent.min(100));
44    u8::try_from(v.saturating_mul(p).saturating_add(50).checked_div(100).unwrap_or_default()).unwrap_or(255)
45}
46
47#[inline]
48const fn correct(red: u8, green: u8, blue: u8, brightness_percent: u8) -> (u8, u8, u8) {
49    (
50        gamma_correction(scale(red, brightness_percent)),
51        gamma_correction(scale(green, brightness_percent)),
52        gamma_correction(scale(blue, brightness_percent)),
53    )
54}
55
56/// Full backlight state, kept in one place so any path can do a consistent
57/// redraw without losing information.
58#[derive(Clone, Copy)]
59struct BacklightState {
60    /// Whether Caps Lock is currently active.
61    caps_lock: bool,
62    /// Global brightness percentage (0–100); reduced under thermal throttle.
63    brightness: u8,
64}
65
66impl BacklightState {
67    const fn new() -> Self { Self { caps_lock: false, brightness: 100 } }
68}
69
70/// Writes every LED to hardware according to `state`.
71///
72/// Paints the background white at the current brightness, then overlays the
73/// Caps Lock indicator. Called after any state change requiring a full redraw
74/// (e.g. thermal-throttle transitions).
75///
76/// # Errors
77///
78/// Returns `Err` if any bus transaction fails.
79async fn render_all(
80    driver: &mut BacklightDriver,
81    state: BacklightState,
82) -> Result<(), I2cTransportError<<I2c<'static, Async, i2c::Master> as ErrorType>::Error>> {
83    let (r, g, b) = correct(255, 255, 255, state.brightness);
84    driver.stage_all_leds(r, g, b);
85    render_indicator(driver, state).await
86}
87
88/// Writes only the Caps Lock indicator LED according to `state`.
89///
90/// Used when only indicator state has changed and the background is already
91/// correct, avoiding a full redraw.
92///
93/// # Errors
94///
95/// Returns `Err` if any bus transaction fails.
96async fn render_indicator(
97    driver: &mut BacklightDriver,
98    state: BacklightState,
99) -> Result<(), I2cTransportError<<I2c<'static, Async, i2c::Master> as ErrorType>::Error>> {
100    // Red when Caps Lock is active, white when inactive.
101    let color = if state.caps_lock { INDICATOR_RED } else { INDICATOR_WHITE };
102    let (r, g, b) = correct(color.0, color.1, color.2, INDICATOR_BRIGHTNESS);
103    driver.stage_led(CAPS_LOCK_LED_INDEX, r, g, b);
104    driver.flush().await
105}
106
107/// Performs a soft-start brightness ramp on all LEDs at initialization.
108///
109/// Linearly steps brightness from 0 up to `target_brightness` over
110/// [`SOFTSTART_STEPS`] increments across [`SOFTSTART_RAMP_MS`] milliseconds.
111///
112/// # Errors
113///
114/// Returns `Err` if any bus transaction fails during the ramp.
115#[optimize(size)]
116async fn softstart(
117    driver: &mut BacklightDriver,
118    base_red: u8,
119    base_green: u8,
120    base_blue: u8,
121    target_brightness: u8,
122) -> Result<(), I2cTransportError<<I2c<'static, Async, i2c::Master> as ErrorType>::Error>> {
123    let target = u32::from(target_brightness.min(100));
124    let steps = u32::from(SOFTSTART_STEPS.max(1));
125    let delay_ms = u64::from(SOFTSTART_RAMP_MS.checked_div(steps).unwrap_or(1).max(1));
126
127    for step in 0..=steps {
128        let percent = u8::try_from(target.saturating_mul(step).saturating_div(steps)).unwrap_or(0);
129        let (r, g, b) = correct(base_red, base_green, base_blue, percent);
130        driver.set_all_leds(r, g, b).await?;
131        if step < steps {
132            Timer::after_millis(delay_ms).await;
133        }
134    }
135    Ok(())
136}
137
138/// Runs the backlight controller loop.
139///
140/// Initializes both SNLED27351 driver chips, performs a soft-start brightness
141/// ramp, then enters an event loop that concurrently handles two concerns:
142///
143/// - **Indicator commands** arriving on [`BACKLIGHT_CH`]: updates the Caps Lock
144///   LED without touching the rest of the matrix.
145/// - **Thermal polling** on a [`THERMAL_POLL`] ticker: reads the TDF register
146///   from both chips and reduces overall brightness to
147///   [`THERMAL_THROTTLE_BRIGHTNESS`] if either exceeds 70 °C, restoring it
148///   automatically when both cool down. A full redraw is issued on each
149///   transition so the indicator LED is never lost.
150#[optimize(size)]
151pub async fn backlight_runner(i2c: I2c<'static, Async, i2c::Master>, addr0: u8, addr1: u8) -> ! {
152    let transport = I2cTransport::new(i2c, [addr0, addr1]);
153    let mut driver = BacklightDriver::new(transport, LED_LAYOUT);
154
155    if driver.init(0xFF).await.is_ok() {
156        let _ = softstart(&mut driver, 255, 255, 255, 100).await;
157    }
158
159    let rx = BACKLIGHT_CH.receiver();
160    let mut thermal_ticker = Ticker::every(THERMAL_POLL);
161    let mut state = BacklightState::new();
162
163    loop {
164        match select(rx.receive(), thermal_ticker.next()).await {
165            Either::First(cmd) => match cmd {
166                BacklightCmd::Indicators { caps } => {
167                    state.caps_lock = caps;
168                    // Background is already correct; only repaint the indicator.
169                    let _ = render_indicator(&mut driver, state).await;
170                }
171            },
172            Either::Second(_) => {
173                // Poll both chips; throttle if either exceeds the 70 °C threshold.
174                let hot = driver.check_thermal_flag_set(0).await || driver.check_thermal_flag_set(1).await;
175                let new_brightness = if hot { THERMAL_THROTTLE_BRIGHTNESS } else { 100 };
176
177                // Only rewrite all LEDs when the brightness level actually changes.
178                if new_brightness != state.brightness {
179                    state.brightness = new_brightness;
180                    let _ = render_all(&mut driver, state).await;
181                }
182            }
183        }
184    }
185}