1use crate::error::Error;
9use alloc::vec::Vec;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[repr(u8)]
14#[allow(missing_docs)]
15pub enum LedColor {
16 Black = 0,
17 Red = 1,
18 Green = 2,
19 Amber = 3,
20 Blue = 4,
21 Magenta = 5,
22 Cyan = 6,
23 White = 7,
24}
25
26impl LedColor {
27 pub const fn from_byte(b: u8) -> Self {
29 match b {
30 0 => Self::Black,
31 1 => Self::Red,
32 2 => Self::Green,
33 3 => Self::Amber,
34 4 => Self::Blue,
35 5 => Self::Magenta,
36 6 => Self::Cyan,
37 7 => Self::White,
38 _ => Self::Black,
39 }
40 }
41
42 pub const fn as_byte(self) -> u8 {
44 self as u8
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct LedTemporary {
51 pub control: u8,
53 pub on_time: u8,
55 pub off_time: u8,
57 pub on_color: LedColor,
59 pub off_color: LedColor,
61 pub timer: u16,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct LedPermanent {
68 pub control: u8,
70 pub on_time: u8,
72 pub off_time: u8,
74 pub on_color: LedColor,
76 pub off_color: LedColor,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct LedRecord {
83 pub reader: u8,
85 pub led: u8,
87 pub temporary: LedTemporary,
89 pub permanent: LedPermanent,
91}
92
93impl LedRecord {
94 pub const WIRE_LEN: usize = 14;
96
97 fn encode_into(&self, out: &mut Vec<u8>) {
98 out.push(self.reader);
99 out.push(self.led);
100
101 out.push(self.temporary.control);
102 out.push(self.temporary.on_time);
103 out.push(self.temporary.off_time);
104 out.push(self.temporary.on_color.as_byte());
105 out.push(self.temporary.off_color.as_byte());
106 out.extend_from_slice(&self.temporary.timer.to_le_bytes());
107
108 out.push(self.permanent.control);
109 out.push(self.permanent.on_time);
110 out.push(self.permanent.off_time);
111 out.push(self.permanent.on_color.as_byte());
112 out.push(self.permanent.off_color.as_byte());
113 }
114
115 fn decode(buf: &[u8]) -> Self {
116 Self {
117 reader: buf[0],
118 led: buf[1],
119 temporary: LedTemporary {
120 control: buf[2],
121 on_time: buf[3],
122 off_time: buf[4],
123 on_color: LedColor::from_byte(buf[5]),
124 off_color: LedColor::from_byte(buf[6]),
125 timer: u16::from_le_bytes([buf[7], buf[8]]),
126 },
127 permanent: LedPermanent {
128 control: buf[9],
129 on_time: buf[10],
130 off_time: buf[11],
131 on_color: LedColor::from_byte(buf[12]),
132 off_color: LedColor::from_byte(buf[13]),
133 },
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct LedControl {
141 pub records: Vec<LedRecord>,
143}
144
145impl LedControl {
146 pub fn new(records: Vec<LedRecord>) -> Self {
148 Self { records }
149 }
150
151 pub fn encode(&self) -> Result<Vec<u8>, Error> {
153 if self.records.is_empty() {
154 return Err(Error::MalformedPayload {
155 code: 0x69,
156 reason: "LED requires at least one record",
157 });
158 }
159 let mut out = Vec::with_capacity(self.records.len() * LedRecord::WIRE_LEN);
160 for r in &self.records {
161 r.encode_into(&mut out);
162 }
163 Ok(out)
164 }
165
166 pub fn decode(data: &[u8]) -> Result<Self, Error> {
168 if data.is_empty() || data.len() % LedRecord::WIRE_LEN != 0 {
169 return Err(Error::MalformedPayload {
170 code: 0x69,
171 reason: "LED payload must be a multiple of 14 bytes",
172 });
173 }
174 let records = data
175 .chunks_exact(LedRecord::WIRE_LEN)
176 .map(LedRecord::decode)
177 .collect();
178 Ok(Self { records })
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn roundtrip() {
188 let body = LedControl::new(alloc::vec![LedRecord {
189 reader: 0,
190 led: 1,
191 temporary: LedTemporary {
192 control: 2,
193 on_time: 5,
194 off_time: 5,
195 on_color: LedColor::Green,
196 off_color: LedColor::Black,
197 timer: 50,
198 },
199 permanent: LedPermanent {
200 control: 1,
201 on_time: 0xFF,
202 off_time: 0,
203 on_color: LedColor::Red,
204 off_color: LedColor::Black,
205 },
206 }]);
207 let bytes = body.encode().unwrap();
208 assert_eq!(bytes.len(), 14);
209 let decoded = LedControl::decode(&bytes).unwrap();
210 assert_eq!(decoded, body);
211 }
212}