1use 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#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum DeviceProfile {
48 BAac,
50 BAsc,
52 BOws,
54 BBc,
56 BOp,
58 BRouter,
60 BGw,
62 BSc,
64 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#[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#[derive(Debug, Clone)]
103pub struct PropertySupport {
104 pub property_id: PropertyIdentifier,
105 pub access: PropertyAccess,
106}
107
108#[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#[derive(Debug, Clone)]
119pub struct ServiceSupport {
120 pub service_name: String,
121 pub initiator: bool,
122 pub executor: bool,
123}
124
125#[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#[derive(Debug, Clone)]
149pub struct NetworkLayerSupport {
150 pub router: bool,
151 pub bbmd: bool,
152 pub foreign_device: bool,
153}
154
155#[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#[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
219pub 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 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 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 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
395impl Pics {
398 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 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
602pub 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
615fn 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;