Skip to main content

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}