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::{Bip6PortConfig, BipPortConfig, NetworkPortObject};
3use bacnet_types::{MacAddr, enums::NetworkType, error::Error};
4use core::net::{Ipv4Addr, Ipv6Addr};
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, IPV6=9 for B/IPv6,
16    /// VIRTUAL=7 for SC/loopback).
17    pub network_type: u32,
18    /// BACnet network number for this port.
19    pub network_number: u32,
20    /// Port MAC (B/IP 6-byte IP+port, B/IPv6 3-byte VMAC, SC VMAC,
21    /// loopback MAC). B/IP admission rejects a MAC inconsistent with the
22    /// configured IP/UDP.
23    pub mac: MacAddr,
24    /// IPv4 octets for B/IP; zeros otherwise.
25    pub ip: [u8; 4],
26    /// IPv6 octets for B/IPv6; zeros otherwise.
27    pub ipv6: [u8; 16],
28    /// UDP port for B/IP and B/IPv6; 0 for SC/loopback.
29    pub udp_port: u16,
30}
31
32impl NetworkPortEntry {
33    /// B/IP entry with the configured IP/UDP port (possibly unbound).
34    ///
35    /// Derives the 6-byte B/IP MAC (IP octets + big-endian UDP port) from the
36    /// configured socket address. Explicit endpoint registration reconciles the
37    /// selected entry with its actual ephemeral port during startup.
38    ///
39    /// ```
40    /// use core::net::Ipv4Addr;
41    /// use bacnet_objects::device_identity::NetworkPortEntry;
42    ///
43    /// let entry = NetworkPortEntry::bip(1, 0, Ipv4Addr::LOCALHOST, 47808);
44    /// assert_eq!(entry.instance, 1);
45    /// assert_eq!(entry.udp_port, 47808);
46    /// ```
47    pub fn bip(instance: u32, network_number: u32, ip: Ipv4Addr, udp_port: u16) -> Self {
48        let o = ip.octets();
49        let mut mac = MacAddr::new();
50        mac.extend_from_slice(&o);
51        mac.extend_from_slice(&udp_port.to_be_bytes());
52        Self {
53            instance,
54            network_type: NetworkType::IPV4.to_raw(),
55            network_number,
56            mac,
57            ip: o,
58            ipv6: [0; 16],
59            udp_port,
60        }
61    }
62
63    /// B/IPv6 entry (Annex U) with the configured address and UDP port
64    /// (possibly unbound).
65    ///
66    /// The VMAC is a zero placeholder: registering the port on a started
67    /// `Bip6Transport` publishes the VMAC the transport uses, with its
68    /// actual UDP port.
69    ///
70    /// ```
71    /// use core::net::Ipv6Addr;
72    /// use bacnet_objects::device_identity::NetworkPortEntry;
73    ///
74    /// let entry = NetworkPortEntry::bip6(1, 0, Ipv6Addr::LOCALHOST, 47808);
75    /// assert_eq!(entry.ipv6, Ipv6Addr::LOCALHOST.octets());
76    /// assert_eq!(entry.mac.as_slice(), &[0, 0, 0]);
77    /// ```
78    pub fn bip6(instance: u32, network_number: u32, ip: Ipv6Addr, udp_port: u16) -> Self {
79        Self::bip6_with_vmac(instance, network_number, ip, udp_port, [0; 3])
80    }
81
82    /// [`bip6`](Self::bip6) with a known VMAC.
83    pub(super) fn bip6_with_vmac(
84        instance: u32,
85        network_number: u32,
86        ip: Ipv6Addr,
87        udp_port: u16,
88        vmac: [u8; 3],
89    ) -> Self {
90        Self {
91            instance,
92            network_type: NetworkType::IPV6.to_raw(),
93            network_number,
94            mac: MacAddr::from_slice(&vmac),
95            ip: [0; 4],
96            ipv6: ip.octets(),
97            udp_port,
98        }
99    }
100
101    /// Build the declared configuration; this does not inspect an active port.
102    pub(super) fn build_object(&self) -> Result<NetworkPortObject, Error> {
103        let network_number = u16::try_from(self.network_number)
104            .map_err(|_| Error::Encoding("network number must be 0..65534".into()))?;
105        let name = format!("port-{}", self.instance);
106        // Configured B/IP/SC capacity, independently of Device62/role limits.
107        // Live transport association and capability validation are separate.
108        let apdu_length = 1476;
109        if self.network_type == NetworkType::IPV4.to_raw() {
110            // Public entry fields must describe one configured address. Reject
111            // divergent input instead of storing one MAC while deriving another
112            // in either database builder.
113            let [a, b, c, d] = self.ip;
114            let [hi, lo] = self.udp_port.to_be_bytes();
115            if self.mac.as_slice() != [a, b, c, d, hi, lo] {
116                return Err(Error::Encoding(
117                    "B/IP MAC must match configured IPv4 address and UDP port".into(),
118                ));
119            }
120            if self.ipv6 != [0; 16] {
121                return Err(Error::Encoding(
122                    "a B/IP entry carries no IPv6 address".into(),
123                ));
124            }
125            NetworkPortObject::new_bip(
126                self.instance,
127                name,
128                BipPortConfig {
129                    network_number,
130                    apdu_length,
131                    ip_address: self.ip,
132                    udp_port: self.udp_port,
133                    ..Default::default()
134                },
135            )
136        } else if self.network_type == NetworkType::IPV6.to_raw() {
137            NetworkPortObject::new_bip6(
138                self.instance,
139                name,
140                Bip6PortConfig {
141                    network_number,
142                    apdu_length,
143                    ip_address: self.ipv6,
144                    udp_port: self.udp_port,
145                    vmac: self.vmac()?,
146                    ..Default::default()
147                },
148            )
149        } else {
150            NetworkPortObject::new_non_bip(
151                self.instance,
152                name,
153                NetworkType::from_raw(self.network_type),
154                network_number,
155                self.mac.clone(),
156                apdu_length,
157            )
158        }
159    }
160
161    /// SC entry keyed by VMAC (no B/IP socket fields).
162    ///
163    /// IP octets stay zero and the UDP port stays 0: SC has no B/IP socket.
164    /// The VMAC must equal the SC builder's VMAC at hub-dial build time.
165    pub fn sc(instance: u32, network_number: u32, vmac: [u8; 6]) -> Self {
166        Self {
167            instance,
168            network_type: NetworkType::VIRTUAL.to_raw(),
169            network_number,
170            mac: MacAddr::from_slice(&vmac),
171            ip: [0; 4],
172            ipv6: [0; 16],
173            udp_port: 0,
174        }
175    }
176
177    /// A B/IPv6 entry's VMAC: exactly three octets (Annex U.1.3), and no
178    /// IPv4 address beside its IPv6 one.
179    pub(super) fn vmac(&self) -> Result<[u8; 3], Error> {
180        if self.ip != [0; 4] {
181            return Err(Error::Encoding(
182                "a B/IPv6 entry carries no IPv4 address".into(),
183            ));
184        }
185        <[u8; 3]>::try_from(self.mac.as_slice())
186            .map_err(|_| Error::Encoding("B/IPv6 MAC must be a 3-octet VMAC".into()))
187    }
188}