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}