1use anyhow::{bail, ensure, Context, Result};
6use serde_json::{json, Map, Value};
7use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
8
9pub const MAX_FRAME: usize = 512;
12pub 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
38pub 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
63pub 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
83pub 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#[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#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum Data {
158 Small(u8),
159 Bytes(Vec<u8>),
160}
161
162#[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
172pub 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 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
239pub 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
250pub 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
261pub 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
294pub 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
346pub 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
380pub 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}