use bacnet_encoding::apdu::{self, Apdu};
use bacnet_encoding::npdu;
use bacnet_services::who_is::IAmRequest;
use bacnet_types::enums::UnconfirmedServiceChoice;
use bytes::Bytes;
#[derive(Debug)]
pub struct DecodedNpdu {
pub is_network_message: bool,
pub source_network: Option<u16>,
pub dest_network: Option<u16>,
pub network_message_type: Option<u8>,
pub broadcast: bool,
pub apdu: Option<DecodedApdu>,
}
#[derive(Debug)]
pub struct DecodedApdu {
pub pdu_type: String,
pub invoke_id: Option<u8>,
pub segmented: bool,
pub service_name: String,
pub service: Option<String>,
pub problem: Option<String>,
pub device: Option<u32>,
}
pub fn decode_npdu_layer(npdu: Bytes) -> Result<DecodedNpdu, String> {
let npdu_result = npdu::decode_npdu(npdu).map_err(|e| format!("NPDU decode error: {e}"))?;
let source_network = npdu_result.source.as_ref().map(|a| a.network);
let dest_network = npdu_result.destination.as_ref().map(|a| a.network);
let apdu = if !npdu_result.is_network_message() && !npdu_result.payload.is_empty() {
Some(decode_apdu_layer(&npdu_result)?)
} else {
None
};
Ok(DecodedNpdu {
is_network_message: npdu_result.is_network_message(),
source_network,
dest_network,
network_message_type: npdu_result.message_type.map(|kind| kind.to_raw()),
broadcast: npdu_result
.destination
.as_ref()
.is_some_and(|d| d.mac_address.is_empty()),
apdu,
})
}
fn decode_apdu_layer(npdu_data: &npdu::Npdu) -> Result<DecodedApdu, String> {
let apdu_result = apdu::decode_apdu(npdu_data.payload.clone())
.map_err(|e| format!("APDU decode error: {e}"))?;
let service = |choice: &dyn std::fmt::Display| Some(choice.to_string());
let decoded = match apdu_result {
Apdu::ConfirmedRequest(ref cr) => DecodedApdu {
pdu_type: "Confirmed-Request".to_string(),
invoke_id: Some(cr.invoke_id),
segmented: cr.segmented,
service_name: format!("{}", cr.service_choice),
service: service(&cr.service_choice),
problem: None,
device: None,
},
Apdu::UnconfirmedRequest(ref ur) => DecodedApdu {
pdu_type: "Unconfirmed-Request".to_string(),
invoke_id: None,
segmented: false,
service_name: format!("{}", ur.service_choice),
service: service(&ur.service_choice),
problem: None,
device: (ur.service_choice == UnconfirmedServiceChoice::I_AM)
.then(|| IAmRequest::decode(&ur.service_request).ok())
.flatten()
.map(|i_am| i_am.object_identifier.instance_number()),
},
Apdu::SimpleAck(ref sa) => DecodedApdu {
pdu_type: "Simple-ACK".to_string(),
invoke_id: Some(sa.invoke_id),
segmented: false,
service_name: format!("{}-ACK", sa.service_choice),
service: service(&sa.service_choice),
problem: None,
device: None,
},
Apdu::ComplexAck(ref ca) => DecodedApdu {
pdu_type: "Complex-ACK".to_string(),
invoke_id: Some(ca.invoke_id),
segmented: ca.segmented,
service_name: format!("{}-ACK", ca.service_choice),
service: service(&ca.service_choice),
problem: None,
device: None,
},
Apdu::SegmentAck(ref sa) => DecodedApdu {
pdu_type: "Segment-ACK".to_string(),
invoke_id: Some(sa.invoke_id),
segmented: false,
service_name: "SegmentACK".to_string(),
service: None,
problem: None,
device: None,
},
Apdu::Error(ref ep) => DecodedApdu {
pdu_type: "Error".to_string(),
invoke_id: Some(ep.invoke_id),
segmented: false,
service_name: format!("{}-Error", ep.service_choice),
service: service(&ep.service_choice),
problem: Some(format!("{}:{}", ep.error_class, ep.error_code)),
device: None,
},
Apdu::Reject(ref rp) => DecodedApdu {
pdu_type: "Reject".to_string(),
invoke_id: Some(rp.invoke_id),
segmented: false,
service_name: format!("Reject({})", rp.reject_reason),
service: None,
problem: Some(format!("reject {}", rp.reject_reason)),
device: None,
},
Apdu::Abort(ref ap) => DecodedApdu {
pdu_type: "Abort".to_string(),
invoke_id: Some(ap.invoke_id),
segmented: false,
service_name: format!("Abort({})", ap.abort_reason),
service: None,
problem: Some(format!("abort {}", ap.abort_reason)),
device: None,
},
};
Ok(decoded)
}