openlogi-core 0.8.14

Core types, config, and paths for OpenLogi. No I/O specifics.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
//! HID++ keyboard-backlight mode, status, read-back state, and changes.
//!
//! These types describe the semantic state of HID++ `Backlight` feature
//! `0x1982`. The protocol conversion and transport operations that produce
//! them remain in `openlogi-device`.

use std::fmt;

use serde::{Deserialize, Serialize};

/// How the firmware decides the backlight brightness level.
///
/// Crosses the agent↔GUI IPC, where serde encodes the variant *index*, so
/// variant order is wire format — changes require a `PROTOCOL_VERSION` bump
/// (guarded by `openlogi-ipc/tests/wire_format.rs`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BacklightMode {
    /// No mode selected.
    None,
    /// Level follows the ambient-light sensor.
    Automatic,
    /// Level adjusted with the keyboard's own backlight keys. The firmware
    /// enters this mode on its own; software cannot write it.
    TemporaryManual,
    /// Level set by software and held until changed.
    PermanentManual,
}

/// Why the backlight is in its current state, as reported by the device.
///
/// Crosses the agent↔GUI IPC, where serde encodes the variant *index*, so
/// variant order is wire format — changes require a `PROTOCOL_VERSION` bump
/// (guarded by `openlogi-ipc/tests/wire_format.rs`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BacklightStatus {
    /// Turned off by software — the LEDs stay dark regardless of ambient
    /// light or hand proximity.
    DisabledBySoftware,
    /// Turned off because the battery is critically low.
    DisabledByCriticalBattery,
    /// Following the ambient-light sensor.
    AlsAutomatic,
    /// Following the ambient-light sensor, which reads bright enough that the
    /// LEDs are off.
    AlsSaturated,
    /// Holding a level the user picked with the backlight keys.
    TemporaryManual,
    /// Holding a level written by software.
    PermanentManual,
}

/// Snapshot of a keyboard's backlight, merged from the `0x1982`
/// `getBacklightConfig` and `getBacklightInfo` responses.
///
/// Crosses the agent↔GUI IPC, so field order is wire format — changes require
/// a `PROTOCOL_VERSION` bump (guarded by
/// `openlogi-ipc/tests/wire_format.rs`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BacklightState {
    /// Whether the backlight system is enabled at all. When `false` the
    /// firmware keeps the LEDs dark no matter what the sensors report, and
    /// [`Self::status`] reads [`BacklightStatus::DisabledBySoftware`].
    pub enabled: bool,
    /// How the level is chosen while the backlight is enabled.
    pub mode: BacklightMode,
    /// Why the backlight is in its current state.
    pub status: BacklightStatus,
    /// Current brightness level, `0` (off) up to [`Self::nb_levels`] minus one.
    pub current_level: u8,
    /// Number of user-selectable brightness levels the device reports.
    pub nb_levels: u8,
    /// Whether battery saving is on: the firmware turns the backlight off
    /// when the battery runs low.
    ///
    /// `serde(default)` only keeps recorded JSON fixtures that predate the
    /// field loading; the IPC encoding always carries it.
    #[serde(default)]
    pub power_save: bool,
    /// Which [`BacklightChange`]s the keyboard accepts.
    #[serde(default)]
    pub controls: BacklightControls,
}

/// What software may change on a keyboard backlight beyond turning it on or
/// off, from the `0x1982` capability bits and feature version.
///
/// Crosses the agent↔GUI IPC inside [`BacklightState`], so field order is
/// wire format — changes require a `PROTOCOL_VERSION` bump (guarded by
/// `openlogi-ipc/tests/wire_format.rs`).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BacklightControls {
    /// Software can switch between the ambient-light sensor choosing the
    /// level and holding one (permanent-manual mode): the keyboard supports
    /// both.
    pub automatic: bool,
    /// Software can pick the level: permanent-manual mode, `0x1982` version 3
    /// or later, and more than one level.
    pub level: bool,
    /// The firmware can turn the backlight off on a low battery.
    pub power_save: bool,
}

impl BacklightControls {
    /// Whether `change` is one this keyboard accepts, ignoring the level
    /// range: [`BacklightChange::Level`] also needs a level below
    /// [`BacklightState::nb_levels`].
    #[must_use]
    pub fn allow(self, change: BacklightChange) -> bool {
        match change {
            BacklightChange::Enabled(_) => true,
            BacklightChange::Automatic(_) => self.automatic,
            BacklightChange::Level(_) => self.level,
            BacklightChange::PowerSave(_) => self.power_save,
        }
    }
}

/// One change to a keyboard backlight, written to the keyboard's own memory.
///
/// Crosses the agent↔GUI IPC, where serde encodes the variant *index*, so
/// variant order is wire format — changes require a `PROTOCOL_VERSION` bump
/// (guarded by `openlogi-ipc/tests/wire_format.rs`).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BacklightChange {
    /// Turn the backlight system on or off.
    Enabled(bool),
    /// Let the ambient-light sensor choose the level (`true`), or hold the
    /// level the keyboard shows now (`false`), so nothing visibly changes.
    Automatic(bool),
    /// Hold this level, `0` (off) up to [`BacklightState::nb_levels`] minus
    /// one. Leaves automatic mode.
    Level(u8),
    /// Turn battery saving on or off.
    PowerSave(bool),
}

impl fmt::Display for BacklightChange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let on_off = |on: bool| if on { "on" } else { "off" };
        match *self {
            Self::Enabled(on) => write!(f, "backlight {}", on_off(on)),
            Self::Automatic(on) => write!(f, "automatic brightness {}", on_off(on)),
            Self::Level(level) => write!(f, "backlight level {level}"),
            Self::PowerSave(on) => write!(f, "battery saving {}", on_off(on)),
        }
    }
}

impl BacklightState {
    /// Whether the keyboard accepts `change` right now: the control exists
    /// and, for a level, the level is in range.
    #[must_use]
    pub fn accepts(self, change: BacklightChange) -> bool {
        self.controls.allow(change)
            && match change {
                BacklightChange::Level(level) => level < self.nb_levels,
                _ => true,
            }
    }

    /// Whether the ambient-light sensor chooses the level — the firmware's
    /// automatic mode, or the temporary level the backlight keys set on top
    /// of it.
    #[must_use]
    pub fn is_automatic(self) -> bool {
        matches!(
            self.mode,
            BacklightMode::Automatic | BacklightMode::TemporaryManual
        )
    }

    /// Whether the LEDs are dark right now, for whatever reason — software
    /// disable, critical battery, a saturated ambient-light sensor, or a zero
    /// manual level.
    #[must_use]
    pub fn is_dark(self) -> bool {
        !self.enabled
            || self.current_level == 0
            || matches!(
                self.status,
                BacklightStatus::DisabledBySoftware
                    | BacklightStatus::DisabledByCriticalBattery
                    | BacklightStatus::AlsSaturated
            )
    }

    /// The backlight this one becomes once the keyboard keeps `change`, as
    /// far as the change alone decides — what software can show before the
    /// keyboard reads back, and what a keyboard double answers.
    ///
    /// The firmware owns the rest and it is kept: a critical-battery shutoff
    /// stays in force, and a status the mode did not change — a saturated
    /// sensor, say — stands. Turning the backlight off always reads
    /// [`BacklightStatus::DisabledBySoftware`], the status the keyboard
    /// reports for it. `change` is not checked against [`Self::accepts`].
    #[must_use]
    pub fn changed(self, change: BacklightChange) -> Self {
        let mut next = self;
        match change {
            BacklightChange::Enabled(on) => next.enabled = on,
            BacklightChange::Automatic(true) => next.mode = BacklightMode::Automatic,
            // The level shown now is the one held, as the device write does.
            BacklightChange::Automatic(false) => next.mode = BacklightMode::PermanentManual,
            BacklightChange::Level(level) => {
                next.mode = BacklightMode::PermanentManual;
                next.current_level = level;
            }
            BacklightChange::PowerSave(on) => next.power_save = on,
        }
        next.status = if !next.enabled {
            BacklightStatus::DisabledBySoftware
        } else if self.status == BacklightStatus::DisabledByCriticalBattery
            || (self.enabled && next.mode == self.mode)
        {
            self.status
        } else {
            match next.mode {
                BacklightMode::Automatic => BacklightStatus::AlsAutomatic,
                BacklightMode::TemporaryManual => BacklightStatus::TemporaryManual,
                BacklightMode::PermanentManual => BacklightStatus::PermanentManual,
                BacklightMode::None => self.status,
            }
        };
        next
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn lit() -> BacklightState {
        BacklightState {
            enabled: true,
            mode: BacklightMode::Automatic,
            status: BacklightStatus::AlsAutomatic,
            current_level: 4,
            nb_levels: 8,
            power_save: true,
            controls: BacklightControls {
                automatic: true,
                level: true,
                power_save: true,
            },
        }
    }

    #[test]
    fn every_change_needs_its_control() {
        let all = lit();
        let none = BacklightState {
            controls: BacklightControls::default(),
            ..lit()
        };
        for change in [
            BacklightChange::Automatic(false),
            BacklightChange::Level(3),
            BacklightChange::PowerSave(false),
        ] {
            assert!(all.accepts(change), "{change}");
            assert!(!none.accepts(change), "{change}");
        }
        // On/off is part of every 0x1982 keyboard.
        assert!(none.accepts(BacklightChange::Enabled(false)));
    }

    #[test]
    fn a_level_must_be_below_the_level_count() {
        assert!(lit().accepts(BacklightChange::Level(0)));
        assert!(lit().accepts(BacklightChange::Level(7)));
        assert!(!lit().accepts(BacklightChange::Level(8)));
    }

    #[test]
    fn the_backlight_keys_leave_the_sensor_in_charge() {
        let keys = BacklightState {
            mode: BacklightMode::TemporaryManual,
            ..lit()
        };
        let held = BacklightState {
            mode: BacklightMode::PermanentManual,
            ..lit()
        };
        assert!(lit().is_automatic());
        assert!(keys.is_automatic());
        assert!(!held.is_automatic());
    }

    #[test]
    fn changes_name_themselves_for_error_messages() {
        assert_eq!(BacklightChange::Enabled(true).to_string(), "backlight on");
        assert_eq!(
            BacklightChange::Automatic(false).to_string(),
            "automatic brightness off"
        );
        assert_eq!(BacklightChange::Level(3).to_string(), "backlight level 3");
        assert_eq!(
            BacklightChange::PowerSave(true).to_string(),
            "battery saving on"
        );
    }

    #[test]
    fn a_lit_backlight_is_not_dark() {
        assert!(!lit().is_dark());
    }

    #[test]
    fn software_disable_reads_as_dark() {
        let state = BacklightState {
            enabled: false,
            status: BacklightStatus::DisabledBySoftware,
            ..lit()
        };
        assert!(state.is_dark());
    }

    #[test]
    fn a_zero_level_reads_as_dark_even_while_enabled() {
        let state = BacklightState {
            current_level: 0,
            ..lit()
        };
        assert!(state.is_dark());
    }

    #[test]
    fn a_saturated_ambient_sensor_reads_as_dark() {
        let state = BacklightState {
            status: BacklightStatus::AlsSaturated,
            ..lit()
        };
        assert!(state.is_dark());
    }

    #[test]
    fn turning_the_backlight_off_reads_as_a_software_disable() {
        let off = lit().changed(BacklightChange::Enabled(false));
        assert!(!off.enabled);
        assert_eq!(off.status, BacklightStatus::DisabledBySoftware);
        assert_eq!(
            off.mode,
            BacklightMode::Automatic,
            "the mode is kept for later"
        );
    }

    #[test]
    fn turning_it_back_on_reports_the_kept_mode() {
        let off = lit().changed(BacklightChange::Enabled(false));
        assert_eq!(off.changed(BacklightChange::Enabled(true)), lit());
        let manual = BacklightState {
            mode: BacklightMode::PermanentManual,
            ..off
        };
        assert_eq!(
            manual.changed(BacklightChange::Enabled(true)).status,
            BacklightStatus::PermanentManual
        );
    }

    #[test]
    fn leaving_automatic_holds_the_level_shown() {
        let held = lit().changed(BacklightChange::Automatic(false));
        assert_eq!(held.mode, BacklightMode::PermanentManual);
        assert_eq!(held.status, BacklightStatus::PermanentManual);
        assert_eq!(held.current_level, lit().current_level);
        let back = held.changed(BacklightChange::Automatic(true));
        assert_eq!(back.mode, BacklightMode::Automatic);
        assert_eq!(back.status, BacklightStatus::AlsAutomatic);
    }

    #[test]
    fn a_level_leaves_automatic_mode() {
        let set = lit().changed(BacklightChange::Level(2));
        assert_eq!(set.current_level, 2);
        assert_eq!(set.mode, BacklightMode::PermanentManual);
        assert_eq!(set.status, BacklightStatus::PermanentManual);
    }

    #[test]
    fn battery_saving_changes_nothing_else() {
        let saving_off = lit().changed(BacklightChange::PowerSave(false));
        assert_eq!(
            saving_off,
            BacklightState {
                power_save: false,
                ..lit()
            }
        );
    }

    #[test]
    fn firmware_owned_status_is_kept() {
        let saturated = BacklightState {
            status: BacklightStatus::AlsSaturated,
            ..lit()
        };
        assert_eq!(
            saturated.changed(BacklightChange::PowerSave(false)).status,
            BacklightStatus::AlsSaturated,
            "the sensor still reads bright"
        );
        let critical = BacklightState {
            status: BacklightStatus::DisabledByCriticalBattery,
            ..lit()
        };
        assert_eq!(
            critical.changed(BacklightChange::Level(3)).status,
            BacklightStatus::DisabledByCriticalBattery,
            "a level does not override the battery shutoff"
        );
        assert_eq!(
            critical.changed(BacklightChange::Enabled(false)).status,
            BacklightStatus::DisabledBySoftware
        );
    }
}