//! Read data from Corsair RMi and HXi series power supplies.
//!
//! This uses the Linux HIDRAW interface to communicate with the power supply.
//!
//! This crate is based off of this implementation in C: [notaz/corsairmi]
//!
//! # Example
//!
//! ```no_run
//! use corsairmi::PowerSupply;
//!
//! let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
//! println!("Power consumption: {:.1} Watts", psu.input_power()?);
//! # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
//! ```
//!
//! # Features
//!
//! An asynchronous implementation is available with the `tokio` feature flag.
//!
//! # udev rules
//!
//! You will most likely want to update your udev rules so that you can access
//! the power supply as a non superuser.
//!
//! These are my udev rules, you will need to update the `idProduct` field for
//! the product ID of your power supply, you can figure this value out with
//! `lsusb`, or by reading the source.
//!
//! Also note the value for `idProduct` must be **lowercase** hexadecimal.
//!
//! ```text
//! # /etc/udev/rules.d/99-corsair.rules
//! SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c06", MODE="0666"
//! ```
//!
//! udev rules can be reloaded with
//! `sudo udevadm control --reload-rules && sudo udevadm trigger`
//!
//! [notaz/corsairmi]: https://github.com/notaz/corsairmi
#![cfg_attr(docsrs, feature(doc_cfg), feature(doc_auto_cfg))]
use std::{
ffi::OsString,
fs::{self, File, OpenOptions},
io::{self, ErrorKind, Read, Write},
os::unix::io::AsRawFd,
path::{Path, PathBuf},
time::Duration,
};
mod cmd;
/// Asynchronous power supply implementation.
///
/// This requires the `tokio` feature flag.
#[cfg(feature = "tokio")]
pub mod aio;
/// Corsair vendor ID.
pub const VID: u16 = 0x1B1C;
/// Power supply models compatible with this API.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Model {
AX1500i,
HX650i,
HX750i,
HX850i,
HX1000i,
HX1200i,
HX1500i,
RM650i,
RM750i,
RM850i,
RM1000i,
}
impl Model {
/// Get the product ID for the power supply model.
///
/// # Example
///
/// ```
/// use corsairmi::Model;
///
/// let m: Model = Model::RM850i;
/// assert_eq!(m.pid(), 0x1C0Cu16);
/// ```
pub fn pid(&self) -> u16 {
match self {
Model::AX1500i => 0x1c02,
Model::HX650i => 0x1c04,
Model::HX750i => 0x1c05,
Model::HX850i => 0x1c06,
Model::HX1000i => 0x1c07,
Model::HX1200i => 0x1c08,
Model::HX1500i => 0x1c1f,
Model::RM650i => 0x1c0a,
Model::RM750i => 0x1c0b,
Model::RM850i => 0x1c0c,
Model::RM1000i => 0x1c0d,
}
}
}
/// Array of all models.
pub const MODELS: [Model; 11] = [
Model::AX1500i,
Model::HX650i,
Model::HX750i,
Model::HX850i,
Model::HX1000i,
Model::HX1200i,
Model::HX1500i,
Model::RM650i,
Model::RM750i,
Model::RM850i,
Model::RM1000i,
];
/// Power supply output rail.
///
/// This is an input argument for [`PowerSupply::output_select`] and
/// [`aio::PowerSupply::output_select`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Rail {
/// 12V rail.
Rail12v,
/// 5V rail.
Rail5v,
/// 3.3V rail.
Rail3v3,
}
impl Rail {
pub(crate) const fn idx(&self) -> u8 {
match self {
Rail::Rail12v => 0,
Rail::Rail5v => 1,
Rail::Rail3v3 => 2,
}
}
}
impl std::fmt::Display for Rail {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Rail::Rail12v => write!(f, "12V"),
Rail::Rail5v => write!(f, "5V"),
Rail::Rail3v3 => write!(f, "3.3V"),
}
}
}
/// Array of all rails.
pub const RAILS: [Rail; 3] = [Rail::Rail12v, Rail::Rail5v, Rail::Rail3v3];
#[repr(C)]
#[derive(Debug)]
#[allow(non_snake_case)]
struct hidraw_devinfo {
bustype: u32,
vendor: u16,
product: u16,
}
/// Power supply error.
#[derive(Debug)]
pub enum OpenError {
/// IO error.
Io(io::Error),
/// Invalid vendor ID.
///
/// The inner value is the invalid vendor ID received.
InvalidVendorId(u16),
/// Invalid product ID.
///
/// The inner value is the invalid product ID received.
InvalidProductId(u16),
}
impl From<io::Error> for OpenError {
fn from(e: io::Error) -> Self {
OpenError::Io(e)
}
}
impl std::fmt::Display for OpenError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
OpenError::Io(e) => write!(f, "{}", e),
OpenError::InvalidVendorId(vid) => write!(
f,
"Invalid power supply vendor ID 0x{:04X} (expected 0x{:04X})",
vid, VID
),
OpenError::InvalidProductId(pid) => {
write!(f, "Invalid power supply product ID 0x{:04X}", pid)
}
}
}
}
impl std::error::Error for OpenError {}
/// Parses the USB (VID, PID) from the file path component.
///
/// The component is in the form of `0003:046D:C083.0006`.
fn parse_component(component: Option<OsString>) -> Option<(u16, u16)> {
let component = component?;
let data: &str = component.to_str()?;
let vid: u16 = u16::from_str_radix(data.get(5..9)?, 16).ok()?;
let pid: u16 = u16::from_str_radix(data.get(10..14)?, 16).ok()?;
Some((vid, pid))
}
/// Returns `true` if the VID and PID correspond to a valid power supply.
fn valid_vid_pid(vid: u16, pid: u16) -> bool {
vid == VID && MODELS.iter().any(|m| m.pid() == pid)
}
/// Last component of a pathbuf, if it exists.
fn last_component(p: &Path) -> Option<OsString> {
Some(p.components().into_iter().last()?.as_os_str().to_owned())
}
/// List power supply device paths.
///
/// This works by reading the USB vendor ID (VID) and product ID (PID) under
/// `/sys/class/hidraw` and comparing them to known IDs.
///
/// Typically these files are accessible without super user permissions.
///
/// # Example
///
/// ```
/// let mut list = corsairmi::list()?;
/// if let Some(path) = list.pop() {
/// // open PSU here
/// } else {
/// eprintln!("No PSUs found");
/// }
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn list() -> io::Result<Vec<PathBuf>> {
let mut ret: Vec<PathBuf> = Vec::new();
let sys_class_hidraw: &Path = Path::new("/sys/class/hidraw/");
if sys_class_hidraw.is_dir() {
for entry in fs::read_dir(sys_class_hidraw)? {
if let Ok(mut link) = entry?.path().read_link() {
if let Some(hidrawx) = last_component(&link) {
link.pop(); // e.g. hidraw9
link.pop(); // e.g. hidraw
if let Some((vid, pid)) = parse_component(last_component(&link)) {
if valid_vid_pid(vid, pid) {
let mut dev: PathBuf = PathBuf::from("/dev/");
dev.push(hidrawx);
if dev.exists() {
ret.push(dev);
}
}
}
}
}
}
}
ret.sort();
ret.dedup();
Ok(ret)
}
fn open<F>(f: F) -> Result<(F, Model), OpenError>
where
F: AsRawFd,
{
// Only one IOCTL is needed for this crate.
// I did not use the nix crate as it greatly increased compile times.
const IOC_READ: libc::c_ulong = 2;
const IOC_NRBITS: libc::c_ulong = 8;
const IOC_TYPEBITS: libc::c_ulong = 8;
const IOC_SIZEBITS: libc::c_ulong = 14;
const IOC_NRSHIFT: libc::c_ulong = 0;
const IOC_TYPESHIFT: libc::c_ulong = IOC_NRSHIFT + IOC_NRBITS;
const IOC_SIZESHIFT: libc::c_ulong = IOC_TYPESHIFT + IOC_TYPEBITS;
const IOC_DIRSHIFT: libc::c_ulong = IOC_SIZESHIFT + IOC_SIZEBITS;
const HIDIOCGRAWINFO: libc::c_ulong = (IOC_READ << IOC_DIRSHIFT)
| ((b'H' as libc::c_ulong) << IOC_TYPESHIFT)
| (0x03 << IOC_NRSHIFT)
| (std::mem::size_of::<hidraw_devinfo>() << IOC_SIZESHIFT) as libc::c_ulong;
let mut info = hidraw_devinfo {
bustype: u32::MAX,
vendor: u16::MAX,
product: u16::MAX,
};
let fd = f.as_raw_fd();
// safety: `fd` will not be dropped until `f` is dropped
let rc = unsafe { libc::ioctl(fd, HIDIOCGRAWINFO, &mut info) };
if rc == -1 {
Err(OpenError::Io(io::Error::last_os_error()))
} else if info.vendor != VID {
Err(OpenError::InvalidVendorId(info.vendor))
} else if let Some(model) = MODELS.iter().find(|m| m.pid() == info.product) {
Ok((f, *model))
} else {
Err(OpenError::InvalidProductId(info.product))
}
}
/// HID report length in bytes.
const HID_REPORT_LEN: usize = 64;
/// Power supply.
#[derive(Debug)]
pub struct PowerSupply {
f: File,
model: Model,
}
impl PowerSupply {
/// Open the power supply by file path.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // call psu methods here
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn open<P: AsRef<Path>>(path: P) -> Result<PowerSupply, OpenError> {
let f: File = OpenOptions::new().read(true).write(true).open(path)?;
let (f, model) = open(f)?;
Ok(PowerSupply { f, model })
}
/// Get the power supply model.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "PSU model: HX580i"
/// println!("PSU model: {:?}", psu.model());
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub const fn model(&self) -> Model {
self.model
}
fn read(&mut self, cmd: &[u8; 3]) -> io::Result<[u8; HID_REPORT_LEN]> {
let mut buf: [u8; HID_REPORT_LEN] = [0; HID_REPORT_LEN];
self.f.write_all(cmd)?;
self.f.read_exact(&mut buf)?;
if buf[0] != cmd[0] || buf[1] != cmd[1] {
Err(io::Error::new(
ErrorKind::Other,
"Unexpected response from power supply",
))
} else {
Ok(buf)
}
}
fn read_string(&mut self, cmd: &[u8; 3]) -> io::Result<String> {
const RESPONSE_BYTES: usize = 2;
let buf = self.read(cmd)?;
let null_term: usize = buf
.iter()
.skip(RESPONSE_BYTES)
.position(|x| *x == 0)
.unwrap_or(buf.len() - RESPONSE_BYTES)
+ RESPONSE_BYTES;
Ok(String::from_utf8_lossy(&buf[RESPONSE_BYTES..null_term]).to_string())
}
fn read_u32(&mut self, reg: u8) -> io::Result<u32> {
let buf = self.read(&[0x03, reg, 0x0])?;
Ok(u32::from_le_bytes([buf[2], buf[3], buf[4], buf[5]]))
}
fn read_u16(&mut self, reg: u8) -> io::Result<u16> {
let buf = self.read(&[0x03, reg, 0x0])?;
Ok(u16::from_le_bytes([buf[2], buf[3]]))
}
/// PC uptime.
///
/// This is the duration that the PSU has been powering your PC.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "PC uptime: 6935s"
/// println!("PC uptime: {:?}", psu.pc_uptime()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn pc_uptime(&mut self) -> io::Result<Duration> {
let uptime: u32 = self.read_u32(cmd::PC_UPTIME)?;
Ok(Duration::from_secs(u64::from(uptime)))
}
/// Power supply uptime.
///
/// This is the duration that the PSU has been connected to AC power,
/// regardless of whether or not your PC has been powered on.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "PSU uptime: 10535s"
/// println!("PSU uptime: {:?}", psu.uptime()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn uptime(&mut self) -> io::Result<Duration> {
let uptime: u32 = self.read_u32(cmd::UPTIME)?;
Ok(Duration::from_secs(u64::from(uptime)))
}
/// Model name.
///
/// This often contains the same information as [`PowerSupply::model`],
/// but this method is more expensive to call.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "PSU name: HX850i"
/// println!("PSU name: {:?}", psu.name()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn name(&mut self) -> io::Result<String> {
self.read_string(&cmd::NAME)
}
/// Vendor name.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "PSU name: CORSAIR"
/// println!("PSU name: {:?}", psu.vendor()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn vendor(&mut self) -> io::Result<String> {
self.read_string(&cmd::VENDOR)
}
/// Product name.
///
/// This often contains the same information as [`PowerSupply::model`],
/// but this method is more expensive to call.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "PSU product: HX850i"
/// println!("PSU product: {:?}", psu.product()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn product(&mut self) -> io::Result<String> {
self.read_string(&cmd::PRODUCT)
}
/// Temperature reading in Celsius.
///
/// I do not know what this is a temperature reading of.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "Temperature: 42.25"
/// println!("Temperature: {:.2}", psu.temp1()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn temp1(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::TEMP1)?))
}
/// Temperature reading in Celsius.
///
/// I do not know what this is a temperature reading of.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "Temperature: 34.25"
/// println!("Temperature: {:.2}", psu.temp2()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn temp2(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::TEMP2)?))
}
/// Fan rotations per minute.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "RPM: 0.0"
/// println!("RPM: {:.1}", psu.rpm()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn rpm(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::RPM)?))
}
/// Input voltage in volts.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "Input voltage: 115.0"
/// println!("Input voltage: {:.1}", psu.input_voltage()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn input_voltage(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::IN_VOLTAGE)?))
}
/// Input power in watts.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "Input power: 18.0"
/// println!("Input power: {:.1}", psu.input_power()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn input_power(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::IN_POWER)?))
}
/// Input current in amps.
///
/// This is derived from the input power and input voltage.
///
/// # Example
///
/// ```no_run
/// use corsairmi::PowerSupply;
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// // e.g. "Input current: 0.16"
/// println!("Input current: {:.2}", psu.input_current()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn input_current(&mut self) -> io::Result<f32> {
Ok(self.input_power()? / self.input_voltage()?)
}
/// Select the output rail to read from.
///
/// This should be called before calling [`PowerSupply::output_voltage`],
/// [`PowerSupply::output_current`], or [`PowerSupply::output_power`].
///
/// # Example
///
/// ```no_run
/// use corsairmi::{PowerSupply, Rail, RAILS};
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// for rail in RAILS.iter() {
/// psu.output_select(*rail)?;
/// println!("{} output voltage: {}V", rail, psu.output_voltage()?);
/// println!("{} output current: {}A", rail, psu.output_current()?);
/// println!("{} output power: {}W", rail, psu.output_power()?);
/// }
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn output_select(&mut self, rail: Rail) -> io::Result<()> {
debug_assert!(rail.idx() <= 3);
let cmd: [u8; 3] = cmd::output_select(rail.idx());
self.read(&cmd)?;
Ok(())
}
/// Get the output voltage in volts.
///
/// Call [`PowerSupply::output_select`] to select the rail to read from.
///
/// # Example
///
/// ```no_run
/// use corsairmi::{PowerSupply, Rail};
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// psu.output_select(Rail::Rail12v)?;
/// println!("12V rail output voltage: {}V", psu.output_voltage()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn output_voltage(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::OUT_VOLTAGE)?))
}
/// Get the output current in amps.
///
/// Call [`PowerSupply::output_select`] to select the rail to read from.
///
/// # Example
///
/// ```no_run
/// use corsairmi::{PowerSupply, Rail};
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// psu.output_select(Rail::Rail12v)?;
/// println!("12V rail output current: {}A", psu.output_current()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn output_current(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::OUT_CURRENT)?))
}
/// Get the output power in watts.
///
/// Call [`PowerSupply::output_select`] to select the rail to read from.
///
/// # Example
///
/// ```no_run
/// use corsairmi::{PowerSupply, Rail};
///
/// let mut psu: PowerSupply = PowerSupply::open("/dev/hidraw5")?;
/// psu.output_select(Rail::Rail12v)?;
/// println!("12V rail output power: {}W", psu.output_power()?);
/// # Ok::<(), std::boxed::Box<dyn std::error::Error>>(())
/// ```
pub fn output_power(&mut self) -> io::Result<f32> {
Ok(half(self.read_u16(cmd::OUT_POWER)?))
}
}
/// Number format is IEEE half-precision float:
/// * 1 bit sign
/// * 5 bits exponent
/// * 10 bits fraction
#[must_use = "Why covert a value if you are not going to use the result?"]
fn half(reg: u16) -> f32 {
let exponent: i32 = ((reg as i16) >> 11) as i32;
let fraction: i32 = ((reg as i32) << 21) >> 21;
(fraction as f32) * 2.0_f32.powi(exponent)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn unique_pid() {
let mut pids: HashSet<u16> = HashSet::with_capacity(MODELS.len());
for model in MODELS.iter() {
let pid: u16 = model.pid();
if pids.get(&pid).is_some() {
panic!("PID 0x{:04X} for model {:?} is a duplicate", pid, model);
}
pids.insert(model.pid());
}
}
#[test]
#[allow(clippy::float_cmp)]
fn half_convert() {
assert_eq!(half(0), 0.0);
assert_eq!(half(0xF087), 33.75);
assert_eq!(half(0xF07D), 31.25);
assert_eq!(half(0xF062), 24.5);
assert_eq!(half(0x1000), 0.0);
assert_eq!(half(0xF8E6), 115.0);
assert_eq!(half(0x0809), 18.0);
assert_eq!(half(0xD30A), 12.15625);
assert_eq!(half(0xF003), 0.75);
assert_eq!(half(0x0804), 8.0);
assert_eq!(half(0xD141), 5.015625);
assert_eq!(half(0xE01A), 1.625);
assert_eq!(half(0xF80F), 7.5);
assert_eq!(half(0xD0D3), 3.296875);
assert_eq!(half(0xE00D), 0.8125);
assert_eq!(half(0xF805), 2.5);
}
#[test]
fn parse_component_some() {
assert_eq!(
parse_component(Some(OsString::from("0000:1B1C:1C06.000A"))),
Some((0x1B1C, 0x1C06))
);
assert_eq!(
parse_component(Some(OsString::from("0000:1b1c:1c06.000a"))),
Some((0x1B1C, 0x1C06))
);
assert_eq!(
parse_component(Some(OsString::from("0000:1B1C:1C06"))),
Some((0x1B1C, 0x1C06))
);
assert_eq!(
parse_component(Some(OsString::from("0000:1B1C:1C06.000AAAAAAAAA"))),
Some((0x1B1C, 0x1C06)),
);
}
#[test]
fn parse_component_none() {
assert_eq!(parse_component(None), None);
assert_eq!(
parse_component(Some(OsString::from("0000:1B1Z:1C06.000A"))),
None,
);
assert_eq!(parse_component(Some(OsString::from("0000:1B1C:1C0"))), None);
}
#[test]
fn test_valid_vid_pid() {
assert!(valid_vid_pid(VID, Model::HX850i.pid()));
assert!(!valid_vid_pid(0x1234, Model::HX850i.pid()));
assert!(!valid_vid_pid(VID, 0x1234));
}
}