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_encoding::primitives::charset;
10use bacnet_objects::database::ObjectDatabase;
11use bacnet_types::bitstring::ServicesSupported;
12use bacnet_types::enums::{ObjectType, PropertyIdentifier, ServiceSupported};
13use bacnet_types::primitives::PropertyValue;
14
15use crate::server::ServerConfig;
16
17const PROPERTY_CAPABILITIES_EXPLANATION: &str = "Property rows aggregate configured instances: a row or access flag means at least one instance supports it. Actual availability and access depend on the concrete object. Optional is the metadata conformance classification; a required declaration wins, and absent rows do not vote.";
18
19/// How the text and Markdown PICS introduce a type's
20/// [`ObjectTypeSupport::creation_only_properties`]: read-only to
21/// WriteProperty as a whole, but a CreateObject initial value sets them.
22const CREATION_ONLY_LABEL: &str = "Whole value set only by CreateObject";
23
24/// A row's access as the text and Markdown PICS print it: the flags, and for
25/// a row writes of another property change
26/// ([`PropertySupport::written_through`]) a note saying so, since no write
27/// naming the row itself is taken (#1443).
28fn access_text(row: &PropertySupport) -> String {
29    match row.written_through {
30        Some(through) => format!(
31            "{} (resized through {through}: a whole write or its size at index 0)",
32            row.access
33        ),
34        None => row.access.to_string(),
35    }
36}
37
38/// The type's creation-only properties, comma-separated, or `None` if it
39/// has none.
40fn creation_only_names(support: &ObjectTypeSupport) -> Option<String> {
41    let names: Vec<_> = support
42        .creation_only_properties
43        .iter()
44        .map(ToString::to_string)
45        .collect();
46    (!names.is_empty()).then(|| names.join(", "))
47}
48
49// ───────────────────────────── Data model ──────────────────────────────────
50
51/// Complete PICS document per ASHRAE 135-2020 Annex A.
52#[derive(Debug, Clone)]
53pub struct Pics {
54    /// Vendor and device identification.
55    pub vendor_info: VendorInfo,
56    /// Device profile claimed.
57    pub device_profile: DeviceProfile,
58    /// Object types present in the database with their creation, deletion and property support.
59    pub supported_object_types: Vec<ObjectTypeSupport>,
60    /// Services the server can initiate or execute.
61    pub supported_services: Vec<ServiceSupport>,
62    /// Data-link layers the device supports.
63    pub data_link_layers: Vec<DataLinkSupport>,
64    /// Network-layer roles the device fills.
65    pub network_layer: NetworkLayerSupport,
66    /// Character sets the device supports.
67    pub character_sets: Vec<CharacterSet>,
68    /// Free-text descriptions of special functionality.
69    pub special_functionality: Vec<String>,
70}
71
72/// Vendor and device identification.
73#[derive(Debug, Clone)]
74pub struct VendorInfo {
75    /// Vendor identifier assigned by ASHRAE.
76    pub vendor_id: u16,
77    /// Vendor name.
78    pub vendor_name: String,
79    /// Product model name.
80    pub model_name: String,
81    /// Firmware revision string.
82    pub firmware_revision: String,
83    /// Application software version string.
84    pub application_software_version: String,
85    /// BACnet protocol version implemented.
86    pub protocol_version: u16,
87    /// BACnet protocol revision implemented.
88    pub protocol_revision: u16,
89}
90
91/// BACnet device profile.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum DeviceProfile {
94    /// BACnet Advanced Application Controller.
95    BAac,
96    /// BACnet Application Specific Controller.
97    BAsc,
98    /// BACnet Operator Workstation.
99    BOws,
100    /// BACnet Building Controller.
101    BBc,
102    /// BACnet Operator Panel.
103    BOp,
104    /// BACnet Router.
105    BRouter,
106    /// BACnet Gateway.
107    BGw,
108    /// BACnet Smart Controller.
109    BSc,
110    /// Custom / non-standard profile.
111    Custom(String),
112}
113
114impl fmt::Display for DeviceProfile {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        match self {
117            Self::BAac => f.write_str("B-AAC"),
118            Self::BAsc => f.write_str("B-ASC"),
119            Self::BOws => f.write_str("B-OWS"),
120            Self::BBc => f.write_str("B-BC"),
121            Self::BOp => f.write_str("B-OP"),
122            Self::BRouter => f.write_str("B-ROUTER"),
123            Self::BGw => f.write_str("B-GW"),
124            Self::BSc => f.write_str("B-SC"),
125            Self::Custom(s) => f.write_str(s),
126        }
127    }
128}
129
130/// Property capabilities across configured instances of an object type.
131/// Read/write flags mean at least one instance supports that access. Optional is
132/// true only when every present metadata row is optional; absent rows do not vote.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct PropertyAccess {
135    /// At least one instance can be read.
136    pub readable: bool,
137    /// At least one instance can be written.
138    pub writable: bool,
139    /// Every instance that has the property treats it as optional.
140    pub optional: bool,
141}
142
143impl fmt::Display for PropertyAccess {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        let r = if self.readable { "R" } else { "" };
146        let w = if self.writable { "W" } else { "" };
147        let o = if self.optional { "O" } else { "" };
148        write!(f, "{r}{w}{o}")
149    }
150}
151
152/// Supported property with its access flags.
153#[derive(Debug, Clone)]
154pub struct PropertySupport {
155    /// Property this row describes.
156    pub property_id: PropertyIdentifier,
157    /// Aggregated access flags for the property.
158    pub access: PropertyAccess,
159    /// The property whose writes change this one on some instance, although
160    /// a write naming this one is refused: State_Text, written whole or at
161    /// index 0, for a multi-state object's Number_Of_States (#1443).
162    pub written_through: Option<PropertyIdentifier>,
163}
164
165/// Object type support declaration.
166#[derive(Debug, Clone)]
167pub struct ObjectTypeSupport {
168    /// Object type described.
169    pub object_type: ObjectType,
170    /// Whether the type can be created remotely with CreateObject.
171    pub createable: bool,
172    /// The properties a CreateObject initial value may set whole on a type
173    /// that is createable, although WriteProperty can't change them later
174    /// (#1429). Empty when the type isn't createable. A property writes of
175    /// another one change, such as Number_Of_States, isn't listed: its row
176    /// says so instead ([`PropertySupport::written_through`]).
177    pub creation_only_properties: Vec<PropertyIdentifier>,
178    /// Whether the type can be deleted remotely with DeleteObject.
179    pub deleteable: bool,
180    /// Union of effective instance rows in ascending property-ID order.
181    pub supported_properties: Vec<PropertySupport>,
182}
183
184/// Service support declaration.
185#[derive(Debug, Clone)]
186pub struct ServiceSupport {
187    /// Service name.
188    pub service_name: String,
189    /// Device can request the service.
190    pub initiator: bool,
191    /// Device can execute the service.
192    pub executor: bool,
193}
194
195/// Data link layer support.
196///
197/// It is `#[non_exhaustive]`, so a new option isn't a breaking change: a
198/// `match` on it needs a wildcard arm.
199#[derive(Debug, Clone, PartialEq, Eq)]
200#[non_exhaustive]
201pub enum DataLinkSupport {
202    /// BACnet/IP over IPv4.
203    BipV4,
204    /// BACnet/IP over IPv4 with NAT traversal: a BBMD behind a NAT router,
205    /// reached at a configured global address (Annex J.7.5). Claim it with
206    /// [`BipV4`](Self::BipV4) for a device that can run that way.
207    BipV4NatTraversal,
208    /// BACnet/IP over IPv6.
209    BipV6,
210    /// MS/TP over RS-485.
211    Mstp,
212    /// Raw BACnet Ethernet (802.3).
213    Ethernet,
214    /// BACnet Secure Connect.
215    BacnetSc,
216}
217
218impl fmt::Display for DataLinkSupport {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        match self {
221            Self::BipV4 => f.write_str("BACnet/IP (Annex J)"),
222            Self::BipV4NatTraversal => f.write_str("BACnet/IP with NAT traversal (Annex J.7.5)"),
223            Self::BipV6 => f.write_str("BACnet/IPv6 (Annex U)"),
224            Self::Mstp => f.write_str("MS/TP (Clause 9)"),
225            Self::Ethernet => f.write_str("BACnet Ethernet (Clause 7)"),
226            Self::BacnetSc => f.write_str("BACnet/SC (Annex AB)"),
227        }
228    }
229}
230
231/// Network layer capabilities.
232#[derive(Debug, Clone)]
233pub struct NetworkLayerSupport {
234    /// Device routes between networks.
235    pub router: bool,
236    /// Device acts as a BACnet Broadcast Management Device.
237    pub bbmd: bool,
238    /// Device can register as a foreign device with a BBMD.
239    pub foreign_device: bool,
240}
241
242/// A character set the PICS can claim under Annex A "Character Sets Supported".
243///
244/// The variants are exactly the six sets Annex A offers. Each discriminant is the
245/// set's code from Clause 20.2.9, the initial octet of an encoded CharacterString.
246/// `Display` prints the set's Annex A label. Claiming several sets does not mean
247/// the device can use them all at once.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
249#[repr(u8)]
250pub enum CharacterSet {
251    /// ISO 10646 UTF-8, code X'00'.
252    Utf8 = charset::UTF8,
253    /// IBM/Microsoft DBCS, code X'01'. Its strings carry a two-octet code page.
254    IbmMicrosoftDbcs = charset::IBM_MICROSOFT_DBCS,
255    /// JIS X 0208, code X'02'.
256    JisX0208 = charset::JIS_X_0208,
257    /// ISO 10646 UCS-4, code X'03'.
258    Ucs4 = charset::UCS4,
259    /// ISO 10646 UCS-2, code X'04'.
260    Ucs2 = charset::UCS2,
261    /// ISO 8859-1, code X'05'.
262    Iso8859_1 = charset::ISO_8859_1,
263}
264
265impl CharacterSet {
266    /// Every set Annex A offers, in Clause 20.2.9 code order.
267    pub const ALL: [Self; 6] = [
268        Self::Utf8,
269        Self::IbmMicrosoftDbcs,
270        Self::JisX0208,
271        Self::Ucs4,
272        Self::Ucs2,
273        Self::Iso8859_1,
274    ];
275
276    /// The set's Clause 20.2.9 code, one of the `bacnet_encoding::primitives::charset` constants.
277    pub const fn code(self) -> u8 {
278        self as u8
279    }
280}
281
282impl fmt::Display for CharacterSet {
283    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
284        f.write_str(match self {
285            Self::Utf8 => "ISO 10646 (UTF-8)",
286            Self::IbmMicrosoftDbcs => "IBM/Microsoft DBCS",
287            Self::JisX0208 => "JIS X 0208",
288            Self::Ucs4 => "ISO 10646 (UCS-4)",
289            Self::Ucs2 => "ISO 10646 (UCS-2)",
290            Self::Iso8859_1 => "ISO 8859-1",
291        })
292    }
293}
294
295// ────────────────────────────── Configuration ──────────────────────────────
296
297/// Configuration for PICS generation that cannot be inferred from the database.
298#[derive(Debug, Clone)]
299pub struct PicsConfig {
300    /// Vendor name.
301    pub vendor_name: String,
302    /// Product model name.
303    pub model_name: String,
304    /// Firmware revision string.
305    pub firmware_revision: String,
306    /// Application software version string.
307    pub application_software_version: String,
308    /// BACnet protocol version implemented (default 1).
309    pub protocol_version: u16,
310    /// BACnet protocol revision implemented (default 24).
311    pub protocol_revision: u16,
312    /// Device profile claimed.
313    pub device_profile: DeviceProfile,
314    /// Data-link layers the device supports.
315    pub data_link_layers: Vec<DataLinkSupport>,
316    /// Network-layer roles the device fills.
317    pub network_layer: NetworkLayerSupport,
318    /// Character sets the device supports.
319    pub character_sets: Vec<CharacterSet>,
320    /// Free-text descriptions of special functionality.
321    pub special_functionality: Vec<String>,
322}
323
324impl Default for PicsConfig {
325    fn default() -> Self {
326        Self {
327            vendor_name: String::new(),
328            model_name: String::new(),
329            firmware_revision: String::new(),
330            application_software_version: String::new(),
331            protocol_version: 1,
332            protocol_revision: 24,
333            device_profile: DeviceProfile::BAsc,
334            data_link_layers: vec![DataLinkSupport::BipV4],
335            network_layer: NetworkLayerSupport {
336                router: false,
337                bbmd: false,
338                foreign_device: false,
339            },
340            character_sets: vec![CharacterSet::Utf8],
341            special_functionality: Vec::new(),
342        }
343    }
344}
345
346// ────────────────────────────── Generator ──────────────────────────────────
347
348/// Generates PICS from raw object declarations and configuration. This standalone
349/// generator does not establish service execution; use `BACnetServer::generate_pics`
350/// for the full server's effective Device contract.
351pub struct PicsGenerator<'a> {
352    db: &'a ObjectDatabase,
353    server_config: &'a ServerConfig,
354    pics_config: &'a PicsConfig,
355    served: bool,
356}
357
358impl<'a> PicsGenerator<'a> {
359    /// Create a generator over the object database and the server and PICS configuration.
360    pub fn new(
361        db: &'a ObjectDatabase,
362        server_config: &'a ServerConfig,
363        pics_config: &'a PicsConfig,
364    ) -> Self {
365        Self {
366            db,
367            server_config,
368            pics_config,
369            served: false,
370        }
371    }
372
373    pub(crate) fn for_server(mut self) -> Self {
374        self.served = true;
375        self
376    }
377
378    /// Generate the complete PICS document.
379    pub fn generate(&self) -> Pics {
380        Pics {
381            vendor_info: self.build_vendor_info(),
382            device_profile: self.pics_config.device_profile.clone(),
383            supported_object_types: self.build_object_types(),
384            supported_services: self.build_services(),
385            data_link_layers: self.pics_config.data_link_layers.clone(),
386            network_layer: self.pics_config.network_layer.clone(),
387            character_sets: self.pics_config.character_sets.clone(),
388            special_functionality: self.pics_config.special_functionality.clone(),
389        }
390    }
391
392    fn build_vendor_info(&self) -> VendorInfo {
393        VendorInfo {
394            vendor_id: self.server_config.device.vendor_id(),
395            vendor_name: self.pics_config.vendor_name.clone(),
396            model_name: self.pics_config.model_name.clone(),
397            firmware_revision: self.pics_config.firmware_revision.clone(),
398            application_software_version: self.pics_config.application_software_version.clone(),
399            protocol_version: self.pics_config.protocol_version,
400            protocol_revision: self.pics_config.protocol_revision,
401        }
402    }
403
404    fn build_object_types(&self) -> Vec<ObjectTypeSupport> {
405        let mut by_type: BTreeMap<u32, Vec<&dyn bacnet_objects::traits::BACnetObject>> =
406            BTreeMap::new();
407        for (_oid, obj) in self.db.iter_objects() {
408            by_type
409                .entry(obj.object_identifier().object_type().to_raw())
410                .or_default()
411                .push(obj);
412        }
413
414        let mut result = Vec::with_capacity(by_type.len());
415        for (raw_type, objects) in &by_type {
416            let object_type = ObjectType::from_raw(*raw_type);
417            let representative = objects[0];
418            let view = (self.served && object_type == ObjectType::DEVICE).then(|| {
419                crate::device_view::DeviceReadContext::new(
420                    self.db,
421                    crate::device_view::DeviceExecution::FullServer,
422                )
423                .with_identity(Some(&self.server_config.device))
424            });
425            let supported_properties =
426                Self::union_property_support(objects.iter().flat_map(|object| {
427                    if let Some(view) = &view {
428                        Self::object_property_support(&view.object(*object))
429                    } else {
430                        Self::object_property_support(*object)
431                    }
432                }));
433
434            // Factory/deletion capabilities are type-level declarations, not
435            // per-instance property capabilities. Preserve that separate policy.
436
437            let createable = representative.is_createable();
438            let deleteable = representative.is_deleteable();
439            let written_through = |property: &PropertyIdentifier| {
440                supported_properties
441                    .iter()
442                    .any(|row| row.property_id == *property && row.written_through.is_some())
443            };
444            let creation_only_properties = if createable {
445                let mut properties = representative.creation_only_properties().to_vec();
446                properties.retain(|property| !written_through(property));
447                properties
448            } else {
449                Vec::new()
450            };
451
452            result.push(ObjectTypeSupport {
453                object_type,
454                createable,
455                creation_only_properties,
456                deleteable,
457                supported_properties,
458            });
459        }
460        result
461    }
462
463    fn object_property_support(
464        object: &dyn bacnet_objects::traits::BACnetObject,
465    ) -> Vec<PropertySupport> {
466        let metadata = object.property_metadata();
467        if metadata.is_empty() {
468            let all_props = object.property_list();
469            let required = object.required_properties();
470            all_props
471                .iter()
472                .map(|&property_id| PropertySupport {
473                    property_id,
474                    access: PropertyAccess {
475                        readable: true,
476                        writable: object.is_writable_property(property_id),
477                        optional: !required.contains(&property_id),
478                    },
479                    written_through: None,
480                })
481                .collect()
482        } else {
483            metadata
484                .iter()
485                .map(|row| PropertySupport {
486                    property_id: row.property_identifier,
487                    access: PropertyAccess {
488                        readable: true,
489                        writable: row.write_capability.is_writable(),
490                        optional: !row.is_required(),
491                    },
492                    written_through: row.write_capability.written_through(),
493                })
494                .collect()
495        }
496    }
497
498    fn union_property_support(
499        rows: impl IntoIterator<Item = PropertySupport>,
500    ) -> Vec<PropertySupport> {
501        // A type row records capabilities across all configured instances.
502        // Missing properties do not vote on conformance; a required present row
503        // wins over optional rows. Sort even a single instance by property ID.
504        let mut properties: BTreeMap<u32, PropertySupport> = BTreeMap::new();
505        for row in rows {
506            properties
507                .entry(row.property_id.to_raw())
508                .and_modify(|existing| {
509                    existing.access.readable |= row.access.readable;
510                    existing.access.writable |= row.access.writable;
511                    existing.access.optional &= row.access.optional;
512                    existing.written_through = existing.written_through.or(row.written_through);
513                })
514                .or_insert(row);
515        }
516        properties.into_values().collect()
517    }
518
519    /// Build the service support list based on what the server actually handles.
520    /// Services this server initiates (the PICS initiator column): replies
521    /// and notifications constructed outbound by `bacnet-server`. Distinct
522    /// from [`EXECUTED_SERVICES`](bacnet_objects::device::EXECUTED_SERVICES), which Clause 12.11 ties to execution.
523    const INITIATED_SERVICES: &'static [ServiceSupported] = &[
524        ServiceSupported::I_AM,
525        ServiceSupported::I_HAVE,
526        ServiceSupported::CONFIRMED_COV_NOTIFICATION,
527        ServiceSupported::CONFIRMED_EVENT_NOTIFICATION,
528        ServiceSupported::UNCONFIRMED_COV_NOTIFICATION,
529        ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION,
530        ServiceSupported::CONFIRMED_COV_NOTIFICATION_MULTIPLE,
531        ServiceSupported::UNCONFIRMED_COV_NOTIFICATION_MULTIPLE,
532    ];
533
534    fn build_services(&self) -> Vec<ServiceSupport> {
535        // Standalone callers inspect the selected Device's raw declaration. The
536        // full server always uses its fixed execution profile, filtered by
537        // database clock availability; a mutable/custom Device cannot override
538        // that contract.
539        let effective_executed = (!self.served)
540            .then(|| {
541                let device = self.db.get(&self.db.selected_device()?)?;
542                match device
543                    .read_property(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED, None)
544                    .ok()?
545                {
546                    PropertyValue::BitString { data, .. } => {
547                        Some(ServicesSupported::from_bacnet(&data))
548                    }
549                    _ => None,
550                }
551            })
552            .flatten();
553        let mut service_map: BTreeMap<&'static str, (bool, bool)> = BTreeMap::new();
554        let clock_available = self.db.clock_frame().is_some();
555        let executed: Box<dyn Iterator<Item = ServiceSupported> + '_> = match &effective_executed {
556            Some(services) => Box::new(services.iter()),
557            None => {
558                Box::new(crate::device_view::DeviceExecution::FullServer.services(clock_available))
559            }
560        };
561        for service in executed {
562            service_map
563                .entry(service_display_name(service))
564                .or_default()
565                .1 = true;
566        }
567        for service in Self::INITIATED_SERVICES {
568            service_map
569                .entry(service_display_name(*service))
570                .or_default()
571                .0 = true;
572        }
573
574        service_map
575            .into_iter()
576            .map(|(name, (initiator, executor))| ServiceSupport {
577                service_name: name.to_string(),
578                initiator,
579                executor,
580            })
581            .collect()
582    }
583}
584
585// ─────────────────────────── Text output ───────────────────────────────────
586
587impl Pics {
588    /// Render the PICS as human-readable text per Annex A layout.
589    pub fn generate_text(&self) -> String {
590        let mut out = String::with_capacity(4096);
591
592        out.push_str("=== BACnet Protocol Implementation Conformance Statement (PICS) ===\n");
593        out.push_str("    Per ASHRAE 135-2020 Annex A\n\n");
594
595        out.push_str("--- Vendor Information ---\n");
596        out.push_str(&format!(
597            "Vendor ID:                      {}\n",
598            self.vendor_info.vendor_id
599        ));
600        out.push_str(&format!(
601            "Vendor Name:                    {}\n",
602            self.vendor_info.vendor_name
603        ));
604        out.push_str(&format!(
605            "Model Name:                     {}\n",
606            self.vendor_info.model_name
607        ));
608        out.push_str(&format!(
609            "Firmware Revision:              {}\n",
610            self.vendor_info.firmware_revision
611        ));
612        out.push_str(&format!(
613            "Application Software Version:   {}\n",
614            self.vendor_info.application_software_version
615        ));
616        out.push_str(&format!(
617            "Protocol Version:               {}\n",
618            self.vendor_info.protocol_version
619        ));
620        out.push_str(&format!(
621            "Protocol Revision:              {}\n\n",
622            self.vendor_info.protocol_revision
623        ));
624
625        out.push_str("--- BACnet Device Profile ---\n");
626        out.push_str(&format!("Profile: {}\n\n", self.device_profile));
627
628        out.push_str("--- Supported Object Types ---\n");
629        out.push_str(PROPERTY_CAPABILITIES_EXPLANATION);
630        out.push('\n');
631        for ot in &self.supported_object_types {
632            out.push_str(&format!(
633                "\n  Object Type: {} (createable={}, deleteable={})\n",
634                ot.object_type, ot.createable, ot.deleteable
635            ));
636            if let Some(names) = creation_only_names(ot) {
637                out.push_str(&format!("  {CREATION_ONLY_LABEL}: {names}\n"));
638            }
639            out.push_str("  Properties:\n");
640            for prop in &ot.supported_properties {
641                out.push_str(&format!(
642                    "    {:<40} {}\n",
643                    prop.property_id.to_string(),
644                    access_text(prop)
645                ));
646            }
647        }
648        out.push('\n');
649
650        out.push_str("--- Supported Services ---\n");
651        out.push_str(&format!(
652            "  {:<45} {:>9} {:>9}\n",
653            "Service", "Initiator", "Executor"
654        ));
655        out.push_str(&format!("  {:-<45} {:-<9} {:-<9}\n", "", "", ""));
656        for svc in &self.supported_services {
657            let init = if svc.initiator { "Yes" } else { "No" };
658            let exec = if svc.executor { "Yes" } else { "No" };
659            out.push_str(&format!(
660                "  {:<45} {:>9} {:>9}\n",
661                svc.service_name, init, exec
662            ));
663        }
664        out.push('\n');
665
666        out.push_str("--- Data Link Layer Support ---\n");
667        for dl in &self.data_link_layers {
668            out.push_str(&format!("  {dl}\n"));
669        }
670        out.push('\n');
671
672        out.push_str("--- Network Layer Options ---\n");
673        out.push_str(&format!(
674            "  Router:         {}\n",
675            self.network_layer.router
676        ));
677        out.push_str(&format!("  BBMD:           {}\n", self.network_layer.bbmd));
678        out.push_str(&format!(
679            "  Foreign Device: {}\n\n",
680            self.network_layer.foreign_device
681        ));
682
683        out.push_str("--- Character Sets Supported ---\n");
684        for cs in &self.character_sets {
685            out.push_str(&format!("  {cs}\n"));
686        }
687        out.push('\n');
688
689        if !self.special_functionality.is_empty() {
690            out.push_str("--- Special Functionality ---\n");
691            for sf in &self.special_functionality {
692                out.push_str(&format!("  {sf}\n"));
693            }
694            out.push('\n');
695        }
696
697        out
698    }
699
700    /// Render the PICS as Markdown for documentation.
701    pub fn generate_markdown(&self) -> String {
702        let mut out = String::with_capacity(4096);
703
704        out.push_str("# BACnet Protocol Implementation Conformance Statement (PICS)\n\n");
705        out.push_str("*Per ASHRAE 135-2020 Annex A*\n\n");
706
707        out.push_str("## Vendor Information\n\n");
708        out.push_str("| Field | Value |\n");
709        out.push_str("|-------|-------|\n");
710        out.push_str(&format!("| Vendor ID | {} |\n", self.vendor_info.vendor_id));
711        out.push_str(&format!(
712            "| Vendor Name | {} |\n",
713            self.vendor_info.vendor_name
714        ));
715        out.push_str(&format!(
716            "| Model Name | {} |\n",
717            self.vendor_info.model_name
718        ));
719        out.push_str(&format!(
720            "| Firmware Revision | {} |\n",
721            self.vendor_info.firmware_revision
722        ));
723        out.push_str(&format!(
724            "| Application Software Version | {} |\n",
725            self.vendor_info.application_software_version
726        ));
727        out.push_str(&format!(
728            "| Protocol Version | {} |\n",
729            self.vendor_info.protocol_version
730        ));
731        out.push_str(&format!(
732            "| Protocol Revision | {} |\n\n",
733            self.vendor_info.protocol_revision
734        ));
735
736        out.push_str("## BACnet Device Profile\n\n");
737        out.push_str(&format!("**{}**\n\n", self.device_profile));
738
739        out.push_str("## Supported Object Types\n\n");
740        out.push_str(PROPERTY_CAPABILITIES_EXPLANATION);
741        out.push_str("\n\n");
742        for ot in &self.supported_object_types {
743            out.push_str(&format!(
744                "### {}\n\n- Createable: {}\n- Deleteable: {}\n",
745                ot.object_type, ot.createable, ot.deleteable
746            ));
747            if let Some(names) = creation_only_names(ot) {
748                out.push_str(&format!("- {CREATION_ONLY_LABEL}: {names}\n"));
749            }
750            out.push('\n');
751            out.push_str("| Property | Access |\n");
752            out.push_str("|----------|--------|\n");
753            for prop in &ot.supported_properties {
754                out.push_str(&format!(
755                    "| {} | {} |\n",
756                    prop.property_id,
757                    access_text(prop)
758                ));
759            }
760            out.push('\n');
761        }
762
763        out.push_str("## Supported Services\n\n");
764        out.push_str("| Service | Initiator | Executor |\n");
765        out.push_str("|---------|-----------|----------|\n");
766        for svc in &self.supported_services {
767            let init = if svc.initiator { "Yes" } else { "No" };
768            let exec = if svc.executor { "Yes" } else { "No" };
769            out.push_str(&format!("| {} | {} | {} |\n", svc.service_name, init, exec));
770        }
771        out.push('\n');
772
773        out.push_str("## Data Link Layer Support\n\n");
774        for dl in &self.data_link_layers {
775            out.push_str(&format!("- {dl}\n"));
776        }
777        out.push('\n');
778
779        out.push_str("## Network Layer Options\n\n");
780        out.push_str("| Feature | Supported |\n");
781        out.push_str("|---------|-----------|\n");
782        out.push_str(&format!("| Router | {} |\n", self.network_layer.router));
783        out.push_str(&format!("| BBMD | {} |\n", self.network_layer.bbmd));
784        out.push_str(&format!(
785            "| Foreign Device | {} |\n\n",
786            self.network_layer.foreign_device
787        ));
788
789        out.push_str("## Character Sets Supported\n\n");
790        for cs in &self.character_sets {
791            out.push_str(&format!("- {cs}\n"));
792        }
793        out.push('\n');
794
795        if !self.special_functionality.is_empty() {
796            out.push_str("## Special Functionality\n\n");
797            for sf in &self.special_functionality {
798                out.push_str(&format!("- {sf}\n"));
799            }
800            out.push('\n');
801        }
802
803        out
804    }
805}
806
807// ─────────────────────────── Standalone helper ─────────────────────────────
808
809/// Generate a PICS document from an ObjectDatabase and configuration.
810///
811/// This is a convenience function for use without a running BACnetServer.
812pub fn generate_pics(
813    db: &ObjectDatabase,
814    server_config: &ServerConfig,
815    pics_config: &PicsConfig,
816) -> Pics {
817    PicsGenerator::new(db, server_config, pics_config).generate()
818}
819
820// ─────────────────────────────── Tests ─────────────────────────────────────
821
822/// PICS display name for a `BACnetServicesSupported` bit position.
823fn service_display_name(service: ServiceSupported) -> &'static str {
824    match service.to_raw() {
825        0 => "AcknowledgeAlarm",
826        1 => "ConfirmedCOVNotification",
827        2 => "ConfirmedEventNotification",
828        3 => "GetAlarmSummary",
829        4 => "GetEnrollmentSummary",
830        5 => "SubscribeCOV",
831        6 => "AtomicReadFile",
832        7 => "AtomicWriteFile",
833        8 => "AddListElement",
834        9 => "RemoveListElement",
835        10 => "CreateObject",
836        11 => "DeleteObject",
837        12 => "ReadProperty",
838        14 => "ReadPropertyMultiple",
839        15 => "WriteProperty",
840        16 => "WritePropertyMultiple",
841        17 => "DeviceCommunicationControl",
842        18 => "ConfirmedPrivateTransfer",
843        19 => "ConfirmedTextMessage",
844        20 => "ReinitializeDevice",
845        21 => "VT-Open",
846        22 => "VT-Close",
847        23 => "VT-Data",
848        26 => "I-Am",
849        27 => "I-Have",
850        28 => "UnconfirmedCOVNotification",
851        29 => "UnconfirmedEventNotification",
852        30 => "UnconfirmedPrivateTransfer",
853        31 => "UnconfirmedTextMessage",
854        32 => "TimeSynchronization",
855        33 => "WhoHas",
856        34 => "WhoIs",
857        35 => "ReadRange",
858        36 => "UTCTimeSynchronization",
859        37 => "LifeSafetyOperation",
860        38 => "SubscribeCOVProperty",
861        39 => "GetEventInformation",
862        40 => "WriteGroup",
863        41 => "SubscribeCOVPropertyMultiple",
864        42 => "ConfirmedCOVNotificationMultiple",
865        43 => "UnconfirmedCOVNotificationMultiple",
866        44 => "ConfirmedAuditNotification",
867        45 => "AuditLogQuery",
868        46 => "UnconfirmedAuditNotification",
869        47 => "Who-Am-I",
870        48 => "You-Are",
871        _ => "Unknown",
872    }
873}
874
875#[cfg(test)]
876mod tests;
877
878#[cfg(test)]
879mod property_metadata_tests;
880
881#[cfg(test)]
882mod acked_transitions_policy_tests;
883
884#[cfg(test)]
885mod truth_source_tests;
886
887#[cfg(test)]
888mod property_union_tests;
889
890#[cfg(test)]
891mod character_set_tests;
892
893#[cfg(test)]
894mod selected_device_tests;
895
896#[cfg(test)]
897mod creation_only_tests;
898
899#[cfg(test)]
900mod data_link_tests;