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>> =
266 BTreeMap::new();
267 for (_oid, obj) in self.db.iter_objects() {
268 by_type
269 .entry(obj.object_identifier().object_type().to_raw())
270 .or_default()
271 .push(obj);
272 }
273
274 let mut result = Vec::with_capacity(by_type.len());
275 for (raw_type, objects) in &by_type {
276 let object_type = ObjectType::from_raw(*raw_type);
277 let representative = objects[0];
279 let all_props = representative.property_list();
280 let required = representative.required_properties();
281
282 let supported_properties = all_props
283 .iter()
284 .map(|&pid| {
285 let is_required = required.contains(&pid);
286 let writable = Self::is_writable_property(object_type, pid);
289 PropertySupport {
290 property_id: pid,
291 access: PropertyAccess {
292 readable: true,
293 writable,
294 optional: !is_required,
295 },
296 }
297 })
298 .collect();
299
300 let createable = Self::is_createable(object_type);
301 let deleteable = Self::is_deleteable(object_type);
302
303 result.push(ObjectTypeSupport {
304 object_type,
305 createable,
306 deleteable,
307 supported_properties,
308 });
309 }
310 result
311 }
312
313 fn is_writable_property(object_type: ObjectType, pid: PropertyIdentifier) -> bool {
315 if pid == PropertyIdentifier::OBJECT_IDENTIFIER
317 || pid == PropertyIdentifier::OBJECT_TYPE
318 || pid == PropertyIdentifier::PROPERTY_LIST
319 || pid == PropertyIdentifier::STATUS_FLAGS
320 {
321 return false;
322 }
323
324 if pid == PropertyIdentifier::OBJECT_NAME {
326 return true;
327 }
328
329 if pid == PropertyIdentifier::PRESENT_VALUE {
331 return object_type != ObjectType::ANALOG_INPUT
332 && object_type != ObjectType::BINARY_INPUT
333 && object_type != ObjectType::MULTI_STATE_INPUT;
334 }
335
336 pid == PropertyIdentifier::DESCRIPTION
338 || pid == PropertyIdentifier::OUT_OF_SERVICE
339 || pid == PropertyIdentifier::COV_INCREMENT
340 || pid == PropertyIdentifier::HIGH_LIMIT
341 || pid == PropertyIdentifier::LOW_LIMIT
342 || pid == PropertyIdentifier::DEADBAND
343 || pid == PropertyIdentifier::NOTIFICATION_CLASS
344 }
345
346 fn is_createable(object_type: ObjectType) -> bool {
347 object_type != ObjectType::DEVICE && object_type != ObjectType::NETWORK_PORT
349 }
350
351 fn is_deleteable(object_type: ObjectType) -> bool {
352 object_type != ObjectType::DEVICE && object_type != ObjectType::NETWORK_PORT
353 }
354
355 fn build_services(&self) -> Vec<ServiceSupport> {
357 let mut services = Vec::new();
358
359 let executor_services = [
361 "ReadProperty",
362 "WriteProperty",
363 "ReadPropertyMultiple",
364 "WritePropertyMultiple",
365 "SubscribeCOV",
366 "SubscribeCOVProperty",
367 "CreateObject",
368 "DeleteObject",
369 "DeviceCommunicationControl",
370 "ReinitializeDevice",
371 "GetEventInformation",
372 "AcknowledgeAlarm",
373 "ReadRange",
374 "AtomicReadFile",
375 "AtomicWriteFile",
376 "AddListElement",
377 "RemoveListElement",
378 ];
379
380 let initiator_services = ["ConfirmedCOVNotification", "ConfirmedEventNotification"];
382
383 let unconfirmed_executor = [
385 "WhoIs",
386 "WhoHas",
387 "TimeSynchronization",
388 "UTCTimeSynchronization",
389 ];
390
391 let unconfirmed_initiator = [
393 "I-Am",
394 "I-Have",
395 "UnconfirmedCOVNotification",
396 "UnconfirmedEventNotification",
397 ];
398
399 let mut service_map: BTreeMap<&str, (bool, bool)> = BTreeMap::new();
401 for name in &executor_services {
402 service_map.entry(name).or_default().1 = true;
403 }
404 for name in &initiator_services {
405 service_map.entry(name).or_default().0 = true;
406 }
407 for name in &unconfirmed_executor {
408 service_map.entry(name).or_default().1 = true;
409 }
410 for name in &unconfirmed_initiator {
411 service_map.entry(name).or_default().0 = true;
412 }
413
414 for (name, (initiator, executor)) in &service_map {
415 services.push(ServiceSupport {
416 service_name: (*name).to_string(),
417 initiator: *initiator,
418 executor: *executor,
419 });
420 }
421
422 services
423 }
424}
425
426impl Pics {
429 pub fn generate_text(&self) -> String {
431 let mut out = String::with_capacity(4096);
432
433 out.push_str("=== BACnet Protocol Implementation Conformance Statement (PICS) ===\n");
434 out.push_str(" Per ASHRAE 135-2020 Annex A\n\n");
435
436 out.push_str("--- Vendor Information ---\n");
438 out.push_str(&format!(
439 "Vendor ID: {}\n",
440 self.vendor_info.vendor_id
441 ));
442 out.push_str(&format!(
443 "Vendor Name: {}\n",
444 self.vendor_info.vendor_name
445 ));
446 out.push_str(&format!(
447 "Model Name: {}\n",
448 self.vendor_info.model_name
449 ));
450 out.push_str(&format!(
451 "Firmware Revision: {}\n",
452 self.vendor_info.firmware_revision
453 ));
454 out.push_str(&format!(
455 "Application Software Version: {}\n",
456 self.vendor_info.application_software_version
457 ));
458 out.push_str(&format!(
459 "Protocol Version: {}\n",
460 self.vendor_info.protocol_version
461 ));
462 out.push_str(&format!(
463 "Protocol Revision: {}\n\n",
464 self.vendor_info.protocol_revision
465 ));
466
467 out.push_str("--- BACnet Device Profile ---\n");
469 out.push_str(&format!("Profile: {}\n\n", self.device_profile));
470
471 out.push_str("--- Supported Object Types ---\n");
473 for ot in &self.supported_object_types {
474 out.push_str(&format!(
475 "\n Object Type: {} (createable={}, deleteable={})\n",
476 ot.object_type, ot.createable, ot.deleteable
477 ));
478 out.push_str(" Properties:\n");
479 for prop in &ot.supported_properties {
480 out.push_str(&format!(
481 " {:<40} {}\n",
482 prop.property_id.to_string(),
483 prop.access
484 ));
485 }
486 }
487 out.push('\n');
488
489 out.push_str("--- Supported Services ---\n");
491 out.push_str(&format!(
492 " {:<45} {:>9} {:>9}\n",
493 "Service", "Initiator", "Executor"
494 ));
495 out.push_str(&format!(" {:-<45} {:-<9} {:-<9}\n", "", "", ""));
496 for svc in &self.supported_services {
497 let init = if svc.initiator { "Yes" } else { "No" };
498 let exec = if svc.executor { "Yes" } else { "No" };
499 out.push_str(&format!(
500 " {:<45} {:>9} {:>9}\n",
501 svc.service_name, init, exec
502 ));
503 }
504 out.push('\n');
505
506 out.push_str("--- Data Link Layer Support ---\n");
508 for dl in &self.data_link_layers {
509 out.push_str(&format!(" {dl}\n"));
510 }
511 out.push('\n');
512
513 out.push_str("--- Network Layer Options ---\n");
515 out.push_str(&format!(
516 " Router: {}\n",
517 self.network_layer.router
518 ));
519 out.push_str(&format!(" BBMD: {}\n", self.network_layer.bbmd));
520 out.push_str(&format!(
521 " Foreign Device: {}\n\n",
522 self.network_layer.foreign_device
523 ));
524
525 out.push_str("--- Character Sets Supported ---\n");
527 for cs in &self.character_sets {
528 out.push_str(&format!(" {cs}\n"));
529 }
530 out.push('\n');
531
532 if !self.special_functionality.is_empty() {
534 out.push_str("--- Special Functionality ---\n");
535 for sf in &self.special_functionality {
536 out.push_str(&format!(" {sf}\n"));
537 }
538 out.push('\n');
539 }
540
541 out
542 }
543
544 pub fn generate_markdown(&self) -> String {
546 let mut out = String::with_capacity(4096);
547
548 out.push_str("# BACnet Protocol Implementation Conformance Statement (PICS)\n\n");
549 out.push_str("*Per ASHRAE 135-2020 Annex A*\n\n");
550
551 out.push_str("## Vendor Information\n\n");
553 out.push_str("| Field | Value |\n");
554 out.push_str("|-------|-------|\n");
555 out.push_str(&format!("| Vendor ID | {} |\n", self.vendor_info.vendor_id));
556 out.push_str(&format!(
557 "| Vendor Name | {} |\n",
558 self.vendor_info.vendor_name
559 ));
560 out.push_str(&format!(
561 "| Model Name | {} |\n",
562 self.vendor_info.model_name
563 ));
564 out.push_str(&format!(
565 "| Firmware Revision | {} |\n",
566 self.vendor_info.firmware_revision
567 ));
568 out.push_str(&format!(
569 "| Application Software Version | {} |\n",
570 self.vendor_info.application_software_version
571 ));
572 out.push_str(&format!(
573 "| Protocol Version | {} |\n",
574 self.vendor_info.protocol_version
575 ));
576 out.push_str(&format!(
577 "| Protocol Revision | {} |\n\n",
578 self.vendor_info.protocol_revision
579 ));
580
581 out.push_str("## BACnet Device Profile\n\n");
583 out.push_str(&format!("**{}**\n\n", self.device_profile));
584
585 out.push_str("## Supported Object Types\n\n");
587 for ot in &self.supported_object_types {
588 out.push_str(&format!(
589 "### {}\n\n- Createable: {}\n- Deleteable: {}\n\n",
590 ot.object_type, ot.createable, ot.deleteable
591 ));
592 out.push_str("| Property | Access |\n");
593 out.push_str("|----------|--------|\n");
594 for prop in &ot.supported_properties {
595 out.push_str(&format!("| {} | {} |\n", prop.property_id, prop.access));
596 }
597 out.push('\n');
598 }
599
600 out.push_str("## Supported Services\n\n");
602 out.push_str("| Service | Initiator | Executor |\n");
603 out.push_str("|---------|-----------|----------|\n");
604 for svc in &self.supported_services {
605 let init = if svc.initiator { "Yes" } else { "No" };
606 let exec = if svc.executor { "Yes" } else { "No" };
607 out.push_str(&format!("| {} | {} | {} |\n", svc.service_name, init, exec));
608 }
609 out.push('\n');
610
611 out.push_str("## Data Link Layer Support\n\n");
613 for dl in &self.data_link_layers {
614 out.push_str(&format!("- {dl}\n"));
615 }
616 out.push('\n');
617
618 out.push_str("## Network Layer Options\n\n");
620 out.push_str("| Feature | Supported |\n");
621 out.push_str("|---------|-----------|\n");
622 out.push_str(&format!("| Router | {} |\n", self.network_layer.router));
623 out.push_str(&format!("| BBMD | {} |\n", self.network_layer.bbmd));
624 out.push_str(&format!(
625 "| Foreign Device | {} |\n\n",
626 self.network_layer.foreign_device
627 ));
628
629 out.push_str("## Character Sets Supported\n\n");
631 for cs in &self.character_sets {
632 out.push_str(&format!("- {cs}\n"));
633 }
634 out.push('\n');
635
636 if !self.special_functionality.is_empty() {
638 out.push_str("## Special Functionality\n\n");
639 for sf in &self.special_functionality {
640 out.push_str(&format!("- {sf}\n"));
641 }
642 out.push('\n');
643 }
644
645 out
646 }
647}
648
649pub fn generate_pics(
655 db: &ObjectDatabase,
656 server_config: &ServerConfig,
657 pics_config: &PicsConfig,
658) -> Pics {
659 PicsGenerator::new(db, server_config, pics_config).generate()
660}
661
662#[cfg(test)]
665mod tests {
666 use std::borrow::Cow;
667
668 use bacnet_objects::traits::BACnetObject;
669 use bacnet_types::enums::{ErrorClass, ErrorCode, ObjectType, PropertyIdentifier};
670 use bacnet_types::error::Error;
671 use bacnet_types::primitives::{ObjectIdentifier, PropertyValue};
672
673 use super::*;
674
675 struct TestAnalogInput {
678 oid: ObjectIdentifier,
679 name: String,
680 }
681
682 impl BACnetObject for TestAnalogInput {
683 fn object_identifier(&self) -> ObjectIdentifier {
684 self.oid
685 }
686 fn object_name(&self) -> &str {
687 &self.name
688 }
689 fn read_property(
690 &self,
691 _property: PropertyIdentifier,
692 _array_index: Option<u32>,
693 ) -> Result<PropertyValue, Error> {
694 Ok(PropertyValue::Real(0.0))
695 }
696 fn write_property(
697 &mut self,
698 _property: PropertyIdentifier,
699 _array_index: Option<u32>,
700 _value: PropertyValue,
701 _priority: Option<u8>,
702 ) -> Result<(), Error> {
703 Err(Error::Protocol {
704 class: ErrorClass::PROPERTY.to_raw() as u32,
705 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
706 })
707 }
708 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
709 static PROPS: [PropertyIdentifier; 8] = [
710 PropertyIdentifier::OBJECT_IDENTIFIER,
711 PropertyIdentifier::OBJECT_NAME,
712 PropertyIdentifier::OBJECT_TYPE,
713 PropertyIdentifier::PROPERTY_LIST,
714 PropertyIdentifier::PRESENT_VALUE,
715 PropertyIdentifier::STATUS_FLAGS,
716 PropertyIdentifier::OUT_OF_SERVICE,
717 PropertyIdentifier::UNITS,
718 ];
719 Cow::Borrowed(&PROPS)
720 }
721 }
722
723 struct TestBinaryValue {
724 oid: ObjectIdentifier,
725 name: String,
726 }
727
728 impl BACnetObject for TestBinaryValue {
729 fn object_identifier(&self) -> ObjectIdentifier {
730 self.oid
731 }
732 fn object_name(&self) -> &str {
733 &self.name
734 }
735 fn read_property(
736 &self,
737 _property: PropertyIdentifier,
738 _array_index: Option<u32>,
739 ) -> Result<PropertyValue, Error> {
740 Ok(PropertyValue::Boolean(false))
741 }
742 fn write_property(
743 &mut self,
744 _property: PropertyIdentifier,
745 _array_index: Option<u32>,
746 _value: PropertyValue,
747 _priority: Option<u8>,
748 ) -> Result<(), Error> {
749 Ok(())
750 }
751 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
752 static PROPS: [PropertyIdentifier; 6] = [
753 PropertyIdentifier::OBJECT_IDENTIFIER,
754 PropertyIdentifier::OBJECT_NAME,
755 PropertyIdentifier::OBJECT_TYPE,
756 PropertyIdentifier::PROPERTY_LIST,
757 PropertyIdentifier::PRESENT_VALUE,
758 PropertyIdentifier::STATUS_FLAGS,
759 ];
760 Cow::Borrowed(&PROPS)
761 }
762 }
763
764 struct TestDevice {
765 oid: ObjectIdentifier,
766 name: String,
767 }
768
769 impl BACnetObject for TestDevice {
770 fn object_identifier(&self) -> ObjectIdentifier {
771 self.oid
772 }
773 fn object_name(&self) -> &str {
774 &self.name
775 }
776 fn read_property(
777 &self,
778 _property: PropertyIdentifier,
779 _array_index: Option<u32>,
780 ) -> Result<PropertyValue, Error> {
781 Ok(PropertyValue::Unsigned(0))
782 }
783 fn write_property(
784 &mut self,
785 _property: PropertyIdentifier,
786 _array_index: Option<u32>,
787 _value: PropertyValue,
788 _priority: Option<u8>,
789 ) -> Result<(), Error> {
790 Err(Error::Protocol {
791 class: ErrorClass::PROPERTY.to_raw() as u32,
792 code: ErrorCode::WRITE_ACCESS_DENIED.to_raw() as u32,
793 })
794 }
795 fn property_list(&self) -> Cow<'static, [PropertyIdentifier]> {
796 static PROPS: [PropertyIdentifier; 6] = [
797 PropertyIdentifier::OBJECT_IDENTIFIER,
798 PropertyIdentifier::OBJECT_NAME,
799 PropertyIdentifier::OBJECT_TYPE,
800 PropertyIdentifier::PROPERTY_LIST,
801 PropertyIdentifier::PROTOCOL_VERSION,
802 PropertyIdentifier::PROTOCOL_REVISION,
803 ];
804 Cow::Borrowed(&PROPS)
805 }
806 }
807
808 fn make_test_db() -> ObjectDatabase {
811 let mut db = ObjectDatabase::new();
812 db.add(Box::new(TestDevice {
813 oid: ObjectIdentifier::new(ObjectType::DEVICE, 1).unwrap(),
814 name: "Test Device".into(),
815 }))
816 .unwrap();
817 db.add(Box::new(TestAnalogInput {
818 oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 1).unwrap(),
819 name: "AI-1".into(),
820 }))
821 .unwrap();
822 db.add(Box::new(TestAnalogInput {
823 oid: ObjectIdentifier::new(ObjectType::ANALOG_INPUT, 2).unwrap(),
824 name: "AI-2".into(),
825 }))
826 .unwrap();
827 db.add(Box::new(TestBinaryValue {
828 oid: ObjectIdentifier::new(ObjectType::BINARY_VALUE, 1).unwrap(),
829 name: "BV-1".into(),
830 }))
831 .unwrap();
832 db
833 }
834
835 fn make_pics_config() -> PicsConfig {
836 PicsConfig {
837 vendor_name: "Acme Corp".into(),
838 model_name: "BACnet Controller 3000".into(),
839 firmware_revision: "1.0.0".into(),
840 application_software_version: "2.0.0".into(),
841 protocol_version: 1,
842 protocol_revision: 24,
843 device_profile: DeviceProfile::BAsc,
844 data_link_layers: vec![DataLinkSupport::BipV4],
845 network_layer: NetworkLayerSupport {
846 router: false,
847 bbmd: false,
848 foreign_device: false,
849 },
850 character_sets: vec![CharacterSet::Utf8],
851 special_functionality: vec!["Intrinsic event reporting".into()],
852 }
853 }
854
855 #[test]
858 fn generate_pics_basic() {
859 let db = make_test_db();
860 let server_config = ServerConfig {
861 vendor_id: 999,
862 ..ServerConfig::default()
863 };
864 let pics_config = make_pics_config();
865 let pics = generate_pics(&db, &server_config, &pics_config);
866
867 assert_eq!(pics.vendor_info.vendor_id, 999);
868 assert_eq!(pics.vendor_info.vendor_name, "Acme Corp");
869 assert_eq!(pics.device_profile, DeviceProfile::BAsc);
870 assert_eq!(pics.character_sets, vec![CharacterSet::Utf8]);
871 assert_eq!(pics.data_link_layers, vec![DataLinkSupport::BipV4]);
872 }
873
874 #[test]
875 fn all_object_types_listed() {
876 let db = make_test_db();
877 let server_config = ServerConfig::default();
878 let pics_config = make_pics_config();
879 let pics = generate_pics(&db, &server_config, &pics_config);
880
881 let types: Vec<ObjectType> = pics
882 .supported_object_types
883 .iter()
884 .map(|ot| ot.object_type)
885 .collect();
886 assert!(types.contains(&ObjectType::DEVICE));
887 assert!(types.contains(&ObjectType::ANALOG_INPUT));
888 assert!(types.contains(&ObjectType::BINARY_VALUE));
889 assert_eq!(types.len(), 3);
891 }
892
893 #[test]
894 fn object_type_properties_populated() {
895 let db = make_test_db();
896 let server_config = ServerConfig::default();
897 let pics_config = make_pics_config();
898 let pics = generate_pics(&db, &server_config, &pics_config);
899
900 let ai = pics
901 .supported_object_types
902 .iter()
903 .find(|ot| ot.object_type == ObjectType::ANALOG_INPUT)
904 .expect("ANALOG_INPUT should be in PICS");
905
906 assert_eq!(ai.supported_properties.len(), 8);
908
909 let pv = ai
911 .supported_properties
912 .iter()
913 .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
914 .expect("PRESENT_VALUE should exist");
915 assert!(pv.access.readable);
916 assert!(!pv.access.writable);
917 }
918
919 #[test]
920 fn device_not_createable_or_deleteable() {
921 let db = make_test_db();
922 let server_config = ServerConfig::default();
923 let pics_config = make_pics_config();
924 let pics = generate_pics(&db, &server_config, &pics_config);
925
926 let dev = pics
927 .supported_object_types
928 .iter()
929 .find(|ot| ot.object_type == ObjectType::DEVICE)
930 .expect("DEVICE should be in PICS");
931 assert!(!dev.createable);
932 assert!(!dev.deleteable);
933 }
934
935 #[test]
936 fn services_match_implementation() {
937 let db = make_test_db();
938 let server_config = ServerConfig::default();
939 let pics_config = make_pics_config();
940 let pics = generate_pics(&db, &server_config, &pics_config);
941
942 let service_names: Vec<&str> = pics
943 .supported_services
944 .iter()
945 .map(|s| s.service_name.as_str())
946 .collect();
947
948 assert!(service_names.contains(&"ReadProperty"));
950 assert!(service_names.contains(&"WriteProperty"));
951 assert!(service_names.contains(&"ReadPropertyMultiple"));
952 assert!(service_names.contains(&"SubscribeCOV"));
953 assert!(service_names.contains(&"CreateObject"));
954 assert!(service_names.contains(&"DeleteObject"));
955 assert!(service_names.contains(&"WhoIs"));
956
957 assert!(service_names.contains(&"I-Am"));
959 assert!(service_names.contains(&"ConfirmedCOVNotification"));
960
961 let rp = pics
963 .supported_services
964 .iter()
965 .find(|s| s.service_name == "ReadProperty")
966 .expect("ReadProperty should be listed");
967 assert!(!rp.initiator);
968 assert!(rp.executor);
969
970 let iam = pics
972 .supported_services
973 .iter()
974 .find(|s| s.service_name == "I-Am")
975 .expect("I-Am should be listed");
976 assert!(iam.initiator);
977 assert!(!iam.executor);
978 }
979
980 #[test]
981 fn text_output_contains_key_sections() {
982 let db = make_test_db();
983 let server_config = ServerConfig {
984 vendor_id: 42,
985 ..ServerConfig::default()
986 };
987 let pics_config = make_pics_config();
988 let pics = generate_pics(&db, &server_config, &pics_config);
989 let text = pics.generate_text();
990
991 assert!(text.contains("Protocol Implementation Conformance Statement"));
992 assert!(text.contains("Vendor ID:"));
993 assert!(text.contains("42"));
994 assert!(text.contains("Acme Corp"));
995 assert!(text.contains("B-ASC"));
996 assert!(text.contains("Supported Object Types"));
997 assert!(text.contains("ANALOG_INPUT"));
998 assert!(text.contains("Supported Services"));
999 assert!(text.contains("ReadProperty"));
1000 assert!(text.contains("Data Link Layer Support"));
1001 assert!(text.contains("BACnet/IP (Annex J)"));
1002 assert!(text.contains("Character Sets Supported"));
1003 assert!(text.contains("UTF-8"));
1004 assert!(text.contains("Special Functionality"));
1005 assert!(text.contains("Intrinsic event reporting"));
1006 }
1007
1008 #[test]
1009 fn markdown_output_has_tables() {
1010 let db = make_test_db();
1011 let server_config = ServerConfig::default();
1012 let pics_config = make_pics_config();
1013 let pics = generate_pics(&db, &server_config, &pics_config);
1014 let md = pics.generate_markdown();
1015
1016 assert!(md.contains("# BACnet Protocol Implementation Conformance Statement"));
1017 assert!(md.contains("| Field | Value |"));
1018 assert!(md.contains("| Service | Initiator | Executor |"));
1019 assert!(md.contains("| Property | Access |"));
1020 assert!(md.contains("## Supported Object Types"));
1021 assert!(md.contains("## Supported Services"));
1022 }
1023
1024 #[test]
1025 fn empty_database_produces_empty_object_list() {
1026 let db = ObjectDatabase::new();
1027 let server_config = ServerConfig::default();
1028 let pics_config = PicsConfig::default();
1029 let pics = generate_pics(&db, &server_config, &pics_config);
1030
1031 assert!(pics.supported_object_types.is_empty());
1032 assert!(!pics.supported_services.is_empty());
1034 }
1035
1036 #[test]
1037 fn device_profile_display() {
1038 assert_eq!(DeviceProfile::BAac.to_string(), "B-AAC");
1039 assert_eq!(DeviceProfile::BAsc.to_string(), "B-ASC");
1040 assert_eq!(DeviceProfile::BOws.to_string(), "B-OWS");
1041 assert_eq!(DeviceProfile::BBc.to_string(), "B-BC");
1042 assert_eq!(DeviceProfile::BOp.to_string(), "B-OP");
1043 assert_eq!(DeviceProfile::BRouter.to_string(), "B-ROUTER");
1044 assert_eq!(DeviceProfile::BGw.to_string(), "B-GW");
1045 assert_eq!(DeviceProfile::BSc.to_string(), "B-SC");
1046 assert_eq!(
1047 DeviceProfile::Custom("MyProfile".into()).to_string(),
1048 "MyProfile"
1049 );
1050 }
1051
1052 #[test]
1053 fn property_access_display() {
1054 let rw = PropertyAccess {
1055 readable: true,
1056 writable: true,
1057 optional: false,
1058 };
1059 assert_eq!(rw.to_string(), "RW");
1060
1061 let ro = PropertyAccess {
1062 readable: true,
1063 writable: false,
1064 optional: true,
1065 };
1066 assert_eq!(ro.to_string(), "RO");
1067
1068 let wo = PropertyAccess {
1069 readable: false,
1070 writable: true,
1071 optional: false,
1072 };
1073 assert_eq!(wo.to_string(), "W");
1074 }
1075
1076 #[test]
1077 fn binary_value_present_value_is_writable() {
1078 let db = make_test_db();
1079 let server_config = ServerConfig::default();
1080 let pics_config = make_pics_config();
1081 let pics = generate_pics(&db, &server_config, &pics_config);
1082
1083 let bv = pics
1084 .supported_object_types
1085 .iter()
1086 .find(|ot| ot.object_type == ObjectType::BINARY_VALUE)
1087 .expect("BINARY_VALUE should be in PICS");
1088
1089 let pv = bv
1090 .supported_properties
1091 .iter()
1092 .find(|p| p.property_id == PropertyIdentifier::PRESENT_VALUE)
1093 .expect("PRESENT_VALUE should exist on BV");
1094 assert!(
1095 pv.access.writable,
1096 "BinaryValue PRESENT_VALUE should be writable"
1097 );
1098 }
1099}