Skip to main content

bacnet_objects/device_identity/
mod.rs

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