1use crate::error::Error;
14use crate::payload_util::require_positive_multiple_of;
15use alloc::vec::Vec;
16
17#[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 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 pub const fn as_byte(self) -> u8 {
53 self as u8
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct OutputRecord {
60 pub output: u8,
62 pub code: OutputControlCode,
64 pub timer: u16,
66}
67
68impl OutputRecord {
69 pub const WIRE_LEN: usize = 4;
71}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct OutputControl {
76 pub records: Vec<OutputRecord>,
78}
79
80impl OutputControl {
81 pub fn new(records: Vec<OutputRecord>) -> Self {
83 Self { records }
84 }
85
86 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 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}