Skip to main content

osdp/command/
output.rs

1//! `osdp_OUT` (`0x68`) — output control.
2//!
3//! # Spec: §6.8, Table 14
4//!
5//! Body is a sequence of 4-byte records, one per output to drive:
6//!
7//! ```text
8//! +----------+-----------+--------------+--------------+
9//! | output#  | code      | timer LSB    | timer MSB    |
10//! +----------+-----------+--------------+--------------+
11//! ```
12
13use crate::error::Error;
14use crate::payload_util::require_positive_multiple_of;
15use alloc::vec::Vec;
16
17/// Output control codes — Table 14.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[repr(u8)]
20#[allow(missing_docs)]
21pub enum OutputControlCode {
22    Nop = 0x00,
23    PermanentOffAbortTimed = 0x01,
24    PermanentOnAbortTimed = 0x02,
25    PermanentOffAllowTimed = 0x03,
26    PermanentOnAllowTimed = 0x04,
27    TemporaryOnResume = 0x05,
28    TemporaryOffResume = 0x06,
29}
30
31impl OutputControlCode {
32    /// Parse from raw byte.
33    pub const fn from_byte(b: u8) -> Result<Self, Error> {
34        Ok(match b {
35            0x00 => Self::Nop,
36            0x01 => Self::PermanentOffAbortTimed,
37            0x02 => Self::PermanentOnAbortTimed,
38            0x03 => Self::PermanentOffAllowTimed,
39            0x04 => Self::PermanentOnAllowTimed,
40            0x05 => Self::TemporaryOnResume,
41            0x06 => Self::TemporaryOffResume,
42            _ => {
43                return Err(Error::MalformedPayload {
44                    code: 0x68,
45                    reason: "unknown output control code",
46                });
47            }
48        })
49    }
50
51    /// Raw byte value.
52    pub const fn as_byte(self) -> u8 {
53        self as u8
54    }
55}
56
57/// Single output record.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct OutputRecord {
60    /// Output number on the PD.
61    pub output: u8,
62    /// Control code.
63    pub code: OutputControlCode,
64    /// Timer in 100 ms increments (16-bit LE).
65    pub timer: u16,
66}
67
68impl OutputRecord {
69    /// Wire size of one record.
70    pub const WIRE_LEN: usize = 4;
71}
72
73/// `osdp_OUT` body.
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct OutputControl {
76    /// Records (one per output).
77    pub records: Vec<OutputRecord>,
78}
79
80impl OutputControl {
81    /// New, with the given records.
82    pub fn new(records: Vec<OutputRecord>) -> Self {
83        Self { records }
84    }
85
86    /// Encode.
87    pub fn encode(&self) -> Result<Vec<u8>, Error> {
88        if self.records.is_empty() {
89            return Err(Error::MalformedPayload {
90                code: 0x68,
91                reason: "OUT requires at least one record",
92            });
93        }
94        let mut out = Vec::with_capacity(self.records.len() * OutputRecord::WIRE_LEN);
95        for r in &self.records {
96            out.push(r.output);
97            out.push(r.code.as_byte());
98            out.extend_from_slice(&r.timer.to_le_bytes());
99        }
100        Ok(out)
101    }
102
103    /// Decode.
104    pub fn decode(data: &[u8]) -> Result<Self, Error> {
105        require_positive_multiple_of(data, OutputRecord::WIRE_LEN, 0x68)?;
106        let mut records = Vec::with_capacity(data.len() / OutputRecord::WIRE_LEN);
107        for chunk in data.chunks_exact(OutputRecord::WIRE_LEN) {
108            records.push(OutputRecord {
109                output: chunk[0],
110                code: OutputControlCode::from_byte(chunk[1])?,
111                timer: u16::from_le_bytes([chunk[2], chunk[3]]),
112            });
113        }
114        Ok(Self { records })
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn single_record_roundtrip() {
124        let body = OutputControl::new(alloc::vec![OutputRecord {
125            output: 0x02,
126            code: OutputControlCode::PermanentOnAllowTimed,
127            timer: 0x00FA,
128        }]);
129        let bytes = body.encode().unwrap();
130        assert_eq!(bytes, [0x02, 0x04, 0xFA, 0x00]);
131        assert_eq!(OutputControl::decode(&bytes).unwrap(), body);
132    }
133
134    #[test]
135    fn empty_records_rejected() {
136        assert!(matches!(
137            OutputControl::new(Vec::new()).encode(),
138            Err(Error::MalformedPayload { code: 0x68, .. })
139        ));
140    }
141
142    #[test]
143    fn decode_rejects_misaligned_payload() {
144        assert!(matches!(
145            OutputControl::decode(&[0x00, 0x00, 0x00]),
146            Err(Error::PayloadNotMultiple { code: 0x68, .. })
147        ));
148    }
149
150    #[test]
151    fn decode_rejects_unknown_control_code() {
152        assert!(matches!(
153            OutputControl::decode(&[0x00, 0x99, 0x00, 0x00]),
154            Err(Error::MalformedPayload { code: 0x68, .. })
155        ));
156    }
157}