Skip to main content

bacnet_objects/device_identity/
network_port.rs

1//! Configured Network Port declarations, independent of a running transport.
2use crate::network_port::{BipPortConfig, NetworkPortObject};
3use bacnet_types::{enums::NetworkType, error::Error, MacAddr};
4use std::net::Ipv4Addr;
5
6/// One configured Network-Port entry (see module docs for the profile boundary).
7///
8/// Provenance: population-rule coverage is Loopback-only except the B/IP +
9/// SC entries exercised in the RB-16 real-transport proofs. Field layout
10/// follows Clause 12.56; see the module docs for per-transport semantics.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct NetworkPortEntry {
13    /// Network-Port object instance (caller-stable; B/IP=1, SC=2 by convention).
14    pub instance: u32,
15    /// Clause 12.56 Network_Type (IPV4=5 for B/IP, VIRTUAL=7 for SC/loopback).
16    pub network_type: u32,
17    /// BACnet network number for this port.
18    pub network_number: u32,
19    /// Port MAC (B/IP 6-byte IP+port, SC VMAC, loopback MAC).
20    /// B/IP admission rejects a MAC inconsistent with the configured IP/UDP.
21    pub mac: MacAddr,
22    /// IPv4 octets for B/IP; zeros for SC/loopback.
23    pub ip: [u8; 4],
24    /// UDP port for B/IP; 0 for SC/loopback.
25    pub udp_port: u16,
26}
27
28impl NetworkPortEntry {
29    /// B/IP entry with the configured IP/UDP port (possibly unbound).
30    ///
31    /// Derives the 6-byte B/IP MAC (IP octets + big-endian UDP port) from the
32    /// configured socket address. Explicit endpoint registration reconciles the
33    /// selected entry with its actual ephemeral port during startup.
34    ///
35    /// ```
36    /// use std::net::Ipv4Addr;
37    /// use bacnet_objects::device_identity::NetworkPortEntry;
38    ///
39    /// let entry = NetworkPortEntry::bip(1, 0, Ipv4Addr::LOCALHOST, 47808);
40    /// assert_eq!(entry.instance, 1);
41    /// assert_eq!(entry.udp_port, 47808);
42    /// ```
43    pub fn bip(instance: u32, network_number: u32, ip: Ipv4Addr, udp_port: u16) -> Self {
44        let o = ip.octets();
45        let mut mac = MacAddr::new();
46        mac.extend_from_slice(&o);
47        mac.extend_from_slice(&udp_port.to_be_bytes());
48        Self {
49            instance,
50            network_type: NetworkType::IPV4.to_raw(),
51            network_number,
52            mac,
53            ip: o,
54            udp_port,
55        }
56    }
57
58    /// Build the declared configuration; this does not inspect an active port.
59    pub(super) fn build_object(&self) -> Result<NetworkPortObject, Error> {
60        let network_number = u16::try_from(self.network_number)
61            .map_err(|_| Error::Encoding("network number must be 0..65534".into()))?;
62        let name = format!("port-{}", self.instance);
63        // Configured B/IP/SC capacity, independently of Device62/role limits.
64        // Live transport association and capability validation are separate.
65        let apdu_length = 1476;
66        if self.network_type == NetworkType::IPV4.to_raw() {
67            // Public entry fields must describe one configured address. Reject
68            // divergent input instead of storing one MAC while deriving another
69            // in either database builder.
70            let [a, b, c, d] = self.ip;
71            let [hi, lo] = self.udp_port.to_be_bytes();
72            if self.mac.as_slice() != [a, b, c, d, hi, lo] {
73                return Err(Error::Encoding(
74                    "B/IP MAC must match configured IPv4 address and UDP port".into(),
75                ));
76            }
77            NetworkPortObject::new_bip(
78                self.instance,
79                name,
80                BipPortConfig {
81                    network_number,
82                    apdu_length,
83                    ip_address: self.ip,
84                    udp_port: self.udp_port,
85                    ..Default::default()
86                },
87            )
88        } else {
89            NetworkPortObject::new_non_bip(
90                self.instance,
91                name,
92                NetworkType::from_raw(self.network_type),
93                network_number,
94                self.mac.clone(),
95                apdu_length,
96            )
97        }
98    }
99
100    /// SC entry keyed by VMAC (no B/IP socket fields).
101    ///
102    /// IP octets stay zero and the UDP port stays 0: SC has no B/IP socket.
103    /// The VMAC must equal the SC builder's VMAC at hub-dial build time.
104    pub fn sc(instance: u32, network_number: u32, vmac: [u8; 6]) -> Self {
105        Self {
106            instance,
107            network_type: NetworkType::VIRTUAL.to_raw(),
108            network_number,
109            mac: MacAddr::from_slice(&vmac),
110            ip: [0; 4],
111            udp_port: 0,
112        }
113    }
114}