Skip to main content

bacnet_objects/device/
mod.rs

1//! Device object (type 8) per ASHRAE 135-2020 Clause 12.11.
2//!
3//! The Device object is required in every BACnet device and exposes
4//! device-level properties such as vendor info, protocol support,
5//! and configuration parameters.
6
7use std::borrow::Cow;
8use std::collections::HashMap;
9use std::sync::Arc;
10
11use bacnet_types::constructed::BACnetCOVSubscription;
12use bacnet_types::enums::{
13    ErrorClass, ErrorCode, ObjectType, PropertyIdentifier, Segmentation, ServiceSupported,
14};
15use bacnet_types::error::Error;
16use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
17
18use crate::clock::{ClockFrame, ClockReader};
19use crate::common::read_property_list_property;
20use crate::traits::BACnetObject;
21
22/// Every service the bundled `bacnet-server` dispatch executes, as
23/// `BACnetServicesSupported` bit positions (Clause 21).
24///
25/// Default source for `Protocol_Services_Supported`, which Clause 12.11 ties
26/// to services *executed* — the server's initiate-only services (I-Am,
27/// I-Have, COV/event notifications) are deliberately absent. Kept in lockstep
28/// with the dispatch arms by `bacnet-server`'s executed-services cross-check
29/// test; deployments with a different dispatch surface override via
30/// [`DeviceObject::set_services_supported`].
31pub const EXECUTED_SERVICES: &[ServiceSupported] = &[
32    ServiceSupported::ACKNOWLEDGE_ALARM,
33    ServiceSupported::GET_ALARM_SUMMARY,
34    ServiceSupported::GET_ENROLLMENT_SUMMARY,
35    ServiceSupported::SUBSCRIBE_COV,
36    ServiceSupported::ATOMIC_READ_FILE,
37    ServiceSupported::ATOMIC_WRITE_FILE,
38    ServiceSupported::ADD_LIST_ELEMENT,
39    ServiceSupported::REMOVE_LIST_ELEMENT,
40    ServiceSupported::CREATE_OBJECT,
41    ServiceSupported::DELETE_OBJECT,
42    ServiceSupported::READ_PROPERTY,
43    ServiceSupported::READ_PROPERTY_MULTIPLE,
44    ServiceSupported::WRITE_PROPERTY,
45    ServiceSupported::WRITE_PROPERTY_MULTIPLE,
46    ServiceSupported::DEVICE_COMMUNICATION_CONTROL,
47    ServiceSupported::CONFIRMED_TEXT_MESSAGE,
48    ServiceSupported::REINITIALIZE_DEVICE,
49    ServiceSupported::UNCONFIRMED_TEXT_MESSAGE,
50    ServiceSupported::TIME_SYNCHRONIZATION,
51    ServiceSupported::WHO_HAS,
52    ServiceSupported::WHO_IS,
53    ServiceSupported::READ_RANGE,
54    ServiceSupported::UTC_TIME_SYNCHRONIZATION,
55    // Executes authorized silence/unsilence operations. Built-in reset execution
56    // stays unsupported pending application-executor and replay semantics.
57    ServiceSupported::LIFE_SAFETY_OPERATION,
58    ServiceSupported::SUBSCRIBE_COV_PROPERTY,
59    ServiceSupported::GET_EVENT_INFORMATION,
60    ServiceSupported::SUBSCRIBE_COV_PROPERTY_MULTIPLE,
61    ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION,
62    ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION,
63    ServiceSupported::AUDIT_LOG_QUERY,
64];
65
66/// Number of bits in the `BACnetServicesSupported` production: bits 0..=48
67/// (you-Are is the highest defined bit, Clause 21).
68const SERVICES_SUPPORTED_BITS: usize = 49;
69
70/// Build the `Protocol_Services_Supported` bit string from a service set,
71/// sized for the full production (7 octets, 7 unused bits) and packed
72/// MSB-first per Clause 20.2.10: bit N at byte N/8, position 7-(N%8).
73fn compute_services_supported(services: &[ServiceSupported]) -> Vec<u8> {
74    let mut bits = vec![0u8; SERVICES_SUPPORTED_BITS.div_ceil(8)];
75    for service in services {
76        let n = service.to_raw() as usize;
77        if n < SERVICES_SUPPORTED_BITS {
78            bits[n / 8] |= 0x80 >> (n % 8);
79        }
80    }
81    bits
82}
83
84/// Build a BACnet bitstring representing supported object types.
85/// Each type N sets bit at byte N/8, position 7-(N%8) (MSB-first within each byte).
86fn compute_object_types_supported(types: &[u32]) -> Vec<u8> {
87    let max_type = types.iter().copied().max().unwrap_or(0) as usize;
88    let num_bytes = (max_type / 8) + 1;
89    let mut bitstring = vec![0u8; num_bytes];
90    for &t in types {
91        let byte_idx = (t as usize) / 8;
92        let bit_pos = 7 - ((t as usize) % 8);
93        if byte_idx < bitstring.len() {
94            bitstring[byte_idx] |= 1 << bit_pos;
95        }
96    }
97    bitstring
98}
99
100/// Configuration for creating a Device object.
101pub struct DeviceConfig {
102    /// Device instance number (0..4194303).
103    pub instance: u32,
104    /// Device object name.
105    pub name: String,
106    /// Vendor name string.
107    pub vendor_name: String,
108    /// ASHRAE-assigned vendor identifier.
109    pub vendor_id: u16,
110    /// Model name string.
111    pub model_name: String,
112    /// Firmware revision string.
113    pub firmware_revision: String,
114    /// Application software version string.
115    pub application_software_version: String,
116    /// Maximum APDU length accepted (typically 1476 for BIP).
117    pub max_apdu_length: u32,
118    /// Segmentation support level.
119    pub segmentation_supported: Segmentation,
120    /// APDU timeout in milliseconds.
121    pub apdu_timeout: u32,
122    /// Number of APDU retries.
123    pub apdu_retries: u32,
124}
125
126impl Default for DeviceConfig {
127    fn default() -> Self {
128        Self {
129            instance: 1,
130            name: "BACnet Device".into(),
131            vendor_name: "Rusty BACnet".into(),
132            vendor_id: 0,
133            model_name: "rusty-bacnet".into(),
134            firmware_revision: "0.1.0".into(),
135            application_software_version: "0.1.0".into(),
136            max_apdu_length: 1476,
137            segmentation_supported: Segmentation::NONE,
138            apdu_timeout: 6000,
139            apdu_retries: 3,
140        }
141    }
142}
143
144/// BACnet Device object.
145pub struct DeviceObject {
146    oid: ObjectIdentifier,
147    properties: HashMap<PropertyIdentifier, PropertyValue>,
148    /// Cached object list for array-indexed reads.
149    object_list: Vec<ObjectIdentifier>,
150    /// Protocol_Object_Types_Supported — bitstring indicating which object
151    /// types this device supports (one bit per type, MSB-first within each byte).
152    protocol_object_types_supported: Vec<u8>,
153    /// Configured executed services before clock-availability filtering.
154    configured_services_supported: Vec<ServiceSupported>,
155    /// Shared dynamic clock sample source. `None` is explicitly clockless.
156    clock: Option<Arc<dyn ClockReader>>,
157    /// Active COV subscriptions maintained by the server.
158    active_cov_subscriptions: Vec<BACnetCOVSubscription>,
159}
160
161impl DeviceObject {
162    /// Create a new Device object from configuration.
163    pub fn new(config: DeviceConfig) -> Result<Self, Error> {
164        let oid = ObjectIdentifier::new(ObjectType::DEVICE, config.instance)?;
165        let mut properties = HashMap::new();
166
167        properties.insert(
168            PropertyIdentifier::OBJECT_IDENTIFIER,
169            PropertyValue::ObjectIdentifier(oid),
170        );
171        properties.insert(
172            PropertyIdentifier::OBJECT_NAME,
173            PropertyValue::CharacterString(config.name),
174        );
175        properties.insert(
176            PropertyIdentifier::OBJECT_TYPE,
177            PropertyValue::Enumerated(ObjectType::DEVICE.to_raw()),
178        );
179        properties.insert(
180            PropertyIdentifier::SYSTEM_STATUS,
181            PropertyValue::Enumerated(0), // operational
182        );
183        properties.insert(
184            PropertyIdentifier::VENDOR_NAME,
185            PropertyValue::CharacterString(config.vendor_name),
186        );
187        properties.insert(
188            PropertyIdentifier::VENDOR_IDENTIFIER,
189            PropertyValue::Unsigned(config.vendor_id as u64),
190        );
191        properties.insert(
192            PropertyIdentifier::MODEL_NAME,
193            PropertyValue::CharacterString(config.model_name),
194        );
195        properties.insert(
196            PropertyIdentifier::FIRMWARE_REVISION,
197            PropertyValue::CharacterString(config.firmware_revision),
198        );
199        properties.insert(
200            PropertyIdentifier::APPLICATION_SOFTWARE_VERSION,
201            PropertyValue::CharacterString(config.application_software_version),
202        );
203        properties.insert(
204            PropertyIdentifier::PROTOCOL_VERSION,
205            PropertyValue::Unsigned(1),
206        );
207        properties.insert(
208            PropertyIdentifier::PROTOCOL_REVISION,
209            PropertyValue::Unsigned(22), // Revision 22 (2020)
210        );
211        properties.insert(
212            PropertyIdentifier::MAX_APDU_LENGTH_ACCEPTED,
213            PropertyValue::Unsigned(config.max_apdu_length as u64),
214        );
215        properties.insert(
216            PropertyIdentifier::SEGMENTATION_SUPPORTED,
217            PropertyValue::Enumerated(config.segmentation_supported.to_raw() as u32),
218        );
219        properties.insert(
220            PropertyIdentifier::APDU_TIMEOUT,
221            PropertyValue::Unsigned(config.apdu_timeout as u64),
222        );
223        properties.insert(
224            PropertyIdentifier::NUMBER_OF_APDU_RETRIES,
225            PropertyValue::Unsigned(config.apdu_retries as u64),
226        );
227        properties.insert(
228            PropertyIdentifier::DATABASE_REVISION,
229            PropertyValue::Unsigned(0),
230        );
231        properties.insert(
232            PropertyIdentifier::DESCRIPTION,
233            PropertyValue::CharacterString(String::new()),
234        );
235
236        // Device_Address_Binding — starts empty; populated as devices are discovered.
237        properties.insert(
238            PropertyIdentifier::DEVICE_ADDRESS_BINDING,
239            PropertyValue::List(Vec::new()),
240        );
241
242        // Last_Restart_Reason: 0=unknown, 1=coldstart, 2=warmstart, etc.
243        properties.insert(
244            PropertyIdentifier::LAST_RESTART_REASON,
245            PropertyValue::Enumerated(0), // unknown
246        );
247
248        // Device_UUID: 16-byte UUID stored as OctetString. Default: all zeros.
249        properties.insert(
250            PropertyIdentifier::DEVICE_UUID,
251            PropertyValue::OctetString(vec![0u8; 16]),
252        );
253
254        // Max_Segments_Accepted — only included when segmentation is supported.
255        if config.segmentation_supported != Segmentation::NONE {
256            let max_segments_accepted = if config.segmentation_supported == Segmentation::TRANSMIT {
257                1
258            } else {
259                65
260            };
261            properties.insert(
262                PropertyIdentifier::MAX_SEGMENTS_ACCEPTED,
263                PropertyValue::Unsigned(max_segments_accepted),
264            );
265        }
266
267        // Protocol_Object_Types_Supported: bitstring with one bit per
268        // implemented object type.  Computed from the full set of types
269        // that have concrete struct implementations in this crate.
270        let protocol_object_types_supported = compute_object_types_supported(&[
271            ObjectType::ANALOG_INPUT.to_raw(),
272            ObjectType::ANALOG_OUTPUT.to_raw(),
273            ObjectType::ANALOG_VALUE.to_raw(),
274            ObjectType::BINARY_INPUT.to_raw(),
275            ObjectType::BINARY_OUTPUT.to_raw(),
276            ObjectType::BINARY_VALUE.to_raw(),
277            ObjectType::CALENDAR.to_raw(),
278            ObjectType::COMMAND.to_raw(),
279            ObjectType::DEVICE.to_raw(),
280            ObjectType::EVENT_ENROLLMENT.to_raw(),
281            ObjectType::FILE.to_raw(),
282            ObjectType::GROUP.to_raw(),
283            ObjectType::LOOP.to_raw(),
284            ObjectType::MULTI_STATE_INPUT.to_raw(),
285            ObjectType::MULTI_STATE_OUTPUT.to_raw(),
286            ObjectType::NOTIFICATION_CLASS.to_raw(),
287            ObjectType::PROGRAM.to_raw(),
288            ObjectType::SCHEDULE.to_raw(),
289            ObjectType::AVERAGING.to_raw(),
290            ObjectType::MULTI_STATE_VALUE.to_raw(),
291            ObjectType::TREND_LOG.to_raw(),
292            ObjectType::LIFE_SAFETY_POINT.to_raw(),
293            ObjectType::LIFE_SAFETY_ZONE.to_raw(),
294            ObjectType::ACCUMULATOR.to_raw(),
295            ObjectType::PULSE_CONVERTER.to_raw(),
296            ObjectType::EVENT_LOG.to_raw(),
297            ObjectType::GLOBAL_GROUP.to_raw(),
298            ObjectType::TREND_LOG_MULTIPLE.to_raw(),
299            ObjectType::LOAD_CONTROL.to_raw(),
300            ObjectType::STRUCTURED_VIEW.to_raw(),
301            ObjectType::ACCESS_DOOR.to_raw(),
302            ObjectType::TIMER.to_raw(),
303            ObjectType::ACCESS_CREDENTIAL.to_raw(),
304            ObjectType::ACCESS_POINT.to_raw(),
305            ObjectType::ACCESS_RIGHTS.to_raw(),
306            ObjectType::ACCESS_USER.to_raw(),
307            ObjectType::ACCESS_ZONE.to_raw(),
308            ObjectType::CREDENTIAL_DATA_INPUT.to_raw(),
309            ObjectType::BITSTRING_VALUE.to_raw(),
310            ObjectType::CHARACTERSTRING_VALUE.to_raw(),
311            ObjectType::DATEPATTERN_VALUE.to_raw(),
312            ObjectType::DATE_VALUE.to_raw(),
313            ObjectType::DATETIMEPATTERN_VALUE.to_raw(),
314            ObjectType::DATETIME_VALUE.to_raw(),
315            ObjectType::INTEGER_VALUE.to_raw(),
316            ObjectType::LARGE_ANALOG_VALUE.to_raw(),
317            ObjectType::OCTETSTRING_VALUE.to_raw(),
318            ObjectType::POSITIVE_INTEGER_VALUE.to_raw(),
319            ObjectType::TIMEPATTERN_VALUE.to_raw(),
320            ObjectType::TIME_VALUE.to_raw(),
321            ObjectType::ALERT_ENROLLMENT.to_raw(),
322            ObjectType::LIGHTING_OUTPUT.to_raw(),
323            ObjectType::BINARY_LIGHTING_OUTPUT.to_raw(),
324            ObjectType::NETWORK_PORT.to_raw(),
325            ObjectType::ELEVATOR_GROUP.to_raw(),
326            ObjectType::ESCALATOR.to_raw(),
327            ObjectType::LIFT.to_raw(),
328            ObjectType::STAGING.to_raw(),
329            ObjectType::AUDIT_LOG.to_raw(),
330            ObjectType::AUDIT_REPORTER.to_raw(),
331            ObjectType::COLOR.to_raw(),
332            ObjectType::COLOR_TEMPERATURE.to_raw(),
333        ]);
334
335        Ok(Self {
336            oid,
337            properties,
338            object_list: vec![oid], // Device itself is always in the list
339            protocol_object_types_supported,
340            configured_services_supported: EXECUTED_SERVICES.to_vec(),
341            clock: None,
342            active_cov_subscriptions: Vec::new(),
343        })
344    }
345
346    /// Update the object-list with the current database contents.
347    pub fn set_object_list(&mut self, oids: Vec<ObjectIdentifier>) {
348        self.object_list = oids;
349    }
350
351    /// Replace the advertised executed-service set (`Protocol_Services_Supported`,
352    /// Clause 12.11). For deployments whose dispatch surface differs from the
353    /// bundled server's [`EXECUTED_SERVICES`].
354    ///
355    /// The production is closed at you-Are (bit 48); values past it are not
356    /// representable and are dropped.
357    pub fn set_services_supported(&mut self, services: &[ServiceSupported]) {
358        self.configured_services_supported = services.to_vec();
359    }
360
361    /// Get the device instance number.
362    pub fn instance(&self) -> u32 {
363        self.oid.instance_number()
364    }
365
366    /// Set the description string.
367    pub fn set_description(&mut self, desc: impl Into<String>) {
368        self.properties.insert(
369            PropertyIdentifier::DESCRIPTION,
370            PropertyValue::CharacterString(desc.into()),
371        );
372    }
373
374    /// Replace the entire active COV subscriptions list.
375    pub fn set_active_cov_subscriptions(&mut self, subs: Vec<BACnetCOVSubscription>) {
376        self.active_cov_subscriptions = subs;
377    }
378
379    /// Add a single COV subscription.
380    pub fn add_cov_subscription(&mut self, sub: BACnetCOVSubscription) {
381        self.active_cov_subscriptions.push(sub);
382    }
383
384    fn clock_frame(&self) -> Option<ClockFrame> {
385        self.clock.as_ref()?.read_clock()
386    }
387
388    fn services_supported(&self) -> Vec<u8> {
389        let clock_available = self.clock_frame().is_some();
390        let services = self
391            .configured_services_supported
392            .iter()
393            .copied()
394            .filter(|service| {
395                clock_available
396                    || (*service != ServiceSupported::TIME_SYNCHRONIZATION
397                        && *service != ServiceSupported::UTC_TIME_SYNCHRONIZATION)
398            })
399            .collect::<Vec<_>>();
400        compute_services_supported(&services)
401    }
402}
403
404impl BACnetObject for DeviceObject {
405    fn object_identifier(&self) -> ObjectIdentifier {
406        self.oid
407    }
408
409    fn object_name(&self) -> &str {
410        match self.properties.get(&PropertyIdentifier::OBJECT_NAME) {
411            Some(PropertyValue::CharacterString(s)) => s,
412            _ => "Unknown",
413        }
414    }
415
416    fn read_property(
417        &self,
418        property: PropertyIdentifier,
419        array_index: Option<u32>,
420    ) -> Result<PropertyValue, Error> {
421        if property == PropertyIdentifier::OBJECT_LIST {
422            return match array_index {
423                None => {
424                    let elements = self
425                        .object_list
426                        .iter()
427                        .map(|oid| PropertyValue::ObjectIdentifier(*oid))
428                        .collect();
429                    Ok(PropertyValue::List(elements))
430                }
431                Some(0) => {
432                    // Index 0 = array length per BACnet convention
433                    Ok(PropertyValue::Unsigned(self.object_list.len() as u64))
434                }
435                Some(idx) => {
436                    let i = (idx - 1) as usize; // BACnet arrays are 1-based
437                    if i < self.object_list.len() {
438                        Ok(PropertyValue::ObjectIdentifier(self.object_list[i]))
439                    } else {
440                        Err(Error::Protocol {
441                            class: ErrorClass::PROPERTY.to_raw() as u32,
442                            code: ErrorCode::INVALID_ARRAY_INDEX.to_raw() as u32,
443                        })
444                    }
445                }
446            };
447        }
448
449        if property == PropertyIdentifier::PROPERTY_LIST {
450            return read_property_list_property(&self.property_list(), array_index);
451        }
452
453        if matches!(
454            property,
455            PropertyIdentifier::LOCAL_DATE
456                | PropertyIdentifier::LOCAL_TIME
457                | PropertyIdentifier::UTC_OFFSET
458                | PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS
459        ) {
460            let frame = self.clock_frame().ok_or(Error::Protocol {
461                class: ErrorClass::PROPERTY.to_raw() as u32,
462                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
463            })?;
464            return Ok(match property {
465                PropertyIdentifier::LOCAL_DATE => PropertyValue::Date(frame.local_date),
466                PropertyIdentifier::LOCAL_TIME => PropertyValue::Time(frame.local_time),
467                PropertyIdentifier::UTC_OFFSET => {
468                    PropertyValue::Signed(i32::from(frame.utc_offset))
469                }
470                PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS => {
471                    PropertyValue::Boolean(frame.daylight_savings_status)
472                }
473                _ => unreachable!(),
474            });
475        }
476
477        if property == PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED {
478            let num_bytes = self.protocol_object_types_supported.len();
479            let total_bits = num_bytes * 8;
480            // Find highest set bit to determine actual used bits
481            let mut max_type = 0u32;
482            for (byte_idx, &byte) in self.protocol_object_types_supported.iter().enumerate() {
483                for bit in 0..8 {
484                    if byte & (1 << (7 - bit)) != 0 {
485                        max_type = (byte_idx * 8 + bit) as u32;
486                    }
487                }
488            }
489            let used_bits = max_type as usize + 1;
490            let unused = (total_bits - used_bits) as u8;
491            return Ok(PropertyValue::BitString {
492                unused_bits: unused,
493                data: self.protocol_object_types_supported.clone(),
494            });
495        }
496
497        if property == PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED {
498            let services_supported = self.services_supported();
499            let unused = (services_supported.len() * 8 - SERVICES_SUPPORTED_BITS) as u8;
500            return Ok(PropertyValue::BitString {
501                unused_bits: unused,
502                data: services_supported,
503            });
504        }
505
506        if property == PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS {
507            let mut buf = bytes::BytesMut::new();
508            bacnet_encoding::constructed::encode_cov_subscription_list(
509                &mut buf,
510                &self.active_cov_subscriptions,
511            );
512            return Ok(PropertyValue::ApplicationData(buf.to_vec()));
513        }
514
515        self.properties
516            .get(&property)
517            .cloned()
518            .ok_or(Error::Protocol {
519                class: ErrorClass::PROPERTY.to_raw() as u32,
520                code: ErrorCode::UNKNOWN_PROPERTY.to_raw() as u32,
521            })
522    }
523
524    fn write_property(
525        &mut self,
526        property: PropertyIdentifier,
527        _array_index: Option<u32>,
528        value: PropertyValue,
529        _priority: Option<u8>,
530    ) -> Result<(), Error> {
531        if property == PropertyIdentifier::DESCRIPTION {
532            if let PropertyValue::CharacterString(_) = &value {
533                self.properties.insert(property, value);
534                return Ok(());
535            }
536            return Err(Error::Protocol {
537                class: ErrorClass::PROPERTY.to_raw() as u32,
538                code: ErrorCode::INVALID_DATA_TYPE.to_raw() as u32,
539            });
540        }
541        Err(Error::Protocol {
542            class: ErrorClass::PROPERTY.to_raw() as u32,
543            code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
544        })
545    }
546
547    fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
548        let mut props: Vec<PropertyIdentifier> = self.properties.keys().copied().collect();
549        props.push(PropertyIdentifier::OBJECT_LIST);
550        props.push(PropertyIdentifier::PROPERTY_LIST);
551        props.push(PropertyIdentifier::PROTOCOL_OBJECT_TYPES_SUPPORTED);
552        props.push(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED);
553        props.push(PropertyIdentifier::ACTIVE_COV_SUBSCRIPTIONS);
554        if self.clock_frame().is_some() {
555            props.extend([
556                PropertyIdentifier::LOCAL_DATE,
557                PropertyIdentifier::LOCAL_TIME,
558                PropertyIdentifier::UTC_OFFSET,
559                PropertyIdentifier::DAYLIGHT_SAVINGS_STATUS,
560            ]);
561        }
562        props.sort_by_key(|p| p.to_raw());
563        Cow::Owned(props)
564    }
565
566    /// Device is not createable or deleteable at runtime.
567    fn is_createable(&self) -> bool {
568        false
569    }
570    fn is_deleteable(&self) -> bool {
571        false
572    }
573
574    fn bind_clock_internal(&mut self, clock: Option<Arc<dyn ClockReader>>) {
575        self.clock = clock;
576    }
577}
578
579#[cfg(test)]
580mod tests;