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