Skip to main content

bacnet_ops/decode/
layers.rs

1//! The network and application layers of a frame, decoded as far as a
2//! summary needs: routing, the message type, the PDU type, invoke ID,
3//! service and what went wrong.
4
5use bacnet_encoding::apdu::{self, Apdu};
6use bacnet_encoding::npdu;
7use bacnet_services::who_is::IAmRequest;
8use bacnet_types::enums::UnconfirmedServiceChoice;
9use bytes::Bytes;
10
11/// Decoded network layer.
12#[derive(Debug)]
13pub struct DecodedNpdu {
14    /// A network-layer message rather than an APDU.
15    pub is_network_message: bool,
16    /// SNET, when a router added one.
17    pub source_network: Option<u16>,
18    /// DNET, for a routed or globally broadcast NPDU.
19    pub dest_network: Option<u16>,
20    /// The network-layer message type, for a network message.
21    pub network_message_type: Option<u8>,
22    /// A DNET with an empty DADR: a broadcast on that network, or a global one.
23    pub broadcast: bool,
24    /// The APDU, unless this is a network message or carries none.
25    pub apdu: Option<DecodedApdu>,
26}
27
28/// Decoded application layer.
29#[derive(Debug)]
30pub struct DecodedApdu {
31    /// The PDU type, such as `Confirmed-Request`.
32    pub pdu_type: String,
33    /// The invoke ID, for a PDU that carries one.
34    pub invoke_id: Option<u8>,
35    /// The segmented-message flag.
36    pub segmented: bool,
37    /// The service as the one-shot capture prints it, such as
38    /// `READ_PROPERTY-ACK`.
39    pub service_name: String,
40    /// The service the PDU belongs to, without `-ACK` or `-Error`; `None`
41    /// for a Segment-ACK, a Reject or an Abort, which don't carry one.
42    pub service: Option<String>,
43    /// What went wrong: an Error's class and code, a Reject's or an
44    /// Abort's reason.
45    pub problem: Option<String>,
46    /// The device instance an I-Am announces.
47    pub device: Option<u32>,
48}
49
50/// Decode the NPDU layer a BVLL message carries.
51pub fn decode_npdu_layer(npdu: Bytes) -> Result<DecodedNpdu, String> {
52    let npdu_result = npdu::decode_npdu(npdu).map_err(|e| format!("NPDU decode error: {e}"))?;
53
54    let source_network = npdu_result.source.as_ref().map(|a| a.network);
55    let dest_network = npdu_result.destination.as_ref().map(|a| a.network);
56
57    let apdu = if !npdu_result.is_network_message() && !npdu_result.payload.is_empty() {
58        Some(decode_apdu_layer(&npdu_result)?)
59    } else {
60        None
61    };
62
63    Ok(DecodedNpdu {
64        is_network_message: npdu_result.is_network_message(),
65        source_network,
66        dest_network,
67        network_message_type: npdu_result.message_type.map(|kind| kind.to_raw()),
68        broadcast: npdu_result
69            .destination
70            .as_ref()
71            .is_some_and(|d| d.mac_address.is_empty()),
72        apdu,
73    })
74}
75
76/// Decode the APDU layer from an NPDU.
77fn decode_apdu_layer(npdu_data: &npdu::Npdu) -> Result<DecodedApdu, String> {
78    let apdu_result = apdu::decode_apdu(npdu_data.payload.clone())
79        .map_err(|e| format!("APDU decode error: {e}"))?;
80
81    let service = |choice: &dyn std::fmt::Display| Some(choice.to_string());
82    let decoded = match apdu_result {
83        Apdu::ConfirmedRequest(ref cr) => DecodedApdu {
84            pdu_type: "Confirmed-Request".to_string(),
85            invoke_id: Some(cr.invoke_id),
86            segmented: cr.segmented,
87            service_name: format!("{}", cr.service_choice),
88            service: service(&cr.service_choice),
89            problem: None,
90            device: None,
91        },
92        Apdu::UnconfirmedRequest(ref ur) => DecodedApdu {
93            pdu_type: "Unconfirmed-Request".to_string(),
94            invoke_id: None,
95            segmented: false,
96            service_name: format!("{}", ur.service_choice),
97            service: service(&ur.service_choice),
98            problem: None,
99            device: (ur.service_choice == UnconfirmedServiceChoice::I_AM)
100                .then(|| IAmRequest::decode(&ur.service_request).ok())
101                .flatten()
102                .map(|i_am| i_am.object_identifier.instance_number()),
103        },
104        Apdu::SimpleAck(ref sa) => DecodedApdu {
105            pdu_type: "Simple-ACK".to_string(),
106            invoke_id: Some(sa.invoke_id),
107            segmented: false,
108            service_name: format!("{}-ACK", sa.service_choice),
109            service: service(&sa.service_choice),
110            problem: None,
111            device: None,
112        },
113        Apdu::ComplexAck(ref ca) => DecodedApdu {
114            pdu_type: "Complex-ACK".to_string(),
115            invoke_id: Some(ca.invoke_id),
116            segmented: ca.segmented,
117            service_name: format!("{}-ACK", ca.service_choice),
118            service: service(&ca.service_choice),
119            problem: None,
120            device: None,
121        },
122        Apdu::SegmentAck(ref sa) => DecodedApdu {
123            pdu_type: "Segment-ACK".to_string(),
124            invoke_id: Some(sa.invoke_id),
125            segmented: false,
126            service_name: "SegmentACK".to_string(),
127            service: None,
128            problem: None,
129            device: None,
130        },
131        Apdu::Error(ref ep) => DecodedApdu {
132            pdu_type: "Error".to_string(),
133            invoke_id: Some(ep.invoke_id),
134            segmented: false,
135            service_name: format!("{}-Error", ep.service_choice),
136            service: service(&ep.service_choice),
137            problem: Some(format!("{}:{}", ep.error_class, ep.error_code)),
138            device: None,
139        },
140        Apdu::Reject(ref rp) => DecodedApdu {
141            pdu_type: "Reject".to_string(),
142            invoke_id: Some(rp.invoke_id),
143            segmented: false,
144            service_name: format!("Reject({})", rp.reject_reason),
145            service: None,
146            problem: Some(format!("reject {}", rp.reject_reason)),
147            device: None,
148        },
149        Apdu::Abort(ref ap) => DecodedApdu {
150            pdu_type: "Abort".to_string(),
151            invoke_id: Some(ap.invoke_id),
152            segmented: false,
153            service_name: format!("Abort({})", ap.abort_reason),
154            service: None,
155            problem: Some(format!("abort {}", ap.abort_reason)),
156            device: None,
157        },
158    };
159
160    Ok(decoded)
161}