Skip to main content

bacnet_objects/
network_port.rs

1//! Configured Network Port snapshots and selected live state (135-2020 Clause 12.56).
2//!
3//! The B/IP constructor supplies the flat application-level IPV4/NORMAL
4//! profile. Configuration is read-only over BACnet: this object has no pending
5//! activation owner, socket, or NIC discovery. Reconstruct it to change local
6//! configuration. An explicit NORMAL B/IP owner may publish its actual bind and
7//! learn Network_Number/Quality without changing configured provenance.
8//! Non-B/IP snapshots expose only the common application rows;
9//! they do not claim a complete transport-specific SC, Ethernet or MS/TP profile.
10
11use bacnet_types::enums::{NetworkType, ObjectType, PropertyIdentifier, Reliability};
12use bacnet_types::error::Error;
13use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
14use bacnet_types::MacAddr;
15use std::borrow::Cow;
16
17use crate::common::{self, read_common_properties};
18use crate::traits::BACnetObject;
19
20mod bip_config;
21mod metadata;
22mod registration;
23use bacnet_types::network_number::NetworkNumber;
24pub use bip_config::BipPortConfig;
25
26/// A declared application-port snapshot, optionally associated with an owned link.
27pub struct NetworkPortObject {
28    oid: ObjectIdentifier,
29    name: String,
30    description: String,
31    status_flags: StatusFlags,
32    out_of_service: bool,
33    reliability: Reliability,
34    network_type: NetworkType,
35    network_number: NetworkNumber,
36    mac_address: MacAddr,
37    apdu_length: u32,
38    bip: Option<BipPortConfig>,
39    binding: std::sync::Weak<()>,
40}
41
42impl NetworkPortObject {
43    /// Complete IPV4/NORMAL snapshot. Instance is the declared local Port ID
44    /// (local policy: 1..=255); UDP port zero remains valid unbound configuration.
45    pub fn new_bip(
46        instance: u32,
47        name: impl Into<String>,
48        config: BipPortConfig,
49    ) -> Result<Self, Error> {
50        config.validate(instance)?;
51        let mut mac = MacAddr::from_slice(&config.ip_address);
52        mac.extend_from_slice(&config.udp_port.to_be_bytes());
53        Ok(Self {
54            oid: ObjectIdentifier::new(ObjectType::NETWORK_PORT, instance)?,
55            name: name.into(),
56            description: String::new(),
57            status_flags: StatusFlags::empty(),
58            out_of_service: false,
59            reliability: Reliability::NO_FAULT_DETECTED,
60            network_type: NetworkType::IPV4,
61            network_number: NetworkNumber::configured(config.network_number)
62                .expect("validated Network Number"),
63            mac_address: mac,
64            apdu_length: config.apdu_length,
65            bip: Some(config),
66            binding: std::sync::Weak::new(),
67        })
68    }
69
70    /// Common application rows for Ethernet or VIRTUAL configuration. This is not a
71    /// complete link-specific profile. In particular it exposes no IPv4 fields.
72    /// IPV4 must use `new_bip`; the flat B/IP instance policy does not apply here.
73    pub fn new_non_bip(
74        instance: u32,
75        name: impl Into<String>,
76        network_type: NetworkType,
77        network_number: u16,
78        mac_address: MacAddr,
79        apdu_length: u32,
80    ) -> Result<Self, Error> {
81        if !matches!(network_type, NetworkType::ETHERNET | NetworkType::VIRTUAL)
82            || network_number == u16::MAX
83            || apdu_length < 50
84        {
85            return Err(common::value_out_of_range_error());
86        }
87        Ok(Self {
88            oid: ObjectIdentifier::new(ObjectType::NETWORK_PORT, instance)?,
89            name: name.into(),
90            description: String::new(),
91            status_flags: StatusFlags::empty(),
92            out_of_service: false,
93            reliability: Reliability::NO_FAULT_DETECTED,
94            network_type,
95            network_number: NetworkNumber::configured(network_number)
96                .expect("validated Network Number"),
97            mac_address,
98            apdu_length,
99            bip: None,
100            binding: std::sync::Weak::new(),
101        })
102    }
103
104    /// Set the optional descriptive label without altering port configuration.
105    pub fn set_description(&mut self, desc: impl Into<String>) {
106        self.description = desc.into();
107    }
108}
109
110impl BACnetObject for NetworkPortObject {
111    fn object_identifier(&self) -> ObjectIdentifier {
112        self.oid
113    }
114    fn object_name(&self) -> &str {
115        &self.name
116    }
117
118    fn read_property(
119        &self,
120        property: PropertyIdentifier,
121        array_index: Option<u32>,
122    ) -> Result<PropertyValue, Error> {
123        use PropertyIdentifier as P;
124        if let Some(result) = read_common_properties!(self, property, array_index) {
125            return result;
126        }
127        match property {
128            P::OBJECT_TYPE => Ok(PropertyValue::Enumerated(ObjectType::NETWORK_PORT.to_raw())),
129            P::NETWORK_TYPE => Ok(PropertyValue::Enumerated(self.network_type.to_raw())),
130            P::PROTOCOL_LEVEL => Ok(PropertyValue::Enumerated(2)),
131            P::NETWORK_NUMBER => Ok(PropertyValue::Unsigned(
132                self.network_number.snapshot().0.into(),
133            )),
134            P::NETWORK_NUMBER_QUALITY => Ok(PropertyValue::Enumerated(
135                self.network_number.snapshot().1.into(),
136            )),
137            P::MAC_ADDRESS => Ok(PropertyValue::OctetString(self.mac_address.to_vec())),
138            P::APDU_LENGTH => Ok(PropertyValue::Unsigned(self.apdu_length.into())),
139            // Optional unknown rate, not a measured NIC speed (errata item 23).
140            P::LINK_SPEED => Ok(PropertyValue::Real(0.0)),
141            P::CHANGES_PENDING => Ok(PropertyValue::Boolean(false)),
142            _ => {
143                let config = self
144                    .bip
145                    .as_ref()
146                    .ok_or_else(common::unknown_property_error)?;
147                match property {
148                    P::BACNET_IP_MODE => Ok(PropertyValue::Enumerated(0)),
149                    P::IP_ADDRESS => Ok(PropertyValue::OctetString(config.ip_address.to_vec())),
150                    P::IP_SUBNET_MASK => {
151                        Ok(PropertyValue::OctetString(config.subnet_mask.to_vec()))
152                    }
153                    P::IP_DEFAULT_GATEWAY => {
154                        Ok(PropertyValue::OctetString(config.default_gateway.to_vec()))
155                    }
156                    P::BACNET_IP_UDP_PORT => Ok(PropertyValue::Unsigned(config.udp_port.into())),
157                    P::IP_DNS_SERVER => match array_index {
158                        None => Ok(PropertyValue::List(
159                            config
160                                .dns_servers
161                                .iter()
162                                .map(|ip| PropertyValue::OctetString(ip.to_vec()))
163                                .collect(),
164                        )),
165                        Some(0) => Ok(PropertyValue::Unsigned(config.dns_servers.len() as u64)),
166                        Some(index) => config
167                            .dns_servers
168                            .get(index as usize - 1)
169                            .map(|ip| PropertyValue::OctetString(ip.to_vec()))
170                            .ok_or_else(common::invalid_array_index_error),
171                    },
172                    _ => Err(common::unknown_property_error()),
173                }
174            }
175        }
176    }
177
178    fn write_property(
179        &mut self,
180        property: PropertyIdentifier,
181        _array_index: Option<u32>,
182        value: PropertyValue,
183        _priority: Option<u8>,
184    ) -> Result<(), Error> {
185        if property == PropertyIdentifier::OUT_OF_SERVICE && self.is_bound() {
186            return Err(common::write_access_denied_error());
187        }
188        if let Some(result) =
189            common::write_out_of_service(&mut self.out_of_service, property, &value)
190        {
191            return result;
192        }
193        if let Some(result) = common::write_description(&mut self.description, property, &value) {
194            return result;
195        }
196        // Configuration writes require actual activation (12.56); this snapshot
197        // deliberately provides no apply/Command path or inert pending state.
198        if self
199            .property_metadata()
200            .iter()
201            .any(|row| row.property_identifier == property)
202        {
203            Err(common::write_access_denied_error())
204        } else {
205            Err(common::unknown_property_error())
206        }
207    }
208
209    fn property_metadata(&self) -> Cow<'_, [crate::property_metadata::PropertyMetadata]> {
210        metadata::for_object(self)
211    }
212    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
213        crate::property_metadata::property_list_from_metadata(self.property_metadata().as_ref())
214    }
215    fn is_createable(&self) -> bool {
216        false
217    }
218    fn is_deleteable(&self) -> bool {
219        false
220    }
221}
222
223#[cfg(test)]
224mod tests;