Skip to main content

openlogi_device/
backlight.rs

1//! HID++ `Backlight` (feature `0x1982`) — keyboard backlight control.
2//!
3//! The protocol-level `0x1982` wrapper lives in `openlogi-hidpp`; this module
4//! keeps OpenLogi's IPC/config-facing mode, status, and snapshot types.
5//!
6//! This is the backlight family used by the MX Keys line: a white,
7//! level-adjustable backlight driven by an ambient-light sensor and a hand
8//! proximity sensor. It is distinct from the RGB families (`0x8070`
9//! ColorLedEffects, `0x8080` PerKeyLighting) that [`crate::set_keyboard_color`]
10//! drives — a device exposes one or the other, never both.
11//!
12//! `setBacklightConfig` writes to the device's non-volatile memory, so a
13//! disabled backlight stays disabled across reconnects, host switches, and
14//! power cycles without a daemon re-applying it.
15
16use serde::{Deserialize, Serialize};
17
18/// How the firmware decides the backlight brightness level.
19///
20/// Crosses the agent↔GUI IPC, where serde encodes the variant *index*, so
21/// variant order is wire format — changes require a `PROTOCOL_VERSION` bump
22/// (guarded by `openlogi-ipc/tests/wire_format.rs`).
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
24pub enum BacklightMode {
25    /// No mode selected.
26    None,
27    /// Level follows the ambient-light sensor.
28    Automatic,
29    /// Level adjusted with the keyboard's own backlight keys. The firmware
30    /// enters this mode on its own; software cannot write it.
31    TemporaryManual,
32    /// Level set by software and held until changed.
33    PermanentManual,
34}
35
36/// Why the backlight is in its current state, as reported by
37/// `getBacklightInfo`.
38///
39/// Crosses the agent↔GUI IPC, where serde encodes the variant *index*, so
40/// variant order is wire format — changes require a `PROTOCOL_VERSION` bump
41/// (guarded by `openlogi-ipc/tests/wire_format.rs`).
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43pub enum BacklightStatus {
44    /// Turned off by software — the LEDs stay dark regardless of ambient
45    /// light or hand proximity. This is what [`crate::set_backlight_enabled`]
46    /// with `false` produces.
47    DisabledBySoftware,
48    /// Turned off because the battery is critically low.
49    DisabledByCriticalBattery,
50    /// Following the ambient-light sensor.
51    AlsAutomatic,
52    /// Following the ambient-light sensor, which reads bright enough that the
53    /// LEDs are off.
54    AlsSaturated,
55    /// Holding a level the user picked with the backlight keys.
56    TemporaryManual,
57    /// Holding a level written by software.
58    PermanentManual,
59}
60
61/// Snapshot of a keyboard's backlight, merged from the `0x1982`
62/// `getBacklightConfig` and `getBacklightInfo` responses.
63///
64/// Crosses the agent↔GUI IPC, so field order is wire format — changes require
65/// a `PROTOCOL_VERSION` bump (guarded by
66/// `openlogi-ipc/tests/wire_format.rs`).
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
68pub struct BacklightState {
69    /// Whether the backlight system is enabled at all. When `false` the
70    /// firmware keeps the LEDs dark no matter what the sensors report, and
71    /// [`Self::status`] reads [`BacklightStatus::DisabledBySoftware`].
72    pub enabled: bool,
73    /// How the level is chosen while the backlight is enabled.
74    pub mode: BacklightMode,
75    /// Why the backlight is in its current state.
76    pub status: BacklightStatus,
77    /// Current brightness level, `0` (off) up to [`Self::nb_levels`] minus one.
78    pub current_level: u8,
79    /// Number of user-selectable brightness levels the device reports.
80    pub nb_levels: u8,
81}
82
83impl BacklightState {
84    /// Whether the LEDs are dark right now, for whatever reason — software
85    /// disable, critical battery, a saturated ambient-light sensor, or a zero
86    /// manual level.
87    #[must_use]
88    pub fn is_dark(self) -> bool {
89        !self.enabled
90            || self.current_level == 0
91            || matches!(
92                self.status,
93                BacklightStatus::DisabledBySoftware
94                    | BacklightStatus::DisabledByCriticalBattery
95                    | BacklightStatus::AlsSaturated
96            )
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn lit() -> BacklightState {
105        BacklightState {
106            enabled: true,
107            mode: BacklightMode::Automatic,
108            status: BacklightStatus::AlsAutomatic,
109            current_level: 4,
110            nb_levels: 8,
111        }
112    }
113
114    #[test]
115    fn a_lit_backlight_is_not_dark() {
116        assert!(!lit().is_dark());
117    }
118
119    #[test]
120    fn software_disable_reads_as_dark() {
121        let state = BacklightState {
122            enabled: false,
123            status: BacklightStatus::DisabledBySoftware,
124            ..lit()
125        };
126        assert!(state.is_dark());
127    }
128
129    #[test]
130    fn a_zero_level_reads_as_dark_even_while_enabled() {
131        let state = BacklightState {
132            current_level: 0,
133            ..lit()
134        };
135        assert!(state.is_dark());
136    }
137
138    #[test]
139    fn a_saturated_ambient_sensor_reads_as_dark() {
140        let state = BacklightState {
141            status: BacklightStatus::AlsSaturated,
142            ..lit()
143        };
144        assert!(state.is_dark());
145    }
146}