tsl_umd/
v3_1.rs

1//! Version 3.1 implementation
2use core::{fmt::Display, ops::RangeInclusive};
3
4/// TSL 3.1 packets are always 18 bytes long
5pub const PACKET_LENGTH_31: usize = 18;
6
7/// Range of values valid as display data (printable bytes)
8pub const VALID_DISPLAY: RangeInclusive<u8> = 0x20..=0x7F;
9
10/// A wrapper around a byte slice reference representing a TSL v3.1 Packet
11#[derive(Debug, PartialEq, Eq)]
12#[cfg_attr(feature = "defmt", derive(defmt::Format))]
13pub struct TSL31Packet<T: AsRef<[u8]>> {
14    pub(crate) buf: T,
15}
16
17/// Tally light brightness, in 4 discrete steps
18#[derive(Debug, PartialEq, Eq, Clone, Copy)]
19#[cfg_attr(feature = "defmt", derive(defmt::Format))]
20pub enum Brightness {
21    Zero,
22    OneSeventh,
23    OneHalf,
24    Full,
25}
26
27impl Into<u8> for Brightness {
28    /// The brightness value as a u8
29    fn into(self) -> u8 {
30        match self {
31            Self::Zero => 0,
32            Self::OneSeventh => 36, // Approx
33            Self::OneHalf => 128,
34            Self::Full => 255,
35        }
36    }
37}
38
39impl Display for Brightness {
40    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
41        write!(
42            f,
43            "{}",
44            match self {
45                Self::Zero => "0",
46                Self::OneSeventh => "1/7",
47                Self::OneHalf => "1/2",
48                Self::Full => "1",
49            }
50        )
51    }
52}
53
54/// Packet checking error
55#[derive(Debug, PartialEq, Eq)]
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
57pub enum Error {
58    /// The first bit of the address isn't set - so it isn't a valid address
59    AddressInvalid,
60    /// The packet was an unexpected length
61    BadLength { expected: usize, got: usize },
62    /// Bad (non-ascii) bytes in the display data field.
63    BadDisplayData { position: u8 },
64}
65
66impl Display for Error {
67    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
68        match self {
69            Self::AddressInvalid => write!(f, "AddressInvalid"),
70            Self::BadLength { expected, got } => {
71                write!(f, "BadLength: expected {expected}, got {got}")
72            }
73            Self::BadDisplayData { position } => {
74                write!(f, "BadDisplayData at position {position}")
75            }
76        }
77    }
78}
79
80pub(crate) mod fields {
81    use core::ops::Range;
82
83    use super::PACKET_LENGTH_31;
84
85    pub(crate) const ADDRESS: usize = 0;
86    pub(crate) const CONTROL: usize = 1;
87    pub(crate) const DISPLAY_DATA: Range<usize> = 2..PACKET_LENGTH_31;
88}
89
90#[cfg(feature = "std")]
91impl std::error::Error for Error {}
92
93impl<T> TSL31Packet<T>
94where
95    T: AsRef<[u8]>,
96{
97    /// Summon a packet from the given bytes without checking it.
98    pub fn new_unchecked(buf: T) -> Self {
99        Self { buf }
100    }
101    /// Validate the the given bytes are a packet and return it, or an error
102    pub fn new_checked(buf: T) -> Result<Self, Error> {
103        let p = Self::new_unchecked(buf);
104        p.validate()?;
105        Ok(p)
106    }
107
108    pub(crate) fn validate(&self) -> Result<(), Error> {
109        if self.buf.as_ref().len() != PACKET_LENGTH_31 {
110            return Err(Error::BadLength {
111                expected: PACKET_LENGTH_31,
112                got: self.buf.as_ref().len(),
113            });
114        }
115        if self.buf.as_ref()[fields::ADDRESS] & 0x80 == 0 {
116            return Err(Error::AddressInvalid);
117        }
118        for (i, b) in self.buf.as_ref()[fields::DISPLAY_DATA].iter().enumerate() {
119            // N.B technically null bytes violates the spec, which clearly states that
120            // only ascii in the range 0x20..=0x7f is valid. However at least one OSS
121            // tally tool pads with null so... here we are
122            if !((0x20..=0x7f).contains(b) || *b == 0) {
123                // Safe to cast to u8 as len will never exceed 18
124                return Err(Error::BadDisplayData { position: i as u8 });
125            }
126        }
127        Ok(())
128    }
129
130    /// Consumes self, returning the inner bytes
131    pub fn inner(self) -> T {
132        self.buf
133    }
134
135    /// Return the display data as a string, with trailing space/null bytes removed
136    pub fn display_data(&self) -> &str {
137        // Use up to the first null byte, or the whole 16 chars
138        let range = self.buf.as_ref()[fields::DISPLAY_DATA]
139            .iter()
140            .position(|c| *c == 0)
141            .map(|e| fields::DISPLAY_DATA.start..e + fields::DISPLAY_DATA.start)
142            .unwrap_or(fields::DISPLAY_DATA);
143        // This is checked in `new_checked` so is safe to do
144        unsafe { str::from_utf8_unchecked(&self.buf.as_ref()[range]).trim_end() }
145    }
146
147    /// The packet address, from `0x00..=0x7E`
148    pub fn address(&self) -> u8 {
149        self.buf.as_ref()[fields::ADDRESS] & 0x7f
150    }
151
152    /// Tally states, 4 channels
153    pub fn tally(&self) -> [bool; 4] {
154        let ctrl = self.buf.as_ref()[fields::CONTROL];
155        [
156            ctrl & 0b1 != 0,
157            ctrl & 0b10 != 0,
158            ctrl & 0b100 != 0,
159            ctrl & 0b1000 != 0,
160        ]
161    }
162
163    /// Tally brightness
164    pub fn brightness(&self) -> Brightness {
165        match (self.buf.as_ref()[fields::CONTROL] >> 4) & 0x3 {
166            0 => Brightness::Zero,
167            0b01 => Brightness::OneSeventh,
168            0b10 => Brightness::OneHalf,
169            0b11 => Brightness::Full,
170            _ => unreachable!(),
171        }
172    }
173}
174
175impl<T> TSL31Packet<T>
176where
177    T: AsMut<[u8]> + AsRef<[u8]>,
178{
179    /// Set the address. Return Err(()) if the addr is out of range
180    pub fn set_address(&mut self, addr: u8) -> Result<(), ()> {
181        if !(0x0..=0x7E).contains(&addr) {
182            return Err(());
183        }
184        self.buf.as_mut()[fields::ADDRESS] = addr + 0x80;
185        Ok(())
186    }
187
188    /// Set the tally state
189    pub fn set_tally(&mut self, state: [bool; 4]) {
190        let b: u8 = state
191            .iter()
192            .enumerate()
193            .map(|(i, v)| if *v { 1 << i } else { 0 })
194            .sum();
195        self.buf.as_mut()[fields::CONTROL] = (self.buf.as_ref()[fields::CONTROL] & 0xf0) | b;
196    }
197
198    pub fn set_brightness(&mut self, brightness: Brightness) {
199        let b = match brightness {
200            Brightness::Zero => 0,
201            Brightness::OneSeventh => 0b01 << 4,
202            Brightness::OneHalf => 0b10 << 4,
203            Brightness::Full => 0b11 << 4,
204        };
205        self.buf.as_mut()[fields::CONTROL] = (self.buf.as_ref()[fields::CONTROL] & 0x0f) | b;
206    }
207
208    /// Set the display data. Panics if length > 16 or string does not contain printable ascii
209    pub fn set_display_data<'a, S>(&mut self, s: S)
210    where
211        S: Into<&'a str>,
212    {
213        // TODO: don't panic
214        let s: &str = s.into();
215        if s.len() > 16 {
216            panic!("String must not be longer than 16 chars");
217        }
218        if !s.as_bytes().iter().all(|c| VALID_DISPLAY.contains(c)) {
219            panic!("String must be printable ascii only");
220        }
221        // Length is checked above, so safe to do this
222        self.buf.as_mut()[fields::DISPLAY_DATA.start..fields::DISPLAY_DATA.start + s.len()]
223            .copy_from_slice(s.as_bytes());
224    }
225}
226
227impl<T> Display for TSL31Packet<T>
228where
229    T: AsRef<[u8]>,
230{
231    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
232        write!(
233            f,
234            "addr={}, 1={}, 2={}, 3={}, 4={}, brightness={}, display={}",
235            self.address(),
236            self.tally()[0],
237            self.tally()[1],
238            self.tally()[2],
239            self.tally()[3],
240            self.brightness(),
241            self.display_data()
242        )
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    const VALID_RAW: [u8; PACKET_LENGTH_31] = [
250        0x80 + 0x69,
251        0b00011001,
252        b'h',
253        b'e',
254        b'l',
255        b'l',
256        b'o',
257        b' ',
258        b' ',
259        b' ',
260        b' ',
261        b' ',
262        b' ',
263        b' ',
264        b' ',
265        b' ',
266        b' ',
267        b' ',
268    ];
269
270    #[test]
271    fn test_parse() {
272        let p = TSL31Packet::new_checked(VALID_RAW).unwrap();
273        assert_eq!(p.address(), 0x69);
274        assert_eq!(p.tally(), [true, false, false, true]);
275        assert_eq!(p.brightness(), Brightness::OneSeventh);
276        assert_eq!(p.display_data(), "hello");
277    }
278
279    #[test]
280    fn error_bad_length() {
281        assert_eq!(
282            TSL31Packet::new_checked(&[]),
283            Err(Error::BadLength {
284                expected: PACKET_LENGTH_31,
285                got: 0
286            })
287        );
288        assert_eq!(
289            TSL31Packet::new_checked(&[0; PACKET_LENGTH_31 + 1]),
290            Err(Error::BadLength {
291                expected: PACKET_LENGTH_31,
292                got: 19
293            })
294        );
295    }
296
297    #[test]
298    fn error_bad_address() {
299        let mut bad_raw = VALID_RAW;
300        bad_raw[0] = 0x13;
301        assert_eq!(
302            TSL31Packet::new_checked(bad_raw),
303            Err(Error::AddressInvalid)
304        );
305    }
306
307    #[test]
308    fn error_bad_display() {
309        let mut bad_raw = VALID_RAW;
310        let ohno = "oh no 🤔".as_bytes();
311        bad_raw[2..2 + ohno.len()].copy_from_slice(ohno);
312        assert_eq!(
313            TSL31Packet::new_checked(bad_raw),
314            Err(Error::BadDisplayData { position: 6 })
315        );
316    }
317
318    #[test]
319    fn test_set_address() {
320        let buf = [0u8; PACKET_LENGTH_31];
321        let mut p = TSL31Packet::new_unchecked(buf);
322        p.set_address(42).unwrap();
323        assert_eq!(p.address(), 42);
324        assert!(p.set_address(234).is_err());
325    }
326
327    #[test]
328    fn test_set_tally() {
329        let buf = [0u8; PACKET_LENGTH_31];
330        let mut p = TSL31Packet::new_unchecked(buf);
331        for perm in [
332            [false, false, false, false],
333            [true, false, false, false],
334            [false, true, false, false],
335            [false, false, true, false],
336            [false, false, false, true],
337        ] {
338            p.set_tally(perm);
339            assert_eq!(p.tally(), perm);
340        }
341    }
342
343    #[test]
344    fn test_set_brightness() {
345        let buf = [0u8; PACKET_LENGTH_31];
346        let mut p = TSL31Packet::new_unchecked(buf);
347        for b in [
348            Brightness::Zero,
349            Brightness::OneSeventh,
350            Brightness::OneHalf,
351            Brightness::Full,
352        ] {
353            p.set_brightness(b);
354            assert_eq!(p.brightness(), b);
355        }
356    }
357
358    #[test]
359    fn test_set_display_data() {
360        let buf = [0u8; PACKET_LENGTH_31];
361        let mut p = TSL31Packet::new_unchecked(buf);
362        for s in ["", "hello there", "1234567890=+!)()"] {
363            p.set_display_data(s);
364            assert_eq!(p.display_data(), s);
365        }
366    }
367}