1use core::{fmt::Display, ops::RangeInclusive};
3
4pub const PACKET_LENGTH_31: usize = 18;
6
7pub const VALID_DISPLAY: RangeInclusive<u8> = 0x20..=0x7F;
9
10#[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#[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 fn into(self) -> u8 {
30 match self {
31 Self::Zero => 0,
32 Self::OneSeventh => 36, 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#[derive(Debug, PartialEq, Eq)]
56#[cfg_attr(feature = "defmt", derive(defmt::Format))]
57pub enum Error {
58 AddressInvalid,
60 BadLength { expected: usize, got: usize },
62 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 pub fn new_unchecked(buf: T) -> Self {
99 Self { buf }
100 }
101 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 if !((0x20..=0x7f).contains(b) || *b == 0) {
123 return Err(Error::BadDisplayData { position: i as u8 });
125 }
126 }
127 Ok(())
128 }
129
130 pub fn inner(self) -> T {
132 self.buf
133 }
134
135 pub fn display_data(&self) -> &str {
137 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 unsafe { str::from_utf8_unchecked(&self.buf.as_ref()[range]).trim_end() }
145 }
146
147 pub fn address(&self) -> u8 {
149 self.buf.as_ref()[fields::ADDRESS] & 0x7f
150 }
151
152 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 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 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 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 pub fn set_display_data<'a, S>(&mut self, s: S)
210 where
211 S: Into<&'a str>,
212 {
213 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 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}