Skip to main content

bacnet_server/
pics.rs

1//! Protocol Implementation Conformance Statement (PICS) generation per ASHRAE 135-2020 Annex A.
2//!
3//! A PICS document is a formal declaration of which BACnet features a device supports.
4//! It is required for BACnet certification and interoperability testing.
5
6use std::collections::BTreeMap;
7use std::fmt;
8
9use bacnet_objects::database::ObjectDatabase;
10use bacnet_types::enums::{ObjectType, PropertyIdentifier};
11
12use crate::server::ServerConfig;
13
14// ───────────────────────────── Data model ──────────────────────────────────
15
16/// Complete PICS document per ASHRAE 135-2020 Annex A.
17#[derive(Debug, Clone)]
18pub struct Pics {
19    pub vendor_info: VendorInfo,
20    pub device_profile: DeviceProfile,
21    pub supported_object_types: Vec<ObjectTypeSupport>,
22    pub supported_services: Vec<ServiceSupport>,
23    pub data_link_layers: Vec<DataLinkSupport>,
24    pub network_layer: NetworkLayerSupport,
25    pub character_sets: Vec<CharacterSet>,
26    pub special_functionality: Vec<String>,
27}
28
29/// Vendor and device identification (Annex A.1).
30#[derive(Debug, Clone)]
31pub struct VendorInfo {
32    pub vendor_id: u16,
33    pub vendor_name: String,
34    pub model_name: String,
35    pub firmware_revision: String,
36    pub application_software_version: String,
37    pub protocol_version: u16,
38    pub protocol_revision: u16,
39}
40
41/// BACnet device profile (Annex A.2).
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum DeviceProfile {
44    /// BACnet Advanced Application Controller.
45    BAac,
46    /// BACnet Application Specific Controller.
47    BAsc,
48    /// BACnet Operator Workstation.
49    BOws,
50    /// BACnet Building Controller.
51    BBc,
52    /// BACnet Operator Panel.
53    BOp,
54    /// BACnet Router.
55    BRouter,
56    /// BACnet Gateway.
57    BGw,
58    /// BACnet Smart Controller.
59    BSc,
60    /// Custom / non-standard profile.
61    Custom(String),
62}
63
64impl fmt::Display for DeviceProfile {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        match self {
67            Self::BAac => f.write_str("B-AAC"),
68            Self::BAsc => f.write_str("B-ASC"),
69            Self::BOws => f.write_str("B-OWS"),
70            Self::BBc => f.write_str("B-BC"),
71            Self::BOp => f.write_str("B-OP"),
72            Self::BRouter => f.write_str("B-ROUTER"),
73            Self::BGw => f.write_str("B-GW"),
74            Self::BSc => f.write_str("B-SC"),
75            Self::Custom(s) => f.write_str(s),
76        }
77    }
78}
79
80/// Property access flags for a supported property.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct PropertyAccess {
83    pub readable: bool,
84    pub writable: bool,
85    pub optional: bool,
86}
87
88impl fmt::Display for PropertyAccess {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        let r = if self.readable { "R" } else { "" };
91        let w = if self.writable { "W" } else { "" };
92        let o = if self.optional { "O" } else { "" };
93        write!(f, "{r}{w}{o}")
94    }
95}
96
97/// Supported property with its access flags.
98#[derive(Debug, Clone)]
99pub struct PropertySupport {
100    pub property_id: PropertyIdentifier,
101    pub access: PropertyAccess,
102}
103
104/// Object type support declaration (Annex A.3).
105#[derive(Debug, Clone)]
106pub struct ObjectTypeSupport {
107    pub object_type: ObjectType,
108    pub createable: bool,
109    pub deleteable: bool,
110    pub supported_properties: Vec<PropertySupport>,
111}
112
113/// Service support declaration (Annex A.4).
114#[derive(Debug, Clone)]
115pub struct ServiceSupport {
116    pub service_name: String,
117    pub initiator: bool,
118    pub executor: bool,
119}
120
121/// Data link layer support (Annex A.5).
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub enum DataLinkSupport {
124    BipV4,
125    BipV6,
126    Mstp,
127    Ethernet,
128    BacnetSc,
129}
130
131impl fmt::Display for DataLinkSupport {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::BipV4 => f.write_str("BACnet/IP (Annex J)"),
135            Self::BipV6 => f.write_str("BACnet/IPv6 (Annex U)"),
136            Self::Mstp => f.write_str("MS/TP (Clause 9)"),
137            Self::Ethernet => f.write_str("BACnet Ethernet (Clause 7)"),
138            Self::BacnetSc => f.write_str("BACnet/SC (Annex AB)"),
139        }
140    }
141}
142
143/// Network layer capabilities (Annex A.6).
144#[derive(Debug, Clone)]
145pub struct NetworkLayerSupport {
146    pub router: bool,
147    pub bbmd: bool,
148    pub foreign_device: bool,
149}
150
151/// Character set support (Annex A.7).
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub enum CharacterSet {
154    Utf8,
155    Ansi,
156    DbcsIbm,
157    DbcsMs,
158    Jisx0208,
159    Iso8859_1,
160}
161
162impl fmt::Display for CharacterSet {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            Self::Utf8 => f.write_str("UTF-8"),
166            Self::Ansi => f.write_str("ANSI X3.4"),
167            Self::DbcsIbm => f.write_str("IBM/Microsoft DBCS"),
168            Self::DbcsMs => f.write_str("JIS C 6226"),
169            Self::Jisx0208 => f.write_str("JIS X 0208"),
170            Self::Iso8859_1 => f.write_str("ISO 8859-1"),
171        }
172    }
173}
174
175// ────────────────────────────── Configuration ──────────────────────────────
176
177/// Configuration for PICS generation that cannot be inferred from the database.
178#[derive(Debug, Clone)]
179pub struct PicsConfig {
180    pub vendor_name: String,
181    pub model_name: String,
182    pub firmware_revision: String,
183    pub application_software_version: String,
184    pub protocol_version: u16,
185    pub protocol_revision: u16,
186    pub device_profile: DeviceProfile,
187    pub data_link_layers: Vec<DataLinkSupport>,
188    pub network_layer: NetworkLayerSupport,
189    pub character_sets: Vec<CharacterSet>,
190    pub special_functionality: Vec<String>,
191}
192
193impl Default for PicsConfig {
194    fn default() -> Self {
195        Self {
196            vendor_name: String::new(),
197            model_name: String::new(),
198            firmware_revision: String::new(),
199            application_software_version: String::new(),
200            protocol_version: 1,
201            protocol_revision: 24,
202            device_profile: DeviceProfile::BAsc,
203            data_link_layers: vec![DataLinkSupport::BipV4],
204            network_layer: NetworkLayerSupport {
205                router: false,
206                bbmd: false,
207                foreign_device: false,
208            },
209            character_sets: vec![CharacterSet::Utf8],
210            special_functionality: Vec::new(),
211        }
212    }
213}
214
215// ────────────────────────────── Generator ──────────────────────────────────
216
217/// Generates a [`Pics`] document from an [`ObjectDatabase`] and configuration.
218pub struct PicsGenerator<'a> {
219    db: &'a ObjectDatabase,
220    server_config: &'a ServerConfig,
221    pics_config: &'a PicsConfig,
222}
223
224impl<'a> PicsGenerator<'a> {
225    pub fn new(
226        db: &'a ObjectDatabase,
227        server_config: &'a ServerConfig,
228        pics_config: &'a PicsConfig,
229    ) -> Self {
230        Self {
231            db,
232            server_config,
233            pics_config,
234        }
235    }
236
237    /// Generate the complete PICS document.
238    pub fn generate(&self) -> Pics {
239        Pics {
240            vendor_info: self.build_vendor_info(),
241            device_profile: self.pics_config.device_profile.clone(),
242            supported_object_types: self.build_object_types(),
243            supported_services: self.build_services(),
244            data_link_layers: self.pics_config.data_link_layers.clone(),
245            network_layer: self.pics_config.network_layer.clone(),
246            character_sets: self.pics_config.character_sets.clone(),
247            special_functionality: self.pics_config.special_functionality.clone(),
248        }
249    }
250
251    fn build_vendor_info(&self) -> VendorInfo {
252        VendorInfo {
253            vendor_id: self.server_config.vendor_id,
254            vendor_name: self.pics_config.vendor_name.clone(),
255            model_name: self.pics_config.model_name.clone(),
256            firmware_revision: self.pics_config.firmware_revision.clone(),
257            application_software_version: self.pics_config.application_software_version.clone(),
258            protocol_version: self.pics_config.protocol_version,
259            protocol_revision: self.pics_config.protocol_revision,
260        }
261    }
262
263    fn build_object_types(&self) -> Vec<ObjectTypeSupport> {
264        // Group objects by type using a BTreeMap for deterministic ordering.
265        let mut by_type: BTreeMap<u32, Vec<&dyn bacnet_objects::traits::BACnetObject>> =
266            BTreeMap::new();
267        for (_oid, obj) in self.db.iter_objects() {
268            by_type
269                .entry(obj.object_identifier().object_type().to_raw())
270                .or_default()
271                .push(obj);
272        }
273
274        let mut result = Vec::with_capacity(by_type.len());
275        for (raw_type, objects) in &by_type {
276            let object_type = ObjectType::from_raw(*raw_type);
277            // Use the first object as representative for property enumeration.
278            let representative = objects[0];
279            let all_props = representative.property_list();
280            let required = representative.required_properties();
281
282            let supported_properties = all_props
283                .iter()
284                .map(|&pid| {
285                    let is_required = required.contains(&pid);
286                    // Try a probe write to check writability.  We only check the
287                    // representative and consider all listed properties readable.
288                    let writable = Self::is_writable_property(object_type, pid);
289                    PropertySupport {
290                        property_id: pid,
291                        access: PropertyAccess {
292                            readable: true,
293                            writable,
294                            optional: !is_required,
295                        },
296                    }
297                })
298                .collect();
299
300            let createable = Self::is_createable(object_type);
301            let deleteable = Self::is_deleteable(object_type);
302
303            result.push(ObjectTypeSupport {
304                object_type,
305                createable,
306                deleteable,
307                supported_properties,
308            });
309        }
310        result
311    }
312
313    /// Heuristic: properties that are commonly writable per BACnet standard.
314    fn is_writable_property(object_type: ObjectType, pid: PropertyIdentifier) -> bool {
315        // Universal read-only properties
316        if pid == PropertyIdentifier::OBJECT_IDENTIFIER
317            || pid == PropertyIdentifier::OBJECT_TYPE
318            || pid == PropertyIdentifier::PROPERTY_LIST
319            || pid == PropertyIdentifier::STATUS_FLAGS
320        {
321            return false;
322        }
323
324        // OBJECT_NAME is writable on most objects
325        if pid == PropertyIdentifier::OBJECT_NAME {
326            return true;
327        }
328
329        // PRESENT_VALUE writability depends on object type
330        if pid == PropertyIdentifier::PRESENT_VALUE {
331            return object_type != ObjectType::ANALOG_INPUT
332                && object_type != ObjectType::BINARY_INPUT
333                && object_type != ObjectType::MULTI_STATE_INPUT;
334        }
335
336        // Common writable properties
337        pid == PropertyIdentifier::DESCRIPTION
338            || pid == PropertyIdentifier::OUT_OF_SERVICE
339            || pid == PropertyIdentifier::COV_INCREMENT
340            || pid == PropertyIdentifier::HIGH_LIMIT
341            || pid == PropertyIdentifier::LOW_LIMIT
342            || pid == PropertyIdentifier::DEADBAND
343            || pid == PropertyIdentifier::NOTIFICATION_CLASS
344    }
345
346    fn is_createable(object_type: ObjectType) -> bool {
347        // Device and NetworkPort objects are not dynamically created.
348        object_type != ObjectType::DEVICE && object_type != ObjectType::NETWORK_PORT
349    }
350
351    fn is_deleteable(object_type: ObjectType) -> bool {
352        object_type != ObjectType::DEVICE && object_type != ObjectType::NETWORK_PORT
353    }
354
355    /// Build the service support list based on what the server actually handles.
356    fn build_services(&self) -> Vec<ServiceSupport> {
357        let mut services = Vec::new();
358
359        // Confirmed services the server executes
360        let executor_services = [
361            "ReadProperty",
362            "WriteProperty",
363            "ReadPropertyMultiple",
364            "WritePropertyMultiple",
365            "SubscribeCOV",
366            "SubscribeCOVProperty",
367            "CreateObject",
368            "DeleteObject",
369            "DeviceCommunicationControl",
370            "ReinitializeDevice",
371            "GetEventInformation",
372            "AcknowledgeAlarm",
373            "ReadRange",
374            "AtomicReadFile",
375            "AtomicWriteFile",
376            "AddListElement",
377            "RemoveListElement",
378        ];
379
380        // Confirmed services the server initiates
381        let initiator_services = ["ConfirmedCOVNotification", "ConfirmedEventNotification"];
382
383        // Unconfirmed services the server executes
384        let unconfirmed_executor = [
385            "WhoIs",
386            "WhoHas",
387            "TimeSynchronization",
388            "UTCTimeSynchronization",
389        ];
390
391        // Unconfirmed services the server initiates
392        let unconfirmed_initiator = [
393            "I-Am",
394            "I-Have",
395            "UnconfirmedCOVNotification",
396            "UnconfirmedEventNotification",
397        ];
398
399        // Merge all service names, tracking initiator/executor status.
400        let mut service_map: BTreeMap<&str, (bool, bool)> = BTreeMap::new();
401        for name in &executor_services {
402            service_map.entry(name).or_default().1 = true;
403        }
404        for name in &initiator_services {
405            service_map.entry(name).or_default().0 = true;
406        }
407        for name in &unconfirmed_executor {
408            service_map.entry(name).or_default().1 = true;
409        }
410        for name in &unconfirmed_initiator {
411            service_map.entry(name).or_default().0 = true;
412        }
413
414        for (name, (initiator, executor)) in &service_map {
415            services.push(ServiceSupport {
416                service_name: (*name).to_string(),
417                initiator: *initiator,
418                executor: *executor,
419            });
420        }
421
422        services
423    }
424}
425
426// ─────────────────────────── Text output ───────────────────────────────────
427
428impl Pics {
429    /// Render the PICS as human-readable text per Annex A layout.
430    pub fn generate_text(&self) -> String {
431        let mut out = String::with_capacity(4096);
432
433        out.push_str("=== BACnet Protocol Implementation Conformance Statement (PICS) ===\n");
434        out.push_str("    Per ASHRAE 135-2020 Annex A\n\n");
435
436        // Section 1: Vendor Information
437        out.push_str("--- Vendor Information ---\n");
438        out.push_str(&format!(
439            "Vendor ID:                      {}\n",
440            self.vendor_info.vendor_id
441        ));
442        out.push_str(&format!(
443            "Vendor Name:                    {}\n",
444            self.vendor_info.vendor_name
445        ));
446        out.push_str(&format!(
447            "Model Name:                     {}\n",
448            self.vendor_info.model_name
449        ));
450        out.push_str(&format!(
451            "Firmware Revision:              {}\n",
452            self.vendor_info.firmware_revision
453        ));
454        out.push_str(&format!(
455            "Application Software Version:   {}\n",
456            self.vendor_info.application_software_version
457        ));
458        out.push_str(&format!(
459            "Protocol Version:               {}\n",
460            self.vendor_info.protocol_version
461        ));
462        out.push_str(&format!(
463            "Protocol Revision:              {}\n\n",
464            self.vendor_info.protocol_revision
465        ));
466
467        // Section 2: Device Profile
468        out.push_str("--- BACnet Device Profile ---\n");
469        out.push_str(&format!("Profile: {}\n\n", self.device_profile));
470
471        // Section 3: Supported Object Types
472        out.push_str("--- Supported Object Types ---\n");
473        for ot in &self.supported_object_types {
474            out.push_str(&format!(
475                "\n  Object Type: {} (createable={}, deleteable={})\n",
476                ot.object_type, ot.createable, ot.deleteable
477            ));
478            out.push_str("  Properties:\n");
479            for prop in &ot.supported_properties {
480                out.push_str(&format!(
481                    "    {:<40} {}\n",
482                    prop.property_id.to_string(),
483                    prop.access
484                ));
485            }
486        }
487        out.push('\n');
488
489        // Section 4: Supported Services
490        out.push_str("--- Supported Services ---\n");
491        out.push_str(&format!(
492            "  {:<45} {:>9} {:>9}\n",
493            "Service", "Initiator", "Executor"
494        ));
495        out.push_str(&format!("  {:-<45} {:-<9} {:-<9}\n", "", "", ""));
496        for svc in &self.supported_services {
497            let init = if svc.initiator { "Yes" } else { "No" };
498            let exec = if svc.executor { "Yes" } else { "No" };
499            out.push_str(&format!(
500                "  {:<45} {:>9} {:>9}\n",
501                svc.service_name, init, exec
502            ));
503        }
504        out.push('\n');
505
506        // Section 5: Data Link Layers
507        out.push_str("--- Data Link Layer Support ---\n");
508        for dl in &self.data_link_layers {
509            out.push_str(&format!("  {dl}\n"));
510        }
511        out.push('\n');
512
513        // Section 6: Network Layer
514        out.push_str("--- Network Layer Options ---\n");
515        out.push_str(&format!(
516            "  Router:         {}\n",
517            self.network_layer.router
518        ));
519        out.push_str(&format!("  BBMD:           {}\n", self.network_layer.bbmd));
520        out.push_str(&format!(
521            "  Foreign Device: {}\n\n",
522            self.network_layer.foreign_device
523        ));
524
525        // Section 7: Character Sets
526        out.push_str("--- Character Sets Supported ---\n");
527        for cs in &self.character_sets {
528            out.push_str(&format!("  {cs}\n"));
529        }
530        out.push('\n');
531
532        // Section 8: Special Functionality
533        if !self.special_functionality.is_empty() {
534            out.push_str("--- Special Functionality ---\n");
535            for sf in &self.special_functionality {
536                out.push_str(&format!("  {sf}\n"));
537            }
538            out.push('\n');
539        }
540
541        out
542    }
543
544    /// Render the PICS as Markdown for documentation.
545    pub fn generate_markdown(&self) -> String {
546        let mut out = String::with_capacity(4096);
547
548        out.push_str("# BACnet Protocol Implementation Conformance Statement (PICS)\n\n");
549        out.push_str("*Per ASHRAE 135-2020 Annex A*\n\n");
550
551        // Vendor Info
552        out.push_str("## Vendor Information\n\n");
553        out.push_str("| Field | Value |\n");
554        out.push_str("|-------|-------|\n");
555        out.push_str(&format!("| Vendor ID | {} |\n", self.vendor_info.vendor_id));
556        out.push_str(&format!(
557            "| Vendor Name | {} |\n",
558            self.vendor_info.vendor_name
559        ));
560        out.push_str(&format!(
561            "| Model Name | {} |\n",
562            self.vendor_info.model_name
563        ));
564        out.push_str(&format!(
565            "| Firmware Revision | {} |\n",
566            self.vendor_info.firmware_revision
567        ));
568        out.push_str(&format!(
569            "| Application Software Version | {} |\n",
570            self.vendor_info.application_software_version
571        ));
572        out.push_str(&format!(
573            "| Protocol Version | {} |\n",
574            self.vendor_info.protocol_version
575        ));
576        out.push_str(&format!(
577            "| Protocol Revision | {} |\n\n",
578            self.vendor_info.protocol_revision
579        ));
580
581        // Device Profile
582        out.push_str("## BACnet Device Profile\n\n");
583        out.push_str(&format!("**{}**\n\n", self.device_profile));
584
585        // Object Types
586        out.push_str("## Supported Object Types\n\n");
587        for ot in &self.supported_object_types {
588            out.push_str(&format!(
589                "### {}\n\n- Createable: {}\n- Deleteable: {}\n\n",
590                ot.object_type, ot.createable, ot.deleteable
591            ));
592            out.push_str("| Property | Access |\n");
593            out.push_str("|----------|--------|\n");
594            for prop in &ot.supported_properties {
595                out.push_str(&format!("| {} | {} |\n", prop.property_id, prop.access));
596            }
597            out.push('\n');
598        }
599
600        // Services
601        out.push_str("## Supported Services\n\n");
602        out.push_str("| Service | Initiator | Executor |\n");
603        out.push_str("|---------|-----------|----------|\n");
604        for svc in &self.supported_services {
605            let init = if svc.initiator { "Yes" } else { "No" };
606            let exec = if svc.executor { "Yes" } else { "No" };
607            out.push_str(&format!("| {} | {} | {} |\n", svc.service_name, init, exec));
608        }
609        out.push('\n');
610
611        // Data Link
612        out.push_str("## Data Link Layer Support\n\n");
613        for dl in &self.data_link_layers {
614            out.push_str(&format!("- {dl}\n"));
615        }
616        out.push('\n');
617
618        // Network Layer
619        out.push_str("## Network Layer Options\n\n");
620        out.push_str("| Feature | Supported |\n");
621        out.push_str("|---------|-----------|\n");
622        out.push_str(&format!("| Router | {} |\n", self.network_layer.router));
623        out.push_str(&format!("| BBMD | {} |\n", self.network_layer.bbmd));
624        out.push_str(&format!(
625            "| Foreign Device | {} |\n\n",
626            self.network_layer.foreign_device
627        ));
628
629        // Character Sets
630        out.push_str("## Character Sets Supported\n\n");
631        for cs in &self.character_sets {
632            out.push_str(&format!("- {cs}\n"));
633        }
634        out.push('\n');
635
636        // Special Functionality
637        if !self.special_functionality.is_empty() {
638            out.push_str("## Special Functionality\n\n");
639            for sf in &self.special_functionality {
640                out.push_str(&format!("- {sf}\n"));
641            }
642            out.push('\n');
643        }
644
645        out
646    }
647}
648
649// ─────────────────────────── Standalone helper ─────────────────────────────
650
651/// Generate a PICS document from an ObjectDatabase and configuration.
652///
653/// This is a convenience function for use without a running BACnetServer.
654pub fn generate_pics(
655    db: &ObjectDatabase,
656    server_config: &ServerConfig,
657    pics_config: &PicsConfig,
658) -> Pics {
659    PicsGenerator::new(db, server_config, pics_config).generate()
660}
661
662// ─────────────────────────────── Tests ─────────────────────────────────────
663
664#[cfg(test)]
665mod tests {
666    use std::borrow::Cow;
667
668    use bacnet_objects::traits::BACnetObject;
669    use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
670    use bacnet_types::error::Error;
671    use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
672
673    use super::*;
674
675    // ── Minimal test objects ───────────────────────────────────────────
676
677    struct TestAnalogInput {
678        oid: ObjectIdentifier,
679        name: String,
680    }
681
682    impl BACnetObject for TestAnalogInput {
683        fn object_identifier(&self) -> ObjectIdentifier {
684            self.oid
685        }
686        fn object_name(&self) -> &str {
687            &self.name
688        }
689        fn read_property(
690            &self,
691            _property: PropertyIdentifier,
692            _array_index: Option<u32>,
693        ) -> Result<PropertyValue, Error> {
694            Ok(PropertyValue::Real(0.0))
695        }
696        fn write_property(
697            &mut self,
698            _property: PropertyIdentifier,
699            _array_index: Option<u32>,
700            _value: PropertyValue,
701            _priority: Option<u8>,
702        ) -> Result<(), Error> {
703            Err(Error::Protocol {
704                class: ErrorClass::PROPERTY.to_raw() as u32,
705                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
706            })
707        }
708        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
709            static PROPS: [PropertyIdentifier; 8] = [
710                PropertyIdentifier::OBJECT_IDENTIFIER,
711                PropertyIdentifier::OBJECT_NAME,
712                PropertyIdentifier::OBJECT_TYPE,
713                PropertyIdentifier::PROPERTY_LIST,
714                PropertyIdentifier::PRESENT_VALUE,
715                PropertyIdentifier::STATUS_FLAGS,
716                PropertyIdentifier::OUT_OF_SERVICE,
717                PropertyIdentifier::UNITS,
718            ];
719            Cow::Borrowed(&PROPS)
720        }
721    }
722
723    struct TestBinaryValue {
724        oid: ObjectIdentifier,
725        name: String,
726    }
727
728    impl BACnetObject for TestBinaryValue {
729        fn object_identifier(&self) -> ObjectIdentifier {
730            self.oid
731        }
732        fn object_name(&self) -> &str {
733            &self.name
734        }
735        fn read_property(
736            &self,
737            _property: PropertyIdentifier,
738            _array_index: Option<u32>,
739        ) -> Result<PropertyValue, Error> {
740            Ok(PropertyValue::Boolean(false))
741        }
742        fn write_property(
743            &mut self,
744            _property: PropertyIdentifier,
745            _array_index: Option<u32>,
746            _value: PropertyValue,
747            _priority: Option<u8>,
748        ) -> Result<(), Error> {
749            Ok(())
750        }
751        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
752            static PROPS: [PropertyIdentifier; 6] = [
753                PropertyIdentifier::OBJECT_IDENTIFIER,
754                PropertyIdentifier::OBJECT_NAME,
755                PropertyIdentifier::OBJECT_TYPE,
756                PropertyIdentifier::PROPERTY_LIST,
757                PropertyIdentifier::PRESENT_VALUE,
758                PropertyIdentifier::STATUS_FLAGS,
759            ];
760            Cow::Borrowed(&PROPS)
761        }
762    }
763
764    struct TestDevice {
765        oid: ObjectIdentifier,
766        name: String,
767    }
768
769    impl BACnetObject for TestDevice {
770        fn object_identifier(&self) -> ObjectIdentifier {
771            self.oid
772        }
773        fn object_name(&self) -> &str {
774            &self.name
775        }
776        fn read_property(
777            &self,
778            _property: PropertyIdentifier,
779            _array_index: Option<u32>,
780        ) -> Result<PropertyValue, Error> {
781            Ok(PropertyValue::Unsigned(0))
782        }
783        fn write_property(
784            &mut self,
785            _property: PropertyIdentifier,
786            _array_index: Option<u32>,
787            _value: PropertyValue,
788            _priority: Option<u8>,
789        ) -> Result<(), Error> {
790            Err(Error::Protocol {
791                class: ErrorClass::PROPERTY.to_raw() as u32,
792                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
793            })
794        }
795        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
796            static PROPS: [PropertyIdentifier; 6] = [
797                PropertyIdentifier::OBJECT_IDENTIFIER,
798                PropertyIdentifier::OBJECT_NAME,
799                PropertyIdentifier::OBJECT_TYPE,
800                PropertyIdentifier::PROPERTY_LIST,
801                PropertyIdentifier::PROTOCOL_VERSION,
802                PropertyIdentifier::PROTOCOL_REVISION,
803            ];
804            Cow::Borrowed(&PROPS)
805        }
806    }
807
808    // ── Helpers ────────────────────────────────────────────────────────
809
810    fn make_test_db() -> ObjectDatabase {
811        let mut db = ObjectDatabase::new();
812        db.add(Box::new(TestDevice {
813            oid: ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap(),
814            name: "Test Device".into(),
815        }))
816        .unwrap();
817        db.add(Box::new(TestAnalogInput {
818            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
819            name: "AI-1".into(),
820        }))
821        .unwrap();
822        db.add(Box::new(TestAnalogInput {
823            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap(),
824            name: "AI-2".into(),
825        }))
826        .unwrap();
827        db.add(Box::new(TestBinaryValue {
828            oid: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap(),
829            name: "BV-1".into(),
830        }))
831        .unwrap();
832        db
833    }
834
835    fn make_pics_config() -> PicsConfig {
836        PicsConfig {
837            vendor_name: "Acme Corp".into(),
838            model_name: "BACnet Controller 3000".into(),
839            firmware_revision: "1.0.0".into(),
840            application_software_version: "2.0.0".into(),
841            protocol_version: 1,
842            protocol_revision: 24,
843            device_profile: DeviceProfile::BAsc,
844            data_link_layers: vec![DataLinkSupport::BipV4],
845            network_layer: NetworkLayerSupport {
846                router: false,
847                bbmd: false,
848                foreign_device: false,
849            },
850            character_sets: vec![CharacterSet::Utf8],
851            special_functionality: vec!["Intrinsic event reporting".into()],
852        }
853    }
854
855    // ── Tests ──────────────────────────────────────────────────────────
856
857    #[test]
858    fn generate_pics_basic() {
859        let db = make_test_db();
860        let server_config = ServerConfig {
861            vendor_id: 999,
862            ..ServerConfig::default()
863        };
864        let pics_config = make_pics_config();
865        let pics = generate_pics(&db, &server_config, &pics_config);
866
867        assert_eq!(pics.vendor_info.vendor_id, 999);
868        assert_eq!(pics.vendor_info.vendor_name, "Acme Corp");
869        assert_eq!(pics.device_profile, DeviceProfile::BAsc);
870        assert_eq!(pics.character_sets, vec![CharacterSet::Utf8]);
871        assert_eq!(pics.data_link_layers, vec![DataLinkSupport::BipV4]);
872    }
873
874    #[test]
875    fn all_object_types_listed() {
876        let db = make_test_db();
877        let server_config = ServerConfig::default();
878        let pics_config = make_pics_config();
879        let pics = generate_pics(&db, &server_config, &pics_config);
880
881        let types: Vec<ObjectType> = pics
882            .supported_object_types
883            .iter()
884            .map(|ot| ot.object_type)
885            .collect();
886        assert!(types.contains(&ObjectType::DEVICE));
887        assert!(types.contains(&ObjectType::ANALOG_INPUT));
888        assert!(types.contains(&ObjectType::BINARY_VALUE));
889        // 3 distinct types in our test DB
890        assert_eq!(types.len(), 3);
891    }
892
893    #[test]
894    fn object_type_properties_populated() {
895        let db = make_test_db();
896        let server_config = ServerConfig::default();
897        let pics_config = make_pics_config();
898        let pics = generate_pics(&db, &server_config, &pics_config);
899
900        let ai = pics
901            .supported_object_types
902            .iter()
903            .find(|ot| ot.object_type == ObjectType::ANALOG_INPUT)
904            .expect("ANALOG_INPUT should be in PICS");
905
906        // AI has 8 properties in our test fixture
907        assert_eq!(ai.supported_properties.len(), 8);
908
909        // PRESENT_VALUE should be read-only for input objects
910        let pv = ai
911            .supported_properties
912            .iter()
913            .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
914            .expect("PRESENT_VALUE should exist");
915        assert!(pv.access.readable);
916        assert!(!pv.access.writable);
917    }
918
919    #[test]
920    fn device_not_createable_or_deleteable() {
921        let db = make_test_db();
922        let server_config = ServerConfig::default();
923        let pics_config = make_pics_config();
924        let pics = generate_pics(&db, &server_config, &pics_config);
925
926        let dev = pics
927            .supported_object_types
928            .iter()
929            .find(|ot| ot.object_type == ObjectType::DEVICE)
930            .expect("DEVICE should be in PICS");
931        assert!(!dev.createable);
932        assert!(!dev.deleteable);
933    }
934
935    #[test]
936    fn services_match_implementation() {
937        let db = make_test_db();
938        let server_config = ServerConfig::default();
939        let pics_config = make_pics_config();
940        let pics = generate_pics(&db, &server_config, &pics_config);
941
942        let service_names: Vec<&str> = pics
943            .supported_services
944            .iter()
945            .map(|s| s.service_name.as_str())
946            .collect();
947
948        // Executor services
949        assert!(service_names.contains(&"ReadProperty"));
950        assert!(service_names.contains(&"WriteProperty"));
951        assert!(service_names.contains(&"ReadPropertyMultiple"));
952        assert!(service_names.contains(&"SubscribeCOV"));
953        assert!(service_names.contains(&"CreateObject"));
954        assert!(service_names.contains(&"DeleteObject"));
955        assert!(service_names.contains(&"WhoIs"));
956
957        // Initiator services
958        assert!(service_names.contains(&"I-Am"));
959        assert!(service_names.contains(&"ConfirmedCOVNotification"));
960
961        // Check initiator/executor flags on ReadProperty
962        let rp = pics
963            .supported_services
964            .iter()
965            .find(|s| s.service_name == "ReadProperty")
966            .expect("ReadProperty should be listed");
967        assert!(!rp.initiator);
968        assert!(rp.executor);
969
970        // I-Am is initiator only
971        let iam = pics
972            .supported_services
973            .iter()
974            .find(|s| s.service_name == "I-Am")
975            .expect("I-Am should be listed");
976        assert!(iam.initiator);
977        assert!(!iam.executor);
978    }
979
980    #[test]
981    fn text_output_contains_key_sections() {
982        let db = make_test_db();
983        let server_config = ServerConfig {
984            vendor_id: 42,
985            ..ServerConfig::default()
986        };
987        let pics_config = make_pics_config();
988        let pics = generate_pics(&db, &server_config, &pics_config);
989        let text = pics.generate_text();
990
991        assert!(text.contains("Protocol Implementation Conformance Statement"));
992        assert!(text.contains("Vendor ID:"));
993        assert!(text.contains("42"));
994        assert!(text.contains("Acme Corp"));
995        assert!(text.contains("B-ASC"));
996        assert!(text.contains("Supported Object Types"));
997        assert!(text.contains("ANALOG_INPUT"));
998        assert!(text.contains("Supported Services"));
999        assert!(text.contains("ReadProperty"));
1000        assert!(text.contains("Data Link Layer Support"));
1001        assert!(text.contains("BACnet/IP (Annex J)"));
1002        assert!(text.contains("Character Sets Supported"));
1003        assert!(text.contains("UTF-8"));
1004        assert!(text.contains("Special Functionality"));
1005        assert!(text.contains("Intrinsic event reporting"));
1006    }
1007
1008    #[test]
1009    fn markdown_output_has_tables() {
1010        let db = make_test_db();
1011        let server_config = ServerConfig::default();
1012        let pics_config = make_pics_config();
1013        let pics = generate_pics(&db, &server_config, &pics_config);
1014        let md = pics.generate_markdown();
1015
1016        assert!(md.contains("# BACnet Protocol Implementation Conformance Statement"));
1017        assert!(md.contains("| Field | Value |"));
1018        assert!(md.contains("| Service | Initiator | Executor |"));
1019        assert!(md.contains("| Property | Access |"));
1020        assert!(md.contains("## Supported Object Types"));
1021        assert!(md.contains("## Supported Services"));
1022    }
1023
1024    #[test]
1025    fn empty_database_produces_empty_object_list() {
1026        let db = ObjectDatabase::new();
1027        let server_config = ServerConfig::default();
1028        let pics_config = PicsConfig::default();
1029        let pics = generate_pics(&db, &server_config, &pics_config);
1030
1031        assert!(pics.supported_object_types.is_empty());
1032        // Services should still be listed (server capability, not DB-dependent)
1033        assert!(!pics.supported_services.is_empty());
1034    }
1035
1036    #[test]
1037    fn device_profile_display() {
1038        assert_eq!(DeviceProfile::BAac.to_string(), "B-AAC");
1039        assert_eq!(DeviceProfile::BAsc.to_string(), "B-ASC");
1040        assert_eq!(DeviceProfile::BOws.to_string(), "B-OWS");
1041        assert_eq!(DeviceProfile::BBc.to_string(), "B-BC");
1042        assert_eq!(DeviceProfile::BOp.to_string(), "B-OP");
1043        assert_eq!(DeviceProfile::BRouter.to_string(), "B-ROUTER");
1044        assert_eq!(DeviceProfile::BGw.to_string(), "B-GW");
1045        assert_eq!(DeviceProfile::BSc.to_string(), "B-SC");
1046        assert_eq!(
1047            DeviceProfile::Custom("MyProfile".into()).to_string(),
1048            "MyProfile"
1049        );
1050    }
1051
1052    #[test]
1053    fn property_access_display() {
1054        let rw = PropertyAccess {
1055            readable: true,
1056            writable: true,
1057            optional: false,
1058        };
1059        assert_eq!(rw.to_string(), "RW");
1060
1061        let ro = PropertyAccess {
1062            readable: true,
1063            writable: false,
1064            optional: true,
1065        };
1066        assert_eq!(ro.to_string(), "RO");
1067
1068        let wo = PropertyAccess {
1069            readable: false,
1070            writable: true,
1071            optional: false,
1072        };
1073        assert_eq!(wo.to_string(), "W");
1074    }
1075
1076    #[test]
1077    fn binary_value_present_value_is_writable() {
1078        let db = make_test_db();
1079        let server_config = ServerConfig::default();
1080        let pics_config = make_pics_config();
1081        let pics = generate_pics(&db, &server_config, &pics_config);
1082
1083        let bv = pics
1084            .supported_object_types
1085            .iter()
1086            .find(|ot| ot.object_type == ObjectType::BINARY_VALUE)
1087            .expect("BINARY_VALUE should be in PICS");
1088
1089        let pv = bv
1090            .supported_properties
1091            .iter()
1092            .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
1093            .expect("PRESENT_VALUE should exist on BV");
1094        assert!(
1095            pv.access.writable,
1096            "BinaryValue PRESENT_VALUE should be writable"
1097        );
1098    }
1099}