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}