Skip to main content

netget/server/knx/
wire.rs

1//! KNXnet/IP (tunnelling) and cEMI framing, KNX addresses, and the datapoint-type (DPT)
2//! codec, shared by the server and the client. A group value is never handed to the handler
3//! as bytes: it is decoded by its DPT when one is known, and offered under every plausible
4//! reading when not.
5use anyhow::{bail, ensure, Context, Result};
6use serde_json::{json, Map, Value};
7use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
8
9/// Largest KNXnet/IP frame accepted (the total-length field is 16 bits; real frames are
10/// well under 300 bytes).
11pub const MAX_FRAME: usize = 512;
12/// Longest group value carried (DPT 16 is 14 bytes; extended frames allow 254).
13pub const MAX_APDU_DATA: usize = 254;
14
15pub const SEARCH_REQUEST: u16 = 0x0201;
16pub const SEARCH_RESPONSE: u16 = 0x0202;
17pub const DESCRIPTION_REQUEST: u16 = 0x0203;
18pub const DESCRIPTION_RESPONSE: u16 = 0x0204;
19pub const CONNECT_REQUEST: u16 = 0x0205;
20pub const CONNECT_RESPONSE: u16 = 0x0206;
21pub const CONNECTIONSTATE_REQUEST: u16 = 0x0207;
22pub const CONNECTIONSTATE_RESPONSE: u16 = 0x0208;
23pub const DISCONNECT_REQUEST: u16 = 0x0209;
24pub const DISCONNECT_RESPONSE: u16 = 0x020A;
25pub const TUNNELLING_REQUEST: u16 = 0x0420;
26pub const TUNNELLING_ACK: u16 = 0x0421;
27
28pub const E_NO_ERROR: u8 = 0x00;
29pub const E_CONNECTION_TYPE: u8 = 0x22;
30pub const E_CONNECTION_OPTION: u8 = 0x23;
31pub const E_NO_MORE_CONNECTIONS: u8 = 0x24;
32pub const E_CONNECTION_ID: u8 = 0x21;
33
34pub const L_DATA_REQ: u8 = 0x11;
35pub const L_DATA_CON: u8 = 0x2E;
36pub const L_DATA_IND: u8 = 0x29;
37
38/// A KNXnet/IP frame: its service type and body.
39pub fn parse_frame(buf: &[u8]) -> Result<(u16, &[u8])> {
40    ensure!(
41        buf.len() >= 6 && buf.len() <= MAX_FRAME,
42        "frame size out of range"
43    );
44    ensure!(buf[0] == 0x06 && buf[1] == 0x10, "not KNXnet/IP 1.0");
45    let service = u16::from_be_bytes([buf[2], buf[3]]);
46    let total = u16::from_be_bytes([buf[4], buf[5]]) as usize;
47    ensure!(
48        total == buf.len(),
49        "total length {total} does not match {}",
50        buf.len()
51    );
52    Ok((service, &buf[6..]))
53}
54
55pub fn frame(service: u16, body: &[u8]) -> Vec<u8> {
56    let mut out = vec![0x06, 0x10];
57    out.extend(service.to_be_bytes());
58    out.extend(((body.len() + 6) as u16).to_be_bytes());
59    out.extend(body);
60    out
61}
62
63/// A host protocol address information block (UDP).
64pub fn parse_hpai(b: &[u8]) -> Result<(SocketAddrV4, usize)> {
65    ensure!(b.len() >= 8 && b[0] == 8, "malformed HPAI");
66    ensure!(b[1] == 0x01, "only UDP HPAI is supported");
67    let ip = Ipv4Addr::new(b[2], b[3], b[4], b[5]);
68    let port = u16::from_be_bytes([b[6], b[7]]);
69    Ok((SocketAddrV4::new(ip, port), 8))
70}
71
72pub fn hpai(addr: SocketAddr) -> Vec<u8> {
73    let (ip, port) = match addr {
74        SocketAddr::V4(a) => (*a.ip(), a.port()),
75        SocketAddr::V6(_) => (Ipv4Addr::UNSPECIFIED, 0),
76    };
77    let mut out = vec![8, 0x01];
78    out.extend(ip.octets());
79    out.extend(port.to_be_bytes());
80    out
81}
82
83/// Where to answer: the HPAI, or — when it is 0.0.0.0:0 (NAT mode) — the sender.
84pub fn endpoint(hpai: SocketAddrV4, sender: SocketAddr) -> SocketAddr {
85    if hpai.ip().is_unspecified() || hpai.port() == 0 {
86        sender
87    } else {
88        SocketAddr::V4(hpai)
89    }
90}
91
92pub fn format_group(a: u16) -> String {
93    format!("{}/{}/{}", a >> 11, (a >> 8) & 0x07, a & 0xff)
94}
95
96pub fn parse_group(s: &str) -> Result<u16> {
97    let parts: Vec<&str> = s.split('/').collect();
98    let n = |p: &str| {
99        p.trim()
100            .parse::<u16>()
101            .context("group address parts must be numbers")
102    };
103    Ok(match parts.as_slice() {
104        [main, middle, sub] => {
105            let (m, i, s) = (n(main)?, n(middle)?, n(sub)?);
106            ensure!(
107                m < 32 && i < 8 && s < 256,
108                "group address out of range (31/7/255)"
109            );
110            (m << 11) | (i << 8) | s
111        }
112        [main, sub] => {
113            let (m, s) = (n(main)?, n(sub)?);
114            ensure!(m < 32 && s < 2048, "group address out of range (31/2047)");
115            (m << 11) | s
116        }
117        _ => bail!("a group address is main/middle/sub, e.g. 1/2/3"),
118    })
119}
120
121pub fn format_individual(a: u16) -> String {
122    format!("{}.{}.{}", a >> 12, (a >> 8) & 0x0f, a & 0xff)
123}
124
125pub fn parse_individual(s: &str) -> Result<u16> {
126    let parts: Vec<u16> = s
127        .split('.')
128        .map(|p| p.trim().parse::<u16>())
129        .collect::<std::result::Result<_, _>>()
130        .context("an individual address is area.line.device, e.g. 1.1.250")?;
131    match parts.as_slice() {
132        [a, l, d] if *a < 16 && *l < 16 && *d < 256 => Ok((a << 12) | (l << 8) | d),
133        _ => bail!("an individual address is area.line.device (15.15.255)"),
134    }
135}
136
137/// What a group telegram does.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum Apci {
140    Read,
141    Response,
142    Write,
143}
144
145impl Apci {
146    pub fn name(self) -> &'static str {
147        match self {
148            Apci::Read => "read",
149            Apci::Response => "response",
150            Apci::Write => "write",
151        }
152    }
153}
154
155/// A group value on the wire: either the 6 bits inside the APCI, or bytes after it.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum Data {
158    Small(u8),
159    Bytes(Vec<u8>),
160}
161
162/// A cEMI L_Data frame carrying a group telegram.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub struct Telegram {
165    pub message_code: u8,
166    pub source: u16,
167    pub destination: u16,
168    pub apci: Apci,
169    pub data: Data,
170}
171
172/// Parse a cEMI L_Data frame. `Ok(None)` is a frame that is valid but not a group value
173/// telegram (individual addressing, management services), which is acknowledged and ignored.
174pub fn parse_cemi(b: &[u8]) -> Result<Option<Telegram>> {
175    ensure!(b.len() >= 2, "short cEMI");
176    let code = b[0];
177    let add = b[1] as usize;
178    let b = b
179        .get(2 + add..)
180        .context("cEMI additional info overruns the frame")?;
181    ensure!(b.len() >= 7, "short L_Data");
182    let ctrl2 = b[1];
183    let source = u16::from_be_bytes([b[2], b[3]]);
184    let destination = u16::from_be_bytes([b[4], b[5]]);
185    let len = b[6] as usize;
186    let tpdu = b.get(7..).context("short TPDU")?;
187    ensure!(
188        tpdu.len() == len + 1,
189        "NPDU length {len} does not match the frame"
190    );
191    if ctrl2 & 0x80 == 0 || tpdu.len() < 2 {
192        return Ok(None);
193    }
194    let apci = (u16::from(tpdu[0] & 0x03) << 8) | u16::from(tpdu[1]);
195    let kind = match apci & 0x3c0 {
196        0x000 => Apci::Read,
197        0x040 => Apci::Response,
198        0x080 => Apci::Write,
199        _ => return Ok(None),
200    };
201    ensure!(tpdu.len() - 2 <= MAX_APDU_DATA, "group value too long");
202    let data = if tpdu.len() == 2 {
203        Data::Small((apci & 0x3f) as u8)
204    } else {
205        Data::Bytes(tpdu[2..].to_vec())
206    };
207    Ok(Some(Telegram {
208        message_code: code,
209        source,
210        destination,
211        apci: kind,
212        data,
213    }))
214}
215
216pub fn cemi(t: &Telegram) -> Vec<u8> {
217    let apci: u16 = match t.apci {
218        Apci::Read => 0x000,
219        Apci::Response => 0x040,
220        Apci::Write => 0x080,
221    };
222    let mut tpdu = vec![(apci >> 8) as u8 & 0x03, (apci & 0xff) as u8];
223    match &t.data {
224        Data::Small(v) => tpdu[1] |= v & 0x3f,
225        Data::Bytes(b) => tpdu.extend(b),
226    }
227    let mut out = vec![t.message_code, 0x00];
228    // Standard frame, no repeat, broadcast, low priority, ack request off; group address,
229    // hop count 6.
230    out.push(0xBC);
231    out.push(0xE0);
232    out.extend(t.source.to_be_bytes());
233    out.extend(t.destination.to_be_bytes());
234    out.push((tpdu.len() - 1) as u8);
235    out.extend(tpdu);
236    out
237}
238
239/// A tunnelling request body: connection header and cEMI.
240pub fn tunnelling(channel: u8, seq: u8, cemi: &[u8]) -> Vec<u8> {
241    let mut body = vec![4, channel, seq, 0];
242    body.extend(cemi);
243    frame(TUNNELLING_REQUEST, &body)
244}
245
246pub fn tunnelling_ack(channel: u8, seq: u8, status: u8) -> Vec<u8> {
247    frame(TUNNELLING_ACK, &[4, channel, seq, status])
248}
249
250/// The main number of a DPT name: "9.001" → 9, "dpt1" → 1.
251pub fn dpt_main(dpt: &str) -> Result<u16> {
252    let d = dpt
253        .trim()
254        .trim_start_matches(|c: char| c.is_ascii_alphabetic());
255    d.split('.')
256        .next()
257        .and_then(|m| m.parse().ok())
258        .with_context(|| format!("unknown DPT {dpt}"))
259}
260
261/// The DPTs the codec knows.
262pub const DPT_NOTE: &str = "1 (bool), 5 (0-255; 5.001 is 0-100 %), 6 (i8), 7 (u16), 8 (i16), 9 (2-byte float, e.g. °C), 12 (u32), 13 (i32), 14 (4-byte float), 16 (text, 14 chars), 17 (scene 0-63), 20 (u8 enum)";
263
264fn dpt9_encode(v: f64) -> Result<[u8; 2]> {
265    ensure!(
266        v.is_finite() && (-671088.64..=670760.96).contains(&v),
267        "DPT 9 range is -671088.64..670760.96"
268    );
269    let mut m = (v * 100.0).round() as i64;
270    let mut e = 0u8;
271    while !(-2048..=2047).contains(&m) {
272        m = (m as f64 / 2.0).round() as i64;
273        e += 1;
274    }
275    ensure!(e <= 15, "DPT 9 exponent overflow");
276    let raw = (m & 0x0fff) as u16;
277    let sign = if m < 0 { 0x80 } else { 0 };
278    Ok([
279        sign | (e << 3) | ((raw >> 8) as u8 & 0x07),
280        (raw & 0xff) as u8,
281    ])
282}
283
284fn dpt9_decode(b: [u8; 2]) -> f64 {
285    let e = (b[0] >> 3) & 0x0f;
286    let mut m = (i32::from(b[0] & 0x07) << 8) | i32::from(b[1]);
287    if b[0] & 0x80 != 0 {
288        m -= 2048;
289    }
290    let v = 0.01 * f64::from(m) * f64::from(1u32 << e);
291    (v * 100.0).round() / 100.0
292}
293
294/// Encode a value as DPT `dpt`.
295pub fn encode(dpt: &str, v: &Value) -> Result<Data> {
296    let main = dpt_main(dpt)?;
297    let int = |lo: i64, hi: i64| -> Result<i64> {
298        let n = v
299            .as_i64()
300            .with_context(|| format!("DPT {dpt} takes an integer"))?;
301        ensure!((lo..=hi).contains(&n), "DPT {dpt} range is {lo}..={hi}");
302        Ok(n)
303    };
304    Ok(match main {
305        1 => Data::Small(u8::from(match v {
306            Value::Bool(b) => *b,
307            Value::Number(n) => n.as_u64() == Some(1),
308            _ => bail!("DPT 1 takes true or false"),
309        })),
310        5 if dpt.trim().ends_with(".001") => {
311            let pct = v.as_f64().context("DPT 5.001 takes a percentage")?;
312            ensure!((0.0..=100.0).contains(&pct), "DPT 5.001 range is 0..=100");
313            Data::Bytes(vec![(pct * 255.0 / 100.0).round() as u8])
314        }
315        5 | 20 => Data::Bytes(vec![int(0, 255)? as u8]),
316        17 => Data::Bytes(vec![int(0, 63)? as u8]),
317        6 => Data::Bytes(vec![int(-128, 127)? as i8 as u8]),
318        7 => Data::Bytes((int(0, 65535)? as u16).to_be_bytes().to_vec()),
319        8 => Data::Bytes((int(-32768, 32767)? as i16).to_be_bytes().to_vec()),
320        9 => Data::Bytes(dpt9_encode(v.as_f64().context("DPT 9 takes a number")?)?.to_vec()),
321        12 => Data::Bytes((int(0, u32::MAX as i64)? as u32).to_be_bytes().to_vec()),
322        13 => Data::Bytes(
323            (int(i32::MIN as i64, i32::MAX as i64)? as i32)
324                .to_be_bytes()
325                .to_vec(),
326        ),
327        14 => Data::Bytes(
328            (v.as_f64().context("DPT 14 takes a number")? as f32)
329                .to_be_bytes()
330                .to_vec(),
331        ),
332        16 => {
333            let s = v.as_str().context("DPT 16 takes text")?;
334            ensure!(
335                s.len() <= 14 && s.is_ascii(),
336                "DPT 16 is at most 14 ASCII characters"
337            );
338            let mut b = s.as_bytes().to_vec();
339            b.resize(14, 0);
340            Data::Bytes(b)
341        }
342        _ => bail!("DPT {dpt} is not supported; supported: {DPT_NOTE}"),
343    })
344}
345
346/// Decode as DPT `dpt`, when the data has the right shape for it.
347pub fn decode(dpt: &str, d: &Data) -> Result<Value> {
348    let main = dpt_main(dpt)?;
349    let bytes = |n: usize| -> Result<&[u8]> {
350        match d {
351            Data::Bytes(b) if b.len() == n => Ok(b),
352            _ => bail!("DPT {dpt} carries {n} byte(s)"),
353        }
354    };
355    Ok(match main {
356        1 => match d {
357            Data::Small(v) => json!(*v & 1 == 1),
358            _ => bail!("DPT 1 is a 1-bit value"),
359        },
360        5 if dpt.trim().ends_with(".001") => {
361            json!((f64::from(bytes(1)?[0]) * 100.0 / 255.0 * 10.0).round() / 10.0)
362        }
363        5 | 17 | 20 => json!(bytes(1)?[0]),
364        6 => json!(bytes(1)?[0] as i8),
365        7 => json!(u16::from_be_bytes(bytes(2)?.try_into()?)),
366        8 => json!(i16::from_be_bytes(bytes(2)?.try_into()?)),
367        9 => json!(dpt9_decode(bytes(2)?.try_into()?)),
368        12 => json!(u32::from_be_bytes(bytes(4)?.try_into()?)),
369        13 => json!(i32::from_be_bytes(bytes(4)?.try_into()?)),
370        14 => json!(f32::from_be_bytes(bytes(4)?.try_into()?)),
371        16 => {
372            let b = bytes(14)?;
373            let end = b.iter().position(|c| *c == 0).unwrap_or(14);
374            json!(String::from_utf8_lossy(&b[..end]))
375        }
376        _ => bail!("DPT {dpt} is not supported"),
377    })
378}
379
380/// Every reading the data's size allows, keyed by DPT, for a group address whose type is
381/// not configured.
382pub fn interpretations(d: &Data) -> Value {
383    let mut out = Map::new();
384    let candidates: &[&str] = match d {
385        Data::Small(_) => &["1"],
386        Data::Bytes(b) => match b.len() {
387            1 => &["5", "5.001", "6"],
388            2 => &["9", "7", "8"],
389            4 => &["14", "12", "13"],
390            14 => &["16"],
391            _ => &[],
392        },
393    };
394    for c in candidates {
395        if let Ok(v) = decode(c, d) {
396            out.insert(format!("dpt{c}"), v);
397        }
398    }
399    if let Data::Small(v) = d {
400        out.insert("raw_6bit".into(), json!(v));
401    }
402    Value::Object(out)
403}