Skip to main content

beyond_lib/
helpers.rs

1/*!
2Various helpers for wrapping and handling the raw headset's data.
3*/
4
5use std::{fmt::Display, str::FromStr};
6
7use strum_macros::FromRepr;
8use thiserror::Error;
9
10use crate::ParseError;
11
12#[derive(Clone, Default, Debug, Copy, PartialEq, Eq, FromRepr)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14// // There is a chance more modes will be added in the future HMDs
15// #[non_exhaustive]
16/// Display modes the headset can run at.
17///
18/// See variants for specifics on the display mode capabilities.
19#[repr(u8)]
20pub enum DisplayMode {
21    #[default]
22    /// Displays running at `2560x2560 @ 75 Hz` per-eye.
23    Hz75 = 0,
24    /// Displays running at `1920x1920 @ 90 Hz` per-eye.
25    Hz90 = 1,
26}
27
28impl Display for DisplayMode {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            Self::Hz75 => write!(f, "75Hz"),
32            Self::Hz90 => write!(f, "90Hz"),
33        }
34    }
35}
36
37impl FromStr for DisplayMode {
38    // TODO: Make into real error
39    type Err = String;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s.to_lowercase().as_str() {
43            "75" | "75hz" | "75 hz" => Ok(Self::Hz75),
44            "90" | "90hz" | "90 hz" => Ok(Self::Hz90),
45            _ => Err("Unknown display option. Only 75 & 90 are avalible.".into()),
46        }
47    }
48}
49
50impl DisplayMode {
51    pub const fn id(&self) -> u8 {
52        *self as u8
53    }
54    pub const fn from_id(v: u8) -> Option<Self> {
55        Self::from_repr(v)
56    }
57}
58
59#[derive(Debug, Error, PartialEq, Eq)]
60pub enum ColorStrParseError {
61    #[error("string is not a hexadecimal number")]
62    NonHex,
63    #[error("string is of unknown length {0}")]
64    UnknownSize(usize),
65}
66
67/// Color representation in sRGB.
68#[derive(Clone, Debug, Default, PartialEq, Eq)]
69#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
70pub struct RgbColor {
71    pub red: u8,
72    pub green: u8,
73    pub blue: u8,
74}
75
76impl RgbColor {
77    pub const fn new(red: u8, green: u8, blue: u8) -> Self {
78        Self { red, green, blue }
79    }
80
81    pub const fn as_bytes(&self) -> [u8; 3] {
82        [self.red, self.green, self.blue]
83    }
84    pub fn from_buf(buf: &[u8]) -> Result<Self, ParseError> {
85        Ok(RgbColor {
86            red: *buf.first().ok_or(ParseError::OutOfData)?,
87            green: *buf.get(1).ok_or(ParseError::OutOfData)?,
88            blue: *buf.get(2).ok_or(ParseError::OutOfData)?,
89        })
90    }
91}
92
93impl Display for RgbColor {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        write!(f, "#{:02X}{:02X}{:02X}", self.red, self.green, self.blue)
96    }
97}
98
99impl FromStr for RgbColor {
100    type Err = ColorStrParseError;
101
102    fn from_str(s: &str) -> Result<Self, Self::Err> {
103        let s = s.strip_prefix("#").unwrap_or(s);
104        let v = u64::from_str_radix(s, 16).map_err(|_| ColorStrParseError::NonHex)?;
105
106        let c = match s.len() {
107            // Half-precision grey
108            1 => RgbColor::new(
109                (v & 0xF) as u8 * 0x11,
110                (v & 0xF) as u8 * 0x11,
111                (v & 0xF) as u8 * 0x11,
112            ),
113            // Full-precision grey
114            2 => RgbColor::new(
115                (v & 0xFF) as u8, //
116                (v & 0xFF) as u8, //
117                (v & 0xFF) as u8, //
118            ),
119            // Half-precision RGB
120            3 => RgbColor::new(
121                ((v >> 8) & 0xF) as u8 * 0x11,
122                ((v >> 4) & 0xF) as u8 * 0x11,
123                (v & 0xF) as u8 * 0x11,
124            ),
125            // Half-precision RGBA
126            //4 => Color
127            // Full-precision RGB
128            6 => RgbColor::new(
129                ((v >> 16) & 0xFF) as u8,
130                ((v >> 8) & 0xFF) as u8,
131                (v & 0xFF) as u8,
132            ),
133            // Full-precision RGBA
134            //8 => Color
135            l => return Err(ColorStrParseError::UnknownSize(l)),
136        };
137
138        Ok(c)
139    }
140}
141
142/// Wrapper around the headset's internal brightness value.
143///
144/// Internally, brightness values are handled as an integer between 6 and 542,
145/// mapping onto the -20 to 150 values seen in the official utility.
146/// This provides a safe wrapper to ensure incorrect values don't get passed
147/// around.
148#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
149pub struct Brightness(pub u16);
150
151impl Display for Brightness {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        write!(f, "{}", self.0)
154    }
155}
156
157impl From<u16> for Brightness {
158    fn from(value: u16) -> Self {
159        Self(value)
160    }
161}
162
163impl From<Brightness> for u16 {
164    fn from(val: Brightness) -> Self {
165        val.0
166    }
167}
168
169impl Brightness {
170    /// Lossy attempt to convert from a user-friendly value, to the internal
171    /// quantized representation.
172    ///
173    /// # Errors
174    ///
175    /// If the inputted value is outside the accepted range for conversion.
176    /// Must be between approx `-23.15` and approx `1_988_000_000`.
177    // TODO: Make into real error
178    pub fn try_from_utilfmt(value: f32) -> Result<Self, ()> {
179        let brightness = if value < 100.0 {
180            // Regular calculations
181            (value * 2.16) + 50.0
182        } else {
183            // Overdrive calculations
184            (value * 5.53) - 287.0
185        };
186
187        if brightness < 0.0 || brightness > u16::MAX.into() {
188            return Err(());
189        }
190
191        // SAFETY: Safe due to bounds check beforehand
192        Ok(Self(unsafe { brightness.round().to_int_unchecked() }))
193    }
194
195    /// Converts to a user-friendly.
196    pub fn to_utilfmt(self) -> f32 {
197        let v = self.0;
198
199        if v < 270 {
200            // Regular calculations
201            (f32::from(v) - 50.0) / 2.16
202        } else {
203            // Overdrive calculations
204            (f32::from(v) + 287.0) / 5.53
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn display_mode() {
215        assert_eq!(DisplayMode::from_id(0), Some(DisplayMode::Hz75));
216        assert_eq!(DisplayMode::Hz75.id(), 0);
217
218        assert_eq!(DisplayMode::from_id(1), Some(DisplayMode::Hz90));
219        assert_eq!(DisplayMode::Hz90.id(), 1);
220
221        for i in 2..=0xFF {
222            assert_eq!(DisplayMode::from_id(i), None);
223        }
224    }
225
226    #[test]
227    fn rgb_color_value() {
228        assert_eq!(
229            RgbColor::from_buf(&[10, 20, 30]),
230            Ok(RgbColor::new(10, 20, 30))
231        );
232        assert_eq!(RgbColor::new(10, 20, 30).as_bytes(), [10, 20, 30]);
233
234        assert_eq!(RgbColor::from_buf(&[0]), Err(ParseError::OutOfData));
235        assert_eq!(RgbColor::from_buf(&[0, 0]), Err(ParseError::OutOfData));
236        assert_eq!(
237            RgbColor::from_buf(&[0, 0, 0, 0]),
238            Ok(RgbColor::new(0, 0, 0))
239        );
240    }
241
242    #[test]
243    fn rgb_color_string() {
244        assert_eq!(
245            RgbColor::new(0x12, 0x34, 0x56).to_string(),
246            "#123456".to_owned(),
247        );
248        assert_eq!(
249            RgbColor::new(0xbe, 0xef, 0xf0).to_string(),
250            "#BEEFF0".to_owned(),
251        );
252
253        assert_eq!(
254            RgbColor::from_str("BEEFF0"),
255            Ok(RgbColor::new(0xbe, 0xef, 0xf0)),
256        );
257        assert_eq!(
258            RgbColor::from_str("beeff0"),
259            Ok(RgbColor::new(0xbe, 0xef, 0xf0)),
260        );
261        assert_eq!(
262            RgbColor::from_str("#123456"),
263            Ok(RgbColor::new(0x12, 0x34, 0x56)),
264        );
265
266        assert_eq!(
267            RgbColor::from_str("123"),
268            Ok(RgbColor::new(0x11, 0x22, 0x33)),
269        );
270        assert_eq!(
271            RgbColor::from_str("#7"),
272            Ok(RgbColor::new(0x77, 0x77, 0x77)),
273        );
274        assert_eq!(
275            RgbColor::from_str("f0"),
276            Ok(RgbColor::new(0xf0, 0xf0, 0xf0)),
277        );
278
279        assert_eq!(
280            RgbColor::from_str("eeeeeeeee"),
281            Err(ColorStrParseError::UnknownSize(9)),
282        );
283        assert_eq!(RgbColor::from_str("meow!"), Err(ColorStrParseError::NonHex),);
284    }
285
286    // TODO: Test brightness
287}