Skip to main content

osdp/packet/
header.rs

1//! Address, sequence number, and CTRL byte primitives.
2
3use crate::error::Error;
4
5/// PD bus address.
6///
7/// # Spec: §5.4
8///
9/// Bits 0..7 carry the address. Bit 7 (`0x80`) is the *reply flag*: it is set
10/// in messages from PD → ACU. Address `0x7F` is the broadcast group.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub struct Address(u8);
13
14impl Address {
15    /// Wrap an arbitrary byte. Use [`Address::pd`] / [`Address::reply`] for
16    /// validated constructors.
17    pub const fn from_raw(byte: u8) -> Self {
18        Self(byte)
19    }
20
21    /// Construct an ACU → PD address. Errors if the requested address is
22    /// out of range (`> 0x7F` would clash with the reply flag).
23    pub const fn pd(addr: u8) -> Result<Self, Error> {
24        if addr > crate::BROADCAST_ADDR {
25            return Err(Error::BadAddress(addr));
26        }
27        Ok(Self(addr))
28    }
29
30    /// Construct a PD → ACU address (i.e. with the reply flag set).
31    pub const fn reply(addr: u8) -> Result<Self, Error> {
32        if addr > crate::BROADCAST_ADDR {
33            return Err(Error::BadAddress(addr));
34        }
35        Ok(Self(addr | crate::REPLY_FLAG))
36    }
37
38    /// Underlying byte (with reply flag, if any).
39    pub const fn as_byte(self) -> u8 {
40        self.0
41    }
42
43    /// PD address with the reply flag stripped.
44    pub const fn pd_addr(self) -> u8 {
45        self.0 & !crate::REPLY_FLAG
46    }
47
48    /// `true` if this is a reply address (PD → ACU).
49    pub const fn is_reply(self) -> bool {
50        (self.0 & crate::REPLY_FLAG) != 0
51    }
52
53    /// `true` if this is the broadcast address `0x7F`.
54    pub const fn is_broadcast(self) -> bool {
55        self.pd_addr() == crate::BROADCAST_ADDR
56    }
57}
58
59/// Sequence number (`0..=3`).
60///
61/// # Spec: §5.7, §Table 2
62///
63/// SQN 0 is reserved for boot/recovery. Normal cycle is `1 → 2 → 3 → 1 → …`.
64/// The ACU re-uses an outstanding SQN to request a *reply repeat* from the
65/// PD; the PD treats a new SQN as a new command.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub struct Sqn(u8);
68
69impl Sqn {
70    /// Boot / recovery SQN.
71    pub const ZERO: Self = Self(0);
72
73    /// Validating constructor.
74    pub const fn new(n: u8) -> Result<Self, Error> {
75        if n > 3 {
76            Err(Error::BadSqn(n))
77        } else {
78            Ok(Self(n))
79        }
80    }
81
82    /// Underlying value.
83    pub const fn value(self) -> u8 {
84        self.0
85    }
86
87    /// Next SQN in the cycle, skipping `0`.
88    pub const fn next(self) -> Self {
89        Self(match self.0 {
90            0 | 3 => 1,
91            n => n + 1,
92        })
93    }
94}
95
96bitflags::bitflags! {
97    /// Control byte flags.
98    ///
99    /// # Spec: §5.9 Table 2
100    ///
101    /// ```text
102    /// 7 6 5 4 3 2 1 0
103    ///         | | | |
104    ///         | | +-+--- SQN (0..3)
105    ///         | +------- 0=cksum trailer, 1=CRC trailer
106    ///         +--------- Security Control Block present
107    /// ```
108    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109    pub struct CtrlFlags: u8 {
110        /// CRC-16 trailer (vs. 8-bit checksum when clear).
111        const USE_CRC = 0x04;
112        /// Security Control Block follows the CTRL byte.
113        const HAS_SCB = 0x08;
114    }
115}
116
117/// Decoded control byte.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct ControlByte {
120    /// Sequence number.
121    pub sqn: Sqn,
122    /// Flag bits.
123    pub flags: CtrlFlags,
124}
125
126impl ControlByte {
127    /// Construct from validated parts.
128    pub const fn new(sqn: Sqn, flags: CtrlFlags) -> Self {
129        Self { sqn, flags }
130    }
131
132    /// Encode to the on-wire byte.
133    pub const fn encode(self) -> u8 {
134        self.sqn.value() | self.flags.bits()
135    }
136
137    /// Decode an on-wire byte. Reserved bits (top nibble) must be zero.
138    pub const fn decode(byte: u8) -> Result<Self, Error> {
139        if (byte & 0xF0) != 0 {
140            return Err(Error::BadControlByte(byte));
141        }
142        let sqn = match Sqn::new(byte & 0x03) {
143            Ok(s) => s,
144            Err(e) => return Err(e),
145        };
146        let flags = match CtrlFlags::from_bits(byte & 0x0C) {
147            Some(f) => f,
148            None => return Err(Error::BadControlByte(byte)),
149        };
150        Ok(Self { sqn, flags })
151    }
152
153    /// `true` if the trailer is a CRC-16.
154    pub const fn use_crc(self) -> bool {
155        self.flags.contains(CtrlFlags::USE_CRC)
156    }
157
158    /// `true` if a Security Control Block is present.
159    pub const fn has_scb(self) -> bool {
160        self.flags.contains(CtrlFlags::HAS_SCB)
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn ctrl_byte_roundtrip() {
170        for raw in 0u8..=0x0F {
171            let cb = ControlByte::decode(raw).unwrap();
172            assert_eq!(cb.encode(), raw);
173        }
174    }
175
176    #[test]
177    fn ctrl_byte_rejects_reserved() {
178        for raw in 0x10..=0xFFu8 {
179            assert!(ControlByte::decode(raw).is_err());
180        }
181    }
182
183    #[test]
184    fn sqn_cycles_skipping_zero() {
185        let mut s = Sqn::new(1).unwrap();
186        for _ in 0..10 {
187            s = s.next();
188            assert_ne!(s.value(), 0);
189        }
190    }
191
192    #[test]
193    fn address_reply_flag() {
194        let pd = Address::pd(0x05).unwrap();
195        assert!(!pd.is_reply());
196        assert_eq!(pd.as_byte(), 0x05);
197
198        let reply = Address::reply(0x05).unwrap();
199        assert!(reply.is_reply());
200        assert_eq!(reply.as_byte(), 0x85);
201        assert_eq!(reply.pd_addr(), 0x05);
202
203        let bcast = Address::pd(0x7F).unwrap();
204        assert!(bcast.is_broadcast());
205    }
206}