Skip to main content

bacnet_objects/device_identity/
mod.rs

1//! Shared Device declarations and stock object construction.
2mod network_port;
3use crate::prelude::*;
4use crate::{
5    database::ObjectDatabase,
6    device::{DeviceConfig, DeviceObject},
7    network_port::{Bip6PortConfig, BipPortConfig},
8    traits::BACnetObject,
9};
10use bacnet_types::{
11    enums::{NetworkType, ObjectType, PropertyIdentifier as P, Segmentation, ServiceSupported},
12    error::Error,
13    primitives::{ObjectIdentifier, PropertyValue},
14};
15use core::net::{Ipv4Addr, Ipv6Addr};
16pub use network_port::NetworkPortEntry;
17
18/// Single Device identity source.
19///
20/// Owns the Device declarations shared by a server and an endpoint.
21/// Resolve the requested receive ceiling against the owned transport before
22/// generating a stock Device or validating a supplied one. Services and
23/// Network Ports are configuration data; each executor controls its served
24/// service profile.
25///
26/// ```
27/// use bacnet_objects::device_identity::DeviceIdentity;
28///
29/// let identity = DeviceIdentity::new(1001, 42)?;
30/// assert_eq!(identity.instance(), 1001);
31/// assert_eq!(identity.vendor_id(), 42);
32/// # Ok::<(), bacnet_types::error::Error>(())
33/// ```
34#[derive(Clone, Debug)]
35pub struct DeviceIdentity {
36    instance: u32,
37    vendor_id: u16,
38    max_apdu_length: u16,
39    segmentation_supported: Segmentation,
40    apdu_segment_timeout_ms: u64,
41    services: Vec<ServiceSupported>,
42    network_ports: Vec<NetworkPortEntry>,
43    device_uuid: [u8; 16],
44    name: String,
45}
46
47impl DeviceIdentity {
48    /// Creates an identity with endpoint-composed defaults.
49    ///
50    /// Defaults: `max_apdu = 1476`, `segmentation = NONE`,
51    /// `services = [READ_PROPERTY]` (narrow endpoint reality, no superset),
52    /// no ports, zero UUID (B/IP-only may keep zeros; SC dial requires
53    /// [`with_device_uuid`](Self::with_device_uuid)). Returns
54    /// [`Error::Encoding`] for an
55    /// out-of-range Device instance.
56    pub fn new(instance: u32, vendor_id: u16) -> Result<Self, Error> {
57        ObjectIdentifier::new(ObjectType::DEVICE, instance)?;
58        Ok(Self {
59            instance,
60            vendor_id,
61            max_apdu_length: 1476,
62            segmentation_supported: Segmentation::NONE,
63            apdu_segment_timeout_ms: 5000,
64            services: vec![ServiceSupported::READ_PROPERTY],
65            network_ports: Vec::new(),
66            device_uuid: [0; 16],
67            name: format!("device-{instance}"),
68        })
69    }
70
71    /// Sets the raw requested receive ceiling (at least 50 bytes).
72    ///
73    /// Discovery and Device reads preserve this raw value, including 1474.
74    /// Confirmed-Request header encoders floor it separately. Startup resolves
75    /// it against the owned transport's stable local receive capacity.
76    pub fn with_max_apdu(mut self, max_apdu: u16) -> Result<Self, Error> {
77        if max_apdu < 50 {
78            return Err(Error::Encoding(
79                "Device max-APDU must be at least 50".into(),
80            ));
81        }
82        self.max_apdu_length = max_apdu;
83        Ok(self)
84    }
85
86    /// Overrides segmentation support for a matching full server.
87    ///
88    /// Narrow EndpointSession roles require NONE and reject any other identity
89    /// before ingress starts. A full server uses this declaration with the
90    /// segment timeout supplied by this identity.
91    pub fn with_segmentation(mut self, segmentation: Segmentation) -> Self {
92        self.segmentation_supported = segmentation;
93        self
94    }
95
96    /// Sets the Device and full-server segment timeout. EndpointSession roles
97    /// require segmentation NONE; client timers remain caller-owned.
98    pub fn with_apdu_segment_timeout_ms(mut self, timeout_ms: u64) -> Self {
99        self.apdu_segment_timeout_ms = timeout_ms;
100        self
101    }
102
103    /// Overrides the services profile (must equal what roles can do; no superset).
104    ///
105    /// Default `[READ_PROPERTY]` matches the narrow endpoint responder.
106    /// `EndpointSession::with_device_writes` adds `WRITE_PROPERTY` and
107    /// `EndpointSession::with_reinitialize` adds `REINITIALIZE_DEVICE`; other
108    /// service bits are rejected. Server-role startup also rejects missing
109    /// ReadProperty or unsupported bits with neither opt-in, before ingress starts.
110    /// ClientOnly has no responder and retains this as a local declaration.
111    /// Broader executing profiles require a matching full server.
112    pub fn with_services(mut self, services: &[ServiceSupported]) -> Self {
113        self.services = services.to_vec();
114        self
115    }
116
117    /// Stores the durable caller-owned SC device UUID (no generation/storage).
118    ///
119    /// The caller provisions 16 bytes durably and reuses them for the device
120    /// lifetime. SC hub-dial rejects all-zero; B/IP-only identities may keep
121    /// zeros.
122    pub fn with_device_uuid(mut self, uuid: [u8; 16]) -> Self {
123        self.device_uuid = uuid;
124        self
125    }
126
127    /// Overrides the Device object name (defaults to `device-{instance}`).
128    pub fn with_name(mut self, name: impl Into<String>) -> Self {
129        self.name = name.into();
130        self
131    }
132
133    /// Adds one validated configured Network-Port entry.
134    ///
135    /// Rejects duplicate instances and out-of-range Network-Port instances
136    /// and invalid profile configuration.
137    /// Prefer [`with_bip_port`](Self::with_bip_port),
138    /// [`with_bip6_port`](Self::with_bip6_port) or
139    /// [`with_sc_port`](Self::with_sc_port), which encode the population rule.
140    pub fn with_network_port(mut self, entry: NetworkPortEntry) -> Result<Self, Error> {
141        entry.build_object()?;
142        if self
143            .network_ports
144            .iter()
145            .any(|e| e.instance == entry.instance)
146        {
147            return Err(Error::Encoding(format!(
148                "duplicate Network-Port instance {}",
149                entry.instance
150            )));
151        }
152        self.network_ports.push(entry);
153        Ok(self)
154    }
155
156    /// Adds a B/IP port entry with configured IP/UDP values, including UDP zero.
157    pub fn with_bip_port(
158        self,
159        instance: u32,
160        network_number: u32,
161        ip: Ipv4Addr,
162        udp_port: u16,
163    ) -> Result<Self, Error> {
164        self.with_network_port(NetworkPortEntry::bip(
165            instance,
166            network_number,
167            ip,
168            udp_port,
169        ))
170    }
171
172    /// Adds a B/IPv6 port entry with the configured IPv6 address and UDP
173    /// port, including UDP zero. See [`NetworkPortEntry::bip6`].
174    pub fn with_bip6_port(
175        self,
176        instance: u32,
177        network_number: u32,
178        ip: Ipv6Addr,
179        udp_port: u16,
180    ) -> Result<Self, Error> {
181        self.with_network_port(NetworkPortEntry::bip6(
182            instance,
183            network_number,
184            ip,
185            udp_port,
186        ))
187    }
188
189    /// Adds an SC port entry keyed by VMAC.
190    pub fn with_sc_port(
191        self,
192        instance: u32,
193        network_number: u32,
194        vmac: [u8; 6],
195    ) -> Result<Self, Error> {
196        self.with_network_port(NetworkPortEntry::sc(instance, network_number, vmac))
197    }
198
199    /// The identity's entry for the selected port, checked as a
200    /// declaration.
201    fn registered_entry(&self, oid: ObjectIdentifier) -> Result<&NetworkPortEntry, Error> {
202        let entry = self
203            .network_ports
204            .iter()
205            .find(|entry| entry.instance == oid.instance_number())
206            .ok_or_else(|| Error::Encoding("selected Network Port missing from identity".into()))?;
207        entry.build_object()?;
208        Ok(entry)
209    }
210
211    #[doc(hidden)]
212    pub fn validate_registered_bip_internal(
213        &self,
214        oid: ObjectIdentifier,
215        config: &BipPortConfig,
216    ) -> Result<(), Error> {
217        let entry = self.registered_entry(oid)?;
218        if entry.network_type != NetworkType::IPV4.to_raw()
219            || entry.ip != config.ip_address
220            || entry.udp_port != config.udp_port
221            || entry.network_number != u32::from(config.network_number)
222        {
223            return Err(Error::Encoding(
224                "selected identity Network Port differs from database".into(),
225            ));
226        }
227        Ok(())
228    }
229
230    /// Check the selected B/IPv6 entry against the database's configured
231    /// port: address, UDP port and network number. The VMAC is not compared:
232    /// both are placeholders that publication replaces with the VMAC the
233    /// transport uses, as a B/IP check leaves the derived MAC out.
234    #[doc(hidden)]
235    pub fn validate_registered_bip6_internal(
236        &self,
237        oid: ObjectIdentifier,
238        config: &Bip6PortConfig,
239    ) -> Result<(), Error> {
240        let entry = self.registered_entry(oid)?;
241        if entry.network_type != NetworkType::IPV6.to_raw()
242            || entry.ipv6 != config.ip_address
243            || entry.udp_port != config.udp_port
244            || entry.network_number != u32::from(config.network_number)
245        {
246            return Err(Error::Encoding(
247                "selected identity Network Port differs from database".into(),
248            ));
249        }
250        Ok(())
251    }
252
253    /// Record the started B/IPv6 port's bound address and the VMAC it uses
254    /// in the selected entry.
255    #[doc(hidden)]
256    pub fn publish_registered_bip6_internal(
257        &mut self,
258        oid: ObjectIdentifier,
259        address: core::net::SocketAddrV6,
260        vmac: [u8; 3],
261    ) {
262        let entry = self
263            .network_ports
264            .iter_mut()
265            .find(|entry| entry.instance == oid.instance_number())
266            .expect("validated identity registration");
267        *entry = NetworkPortEntry::bip6_with_vmac(
268            entry.instance,
269            entry.network_number,
270            *address.ip(),
271            address.port(),
272            vmac,
273        );
274    }
275
276    #[doc(hidden)]
277    pub fn publish_registered_bip_internal(
278        &mut self,
279        oid: ObjectIdentifier,
280        address: core::net::SocketAddrV4,
281    ) {
282        let entry = self
283            .network_ports
284            .iter_mut()
285            .find(|entry| entry.instance == oid.instance_number())
286            .expect("validated identity registration");
287        *entry = NetworkPortEntry::bip(
288            entry.instance,
289            entry.network_number,
290            *address.ip(),
291            address.port(),
292        );
293    }
294
295    /// Device instance number.
296    pub fn instance(&self) -> u32 {
297        self.instance
298    }
299
300    /// Vendor identifier (I-Am + Device VENDOR_IDENTIFIER).
301    pub fn vendor_id(&self) -> u16 {
302        self.vendor_id
303    }
304
305    /// Raw requested or resolved local receive ceiling.
306    pub fn max_apdu_length(&self) -> u16 {
307        self.max_apdu_length
308    }
309
310    /// Device object name.
311    pub fn name(&self) -> &str {
312        &self.name
313    }
314
315    /// Segment timeout in milliseconds. The shared default is 5000 ms.
316    pub fn apdu_segment_timeout_ms(&self) -> u64 {
317        self.apdu_segment_timeout_ms
318    }
319
320    /// Resolve the requested receive ceiling before any transport I/O.
321    pub fn resolve(&self, local_receive_capacity: u16) -> Result<Self, Error> {
322        let mut identity = self.clone();
323        identity.max_apdu_length = identity.max_apdu_length.min(local_receive_capacity);
324        identity.validate()?;
325        Ok(identity)
326    }
327
328    /// Check the pure Device declaration. Executors also validate deadlines.
329    pub fn validate(&self) -> Result<(), Error> {
330        ObjectIdentifier::new(ObjectType::DEVICE, self.instance)?;
331        if self.max_apdu_length < 50 {
332            return Err(Error::Encoding(
333                "Device max-APDU must be at least 50".into(),
334            ));
335        }
336        if self.segmentation_supported.to_raw() > Segmentation::NONE.to_raw() {
337            return Err(Error::Encoding(
338                "invalid Device segmentation support".into(),
339            ));
340        }
341        if self.segmentation_supported != Segmentation::NONE && self.apdu_segment_timeout_ms == 0 {
342            return Err(Error::Encoding(
343                "APDU_Segment_Timeout must be positive when segmentation is supported".into(),
344            ));
345        }
346        Ok(())
347    }
348
349    /// Validate a supplied Device without rebuilding it or changing its state.
350    pub fn validate_device(&self, device: &dyn BACnetObject) -> Result<(), Error> {
351        self.validate()?;
352        if device.object_identifier() != self.device_oid() {
353            return Err(Error::Encoding(
354                "selected Device identifier must match identity".into(),
355            ));
356        }
357        for (property, expected) in [
358            (
359                P::OBJECT_IDENTIFIER,
360                PropertyValue::ObjectIdentifier(self.device_oid()),
361            ),
362            (
363                P::VENDOR_IDENTIFIER,
364                PropertyValue::Unsigned(u64::from(self.vendor_id)),
365            ),
366            (
367                P::MAX_APDU_LENGTH_ACCEPTED,
368                PropertyValue::Unsigned(u64::from(self.max_apdu_length)),
369            ),
370            (
371                P::SEGMENTATION_SUPPORTED,
372                PropertyValue::Enumerated(u32::from(self.segmentation_supported.to_raw())),
373            ),
374        ] {
375            if device.read_property(property, None)? != expected {
376                return Err(Error::Encoding(format!(
377                    "selected Device {property:?} must match identity"
378                )));
379            }
380        }
381        if self.segmentation_supported != Segmentation::NONE
382            && device.read_property(P::APDU_SEGMENT_TIMEOUT, None)?
383                != PropertyValue::Unsigned(self.apdu_segment_timeout_ms)
384        {
385            return Err(Error::Encoding(
386                "selected Device APDU_Segment_Timeout must match identity".into(),
387            ));
388        }
389        Ok(())
390    }
391
392    /// Segmentation support (I-Am + Device SEGMENTATION_SUPPORTED).
393    pub fn segmentation(&self) -> Segmentation {
394        self.segmentation_supported
395    }
396
397    /// Services profile (Device PROTOCOL_SERVICES_SUPPORTED; no superset).
398    pub fn services(&self) -> &[ServiceSupported] {
399        &self.services
400    }
401
402    /// Configured Network-Port entries, independent of live transport binding.
403    pub fn network_ports(&self) -> &[NetworkPortEntry] {
404        &self.network_ports
405    }
406
407    /// Durable caller-owned SC device UUID.
408    pub fn device_uuid(&self) -> [u8; 16] {
409        self.device_uuid
410    }
411
412    /// Device object identifier derived from the instance.
413    pub fn device_oid(&self) -> ObjectIdentifier {
414        ObjectIdentifier::new(ObjectType::DEVICE, self.instance)
415            .expect("identity instance was validated at construction")
416    }
417
418    /// Constructs the ObjectDatabase (Device + Network-Port + DEVICE_UUID).
419    ///
420    /// Truth direction: identity -> database. For extra application objects
421    /// use [`build_database_with_extra`]: it seeds `Object_List` with
422    /// Device + ports + extras upfront so no post-add mutation is needed.
423    pub fn build_database(&self) -> Result<ObjectDatabase, Error> {
424        let mut device = DeviceObject::new(DeviceConfig {
425            identity: self.clone(),
426            firmware_revision: env!("CARGO_PKG_VERSION").into(),
427            application_software_version: env!("CARGO_PKG_VERSION").into(),
428            ..DeviceConfig::default()
429        })?;
430        device.set_services_supported(&self.services);
431        device.set_device_uuid(self.device_uuid);
432        let device_oid = device.object_identifier();
433
434        let mut oids = vec![device_oid];
435        let mut ports = Vec::new();
436        for entry in &self.network_ports {
437            let port = entry.build_object()?;
438            oids.push(port.object_identifier());
439            ports.push(port);
440        }
441        device.set_object_list(oids);
442
443        let mut db = ObjectDatabase::new();
444        db.add(Box::new(device))?;
445        for port in ports {
446            db.add(Box::new(port))?;
447        }
448        Ok(db)
449    }
450}
451
452/// Builds a database from an identity plus extra application objects.
453///
454/// Truth direction stays identity-first: the Device `Object_List` is seeded
455/// with Device + ports + every extra OID upfront, so no post-add mutation
456/// (and no device/network-port fork) is needed. Extra objects are boxed
457/// `BACnetObject`s (e.g. Analog Input for proofs).
458///
459/// ```
460/// use bacnet_objects::device_identity::{build_database_with_extra, DeviceIdentity};
461/// use bacnet_objects::analog::AnalogInputObject;
462/// use bacnet_types::enums::EngineeringUnits;
463///
464/// let identity = DeviceIdentity::new(1001, 42)?;
465/// let mut point = AnalogInputObject::new(1, "ai-1", EngineeringUnits::NO_UNITS)?;
466/// point.set_present_value(21.5);
467/// let db = build_database_with_extra(&identity, vec![Box::new(point)])?;
468/// assert!(db.get(&identity.device_oid()).is_some());
469/// # Ok::<(), bacnet_types::error::Error>(())
470/// ```
471pub fn build_database_with_extra(
472    identity: &DeviceIdentity,
473    extra: Vec<Box<dyn crate::traits::BACnetObject>>,
474) -> Result<ObjectDatabase, Error> {
475    let mut device = DeviceObject::new(DeviceConfig {
476        identity: identity.clone(),
477        firmware_revision: env!("CARGO_PKG_VERSION").into(),
478        application_software_version: env!("CARGO_PKG_VERSION").into(),
479        ..DeviceConfig::default()
480    })?;
481    device.set_services_supported(&identity.services);
482    device.set_device_uuid(identity.device_uuid);
483    let device_oid = device.object_identifier();
484
485    let mut oids = vec![device_oid];
486    let mut ports = Vec::new();
487    for entry in &identity.network_ports {
488        let port = entry.build_object()?;
489        oids.push(port.object_identifier());
490        ports.push(port);
491    }
492    for obj in &extra {
493        oids.push(obj.object_identifier());
494    }
495    device.set_object_list(oids);
496
497    let mut db = ObjectDatabase::new();
498    db.add(Box::new(device))?;
499    for port in ports {
500        db.add(Box::new(port))?;
501    }
502    for obj in extra {
503        db.add(obj)?;
504    }
505    Ok(db)
506}
507
508impl Default for DeviceIdentity {
509    fn default() -> Self {
510        Self::new(1, 0)
511            .expect("default Device instance is valid")
512            .with_name("BACnet Device")
513    }
514}