hid-types 0.2.1

Rust types for working with USB HID report descriptors
Documentation
//! Types used in Unit items.

use num_enum::{IntoPrimitive, TryFromPrimitive};

use crate::Ident;

/// Identifiers for some known Unit values.
///
/// Due to the complexity of the HID unit system, this enum does not attempt
/// to represent all the possible unit values.
///
/// Unit values are 32 bits long, split into 8 nibbles (4-bit fields).
///
/// The lowest nibble (bits 3..0) specify a unit system:
///
/// - `1`: SI Linear
/// - `2`: SI Rotation
/// - `3`: English Linear
/// - `4`: English Rotation
///
/// The other 5 nibbles are dedicated to individual unit categories:
///
/// - nibble 1 (bits 7..4): Length (Centimeter, Radian, Inch, or Degree)
/// - nibble 2 (bits 11..8): Mass (Gram or Slug)
/// - nibble 3 (bits ..12): Time (Seconds in all systems)
/// - nibble 4 (bits ..16): Temperature (Kelvin or Fahrenheit)
/// - nibble 5 (bits ..20): Current (Ampere in all systems)
/// - nibble 6 (bits ..24): Luminous Intensity (Candela in all systems)
/// - nibble 7 (bits 31..28): reserved
///
/// The value in each nibble is a signed integer from -8 to +7, and that integer
/// is the exponent attached to each unit. For example:
///
/// - `0x23` is Length^2 in the English Linear system, or Inch^2.
/// - `0x101` is Mass^1 in the SI Linear system, or Gram.
/// - `0xF011` is Time^-1 * Length^1, or Centimeters/Second.
///
/// See [Device Class Definition for Human Interface Devices](https://www.usb.org/document-library/device-class-definition-hid-111),
/// section 6.2.2.7 for more details.
///
#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[repr(u32)]
#[non_exhaustive]
pub enum KnownUnit {
    /// No unit.
    None = 0x0,
    /// SI unit of length.
    // Length^1 in SI Linear (1)
    Centimeter = 0x1_1,
    /// SI unit of rotation.
    // Length^1 in SI Rotation (2)
    Radian = 0x1_2,
    /// English unit of length.
    // Length^1 in English Linear (3)
    Inch = 0x1_3,
    /// English unit of rotation.
    // Length^1 in English Rotation(4)
    Degree = 0x1_4,
    /// SI unit of mass.
    // Mass^1 in SI Linear
    Gram = 0x1_0_1,
    /// SI unit of time.
    // Time^1 in SI Linear
    Seconds = 0x1_0_0_1,
    // "Seconds" can be expressed in any unit system.
    #[doc(hidden)]
    _SecondsSystem2 = 0x1_0_0_2,
    #[doc(hidden)]
    _SecondsSystem3 = 0x1_0_0_3,
    #[doc(hidden)]
    _SecondsSystem4 = 0x1_0_0_4,
    /// SI unit of frequency, Hertz aka second⁻¹)
    // Time^-1 in SI Linear
    Hertz = 0xF_0_0_1,
    /// SI unit of velocity (centimeters/second).
    // Length / Time in SI Linear
    SiVelocity = 0xF_0_1_1,
    /// SI unit of momentum (gram·centimeter/second).
    // Mass * Length / Time in SI Linear
    SiMomentum = 0xF_1_1_1,
    /// SI unit of acceleration (centimeters/second²).
    // Length / Time^2 in SI Linear
    SiAcceleration = 0xE_0_1_1,
    /// SI unit of force (gram·centimeter/second²).
    ///
    /// Divide by 10⁵ to get Newtons (kg·m/s²).
    // Mass * Length / Time^2 in SI Linear
    SiForce = 0xE_1_1_1,
    /// SI unit of energy (gram·centimeter²/second²).
    ///
    /// Divide by 10⁷ to get Joules (kg·m²/s²).
    // Mass * Length^2 / Time^2 in SI Linear
    SiEnergy = 0xE_1_2_1,
    /// SI unit of Angular Velocity (radians/second).
    // Length / Time^2 in SI Rotation
    SiAngularVelocity = 0xF_0_1_2,
    /// English unit of Angular Acceleration (degrees/second).
    // Length / Time^2 in SI Rotation
    DegAngularVelocity = 0xF_0_1_4,
    /// SI unit of Angular Acceleration (radians/second²).
    // Length / Time^2 in SI Rotation
    SiAngularAcceleration = 0xE_0_1_2,
    /// English unit of Angular Acceleration (degrees/second²).
    // Length / Time^2 in SI Rotation
    DegAngularAcceleration = 0xE_0_1_4,
    /// SI unit of temperature.
    Kelvin = 0x1_0_0_0_1,
    /// English unit of temperature.
    DegreesFahrenheit = 0x1_0_0_0_3,
    /// SI unit of current (amperes).
    Current = 0x1_0_0_0_0_1,
    /// SI unit for amp·seconds.
    ///
    /// This unit can be used as "battery capacity". See
    /// [Usage Tables for HID Power Devices](https://www.usb.org/document-library/usage-tables-hid-power-devices-11).
    AmpSeconds = 0x1_0_1_0_0_1,
    /// SI unit of Voltage (gram·centimeter²/amp·seconds³).
    ///
    /// Divide by 10⁷ to get Volts (kg·m²/amp·seconds³).
    // (Mass * Length^2) / (Time^3 * Current)
    Voltage = 0xF_0_D_1_2_1,
    /// SI unit of power (gram-centimeter²/seconds³).
    ///
    /// Divide by 10⁷ to get Watts or Volt-Amps (kg·m²/s³).
    // Mass * Length^2 / Time^3
    SiPower = 0xD_1_2_1,
}

/// A __Unit__ value.
pub type Unit = Ident<KnownUnit, u32>;

/// A unit exponent value.
///
/// The HID specification does not specify any limits on the range of this
/// value. However, some developers have concluded that this value may only
/// be 4 bits wide, leading to the confusing situation of a Unit item with
/// a value of e.g. `0x0F`, which was meant to represent `-1`.
///
/// To work around this confusion, there is a method [`remap_as_i4()`][UnitExponent::remap_as_i4]
/// that will attempt to undo the 4-bit-truncation, but may garble
/// some values.
///
/// Note that this is encoded to a byte with zeros in the upper 4 bits,
/// so `-1` will be stored as `0x0F`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct UnitExponent(pub i32);

impl core::ops::Deref for UnitExponent {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl From<i32> for UnitExponent {
    fn from(value: i32) -> Self {
        UnitExponent(value)
    }
}

impl core::ops::DerefMut for UnitExponent {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

impl UnitExponent {
    /// The raw value stored in the Unit Exponent item.
    ///
    /// Due to odd encodings on some devices, the value returned by [`remap_as_i4()`][Self::remap_as_i4]
    /// may be the intended value.
    pub fn raw_value(self) -> i32 {
        self.0
    }

    /// Attempt to remap the value, assuming it may have been truncated to 4 bits.
    ///
    /// This is always correct for inputs in the range `[i32::MIN, 7]` or `[16, i32::MAX]`.
    /// It remaps the range `[8, 15]` to `[-8, -1]`. This could be wrong!
    /// It depends on what the device manufacturer meant.
    ///
    /// If the upper bits are all zeros, then this will return `Some`
    /// with a value that is in the range `[-8, +7]`.
    ///
    /// Otherwise, this will return the original value.
    ///
    /// See the [`UnitExponent`] docs for an explanation why this exists.
    pub fn remap_as_i4(self) -> i32 {
        let upper_bits = (self.0 as u32) & 0xFFFF_FFF0;
        if upper_bits == 0 {
            // 'as' truncates the upper 24 bits.
            // shift-left, shift-right will cause sign
            // extension from bit 3 to bits 4..31
            self.0 << 28 >> 28
        } else {
            self.0
        }
    }
}

#[cfg(feature = "std")]
mod std_impls {
    use std::fmt::{self, Display};

    use super::*;

    impl Display for UnitExponent {
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
            let raw = self.raw_value();
            let remap = self.remap_as_i4();
            if remap == raw {
                write!(f, "{raw}")
            } else {
                write!(f, "{raw} (possibly intended {remap})")
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_i4() {
        assert_eq!(UnitExponent(0).remap_as_i4(), 0);
        assert_eq!(UnitExponent(1).remap_as_i4(), 1);
        assert_eq!(UnitExponent(7).remap_as_i4(), 7);
        assert_eq!(UnitExponent(-1).remap_as_i4(), -1);
        assert_eq!(UnitExponent(-8).remap_as_i4(), -8);
        assert_eq!(UnitExponent(-9).remap_as_i4(), -9);

        // These values are potentially wrong, but there's no way
        // to tell.
        assert_eq!(UnitExponent(8).remap_as_i4(), -8);
        assert_eq!(UnitExponent(15).remap_as_i4(), -1);

        // Large positive or negative values should always pass through.
        assert_eq!(UnitExponent(16).remap_as_i4(), 16);
        assert_eq!(UnitExponent(-16).remap_as_i4(), -16);
        assert_eq!(UnitExponent(i32::MAX).remap_as_i4(), i32::MAX);
        assert_eq!(UnitExponent(i32::MIN).remap_as_i4(), i32::MIN);
    }
}