cue_sdk/device/
info.rs

1use cue_sdk_sys as ffi;
2use failure::_core::str::Utf8Error;
3
4use super::{
5    channels_from_ffi, Channel, ChannelsFromFfiError, DeviceCapabilities, DeviceId, DeviceLayout,
6    DeviceType,
7};
8use crate::internal::try_c_char_ptr_to_str;
9
10/// The various errors that can occur when reading device info from the iCUE SDK.
11#[derive(Debug, Clone, Fail, PartialEq)]
12pub enum CueDeviceInfoFromFfiError {
13    #[fail(
14        display = "Expected to create a CueDevice from a valid pointer, but received a null pointer instead."
15    )]
16    NullPointer,
17    #[fail(display = "Failed to generate on field: {}, error: {}", field, error)]
18    StringConversionError {
19        field: String,
20        #[cause]
21        error: Utf8Error,
22    },
23    #[fail(display = "Unexpected null pointer on field: {}", _0)]
24    NullPointerField(String),
25    #[fail(display = "Invalid (negative) number of leds: {}", _0)]
26    InvalidLedsCount(i32),
27    #[fail(display = "Error with channels: {}", _0)]
28    ChannelsError(ChannelsFromFfiError),
29}
30
31impl CueDeviceInfo {
32    pub(crate) fn from_ffi(
33        device_info: *mut ffi::CorsairDeviceInfo,
34    ) -> Result<Self, CueDeviceInfoFromFfiError> {
35        if device_info.is_null() {
36            return Err(CueDeviceInfoFromFfiError::NullPointer);
37        }
38
39        let info = unsafe { *device_info };
40        let id = DeviceId::from_ffi(info.deviceId).map_err(|e| {
41            CueDeviceInfoFromFfiError::StringConversionError {
42                field: "deviceId".to_owned(),
43                error: e.0,
44            }
45        })?;
46
47        let device_type = DeviceType::from_ffi(info.type_);
48
49        let model = try_c_char_ptr_to_str(info.model)
50            .map_err(|e| CueDeviceInfoFromFfiError::StringConversionError {
51                field: "model".to_string(),
52                error: e,
53            })?
54            .ok_or_else(|| CueDeviceInfoFromFfiError::NullPointerField("model".to_string()))?
55            .to_string();
56
57        let layout = DeviceLayout::from_ffi_values(info.physicalLayout, info.logicalLayout);
58
59        if info.ledsCount < 0 {
60            return Err(CueDeviceInfoFromFfiError::InvalidLedsCount(info.ledsCount));
61        }
62
63        let leds_count = info.ledsCount as u32;
64
65        let channels = channels_from_ffi(info.channels)
66            .map_err(|e| CueDeviceInfoFromFfiError::ChannelsError(e))?;
67
68        let capabilities = DeviceCapabilities::from_ffi(info.capsMask);
69
70        Ok(CueDeviceInfo {
71            id,
72            capabilities,
73            channels,
74            leds_count,
75            device_type,
76            layout,
77            model,
78        })
79    }
80}
81
82/// The static device info for the attached `CueDevice`, including id, model, capabilities,
83/// leds_count, and more.
84#[derive(Debug, Clone, PartialEq)]
85pub struct CueDeviceInfo {
86    pub id: DeviceId,
87    pub device_type: Option<DeviceType>,
88    pub model: String,
89    pub layout: Option<DeviceLayout>,
90    pub capabilities: DeviceCapabilities,
91    pub leds_count: u32,
92    pub channels: Vec<Channel>,
93}
94
95#[cfg(test)]
96mod tests {
97    use cue_sdk_sys as ffi;
98
99    use std::ffi::CString;
100    use std::ptr;
101
102    use super::{CueDeviceInfo, CueDeviceInfoFromFfiError};
103    use crate::device::DeviceId;
104    use crate::device::{
105        Channel, DeviceCapabilities, DeviceLayout, DeviceType, LogicalLayout, PhysicalLayout,
106    };
107    use std::os::raw::c_char;
108
109    const DEVICE_ID: [c_char; 128] = [
110        0x11, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20,
111        0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20,
112        0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30,
113        0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30,
114        0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20,
115        0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20,
116        0x10, 0x50, 0x30, 0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30,
117        0x30, 0x30, 0x30, 0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50, 0x30, 0x30, 0x30, 0x30,
118        0x10, 0x11, 0x20, 0x50, 0x30, 0x20, 0x10, 0x50,
119    ];
120
121    #[test]
122    fn device_from_ffi_null_ptr() {
123        let result = CueDeviceInfo::from_ffi(ptr::null_mut());
124        assert_eq!(result.unwrap_err(), CueDeviceInfoFromFfiError::NullPointer);
125    }
126
127    #[test]
128    fn device_from_ffi_model_null_ptr() {
129        let channels_info = ffi::CorsairChannelsInfo {
130            channelsCount: 0,
131            channels: ptr::null_mut(),
132        };
133
134        let mut info = ffi::CorsairDeviceInfo {
135            type_: ffi::CorsairDeviceType_CDT_Cooler,
136            model: ptr::null(),
137            physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
138            logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
139            capsMask: 0,
140            ledsCount: 20,
141            channels: channels_info,
142            deviceId: DEVICE_ID,
143        };
144
145        let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
146
147        let result = CueDeviceInfo::from_ffi(info_ptr);
148        assert!(
149            matches!(result.unwrap_err(), CueDeviceInfoFromFfiError::NullPointerField(field) if field == "model")
150        )
151    }
152
153    #[test]
154    fn device_from_ffi_model_invalid_utf8() {
155        let invalid_utf8 = CString::new([0xC0, 0xC0, 0xC0, 0xC0]).unwrap();
156
157        let channels_info = ffi::CorsairChannelsInfo {
158            channelsCount: 0,
159            channels: ptr::null_mut(),
160        };
161
162        let mut info = ffi::CorsairDeviceInfo {
163            type_: ffi::CorsairDeviceType_CDT_Cooler,
164            model: invalid_utf8.as_ptr(),
165            physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
166            logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
167            capsMask: 0,
168            ledsCount: 20,
169            channels: channels_info,
170            deviceId: DEVICE_ID,
171        };
172
173        let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
174
175        let result = CueDeviceInfo::from_ffi(info_ptr);
176        assert!(
177            matches!(result.unwrap_err(), CueDeviceInfoFromFfiError::StringConversionError {field, ..} if field == "model")
178        )
179    }
180
181    #[test]
182    fn device_from_ffi_invalid_leds_count() {
183        let cool_device_model = CString::new("some-cool-device-model").unwrap();
184
185        let channels_info = ffi::CorsairChannelsInfo {
186            channelsCount: 0,
187            channels: ptr::null_mut(),
188        };
189
190        let mut info = ffi::CorsairDeviceInfo {
191            type_: ffi::CorsairDeviceType_CDT_Cooler,
192            model: cool_device_model.as_ptr(),
193            physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
194            logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
195            capsMask: 0,
196            ledsCount: -1,
197            channels: channels_info,
198            deviceId: DEVICE_ID,
199        };
200
201        let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
202
203        let result = CueDeviceInfo::from_ffi(info_ptr);
204        assert_eq!(
205            result.unwrap_err(),
206            CueDeviceInfoFromFfiError::InvalidLedsCount(-1)
207        );
208    }
209
210    #[test]
211    fn device_from_ffi_invalid_channels() {
212        let cool_device_model = CString::new("some-cool-device-model").unwrap();
213
214        let channels_info = ffi::CorsairChannelsInfo {
215            channelsCount: -1,
216            channels: ptr::null_mut(),
217        };
218
219        let mut info = ffi::CorsairDeviceInfo {
220            type_: ffi::CorsairDeviceType_CDT_Cooler,
221            model: cool_device_model.as_ptr(),
222            physicalLayout: ffi::CorsairPhysicalLayout_CPL_BR,
223            logicalLayout: ffi::CorsairLogicalLayout_CLL_BR,
224            capsMask: 0,
225            ledsCount: 23,
226            channels: channels_info,
227            deviceId: DEVICE_ID,
228        };
229
230        let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
231
232        let result = CueDeviceInfo::from_ffi(info_ptr);
233        assert!(matches!(
234            result.unwrap_err(),
235            CueDeviceInfoFromFfiError::ChannelsError(_)
236        ));
237    }
238
239    #[test]
240    fn device_from_ffi_all_valid() {
241        let cool_device_model = CString::new("some-cool-device-model").unwrap();
242
243        let mut channel_devices = [
244            ffi::CorsairChannelDeviceInfo {
245                type_: ffi::CorsairChannelDeviceType_CCDT_QL_Fan,
246                deviceLedCount: 6,
247            },
248            ffi::CorsairChannelDeviceInfo {
249                type_: ffi::CorsairChannelDeviceType_CCDT_Strip,
250                deviceLedCount: 6,
251            },
252        ];
253
254        let mut channels = [
255            ffi::CorsairChannelInfo {
256                totalLedsCount: 2,
257                devicesCount: 0,
258                devices: ptr::null_mut(),
259            },
260            ffi::CorsairChannelInfo {
261                totalLedsCount: 23,
262                devicesCount: 0,
263                devices: channel_devices.as_mut_ptr(),
264            },
265        ];
266
267        let channels_info = ffi::CorsairChannelsInfo {
268            channelsCount: 2,
269            channels: channels.as_mut_ptr(),
270        };
271
272        let mut info = ffi::CorsairDeviceInfo {
273            type_: ffi::CorsairDeviceType_CDT_Cooler,
274            model: cool_device_model.as_ptr(),
275            physicalLayout: ffi::CorsairPhysicalLayout_CPL_JP,
276            logicalLayout: ffi::CorsairLogicalLayout_CLL_JP,
277            capsMask: 0,
278            ledsCount: 32,
279            channels: channels_info,
280            deviceId: DEVICE_ID,
281        };
282
283        let info_ptr: *mut ffi::CorsairDeviceInfo = &mut info;
284
285        let result = CueDeviceInfo::from_ffi(info_ptr);
286        assert_eq!(result.unwrap(), CueDeviceInfo {
287            id: DeviceId("\u{11}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P0000\u{10}\u{11} P0 \u{10}P".to_string()),
288            device_type: Some(DeviceType::Cooler),
289            model: "some-cool-device-model".to_string(),
290            layout: Some(DeviceLayout::Keyboard {
291                physical_layout: PhysicalLayout::KeyboardJp,
292                logical_layout: LogicalLayout::KeyboardJp
293            }),
294            capabilities: DeviceCapabilities {
295                lighting: false,
296                property_lookup: false
297            },
298            leds_count: 32,
299            channels: vec![
300                Channel {
301                    total_led_count: 2,
302                    devices: vec![]
303                },
304                Channel {
305                    total_led_count: 23,
306                    devices: vec![]
307                }
308            ]
309        });
310    }
311}