Skip to main content

openlogi_hid/write/
diagnostics.rs

1use std::sync::Arc;
2
3use hidpp::{
4    device::Device, feature::CreatableFeature, feature::battery_status::BatteryStatusFeature,
5    feature::feature_set::FeatureSetFeature, feature::unified_battery::UnifiedBatteryFeature,
6};
7
8use crate::reprog_controls::{self, CidFlags, CidInfo, ReprogControlsV4};
9use crate::route::DeviceRoute;
10use crate::write::{HidppOperation, WriteError, classify_hidpp_error, open_feature, with_route};
11
12/// Snapshot of one HID++ feature exposed by a device: protocol ID +
13/// version. Returned by [`dump_features`] for diagnostics.
14#[derive(Debug, Clone, Copy)]
15pub struct FeatureEntry {
16    /// HID++ feature ID.
17    pub id: u16,
18    /// Feature version reported by the device.
19    pub version: u8,
20}
21
22/// Snapshot of one HID++ `0x1b04` reprogrammable control. Returned by
23/// [`dump_reprog_controls`] for diagnostics so new device controls can be
24/// identified before OpenLogi maps them to a first-class button.
25#[derive(Debug, Clone, Copy)]
26pub struct ReprogControlEntry {
27    /// HID++ control ID.
28    pub cid: u16,
29    /// Default task ID assigned to the control.
30    pub task_id: u16,
31    /// Capability and classification flags for the control.
32    pub flags: CidFlags,
33}
34
35impl From<CidInfo> for ReprogControlEntry {
36    fn from(info: CidInfo) -> Self {
37        Self {
38            cid: info.cid.into(),
39            task_id: info.task_id.0,
40            flags: info.flags,
41        }
42    }
43}
44
45/// Enumerate every HID++ feature the device on `route` reports — used by
46/// `openlogi diag features` to confirm which DPI / SmartShift / etc.
47/// feature IDs a given peripheral actually exposes (e.g. whether a mouse
48/// speaks `0x2201 AdjustableDpi`, `0x2202 ExtendedAdjustableDpi`, or both —
49/// `write::dpi` drives either).
50pub async fn dump_features(route: &DeviceRoute) -> Result<Vec<FeatureEntry>, WriteError> {
51    let index = route.device_index();
52    with_route(route, move |channel| async move {
53        let mut device = Device::new(Arc::clone(&channel), index)
54            .await
55            .map_err(|_| WriteError::DeviceUnreachable { index })?;
56        // The root feature exposes the FeatureSet (0x0001) at a fixed
57        // address; we look it up directly rather than going through
58        // `enumerate_features` so the iteration is observable.
59        let feature_set_info = device
60            .root()
61            .get_feature(FeatureSetFeature::ID)
62            .await
63            .map_err(|e| {
64                classify_hidpp_error(e, HidppOperation::DumpFeatures, FeatureSetFeature::ID)
65            })?
66            .ok_or(WriteError::FeatureUnsupported {
67                feature_hex: FeatureSetFeature::ID,
68            })?;
69        let feature_set = device.add_feature::<FeatureSetFeature>(feature_set_info.index);
70        let count = feature_set.count().await.map_err(|e| {
71            classify_hidpp_error(e, HidppOperation::DumpFeatures, FeatureSetFeature::ID)
72        })?;
73        let mut entries = Vec::with_capacity(usize::from(count));
74        for i in 0..=count {
75            let info = feature_set.get_feature(i).await.map_err(|e| {
76                classify_hidpp_error(e, HidppOperation::DumpFeatures, FeatureSetFeature::ID)
77            })?;
78            entries.push(FeatureEntry {
79                id: info.id,
80                version: info.version,
81            });
82        }
83        Ok(entries)
84    })
85    .await
86}
87
88/// Enumerate the device's HID++ `0x1b04` reprogrammable controls. This is a
89/// diagnostics-only probe used to discover controls for newly released devices.
90/// For example, MX Master 4 has both a Gesture Button and a separate Haptic
91/// Sense Panel in the thumb area; this probe lets us identify the panel's CID
92/// and capabilities before wiring it into the capture/remapping model.
93pub async fn dump_reprog_controls(
94    route: &DeviceRoute,
95) -> Result<Vec<ReprogControlEntry>, WriteError> {
96    let index = route.device_index();
97    with_route(route, move |channel| async move {
98        let device = Device::new(Arc::clone(&channel), index)
99            .await
100            .map_err(|_| WriteError::DeviceUnreachable { index })?;
101        let info = device
102            .root()
103            .get_feature(reprog_controls::FEATURE_ID)
104            .await
105            .map_err(|e| {
106                classify_hidpp_error(e, HidppOperation::DumpFeatures, reprog_controls::FEATURE_ID)
107            })?
108            .ok_or(WriteError::FeatureUnsupported {
109                feature_hex: reprog_controls::FEATURE_ID,
110            })?;
111        let rc = ReprogControlsV4::new(Arc::clone(&channel), index, info.index);
112        let count = rc.get_count().await.map_err(|e| {
113            classify_hidpp_error(e, HidppOperation::DumpFeatures, reprog_controls::FEATURE_ID)
114        })?;
115        let mut entries = Vec::with_capacity(usize::from(count));
116        for i in 0..count {
117            let control = rc.get_cid_info(i).await.map_err(|e| {
118                classify_hidpp_error(e, HidppOperation::DumpFeatures, reprog_controls::FEATURE_ID)
119            })?;
120            entries.push(control.into());
121        }
122        Ok(entries)
123    })
124    .await
125}
126
127/// Diagnostic read of the device's raw battery report — the unified `0x1004`
128/// fields, or the legacy `0x1000` `discharge_level`/`next_level`/`status`. For
129/// `openlogi diag battery`: surfaces exactly what the firmware reports so a
130/// claim like "MX2S shows 0% while charging" can be confirmed against the wire
131/// instead of guessed (the GUI only ever shows the mapped value).
132pub async fn read_battery_raw(route: &DeviceRoute) -> Result<String, WriteError> {
133    let index = route.device_index();
134    with_route(route, move |channel| async move {
135        let mut device = Device::new(Arc::clone(&channel), index)
136            .await
137            .map_err(|_| WriteError::DeviceUnreachable { index })?;
138
139        match open_feature::<UnifiedBatteryFeature>(&mut device).await {
140            Ok(feature) => {
141                let info = feature
142                    .get_battery_info()
143                    .await
144                    .map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
145                return Ok(format!(
146                    "0x1004 UnifiedBattery: percentage={} level={:?} status={:?}",
147                    info.charging_percentage, info.level, info.status
148                ));
149            }
150            Err(WriteError::FeatureUnsupported { .. }) => {}
151            Err(e) => return Err(e),
152        }
153
154        match open_feature::<BatteryStatusFeature>(&mut device).await {
155            Ok(feature) => {
156                let info = feature
157                    .get_battery_level_status()
158                    .await
159                    .map_err(|e| WriteError::Hidpp(format!("{e:?}")))?;
160                return Ok(format!(
161                    "0x1000 BatteryStatus: discharge_level={} next_level={} status={:?}",
162                    info.discharge_level, info.next_level, info.status
163                ));
164            }
165            Err(WriteError::FeatureUnsupported { .. }) => {}
166            Err(e) => return Err(e),
167        }
168
169        // Reached only when neither 0x1004 nor 0x1000 is present; report the
170        // preferred feature rather than implying 0x1000 was specifically absent.
171        Err(WriteError::FeatureUnsupported {
172            feature_hex: 0x1004,
173        })
174    })
175    .await
176}