diff --git a/Cargo.lock b/Cargo.lock index ff18ad5..b0eb2c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,6 +110,12 @@ dependencies = [ "syn 2.0.77", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -259,6 +265,7 @@ name = "embedded-bacnet" version = "0.4.0" dependencies = [ "bacnet-macros", + "base64", "chrono", "clap", "defmt", diff --git a/Cargo.toml b/Cargo.toml index 00fec86..f25657e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ bacnet-macros = { path = "./bacnet-macros", version = "0.1.0" } [dev-dependencies] simple_logger = "5.0.0" +base64 = "0.22.1" chrono = { version = "0.4.28" } clap = { version = "4.5.4", features = ["derive"] } tokio = { version = "1.40.0", features = ["rt-multi-thread", "net", "macros"] } diff --git a/fixtures/datalink_no_segment b/fixtures/datalink_no_segment new file mode 100644 index 0000000..eeb2df9 --- /dev/null +++ b/fixtures/datalink_no_segment @@ -0,0 +1 @@ +gQoCTgEI/iABCTCZDgwAAABkHilNTnUYAEktMzNAOV9RQUMyX1ZvbHRTeXN0UGhOTylVXpEAkQNfKXVOkQVPKW9OggQATx8MAAAAZR4pTU51GQBJLTMzQDlfUUFDMl9Wb2x0U3lzdFBoUGhPKVVekQCRA18pdU6RBU8pb06CBABPHwwAAABmHilNTnUYAEktMzNAOV9RQUMyX0N1cnJlbnRTeXN0TylVXpEAkQNfKXVOkQNPKW9OggQATx8MAAAAZx4pTU51FgBJLTMzQDlfUUFDMl9GcmVxdWVuY3lPKVVOREJH+uFPKXVOkRtPKW9OggQATx8MAAAAaB4pTU51FgBJLTMzQDlfUUFDMl9Wb2x0UGhOVjFPKVVORENoYUhPKXVOkQVPKW9OggQATx8MAAAAaR4pTU51FgBJLTMzQDlfUUFDMl9Wb2x0UGhOVjJPKVVORENouFJPKXVOkQVPKW9OggQATx8MAAAAah4pTU51FgBJLTMzQDlfUUFDMl9Wb2x0UGhOVjNPKVVORENoczNPKXVOkQVPKW9OggQATx8MAAAAax4pTU51FgBJLTMzQDlfUUFDMl9Wb2x0UGhOVm5PKVVekQCRA18pdU6RBU8pb06CBABPHwwAAABsHilNTnUYAEktMzNAOV9RQUMyX1ZvbHRQaFBoVTEyTylVTkRDyXcKTyl1TpEFTylvToIEAE8fDAAAAG0eKU1OdRgASS0zM0A5X1FBQzJfVm9sdFBoUGhVMjNPKVVOREPJVHtPKXVOkQVPKW9OggQATx8= diff --git a/src/application_protocol/application_pdu.rs b/src/application_protocol/application_pdu.rs index 393dfc7..44b2107 100644 --- a/src/application_protocol/application_pdu.rs +++ b/src/application_protocol/application_pdu.rs @@ -19,7 +19,7 @@ pub enum ApplicationPdu<'a> { ComplexAck(ComplexAck<'a>), SimpleAck(SimpleAck), Error(ConfirmedBacnetError), - Segment(Segment<'a>), + Segment(Segment), SegmentAck(SegmentAck), // add more here (see ApduType) } @@ -88,7 +88,7 @@ impl From for MaxSegments { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[repr(u8)] pub enum MaxAdpu { diff --git a/src/application_protocol/segment.rs b/src/application_protocol/segment.rs index 9639721..41d8dc9 100644 --- a/src/application_protocol/segment.rs +++ b/src/application_protocol/segment.rs @@ -3,7 +3,6 @@ #[cfg(feature = "alloc")] use { - crate::common::spooky::Phantom, alloc::{vec, vec::Vec}, }; @@ -34,7 +33,7 @@ pub struct Segment<'a> { #[cfg(feature = "alloc")] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[derive(Debug, Clone)] -pub struct Segment<'a> { +pub struct Segment { pub apdu_type: ApduType, pub more_follows: bool, @@ -45,10 +44,9 @@ pub struct Segment<'a> { // apdu data pub data: Vec, - _phantom: &'a Phantom, } -impl<'a> Segment<'a> { +impl Segment { #[cfg(feature = "alloc")] pub fn new( apdu_type: ApduType, @@ -59,7 +57,6 @@ impl<'a> Segment<'a> { service_choice: u8, data: Vec, ) -> Self { - use crate::common::spooky::PHANTOM; Segment { apdu_type, @@ -68,8 +65,7 @@ impl<'a> Segment<'a> { sequence_number, window_size, service_choice, - data, - _phantom: &PHANTOM, + data } } diff --git a/src/application_protocol/services/i_am.rs b/src/application_protocol/services/i_am.rs index bceb133..7178eb7 100644 --- a/src/application_protocol/services/i_am.rs +++ b/src/application_protocol/services/i_am.rs @@ -1,5 +1,5 @@ use crate::{ - application_protocol::unconfirmed::UnconfirmedServiceChoice, + application_protocol::{application_pdu::MaxAdpu, unconfirmed::UnconfirmedServiceChoice}, common::{ error::Error, helper::{ @@ -17,7 +17,7 @@ use crate::{ #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct IAm { pub device_id: ObjectId, - pub max_apdu: usize, + pub max_apdu: MaxAdpu, pub segmentation: Segmentation, pub vendor_id: u16, } @@ -26,7 +26,7 @@ impl IAm { pub fn encode(&self, writer: &mut Writer) { writer.push(UnconfirmedServiceChoice::IAm as u8); encode_application_object_id(writer, &self.device_id); - encode_application_unsigned(writer, self.max_apdu as u64); + encode_application_unsigned(writer, self.max_apdu.clone() as u64); encode_application_enumerated(writer, self.segmentation.clone() as u32); encode_application_unsigned(writer, self.vendor_id as u64); } @@ -53,8 +53,20 @@ impl IAm { "expected unsigned_int tag type for IAm max_apdu field", )); } - let max_apdu = decode_unsigned(tag.value, reader, buf)?; - let max_apdu = max_apdu as usize; + let raw_max_apdu = decode_unsigned(tag.value, reader, buf)?; + let max_apdu: MaxAdpu = match raw_max_apdu { + 0 => MaxAdpu::_0, + 128 => MaxAdpu::_128, + 206 => MaxAdpu::_206, + 480 => MaxAdpu::_480, + 1024 => MaxAdpu::_1024, + 1476 => MaxAdpu::_1476, + _ => { + return Err(Error::InvalidValueValue( + "unexpected value for maximum apdu size", raw_max_apdu + )); + } + }; // parse a tag then segmentation let tag = Tag::decode(reader, buf)?; diff --git a/src/application_protocol/services/read_range.rs b/src/application_protocol/services/read_range.rs index a72fdf5..2d194b1 100644 --- a/src/application_protocol/services/read_range.rs +++ b/src/application_protocol/services/read_range.rs @@ -369,27 +369,27 @@ impl<'a> ReadRangeItem<'a> { reader, buf, TagNumber::ContextSpecificOpening(Self::DATE_TIME_TAG), - "ReadRangeItem decode", + "ReadRangeItem decode open date time", )?; Tag::decode_expected( reader, buf, TagNumber::Application(ApplicationTagNumber::Date), - "ReadRangeItem decode", + "ReadRangeItem decode date", )?; let date = Date::decode(reader, buf)?; Tag::decode_expected( reader, buf, TagNumber::Application(ApplicationTagNumber::Time), - "ReadRangeItem decode", + "ReadRangeItem decode time", )?; let time = Time::decode(reader, buf)?; Tag::decode_expected( reader, buf, TagNumber::ContextSpecificClosing(Self::DATE_TIME_TAG), - "ReadRangeItem decode", + "ReadRangeItem decode close date time", )?; // value @@ -397,7 +397,7 @@ impl<'a> ReadRangeItem<'a> { reader, buf, TagNumber::ContextSpecificOpening(Self::VALUE_TAG), - "ReadRangeItem decode", + "ReadRangeItem decode open value", )?; let tag = Tag::decode(reader, buf)?; let value_type: ReadRangeValueType = match tag.number { @@ -410,14 +410,29 @@ impl<'a> ReadRangeItem<'a> { ReadRangeValueType::Real => { let value = f32::from_be_bytes(reader.read_bytes(buf)?); ReadRangeValue::Real(value) - } + }, + ReadRangeValueType::Bool => { + let bytes = reader.read_byte(buf)?; + ReadRangeValue::Bool(bytes > 0) + }, + ReadRangeValueType::Unsigned => { + let value = u32::from_be_bytes(reader.read_bytes(buf)?); + ReadRangeValue::Unsigned(value) + }, + ReadRangeValueType::Signed => { + let value = i32::from_be_bytes(reader.read_bytes(buf)?); + ReadRangeValue::Signed(value) + }, + ReadRangeValueType::Null => { + ReadRangeValue::Null + }, x => return Err(Error::Unimplemented(Unimplemented::ReadRangeValueType(x))), }; Tag::decode_expected( reader, buf, TagNumber::ContextSpecificClosing(Self::VALUE_TAG), - "ReadRangeItem decode", + "ReadRangeItem decode close value", )?; // status flags @@ -425,7 +440,7 @@ impl<'a> ReadRangeItem<'a> { reader, buf, TagNumber::ContextSpecific(Self::STATUS_FLAGS_TAG), - "ReadRangeItem decode", + "ReadRangeItem decode status", )?; let status_flags = BitString::decode(&PropertyId::PropStatusFlags, tag.value, reader, buf)?; diff --git a/src/common/error.rs b/src/common/error.rs index ecd77ef..fc23642 100644 --- a/src/common/error.rs +++ b/src/common/error.rs @@ -12,6 +12,7 @@ use crate::{ pub enum Error { Length((&'static str, u32)), InvalidValue(&'static str), + InvalidValueValue(&'static str, u64), InvalidVariant((&'static str, u32)), Unimplemented(Unimplemented), SegmentationNotSupported, diff --git a/src/common/spec.rs b/src/common/spec.rs index 22e9b09..5443663 100644 --- a/src/common/spec.rs +++ b/src/common/spec.rs @@ -11,7 +11,7 @@ pub const BACNET_MAX_PRIORITY: u32 = 16; /* TODO: use derive_more when it reaches 1.0 (to automatically impl TryFrom for all enums) -#[derive(Debug, Clone, derive_more::TryFrom)] +#[derive(Debug, Clone, derive_more::TryFrom, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[try_from(repr)] @@ -25,7 +25,7 @@ pub enum Segmentation { } */ -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[repr(u32)] diff --git a/src/common/time_value.rs b/src/common/time_value.rs index 357aa13..e4ccaff 100644 --- a/src/common/time_value.rs +++ b/src/common/time_value.rs @@ -20,6 +20,7 @@ pub enum SimpleApplicationDataValue { Real(f32), Double(f64), Enumerated(Enumerated), + Null, } impl SimpleApplicationDataValue { @@ -37,6 +38,9 @@ impl SimpleApplicationDataValue { Self::Enumerated(_) => { Tag::new(TagNumber::Application(ApplicationTagNumber::Enumerated), 1) } + Self::Null => { + Tag::new(TagNumber::Application(ApplicationTagNumber::Null), 0) + } } } pub fn decode(tag: &Tag, reader: &mut Reader, buf: &[u8]) -> Result { @@ -75,6 +79,9 @@ impl SimpleApplicationDataValue { let value = Enumerated::Binary(value); Ok(SimpleApplicationDataValue::Enumerated(value)) } + ApplicationTagNumber::Null => { + Ok(SimpleApplicationDataValue::Null) + } x => Err(Error::Unimplemented(Unimplemented::ApplicationTagNumber( x.clone(), diff --git a/src/lib.rs b/src/lib.rs index cef80ef..53bab59 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![no_std] +#![cfg_attr(not(test), no_std)] #![allow(clippy::large_enum_variant)] // This library supports the IP version of bacnet and this is how the network packet is wrapped: diff --git a/src/network_protocol/data_link.rs b/src/network_protocol/data_link.rs index 712612f..4822588 100644 --- a/src/network_protocol/data_link.rs +++ b/src/network_protocol/data_link.rs @@ -62,7 +62,10 @@ impl<'a> DataLink<'a> { // const BVLC_ORIGINAL_BROADCAST_NPDU: u8 = 11; pub fn new(function: DataLinkFunction, npdu: Option>) -> Self { - Self { function, npdu } + Self { + function, + npdu, + } } pub fn new_confirmed_req(req: ConfirmedRequest<'a>) -> Self { @@ -120,6 +123,38 @@ impl<'a> DataLink<'a> { _ => None, }; - Ok(Self { function, npdu }) + Ok(Self { + function, + npdu, + }) + } +} + +#[cfg(test)] +mod tests { + use std::{fs, path::Path}; + + use crate::{common::io::Reader, network_protocol::network_pdu::Addr}; + use base64::{Engine as _, engine::general_purpose::STANDARD}; + use super::DataLink; + + fn load_fixture(name: &str) -> std::io::Result> { + let path = Path::new("fixtures").join(name); + let base64_string = fs::read_to_string(path)?; + Ok(STANDARD.decode(base64_string.strip_suffix("\n").unwrap()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?) + } + + #[test] + fn decode_no_segment() -> std::io::Result<()> { + let buf = load_fixture("datalink_no_segment")?; + let mut reader = Reader::new_with_len(buf.len()); + let datalink = DataLink::decode(&mut reader, &buf).unwrap(); + let npdu = datalink.npdu.unwrap(); + let source = npdu.src.unwrap(); + assert_eq!(source.net, 65056); + assert_eq!(source.addr.unwrap(), Addr::Mac(9)); + assert!(npdu.dst.is_none()); + Ok(()) } } diff --git a/src/network_protocol/network_pdu.rs b/src/network_protocol/network_pdu.rs index 140c9fa..2571736 100644 --- a/src/network_protocol/network_pdu.rs +++ b/src/network_protocol/network_pdu.rs @@ -261,14 +261,29 @@ impl<'a> NetworkPdu<'a> { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] -pub struct Addr { - pub ipv4: [u8; 4], +pub struct Ipv4Addr { + pub addr: [u8; 4], pub port: u16, } +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Addr { + Ipv4(Ipv4Addr), + Mac(u8), +} + +impl Addr { + pub fn new_ipv4(addr: [u8; 4], port: u16) -> Self { + Self::Ipv4(Ipv4Addr { addr, port }) + } +} + const IPV4_ADDR_LEN: u8 = 6; +const MAC_ADDR_LEN: u8 = 1; +const BROADCAST_ADDR_LEN: u8 = 0; pub type SourceAddress = NetworkAddress; @@ -279,6 +294,20 @@ pub struct NetworkAddress { pub addr: Option, } +impl NetworkAddress { + pub fn new_global_broadcast() -> Self { + Self { net: 0xFFFF, addr: None } + } + + pub fn new_remote_broadcast(net: u16) -> Self { + Self { net, addr: None } + } + + pub fn new_remote_station(net: u16, addr: Addr) -> Self { + Self { net, addr: Some(addr) } + } +} + #[derive(Debug, Clone)] #[cfg_attr(feature = "defmt", derive(defmt::Format))] pub struct DestinationAddress { @@ -300,11 +329,20 @@ impl NetworkAddress { writer.extend_from_slice(&self.net.to_be_bytes()); match self.addr.as_ref() { Some(addr) => { - writer.push(IPV4_ADDR_LEN); - writer.extend_from_slice(&addr.ipv4); - writer.extend_from_slice(&addr.port.to_be_bytes()); + match addr { + Addr::Mac(mac) => { + let encoded = &mac.to_be_bytes(); + writer.push(MAC_ADDR_LEN); + writer.extend_from_slice(encoded); + }, + Addr::Ipv4(addr) => { + writer.push(IPV4_ADDR_LEN); + writer.extend_from_slice(&addr.addr); + writer.extend_from_slice(&addr.port.to_be_bytes()); + } + } } - None => writer.push(0), + None => writer.push(BROADCAST_ADDR_LEN), } } @@ -318,12 +356,16 @@ impl NetworkAddress { Ok(Self { net, - addr: Some(Addr { ipv4, port }), + addr: Some(Addr::Ipv4(Ipv4Addr { port, addr: ipv4 })), }) } - 0 => Ok(Self { net, addr: None }), + MAC_ADDR_LEN => { + let addr = u8::from_be_bytes(reader.read_bytes(buf)?); + Ok(Self { net, addr: Some(Addr::Mac(addr)) }) + } + BROADCAST_ADDR_LEN => Ok(Self { net, addr: None }), x => Err(Error::Length(( - "NetworkAddress decode ip len can only be 6 or 0", + "NetworkAddress decode ip len can only be 6 (IP), 1 (MSTP) or 0", x as u32, ))), }