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 crate::payload_util::require_positive_multiple_of;
10use alloc::vec::Vec;
11
12/// LED color values โ€” Table 18.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14#[repr(u8)]
15#[allow(missing_docs)]
16pub enum LedColor {
17    Black = 0,
18    Red = 1,
19    Green = 2,
20    Amber = 3,
21    Blue = 4,
22    Magenta = 5,
23    Cyan = 6,
24    White = 7,
25}
26
27impl LedColor {
28    /// Parse from byte (preserving "unknown" as Black per spec leniency).
29    pub const fn from_byte(b: u8) -> Self {
30        match b {
31            0 => Self::Black,
32            1 => Self::Red,
33            2 => Self::Green,
34            3 => Self::Amber,
35            4 => Self::Blue,
36            5 => Self::Magenta,
37            6 => Self::Cyan,
38            7 => Self::White,
39            _ => Self::Black,
40        }
41    }
42
43    /// Raw byte value.
44    pub const fn as_byte(self) -> u8 {
45        self as u8
46    }
47}
48
49/// Temporary LED control sub-block (6 bytes).
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct LedTemporary {
52    /// Control code (Table 16): 0=NOP, 1=cancel temp, 2=set temp.
53    pub control: u8,
54    /// On time (units of 100 ms).
55    pub on_time: u8,
56    /// Off time (units of 100 ms).
57    pub off_time: u8,
58    /// Color while on.
59    pub on_color: LedColor,
60    /// Color while off.
61    pub off_color: LedColor,
62    /// Total run time, in units of 100 ms (16-bit LE).
63    pub timer: u16,
64}
65
66/// Permanent LED control sub-block (6 bytes).
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub struct LedPermanent {
69    /// Control code (Table 17): 0=NOP, 1=set permanent state.
70    pub control: u8,
71    /// On time.
72    pub on_time: u8,
73    /// Off time.
74    pub off_time: u8,
75    /// Color while on.
76    pub on_color: LedColor,
77    /// Color while off.
78    pub off_color: LedColor,
79}
80
81/// One LED record (14 bytes).
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub struct LedRecord {
84    /// Reader number on the PD.
85    pub reader: u8,
86    /// LED number on the reader.
87    pub led: u8,
88    /// Temporary control half.
89    pub temporary: LedTemporary,
90    /// Permanent control half.
91    pub permanent: LedPermanent,
92}
93
94impl LedRecord {
95    /// Wire size of one record.
96    pub const WIRE_LEN: usize = 14;
97
98    fn encode_into(&self, out: &mut Vec<u8>) {
99        out.push(self.reader);
100        out.push(self.led);
101
102        out.push(self.temporary.control);
103        out.push(self.temporary.on_time);
104        out.push(self.temporary.off_time);
105        out.push(self.temporary.on_color.as_byte());
106        out.push(self.temporary.off_color.as_byte());
107        out.extend_from_slice(&self.temporary.timer.to_le_bytes());
108
109        out.push(self.permanent.control);
110        out.push(self.permanent.on_time);
111        out.push(self.permanent.off_time);
112        out.push(self.permanent.on_color.as_byte());
113        out.push(self.permanent.off_color.as_byte());
114    }
115
116    fn decode(buf: &[u8]) -> Self {
117        Self {
118            reader: buf[0],
119            led: buf[1],
120            temporary: LedTemporary {
121                control: buf[2],
122                on_time: buf[3],
123                off_time: buf[4],
124                on_color: LedColor::from_byte(buf[5]),
125                off_color: LedColor::from_byte(buf[6]),
126                timer: u16::from_le_bytes([buf[7], buf[8]]),
127            },
128            permanent: LedPermanent {
129                control: buf[9],
130                on_time: buf[10],
131                off_time: buf[11],
132                on_color: LedColor::from_byte(buf[12]),
133                off_color: LedColor::from_byte(buf[13]),
134            },
135        }
136    }
137}
138
139/// `osdp_LED` body.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct LedControl {
142    /// Records (one per LED).
143    pub records: Vec<LedRecord>,
144}
145
146impl LedControl {
147    /// New body.
148    pub fn new(records: Vec<LedRecord>) -> Self {
149        Self { records }
150    }
151
152    /// Encode.
153    pub fn encode(&self) -> Result<Vec<u8>, Error> {
154        if self.records.is_empty() {
155            return Err(Error::MalformedPayload {
156                code: 0x69,
157                reason: "LED requires at least one record",
158            });
159        }
160        let mut out = Vec::with_capacity(self.records.len() * LedRecord::WIRE_LEN);
161        for r in &self.records {
162            r.encode_into(&mut out);
163        }
164        Ok(out)
165    }
166
167    /// Decode.
168    pub fn decode(data: &[u8]) -> Result<Self, Error> {
169        require_positive_multiple_of(data, LedRecord::WIRE_LEN, 0x69)?;
170        let records = data
171            .chunks_exact(LedRecord::WIRE_LEN)
172            .map(LedRecord::decode)
173            .collect();
174        Ok(Self { records })
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn roundtrip() {
184        let body = LedControl::new(alloc::vec![LedRecord {
185            reader: 0,
186            led: 1,
187            temporary: LedTemporary {
188                control: 2,
189                on_time: 5,
190                off_time: 5,
191                on_color: LedColor::Green,
192                off_color: LedColor::Black,
193                timer: 50,
194            },
195            permanent: LedPermanent {
196                control: 1,
197                on_time: 0xFF,
198                off_time: 0,
199                on_color: LedColor::Red,
200                off_color: LedColor::Black,
201            },
202        }]);
203        let bytes = body.encode().unwrap();
204        assert_eq!(bytes.len(), 14);
205        let decoded = LedControl::decode(&bytes).unwrap();
206        assert_eq!(decoded, body);
207    }
208}