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.
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.
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.
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.
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.
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.
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.
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        let mut by_type: BTreeMap<u32, Vec<&dyn bacnet_objects::traits::BACnetObject>> =
265            BTreeMap::new();
266        for (_oid, obj) in self.db.iter_objects() {
267            by_type
268                .entry(obj.object_identifier().object_type().to_raw())
269                .or_default()
270                .push(obj);
271        }
272
273        let mut result = Vec::with_capacity(by_type.len());
274        for (raw_type, objects) in &by_type {
275            let object_type = ObjectType::from_raw(*raw_type);
276            let representative = objects[0];
277            let all_props = representative.property_list();
278            let required = representative.required_properties();
279
280            let supported_properties = all_props
281                .iter()
282                .map(|&pid| {
283                    let is_required = required.contains(&pid);
284                    let writable = Self::is_writable_property(object_type, pid);
285                    PropertySupport {
286                        property_id: pid,
287                        access: PropertyAccess {
288                            readable: true,
289                            writable,
290                            optional: !is_required,
291                        },
292                    }
293                })
294                .collect();
295
296            let createable = Self::is_createable(object_type);
297            let deleteable = Self::is_deleteable(object_type);
298
299            result.push(ObjectTypeSupport {
300                object_type,
301                createable,
302                deleteable,
303                supported_properties,
304            });
305        }
306        result
307    }
308
309    /// Heuristic for commonly writable properties.
310    fn is_writable_property(object_type: ObjectType, pid: PropertyIdentifier) -> bool {
311        if pid == PropertyIdentifier::OBJECT_IDENTIFIER
312            || pid == PropertyIdentifier::OBJECT_TYPE
313            || pid == PropertyIdentifier::PROPERTY_LIST
314            || pid == PropertyIdentifier::STATUS_FLAGS
315        {
316            return false;
317        }
318
319        if pid == PropertyIdentifier::OBJECT_NAME {
320            return true;
321        }
322
323        if pid == PropertyIdentifier::PRESENT_VALUE {
324            return object_type != ObjectType::ANALOG_INPUT
325                && object_type != ObjectType::BINARY_INPUT
326                && object_type != ObjectType::MULTI_STATE_INPUT;
327        }
328
329        pid == PropertyIdentifier::DESCRIPTION
330            || pid == PropertyIdentifier::OUT_OF_SERVICE
331            || pid == PropertyIdentifier::COV_INCREMENT
332            || pid == PropertyIdentifier::HIGH_LIMIT
333            || pid == PropertyIdentifier::LOW_LIMIT
334            || pid == PropertyIdentifier::DEADBAND
335            || pid == PropertyIdentifier::NOTIFICATION_CLASS
336    }
337
338    fn is_createable(object_type: ObjectType) -> bool {
339        object_type != ObjectType::DEVICE && object_type != ObjectType::NETWORK_PORT
340    }
341
342    fn is_deleteable(object_type: ObjectType) -> bool {
343        object_type != ObjectType::DEVICE && object_type != ObjectType::NETWORK_PORT
344    }
345
346    /// Build the service support list based on what the server actually handles.
347    fn build_services(&self) -> Vec<ServiceSupport> {
348        let mut services = Vec::new();
349
350        let executor_services = [
351            "ReadProperty",
352            "WriteProperty",
353            "ReadPropertyMultiple",
354            "WritePropertyMultiple",
355            "SubscribeCOV",
356            "SubscribeCOVProperty",
357            "CreateObject",
358            "DeleteObject",
359            "DeviceCommunicationControl",
360            "ReinitializeDevice",
361            "GetEventInformation",
362            "AcknowledgeAlarm",
363            "ReadRange",
364            "AtomicReadFile",
365            "AtomicWriteFile",
366            "AddListElement",
367            "RemoveListElement",
368        ];
369
370        let initiator_services = ["ConfirmedCOVNotification", "ConfirmedEventNotification"];
371
372        let unconfirmed_executor = [
373            "WhoIs",
374            "WhoHas",
375            "TimeSynchronization",
376            "UTCTimeSynchronization",
377        ];
378
379        let unconfirmed_initiator = [
380            "I-Am",
381            "I-Have",
382            "UnconfirmedCOVNotification",
383            "UnconfirmedEventNotification",
384        ];
385
386        let mut service_map: BTreeMap<&str, (bool, bool)> = BTreeMap::new();
387        for name in &executor_services {
388            service_map.entry(name).or_default().1 = true;
389        }
390        for name in &initiator_services {
391            service_map.entry(name).or_default().0 = true;
392        }
393        for name in &unconfirmed_executor {
394            service_map.entry(name).or_default().1 = true;
395        }
396        for name in &unconfirmed_initiator {
397            service_map.entry(name).or_default().0 = true;
398        }
399
400        for (name, (initiator, executor)) in &service_map {
401            services.push(ServiceSupport {
402                service_name: (*name).to_string(),
403                initiator: *initiator,
404                executor: *executor,
405            });
406        }
407
408        services
409    }
410}
411
412// ─────────────────────────── Text output ───────────────────────────────────
413
414impl Pics {
415    /// Render the PICS as human-readable text per Annex A layout.
416    pub fn generate_text(&self) -> String {
417        let mut out = String::with_capacity(4096);
418
419        out.push_str("=== BACnet Protocol Implementation Conformance Statement (PICS) ===\n");
420        out.push_str("    Per ASHRAE 135-2020 Annex A\n\n");
421
422        out.push_str("--- Vendor Information ---\n");
423        out.push_str(&format!(
424            "Vendor ID:                      {}\n",
425            self.vendor_info.vendor_id
426        ));
427        out.push_str(&format!(
428            "Vendor Name:                    {}\n",
429            self.vendor_info.vendor_name
430        ));
431        out.push_str(&format!(
432            "Model Name:                     {}\n",
433            self.vendor_info.model_name
434        ));
435        out.push_str(&format!(
436            "Firmware Revision:              {}\n",
437            self.vendor_info.firmware_revision
438        ));
439        out.push_str(&format!(
440            "Application Software Version:   {}\n",
441            self.vendor_info.application_software_version
442        ));
443        out.push_str(&format!(
444            "Protocol Version:               {}\n",
445            self.vendor_info.protocol_version
446        ));
447        out.push_str(&format!(
448            "Protocol Revision:              {}\n\n",
449            self.vendor_info.protocol_revision
450        ));
451
452        out.push_str("--- BACnet Device Profile ---\n");
453        out.push_str(&format!("Profile: {}\n\n", self.device_profile));
454
455        out.push_str("--- Supported Object Types ---\n");
456        for ot in &self.supported_object_types {
457            out.push_str(&format!(
458                "\n  Object Type: {} (createable={}, deleteable={})\n",
459                ot.object_type, ot.createable, ot.deleteable
460            ));
461            out.push_str("  Properties:\n");
462            for prop in &ot.supported_properties {
463                out.push_str(&format!(
464                    "    {:<40} {}\n",
465                    prop.property_id.to_string(),
466                    prop.access
467                ));
468            }
469        }
470        out.push('\n');
471
472        out.push_str("--- Supported Services ---\n");
473        out.push_str(&format!(
474            "  {:<45} {:>9} {:>9}\n",
475            "Service", "Initiator", "Executor"
476        ));
477        out.push_str(&format!("  {:-<45} {:-<9} {:-<9}\n", "", "", ""));
478        for svc in &self.supported_services {
479            let init = if svc.initiator { "Yes" } else { "No" };
480            let exec = if svc.executor { "Yes" } else { "No" };
481            out.push_str(&format!(
482                "  {:<45} {:>9} {:>9}\n",
483                svc.service_name, init, exec
484            ));
485        }
486        out.push('\n');
487
488        out.push_str("--- Data Link Layer Support ---\n");
489        for dl in &self.data_link_layers {
490            out.push_str(&format!("  {dl}\n"));
491        }
492        out.push('\n');
493
494        out.push_str("--- Network Layer Options ---\n");
495        out.push_str(&format!(
496            "  Router:         {}\n",
497            self.network_layer.router
498        ));
499        out.push_str(&format!("  BBMD:           {}\n", self.network_layer.bbmd));
500        out.push_str(&format!(
501            "  Foreign Device: {}\n\n",
502            self.network_layer.foreign_device
503        ));
504
505        out.push_str("--- Character Sets Supported ---\n");
506        for cs in &self.character_sets {
507            out.push_str(&format!("  {cs}\n"));
508        }
509        out.push('\n');
510
511        if !self.special_functionality.is_empty() {
512            out.push_str("--- Special Functionality ---\n");
513            for sf in &self.special_functionality {
514                out.push_str(&format!("  {sf}\n"));
515            }
516            out.push('\n');
517        }
518
519        out
520    }
521
522    /// Render the PICS as Markdown for documentation.
523    pub fn generate_markdown(&self) -> String {
524        let mut out = String::with_capacity(4096);
525
526        out.push_str("# BACnet Protocol Implementation Conformance Statement (PICS)\n\n");
527        out.push_str("*Per ASHRAE 135-2020 Annex A*\n\n");
528
529        out.push_str("## Vendor Information\n\n");
530        out.push_str("| Field | Value |\n");
531        out.push_str("|-------|-------|\n");
532        out.push_str(&format!("| Vendor ID | {} |\n", self.vendor_info.vendor_id));
533        out.push_str(&format!(
534            "| Vendor Name | {} |\n",
535            self.vendor_info.vendor_name
536        ));
537        out.push_str(&format!(
538            "| Model Name | {} |\n",
539            self.vendor_info.model_name
540        ));
541        out.push_str(&format!(
542            "| Firmware Revision | {} |\n",
543            self.vendor_info.firmware_revision
544        ));
545        out.push_str(&format!(
546            "| Application Software Version | {} |\n",
547            self.vendor_info.application_software_version
548        ));
549        out.push_str(&format!(
550            "| Protocol Version | {} |\n",
551            self.vendor_info.protocol_version
552        ));
553        out.push_str(&format!(
554            "| Protocol Revision | {} |\n\n",
555            self.vendor_info.protocol_revision
556        ));
557
558        out.push_str("## BACnet Device Profile\n\n");
559        out.push_str(&format!("**{}**\n\n", self.device_profile));
560
561        out.push_str("## Supported Object Types\n\n");
562        for ot in &self.supported_object_types {
563            out.push_str(&format!(
564                "### {}\n\n- Createable: {}\n- Deleteable: {}\n\n",
565                ot.object_type, ot.createable, ot.deleteable
566            ));
567            out.push_str("| Property | Access |\n");
568            out.push_str("|----------|--------|\n");
569            for prop in &ot.supported_properties {
570                out.push_str(&format!("| {} | {} |\n", prop.property_id, prop.access));
571            }
572            out.push('\n');
573        }
574
575        out.push_str("## Supported Services\n\n");
576        out.push_str("| Service | Initiator | Executor |\n");
577        out.push_str("|---------|-----------|----------|\n");
578        for svc in &self.supported_services {
579            let init = if svc.initiator { "Yes" } else { "No" };
580            let exec = if svc.executor { "Yes" } else { "No" };
581            out.push_str(&format!("| {} | {} | {} |\n", svc.service_name, init, exec));
582        }
583        out.push('\n');
584
585        out.push_str("## Data Link Layer Support\n\n");
586        for dl in &self.data_link_layers {
587            out.push_str(&format!("- {dl}\n"));
588        }
589        out.push('\n');
590
591        out.push_str("## Network Layer Options\n\n");
592        out.push_str("| Feature | Supported |\n");
593        out.push_str("|---------|-----------|\n");
594        out.push_str(&format!("| Router | {} |\n", self.network_layer.router));
595        out.push_str(&format!("| BBMD | {} |\n", self.network_layer.bbmd));
596        out.push_str(&format!(
597            "| Foreign Device | {} |\n\n",
598            self.network_layer.foreign_device
599        ));
600
601        out.push_str("## Character Sets Supported\n\n");
602        for cs in &self.character_sets {
603            out.push_str(&format!("- {cs}\n"));
604        }
605        out.push('\n');
606
607        if !self.special_functionality.is_empty() {
608            out.push_str("## Special Functionality\n\n");
609            for sf in &self.special_functionality {
610                out.push_str(&format!("- {sf}\n"));
611            }
612            out.push('\n');
613        }
614
615        out
616    }
617}
618
619// ─────────────────────────── Standalone helper ─────────────────────────────
620
621/// Generate a PICS document from an ObjectDatabase and configuration.
622///
623/// This is a convenience function for use without a running BACnetServer.
624pub fn generate_pics(
625    db: &ObjectDatabase,
626    server_config: &ServerConfig,
627    pics_config: &PicsConfig,
628) -> Pics {
629    PicsGenerator::new(db, server_config, pics_config).generate()
630}
631
632// ─────────────────────────────── Tests ─────────────────────────────────────
633
634#[cfg(test)]
635mod tests {
636    use std::borrow::Cow;
637
638    use bacnet_objects::traits::BACnetObject;
639    use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
640    use bacnet_types::error::Error;
641    use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
642
643    use super::*;
644
645    // ── Minimal test objects ───────────────────────────────────────────
646
647    struct TestAnalogInput {
648        oid: ObjectIdentifier,
649        name: String,
650    }
651
652    impl BACnetObject for TestAnalogInput {
653        fn object_identifier(&self) -> ObjectIdentifier {
654            self.oid
655        }
656        fn object_name(&self) -> &str {
657            &self.name
658        }
659        fn read_property(
660            &self,
661            _property: PropertyIdentifier,
662            _array_index: Option<u32>,
663        ) -> Result<PropertyValue, Error> {
664            Ok(PropertyValue::Real(0.0))
665        }
666        fn write_property(
667            &mut self,
668            _property: PropertyIdentifier,
669            _array_index: Option<u32>,
670            _value: PropertyValue,
671            _priority: Option<u8>,
672        ) -> Result<(), Error> {
673            Err(Error::Protocol {
674                class: ErrorClass::PROPERTY.to_raw() as u32,
675                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
676            })
677        }
678        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
679            static PROPS: [PropertyIdentifier; 8] = [
680                PropertyIdentifier::OBJECT_IDENTIFIER,
681                PropertyIdentifier::OBJECT_NAME,
682                PropertyIdentifier::OBJECT_TYPE,
683                PropertyIdentifier::PROPERTY_LIST,
684                PropertyIdentifier::PRESENT_VALUE,
685                PropertyIdentifier::STATUS_FLAGS,
686                PropertyIdentifier::OUT_OF_SERVICE,
687                PropertyIdentifier::UNITS,
688            ];
689            Cow::Borrowed(&PROPS)
690        }
691    }
692
693    struct TestBinaryValue {
694        oid: ObjectIdentifier,
695        name: String,
696    }
697
698    impl BACnetObject for TestBinaryValue {
699        fn object_identifier(&self) -> ObjectIdentifier {
700            self.oid
701        }
702        fn object_name(&self) -> &str {
703            &self.name
704        }
705        fn read_property(
706            &self,
707            _property: PropertyIdentifier,
708            _array_index: Option<u32>,
709        ) -> Result<PropertyValue, Error> {
710            Ok(PropertyValue::Boolean(false))
711        }
712        fn write_property(
713            &mut self,
714            _property: PropertyIdentifier,
715            _array_index: Option<u32>,
716            _value: PropertyValue,
717            _priority: Option<u8>,
718        ) -> Result<(), Error> {
719            Ok(())
720        }
721        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
722            static PROPS: [PropertyIdentifier; 6] = [
723                PropertyIdentifier::OBJECT_IDENTIFIER,
724                PropertyIdentifier::OBJECT_NAME,
725                PropertyIdentifier::OBJECT_TYPE,
726                PropertyIdentifier::PROPERTY_LIST,
727                PropertyIdentifier::PRESENT_VALUE,
728                PropertyIdentifier::STATUS_FLAGS,
729            ];
730            Cow::Borrowed(&PROPS)
731        }
732    }
733
734    struct TestDevice {
735        oid: ObjectIdentifier,
736        name: String,
737    }
738
739    impl BACnetObject for TestDevice {
740        fn object_identifier(&self) -> ObjectIdentifier {
741            self.oid
742        }
743        fn object_name(&self) -> &str {
744            &self.name
745        }
746        fn read_property(
747            &self,
748            _property: PropertyIdentifier,
749            _array_index: Option<u32>,
750        ) -> Result<PropertyValue, Error> {
751            Ok(PropertyValue::Unsigned(0))
752        }
753        fn write_property(
754            &mut self,
755            _property: PropertyIdentifier,
756            _array_index: Option<u32>,
757            _value: PropertyValue,
758            _priority: Option<u8>,
759        ) -> Result<(), Error> {
760            Err(Error::Protocol {
761                class: ErrorClass::PROPERTY.to_raw() as u32,
762                code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
763            })
764        }
765        fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
766            static PROPS: [PropertyIdentifier; 6] = [
767                PropertyIdentifier::OBJECT_IDENTIFIER,
768                PropertyIdentifier::OBJECT_NAME,
769                PropertyIdentifier::OBJECT_TYPE,
770                PropertyIdentifier::PROPERTY_LIST,
771                PropertyIdentifier::PROTOCOL_VERSION,
772                PropertyIdentifier::PROTOCOL_REVISION,
773            ];
774            Cow::Borrowed(&PROPS)
775        }
776    }
777
778    // ── Helpers ────────────────────────────────────────────────────────
779
780    fn make_test_db() -> ObjectDatabase {
781        let mut db = ObjectDatabase::new();
782        db.add(Box::new(TestDevice {
783            oid: ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap(),
784            name: "Test Device".into(),
785        }))
786        .unwrap();
787        db.add(Box::new(TestAnalogInput {
788            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
789            name: "AI-1".into(),
790        }))
791        .unwrap();
792        db.add(Box::new(TestAnalogInput {
793            oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap(),
794            name: "AI-2".into(),
795        }))
796        .unwrap();
797        db.add(Box::new(TestBinaryValue {
798            oid: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap(),
799            name: "BV-1".into(),
800        }))
801        .unwrap();
802        db
803    }
804
805    fn make_pics_config() -> PicsConfig {
806        PicsConfig {
807            vendor_name: "Acme Corp".into(),
808            model_name: "BACnet Controller 3000".into(),
809            firmware_revision: "1.0.0".into(),
810            application_software_version: "2.0.0".into(),
811            protocol_version: 1,
812            protocol_revision: 24,
813            device_profile: DeviceProfile::BAsc,
814            data_link_layers: vec![DataLinkSupport::BipV4],
815            network_layer: NetworkLayerSupport {
816                router: false,
817                bbmd: false,
818                foreign_device: false,
819            },
820            character_sets: vec![CharacterSet::Utf8],
821            special_functionality: vec!["Intrinsic event reporting".into()],
822        }
823    }
824
825    // ── Tests ──────────────────────────────────────────────────────────
826
827    #[test]
828    fn generate_pics_basic() {
829        let db = make_test_db();
830        let server_config = ServerConfig {
831            vendor_id: 999,
832            ..ServerConfig::default()
833        };
834        let pics_config = make_pics_config();
835        let pics = generate_pics(&db, &server_config, &pics_config);
836
837        assert_eq!(pics.vendor_info.vendor_id, 999);
838        assert_eq!(pics.vendor_info.vendor_name, "Acme Corp");
839        assert_eq!(pics.device_profile, DeviceProfile::BAsc);
840        assert_eq!(pics.character_sets, vec![CharacterSet::Utf8]);
841        assert_eq!(pics.data_link_layers, vec![DataLinkSupport::BipV4]);
842    }
843
844    #[test]
845    fn all_object_types_listed() {
846        let db = make_test_db();
847        let server_config = ServerConfig::default();
848        let pics_config = make_pics_config();
849        let pics = generate_pics(&db, &server_config, &pics_config);
850
851        let types: Vec<ObjectType> = pics
852            .supported_object_types
853            .iter()
854            .map(|ot| ot.object_type)
855            .collect();
856        assert!(types.contains(&ObjectType::DEVICE));
857        assert!(types.contains(&ObjectType::ANALOG_INPUT));
858        assert!(types.contains(&ObjectType::BINARY_VALUE));
859        // 3 distinct types in our test DB
860        assert_eq!(types.len(), 3);
861    }
862
863    #[test]
864    fn object_type_properties_populated() {
865        let db = make_test_db();
866        let server_config = ServerConfig::default();
867        let pics_config = make_pics_config();
868        let pics = generate_pics(&db, &server_config, &pics_config);
869
870        let ai = pics
871            .supported_object_types
872            .iter()
873            .find(|ot| ot.object_type == ObjectType::ANALOG_INPUT)
874            .expect("ANALOG_INPUT should be in PICS");
875
876        // AI has 8 properties in our test fixture
877        assert_eq!(ai.supported_properties.len(), 8);
878
879        // PRESENT_VALUE should be read-only for input objects
880        let pv = ai
881            .supported_properties
882            .iter()
883            .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
884            .expect("PRESENT_VALUE should exist");
885        assert!(pv.access.readable);
886        assert!(!pv.access.writable);
887    }
888
889    #[test]
890    fn device_not_createable_or_deleteable() {
891        let db = make_test_db();
892        let server_config = ServerConfig::default();
893        let pics_config = make_pics_config();
894        let pics = generate_pics(&db, &server_config, &pics_config);
895
896        let dev = pics
897            .supported_object_types
898            .iter()
899            .find(|ot| ot.object_type == ObjectType::DEVICE)
900            .expect("DEVICE should be in PICS");
901        assert!(!dev.createable);
902        assert!(!dev.deleteable);
903    }
904
905    #[test]
906    fn services_match_implementation() {
907        let db = make_test_db();
908        let server_config = ServerConfig::default();
909        let pics_config = make_pics_config();
910        let pics = generate_pics(&db, &server_config, &pics_config);
911
912        let service_names: Vec<&str> = pics
913            .supported_services
914            .iter()
915            .map(|s| s.service_name.as_str())
916            .collect();
917
918        // Executor services
919        assert!(service_names.contains(&"ReadProperty"));
920        assert!(service_names.contains(&"WriteProperty"));
921        assert!(service_names.contains(&"ReadPropertyMultiple"));
922        assert!(service_names.contains(&"SubscribeCOV"));
923        assert!(service_names.contains(&"CreateObject"));
924        assert!(service_names.contains(&"DeleteObject"));
925        assert!(service_names.contains(&"WhoIs"));
926
927        // Initiator services
928        assert!(service_names.contains(&"I-Am"));
929        assert!(service_names.contains(&"ConfirmedCOVNotification"));
930
931        // Check initiator/executor flags on ReadProperty
932        let rp = pics
933            .supported_services
934            .iter()
935            .find(|s| s.service_name == "ReadProperty")
936            .expect("ReadProperty should be listed");
937        assert!(!rp.initiator);
938        assert!(rp.executor);
939
940        // I-Am is initiator only
941        let iam = pics
942            .supported_services
943            .iter()
944            .find(|s| s.service_name == "I-Am")
945            .expect("I-Am should be listed");
946        assert!(iam.initiator);
947        assert!(!iam.executor);
948    }
949
950    #[test]
951    fn text_output_contains_key_sections() {
952        let db = make_test_db();
953        let server_config = ServerConfig {
954            vendor_id: 42,
955            ..ServerConfig::default()
956        };
957        let pics_config = make_pics_config();
958        let pics = generate_pics(&db, &server_config, &pics_config);
959        let text = pics.generate_text();
960
961        assert!(text.contains("Protocol Implementation Conformance Statement"));
962        assert!(text.contains("Vendor ID:"));
963        assert!(text.contains("42"));
964        assert!(text.contains("Acme Corp"));
965        assert!(text.contains("B-ASC"));
966        assert!(text.contains("Supported Object Types"));
967        assert!(text.contains("ANALOG_INPUT"));
968        assert!(text.contains("Supported Services"));
969        assert!(text.contains("ReadProperty"));
970        assert!(text.contains("Data Link Layer Support"));
971        assert!(text.contains("BACnet/IP (Annex J)"));
972        assert!(text.contains("Character Sets Supported"));
973        assert!(text.contains("UTF-8"));
974        assert!(text.contains("Special Functionality"));
975        assert!(text.contains("Intrinsic event reporting"));
976    }
977
978    #[test]
979    fn markdown_output_has_tables() {
980        let db = make_test_db();
981        let server_config = ServerConfig::default();
982        let pics_config = make_pics_config();
983        let pics = generate_pics(&db, &server_config, &pics_config);
984        let md = pics.generate_markdown();
985
986        assert!(md.contains("# BACnet Protocol Implementation Conformance Statement"));
987        assert!(md.contains("| Field | Value |"));
988        assert!(md.contains("| Service | Initiator | Executor |"));
989        assert!(md.contains("| Property | Access |"));
990        assert!(md.contains("## Supported Object Types"));
991        assert!(md.contains("## Supported Services"));
992    }
993
994    #[test]
995    fn empty_database_produces_empty_object_list() {
996        let db = ObjectDatabase::new();
997        let server_config = ServerConfig::default();
998        let pics_config = PicsConfig::default();
999        let pics = generate_pics(&db, &server_config, &pics_config);
1000
1001        assert!(pics.supported_object_types.is_empty());
1002        assert!(!pics.supported_services.is_empty());
1003    }
1004
1005    #[test]
1006    fn device_profile_display() {
1007        assert_eq!(DeviceProfile::BAac.to_string(), "B-AAC");
1008        assert_eq!(DeviceProfile::BAsc.to_string(), "B-ASC");
1009        assert_eq!(DeviceProfile::BOws.to_string(), "B-OWS");
1010        assert_eq!(DeviceProfile::BBc.to_string(), "B-BC");
1011        assert_eq!(DeviceProfile::BOp.to_string(), "B-OP");
1012        assert_eq!(DeviceProfile::BRouter.to_string(), "B-ROUTER");
1013        assert_eq!(DeviceProfile::BGw.to_string(), "B-GW");
1014        assert_eq!(DeviceProfile::BSc.to_string(), "B-SC");
1015        assert_eq!(
1016            DeviceProfile::Custom("MyProfile".into()).to_string(),
1017            "MyProfile"
1018        );
1019    }
1020
1021    #[test]
1022    fn property_access_display() {
1023        let rw = PropertyAccess {
1024            readable: true,
1025            writable: true,
1026            optional: false,
1027        };
1028        assert_eq!(rw.to_string(), "RW");
1029
1030        let ro = PropertyAccess {
1031            readable: true,
1032            writable: false,
1033            optional: true,
1034        };
1035        assert_eq!(ro.to_string(), "RO");
1036
1037        let wo = PropertyAccess {
1038            readable: false,
1039            writable: true,
1040            optional: false,
1041        };
1042        assert_eq!(wo.to_string(), "W");
1043    }
1044
1045    #[test]
1046    fn binary_value_present_value_is_writable() {
1047        let db = make_test_db();
1048        let server_config = ServerConfig::default();
1049        let pics_config = make_pics_config();
1050        let pics = generate_pics(&db, &server_config, &pics_config);
1051
1052        let bv = pics
1053            .supported_object_types
1054            .iter()
1055            .find(|ot| ot.object_type == ObjectType::BINARY_VALUE)
1056            .expect("BINARY_VALUE should be in PICS");
1057
1058        let pv = bv
1059            .supported_properties
1060            .iter()
1061            .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
1062            .expect("PRESENT_VALUE should exist on BV");
1063        assert!(
1064            pv.access.writable,
1065            "BinaryValue PRESENT_VALUE should be writable"
1066        );
1067    }
1068}