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