Skip to main content

knx_core/
knxnetip.rs

1use core::convert::TryFrom;
2
3use crate::{IndividualAddress, KnxError, Result};
4
5/// The length of the KNXnet/IP header itself, in octets. The header states
6/// this in its own first octet, so a frame that opens with anything else is
7/// not a header this crate reads.
8pub const HEADER_LENGTH: u8 = 0x06;
9
10/// The protocol version octet for KNXnet/IP 1.0, the only version this crate
11/// speaks: the major version in the high nibble, the minor in the low one.
12pub const PROTOCOL_VERSION_1_0: u8 = 0x10;
13
14/// The KNXnet/IP services this crate speaks: a chosen subset of the
15/// registry, not the complete set.
16///
17/// Each variant is the 2-octet service-type identifier the header carries,
18/// and a code outside this set is refused as
19/// [`KnxError::UnsupportedServiceType`] rather than passed along
20/// uninterpreted. [`ServiceType::ALL`] fixes the count in its own type, so
21/// widening what this crate speaks is an edit to a count the declaration
22/// states.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24#[repr(u16)]
25pub enum ServiceType {
26    /// A client's search for KNXnet/IP servers, addressed to the
27    /// discovery endpoint the caller chose - the system setup
28    /// multicast group, a broadcast, or a unicast target alike - and
29    /// naming the endpoint it wants answers sent to.
30    SearchRequest = 0x0201,
31    /// A server's answer to a search: its own control endpoint, and a
32    /// description of what it is.
33    SearchResponse = 0x0202,
34    /// A client's request, addressed to a single server's control endpoint,
35    /// for that server's self-description.
36    DescriptionRequest = 0x0203,
37    /// A server's self-description, answering a description request.
38    DescriptionResponse = 0x0204,
39    /// A client's request to open a connection, naming its control and data
40    /// endpoints and the kind of connection it wants.
41    ConnectRequest = 0x0205,
42    /// A server's answer to a connect request: the channel it opened and the
43    /// connection response data that goes with it, or the reason it refused.
44    ConnectResponse = 0x0206,
45    /// A client's heartbeat, asking whether the channel it names is still
46    /// open. A connection the client stops asking about is one the server
47    /// eventually closes on its own.
48    ConnectionStateRequest = 0x0207,
49    /// A server's answer to a heartbeat, stating the channel's state.
50    ConnectionStateResponse = 0x0208,
51    /// Either peer's statement that it is closing the channel it names.
52    DisconnectRequest = 0x0209,
53    /// The answer that completes a disconnect; the channel is closed either
54    /// way, so this confirms rather than permits.
55    DisconnectResponse = 0x020a,
56    /// A cEMI frame carried over a tunnelling connection, stamped with the
57    /// channel it belongs to and the sequence number its acknowledgement
58    /// answers.
59    TunnellingRequest = 0x0420,
60    /// The acknowledgement of 1 tunnelling request, naming the sequence
61    /// number it answers.
62    TunnellingAck = 0x0421,
63    /// A cEMI frame multicast to the routing group. Nothing acknowledges it
64    /// and it carries no channel or sequence number.
65    RoutingIndication = 0x0530,
66    /// A router's report that it dropped routing frames it had no room for,
67    /// and how many.
68    RoutingLostMessage = 0x0531,
69    /// A router's request that senders pause, stating how long it expects to
70    /// stay saturated.
71    RoutingBusy = 0x0532,
72}
73
74impl ServiceType {
75    /// All `ServiceType` variants in declaration order; the canonical single
76    /// source for iteration/decoding.
77    pub const ALL: [ServiceType; 15] = [
78        ServiceType::SearchRequest,
79        ServiceType::SearchResponse,
80        ServiceType::DescriptionRequest,
81        ServiceType::DescriptionResponse,
82        ServiceType::ConnectRequest,
83        ServiceType::ConnectResponse,
84        ServiceType::ConnectionStateRequest,
85        ServiceType::ConnectionStateResponse,
86        ServiceType::DisconnectRequest,
87        ServiceType::DisconnectResponse,
88        ServiceType::TunnellingRequest,
89        ServiceType::TunnellingAck,
90        ServiceType::RoutingIndication,
91        ServiceType::RoutingLostMessage,
92        ServiceType::RoutingBusy,
93    ];
94
95    /// The service-type identifier this variant stands for. The header
96    /// carries it as 2 octets, most significant first.
97    pub const fn as_u16(self) -> u16 {
98        self as u16
99    }
100}
101
102// `ServiceType::ALL` together with `as_u16` is now the single source of truth
103// for the variant <-> discriminant mapping; decoding scans `ALL` rather than
104// mirroring the discriminants in a separate table.
105impl TryFrom<u16> for ServiceType {
106    type Error = KnxError;
107
108    fn try_from(value: u16) -> Result<Self> {
109        ServiceType::ALL
110            .iter()
111            .copied()
112            .find(|st| st.as_u16() == value)
113            .ok_or(KnxError::UnsupportedServiceType(value))
114    }
115}
116
117/// The header every KNXnet/IP frame opens with: its own length, the protocol
118/// version, the service the frame is, and the length of the whole frame.
119///
120/// The 2 fixed octets are not held here - they are the same on every frame
121/// this crate reads or writes, and [`Self::decode`] refuses a frame that
122/// states either of them differently.
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
124pub struct KnxNetIpHeader {
125    service_type: ServiceType,
126    total_length: u16,
127}
128
129impl KnxNetIpHeader {
130    /// Builds a header, refusing a total length that does not account for the
131    /// header itself.
132    ///
133    /// The length a header states covers the whole frame, this header
134    /// included, so a value below [`HEADER_LENGTH`] describes a frame shorter
135    /// than its own head.
136    pub const fn new(service_type: ServiceType, total_length: u16) -> Result<Self> {
137        if total_length < HEADER_LENGTH as u16 {
138            return Err(KnxError::InvalidFrame("total length shorter than header"));
139        }
140
141        Ok(Self { service_type, total_length })
142    }
143
144    /// The service this frame is.
145    pub const fn service_type(self) -> ServiceType {
146        self.service_type
147    }
148
149    /// The whole frame's length in octets, this header's own 6 included.
150    pub const fn total_length(self) -> u16 {
151        self.total_length
152    }
153
154    /// Reads a header and hands back the body that follows it, cut to the
155    /// length the header states.
156    ///
157    /// The length octet and the version octet are checked rather than
158    /// stepped over: a frame disagreeing with either is not one this crate
159    /// can read, and reading on would be reading a different structure. A
160    /// buffer holding less than the stated total length is refused rather
161    /// than truncated, so the body handed back is the whole one or none.
162    pub fn decode(input: &[u8]) -> Result<(Self, &[u8])> {
163        if input.len() < HEADER_LENGTH as usize {
164            return Err(KnxError::BufferTooShort {
165                needed: HEADER_LENGTH as usize,
166                actual: input.len(),
167            });
168        }
169
170        if input[0] != HEADER_LENGTH {
171            return Err(KnxError::InvalidFrame("invalid KNXnet/IP header length"));
172        }
173        if input[1] != PROTOCOL_VERSION_1_0 {
174            return Err(KnxError::InvalidFrame("invalid KNXnet/IP protocol version"));
175        }
176
177        let service_type = ServiceType::try_from(u16::from_be_bytes([input[2], input[3]]))?;
178        let total_length = u16::from_be_bytes([input[4], input[5]]);
179        let header = Self::new(service_type, total_length)?;
180
181        let total_length = usize::from(total_length);
182        if input.len() < total_length {
183            return Err(KnxError::BufferTooShort { needed: total_length, actual: input.len() });
184        }
185
186        Ok((header, &input[HEADER_LENGTH as usize..total_length]))
187    }
188
189    /// Appends the 6 header octets: the fixed length and version, then the
190    /// service type and the total length, each most significant octet first.
191    ///
192    /// The total length is the one this header was built with, not a count of
193    /// what follows - the caller states the frame's width before the body is
194    /// appended, or measures it and builds the header afterwards.
195    #[cfg(feature = "std")]
196    pub fn encode(self, out: &mut std::vec::Vec<u8>) -> Result<()> {
197        out.extend_from_slice(&[
198            HEADER_LENGTH,
199            PROTOCOL_VERSION_1_0,
200            (self.service_type.as_u16() >> 8) as u8,
201            self.service_type.as_u16() as u8,
202            (self.total_length >> 8) as u8,
203            self.total_length as u8,
204        ]);
205        Ok(())
206    }
207}
208
209/// The transport an endpoint speaks, as the host protocol octet of an HPAI
210/// names it.
211#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
212#[repr(u8)]
213pub enum HostProtocol {
214    /// IPv4 over UDP: the connectionless transport discovery and routing use,
215    /// and the one where a tunnelling connection is acknowledged frame by
216    /// frame because nothing underneath it orders what arrives.
217    Ipv4Udp = 0x01,
218    /// IPv4 over TCP: the connection-oriented transport, where the stream
219    /// itself orders and retransmits what a UDP peer has to handle above the
220    /// transport.
221    Ipv4Tcp = 0x02,
222}
223
224// All `HostProtocol` variants in declaration order; together with `as_u8`
225// this is the single source of truth for the variant <-> byte mapping.
226const HOST_PROTOCOL_ALL: [HostProtocol; 2] = [HostProtocol::Ipv4Udp, HostProtocol::Ipv4Tcp];
227
228impl HostProtocol {
229    /// The host protocol code this variant stands for: the octet an HPAI
230    /// carries after its own length.
231    pub const fn as_u8(self) -> u8 {
232        self as u8
233    }
234}
235
236impl TryFrom<u8> for HostProtocol {
237    type Error = KnxError;
238
239    fn try_from(value: u8) -> Result<Self> {
240        HOST_PROTOCOL_ALL
241            .iter()
242            .copied()
243            .find(|hp| hp.as_u8() == value)
244            .ok_or(KnxError::InvalidFrame("unsupported host protocol"))
245    }
246}
247
248/// A Host Protocol Address Information block: the endpoint a peer is asked to
249/// send to, as the sender states it.
250///
251/// The endpoint is stated rather than inferred from where the datagram came
252/// from, which is what lets a client ask for answers on a different socket
253/// than the one it asked from.
254#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
255pub struct Hpai {
256    protocol: HostProtocol,
257    address: [u8; 4],
258    port: u16,
259}
260
261impl Hpai {
262    /// The block is 8 octets: its own length, the host protocol, the 4-octet
263    /// IPv4 address, and the 2-octet port.
264    pub const LENGTH: u8 = 0x08;
265
266    /// Builds the block around an endpoint. Every combination of the 3 fields
267    /// is expressible on the wire, so there is nothing here to refuse.
268    pub const fn new(protocol: HostProtocol, address: [u8; 4], port: u16) -> Self {
269        Self { protocol, address, port }
270    }
271
272    /// The transport the endpoint speaks.
273    pub const fn protocol(self) -> HostProtocol {
274        self.protocol
275    }
276
277    /// The 4 IPv4 address octets in the order they travel, most significant
278    /// first.
279    pub const fn address(self) -> [u8; 4] {
280        self.address
281    }
282
283    /// The port the endpoint expects to be reached on.
284    pub const fn port(self) -> u16 {
285        self.port
286    }
287
288    /// Reads the block and hands back what follows it.
289    ///
290    /// The length octet is checked against the 8 this block always is, and a
291    /// host protocol code this crate does not speak is refused rather than
292    /// carried: an endpoint whose transport is unknown is one nothing can be
293    /// sent to.
294    pub fn decode(input: &[u8]) -> Result<(Self, &[u8])> {
295        if input.len() < Self::LENGTH as usize {
296            return Err(KnxError::BufferTooShort {
297                needed: Self::LENGTH as usize,
298                actual: input.len(),
299            });
300        }
301        if input[0] != Self::LENGTH {
302            return Err(KnxError::InvalidFrame("invalid HPAI length"));
303        }
304
305        let protocol = HostProtocol::try_from(input[1])?;
306        let address = [input[2], input[3], input[4], input[5]];
307        let port = u16::from_be_bytes([input[6], input[7]]);
308
309        Ok((Self::new(protocol, address, port), &input[Self::LENGTH as usize..]))
310    }
311
312    /// Appends the 8 block octets: the fixed length, the protocol code, the
313    /// address, and the port most significant octet first.
314    #[cfg(feature = "std")]
315    pub fn encode(self, out: &mut std::vec::Vec<u8>) -> Result<()> {
316        out.extend_from_slice(&[
317            Self::LENGTH,
318            self.protocol.as_u8(),
319            self.address[0],
320            self.address[1],
321            self.address[2],
322            self.address[3],
323            (self.port >> 8) as u8,
324            self.port as u8,
325        ]);
326        Ok(())
327    }
328}
329
330/// The Connection Response Data a tunnelling CONNECT_RESPONSE carries.
331///
332/// A server hands every tunnelling connection it accepts an individual
333/// address of its own, and this block is where it states it. Skipping
334/// the block leaves a client with no address to stamp on the frames it
335/// sends and no way to recognise its own traffic among what the bus
336/// reports, so the address is parsed rather than discarded.
337#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
338pub struct TunnellingCrd {
339    address: IndividualAddress,
340}
341
342impl TunnellingCrd {
343    /// The block is 4 octets: its own length, the connection type, and
344    /// the 2-octet address.
345    pub const LENGTH: u8 = 0x04;
346
347    /// The connection-type code a tunnelling connection states.
348    pub const CONNECTION_TYPE: u8 = 0x04;
349
350    /// Builds the block around the address a server assigned. Every
351    /// individual address is expressible here, so there is nothing to refuse.
352    pub const fn new(address: IndividualAddress) -> Self {
353        Self { address }
354    }
355
356    /// The individual address the server assigned to this connection.
357    pub const fn address(self) -> IndividualAddress {
358        self.address
359    }
360
361    /// Reads the block, refusing one that is not a tunnelling CRD.
362    ///
363    /// The connection type is checked rather than assumed: the same
364    /// position carries a different block for every other connection
365    /// type, and reading one of those as an address would report a
366    /// number that is not an address at all.
367    pub fn decode(input: &[u8]) -> Result<(Self, &[u8])> {
368        if input.len() < Self::LENGTH as usize {
369            return Err(KnxError::BufferTooShort {
370                needed: Self::LENGTH as usize,
371                actual: input.len(),
372            });
373        }
374        if input[0] != Self::LENGTH {
375            return Err(KnxError::InvalidFrame("invalid tunnelling CRD length"));
376        }
377        if input[1] != Self::CONNECTION_TYPE {
378            return Err(KnxError::InvalidFrame("connection response data is not a tunnelling CRD"));
379        }
380
381        Ok((
382            Self::new(IndividualAddress::from_raw(u16::from_be_bytes([input[2], input[3]]))),
383            &input[Self::LENGTH as usize..],
384        ))
385    }
386
387    /// Appends the 4 block octets: the fixed length, the tunnelling
388    /// connection type, and the assigned address most significant octet
389    /// first.
390    #[cfg(feature = "std")]
391    pub fn encode(self, out: &mut std::vec::Vec<u8>) -> Result<()> {
392        out.extend_from_slice(&[
393            Self::LENGTH,
394            Self::CONNECTION_TYPE,
395            (self.address.raw() >> 8) as u8,
396            self.address.raw() as u8,
397        ]);
398        Ok(())
399    }
400}
401
402/// The header a frame on an established connection carries ahead of its
403/// payload: which channel it belongs to, and where it sits in that channel's
404/// sequence.
405///
406/// A tunnelling connection is ordered by this header rather than by the
407/// transport underneath it, which is why the sequence number travels with
408/// every frame and comes back on the acknowledgement.
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
410pub struct ConnectionHeader {
411    channel_id: u8,
412    sequence_counter: u8,
413    status: u8,
414}
415
416impl ConnectionHeader {
417    /// The header is 4 octets: its own length, the channel id, the sequence
418    /// counter, and the status.
419    pub const LENGTH: u8 = 0x04;
420
421    /// Builds the header. Each field is a single octet this crate places as
422    /// given, so there is nothing here to refuse.
423    pub const fn new(channel_id: u8, sequence_counter: u8, status: u8) -> Self {
424        Self { channel_id, sequence_counter, status }
425    }
426
427    /// The channel this frame belongs to, as the server numbered it when it
428    /// accepted the connection.
429    pub const fn channel_id(self) -> u8 {
430        self.channel_id
431    }
432
433    /// Where this frame sits in the channel's sequence. The acknowledgement
434    /// that answers it names the same number, which is what ties the 2
435    /// together.
436    pub const fn sequence_counter(self) -> u8 {
437        self.sequence_counter
438    }
439
440    /// The status octet as it arrived, reported rather than interpreted. A
441    /// request states 0 here; an acknowledgement states the outcome, and
442    /// those codes are not modeled by this crate.
443    pub const fn status(self) -> u8 {
444        self.status
445    }
446
447    /// Reads the header and hands back the payload that follows it.
448    ///
449    /// The length octet is checked against the 4 this header always is; the
450    /// channel, sequence, and status octets carry no value this layer
451    /// refuses, so whether the frame is one the reader wanted is the caller's
452    /// question.
453    pub fn decode(input: &[u8]) -> Result<(Self, &[u8])> {
454        if input.len() < Self::LENGTH as usize {
455            return Err(KnxError::BufferTooShort {
456                needed: Self::LENGTH as usize,
457                actual: input.len(),
458            });
459        }
460        if input[0] != Self::LENGTH {
461            return Err(KnxError::InvalidFrame("invalid connection header length"));
462        }
463
464        Ok((Self::new(input[1], input[2], input[3]), &input[Self::LENGTH as usize..]))
465    }
466
467    /// Appends the 4 header octets: the fixed length, the channel id, the
468    /// sequence counter, and the status.
469    #[cfg(feature = "std")]
470    pub fn encode(self, out: &mut std::vec::Vec<u8>) -> Result<()> {
471        out.extend_from_slice(&[Self::LENGTH, self.channel_id, self.sequence_counter, self.status]);
472        Ok(())
473    }
474}