Skip to main content

bacnet_server/pics/
render.rs

1//! The text and Markdown renderings of a [`Pics`] document.
2
3use super::device_text::{markdown_cell, text_line};
4use super::{ObjectTypeSupport, Pics, PropertySupport};
5
6const 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.";
7
8/// How the text and Markdown PICS introduce a type's
9/// [`ObjectTypeSupport::creation_only_properties`]: read-only to
10/// WriteProperty as a whole, but a CreateObject initial value sets them.
11const CREATION_ONLY_LABEL: &str = "Whole value set only by CreateObject";
12
13/// A row's access as the text and Markdown PICS print it: the flags, and for
14/// a row writes of another property change
15/// ([`PropertySupport::written_through`]) a note saying so, since no write
16/// naming the row itself is taken (#1443).
17fn access_text(row: &PropertySupport) -> String {
18    match row.written_through {
19        Some(through) => format!(
20            "{} (resized through {through}: a whole write or its size at index 0)",
21            row.access
22        ),
23        None => row.access.to_string(),
24    }
25}
26
27/// The type's creation-only properties, comma-separated, or `None` if it
28/// has none.
29fn creation_only_names(support: &ObjectTypeSupport) -> Option<String> {
30    let names: Vec<_> = support
31        .creation_only_properties
32        .iter()
33        .map(ToString::to_string)
34        .collect();
35    (!names.is_empty()).then(|| names.join(", "))
36}
37
38impl Pics {
39    /// Render the PICS as human-readable text per Annex A layout.
40    pub fn generate_text(&self) -> String {
41        let mut out = String::with_capacity(4096);
42
43        out.push_str("=== BACnet Protocol Implementation Conformance Statement (PICS) ===\n");
44        out.push_str("    Per ASHRAE 135-2020 Annex A\n\n");
45
46        out.push_str("--- Vendor Information ---\n");
47        out.push_str(&format!(
48            "Vendor ID:                      {}\n",
49            self.vendor_info.vendor_id
50        ));
51        out.push_str(&format!(
52            "Vendor Name:                    {}\n",
53            text_line(&self.vendor_info.vendor_name)
54        ));
55        out.push_str(&format!(
56            "Model Name:                     {}\n",
57            text_line(&self.vendor_info.model_name)
58        ));
59        out.push_str(&format!(
60            "Firmware Revision:              {}\n",
61            text_line(&self.vendor_info.firmware_revision)
62        ));
63        out.push_str(&format!(
64            "Application Software Version:   {}\n",
65            text_line(&self.vendor_info.application_software_version)
66        ));
67        out.push_str(&format!(
68            "Protocol Version:               {}\n",
69            self.vendor_info.protocol_version
70        ));
71        out.push_str(&format!(
72            "Protocol Revision:              {}\n\n",
73            self.vendor_info.protocol_revision
74        ));
75
76        out.push_str("--- BACnet Device Profile ---\n");
77        out.push_str(&format!("Profile: {}\n\n", self.device_profile));
78
79        out.push_str("--- Supported Object Types ---\n");
80        out.push_str(PROPERTY_CAPABILITIES_EXPLANATION);
81        out.push('\n');
82        for ot in &self.supported_object_types {
83            out.push_str(&format!(
84                "\n  Object Type: {} (createable={}, deleteable={})\n",
85                ot.object_type, ot.createable, ot.deleteable
86            ));
87            if let Some(names) = creation_only_names(ot) {
88                out.push_str(&format!("  {CREATION_ONLY_LABEL}: {names}\n"));
89            }
90            out.push_str("  Properties:\n");
91            for prop in &ot.supported_properties {
92                out.push_str(&format!(
93                    "    {:<40} {}\n",
94                    prop.property_id.to_string(),
95                    access_text(prop)
96                ));
97            }
98        }
99        out.push('\n');
100
101        out.push_str("--- Supported Services ---\n");
102        out.push_str(&format!(
103            "  {:<45} {:>9} {:>9}\n",
104            "Service", "Initiator", "Executor"
105        ));
106        out.push_str(&format!("  {:-<45} {:-<9} {:-<9}\n", "", "", ""));
107        for svc in &self.supported_services {
108            let init = if svc.initiator { "Yes" } else { "No" };
109            let exec = if svc.executor { "Yes" } else { "No" };
110            out.push_str(&format!(
111                "  {:<45} {:>9} {:>9}\n",
112                svc.service_name, init, exec
113            ));
114        }
115        out.push('\n');
116
117        out.push_str("--- Data Link Layer Support ---\n");
118        for dl in &self.data_link_layers {
119            out.push_str(&format!("  {dl}\n"));
120        }
121        out.push('\n');
122
123        out.push_str("--- Network Layer Options ---\n");
124        out.push_str(&format!(
125            "  Router:         {}\n",
126            self.network_layer.router
127        ));
128        out.push_str(&format!("  BBMD:           {}\n", self.network_layer.bbmd));
129        out.push_str(&format!(
130            "  Foreign Device: {}\n\n",
131            self.network_layer.foreign_device
132        ));
133
134        out.push_str("--- Character Sets Supported ---\n");
135        for cs in &self.character_sets {
136            out.push_str(&format!("  {cs}\n"));
137        }
138        out.push('\n');
139
140        if !self.special_functionality.is_empty() {
141            out.push_str("--- Special Functionality ---\n");
142            for sf in &self.special_functionality {
143                out.push_str(&format!("  {sf}\n"));
144            }
145            out.push('\n');
146        }
147
148        out
149    }
150
151    /// Render the PICS as Markdown for documentation.
152    pub fn generate_markdown(&self) -> String {
153        let mut out = String::with_capacity(4096);
154
155        out.push_str("# BACnet Protocol Implementation Conformance Statement (PICS)\n\n");
156        out.push_str("*Per ASHRAE 135-2020 Annex A*\n\n");
157
158        out.push_str("## Vendor Information\n\n");
159        out.push_str("| Field | Value |\n");
160        out.push_str("|-------|-------|\n");
161        out.push_str(&format!("| Vendor ID | {} |\n", self.vendor_info.vendor_id));
162        out.push_str(&format!(
163            "| Vendor Name | {} |\n",
164            markdown_cell(&self.vendor_info.vendor_name)
165        ));
166        out.push_str(&format!(
167            "| Model Name | {} |\n",
168            markdown_cell(&self.vendor_info.model_name)
169        ));
170        out.push_str(&format!(
171            "| Firmware Revision | {} |\n",
172            markdown_cell(&self.vendor_info.firmware_revision)
173        ));
174        out.push_str(&format!(
175            "| Application Software Version | {} |\n",
176            markdown_cell(&self.vendor_info.application_software_version)
177        ));
178        out.push_str(&format!(
179            "| Protocol Version | {} |\n",
180            self.vendor_info.protocol_version
181        ));
182        out.push_str(&format!(
183            "| Protocol Revision | {} |\n\n",
184            self.vendor_info.protocol_revision
185        ));
186
187        out.push_str("## BACnet Device Profile\n\n");
188        out.push_str(&format!("**{}**\n\n", self.device_profile));
189
190        out.push_str("## Supported Object Types\n\n");
191        out.push_str(PROPERTY_CAPABILITIES_EXPLANATION);
192        out.push_str("\n\n");
193        for ot in &self.supported_object_types {
194            out.push_str(&format!(
195                "### {}\n\n- Createable: {}\n- Deleteable: {}\n",
196                ot.object_type, ot.createable, ot.deleteable
197            ));
198            if let Some(names) = creation_only_names(ot) {
199                out.push_str(&format!("- {CREATION_ONLY_LABEL}: {names}\n"));
200            }
201            out.push('\n');
202            out.push_str("| Property | Access |\n");
203            out.push_str("|----------|--------|\n");
204            for prop in &ot.supported_properties {
205                out.push_str(&format!(
206                    "| {} | {} |\n",
207                    prop.property_id,
208                    access_text(prop)
209                ));
210            }
211            out.push('\n');
212        }
213
214        out.push_str("## Supported Services\n\n");
215        out.push_str("| Service | Initiator | Executor |\n");
216        out.push_str("|---------|-----------|----------|\n");
217        for svc in &self.supported_services {
218            let init = if svc.initiator { "Yes" } else { "No" };
219            let exec = if svc.executor { "Yes" } else { "No" };
220            out.push_str(&format!("| {} | {} | {} |\n", svc.service_name, init, exec));
221        }
222        out.push('\n');
223
224        out.push_str("## Data Link Layer Support\n\n");
225        for dl in &self.data_link_layers {
226            out.push_str(&format!("- {dl}\n"));
227        }
228        out.push('\n');
229
230        out.push_str("## Network Layer Options\n\n");
231        out.push_str("| Feature | Supported |\n");
232        out.push_str("|---------|-----------|\n");
233        out.push_str(&format!("| Router | {} |\n", self.network_layer.router));
234        out.push_str(&format!("| BBMD | {} |\n", self.network_layer.bbmd));
235        out.push_str(&format!(
236            "| Foreign Device | {} |\n\n",
237            self.network_layer.foreign_device
238        ));
239
240        out.push_str("## Character Sets Supported\n\n");
241        for cs in &self.character_sets {
242            out.push_str(&format!("- {cs}\n"));
243        }
244        out.push('\n');
245
246        if !self.special_functionality.is_empty() {
247            out.push_str("## Special Functionality\n\n");
248            for sf in &self.special_functionality {
249                out.push_str(&format!("- {sf}\n"));
250            }
251            out.push('\n');
252        }
253
254        out
255    }
256}