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_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;