Skip to main content

bacnet_server/pics/
mod.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_objects::device::EXECUTED_SERVICES;
11use bacnet_objects::property_metadata::PropertyConformance;
12use bacnet_types::bitstring::ServicesSupported;
13use bacnet_types::enums::{ObjectType, PropertyIdentifier, ServiceSupported};
14use bacnet_types::primitives::PropertyValue;
15
16use crate::server::ServerConfig;
17
18// ───────────────────────────── Data model ──────────────────────────────────
19
20/// Complete PICS document per ASHRAE 135-2020 Annex A.
21#[derive(Debug, Clone)]
22pub struct Pics {
23    pub vendor_info: VendorInfo,
24    pub device_profile: DeviceProfile,
25    pub supported_object_types: Vec<ObjectTypeSupport>,
26    pub supported_services: Vec<ServiceSupport>,
27    pub data_link_layers: Vec<DataLinkSupport>,
28    pub network_layer: NetworkLayerSupport,
29    pub character_sets: Vec<CharacterSet>,
30    pub special_functionality: Vec<String>,
31}
32
33/// Vendor and device identification.
34#[derive(Debug, Clone)]
35pub struct VendorInfo {
36    pub vendor_id: u16,
37    pub vendor_name: String,
38    pub model_name: String,
39    pub firmware_revision: String,
40    pub application_software_version: String,
41    pub protocol_version: u16,
42    pub protocol_revision: u16,
43}
44
45/// BACnet device profile.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum DeviceProfile {
48    /// BACnet Advanced Application Controller.
49    BAac,
50    /// BACnet Application Specific Controller.
51    BAsc,
52    /// BACnet Operator Workstation.
53    BOws,
54    /// BACnet Building Controller.
55    BBc,
56    /// BACnet Operator Panel.
57    BOp,
58    /// BACnet Router.
59    BRouter,
60    /// BACnet Gateway.
61    BGw,
62    /// BACnet Smart Controller.
63    BSc,
64    /// Custom / non-standard profile.
65    Custom(String),
66}
67
68impl fmt::Display for DeviceProfile {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::BAac => f.write_str("B-AAC"),
72            Self::BAsc => f.write_str("B-ASC"),
73            Self::BOws => f.write_str("B-OWS"),
74            Self::BBc => f.write_str("B-BC"),
75            Self::BOp => f.write_str("B-OP"),
76            Self::BRouter => f.write_str("B-ROUTER"),
77            Self::BGw => f.write_str("B-GW"),
78            Self::BSc => f.write_str("B-SC"),
79            Self::Custom(s) => f.write_str(s),
80        }
81    }
82}
83
84/// Property access flags for a supported property.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub struct PropertyAccess {
87    pub readable: bool,
88    pub writable: bool,
89    pub optional: bool,
90}
91
92impl fmt::Display for PropertyAccess {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let r = if self.readable { "R" } else { "" };
95        let w = if self.writable { "W" } else { "" };
96        let o = if self.optional { "O" } else { "" };
97        write!(f, "{r}{w}{o}")
98    }
99}
100
101/// Supported property with its access flags.
102#[derive(Debug, Clone)]
103pub struct PropertySupport {
104    pub property_id: PropertyIdentifier,
105    pub access: PropertyAccess,
106}
107
108/// Object type support declaration.
109#[derive(Debug, Clone)]
110pub struct ObjectTypeSupport {
111    pub object_type: ObjectType,
112    pub createable: bool,
113    pub deleteable: bool,
114    pub supported_properties: Vec<PropertySupport>,
115}
116
117/// Service support declaration.
118#[derive(Debug, Clone)]
119pub struct ServiceSupport {
120    pub service_name: String,
121    pub initiator: bool,
122    pub executor: bool,
123}
124
125/// Data link layer support.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub enum DataLinkSupport {
128    BipV4,
129    BipV6,
130    Mstp,
131    Ethernet,
132    BacnetSc,
133}
134
135impl fmt::Display for DataLinkSupport {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        match self {
138            Self::BipV4 => f.write_str("BACnet/IP (Annex J)"),
139            Self::BipV6 => f.write_str("BACnet/IPv6 (Annex U)"),
140            Self::Mstp => f.write_str("MS/TP (Clause 9)"),
141            Self::Ethernet => f.write_str("BACnet Ethernet (Clause 7)"),
142            Self::BacnetSc => f.write_str("BACnet/SC (Annex AB)"),
143        }
144    }
145}
146
147/// Network layer capabilities.
148#[derive(Debug, Clone)]
149pub struct NetworkLayerSupport {
150    pub router: bool,
151    pub bbmd: bool,
152    pub foreign_device: bool,
153}
154
155/// Character set support.
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub enum CharacterSet {
158    Utf8,
159    Ansi,
160    DbcsIbm,
161    DbcsMs,
162    Jisx0208,
163    Iso8859_1,
164}
165
166impl fmt::Display for CharacterSet {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        match self {
169            Self::Utf8 => f.write_str("UTF-8"),
170            Self::Ansi => f.write_str("ANSI X3.4"),
171            Self::DbcsIbm => f.write_str("IBM/Microsoft DBCS"),
172            Self::DbcsMs => f.write_str("JIS C 6226"),
173            Self::Jisx0208 => f.write_str("JIS X 0208"),
174            Self::Iso8859_1 => f.write_str("ISO 8859-1"),
175        }
176    }
177}
178
179// ────────────────────────────── Configuration ──────────────────────────────
180
181/// Configuration for PICS generation that cannot be inferred from the database.
182#[derive(Debug, Clone)]
183pub struct PicsConfig {
184    pub vendor_name: String,
185    pub model_name: String,
186    pub firmware_revision: String,
187    pub application_software_version: String,
188    pub protocol_version: u16,
189    pub protocol_revision: u16,
190    pub device_profile: DeviceProfile,
191    pub data_link_layers: Vec<DataLinkSupport>,
192    pub network_layer: NetworkLayerSupport,
193    pub character_sets: Vec<CharacterSet>,
194    pub special_functionality: Vec<String>,
195}
196
197impl Default for PicsConfig {
198    fn default() -> Self {
199        Self {
200            vendor_name: String::new(),
201            model_name: String::new(),
202            firmware_revision: String::new(),
203            application_software_version: String::new(),
204            protocol_version: 1,
205            protocol_revision: 24,
206            device_profile: DeviceProfile::BAsc,
207            data_link_layers: vec![DataLinkSupport::BipV4],
208            network_layer: NetworkLayerSupport {
209                router: false,
210                bbmd: false,
211                foreign_device: false,
212            },
213            character_sets: vec![CharacterSet::Utf8],
214            special_functionality: Vec::new(),
215        }
216    }
217}
218
219// ────────────────────────────── Generator ──────────────────────────────────
220
221/// Generates a [`Pics`] document from an [`ObjectDatabase`] and configuration.
222pub struct PicsGenerator<'a> {
223    db: &'a ObjectDatabase,
224    server_config: &'a ServerConfig,
225    pics_config: &'a PicsConfig,
226}
227
228impl<'a> PicsGenerator<'a> {
229    pub fn new(
230        db: &'a ObjectDatabase,
231        server_config: &'a ServerConfig,
232        pics_config: &'a PicsConfig,
233    ) -> Self {
234        Self {
235            db,
236            server_config,
237            pics_config,
238        }
239    }
240
241    /// Generate the complete PICS document.
242    pub fn generate(&self) -> Pics {
243        Pics {
244            vendor_info: self.build_vendor_info(),
245            device_profile: self.pics_config.device_profile.clone(),
246            supported_object_types: self.build_object_types(),
247            supported_services: self.build_services(),
248            data_link_layers: self.pics_config.data_link_layers.clone(),
249            network_layer: self.pics_config.network_layer.clone(),
250            character_sets: self.pics_config.character_sets.clone(),
251            special_functionality: self.pics_config.special_functionality.clone(),
252        }
253    }
254
255    fn build_vendor_info(&self) -> VendorInfo {
256        VendorInfo {
257            vendor_id: self.server_config.vendor_id,
258            vendor_name: self.pics_config.vendor_name.clone(),
259            model_name: self.pics_config.model_name.clone(),
260            firmware_revision: self.pics_config.firmware_revision.clone(),
261            application_software_version: self.pics_config.application_software_version.clone(),
262            protocol_version: self.pics_config.protocol_version,
263            protocol_revision: self.pics_config.protocol_revision,
264        }
265    }
266
267    fn build_object_types(&self) -> Vec<ObjectTypeSupport> {
268        let mut by_type: BTreeMap<u32, Vec<&dyn bacnet_objects::traits::BACnetObject>> =
269            BTreeMap::new();
270        for (_oid, obj) in self.db.iter_objects() {
271            by_type
272                .entry(obj.object_identifier().object_type().to_raw())
273                .or_default()
274                .push(obj);
275        }
276
277        let mut result = Vec::with_capacity(by_type.len());
278        for (raw_type, objects) in &by_type {
279            let object_type = ObjectType::from_raw(*raw_type);
280            let representative = objects[0];
281            let metadata = representative.property_metadata();
282            let supported_properties = if metadata.is_empty() {
283                let all_props = representative.property_list();
284                let required = representative.required_properties();
285                all_props
286                    .iter()
287                    .map(|&property_id| {
288                        let is_required = required.contains(&property_id);
289                        PropertySupport {
290                            property_id,
291                            access: PropertyAccess {
292                                readable: true,
293                                writable: representative.is_writable_property(property_id),
294                                optional: !is_required,
295                            },
296                        }
297                    })
298                    .collect()
299            } else {
300                metadata
301                    .iter()
302                    .map(|row| PropertySupport {
303                        property_id: row.property_identifier,
304                        access: PropertyAccess {
305                            readable: true,
306                            writable: row.write_capability.is_writable(),
307                            optional: row.conformance == PropertyConformance::Optional,
308                        },
309                    })
310                    .collect()
311            };
312
313            let createable = representative.is_createable();
314            let deleteable = representative.is_deleteable();
315
316            result.push(ObjectTypeSupport {
317                object_type,
318                createable,
319                deleteable,
320                supported_properties,
321            });
322        }
323        result
324    }
325
326    /// Build the service support list based on what the server actually handles.
327    /// Services this server initiates (the PICS initiator column): replies
328    /// and notifications constructed outbound by `bacnet-server`. Distinct
329    /// from [`EXECUTED_SERVICES`], which Clause 12.11 ties to execution.
330    const INITIATED_SERVICES: &'static [ServiceSupported] = &[
331        ServiceSupported::I_AM,
332        ServiceSupported::I_HAVE,
333        ServiceSupported::CONFIRMED_COV_NOTIFICATION,
334        ServiceSupported::CONFIRMED_EVENT_NOTIFICATION,
335        ServiceSupported::UNCONFIRMED_COV_NOTIFICATION,
336        ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION,
337        ServiceSupported::CONFIRMED_COV_NOTIFICATION_MULTIPLE,
338        ServiceSupported::UNCONFIRMED_COV_NOTIFICATION_MULTIPLE,
339    ];
340
341    fn build_services(&self) -> Vec<ServiceSupport> {
342        // Prefer the effective Device bit string so runtime modes such as an
343        // explicitly clockless server cannot drift from generated PICS. The
344        // static dispatch contract remains the fallback for databases without
345        // a readable Device service property, filtered by database clock mode.
346        let effective_executed = self
347            .db
348            .iter_objects()
349            .filter(|(oid, _)| oid.object_type() == ObjectType::DEVICE)
350            .find_map(|(_, device)| {
351                match device
352                    .read_property(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED, None)
353                    .ok()?
354                {
355                    PropertyValue::BitString { data, .. } => {
356                        Some(ServicesSupported::from_bacnet(&data))
357                    }
358                    _ => None,
359                }
360            });
361        let mut service_map: BTreeMap<&'static str, (bool, bool)> = BTreeMap::new();
362        let clock_available = self.db.clock_frame().is_some();
363        let executed: Box<dyn Iterator<Item = ServiceSupported> + '_> = match &effective_executed {
364            Some(services) => Box::new(services.iter()),
365            None => Box::new(EXECUTED_SERVICES.iter().copied().filter(move |service| {
366                clock_available
367                    || (*service != ServiceSupported::TIME_SYNCHRONIZATION
368                        && *service != ServiceSupported::UTC_TIME_SYNCHRONIZATION)
369            })),
370        };
371        for service in executed {
372            service_map
373                .entry(service_display_name(service))
374                .or_default()
375                .1 = true;
376        }
377        for service in Self::INITIATED_SERVICES {
378            service_map
379                .entry(service_display_name(*service))
380                .or_default()
381                .0 = true;
382        }
383
384        service_map
385            .into_iter()
386            .map(|(name, (initiator, executor))| ServiceSupport {
387                service_name: name.to_string(),
388                initiator,
389                executor,
390            })
391            .collect()
392    }
393}
394
395// ─────────────────────────── Text output ───────────────────────────────────
396
397impl Pics {
398    /// Render the PICS as human-readable text per Annex A layout.
399    pub fn generate_text(&self) -> String {
400        let mut out = String::with_capacity(4096);
401
402        out.push_str("=== BACnet Protocol Implementation Conformance Statement (PICS) ===\n");
403        out.push_str("    Per ASHRAE 135-2020 Annex A\n\n");
404
405        out.push_str("--- Vendor Information ---\n");
406        out.push_str(&format!(
407            "Vendor ID:                      {}\n",
408            self.vendor_info.vendor_id
409        ));
410        out.push_str(&format!(
411            "Vendor Name:                    {}\n",
412            self.vendor_info.vendor_name
413        ));
414        out.push_str(&format!(
415            "Model Name:                     {}\n",
416            self.vendor_info.model_name
417        ));
418        out.push_str(&format!(
419            "Firmware Revision:              {}\n",
420            self.vendor_info.firmware_revision
421        ));
422        out.push_str(&format!(
423            "Application Software Version:   {}\n",
424            self.vendor_info.application_software_version
425        ));
426        out.push_str(&format!(
427            "Protocol Version:               {}\n",
428            self.vendor_info.protocol_version
429        ));
430        out.push_str(&format!(
431            "Protocol Revision:              {}\n\n",
432            self.vendor_info.protocol_revision
433        ));
434
435        out.push_str("--- BACnet Device Profile ---\n");
436        out.push_str(&format!("Profile: {}\n\n", self.device_profile));
437
438        out.push_str("--- Supported Object Types ---\n");
439        for ot in &self.supported_object_types {
440            out.push_str(&format!(
441                "\n  Object Type: {} (createable={}, deleteable={})\n",
442                ot.object_type, ot.createable, ot.deleteable
443            ));
444            out.push_str("  Properties:\n");
445            for prop in &ot.supported_properties {
446                out.push_str(&format!(
447                    "    {:<40} {}\n",
448                    prop.property_id.to_string(),
449                    prop.access
450                ));
451            }
452        }
453        out.push('\n');
454
455        out.push_str("--- Supported Services ---\n");
456        out.push_str(&format!(
457            "  {:<45} {:>9} {:>9}\n",
458            "Service", "Initiator", "Executor"
459        ));
460        out.push_str(&format!("  {:-<45} {:-<9} {:-<9}\n", "", "", ""));
461        for svc in &self.supported_services {
462            let init = if svc.initiator { "Yes" } else { "No" };
463            let exec = if svc.executor { "Yes" } else { "No" };
464            out.push_str(&format!(
465                "  {:<45} {:>9} {:>9}\n",
466                svc.service_name, init, exec
467            ));
468        }
469        out.push('\n');
470
471        out.push_str("--- Data Link Layer Support ---\n");
472        for dl in &self.data_link_layers {
473            out.push_str(&format!("  {dl}\n"));
474        }
475        out.push('\n');
476
477        out.push_str("--- Network Layer Options ---\n");
478        out.push_str(&format!(
479            "  Router:         {}\n",
480            self.network_layer.router
481        ));
482        out.push_str(&format!("  BBMD:           {}\n", self.network_layer.bbmd));
483        out.push_str(&format!(
484            "  Foreign Device: {}\n\n",
485            self.network_layer.foreign_device
486        ));
487
488        out.push_str("--- Character Sets Supported ---\n");
489        for cs in &self.character_sets {
490            out.push_str(&format!("  {cs}\n"));
491        }
492        out.push('\n');
493
494        if !self.special_functionality.is_empty() {
495            out.push_str("--- Special Functionality ---\n");
496            for sf in &self.special_functionality {
497                out.push_str(&format!("  {sf}\n"));
498            }
499            out.push('\n');
500        }
501
502        out
503    }
504
505    /// Render the PICS as Markdown for documentation.
506    pub fn generate_markdown(&self) -> String {
507        let mut out = String::with_capacity(4096);
508
509        out.push_str("# BACnet Protocol Implementation Conformance Statement (PICS)\n\n");
510        out.push_str("*Per ASHRAE 135-2020 Annex A*\n\n");
511
512        out.push_str("## Vendor Information\n\n");
513        out.push_str("| Field | Value |\n");
514        out.push_str("|-------|-------|\n");
515        out.push_str(&format!("| Vendor ID | {} |\n", self.vendor_info.vendor_id));
516        out.push_str(&format!(
517            "| Vendor Name | {} |\n",
518            self.vendor_info.vendor_name
519        ));
520        out.push_str(&format!(
521            "| Model Name | {} |\n",
522            self.vendor_info.model_name
523        ));
524        out.push_str(&format!(
525            "| Firmware Revision | {} |\n",
526            self.vendor_info.firmware_revision
527        ));
528        out.push_str(&format!(
529            "| Application Software Version | {} |\n",
530            self.vendor_info.application_software_version
531        ));
532        out.push_str(&format!(
533            "| Protocol Version | {} |\n",
534            self.vendor_info.protocol_version
535        ));
536        out.push_str(&format!(
537            "| Protocol Revision | {} |\n\n",
538            self.vendor_info.protocol_revision
539        ));
540
541        out.push_str("## BACnet Device Profile\n\n");
542        out.push_str(&format!("**{}**\n\n", self.device_profile));
543
544        out.push_str("## Supported Object Types\n\n");
545        for ot in &self.supported_object_types {
546            out.push_str(&format!(
547                "### {}\n\n- Createable: {}\n- Deleteable: {}\n\n",
548                ot.object_type, ot.createable, ot.deleteable
549            ));
550            out.push_str("| Property | Access |\n");
551            out.push_str("|----------|--------|\n");
552            for prop in &ot.supported_properties {
553                out.push_str(&format!("| {} | {} |\n", prop.property_id, prop.access));
554            }
555            out.push('\n');
556        }
557
558        out.push_str("## Supported Services\n\n");
559        out.push_str("| Service | Initiator | Executor |\n");
560        out.push_str("|---------|-----------|----------|\n");
561        for svc in &self.supported_services {
562            let init = if svc.initiator { "Yes" } else { "No" };
563            let exec = if svc.executor { "Yes" } else { "No" };
564            out.push_str(&format!("| {} | {} | {} |\n", svc.service_name, init, exec));
565        }
566        out.push('\n');
567
568        out.push_str("## Data Link Layer Support\n\n");
569        for dl in &self.data_link_layers {
570            out.push_str(&format!("- {dl}\n"));
571        }
572        out.push('\n');
573
574        out.push_str("## Network Layer Options\n\n");
575        out.push_str("| Feature | Supported |\n");
576        out.push_str("|---------|-----------|\n");
577        out.push_str(&format!("| Router | {} |\n", self.network_layer.router));
578        out.push_str(&format!("| BBMD | {} |\n", self.network_layer.bbmd));
579        out.push_str(&format!(
580            "| Foreign Device | {} |\n\n",
581            self.network_layer.foreign_device
582        ));
583
584        out.push_str("## Character Sets Supported\n\n");
585        for cs in &self.character_sets {
586            out.push_str(&format!("- {cs}\n"));
587        }
588        out.push('\n');
589
590        if !self.special_functionality.is_empty() {
591            out.push_str("## Special Functionality\n\n");
592            for sf in &self.special_functionality {
593                out.push_str(&format!("- {sf}\n"));
594            }
595            out.push('\n');
596        }
597
598        out
599    }
600}
601
602// ─────────────────────────── Standalone helper ─────────────────────────────
603
604/// Generate a PICS document from an ObjectDatabase and configuration.
605///
606/// This is a convenience function for use without a running BACnetServer.
607pub fn generate_pics(
608    db: &ObjectDatabase,
609    server_config: &ServerConfig,
610    pics_config: &PicsConfig,
611) -> Pics {
612    PicsGenerator::new(db, server_config, pics_config).generate()
613}
614
615// ─────────────────────────────── Tests ─────────────────────────────────────
616
617/// PICS display name for a `BACnetServicesSupported` bit position.
618fn service_display_name(service: ServiceSupported) -> &'static str {
619    match service.to_raw() {
620        0 => "AcknowledgeAlarm",
621        1 => "ConfirmedCOVNotification",
622        2 => "ConfirmedEventNotification",
623        3 => "GetAlarmSummary",
624        4 => "GetEnrollmentSummary",
625        5 => "SubscribeCOV",
626        6 => "AtomicReadFile",
627        7 => "AtomicWriteFile",
628        8 => "AddListElement",
629        9 => "RemoveListElement",
630        10 => "CreateObject",
631        11 => "DeleteObject",
632        12 => "ReadProperty",
633        14 => "ReadPropertyMultiple",
634        15 => "WriteProperty",
635        16 => "WritePropertyMultiple",
636        17 => "DeviceCommunicationControl",
637        18 => "ConfirmedPrivateTransfer",
638        19 => "ConfirmedTextMessage",
639        20 => "ReinitializeDevice",
640        21 => "VT-Open",
641        22 => "VT-Close",
642        23 => "VT-Data",
643        26 => "I-Am",
644        27 => "I-Have",
645        28 => "UnconfirmedCOVNotification",
646        29 => "UnconfirmedEventNotification",
647        30 => "UnconfirmedPrivateTransfer",
648        31 => "UnconfirmedTextMessage",
649        32 => "TimeSynchronization",
650        33 => "WhoHas",
651        34 => "WhoIs",
652        35 => "ReadRange",
653        36 => "UTCTimeSynchronization",
654        37 => "LifeSafetyOperation",
655        38 => "SubscribeCOVProperty",
656        39 => "GetEventInformation",
657        40 => "WriteGroup",
658        41 => "SubscribeCOVPropertyMultiple",
659        42 => "ConfirmedCOVNotificationMultiple",
660        43 => "UnconfirmedCOVNotificationMultiple",
661        44 => "ConfirmedAuditNotification",
662        45 => "AuditLogQuery",
663        46 => "UnconfirmedAuditNotification",
664        47 => "Who-Am-I",
665        48 => "You-Are",
666        _ => "Unknown",
667    }
668}
669
670#[cfg(test)]
671mod tests;
672
673#[cfg(test)]
674mod property_metadata_tests;
675
676#[cfg(test)]
677mod acked_transitions_policy_tests;
678
679#[cfg(test)]
680mod truth_source_tests;