Skip to main content

bacnet_objects/
network_port.rs

1//! NetworkPort object (type 56) per ASHRAE 135-2020 Clause 12.56.
2//!
3//! Represents a physical or virtual network port on a BACnet device,
4//! exposing network configuration (IP address, subnet, gateway, etc.)
5//! and link status information.
6
7use bacnet_types::enums::{ObjectType, PropertyIdentifier};
8use bacnet_types::error::Error;
9use bacnet_types::primitives::{ObjectIdentifier, PropertyValue, StatusFlags};
10use bacnet_types::MacAddr;
11use std::borrow::Cow;
12
13use crate::common::{self, read_common_properties};
14use crate::traits::BACnetObject;
15
16/// BACnet Network Port object.
17///
18/// Models a network interface on the device. Key properties include
19/// the network type (IPv4, IPv6, MS/TP, etc.), link speed, MAC address,
20/// and IP configuration parameters.
21pub struct NetworkPortObject {
22    oid: ObjectIdentifier,
23    name: String,
24    description: String,
25    status_flags: StatusFlags,
26    out_of_service: bool,
27    reliability: u32,
28    /// Network type: 0=IPv4, 1=IPv6, 2=MSTP, etc.
29    network_type: u32,
30    /// The BACnet network number this port is connected to.
31    network_number: u32,
32    /// MAC address of this port.
33    mac_address: MacAddr,
34    /// Maximum APDU length accepted on this port.
35    max_apdu_length_accepted: u32,
36    /// Link speed in bits per second.
37    link_speed: f32,
38    /// Whether uncommitted configuration changes are pending.
39    changes_pending: bool,
40    /// NetworkPortCommand: 0=idle, 1=discardChanges, 2=renewFdRegistration, etc.
41    command: u32,
42    /// IP address (4 bytes for IPv4).
43    ip_address: Vec<u8>,
44    /// Default gateway IP address.
45    ip_default_gateway: Vec<u8>,
46    /// Subnet mask.
47    ip_subnet_mask: Vec<u8>,
48    /// BACnet/IP UDP port number.
49    ip_udp_port: u16,
50}
51
52impl NetworkPortObject {
53    /// Create a new Network Port object.
54    ///
55    /// `network_type` specifies the port type: 0=IPv4, 1=IPv6, 2=MSTP, etc.
56    pub fn new(instance: u32, name: impl Into<String>, network_type: u32) -> Result<Self, Error> {
57        let oid = ObjectIdentifier::new(ObjectType::NETWORK_PORT, instance)?;
58        Ok(Self {
59            oid,
60            name: name.into(),
61            description: String::new(),
62            status_flags: StatusFlags::empty(),
63            out_of_service: false,
64            reliability: 0,
65            network_type,
66            network_number: 0,
67            mac_address: MacAddr::new(),
68            max_apdu_length_accepted: 1476,
69            link_speed: 0.0,
70            changes_pending: false,
71            command: 0,
72            ip_address: vec![0, 0, 0, 0],
73            ip_default_gateway: vec![0, 0, 0, 0],
74            ip_subnet_mask: vec![255, 255, 255, 0],
75            ip_udp_port: 0xBAC0,
76        })
77    }
78
79    /// Set the description string.
80    pub fn set_description(&mut self, desc: impl Into<String>) {
81        self.description = desc.into();
82    }
83
84    /// Set the IP address (4 bytes for IPv4).
85    pub fn set_ip_address(&mut self, addr: Vec<u8>) {
86        self.ip_address = addr;
87    }
88
89    /// Set the default gateway IP address.
90    pub fn set_ip_default_gateway(&mut self, gw: Vec<u8>) {
91        self.ip_default_gateway = gw;
92    }
93
94    /// Set the subnet mask.
95    pub fn set_ip_subnet_mask(&mut self, mask: Vec<u8>) {
96        self.ip_subnet_mask = mask;
97    }
98
99    /// Set the MAC address.
100    pub fn set_mac_address(&mut self, mac: MacAddr) {
101        self.mac_address = mac;
102    }
103
104    /// Set the network number.
105    pub fn set_network_number(&mut self, num: u32) {
106        self.network_number = num;
107    }
108
109    /// Set the link speed in bits per second.
110    pub fn set_link_speed(&mut self, speed: f32) {
111        self.link_speed = speed;
112    }
113
114    /// Set the BACnet/IP UDP port.
115    pub fn set_udp_port(&mut self, port: u16) {
116        self.ip_udp_port = port;
117    }
118}
119
120impl BACnetObject for NetworkPortObject {
121    fn object_identifier(&self) -> ObjectIdentifier {
122        self.oid
123    }
124
125    fn object_name(&self) -> &str {
126        &self.name
127    }
128
129    fn read_property(
130        &self,
131        property: PropertyIdentifier,
132        array_index: Option<u32>,
133    ) -> Result<PropertyValue, Error> {
134        if let Some(result) = read_common_properties!(self, property, array_index) {
135            return result;
136        }
137        match property {
138            p if p == PropertyIdentifier::OBJECT_TYPE => {
139                Ok(PropertyValue::Enumerated(ObjectType::NETWORK_PORT.to_raw()))
140            }
141            p if p == PropertyIdentifier::NETWORK_TYPE => {
142                Ok(PropertyValue::Enumerated(self.network_type))
143            }
144            p if p == PropertyIdentifier::NETWORK_NUMBER => {
145                Ok(PropertyValue::Unsigned(self.network_number as u64))
146            }
147            p if p == PropertyIdentifier::MAC_ADDRESS => {
148                Ok(PropertyValue::OctetString(self.mac_address.to_vec()))
149            }
150            p if p == PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED => Ok(PropertyValue::Unsigned(
151                self.max_apdu_length_accepted as u64,
152            )),
153            p if p == PropertyIdentifier::LINK_SPEED => Ok(PropertyValue::Real(self.link_speed)),
154            p if p == PropertyIdentifier::CHANGES_PENDING => {
155                Ok(PropertyValue::Boolean(self.changes_pending))
156            }
157            p if p == PropertyIdentifier::COMMAND_NP => Ok(PropertyValue::Enumerated(self.command)),
158            p if p == PropertyIdentifier::IP_ADDRESS => {
159                Ok(PropertyValue::OctetString(self.ip_address.clone()))
160            }
161            p if p == PropertyIdentifier::IP_DEFAULT_GATEWAY => {
162                Ok(PropertyValue::OctetString(self.ip_default_gateway.clone()))
163            }
164            p if p == PropertyIdentifier::IP_SUBNET_MASK => {
165                Ok(PropertyValue::OctetString(self.ip_subnet_mask.clone()))
166            }
167            p if p == PropertyIdentifier::BACNET_IP_UDP_PORT => {
168                Ok(PropertyValue::Unsigned(self.ip_udp_port as u64))
169            }
170            _ => Err(common::unknown_property_error()),
171        }
172    }
173
174    fn write_property(
175        &mut self,
176        property: PropertyIdentifier,
177        _array_index: Option<u32>,
178        value: PropertyValue,
179        _priority: Option<u8>,
180    ) -> Result<(), Error> {
181        if let Some(result) =
182            common::write_out_of_service(&mut self.out_of_service, property, &value)
183        {
184            return result;
185        }
186        if let Some(result) = common::write_description(&mut self.description, property, &value) {
187            return result;
188        }
189        if property == PropertyIdentifier::COMMAND_NP {
190            if let PropertyValue::Enumerated(v) = value {
191                self.command = v;
192                return Ok(());
193            }
194            return Err(common::invalid_data_type_error());
195        }
196        if property == PropertyIdentifier::IP_ADDRESS {
197            if let PropertyValue::OctetString(v) = value {
198                self.ip_address = v;
199                self.changes_pending = true;
200                return Ok(());
201            }
202            return Err(common::invalid_data_type_error());
203        }
204        if property == PropertyIdentifier::IP_DEFAULT_GATEWAY {
205            if let PropertyValue::OctetString(v) = value {
206                self.ip_default_gateway = v;
207                self.changes_pending = true;
208                return Ok(());
209            }
210            return Err(common::invalid_data_type_error());
211        }
212        if property == PropertyIdentifier::IP_SUBNET_MASK {
213            if let PropertyValue::OctetString(v) = value {
214                self.ip_subnet_mask = v;
215                self.changes_pending = true;
216                return Ok(());
217            }
218            return Err(common::invalid_data_type_error());
219        }
220        if property == PropertyIdentifier::BACNET_IP_UDP_PORT {
221            if let PropertyValue::Unsigned(v) = value {
222                if v > u16::MAX as u64 {
223                    return Err(common::value_out_of_range_error());
224                }
225                self.ip_udp_port = v as u16;
226                self.changes_pending = true;
227                return Ok(());
228            }
229            return Err(common::invalid_data_type_error());
230        }
231        if property == PropertyIdentifier::NETWORK_NUMBER {
232            if let PropertyValue::Unsigned(v) = value {
233                self.network_number = common::u64_to_u32(v)?;
234                return Ok(());
235            }
236            return Err(common::invalid_data_type_error());
237        }
238        if property == PropertyIdentifier::MAC_ADDRESS {
239            if let PropertyValue::OctetString(v) = value {
240                self.mac_address = v.into();
241                return Ok(());
242            }
243            return Err(common::invalid_data_type_error());
244        }
245        Err(common::write_access_denied_error())
246    }
247
248    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
249        static PROPS: &[PropertyIdentifier] = &[
250            PropertyIdentifier::OBJECT_IDENTIFIER,
251            PropertyIdentifier::OBJECT_NAME,
252            PropertyIdentifier::DESCRIPTION,
253            PropertyIdentifier::OBJECT_TYPE,
254            PropertyIdentifier::STATUS_FLAGS,
255            PropertyIdentifier::OUT_OF_SERVICE,
256            PropertyIdentifier::RELIABILITY,
257            PropertyIdentifier::NETWORK_TYPE,
258            PropertyIdentifier::NETWORK_NUMBER,
259            PropertyIdentifier::MAC_ADDRESS,
260            PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
261            PropertyIdentifier::LINK_SPEED,
262            PropertyIdentifier::CHANGES_PENDING,
263            PropertyIdentifier::COMMAND_NP,
264            PropertyIdentifier::IP_ADDRESS,
265            PropertyIdentifier::IP_DEFAULT_GATEWAY,
266            PropertyIdentifier::IP_SUBNET_MASK,
267            PropertyIdentifier::BACNET_IP_UDP_PORT,
268        ];
269        Cow::Borrowed(PROPS)
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276
277    #[test]
278    fn object_type_is_network_port() {
279        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
280        assert_eq!(
281            np.object_identifier().object_type(),
282            ObjectType::NETWORK_PORT
283        );
284        assert_eq!(np.object_identifier().instance_number(), 1);
285    }
286
287    #[test]
288    fn read_object_name() {
289        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
290        let val = np
291            .read_property(PropertyIdentifier::OBJECT_NAME, None)
292            .unwrap();
293        assert_eq!(val, PropertyValue::CharacterString("NP-1".to_string()));
294    }
295
296    #[test]
297    fn read_object_type() {
298        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
299        let val = np
300            .read_property(PropertyIdentifier::OBJECT_TYPE, None)
301            .unwrap();
302        assert_eq!(
303            val,
304            PropertyValue::Enumerated(ObjectType::NETWORK_PORT.to_raw())
305        );
306    }
307
308    #[test]
309    fn read_network_type() {
310        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
311        let val = np
312            .read_property(PropertyIdentifier::NETWORK_TYPE, None)
313            .unwrap();
314        assert_eq!(val, PropertyValue::Enumerated(0)); // IPv4
315    }
316
317    #[test]
318    fn read_network_number_default() {
319        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
320        let val = np
321            .read_property(PropertyIdentifier::NETWORK_NUMBER, None)
322            .unwrap();
323        assert_eq!(val, PropertyValue::Unsigned(0));
324    }
325
326    #[test]
327    fn read_max_apdu_length() {
328        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
329        let val = np
330            .read_property(PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED, None)
331            .unwrap();
332        assert_eq!(val, PropertyValue::Unsigned(1476));
333    }
334
335    #[test]
336    fn read_link_speed_default() {
337        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
338        let val = np
339            .read_property(PropertyIdentifier::LINK_SPEED, None)
340            .unwrap();
341        assert_eq!(val, PropertyValue::Real(0.0));
342    }
343
344    #[test]
345    fn read_changes_pending_default() {
346        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
347        let val = np
348            .read_property(PropertyIdentifier::CHANGES_PENDING, None)
349            .unwrap();
350        assert_eq!(val, PropertyValue::Boolean(false));
351    }
352
353    #[test]
354    fn read_command_default() {
355        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
356        let val = np
357            .read_property(PropertyIdentifier::COMMAND_NP, None)
358            .unwrap();
359        assert_eq!(val, PropertyValue::Enumerated(0)); // idle
360    }
361
362    #[test]
363    fn read_ip_address_default() {
364        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
365        let val = np
366            .read_property(PropertyIdentifier::IP_ADDRESS, None)
367            .unwrap();
368        assert_eq!(val, PropertyValue::OctetString(vec![0, 0, 0, 0]));
369    }
370
371    #[test]
372    fn read_ip_default_gateway_default() {
373        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
374        let val = np
375            .read_property(PropertyIdentifier::IP_DEFAULT_GATEWAY, None)
376            .unwrap();
377        assert_eq!(val, PropertyValue::OctetString(vec![0, 0, 0, 0]));
378    }
379
380    #[test]
381    fn read_ip_subnet_mask_default() {
382        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
383        let val = np
384            .read_property(PropertyIdentifier::IP_SUBNET_MASK, None)
385            .unwrap();
386        assert_eq!(val, PropertyValue::OctetString(vec![255, 255, 255, 0]));
387    }
388
389    #[test]
390    fn read_udp_port_default() {
391        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
392        let val = np
393            .read_property(PropertyIdentifier::BACNET_IP_UDP_PORT, None)
394            .unwrap();
395        assert_eq!(val, PropertyValue::Unsigned(0xBAC0));
396    }
397
398    #[test]
399    fn write_command() {
400        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
401        np.write_property(
402            PropertyIdentifier::COMMAND_NP,
403            None,
404            PropertyValue::Enumerated(1), // discardChanges
405            None,
406        )
407        .unwrap();
408        let val = np
409            .read_property(PropertyIdentifier::COMMAND_NP, None)
410            .unwrap();
411        assert_eq!(val, PropertyValue::Enumerated(1));
412    }
413
414    #[test]
415    fn write_command_wrong_type() {
416        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
417        let result = np.write_property(
418            PropertyIdentifier::COMMAND_NP,
419            None,
420            PropertyValue::Unsigned(1),
421            None,
422        );
423        assert!(result.is_err());
424    }
425
426    #[test]
427    fn write_ip_address_sets_changes_pending() {
428        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
429        assert_eq!(
430            np.read_property(PropertyIdentifier::CHANGES_PENDING, None)
431                .unwrap(),
432            PropertyValue::Boolean(false)
433        );
434
435        np.write_property(
436            PropertyIdentifier::IP_ADDRESS,
437            None,
438            PropertyValue::OctetString(vec![192, 168, 1, 100]),
439            None,
440        )
441        .unwrap();
442
443        assert_eq!(
444            np.read_property(PropertyIdentifier::IP_ADDRESS, None)
445                .unwrap(),
446            PropertyValue::OctetString(vec![192, 168, 1, 100])
447        );
448        assert_eq!(
449            np.read_property(PropertyIdentifier::CHANGES_PENDING, None)
450                .unwrap(),
451            PropertyValue::Boolean(true)
452        );
453    }
454
455    #[test]
456    fn write_ip_default_gateway_sets_changes_pending() {
457        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
458        np.write_property(
459            PropertyIdentifier::IP_DEFAULT_GATEWAY,
460            None,
461            PropertyValue::OctetString(vec![192, 168, 1, 1]),
462            None,
463        )
464        .unwrap();
465
466        assert_eq!(
467            np.read_property(PropertyIdentifier::IP_DEFAULT_GATEWAY, None)
468                .unwrap(),
469            PropertyValue::OctetString(vec![192, 168, 1, 1])
470        );
471        assert_eq!(
472            np.read_property(PropertyIdentifier::CHANGES_PENDING, None)
473                .unwrap(),
474            PropertyValue::Boolean(true)
475        );
476    }
477
478    #[test]
479    fn write_ip_subnet_mask_sets_changes_pending() {
480        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
481        np.write_property(
482            PropertyIdentifier::IP_SUBNET_MASK,
483            None,
484            PropertyValue::OctetString(vec![255, 255, 0, 0]),
485            None,
486        )
487        .unwrap();
488
489        assert_eq!(
490            np.read_property(PropertyIdentifier::IP_SUBNET_MASK, None)
491                .unwrap(),
492            PropertyValue::OctetString(vec![255, 255, 0, 0])
493        );
494        assert_eq!(
495            np.read_property(PropertyIdentifier::CHANGES_PENDING, None)
496                .unwrap(),
497            PropertyValue::Boolean(true)
498        );
499    }
500
501    #[test]
502    fn write_udp_port_sets_changes_pending() {
503        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
504        np.write_property(
505            PropertyIdentifier::BACNET_IP_UDP_PORT,
506            None,
507            PropertyValue::Unsigned(47809),
508            None,
509        )
510        .unwrap();
511
512        assert_eq!(
513            np.read_property(PropertyIdentifier::BACNET_IP_UDP_PORT, None)
514                .unwrap(),
515            PropertyValue::Unsigned(47809)
516        );
517        assert_eq!(
518            np.read_property(PropertyIdentifier::CHANGES_PENDING, None)
519                .unwrap(),
520            PropertyValue::Boolean(true)
521        );
522    }
523
524    #[test]
525    fn write_udp_port_out_of_range() {
526        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
527        let result = np.write_property(
528            PropertyIdentifier::BACNET_IP_UDP_PORT,
529            None,
530            PropertyValue::Unsigned(70000),
531            None,
532        );
533        assert!(result.is_err());
534    }
535
536    #[test]
537    fn write_udp_port_wrong_type() {
538        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
539        let result = np.write_property(
540            PropertyIdentifier::BACNET_IP_UDP_PORT,
541            None,
542            PropertyValue::Real(47808.0),
543            None,
544        );
545        assert!(result.is_err());
546    }
547
548    #[test]
549    fn write_network_number() {
550        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
551        np.write_property(
552            PropertyIdentifier::NETWORK_NUMBER,
553            None,
554            PropertyValue::Unsigned(5),
555            None,
556        )
557        .unwrap();
558        assert_eq!(
559            np.read_property(PropertyIdentifier::NETWORK_NUMBER, None)
560                .unwrap(),
561            PropertyValue::Unsigned(5)
562        );
563    }
564
565    #[test]
566    fn write_mac_address() {
567        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
568        let mac = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01];
569        np.write_property(
570            PropertyIdentifier::MAC_ADDRESS,
571            None,
572            PropertyValue::OctetString(mac.clone()),
573            None,
574        )
575        .unwrap();
576        assert_eq!(
577            np.read_property(PropertyIdentifier::MAC_ADDRESS, None)
578                .unwrap(),
579            PropertyValue::OctetString(mac)
580        );
581    }
582
583    #[test]
584    fn write_out_of_service() {
585        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
586        np.write_property(
587            PropertyIdentifier::OUT_OF_SERVICE,
588            None,
589            PropertyValue::Boolean(true),
590            None,
591        )
592        .unwrap();
593        assert_eq!(
594            np.read_property(PropertyIdentifier::OUT_OF_SERVICE, None)
595                .unwrap(),
596            PropertyValue::Boolean(true)
597        );
598    }
599
600    #[test]
601    fn write_description() {
602        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
603        np.write_property(
604            PropertyIdentifier::DESCRIPTION,
605            None,
606            PropertyValue::CharacterString("Main Ethernet port".to_string()),
607            None,
608        )
609        .unwrap();
610        assert_eq!(
611            np.read_property(PropertyIdentifier::DESCRIPTION, None)
612                .unwrap(),
613            PropertyValue::CharacterString("Main Ethernet port".to_string())
614        );
615    }
616
617    #[test]
618    fn write_read_only_property_denied() {
619        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
620        // LINK_SPEED is read-only
621        let result = np.write_property(
622            PropertyIdentifier::LINK_SPEED,
623            None,
624            PropertyValue::Real(100_000_000.0),
625            None,
626        );
627        assert!(result.is_err());
628    }
629
630    #[test]
631    fn read_unknown_property() {
632        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
633        let result = np.read_property(PropertyIdentifier::PRESENT_VALUE, None);
634        assert!(result.is_err());
635    }
636
637    #[test]
638    fn property_list_complete() {
639        let np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
640        let props = np.property_list();
641        assert!(props.contains(&PropertyIdentifier::OBJECT_IDENTIFIER));
642        assert!(props.contains(&PropertyIdentifier::OBJECT_NAME));
643        assert!(props.contains(&PropertyIdentifier::OBJECT_TYPE));
644        assert!(props.contains(&PropertyIdentifier::NETWORK_TYPE));
645        assert!(props.contains(&PropertyIdentifier::NETWORK_NUMBER));
646        assert!(props.contains(&PropertyIdentifier::MAC_ADDRESS));
647        assert!(props.contains(&PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED));
648        assert!(props.contains(&PropertyIdentifier::LINK_SPEED));
649        assert!(props.contains(&PropertyIdentifier::CHANGES_PENDING));
650        assert!(props.contains(&PropertyIdentifier::COMMAND_NP));
651        assert!(props.contains(&PropertyIdentifier::IP_ADDRESS));
652        assert!(props.contains(&PropertyIdentifier::IP_DEFAULT_GATEWAY));
653        assert!(props.contains(&PropertyIdentifier::IP_SUBNET_MASK));
654        assert!(props.contains(&PropertyIdentifier::BACNET_IP_UDP_PORT));
655    }
656
657    #[test]
658    fn setter_methods_work() {
659        let mut np = NetworkPortObject::new(1, "NP-1", 0).unwrap();
660        np.set_ip_address(vec![10, 0, 0, 1]);
661        np.set_ip_default_gateway(vec![10, 0, 0, 254]);
662        np.set_ip_subnet_mask(vec![255, 255, 255, 0]);
663        np.set_mac_address(MacAddr::from_slice(&[0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E]));
664        np.set_network_number(7);
665        np.set_link_speed(100_000_000.0);
666        np.set_udp_port(47808);
667        np.set_description("Test port");
668
669        assert_eq!(
670            np.read_property(PropertyIdentifier::IP_ADDRESS, None)
671                .unwrap(),
672            PropertyValue::OctetString(vec![10, 0, 0, 1])
673        );
674        assert_eq!(
675            np.read_property(PropertyIdentifier::IP_DEFAULT_GATEWAY, None)
676                .unwrap(),
677            PropertyValue::OctetString(vec![10, 0, 0, 254])
678        );
679        assert_eq!(
680            np.read_property(PropertyIdentifier::NETWORK_NUMBER, None)
681                .unwrap(),
682            PropertyValue::Unsigned(7)
683        );
684        assert_eq!(
685            np.read_property(PropertyIdentifier::LINK_SPEED, None)
686                .unwrap(),
687            PropertyValue::Real(100_000_000.0)
688        );
689        assert_eq!(
690            np.read_property(PropertyIdentifier::BACNET_IP_UDP_PORT, None)
691                .unwrap(),
692            PropertyValue::Unsigned(47808)
693        );
694    }
695
696    #[test]
697    fn mstp_network_type() {
698        let np = NetworkPortObject::new(2, "NP-MSTP", 2).unwrap();
699        assert_eq!(
700            np.read_property(PropertyIdentifier::NETWORK_TYPE, None)
701                .unwrap(),
702            PropertyValue::Enumerated(2) // MS/TP
703        );
704    }
705
706    #[test]
707    fn full_network_config_scenario() {
708        let mut np = NetworkPortObject::new(1, "Ethernet-1", 0).unwrap();
709
710        // Configure the port
711        np.set_ip_address(vec![192, 168, 1, 100]);
712        np.set_ip_default_gateway(vec![192, 168, 1, 1]);
713        np.set_ip_subnet_mask(vec![255, 255, 255, 0]);
714        np.set_mac_address(MacAddr::from_slice(&[0x00, 0x50, 0x56, 0xAB, 0xCD, 0xEF]));
715        np.set_network_number(1);
716        np.set_link_speed(1_000_000_000.0); // 1 Gbps
717        np.set_udp_port(0xBAC0);
718
719        // Verify all reads
720        assert_eq!(np.object_name(), "Ethernet-1");
721        assert_eq!(
722            np.read_property(PropertyIdentifier::IP_ADDRESS, None)
723                .unwrap(),
724            PropertyValue::OctetString(vec![192, 168, 1, 100])
725        );
726        assert_eq!(
727            np.read_property(PropertyIdentifier::LINK_SPEED, None)
728                .unwrap(),
729            PropertyValue::Real(1_000_000_000.0)
730        );
731        assert_eq!(
732            np.read_property(PropertyIdentifier::MAC_ADDRESS, None)
733                .unwrap(),
734            PropertyValue::OctetString(vec![0x00, 0x50, 0x56, 0xAB, 0xCD, 0xEF])
735        );
736
737        // Write IP via property write (triggers changes_pending)
738        np.write_property(
739            PropertyIdentifier::IP_ADDRESS,
740            None,
741            PropertyValue::OctetString(vec![10, 0, 0, 50]),
742            None,
743        )
744        .unwrap();
745        assert_eq!(
746            np.read_property(PropertyIdentifier::CHANGES_PENDING, None)
747                .unwrap(),
748            PropertyValue::Boolean(true)
749        );
750
751        // Discard changes via command
752        np.write_property(
753            PropertyIdentifier::COMMAND_NP,
754            None,
755            PropertyValue::Enumerated(1), // discardChanges
756            None,
757        )
758        .unwrap();
759        assert_eq!(
760            np.read_property(PropertyIdentifier::COMMAND_NP, None)
761                .unwrap(),
762            PropertyValue::Enumerated(1)
763        );
764    }
765}