azizo_core/
controller.rs

1//! ASUS display controller implementation.
2
3use crate::error::ControllerError;
4use crate::modes::{DisplayMode, EReadingMode, EyeCareMode, ManualMode, NormalMode, VividMode};
5use crate::state::ControllerState;
6
7use libloading::{Library, Symbol};
8use log::{debug, info};
9use std::ffi::c_void;
10use std::fs;
11use std::sync::atomic::{AtomicBool, Ordering};
12use windows_sys::Win32::{
13    Foundation::ERROR_INSUFFICIENT_BUFFER,
14    Storage::Packaging::Appx::{
15        FindPackagesByPackageFamily, GetPackagePathByFullName, PACKAGE_FILTER_HEAD,
16    },
17};
18
19const LOCAL_DLL_NAME: &str = "AsusCustomizationRpcClient.dll";
20
21// =============================================================================
22// Display Controller Trait
23// =============================================================================
24
25/// Trait for display controller implementations.
26///
27/// This allows for mock implementations in tests.
28pub trait DisplayController: Send + Sync {
29    /// Get a snapshot of the current controller state.
30    fn get_state(&self) -> ControllerState;
31
32    /// Refresh slider values from the device.
33    fn refresh_sliders(&self) -> Result<(), ControllerError>;
34
35    /// Sync all slider values from hardware.
36    fn sync_all_sliders(&self) -> Result<(), ControllerError>;
37
38    /// Set the display dimming level (40-100 in splendid units).
39    fn set_dimming(&self, level: i32) -> Result<(), ControllerError>;
40
41    /// Set dimming using percentage (0-100).
42    fn set_dimming_percent(&self, percent: i32) -> Result<(), ControllerError>;
43
44    /// Get the current display mode.
45    fn get_current_mode(&self) -> Result<Box<dyn DisplayMode>, ControllerError>;
46
47    /// Set a display mode.
48    fn set_mode(&self, mode: &dyn DisplayMode) -> Result<(), ControllerError>;
49
50    /// Toggle e-reading mode on/off.
51    fn toggle_e_reading(&self) -> Result<Box<dyn DisplayMode>, ControllerError>;
52}
53
54// =============================================================================
55// Callback State (private module with globals)
56// =============================================================================
57
58mod callback_state {
59    use super::ControllerState;
60    use log::{debug, trace};
61    use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
62
63    static CURRENT_MODE: AtomicI32 = AtomicI32::new(-1);
64    static IS_MONOCHROME: AtomicBool = AtomicBool::new(false);
65    static LAST_NON_EREADING_MODE: AtomicI32 = AtomicI32::new(1);
66
67    static MANUAL_SLIDER: AtomicI32 = AtomicI32::new(50);
68    static EYECARE_SLIDER: AtomicI32 = AtomicI32::new(2);
69    static EREADING_GRAYSCALE: AtomicI32 = AtomicI32::new(3);
70    static EREADING_TEMP: AtomicI32 = AtomicI32::new(562);
71    static CURRENT_DIMMING: AtomicI32 = AtomicI32::new(-1);
72
73    pub(super) fn snapshot() -> ControllerState {
74        ControllerState {
75            mode_id: CURRENT_MODE.load(Ordering::SeqCst),
76            is_monochrome: IS_MONOCHROME.load(Ordering::SeqCst),
77            dimming: CURRENT_DIMMING.load(Ordering::SeqCst),
78            manual_slider: MANUAL_SLIDER.load(Ordering::SeqCst) as u8,
79            eyecare_level: EYECARE_SLIDER.load(Ordering::SeqCst) as u8,
80            ereading_grayscale: EREADING_GRAYSCALE.load(Ordering::SeqCst) as u8,
81            ereading_temp: EREADING_TEMP.load(Ordering::SeqCst) as u8,
82            last_non_ereading_mode: LAST_NON_EREADING_MODE.load(Ordering::SeqCst),
83        }
84    }
85
86    pub(super) fn store_last_non_ereading_mode(mode_id: i32) {
87        LAST_NON_EREADING_MODE.store(mode_id, Ordering::SeqCst);
88    }
89
90    pub(super) fn store_dimming(value: i32) {
91        CURRENT_DIMMING.store(value, Ordering::SeqCst);
92    }
93
94    pub(super) extern "C" fn mode_callback(func: i32, data: i32, str_data: *const i8) {
95        let s = if str_data.is_null() {
96            String::from("null")
97        } else {
98            unsafe {
99                std::ffi::CStr::from_ptr(str_data)
100                    .to_string_lossy()
101                    .to_string()
102            }
103        };
104
105        trace!("callback: func={}, data={}, str='{}'", func, data, s);
106
107        match func {
108            18 => {
109                let parts: Vec<&str> = s.split(',').collect();
110                if parts.len() >= 2 {
111                    if let Ok(dimming) = parts[1].parse::<i32>() {
112                        CURRENT_DIMMING.store(dimming, Ordering::SeqCst);
113                    }
114                }
115                if parts.len() >= 3 {
116                    if let Ok(mono) = parts[2].parse::<i32>() {
117                        IS_MONOCHROME.store(mono != 0, Ordering::SeqCst);
118                    }
119                }
120                CURRENT_MODE.store(data, Ordering::SeqCst);
121
122                debug!(
123                    "mode updated: data={}, dimming={}, monochrome={}",
124                    data,
125                    CURRENT_DIMMING.load(Ordering::SeqCst),
126                    IS_MONOCHROME.load(Ordering::SeqCst)
127                );
128            }
129            20 => {
130                MANUAL_SLIDER.store(data, Ordering::SeqCst);
131                debug!("manual slider updated: {}", data);
132            }
133            21 => {
134                EYECARE_SLIDER.store(data, Ordering::SeqCst);
135                debug!("eyecare slider updated: {}", data);
136            }
137            27 => {
138                let raw = data + 206;
139                let grayscale = raw / 256;
140                let temp = raw % 256;
141                EREADING_GRAYSCALE.store(grayscale, Ordering::SeqCst);
142                EREADING_TEMP.store(temp, Ordering::SeqCst);
143                debug!("e-reading updated: grayscale={}, temp={}", grayscale, temp);
144            }
145            _ => {}
146        }
147    }
148}
149
150// =============================================================================
151// AsusController
152// =============================================================================
153
154/// Guard to ensure only one controller instance exists at a time.
155static INSTANCE_EXISTS: AtomicBool = AtomicBool::new(false);
156
157/// The ASUS display controller.
158///
159/// Provides access to ASUS Splendid display settings including:
160/// - Display modes (Normal, Vivid, Manual, Eye Care, E-Reading)
161/// - Dimming control
162/// - Slider values
163///
164/// # Example
165///
166/// ```no_run
167/// use asus_display_control::{AsusController, DisplayController, NormalMode};
168///
169/// let controller = AsusController::new()?;
170/// controller.set_mode(&NormalMode::new())?;
171/// # Ok::<(), asus_display_control::ControllerError>(())
172/// ```
173///
174/// # Limitations
175///
176/// Only one instance can exist at a time due to DLL/RPC constraints.
177pub struct AsusController {
178    lib: Library,
179    client: *mut c_void,
180}
181
182// Safety: The client pointer is only used with the DLL functions
183// and the Library keeps the DLL loaded for the lifetime of AsusController
184unsafe impl Send for AsusController {}
185unsafe impl Sync for AsusController {}
186
187impl AsusController {
188    /// Create a new controller instance.
189    ///
190    /// Only one instance can exist at a time due to DLL/RPC limitations.
191    /// The instance guard is released when the controller is dropped.
192    ///
193    /// # Errors
194    ///
195    /// - [`ControllerError::AlreadyInitialized`] if another instance already exists
196    /// - [`ControllerError::PackageNotFound`] if the ASUS package is not installed
197    /// - [`ControllerError::DllLoad`] if the DLL fails to load
198    /// - [`ControllerError::RpcInitFailed`] if RPC initialization fails
199    pub fn new() -> Result<Self, ControllerError> {
200        if INSTANCE_EXISTS.swap(true, Ordering::SeqCst) {
201            return Err(ControllerError::AlreadyInitialized);
202        }
203
204        match Self::init_internal() {
205            Ok(controller) => Ok(controller),
206            Err(e) => {
207                INSTANCE_EXISTS.store(false, Ordering::SeqCst);
208                Err(e)
209            }
210        }
211    }
212
213    fn init_internal() -> Result<Self, ControllerError> {
214        let full_name = find_asus_package()?;
215        let path = get_package_path(&full_name)?;
216        let dll_path = format!("{}\\ModuleDll\\HWSettings\\{}", path, LOCAL_DLL_NAME);
217
218        fs::copy(&dll_path, LOCAL_DLL_NAME)?;
219
220        unsafe {
221            let lib = Library::new(LOCAL_DLL_NAME)?;
222
223            type InitFn = unsafe extern "C" fn(*mut *mut c_void) -> i64;
224            let init: Symbol<InitFn> = lib.get(b"MyOptRpcClientInitialize")?;
225
226            let mut client: *mut c_void = std::ptr::null_mut();
227            let result = init(&mut client);
228            if result != 0 || client.is_null() {
229                return Err(ControllerError::RpcInitFailed);
230            }
231
232            type CallbackFn = unsafe extern "C" fn(i32, i32, *const i8);
233            type SetCallbackFn = unsafe extern "C" fn(CallbackFn, *mut c_void);
234            let set_callback: Symbol<SetCallbackFn> =
235                lib.get(b"SetCallbackForReturnOptimizationResult")?;
236            set_callback(callback_state::mode_callback, client);
237
238            Ok(Self { lib, client })
239        }
240    }
241
242    fn call_rpc_get(&self, symbol: &[u8]) -> Result<i64, ControllerError> {
243        unsafe {
244            type GetFn = unsafe extern "C" fn(*mut c_void) -> i64;
245            let func: Symbol<GetFn> = self.lib.get(symbol)?;
246            Ok(func(self.client))
247        }
248    }
249
250    /// Set a splendid mode with a value parameter.
251    ///
252    /// This is used internally by mode implementations.
253    pub fn set_splendid_mode(&self, symbol: &[u8], value: u8) -> Result<(), ControllerError> {
254        unsafe {
255            type SetModeFn = unsafe extern "C" fn(u8, *const i8, *mut c_void) -> i64;
256            let set_fn: Symbol<SetModeFn> = self.lib.get(symbol)?;
257            let empty_str = b"\0".as_ptr() as *const i8;
258            set_fn(value, empty_str, self.client);
259            Ok(())
260        }
261    }
262
263    /// Set monochrome/e-reading mode with grayscale and temp.
264    ///
265    /// This is used internally by [`EReadingMode`].
266    pub fn set_monochrome_mode(&self, grayscale: u8, temp: u8) -> Result<(), ControllerError> {
267        unsafe {
268            type SetMonoFn = unsafe extern "C" fn(i32, *mut c_void) -> i64;
269            let set_mono: Symbol<SetMonoFn> = self.lib.get(b"MyOptSetSplendidMonochromeFunc")?;
270            let value = (grayscale as i32 * 256) + temp as i32 - 206;
271            set_mono(value, self.client);
272            Ok(())
273        }
274    }
275
276    /// Convert dimming from splendid units (40-100) to percentage (0-100).
277    pub fn dimming_to_percent(splendid_value: i32) -> i32 {
278        let clamped = splendid_value.clamp(40, 100);
279        ((clamped - 40) as f32 / 60.0 * 100.0).round() as i32
280    }
281
282    /// Convert dimming from percentage (0-100) to splendid units (40-100).
283    pub fn percent_to_dimming(percent: i32) -> i32 {
284        40 + (percent as f32 / 100.0 * 60.0).round() as i32
285    }
286
287    fn mode_from_state(
288        &self,
289        state: &ControllerState,
290    ) -> Result<Box<dyn DisplayMode>, ControllerError> {
291        match (state.mode_id, state.is_monochrome) {
292            (1, false) => Ok(Box::new(NormalMode::new())),
293            (2, false) => Ok(Box::new(VividMode::new())),
294            (6, false) => Ok(Box::new(ManualMode::from_controller_state(state))),
295            (7, false) => Ok(Box::new(EyeCareMode::from_controller_state(state))),
296            (_, true) => {
297                callback_state::store_last_non_ereading_mode(state.mode_id);
298                Ok(Box::new(EReadingMode::from_controller_state(state)))
299            }
300            _ => Err(ControllerError::ModeNotDetected),
301        }
302    }
303
304    fn restore_last_mode(&self, state: &ControllerState) -> Box<dyn DisplayMode> {
305        match state.last_non_ereading_mode {
306            2 => Box::new(VividMode::new()),
307            6 => Box::new(ManualMode::from_controller_state(state)),
308            7 => Box::new(EyeCareMode::from_controller_state(state)),
309            _ => Box::new(NormalMode::new()),
310        }
311    }
312}
313
314impl DisplayController for AsusController {
315    fn get_state(&self) -> ControllerState {
316        callback_state::snapshot()
317    }
318
319    fn refresh_sliders(&self) -> Result<(), ControllerError> {
320        self.call_rpc_get(b"MyOptGetSplendidManualModeFunc")?;
321        self.call_rpc_get(b"MyOptGetSplendidEyecareModeFunc")?;
322        self.call_rpc_get(b"MyOptGetSplendidMonochromeFunc")?;
323        Ok(())
324    }
325
326    fn sync_all_sliders(&self) -> Result<(), ControllerError> {
327        debug!("syncing all sliders from ASUS...");
328
329        let _ = self.get_current_mode();
330        self.refresh_sliders()?;
331        std::thread::sleep(std::time::Duration::from_millis(500));
332
333        let state = self.get_state();
334        debug!(
335            "sync complete: dimming={}({}%), manual={}, eyecare={}, e-reading(grayscale={}, temp={})",
336            state.dimming,
337            Self::dimming_to_percent(state.dimming),
338            state.manual_slider,
339            state.eyecare_level,
340            state.ereading_grayscale,
341            state.ereading_temp
342        );
343        Ok(())
344    }
345
346    fn set_dimming(&self, level: i32) -> Result<(), ControllerError> {
347        let level = level.clamp(40, 100);
348        unsafe {
349            type SetDimmingFn = unsafe extern "C" fn(i32, *const i8, *mut c_void) -> i64;
350            let set_dimming: Symbol<SetDimmingFn> = self.lib.get(b"MyOptSetSplendidDimmingFunc")?;
351
352            let empty_str = b"\0".as_ptr() as *const i8;
353            let result = set_dimming(level, empty_str, self.client);
354            debug!("set dimming to {}, result: {}", level, result);
355
356            if result == 0 {
357                callback_state::store_dimming(level);
358                Ok(())
359            } else {
360                Err(ControllerError::DimmingFailed(result))
361            }
362        }
363    }
364
365    fn set_dimming_percent(&self, percent: i32) -> Result<(), ControllerError> {
366        let splendid_value = Self::percent_to_dimming(percent.clamp(0, 100));
367        self.set_dimming(splendid_value)
368    }
369
370    fn get_current_mode(&self) -> Result<Box<dyn DisplayMode>, ControllerError> {
371        self.call_rpc_get(b"MyOptGetSplendidColorModeFunc")?;
372        std::thread::sleep(std::time::Duration::from_millis(500));
373
374        let state = self.get_state();
375        self.mode_from_state(&state)
376    }
377
378    fn set_mode(&self, mode: &dyn DisplayMode) -> Result<(), ControllerError> {
379        mode.apply(self)
380    }
381
382    fn toggle_e_reading(&self) -> Result<Box<dyn DisplayMode>, ControllerError> {
383        let current = self.get_current_mode()?;
384        debug!("current mode: {:?}", current);
385
386        let state = self.get_state();
387        let target: Box<dyn DisplayMode> = if current.is_ereading() {
388            let restored = self.restore_last_mode(&state);
389            info!("switching from e-reading to {:?}", restored);
390            restored
391        } else {
392            info!("switching to e-reading");
393            Box::new(EReadingMode::from_controller_state(&state))
394        };
395
396        self.set_mode(&*target)?;
397        Ok(target)
398    }
399}
400
401impl Drop for AsusController {
402    fn drop(&mut self) {
403        unsafe {
404            type UninitFn = unsafe extern "C" fn(*mut c_void);
405            if let Ok(uninit) = self.lib.get::<UninitFn>(b"MyOptRpcClientUninitialize") {
406                uninit(self.client);
407            }
408        }
409        INSTANCE_EXISTS.store(false, Ordering::SeqCst);
410    }
411}
412
413// =============================================================================
414// Windows Package Helpers
415// =============================================================================
416
417fn find_asus_package() -> Result<String, ControllerError> {
418    let family_name: Vec<u16> = "B9ECED6F.ASUSPCAssistant_qmba6cd70vzyy\0"
419        .encode_utf16()
420        .collect();
421
422    let mut count = 0u32;
423    let mut buffer_length = 0u32;
424
425    let result = unsafe {
426        FindPackagesByPackageFamily(
427            family_name.as_ptr(),
428            PACKAGE_FILTER_HEAD,
429            &mut count,
430            std::ptr::null_mut(),
431            &mut buffer_length,
432            std::ptr::null_mut(),
433            std::ptr::null_mut(),
434        )
435    };
436
437    if result != ERROR_INSUFFICIENT_BUFFER {
438        return Err(ControllerError::PackageNotFound(result));
439    }
440
441    let mut package_names: Vec<*mut u16> = vec![std::ptr::null_mut(); count as usize];
442    let mut buffer: Vec<u16> = vec![0; buffer_length as usize];
443
444    let result = unsafe {
445        FindPackagesByPackageFamily(
446            family_name.as_ptr(),
447            PACKAGE_FILTER_HEAD,
448            &mut count,
449            package_names.as_mut_ptr(),
450            &mut buffer_length,
451            buffer.as_mut_ptr(),
452            std::ptr::null_mut(),
453        )
454    };
455
456    if result != 0 {
457        return Err(ControllerError::PackageNotFound(result));
458    }
459
460    let full_name = unsafe {
461        let ptr = package_names[0];
462        let len = (0..).take_while(|&i| *ptr.add(i) != 0).count();
463        String::from_utf16_lossy(std::slice::from_raw_parts(ptr, len))
464    };
465
466    Ok(full_name)
467}
468
469fn get_package_path(full_name: &str) -> Result<String, ControllerError> {
470    let full_name_wide: Vec<u16> = format!("{}\0", full_name).encode_utf16().collect();
471    let mut buffer_length = 0u32;
472
473    let result = unsafe {
474        GetPackagePathByFullName(
475            full_name_wide.as_ptr(),
476            &mut buffer_length,
477            std::ptr::null_mut(),
478        )
479    };
480
481    if result != ERROR_INSUFFICIENT_BUFFER {
482        return Err(ControllerError::PackagePathError(result));
483    }
484
485    let mut buffer: Vec<u16> = vec![0; buffer_length as usize];
486    let result = unsafe {
487        GetPackagePathByFullName(
488            full_name_wide.as_ptr(),
489            &mut buffer_length,
490            buffer.as_mut_ptr(),
491        )
492    };
493
494    if result != 0 {
495        return Err(ControllerError::PackagePathError(result));
496    }
497
498    let len = buffer.iter().take_while(|&&c| c != 0).count();
499    Ok(String::from_utf16_lossy(&buffer[..len]))
500}