hinge-angle 0.1.0

A crate for accessing hinge angle sensors on various platforms
Documentation
//! # MacOS Lid Angle Sensor Implementation
//!
//! This module provides access to the MacBook's internal lid angle sensor.
//!
//! ## Compatibility
//!
//! - Introduced with the 2019 16-inch MacBook Pro
//! - Works on newer MacBooks and iMacs
//! - **Known issues**: Does not work on M1/M2 devices

use hidapi::{HidApi, HidDevice, HidError};

use super::HingeAngle;

/// MacBook lid angle sensor interface.
///
/// Provides real-time angle measurements between the laptop lid and base.
#[derive(Debug)]
pub struct Hinge {
    device: HidDevice,
}

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("Device not available")]
    DeviceNotAvailable(#[source] HidError),
    #[error("Failed to read from device")]
    ReadFailed(#[source] HidError),
    #[error("Sensor not found")]
    SensorNotFound,
    #[error("Insufficient data length")]
    InsufficientDataLength,
}

impl HingeAngle for Hinge {
    type Output = Result<u16, Error>;

    /// Reads the current lid angle.
    ///
    /// Returns the angle in degrees (0-360° range).
    /// Returns an error if the device is unavailable or reading fails.
    fn angle(&self) -> Self::Output {
        // [report_id, low_byte, high_byte]
        let mut buf = [1, 0, 0];
        let len = self
            .device
            .get_feature_report(&mut buf)
            .map_err(Error::ReadFailed)?;

        if len == buf.len() {
            // The sensor is accessed via HID feature reports. The angle data is read as
            // a little-endian 16-bit unsigned integer representing degrees.
            let [_, low, high] = buf;
            let raw = u16::from_le_bytes([low, high]);
            Ok(raw)
        } else {
            Err(Error::InsufficientDataLength)
        }
    }
}

impl Hinge {
    /// Apple Inc. vendor ID.
    const VENDOR_ID: u16 = 0x05AC;
    /// Product ID for the lid angle sensor.
    const PRODUCT_ID: u16 = 0x8104;
    /// Sensor usage page.
    const USAGE_PAGE: u16 = 0x0020;
    /// Orientation usage.
    const USAGE: u16 = 0x008A;

    /// Creates a new lid angle sensor interface.
    ///
    /// Attempts to find and connect to the MacBook's lid angle sensor.
    /// Returns `None` if the sensor is not found or cannot be accessed.
    pub fn new() -> Result<Self, Error> {
        let api = HidApi::new().map_err(Error::DeviceNotAvailable)?;

        let device_info = api
            .device_list()
            .find(|d| {
                d.vendor_id() == Self::VENDOR_ID
                    && d.product_id() == Self::PRODUCT_ID
                    && d.usage_page() == Self::USAGE_PAGE
                    && d.usage() == Self::USAGE
            })
            .ok_or(Error::SensorNotFound)?;

        let device = device_info
            .open_device(&api)
            .map_err(Error::DeviceNotAvailable)?;

        Ok(Hinge { device })
    }
}

impl From<HidDevice> for Hinge {
    /// Creates a `Hinge` instance from an existing `HidDevice`.
    fn from(device: HidDevice) -> Self {
        Hinge { device }
    }
}