Skip to main content

osdp/command/
led.rs

1//! `osdp_LED` (`0x69`) โ€” reader LED control.
2//!
3//! # Spec: ยง6.9, Tables 16โ€“18
4//!
5//! Body is a sequence of 14-byte records (Reader#, LED#, then a 6-byte
6//! "temporary" control, then a 6-byte "permanent" control).
7
8use crate::error::Error;
9use alloc::vec::Vec;
10
11/// LED color values โ€” Table 18.
12#[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    /// Parse from byte (preserving "unknown" as Black per spec leniency).
28    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    /// Raw byte value.
43    pub const fn as_byte(self) -> u8 {
44        self as u8
45    }
46}
47
48/// Temporary LED control sub-block (6 bytes).
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct LedTemporary {
51    /// Control code (Table 16): 0=NOP, 1=cancel temp, 2=set temp.
52    pub control: u8,
53    /// On time (units of 100 ms).
54    pub on_time: u8,
55    /// Off time (units of 100 ms).
56    pub off_time: u8,
57    /// Color while on.
58    pub on_color: LedColor,
59    /// Color while off.
60    pub off_color: LedColor,
61    /// Total run time, in units of 100 ms (16-bit LE).
62    pub timer: u16,
63}
64
65/// Permanent LED control sub-block (6 bytes).
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct LedPermanent {
68    /// Control code (Table 17): 0=NOP, 1=set permanent state.
69    pub control: u8,
70    /// On time.
71    pub on_time: u8,
72    /// Off time.
73    pub off_time: u8,
74    /// Color while on.
75    pub on_color: LedColor,
76    /// Color while off.
77    pub off_color: LedColor,
78}
79
80/// One LED record (14 bytes).
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct LedRecord {
83    /// Reader number on the PD.
84    pub reader: u8,
85    /// LED number on the reader.
86    pub led: u8,
87    /// Temporary control half.
88    pub temporary: LedTemporary,
89    /// Permanent control half.
90    pub permanent: LedPermanent,
91}
92
93impl LedRecord {
94    /// Wire size of one record.
95    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/// `osdp_LED` body.
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct LedControl {
141    /// Records (one per LED).
142    pub records: Vec<LedRecord>,
143}
144
145impl LedControl {
146    /// New body.
147    pub fn new(records: Vec<LedRecord>) -> Self {
148        Self { records }
149    }
150
151    /// Encode.
152    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    /// Decode.
167    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}