Skip to main content

knx_core/
cemi.rs

1use core::convert::TryFrom;
2
3use crate::{Apci, GroupAddress, IndividualAddress, KnxError, Result};
4
5#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7#[repr(u8)]
8pub enum CemiMessageCode {
9    LDataRequest = 0x11,
10    LDataIndication = 0x29,
11    LDataConfirmation = 0x2e,
12}
13
14// All `CemiMessageCode` variants in declaration order; together with `as_u8`
15// this is the single source of truth for the variant <-> byte mapping.
16const CEMI_MESSAGE_CODE_ALL: [CemiMessageCode; 3] = [
17    CemiMessageCode::LDataRequest,
18    CemiMessageCode::LDataIndication,
19    CemiMessageCode::LDataConfirmation,
20];
21
22impl CemiMessageCode {
23    pub const fn as_u8(self) -> u8 {
24        self as u8
25    }
26}
27
28impl TryFrom<u8> for CemiMessageCode {
29    type Error = KnxError;
30
31    fn try_from(value: u8) -> Result<Self> {
32        CEMI_MESSAGE_CODE_ALL
33            .iter()
34            .copied()
35            .find(|mc| mc.as_u8() == value)
36            .ok_or(KnxError::InvalidFrame("unsupported cEMI message code"))
37    }
38}
39
40#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct GroupTelegram {
43    source: IndividualAddress,
44    destination: GroupAddress,
45    apci: Apci,
46    payload: std::vec::Vec<u8>,
47}
48
49impl GroupTelegram {
50    /// Builds a validated group telegram.
51    ///
52    /// Payload rules:
53    /// - `GroupValueRead` carries no payload (a non-empty payload is rejected).
54    /// - `GroupValueWrite` / `GroupValueResponse` must carry a payload. An
55    ///   empty Write/Response payload is rejected because it is ambiguous with
56    ///   the compact one-byte zero value on APDU decode (a 2-byte compact
57    ///   APDU always decodes back to `[0x00]`, never to an empty payload), so
58    ///   an empty Write/Response could not round-trip distinctly. `[0x00]`
59    ///   remains a valid compact one-byte value payload.
60    /// - Payloads longer than 254 bytes are rejected.
61    pub fn new(
62        source: IndividualAddress,
63        destination: GroupAddress,
64        apci: Apci,
65        payload: &[u8],
66    ) -> Result<Self> {
67        if matches!(apci, Apci::GroupValueRead) && !payload.is_empty() {
68            return Err(KnxError::InvalidFrame("group read cannot carry payload"));
69        }
70        if !matches!(apci, Apci::GroupValueRead) && payload.is_empty() {
71            return Err(KnxError::InvalidFrame(
72                "group write/response must carry payload",
73            ));
74        }
75        if payload.len() > 254 {
76            return Err(KnxError::InvalidFrame("group payload too long"));
77        }
78
79        Ok(Self {
80            source,
81            destination,
82            apci,
83            payload: payload.to_vec(),
84        })
85    }
86
87    pub const fn source(&self) -> IndividualAddress {
88        self.source
89    }
90
91    pub const fn destination(&self) -> GroupAddress {
92        self.destination
93    }
94
95    pub const fn apci(&self) -> Apci {
96        self.apci
97    }
98
99    pub fn payload(&self) -> &[u8] {
100        &self.payload
101    }
102
103    fn encode_apdu(&self, out: &mut std::vec::Vec<u8>) -> Result<()> {
104        out.push(0x00);
105
106        match self.payload.as_slice() {
107            [] => out.push(self.apci.service_bits()),
108            [short] if *short <= 0x3f => out.push(self.apci.service_bits() | short),
109            payload => {
110                out.push(self.apci.service_bits());
111                out.extend_from_slice(payload);
112            }
113        }
114
115        Ok(())
116    }
117
118    fn decode_apdu(
119        source: IndividualAddress,
120        destination: GroupAddress,
121        apdu: &[u8],
122    ) -> Result<Self> {
123        if apdu.len() < 2 {
124            return Err(KnxError::BufferTooShort {
125                needed: 2,
126                actual: apdu.len(),
127            });
128        }
129        if apdu[0] != 0x00 {
130            return Err(KnxError::InvalidFrame("unsupported TPCI field"));
131        }
132
133        let apci = Apci::try_from(apdu[1])?;
134        let payload = if apdu.len() == 2 {
135            match apci {
136                Apci::GroupValueRead => std::vec::Vec::new(),
137                Apci::GroupValueResponse | Apci::GroupValueWrite => {
138                    std::vec::Vec::from([apdu[1] & 0x3f])
139                }
140            }
141        } else {
142            if apdu[1] & 0x3f != 0 {
143                return Err(KnxError::InvalidFrame("extended APDU has inline data bits"));
144            }
145            apdu[2..].to_vec()
146        };
147
148        Self::new(source, destination, apci, &payload)
149    }
150}
151
152#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct CemiFrame {
155    message_code: CemiMessageCode,
156    control1: u8,
157    control2: u8,
158    telegram: GroupTelegram,
159    /// Opaque cEMI additional-info block, preserved verbatim across
160    /// decode/encode. Empty for frames built via the public constructors.
161    /// Individual additional-info elements are intentionally not parsed yet.
162    additional_info: std::vec::Vec<u8>,
163}
164
165impl CemiFrame {
166    pub const DEFAULT_CONTROL1: u8 = 0xbc;
167    pub const DEFAULT_CONTROL2_GROUP: u8 = 0xe0;
168
169    pub const fn new(message_code: CemiMessageCode, telegram: GroupTelegram) -> Self {
170        Self {
171            message_code,
172            control1: Self::DEFAULT_CONTROL1,
173            control2: Self::DEFAULT_CONTROL2_GROUP,
174            telegram,
175            additional_info: std::vec::Vec::new(),
176        }
177    }
178
179    pub fn group_value_read(source: IndividualAddress, destination: GroupAddress) -> Result<Self> {
180        Ok(Self::new(
181            CemiMessageCode::LDataRequest,
182            GroupTelegram::new(source, destination, Apci::GroupValueRead, &[])?,
183        ))
184    }
185
186    pub fn group_value_response(
187        source: IndividualAddress,
188        destination: GroupAddress,
189        payload: &[u8],
190    ) -> Result<Self> {
191        Ok(Self::new(
192            CemiMessageCode::LDataRequest,
193            GroupTelegram::new(source, destination, Apci::GroupValueResponse, payload)?,
194        ))
195    }
196
197    pub fn group_value_write(
198        source: IndividualAddress,
199        destination: GroupAddress,
200        payload: &[u8],
201    ) -> Result<Self> {
202        Ok(Self::new(
203            CemiMessageCode::LDataRequest,
204            GroupTelegram::new(source, destination, Apci::GroupValueWrite, payload)?,
205        ))
206    }
207
208    pub const fn message_code(&self) -> CemiMessageCode {
209        self.message_code
210    }
211
212    pub const fn control1(&self) -> u8 {
213        self.control1
214    }
215
216    pub const fn control2(&self) -> u8 {
217        self.control2
218    }
219
220    pub const fn telegram(&self) -> &GroupTelegram {
221        &self.telegram
222    }
223
224    /// Opaque cEMI additional-info bytes (empty for constructor-built frames).
225    pub fn additional_info(&self) -> &[u8] {
226        &self.additional_info
227    }
228
229    /// Returns the frame with the given opaque additional-info block.
230    ///
231    /// The cEMI additional-info length is a single octet, so the block must
232    /// be at most 255 bytes; longer input is rejected with
233    /// `KnxError::InvalidFrame("additional info too long")`.
234    pub fn with_additional_info(
235        mut self,
236        additional_info: impl Into<std::vec::Vec<u8>>,
237    ) -> Result<Self> {
238        let additional_info = additional_info.into();
239        if u8::try_from(additional_info.len()).is_err() {
240            return Err(KnxError::InvalidFrame("additional info too long"));
241        }
242        self.additional_info = additional_info;
243        Ok(self)
244    }
245
246    pub fn decode(input: &[u8]) -> Result<(Self, &[u8])> {
247        if input.len() < 2 {
248            return Err(KnxError::BufferTooShort {
249                needed: 2,
250                actual: input.len(),
251            });
252        }
253
254        let message_code = CemiMessageCode::try_from(input[0])?;
255        let additional_info_len = input[1] as usize;
256        let fixed_start = 2 + additional_info_len;
257        let fixed_len = fixed_start + 7;
258
259        if input.len() < fixed_len {
260            return Err(KnxError::BufferTooShort {
261                needed: fixed_len,
262                actual: input.len(),
263            });
264        }
265
266        // input.len() >= fixed_len >= fixed_start, so this slice is in-bounds.
267        let additional_info = input[2..fixed_start].to_vec();
268        let control1 = input[fixed_start];
269        let control2 = input[fixed_start + 1];
270        let source = IndividualAddress::from_raw(u16::from_be_bytes([
271            input[fixed_start + 2],
272            input[fixed_start + 3],
273        ]));
274        let destination = GroupAddress::from_raw(u16::from_be_bytes([
275            input[fixed_start + 4],
276            input[fixed_start + 5],
277        ]));
278        let apdu_len = input[fixed_start + 6] as usize + 1;
279        let apdu_start = fixed_start + 7;
280        let needed = apdu_start + apdu_len;
281
282        if input.len() < needed {
283            return Err(KnxError::BufferTooShort {
284                needed,
285                actual: input.len(),
286            });
287        }
288
289        let telegram = GroupTelegram::decode_apdu(source, destination, &input[apdu_start..needed])?;
290
291        Ok((
292            Self {
293                message_code,
294                control1,
295                control2,
296                telegram,
297                additional_info,
298            },
299            &input[needed..],
300        ))
301    }
302
303    pub fn encode(&self, out: &mut std::vec::Vec<u8>) -> Result<()> {
304        let mut apdu = std::vec::Vec::new();
305        self.telegram.encode_apdu(&mut apdu)?;
306        let npdu_len = apdu
307            .len()
308            .checked_sub(1)
309            .and_then(|value| u8::try_from(value).ok())
310            .ok_or(KnxError::InvalidFrame("APDU length out of range"))?;
311        let additional_info_len = u8::try_from(self.additional_info.len())
312            .map_err(|_| KnxError::InvalidFrame("additional info too long"))?;
313
314        out.push(self.message_code.as_u8());
315        out.push(additional_info_len);
316        out.extend_from_slice(&self.additional_info);
317        out.extend_from_slice(&[
318            self.control1,
319            self.control2,
320            (self.telegram.source().raw() >> 8) as u8,
321            self.telegram.source().raw() as u8,
322            (self.telegram.destination().raw() >> 8) as u8,
323            self.telegram.destination().raw() as u8,
324            npdu_len,
325        ]);
326        out.extend_from_slice(&apdu);
327
328        Ok(())
329    }
330}