use crate::error::Error;
use crate::payload_util::require_positive_multiple_of;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
#[allow(missing_docs)]
pub enum OutputControlCode {
Nop = 0x00,
PermanentOffAbortTimed = 0x01,
PermanentOnAbortTimed = 0x02,
PermanentOffAllowTimed = 0x03,
PermanentOnAllowTimed = 0x04,
TemporaryOnResume = 0x05,
TemporaryOffResume = 0x06,
}
impl OutputControlCode {
pub const fn from_byte(b: u8) -> Result<Self, Error> {
Ok(match b {
0x00 => Self::Nop,
0x01 => Self::PermanentOffAbortTimed,
0x02 => Self::PermanentOnAbortTimed,
0x03 => Self::PermanentOffAllowTimed,
0x04 => Self::PermanentOnAllowTimed,
0x05 => Self::TemporaryOnResume,
0x06 => Self::TemporaryOffResume,
_ => {
return Err(Error::MalformedPayload {
code: 0x68,
reason: "unknown output control code",
});
}
})
}
pub const fn as_byte(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutputRecord {
pub output: u8,
pub code: OutputControlCode,
pub timer: u16,
}
impl OutputRecord {
pub const WIRE_LEN: usize = 4;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutputControl {
pub records: Vec<OutputRecord>,
}
impl OutputControl {
pub fn new(records: Vec<OutputRecord>) -> Self {
Self { records }
}
pub fn encode(&self) -> Result<Vec<u8>, Error> {
if self.records.is_empty() {
return Err(Error::MalformedPayload {
code: 0x68,
reason: "OUT requires at least one record",
});
}
let mut out = Vec::with_capacity(self.records.len() * OutputRecord::WIRE_LEN);
for r in &self.records {
out.push(r.output);
out.push(r.code.as_byte());
out.extend_from_slice(&r.timer.to_le_bytes());
}
Ok(out)
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
require_positive_multiple_of(data, OutputRecord::WIRE_LEN, 0x68)?;
let mut records = Vec::with_capacity(data.len() / OutputRecord::WIRE_LEN);
for chunk in data.chunks_exact(OutputRecord::WIRE_LEN) {
records.push(OutputRecord {
output: chunk[0],
code: OutputControlCode::from_byte(chunk[1])?,
timer: u16::from_le_bytes([chunk[2], chunk[3]]),
});
}
Ok(Self { records })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_record_roundtrip() {
let body = OutputControl::new(alloc::vec![OutputRecord {
output: 0x02,
code: OutputControlCode::PermanentOnAllowTimed,
timer: 0x00FA,
}]);
let bytes = body.encode().unwrap();
assert_eq!(bytes, [0x02, 0x04, 0xFA, 0x00]);
assert_eq!(OutputControl::decode(&bytes).unwrap(), body);
}
#[test]
fn empty_records_rejected() {
assert!(matches!(
OutputControl::new(Vec::new()).encode(),
Err(Error::MalformedPayload { code: 0x68, .. })
));
}
#[test]
fn decode_rejects_misaligned_payload() {
assert!(matches!(
OutputControl::decode(&[0x00, 0x00, 0x00]),
Err(Error::PayloadNotMultiple { code: 0x68, .. })
));
}
#[test]
fn decode_rejects_unknown_control_code() {
assert!(matches!(
OutputControl::decode(&[0x00, 0x99, 0x00, 0x00]),
Err(Error::MalformedPayload { code: 0x68, .. })
));
}
}