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 alloc::vec::Vec;
15
16/// Output control codes — Table 14.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
18#[repr(u8)]
19#[allow(missing_docs)]
20pub enum OutputControlCode {
21    Nop = 0x00,
22    PermanentOffAbortTimed = 0x01,
23    PermanentOnAbortTimed = 0x02,
24    PermanentOffAllowTimed = 0x03,
25    PermanentOnAllowTimed = 0x04,
26    TemporaryOnResume = 0x05,
27    TemporaryOffResume = 0x06,
28}
29
30impl OutputControlCode {
31    /// Parse from raw byte.
32    pub const fn from_byte(b: u8) -> Result<Self, Error> {
33        Ok(match b {
34            0x00 => Self::Nop,
35            0x01 => Self::PermanentOffAbortTimed,
36            0x02 => Self::PermanentOnAbortTimed,
37            0x03 => Self::PermanentOffAllowTimed,
38            0x04 => Self::PermanentOnAllowTimed,
39            0x05 => Self::TemporaryOnResume,
40            0x06 => Self::TemporaryOffResume,
41            _ => {
42                return Err(Error::MalformedPayload {
43                    code: 0x68,
44                    reason: "unknown output control code",
45                });
46            }
47        })
48    }
49
50    /// Raw byte value.
51    pub const fn as_byte(self) -> u8 {
52        self as u8
53    }
54}
55
56/// Single output record.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub struct OutputRecord {
59    /// Output number on the PD.
60    pub output: u8,
61    /// Control code.
62    pub code: OutputControlCode,
63    /// Timer in 100 ms increments (16-bit LE).
64    pub timer: u16,
65}
66
67impl OutputRecord {
68    /// Wire size of one record.
69    pub const WIRE_LEN: usize = 4;
70}
71
72/// `osdp_OUT` body.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct OutputControl {
75    /// Records (one per output).
76    pub records: Vec<OutputRecord>,
77}
78
79impl OutputControl {
80    /// New, with the given records.
81    pub fn new(records: Vec<OutputRecord>) -> Self {
82        Self { records }
83    }
84
85    /// Encode.
86    pub fn encode(&self) -> Result<Vec<u8>, Error> {
87        if self.records.is_empty() {
88            return Err(Error::MalformedPayload {
89                code: 0x68,
90                reason: "OUT requires at least one record",
91            });
92        }
93        let mut out = Vec::with_capacity(self.records.len() * OutputRecord::WIRE_LEN);
94        for r in &self.records {
95            out.push(r.output);
96            out.push(r.code.as_byte());
97            out.extend_from_slice(&r.timer.to_le_bytes());
98        }
99        Ok(out)
100    }
101
102    /// Decode.
103    pub fn decode(data: &[u8]) -> Result<Self, Error> {
104        if data.is_empty() || data.len() % OutputRecord::WIRE_LEN != 0 {
105            return Err(Error::MalformedPayload {
106                code: 0x68,
107                reason: "OUT payload must be a multiple of 4 bytes",
108            });
109        }
110        let mut records = Vec::with_capacity(data.len() / OutputRecord::WIRE_LEN);
111        for chunk in data.chunks_exact(OutputRecord::WIRE_LEN) {
112            records.push(OutputRecord {
113                output: chunk[0],
114                code: OutputControlCode::from_byte(chunk[1])?,
115                timer: u16::from_le_bytes([chunk[2], chunk[3]]),
116            });
117        }
118        Ok(Self { records })
119    }
120}