Skip to main content

cue_sdk/
session.rs

1use std::mem::MaybeUninit;
2use std::sync::mpsc;
3use std::time::Duration;
4
5use core::ffi::{c_char, c_int};
6use cue_sdk_sys as ffi;
7
8use crate::callback::{self, SessionStateChange};
9use crate::device::{DeviceId, DeviceInfo, DeviceType};
10use crate::error::{self, Result, SdkError};
11#[cfg(feature = "async")]
12use crate::event::AsyncEventSubscription;
13use crate::event::{EventSubscription, MacroKeyId};
14use crate::led::{LedColor, LedPosition};
15use crate::property::{DataType, PropertyFlags, PropertyId, PropertyInfo, PropertyValue};
16
17// ---------------------------------------------------------------------------
18// Version
19// ---------------------------------------------------------------------------
20
21/// A semantic version triple as reported by the SDK.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Version {
24    pub major: i32,
25    pub minor: i32,
26    pub patch: i32,
27}
28
29impl Version {
30    pub(crate) fn from_ffi(v: &ffi::CorsairVersion) -> Self {
31        Self {
32            major: v.major,
33            minor: v.minor,
34            patch: v.patch,
35        }
36    }
37}
38
39impl std::fmt::Display for Version {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
42    }
43}
44
45// ---------------------------------------------------------------------------
46// SessionDetails
47// ---------------------------------------------------------------------------
48
49/// Version information about the client, server, and host.
50#[derive(Debug, Clone, Copy)]
51pub struct SessionDetails {
52    pub client_version: Version,
53    pub server_version: Version,
54    pub server_host_version: Version,
55}
56
57impl SessionDetails {
58    pub(crate) fn from_ffi(d: &ffi::CorsairSessionDetails) -> Self {
59        Self {
60            client_version: Version::from_ffi(&d.clientVersion),
61            server_version: Version::from_ffi(&d.serverVersion),
62            server_host_version: Version::from_ffi(&d.serverHostVersion),
63        }
64    }
65}
66
67// ---------------------------------------------------------------------------
68// SessionState
69// ---------------------------------------------------------------------------
70
71/// The current state of the SDK session.
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum SessionState {
74    Invalid,
75    Closed,
76    Connecting,
77    Timeout,
78    ConnectionRefused,
79    ConnectionLost,
80    Connected,
81    Unknown(u32),
82}
83
84impl SessionState {
85    pub(crate) fn from_ffi(raw: ffi::CorsairSessionState) -> Self {
86        match raw {
87            ffi::CorsairSessionState_CSS_Invalid => Self::Invalid,
88            ffi::CorsairSessionState_CSS_Closed => Self::Closed,
89            ffi::CorsairSessionState_CSS_Connecting => Self::Connecting,
90            ffi::CorsairSessionState_CSS_Timeout => Self::Timeout,
91            ffi::CorsairSessionState_CSS_ConnectionRefused => Self::ConnectionRefused,
92            ffi::CorsairSessionState_CSS_ConnectionLost => Self::ConnectionLost,
93            ffi::CorsairSessionState_CSS_Connected => Self::Connected,
94            other => Self::Unknown(other),
95        }
96    }
97}
98
99// ---------------------------------------------------------------------------
100// AccessLevel
101// ---------------------------------------------------------------------------
102
103/// SDK access level for a device.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105#[repr(u32)]
106pub enum AccessLevel {
107    Shared = ffi::CorsairAccessLevel_CAL_Shared,
108    ExclusiveLightingControl = ffi::CorsairAccessLevel_CAL_ExclusiveLightingControl,
109    ExclusiveKeyEventsListening = ffi::CorsairAccessLevel_CAL_ExclusiveKeyEventsListening,
110    ExclusiveLightingControlAndKeyEventsListening =
111        ffi::CorsairAccessLevel_CAL_ExclusiveLightingControlAndKeyEventsListening,
112}
113
114// ---------------------------------------------------------------------------
115// Session
116// ---------------------------------------------------------------------------
117
118/// A connected session to the iCUE SDK.
119///
120/// All SDK operations are methods on this struct.  Dropping the session calls
121/// `CorsairDisconnect`.
122///
123/// Only one `Session` should exist at a time per process.
124pub struct Session {
125    state_rx: mpsc::Receiver<SessionStateChange>,
126    // Prevent the sender from being dropped while the SDK holds the pointer.
127    _state_sender: callback::SessionStateSender,
128}
129
130// SAFETY: The iCUE SDK is documented as thread-safe.  All SDK functions may be
131// called from any thread, and our callback trampolines only send through
132// `mpsc::Sender` which is `Send`.
133unsafe impl Send for Session {}
134unsafe impl Sync for Session {}
135
136impl Session {
137    /// Initiate a connection to iCUE.
138    ///
139    /// This registers the session-state callback and calls `CorsairConnect`.
140    /// Use [`wait_for_connection`](Self::wait_for_connection) afterwards to
141    /// block until the session reaches the `Connected` state.
142    pub fn connect() -> Result<Self> {
143        let (sender, rx) = callback::session_state_channel();
144        let ctx = callback::sender_as_context(&sender);
145
146        // SAFETY: We pass a valid function pointer and a context pointer derived
147        // from a pinned boxed sender that we keep alive in the returned `Session`.
148        error::check(unsafe {
149            ffi::CorsairConnect(Some(callback::session_state_trampoline), ctx)
150        })?;
151
152        Ok(Self {
153            state_rx: rx,
154            _state_sender: sender,
155        })
156    }
157
158    /// Block until the session state becomes `Connected` or the timeout
159    /// elapses.
160    ///
161    /// On success returns the [`SessionDetails`] that were provided with the
162    /// `Connected` state change.
163    ///
164    /// Returns `Err(SdkError::NotConnected)` on timeout or if the session
165    /// enters a terminal error state (refused, lost).
166    pub fn wait_for_connection(&self, timeout: Duration) -> Result<SessionDetails> {
167        let deadline = std::time::Instant::now() + timeout;
168        loop {
169            let remaining = deadline.saturating_duration_since(std::time::Instant::now());
170            if remaining.is_zero() {
171                return Err(SdkError::NotConnected);
172            }
173            match self.state_rx.recv_timeout(remaining) {
174                Ok(change) => {
175                    let state = SessionState::from_ffi(change.state);
176                    match state {
177                        SessionState::Connected => {
178                            return Ok(SessionDetails::from_ffi(&change.details));
179                        }
180                        SessionState::Connecting => continue,
181                        _ => return Err(SdkError::NotConnected),
182                    }
183                }
184                Err(mpsc::RecvTimeoutError::Timeout) => return Err(SdkError::NotConnected),
185                Err(mpsc::RecvTimeoutError::Disconnected) => return Err(SdkError::NotConnected),
186            }
187        }
188    }
189
190    /// Get the current session details (client/server/host versions).
191    pub fn details(&self) -> Result<SessionDetails> {
192        let mut raw = MaybeUninit::<ffi::CorsairSessionDetails>::uninit();
193        // SAFETY: We pass a valid pointer to uninitialised memory that the SDK
194        // will write into.  On success, all fields are initialised.
195        error::check(unsafe { ffi::CorsairGetSessionDetails(raw.as_mut_ptr()) })?;
196        // SAFETY: `check` returned `Ok`, so the SDK has fully initialised `raw`.
197        Ok(SessionDetails::from_ffi(unsafe { &raw.assume_init() }))
198    }
199
200    // ---- Devices ----------------------------------------------------------
201
202    /// Enumerate connected devices matching the given type filter.
203    pub fn get_devices(&self, filter: DeviceType) -> Result<Vec<DeviceInfo>> {
204        let ffi_filter = ffi::CorsairDeviceFilter {
205            deviceTypeMask: filter.bits() as c_int,
206        };
207        let mut buf = [MaybeUninit::<ffi::CorsairDeviceInfo>::uninit();
208            ffi::CORSAIR_DEVICE_COUNT_MAX as usize];
209        let mut count: c_int = 0;
210
211        // SAFETY: `buf` is a stack-allocated array large enough for the SDK's
212        // maximum device count.  `count` receives the actual number written.
213        error::check(unsafe {
214            ffi::CorsairGetDevices(
215                &ffi_filter,
216                buf.len() as c_int,
217                buf.as_mut_ptr().cast(),
218                &mut count,
219            )
220        })?;
221
222        let devices = (0..count as usize)
223            // SAFETY: The SDK has initialised exactly `count` elements.
224            .map(|i| DeviceInfo::from_ffi(unsafe { buf[i].assume_init_ref() }))
225            .collect();
226        Ok(devices)
227    }
228
229    /// Get detailed information about a specific device.
230    pub fn get_device_info(&self, device_id: &DeviceId) -> Result<DeviceInfo> {
231        let mut raw = MaybeUninit::<ffi::CorsairDeviceInfo>::uninit();
232        // SAFETY: `device_id.as_ptr()` is a valid null-terminated C string.
233        // `raw` is valid uninitialised memory for the SDK to write into.
234        error::check(unsafe { ffi::CorsairGetDeviceInfo(device_id.as_ptr(), raw.as_mut_ptr()) })?;
235        // SAFETY: `check` returned `Ok`, so the SDK has fully initialised `raw`.
236        Ok(DeviceInfo::from_ffi(unsafe { raw.assume_init_ref() }))
237    }
238
239    // ---- LEDs -------------------------------------------------------------
240
241    /// Get the positions of all LEDs on a device.
242    pub fn get_led_positions(&self, device_id: &DeviceId) -> Result<Vec<LedPosition>> {
243        let mut buf = [MaybeUninit::<ffi::CorsairLedPosition>::uninit();
244            ffi::CORSAIR_DEVICE_LEDCOUNT_MAX as usize];
245        let mut count: c_int = 0;
246
247        // SAFETY: `buf` is large enough for the maximum LED count.
248        // `count` receives the actual number of positions written.
249        error::check(unsafe {
250            ffi::CorsairGetLedPositions(
251                device_id.as_ptr(),
252                buf.len() as c_int,
253                buf.as_mut_ptr().cast(),
254                &mut count,
255            )
256        })?;
257
258        let positions = (0..count as usize)
259            // SAFETY: The SDK has initialised exactly `count` elements.
260            .map(|i| LedPosition::from_ffi(unsafe { buf[i].assume_init_ref() }))
261            .collect();
262        Ok(positions)
263    }
264
265    /// Set LED colors on a device immediately.
266    ///
267    /// `colors` must be a slice of [`LedColor`] with the LED LUIDs set
268    /// correctly for the target device.
269    pub fn set_led_colors(&self, device_id: &DeviceId, colors: &[LedColor]) -> Result<()> {
270        // SAFETY: `LedColor` is `#[repr(C)]` and layout-identical to
271        // `CorsairLedColor` (verified by compile-time assertions in led.rs),
272        // so the pointer cast is valid.  `colors` is a valid slice.
273        error::check(unsafe {
274            ffi::CorsairSetLedColors(
275                device_id.as_ptr(),
276                colors.len() as c_int,
277                colors.as_ptr().cast(),
278            )
279        })
280    }
281
282    /// Buffer LED colors for later flushing with
283    /// [`flush_led_colors`](Self::flush_led_colors).
284    pub fn set_led_colors_buffer(&self, device_id: &DeviceId, colors: &[LedColor]) -> Result<()> {
285        // SAFETY: Same layout guarantee as `set_led_colors`.
286        error::check(unsafe {
287            ffi::CorsairSetLedColorsBuffer(
288                device_id.as_ptr(),
289                colors.len() as c_int,
290                colors.as_ptr().cast(),
291            )
292        })
293    }
294
295    /// Flush all buffered LED color changes.
296    ///
297    /// This is a synchronous wrapper around `CorsairSetLedColorsFlushBufferAsync`:
298    /// it blocks until the SDK signals completion.
299    pub fn flush_led_colors(&self) -> Result<()> {
300        let (sender, rx) = callback::flush_channel();
301        let ctx = callback::sender_as_context(&sender);
302
303        // SAFETY: We pass a valid trampoline and a context pointer to a pinned
304        // sender.  `sender` stays alive on this stack frame until `rx.recv()`
305        // returns, which happens after the SDK invokes the callback.
306        error::check(unsafe {
307            ffi::CorsairSetLedColorsFlushBufferAsync(Some(callback::flush_trampoline), ctx)
308        })?;
309
310        // Wait for the async callback to fire.
311        match rx.recv() {
312            Ok(code) => error::check(code),
313            Err(_) => Err(SdkError::NotConnected),
314        }
315    }
316
317    /// Read current LED colors from a device.
318    ///
319    /// The `colors` slice must have the `id` field of each element pre-set to
320    /// the LED LUID to query; the SDK fills in the `r`, `g`, `b`, `a` values.
321    pub fn get_led_colors(&self, device_id: &DeviceId, colors: &mut [LedColor]) -> Result<()> {
322        // SAFETY: Same layout guarantee as `set_led_colors`.  The SDK reads
323        // each element's `id` and writes the colour fields in place.
324        error::check(unsafe {
325            ffi::CorsairGetLedColors(
326                device_id.as_ptr(),
327                colors.len() as c_int,
328                colors.as_mut_ptr().cast(),
329            )
330        })
331    }
332
333    /// Look up the LED LUID for a key name character on a keyboard device.
334    pub fn get_led_luid_for_key_name(&self, device_id: &DeviceId, key_name: c_char) -> Result<u32> {
335        let mut luid: ffi::CorsairLedLuid = 0;
336        // SAFETY: `luid` is a valid output pointer.
337        error::check(unsafe {
338            ffi::CorsairGetLedLuidForKeyName(device_id.as_ptr(), key_name, &mut luid)
339        })?;
340        Ok(luid)
341    }
342
343    /// Set the layer priority for this client (0–255).
344    pub fn set_layer_priority(&self, priority: u32) -> Result<()> {
345        // SAFETY: No pointer arguments; pure value call.
346        error::check(unsafe { ffi::CorsairSetLayerPriority(priority) })
347    }
348
349    // ---- Access control ---------------------------------------------------
350
351    /// Request exclusive control of a device.
352    pub fn request_control(&self, device_id: &DeviceId, level: AccessLevel) -> Result<()> {
353        // SAFETY: `device_id.as_ptr()` is a valid null-terminated C string.
354        error::check(unsafe {
355            ffi::CorsairRequestControl(device_id.as_ptr(), level as ffi::CorsairAccessLevel)
356        })
357    }
358
359    /// Release exclusive control of a device.
360    pub fn release_control(&self, device_id: &DeviceId) -> Result<()> {
361        // SAFETY: `device_id.as_ptr()` is a valid null-terminated C string.
362        error::check(unsafe { ffi::CorsairReleaseControl(device_id.as_ptr()) })
363    }
364
365    // ---- Events -----------------------------------------------------------
366
367    /// Subscribe to SDK events (device connect/disconnect, key events).
368    ///
369    /// Returns an [`EventSubscription`] which unsubscribes on drop.
370    pub fn subscribe_for_events(&self) -> Result<EventSubscription> {
371        let (sender, rx) = callback::event_channel();
372        EventSubscription::new(sender, rx)
373    }
374
375    /// Subscribe to SDK events with an async receiver.
376    ///
377    /// Returns an [`AsyncEventSubscription`] whose [`recv`](AsyncEventSubscription::recv)
378    /// method is `async`.  The subscription unsubscribes on drop.
379    ///
380    /// Requires the `async` feature.
381    #[cfg(feature = "async")]
382    pub fn subscribe_for_events_async(&self) -> Result<AsyncEventSubscription> {
383        let (sender, rx) = callback::async_event_channel();
384        AsyncEventSubscription::new(sender, rx)
385    }
386
387    /// Flush all buffered LED color changes asynchronously.
388    ///
389    /// This is the async counterpart to [`flush_led_colors`](Self::flush_led_colors):
390    /// it `.await`s instead of blocking.
391    ///
392    /// Requires the `async` feature.
393    #[cfg(feature = "async")]
394    pub async fn flush_led_colors_async(&self) -> Result<()> {
395        let (sender, mut rx) = callback::async_flush_channel();
396        let ctx = callback::async_sender_as_context(&sender);
397
398        // SAFETY: We pass a valid trampoline and a context pointer to a pinned
399        // sender.  `sender` stays alive in this async fn's state until
400        // `rx.recv().await` returns, which happens after the SDK invokes the
401        // callback.
402        error::check(unsafe {
403            ffi::CorsairSetLedColorsFlushBufferAsync(Some(callback::async_flush_trampoline), ctx)
404        })?;
405
406        match rx.recv().await {
407            Some(code) => error::check(code),
408            None => Err(SdkError::NotConnected),
409        }
410    }
411
412    /// Configure whether a macro key event should be intercepted.
413    pub fn configure_key_event(
414        &self,
415        device_id: &DeviceId,
416        key_id: MacroKeyId,
417        is_intercepted: bool,
418    ) -> Result<()> {
419        let config = ffi::CorsairKeyEventConfiguration {
420            keyId: key_id as ffi::CorsairMacroKeyId,
421            isIntercepted: is_intercepted,
422        };
423        // SAFETY: `config` is a valid stack-allocated struct.
424        error::check(unsafe { ffi::CorsairConfigureKeyEvent(device_id.as_ptr(), &config) })
425    }
426
427    // ---- Properties -------------------------------------------------------
428
429    /// Get metadata about a device property.
430    pub fn get_device_property_info(
431        &self,
432        device_id: &DeviceId,
433        property: PropertyId,
434        index: u32,
435    ) -> Result<PropertyInfo> {
436        let mut data_type: ffi::CorsairDataType = 0;
437        let mut flags: u32 = 0;
438
439        // SAFETY: Output pointers are valid stack-allocated values.
440        error::check(unsafe {
441            ffi::CorsairGetDevicePropertyInfo(
442                device_id.as_ptr(),
443                property.to_ffi(),
444                index,
445                &mut data_type,
446                &mut flags,
447            )
448        })?;
449
450        Ok(PropertyInfo {
451            data_type: DataType::from_ffi(data_type).unwrap_or(DataType::Int32), // fallback for unknown types
452            flags: PropertyFlags::from_bits_truncate(flags),
453        })
454    }
455
456    /// Read a device property value.
457    ///
458    /// The SDK-allocated memory is freed immediately after the value is copied
459    /// into an owned [`PropertyValue`].
460    pub fn read_device_property(
461        &self,
462        device_id: &DeviceId,
463        property: PropertyId,
464        index: u32,
465    ) -> Result<PropertyValue> {
466        let mut prop = MaybeUninit::<ffi::CorsairProperty>::zeroed();
467
468        // SAFETY: `prop` points to zeroed memory suitable for the SDK to write
469        // into.  On success all fields are initialised.
470        error::check(unsafe {
471            ffi::CorsairReadDeviceProperty(
472                device_id.as_ptr(),
473                property.to_ffi(),
474                index,
475                prop.as_mut_ptr(),
476            )
477        })?;
478
479        // SAFETY: `check` returned `Ok`, so the SDK has fully initialised `prop`.
480        let mut prop = unsafe { prop.assume_init() };
481        // SAFETY: The property was just initialised by the SDK and its `type_`
482        // field matches the union variant.  `from_ffi_and_free` copies the data
483        // out and calls `CorsairFreeProperty` to release SDK memory.
484        unsafe { PropertyValue::from_ffi_and_free(&mut prop) }.ok_or(SdkError::InvalidOperation)
485    }
486
487    /// Write a boolean property to a device.
488    pub fn write_device_property_bool(
489        &self,
490        device_id: &DeviceId,
491        property: PropertyId,
492        index: u32,
493        value: bool,
494    ) -> Result<()> {
495        let prop = crate::property::make_bool_property(value);
496        // SAFETY: `prop` is a valid stack-allocated struct with matching
497        // `type_` and `value` fields.
498        error::check(unsafe {
499            ffi::CorsairWriteDeviceProperty(device_id.as_ptr(), property.to_ffi(), index, &prop)
500        })
501    }
502
503    /// Write an integer property to a device.
504    pub fn write_device_property_int32(
505        &self,
506        device_id: &DeviceId,
507        property: PropertyId,
508        index: u32,
509        value: i32,
510    ) -> Result<()> {
511        let prop = crate::property::make_int32_property(value);
512        // SAFETY: Same as `write_device_property_bool`.
513        error::check(unsafe {
514            ffi::CorsairWriteDeviceProperty(device_id.as_ptr(), property.to_ffi(), index, &prop)
515        })
516    }
517
518    /// Write a float property to a device.
519    pub fn write_device_property_float64(
520        &self,
521        device_id: &DeviceId,
522        property: PropertyId,
523        index: u32,
524        value: f64,
525    ) -> Result<()> {
526        let prop = crate::property::make_float64_property(value);
527        // SAFETY: Same as `write_device_property_bool`.
528        error::check(unsafe {
529            ffi::CorsairWriteDeviceProperty(device_id.as_ptr(), property.to_ffi(), index, &prop)
530        })
531    }
532}
533
534impl Drop for Session {
535    fn drop(&mut self) {
536        // SAFETY: `CorsairDisconnect` is safe to call at any time; it is a
537        // no-op if not connected.  We ignore the return value because we
538        // cannot propagate errors from `Drop`.
539        unsafe {
540            let _ = ffi::CorsairDisconnect();
541        }
542    }
543}