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}