Skip to main content

openlogi_core/hid/
error.rs

1//! HID++ read/write error vocabulary — pure data, no I/O.
2//!
3//! The conversions from `hidpp`/`async_hid` error types into [`WriteError`]
4//! live in `openlogi_hid::write::error`, which this crate must never depend
5//! on.
6
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9
10use super::backlight::BacklightChange;
11use super::dpi::Dpi;
12use super::report_rate::ReportRate;
13
14/// Error returned by HID++ read/write operations.
15///
16/// Serializable + Clone so it can cross the agent↔GUI IPC unchanged: the GUI
17/// classifies a device read/write error as permanent (FeatureUnsupported /
18/// EmptyDpiList) vs transient, so the discriminating variant must survive the
19/// wire — stringifying it would collapse every case to "transient" and a device
20/// that genuinely lacks a feature would be re-probed forever. Variant order is
21/// therefore wire format: changes require a `PROTOCOL_VERSION` bump (guarded
22/// by `openlogi-ipc/tests/wire_format.rs`).
23#[derive(Debug, Clone, Error, PartialEq, Eq, Serialize, Deserialize)]
24pub enum WriteError {
25    /// HID transport error serialized as text.
26    ///
27    /// `openlogi_hid::BackendError` isn't `Serialize`, so carry its message as
28    /// text; the typed error is never matched on (only constructed +
29    /// displayed).
30    #[error("HID transport error: {0}")]
31    Hid(String),
32    /// No currently connected device matched the requested route.
33    #[error("no connected device matched the route")]
34    DeviceNotFound,
35    /// The HID node opened, but the target HID++ device index did not answer.
36    #[error("device at index {index:#04x} did not respond to HID++")]
37    DeviceUnreachable {
38        /// HID++ device index that failed to answer.
39        index: u8,
40    },
41    /// Device does not expose the requested HID++ feature.
42    #[error("device does not expose HID++ feature {feature_hex:#06x}")]
43    FeatureUnsupported {
44        /// HID++ feature ID that was not present.
45        feature_hex: u16,
46    },
47    /// Device reported no valid DPI values.
48    #[error("device returned no supported DPI values")]
49    EmptyDpiList,
50    /// Generic HID++ protocol error serialized as text.
51    #[error("HID++ protocol error: {0}")]
52    Hidpp(String),
53    /// HID++ feature error response.
54    #[error("HID++ feature error during {operation:?} for feature {feature_hex:#06x}: {kind:?}")]
55    HidppFeature {
56        /// Operation being performed.
57        operation: HidppOperation,
58        /// HID++ feature ID involved in the operation.
59        feature_hex: u16,
60        /// HID++ feature error kind.
61        kind: HidppFeatureErrorKind,
62    },
63    /// Device returned a structurally unsupported response.
64    #[error("HID++ unsupported response during {operation:?} for feature {feature_hex:#06x}")]
65    UnsupportedResponse {
66        /// Operation being performed.
67        operation: HidppOperation,
68        /// HID++ feature ID involved in the operation.
69        feature_hex: u16,
70    },
71    /// HID++ request timed out.
72    #[error("HID++ request timed out during {operation:?}")]
73    RequestTimedOut {
74        /// Operation that timed out.
75        operation: HidppOperation,
76    },
77    /// Tokio runtime could not be initialized for a sync caller.
78    #[error("tokio runtime init failed: {message}")]
79    RuntimeInit {
80        /// Runtime initialization error message.
81        message: String,
82    },
83    /// Background agent write path is unavailable.
84    #[error("background agent is unavailable")]
85    AgentUnavailable,
86    /// A standalone light value is outside the driver's supported range.
87    #[error("invalid light value for {control}: {value}")]
88    InvalidLightValue {
89        /// Semantic control name.
90        control: String,
91        /// Rejected value in the semantic/native unit supplied by the caller.
92        value: u16,
93    },
94    /// The selected light driver does not implement a requested control.
95    #[error("light control is unsupported: {control}")]
96    LightUnsupported {
97        /// Semantic control name.
98        control: String,
99    },
100    /// Multiple raw HID nodes matched one physical route.
101    #[error("multiple raw HID devices matched the route")]
102    AmbiguousRawDevice,
103    /// A DPI write asked for a value the device does not report as supported.
104    ///
105    /// Nothing was written: the device layer checks the device-reported list
106    /// first rather than leaving the firmware to clamp, ignore or reject it.
107    #[error("DPI {requested} is not one of the device-reported supported values")]
108    DpiUnsupported {
109        /// The rejected DPI.
110        requested: Dpi,
111    },
112    /// The device acknowledged a DPI write but reads back a different value,
113    /// including after one re-write.
114    #[error("device reports DPI {actual} after a write of {requested}")]
115    DpiNotApplied {
116        /// The DPI that was written.
117        requested: Dpi,
118        /// The DPI the device reports afterwards.
119        actual: Dpi,
120    },
121    /// A haptic play asked for a waveform the device does not advertise.
122    ///
123    /// Nothing was sent: the device layer checks the `0x19b0` capability mask
124    /// first, as Logi Options+ does, so a retry cannot succeed.
125    #[error("device does not advertise haptic waveform {waveform}")]
126    HapticWaveformUnsupported {
127        /// The waveform ID that was refused.
128        waveform: u8,
129    },
130    /// A report-rate write asked for a rate the device does not list for its
131    /// current connection.
132    ///
133    /// Nothing was written: the device layer checks the device-reported list
134    /// first, so the firmware never has to clamp or reject it.
135    #[error("report rate {requested} Hz is not one the device lists for this connection")]
136    ReportRateUnsupported {
137        /// The rejected rate.
138        requested: ReportRate,
139    },
140    /// The device acknowledged a report-rate write but reads back a
141    /// different rate — for instance a gaming mouse whose onboard profile
142    /// owns the rate.
143    #[error("device reports {actual} Hz after a write of {requested} Hz")]
144    ReportRateNotApplied {
145        /// The rate that was written.
146        requested: ReportRate,
147        /// The rate the device reports afterwards.
148        actual: ReportRate,
149    },
150    /// A backlight change the keyboard does not offer — a level beyond its
151    /// level count, or a mode, level or battery-saving control its `0x1982`
152    /// capability bits leave out.
153    ///
154    /// Nothing was written: the device layer checks a fresh read first.
155    #[error("the keyboard does not offer {change}")]
156    BacklightChangeUnsupported {
157        /// The rejected change.
158        change: BacklightChange,
159    },
160    /// The keyboard acknowledged a backlight write but reads back a
161    /// configuration without the change.
162    #[error("keyboard does not report {change} after the write")]
163    BacklightNotApplied {
164        /// The change that was written.
165        change: BacklightChange,
166    },
167    /// A haptic play refused because the device reports a non-zero `0x19b0`
168    /// haptic status.
169    ///
170    /// Nothing was sent: Logi Options+ plays only while the status is zero,
171    /// and the device layer applies the same gate, so a retry cannot succeed
172    /// until the device reports otherwise.
173    #[error("device reports haptic status {status}, so haptics cannot play")]
174    HapticsUnavailable {
175        /// The raw status byte the device reported.
176        status: u8,
177    },
178    /// A rename of an Easy-Switch slot's computer on a device whose `0x1815`
179    /// capabilities leave renaming out.
180    ///
181    /// Nothing was written.
182    #[error("the device does not let software rename its Easy-Switch hosts")]
183    HostRenameUnsupported,
184    /// A rename of an Easy-Switch slot that has no computer paired, or that
185    /// the device does not have.
186    ///
187    /// Nothing was written.
188    #[error("Easy-Switch channel {} has no paired computer", u16::from(*slot) + 1)]
189    HostSlotEmpty {
190        /// The zero-based slot index asked for.
191        slot: u8,
192    },
193    /// A host name that is empty, holds a NUL, or is longer than the slot
194    /// stores.
195    ///
196    /// Nothing was written.
197    #[error("a host name must be 1 to {max_len} bytes of UTF-8 without NUL")]
198    HostNameRejected {
199        /// The longest name, in UTF-8 bytes, the slot stores.
200        max_len: u8,
201    },
202    /// The device acknowledged a host-name write but reads back another
203    /// name.
204    #[error("device reports host name {actual:?} after a write of {requested:?}")]
205    HostNameNotApplied {
206        /// The name that was written.
207        requested: String,
208        /// The name the device reports afterwards, if any.
209        actual: Option<String>,
210    },
211}
212
213/// HID++ operation being performed when a device write/read failed.
214///
215/// Variant order is wire format because this travels inside [`WriteError`].
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
217pub enum HidppOperation {
218    /// Resolve a feature ID to its runtime feature index.
219    ResolveFeature,
220    /// Enumerate the device feature table.
221    DumpFeatures,
222    /// Read current DPI.
223    ReadDpi,
224    /// Read DPI capabilities.
225    ReadDpiCapabilities,
226    /// Write DPI.
227    WriteDpi,
228    /// Read SmartShift status.
229    ReadSmartShift,
230    /// Write SmartShift status.
231    WriteSmartShift,
232    /// Write keyboard lighting.
233    Lighting,
234    /// Read HiResWheel capabilities or the current wheel mode.
235    ReadWheelMode,
236    /// Write and verify the native HiResWheel mode.
237    WriteWheelMode,
238    /// Read the keyboard backlight config or info.
239    ReadBacklight,
240    /// Write the keyboard backlight config.
241    WriteBacklight,
242    /// Write keyboard Fn-lock (fn inversion).
243    WriteFnLock,
244    /// Write a standalone-light command. Appended last — variant order is wire format.
245    Light,
246    /// Play one haptic waveform. Appended last — variant order is wire format.
247    PlayHaptic,
248    /// Read keyboard Fn-lock (fn inversion). Appended last — variant order is
249    /// wire format.
250    ReadFnLock,
251    /// Read the `0x2205` pointer-motion scaling. Appended last — variant order
252    /// is wire format.
253    ReadPointerScaling,
254    /// Write the `0x2205` pointer-motion scaling. Appended last — variant order
255    /// is wire format.
256    WritePointerScaling,
257    /// Keep a keyboard's `0x4531` platform on the host OS: read the platform
258    /// table and the current host's platform, and write it when it differs.
259    /// Appended last — variant order is wire format.
260    WriteOsLayout,
261    /// Read the `0x8060` / `0x8061` report rate and the rates the device
262    /// lists. Appended last — variant order is wire format.
263    ReadReportRate,
264    /// Write the `0x8060` / `0x8061` report rate. Appended last — variant
265    /// order is wire format.
266    WriteReportRate,
267    /// Read the `0x1815` Easy-Switch host slots, their names and OS.
268    /// Appended last — variant order is wire format.
269    ReadHosts,
270    /// Write an Easy-Switch slot's `0x1815` host name. Appended last —
271    /// variant order is wire format.
272    RenameHost,
273}
274
275/// HID++ feature error kind in a serializable wire-safe form.
276///
277/// Variant order is wire format because this travels inside [`WriteError`].
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
279pub enum HidppFeatureErrorKind {
280    /// HID++ `NoError` code.
281    NoError,
282    /// Unknown HID++ error.
283    Unknown,
284    /// Invalid argument.
285    InvalidArgument,
286    /// Argument out of range.
287    OutOfRange,
288    /// Hardware error.
289    HwError,
290    /// Logitech-internal firmware error.
291    LogitechInternal,
292    /// Invalid feature index.
293    InvalidFeatureIndex,
294    /// Invalid function ID.
295    InvalidFunctionId,
296    /// Device is busy.
297    Busy,
298    /// Operation is unsupported.
299    Unsupported,
300    /// Error code not modeled by OpenLogi.
301    Unrecognized,
302}