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}