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}