1use 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#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum DeviceProfile {
44 BAac,
46 BAsc,
48 BOws,
50 BBc,
52 BOp,
54 BRouter,
56 BGw,
58 BSc,
60 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#[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#[derive(Debug, Clone)]
99pub struct PropertySupport {
100 pub property_id: PropertyIdentifier,
101 pub access: PropertyAccess,
102}
103
104#[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#[derive(Debug, Clone)]
115pub struct ServiceSupport {
116 pub service_name: String,
117 pub initiator: bool,
118 pub executor: bool,
119}
120
121#[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#[derive(Debug, Clone)]
145pub struct NetworkLayerSupport {
146 pub router: bool,
147 pub bbmd: bool,
148 pub foreign_device: bool,
149}
150
151#[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#[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
215pub 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 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 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 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
412impl Pics {
415 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 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
619pub 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#[cfg(test)]
635mod tests {
636 use std::borrow::Cow;
637
638 use bacnet_objects::traits::BACnetObject;
639 use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
640 use bacnet_types::error::Error;
641 use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
642
643 use super::*;
644
645 struct TestAnalogInput {
648 oid: ObjectIdentifier,
649 name: String,
650 }
651
652 impl BACnetObject for TestAnalogInput {
653 fn object_identifier(&self) -> ObjectIdentifier {
654 self.oid
655 }
656 fn object_name(&self) -> &str {
657 &self.name
658 }
659 fn read_property(
660 &self,
661 _property: PropertyIdentifier,
662 _array_index: Option<u32>,
663 ) -> Result<PropertyValue, Error> {
664 Ok(PropertyValue::Real(0.0))
665 }
666 fn write_property(
667 &mut self,
668 _property: PropertyIdentifier,
669 _array_index: Option<u32>,
670 _value: PropertyValue,
671 _priority: Option<u8>,
672 ) -> Result<(), Error> {
673 Err(Error::Protocol {
674 class: ErrorClass::PROPERTY.to_raw() as u32,
675 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
676 })
677 }
678 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
679 static PROPS: [PropertyIdentifier; 8] = [
680 PropertyIdentifier::OBJECT_IDENTIFIER,
681 PropertyIdentifier::OBJECT_NAME,
682 PropertyIdentifier::OBJECT_TYPE,
683 PropertyIdentifier::PROPERTY_LIST,
684 PropertyIdentifier::PRESENT_VALUE,
685 PropertyIdentifier::STATUS_FLAGS,
686 PropertyIdentifier::OUT_OF_SERVICE,
687 PropertyIdentifier::UNITS,
688 ];
689 Cow::Borrowed(&PROPS)
690 }
691 }
692
693 struct TestBinaryValue {
694 oid: ObjectIdentifier,
695 name: String,
696 }
697
698 impl BACnetObject for TestBinaryValue {
699 fn object_identifier(&self) -> ObjectIdentifier {
700 self.oid
701 }
702 fn object_name(&self) -> &str {
703 &self.name
704 }
705 fn read_property(
706 &self,
707 _property: PropertyIdentifier,
708 _array_index: Option<u32>,
709 ) -> Result<PropertyValue, Error> {
710 Ok(PropertyValue::Boolean(false))
711 }
712 fn write_property(
713 &mut self,
714 _property: PropertyIdentifier,
715 _array_index: Option<u32>,
716 _value: PropertyValue,
717 _priority: Option<u8>,
718 ) -> Result<(), Error> {
719 Ok(())
720 }
721 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
722 static PROPS: [PropertyIdentifier; 6] = [
723 PropertyIdentifier::OBJECT_IDENTIFIER,
724 PropertyIdentifier::OBJECT_NAME,
725 PropertyIdentifier::OBJECT_TYPE,
726 PropertyIdentifier::PROPERTY_LIST,
727 PropertyIdentifier::PRESENT_VALUE,
728 PropertyIdentifier::STATUS_FLAGS,
729 ];
730 Cow::Borrowed(&PROPS)
731 }
732 }
733
734 struct TestDevice {
735 oid: ObjectIdentifier,
736 name: String,
737 }
738
739 impl BACnetObject for TestDevice {
740 fn object_identifier(&self) -> ObjectIdentifier {
741 self.oid
742 }
743 fn object_name(&self) -> &str {
744 &self.name
745 }
746 fn read_property(
747 &self,
748 _property: PropertyIdentifier,
749 _array_index: Option<u32>,
750 ) -> Result<PropertyValue, Error> {
751 Ok(PropertyValue::Unsigned(0))
752 }
753 fn write_property(
754 &mut self,
755 _property: PropertyIdentifier,
756 _array_index: Option<u32>,
757 _value: PropertyValue,
758 _priority: Option<u8>,
759 ) -> Result<(), Error> {
760 Err(Error::Protocol {
761 class: ErrorClass::PROPERTY.to_raw() as u32,
762 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
763 })
764 }
765 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
766 static PROPS: [PropertyIdentifier; 6] = [
767 PropertyIdentifier::OBJECT_IDENTIFIER,
768 PropertyIdentifier::OBJECT_NAME,
769 PropertyIdentifier::OBJECT_TYPE,
770 PropertyIdentifier::PROPERTY_LIST,
771 PropertyIdentifier::PROTOCOL_VERSION,
772 PropertyIdentifier::PROTOCOL_REVISION,
773 ];
774 Cow::Borrowed(&PROPS)
775 }
776 }
777
778 fn make_test_db() -> ObjectDatabase {
781 let mut db = ObjectDatabase::new();
782 db.add(Box::new(TestDevice {
783 oid: ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap(),
784 name: "Test Device".into(),
785 }))
786 .unwrap();
787 db.add(Box::new(TestAnalogInput {
788 oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
789 name: "AI-1".into(),
790 }))
791 .unwrap();
792 db.add(Box::new(TestAnalogInput {
793 oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap(),
794 name: "AI-2".into(),
795 }))
796 .unwrap();
797 db.add(Box::new(TestBinaryValue {
798 oid: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap(),
799 name: "BV-1".into(),
800 }))
801 .unwrap();
802 db
803 }
804
805 fn make_pics_config() -> PicsConfig {
806 PicsConfig {
807 vendor_name: "Acme Corp".into(),
808 model_name: "BACnet Controller 3000".into(),
809 firmware_revision: "1.0.0".into(),
810 application_software_version: "2.0.0".into(),
811 protocol_version: 1,
812 protocol_revision: 24,
813 device_profile: DeviceProfile::BAsc,
814 data_link_layers: vec![DataLinkSupport::BipV4],
815 network_layer: NetworkLayerSupport {
816 router: false,
817 bbmd: false,
818 foreign_device: false,
819 },
820 character_sets: vec![CharacterSet::Utf8],
821 special_functionality: vec!["Intrinsic event reporting".into()],
822 }
823 }
824
825 #[test]
828 fn generate_pics_basic() {
829 let db = make_test_db();
830 let server_config = ServerConfig {
831 vendor_id: 999,
832 ..ServerConfig::default()
833 };
834 let pics_config = make_pics_config();
835 let pics = generate_pics(&db, &server_config, &pics_config);
836
837 assert_eq!(pics.vendor_info.vendor_id, 999);
838 assert_eq!(pics.vendor_info.vendor_name, "Acme Corp");
839 assert_eq!(pics.device_profile, DeviceProfile::BAsc);
840 assert_eq!(pics.character_sets, vec![CharacterSet::Utf8]);
841 assert_eq!(pics.data_link_layers, vec![DataLinkSupport::BipV4]);
842 }
843
844 #[test]
845 fn all_object_types_listed() {
846 let db = make_test_db();
847 let server_config = ServerConfig::default();
848 let pics_config = make_pics_config();
849 let pics = generate_pics(&db, &server_config, &pics_config);
850
851 let types: Vec<ObjectType> = pics
852 .supported_object_types
853 .iter()
854 .map(|ot| ot.object_type)
855 .collect();
856 assert!(types.contains(&ObjectType::DEVICE));
857 assert!(types.contains(&ObjectType::ANALOG_INPUT));
858 assert!(types.contains(&ObjectType::BINARY_VALUE));
859 assert_eq!(types.len(), 3);
861 }
862
863 #[test]
864 fn object_type_properties_populated() {
865 let db = make_test_db();
866 let server_config = ServerConfig::default();
867 let pics_config = make_pics_config();
868 let pics = generate_pics(&db, &server_config, &pics_config);
869
870 let ai = pics
871 .supported_object_types
872 .iter()
873 .find(|ot| ot.object_type == ObjectType::ANALOG_INPUT)
874 .expect("ANALOG_INPUT should be in PICS");
875
876 assert_eq!(ai.supported_properties.len(), 8);
878
879 let pv = ai
881 .supported_properties
882 .iter()
883 .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
884 .expect("PRESENT_VALUE should exist");
885 assert!(pv.access.readable);
886 assert!(!pv.access.writable);
887 }
888
889 #[test]
890 fn device_not_createable_or_deleteable() {
891 let db = make_test_db();
892 let server_config = ServerConfig::default();
893 let pics_config = make_pics_config();
894 let pics = generate_pics(&db, &server_config, &pics_config);
895
896 let dev = pics
897 .supported_object_types
898 .iter()
899 .find(|ot| ot.object_type == ObjectType::DEVICE)
900 .expect("DEVICE should be in PICS");
901 assert!(!dev.createable);
902 assert!(!dev.deleteable);
903 }
904
905 #[test]
906 fn services_match_implementation() {
907 let db = make_test_db();
908 let server_config = ServerConfig::default();
909 let pics_config = make_pics_config();
910 let pics = generate_pics(&db, &server_config, &pics_config);
911
912 let service_names: Vec<&str> = pics
913 .supported_services
914 .iter()
915 .map(|s| s.service_name.as_str())
916 .collect();
917
918 assert!(service_names.contains(&"ReadProperty"));
920 assert!(service_names.contains(&"WriteProperty"));
921 assert!(service_names.contains(&"ReadPropertyMultiple"));
922 assert!(service_names.contains(&"SubscribeCOV"));
923 assert!(service_names.contains(&"CreateObject"));
924 assert!(service_names.contains(&"DeleteObject"));
925 assert!(service_names.contains(&"WhoIs"));
926
927 assert!(service_names.contains(&"I-Am"));
929 assert!(service_names.contains(&"ConfirmedCOVNotification"));
930
931 let rp = pics
933 .supported_services
934 .iter()
935 .find(|s| s.service_name == "ReadProperty")
936 .expect("ReadProperty should be listed");
937 assert!(!rp.initiator);
938 assert!(rp.executor);
939
940 let iam = pics
942 .supported_services
943 .iter()
944 .find(|s| s.service_name == "I-Am")
945 .expect("I-Am should be listed");
946 assert!(iam.initiator);
947 assert!(!iam.executor);
948 }
949
950 #[test]
951 fn text_output_contains_key_sections() {
952 let db = make_test_db();
953 let server_config = ServerConfig {
954 vendor_id: 42,
955 ..ServerConfig::default()
956 };
957 let pics_config = make_pics_config();
958 let pics = generate_pics(&db, &server_config, &pics_config);
959 let text = pics.generate_text();
960
961 assert!(text.contains("Protocol Implementation Conformance Statement"));
962 assert!(text.contains("Vendor ID:"));
963 assert!(text.contains("42"));
964 assert!(text.contains("Acme Corp"));
965 assert!(text.contains("B-ASC"));
966 assert!(text.contains("Supported Object Types"));
967 assert!(text.contains("ANALOG_INPUT"));
968 assert!(text.contains("Supported Services"));
969 assert!(text.contains("ReadProperty"));
970 assert!(text.contains("Data Link Layer Support"));
971 assert!(text.contains("BACnet/IP (Annex J)"));
972 assert!(text.contains("Character Sets Supported"));
973 assert!(text.contains("UTF-8"));
974 assert!(text.contains("Special Functionality"));
975 assert!(text.contains("Intrinsic event reporting"));
976 }
977
978 #[test]
979 fn markdown_output_has_tables() {
980 let db = make_test_db();
981 let server_config = ServerConfig::default();
982 let pics_config = make_pics_config();
983 let pics = generate_pics(&db, &server_config, &pics_config);
984 let md = pics.generate_markdown();
985
986 assert!(md.contains("# BACnet Protocol Implementation Conformance Statement"));
987 assert!(md.contains("| Field | Value |"));
988 assert!(md.contains("| Service | Initiator | Executor |"));
989 assert!(md.contains("| Property | Access |"));
990 assert!(md.contains("## Supported Object Types"));
991 assert!(md.contains("## Supported Services"));
992 }
993
994 #[test]
995 fn empty_database_produces_empty_object_list() {
996 let db = ObjectDatabase::new();
997 let server_config = ServerConfig::default();
998 let pics_config = PicsConfig::default();
999 let pics = generate_pics(&db, &server_config, &pics_config);
1000
1001 assert!(pics.supported_object_types.is_empty());
1002 assert!(!pics.supported_services.is_empty());
1003 }
1004
1005 #[test]
1006 fn device_profile_display() {
1007 assert_eq!(DeviceProfile::BAac.to_string(), "B-AAC");
1008 assert_eq!(DeviceProfile::BAsc.to_string(), "B-ASC");
1009 assert_eq!(DeviceProfile::BOws.to_string(), "B-OWS");
1010 assert_eq!(DeviceProfile::BBc.to_string(), "B-BC");
1011 assert_eq!(DeviceProfile::BOp.to_string(), "B-OP");
1012 assert_eq!(DeviceProfile::BRouter.to_string(), "B-ROUTER");
1013 assert_eq!(DeviceProfile::BGw.to_string(), "B-GW");
1014 assert_eq!(DeviceProfile::BSc.to_string(), "B-SC");
1015 assert_eq!(
1016 DeviceProfile::Custom("MyProfile".into()).to_string(),
1017 "MyProfile"
1018 );
1019 }
1020
1021 #[test]
1022 fn property_access_display() {
1023 let rw = PropertyAccess {
1024 readable: true,
1025 writable: true,
1026 optional: false,
1027 };
1028 assert_eq!(rw.to_string(), "RW");
1029
1030 let ro = PropertyAccess {
1031 readable: true,
1032 writable: false,
1033 optional: true,
1034 };
1035 assert_eq!(ro.to_string(), "RO");
1036
1037 let wo = PropertyAccess {
1038 readable: false,
1039 writable: true,
1040 optional: false,
1041 };
1042 assert_eq!(wo.to_string(), "W");
1043 }
1044
1045 #[test]
1046 fn binary_value_present_value_is_writable() {
1047 let db = make_test_db();
1048 let server_config = ServerConfig::default();
1049 let pics_config = make_pics_config();
1050 let pics = generate_pics(&db, &server_config, &pics_config);
1051
1052 let bv = pics
1053 .supported_object_types
1054 .iter()
1055 .find(|ot| ot.object_type == ObjectType::BINARY_VALUE)
1056 .expect("BINARY_VALUE should be in PICS");
1057
1058 let pv = bv
1059 .supported_properties
1060 .iter()
1061 .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
1062 .expect("PRESENT_VALUE should exist on BV");
1063 assert!(
1064 pv.access.writable,
1065 "BinaryValue PRESENT_VALUE should be writable"
1066 );
1067 }
1068}