alienware/
lib.rs

1use regex::Regex;
2use std::collections::HashMap;
3use std::fmt;
4use std::fs::File;
5use std::io::prelude::*;
6use std::path::{Path, PathBuf};
7use std::sync::OnceLock;
8
9/// The possible sources of the HDMI output port
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum HDMISource {
12    Cable,
13    Gpu,
14    Unknown,
15}
16
17impl fmt::Display for HDMISource {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        match self {
20            HDMISource::Cable => {
21                write!(f, "cable")
22            }
23            HDMISource::Gpu => {
24                write!(f, "gpu")
25            }
26            _ => {
27                write!(f, "unknown")
28            }
29        }
30    }
31}
32
33/// The possible states of the Input HDMI port
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum HDMICableState {
36    Connected,
37    Unconnected,
38    Unknown,
39}
40
41impl fmt::Display for HDMICableState {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            HDMICableState::Connected => {
45                write!(f, "connected")
46            }
47            HDMICableState::Unconnected => {
48                write!(f, "unconnected")
49            }
50            _ => {
51                write!(f, "unknown")
52            }
53        }
54    }
55}
56
57/// Enumeration of possible LEDs
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub enum Zone {
60    Head,
61    Left,
62    Right,
63}
64
65impl fmt::Display for Zone {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            Zone::Head => {
69                write!(f, "head")
70            }
71            Zone::Left => {
72                write!(f, "left")
73            }
74            Zone::Right => {
75                write!(f, "right")
76            }
77        }
78    }
79}
80
81/// State of the HDMI ports
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83pub struct HDMI {
84    pub source: HDMISource,
85    pub cable_state: HDMICableState,
86    pub exists: bool,
87}
88
89impl Default for HDMI {
90    fn default() -> Self {
91        Self {
92            source: HDMISource::Unknown,
93            cable_state: HDMICableState::Unknown,
94            exists: false,
95        }
96    }
97}
98
99/// Setup of a particular LED
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct RGBZone {
102    pub zone: Zone,
103    pub red: u8,
104    pub green: u8,
105    pub blue: u8,
106}
107
108/// Setup of all of the LEDs
109#[derive(Clone, Default, Debug, PartialEq, Eq)]
110pub struct RGBZones {
111    pub zones: HashMap<Zone, RGBZone>,
112    pub exists: bool,
113}
114
115/// Access to the settings for a Alienware server
116pub struct Alienware {
117    platform: String,
118}
119
120impl Default for Alienware {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl Alienware {
127    /// Construct a new instance of Alienware
128    pub fn new() -> Alienware {
129        Alienware {
130            platform: "/sys/devices/platform/alienware-wmi".to_string(),
131        }
132    }
133
134    /// Construct a new instance of Alienware used for testing that can change the root of the sysfs files
135    #[allow(dead_code)]
136    fn test(platform: String) -> Alienware {
137        Alienware { platform }
138    }
139
140    /// Check that this is an Alienware server (i.e. has the alienware platform settings in sysfs)
141    pub fn is_alienware(&self) -> bool {
142        Path::new(&self.platform).exists()
143    }
144
145    /// Get the state of the HDMI ports
146    pub fn get_hdmi(&self) -> std::io::Result<HDMI> {
147        let mut source = HDMISource::Unknown;
148        let mut cable_state = HDMICableState::Unknown;
149        let mut exists = false;
150        if self.is_alienware() {
151            exists = true;
152            let mut path_buf = PathBuf::new();
153            path_buf.push(&self.platform);
154            path_buf.push("hdmi");
155
156            if path_buf.exists() {
157                source = self.parse_source()?;
158                cable_state = self.parse_cable_state()?;
159            }
160        }
161        Ok(HDMI {
162            source,
163            cable_state,
164            exists,
165        })
166    }
167
168    /// Parse the state of the HDMI Output source
169    fn parse_source(&self) -> std::io::Result<HDMISource> {
170        match self.parse_sys_file("hdmi/source") {
171            Ok(Some(s)) => {
172                if s.eq("cable") {
173                    Ok(HDMISource::Cable)
174                } else if s.eq("gpu") {
175                    Ok(HDMISource::Gpu)
176                } else {
177                    Ok(HDMISource::Unknown)
178                }
179            }
180            Ok(None) => Ok(HDMISource::Unknown),
181            Err(x) => Err(x),
182        }
183    }
184
185    /// Parse the state of the HDMI input cable
186    fn parse_cable_state(&self) -> std::io::Result<HDMICableState> {
187        match self.parse_sys_file("hdmi/cable") {
188            Ok(Some(s)) => {
189                if s.eq("connected") {
190                    Ok(HDMICableState::Connected)
191                } else if s.eq("unconnected") {
192                    Ok(HDMICableState::Unconnected)
193                } else {
194                    Ok(HDMICableState::Unknown)
195                }
196            }
197            Ok(None) => Ok(HDMICableState::Unknown),
198            Err(x) => Err(x),
199        }
200    }
201
202    /// Set the source for the HDMI Output port
203    pub fn set_hdmi_source(self, source: HDMISource) -> std::io::Result<()> {
204        self.write_sys_file(
205            "hdmi/source",
206            match source {
207                HDMISource::Cable => "cable",
208                HDMISource::Gpu => "gpu",
209                HDMISource::Unknown => "unknown",
210            },
211        )?;
212        Ok(())
213    }
214
215    /// Get the state of the various LEDs
216    pub fn get_rgb_zones(&self) -> std::io::Result<RGBZones> {
217        let mut zones = HashMap::new();
218        let mut exists = false;
219        if self.is_alienware() {
220            exists = true;
221            let mut path_buf = PathBuf::new();
222            path_buf.push(&self.platform);
223            path_buf.push("rgb_zones");
224            if path_buf.exists() {
225                path_buf.push("zone00");
226                if path_buf.exists() {
227                    zones.insert(
228                        Zone::Head,
229                        self.parse_rgb_zone(Zone::Head, "rgb_zones/zone00")?,
230                    );
231                }
232
233                path_buf.pop();
234                path_buf.push("zone01");
235                if path_buf.exists() {
236                    zones.insert(
237                        Zone::Left,
238                        self.parse_rgb_zone(Zone::Left, "rgb_zones/zone01")?,
239                    );
240                }
241
242                path_buf.pop();
243                path_buf.push("zone02");
244                if path_buf.exists() {
245                    zones.insert(
246                        Zone::Right,
247                        self.parse_rgb_zone(Zone::Right, "rgb_zones/zone02")?,
248                    );
249                }
250            }
251        }
252        Ok(RGBZones { zones, exists })
253    }
254
255    /// Set an LED colour
256    pub fn set_rgb_zone(&self, zone: Zone, red: u8, green: u8, blue: u8) -> std::io::Result<()> {
257        let rgb = format!("{red:02x}{green:02x}{blue:02x}");
258        self.write_sys_file(
259            match zone {
260                Zone::Head => "rgb_zones/zone00",
261                Zone::Left => "rgb_zones/zone01",
262                Zone::Right => "rgb_zones/zone02",
263            },
264            rgb.as_str(),
265        )?;
266        Ok(())
267    }
268
269    /// Parse the current colour of an LED
270    fn parse_rgb_zone(&self, zone: Zone, file_name: &str) -> std::io::Result<RGBZone> {
271        match self.parse_sys_rgb_file(file_name) {
272            Ok((red, green, blue)) => Ok(RGBZone {
273                zone,
274                red,
275                green,
276                blue,
277            }),
278            Err(x) => Err(x),
279        }
280    }
281
282    /// Checks whether the alienware HDMI setup is available
283    pub fn has_hdmi(self) -> bool {
284        if let Ok(hdmi) = self.get_hdmi() {
285            hdmi.exists
286        } else {
287            false
288        }
289    }
290
291    /// Checks whether the alienware LED setup is available
292    pub fn has_rgb_zones(self) -> bool {
293        if let Ok(rgb_zones) = self.get_rgb_zones() {
294            rgb_zones.exists
295        } else {
296            false
297        }
298    }
299
300    /// Parses a single setting sysfs file
301    fn parse_sys_file(&self, file_name: &str) -> std::io::Result<Option<String>> {
302        static RE: OnceLock<Regex> = OnceLock::new();
303        let re = RE.get_or_init(|| Regex::new(r"\[([^)]+)\]").unwrap());
304        let mut path_buf = PathBuf::new();
305        path_buf.push(&self.platform);
306        path_buf.push(file_name);
307        let mut file = File::open(path_buf.as_path())?;
308        let mut contents = String::new();
309        file.read_to_string(&mut contents).unwrap();
310        let caps = re.captures(contents.as_str()).unwrap();
311        match caps.len() > 0 {
312            true => Ok(Some(caps[1].to_string())),
313            false => Ok(None),
314        }
315    }
316
317    /// Parses a sysfs file that holds an RGB setting
318    fn parse_sys_rgb_file(&self, file_name: &str) -> std::io::Result<(u8, u8, u8)> {
319        static RE: OnceLock<Regex> = OnceLock::new();
320        let re = RE.get_or_init(|| Regex::new(r"^red: (\d+), green: (\d+), blue: (\d+)").unwrap());
321        let mut path_buf = PathBuf::new();
322        path_buf.push(&self.platform);
323        path_buf.push(file_name);
324        let mut file = File::open(path_buf)?;
325        let mut contents = String::new();
326        file.read_to_string(&mut contents).unwrap();
327        match re.captures(contents.as_str()) {
328            Some(caps) if caps.len() == 4 => {
329                let red = &caps[1];
330                let green = &caps[2];
331                let blue = &caps[3];
332                Ok((
333                    red.parse::<u8>().unwrap(),
334                    green.parse::<u8>().unwrap(),
335                    blue.parse::<u8>().unwrap(),
336                ))
337            }
338            _ => Ok((0u8, 0u8, 0u8)),
339        }
340    }
341
342    /// Write a value to a sysfs file
343    fn write_sys_file(&self, file_name: &str, value: &str) -> std::io::Result<()> {
344        let mut path_buf = PathBuf::new();
345        path_buf.push(&self.platform);
346        path_buf.push(file_name);
347        let mut sys_file = File::create(path_buf)?;
348        sys_file.write_all(value.as_bytes())?;
349        Ok(())
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use crate::{HDMISource, Zone};
356    use std::fs::{create_dir_all, metadata, remove_dir_all, File};
357    use std::io::prelude::*;
358    use std::path::{Path, PathBuf};
359
360    #[test]
361    fn is_alienware() {
362        let alienware = crate::Alienware::test(setup_aw("is_alienware"));
363        let rtn = alienware.is_alienware();
364        assert!(rtn);
365    }
366
367    #[test]
368    fn is_not_alienware() {
369        let alienware = crate::Alienware::test(setup_not_aw("is_not_alienware"));
370        let rtn = alienware.is_alienware();
371        assert!(!rtn);
372    }
373
374    #[test]
375    fn has_rgb_zones() {
376        let alienware = crate::Alienware::test(setup_aw("has_rgb_zones"));
377        let rtn = alienware.has_rgb_zones();
378        assert!(rtn);
379    }
380
381    #[test]
382    fn get_rgb_zones() {
383        let alienware = crate::Alienware::test(setup_aw("get_rgb_zones"));
384        let rgbzone = alienware.get_rgb_zones();
385        assert!(rgbzone.is_ok());
386        if let Ok(rgbzone) = rgbzone {
387            assert_eq!(rgbzone.zones.len(), 3);
388            let head = rgbzone.zones.get(&crate::Zone::Head).unwrap();
389            assert_eq!(head.zone, crate::Zone::Head);
390            assert_eq!(head.red, 0u8);
391            assert_eq!(head.green, 0u8);
392            assert_eq!(head.blue, 15u8);
393            let left = rgbzone.zones.get(&crate::Zone::Left).unwrap();
394            assert_eq!(left.zone, crate::Zone::Left);
395            assert_eq!(left.red, 0u8);
396            assert_eq!(left.green, 15u8);
397            assert_eq!(left.blue, 0u8);
398            let right = rgbzone.zones.get(&crate::Zone::Right).unwrap();
399            assert_eq!(right.zone, crate::Zone::Right);
400            assert_eq!(right.red, 15u8);
401            assert_eq!(right.green, 0u8);
402            assert_eq!(right.blue, 0u8);
403        }
404    }
405
406    #[test]
407    fn set_rgb_zones() {
408        let alienware = crate::Alienware::test(setup_aw("set_rgb_zones"));
409        match alienware.set_rgb_zone(Zone::Left, 15, 7, 0) {
410            Err(_) => {
411                panic!("Failed to set the RGB Zone");
412            }
413            Ok(()) => {
414                let path = Path::new(
415                    "/tmp/alienware_wmi_test/set_rgb_zones/alienware-wmi/rgb_zones/zone01",
416                );
417                assert!(path.exists());
418                let mut file = File::open(path).unwrap();
419                let mut contents = String::new();
420                file.read_to_string(&mut contents).unwrap();
421                assert_eq!("0f0700", contents);
422            }
423        }
424    }
425
426    #[test]
427    fn has_hdmi() {
428        let alienware = crate::Alienware::test(setup_aw("has_hdmi"));
429        let rtn = alienware.has_hdmi();
430        assert!(rtn);
431    }
432
433    #[test]
434    fn get_hdmi() {
435        let alienware = crate::Alienware::test(setup_aw("get_hdmi"));
436        let hdmi = alienware.get_hdmi();
437        assert!(hdmi.is_ok());
438        if let Ok(hdmi) = hdmi {
439            assert!(hdmi.exists);
440            assert_eq!(hdmi.source, crate::HDMISource::Gpu);
441            assert_eq!(hdmi.cable_state, crate::HDMICableState::Connected);
442        }
443    }
444
445    #[test]
446    fn set_hdmi_source() {
447        let alienware = crate::Alienware::test(setup_aw("set_hdmi_source"));
448        match alienware.set_hdmi_source(HDMISource::Cable) {
449            Err(_) => {
450                panic!("Failed to set the HDMI Source");
451            }
452            Ok(()) => {
453                let path = "/tmp/alienware_wmi_test/set_hdmi_source/hdmi/source";
454                if metadata(path).is_ok() {
455                    let mut file = File::open(path).unwrap();
456                    let mut contents = String::new();
457                    file.read_to_string(&mut contents).unwrap();
458                    assert_eq!("cable", contents);
459                }
460            }
461        }
462    }
463
464    const TEST_PATH: &str = "/tmp/alienware_wmi_test";
465
466    fn setup_not_aw(test: &str) -> String {
467        let mut path_buf = PathBuf::new();
468        path_buf.push(TEST_PATH);
469        path_buf.push(test);
470        if path_buf.exists() && remove_dir_all(path_buf.as_path()).is_err() {
471            panic!("Failed to remove test path while setting up not_aw scenario")
472        }
473        if create_dir_all(path_buf.as_path()).is_err() {
474            panic!("Failed to setup test path while setting up not_aw scenario")
475        };
476
477        path_buf.push("alienware-wmi");
478        let platform = path_buf.as_os_str().to_str().unwrap().to_string();
479        platform
480    }
481
482    fn setup_aw(test: &str) -> String {
483        let mut path_buf = PathBuf::new();
484        path_buf.push(TEST_PATH);
485        path_buf.push(test);
486        if metadata(path_buf.as_path()).is_ok() && remove_dir_all(path_buf.as_path()).is_err() {
487            panic!("Failed to remove test path while setting up aw scenario")
488        }
489        path_buf.push("alienware-wmi");
490        if create_dir_all(path_buf.as_path()).is_err() {
491            panic!("Failed to setup test path while setting up aw scenario")
492        };
493        // hdmi mux
494        path_buf.push("hdmi");
495        if create_dir_all(path_buf.as_path()).is_err() {
496            panic!("Failed to setup hdmi while setting up aw scenario")
497        };
498
499        // cable file
500        path_buf.push("cable");
501        let mut file = File::create(path_buf.as_path()).unwrap();
502        file.write_all(b"unconnected [connected] unknown").unwrap();
503        path_buf.pop();
504
505        // source file
506        path_buf.push("source");
507        let mut file = File::create(path_buf.as_path()).unwrap();
508        file.write_all(b"cable [gpu] unknown,").unwrap();
509        path_buf.pop();
510
511        path_buf.pop();
512        // rgb_zones
513        path_buf.push("rgb_zones");
514        if create_dir_all(path_buf.as_path()).is_err() {
515            panic!("Failed to setup rgb_zones while setting up aw scenario")
516        };
517
518        // zone00
519        path_buf.push("zone00");
520        let mut file = File::create(path_buf.as_path()).unwrap();
521        file.write_all(b"red: 0, green: 0, blue: 15").unwrap();
522        path_buf.pop();
523
524        // zone01
525        path_buf.push("zone01");
526        let mut file = File::create(path_buf.as_path()).unwrap();
527        file.write_all(b"red: 0, green: 15, blue: 0").unwrap();
528        path_buf.pop();
529
530        // zone02
531        path_buf.push("zone02");
532        let mut file = File::create(path_buf.as_path()).unwrap();
533        file.write_all(b"red: 15, green: 0, blue: 0").unwrap();
534        path_buf.pop();
535
536        path_buf.pop();
537
538        let platform = path_buf.as_os_str().to_str().unwrap().to_string();
539        platform
540    }
541}