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
//! Implements the legacy `BatteryStatus` feature (ID `0x1000`) that reports a
//! device's battery charge as a discharge level plus a charging status.
//!
//! This is the predecessor of `UnifiedBattery` (`0x1004`): older mice such as
//! the MX Master 2S expose `0x1000` and never `0x1004`, so the inventory probe
//! falls back to this feature when the unified one is absent — the same
//! enhanced-then-legacy pattern `SmartShift` uses for `0x2111` / `0x2110`.
//!
//! Only `getBatteryLevelStatus` (function `0`) is implemented; the optional
//! `getBatteryCapability` (function `1`) isn't needed to display a charge
//! reading. The device also broadcasts event `0` when its level or charging
//! status changes; [`BatteryStatusFeature::is_status_broadcast`] recognises it
//! so a listener can re-read the battery instead of waiting for a poll.
use std::hash::Hash;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use openlogi_hidpp_derive::Feature;
use crate::{feature::FeatureEndpoint, protocol::v20::Hidpp20Error};
/// Implements the legacy `BatteryStatus` / `0x1000` feature.
#[derive(Feature)]
#[creatable(id = 0x1000, version = 0)]
pub struct BatteryStatusFeature {
/// The endpoint this feature talks to.
endpoint: FeatureEndpoint,
}
impl BatteryStatusFeature {
/// Whether an unsolicited report on this feature's index is its
/// battery-level broadcast (event `0`; the Linux driver's
/// `EVENT_BATTERY_LEVEL_STATUS_BROADCAST`).
///
/// Deliberately independent of the payload: the broadcast carries the same
/// three bytes as [`Self::get_battery_level_status`], and a listener that
/// only needs to know *that* the reading changed should re-read it rather
/// than drop a broadcast whose status value this crate does not know.
#[must_use]
pub const fn is_status_broadcast(function_id: u8) -> bool {
function_id == 0
}
/// Reads the current battery level and charging status (function `0`,
/// `getBatteryLevelStatus`).
pub async fn get_battery_level_status(&self) -> Result<LegacyBatteryInfo, Hidpp20Error> {
let payload = self.endpoint.call(0, [0; 3]).await?.extend_payload();
Ok(LegacyBatteryInfo {
discharge_level: payload[0],
next_level: payload[1],
status: LegacyBatteryStatus::try_from(payload[2])
.map_err(|_| Hidpp20Error::UnsupportedResponse)?,
})
}
}
/// A reading from the legacy `0x1000` `getBatteryLevelStatus` function.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub struct LegacyBatteryInfo {
/// Current battery charge as a percentage (`0`–`100`). Logitech firmware
/// reports this in coarse steps rather than a continuous value.
///
/// A reading only while [`status`](Self::status) is
/// [`LegacyBatteryStatus::Discharging`]. In every other state the firmware
/// reports `0`, meaning unknown, as the Linux HID++ driver reads it.
pub discharge_level: u8,
/// The next lower discharge step the firmware will report — a hint at the
/// reporting granularity. Unused for display.
pub next_level: u8,
/// The current charging status.
pub status: LegacyBatteryStatus,
}
/// Charging status reported by the legacy `0x1000` feature. Values follow the
/// HID++ `batteryStatus` enumeration (see Solaar / `hid-logitech-hidpp`).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
#[repr(u8)]
pub enum LegacyBatteryStatus {
/// Battery is discharging.
Discharging = 0,
/// Battery is recharging.
Recharging = 1,
/// Battery is charging and nearly full.
AlmostFull = 2,
/// Battery charge is complete.
Full = 3,
/// Battery is recharging below optimal speed.
SlowRecharge = 4,
/// The battery type is invalid.
InvalidBattery = 5,
/// The battery subsystem reported a thermal error.
ThermalError = 6,
/// "Other charging error" (Solaar lists value 7). Kept explicit so a device
/// reporting it surfaces as Unknown instead of failing the parse and making
/// the battery indicator vanish from the UI.
Other = 7,
}