1use std::collections::BTreeMap;
7use std::fmt;
8
9use bacnet_encoding::primitives::charset;
10use bacnet_objects::database::ObjectDatabase;
11use bacnet_objects::traits::BACnetObject;
12use bacnet_types::bitstring::ServicesSupported;
13use bacnet_types::enums::{ObjectType, PropertyIdentifier, ServiceSupported};
14use bacnet_types::primitives::PropertyValue;
15use bacnet_types::{PROTOCOL_REVISION, PROTOCOL_VERSION};
16
17use crate::local_device::announcement_device;
18use crate::server::ServerConfig;
19
20#[derive(Debug, Clone)]
24pub struct Pics {
25 pub vendor_info: VendorInfo,
27 pub device_profile: DeviceProfile,
29 pub supported_object_types: Vec<ObjectTypeSupport>,
31 pub supported_services: Vec<ServiceSupport>,
33 pub data_link_layers: Vec<DataLinkSupport>,
35 pub network_layer: NetworkLayerSupport,
37 pub character_sets: Vec<CharacterSet>,
39 pub special_functionality: Vec<String>,
41}
42
43#[derive(Debug, Clone)]
45pub struct VendorInfo {
46 pub vendor_id: u16,
49 pub vendor_name: String,
51 pub model_name: String,
53 pub firmware_revision: String,
55 pub application_software_version: String,
57 pub protocol_version: u16,
60 pub protocol_revision: u16,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
67pub enum DeviceProfile {
68 BAac,
70 BAsc,
72 BOws,
74 BBc,
76 BOp,
78 BRouter,
80 BGw,
82 BSc,
84 Custom(String),
86}
87
88impl fmt::Display for DeviceProfile {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 match self {
91 Self::BAac => f.write_str("B-AAC"),
92 Self::BAsc => f.write_str("B-ASC"),
93 Self::BOws => f.write_str("B-OWS"),
94 Self::BBc => f.write_str("B-BC"),
95 Self::BOp => f.write_str("B-OP"),
96 Self::BRouter => f.write_str("B-ROUTER"),
97 Self::BGw => f.write_str("B-GW"),
98 Self::BSc => f.write_str("B-SC"),
99 Self::Custom(s) => f.write_str(s),
100 }
101 }
102}
103
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct PropertyAccess {
109 pub readable: bool,
111 pub writable: bool,
113 pub optional: bool,
115}
116
117impl fmt::Display for PropertyAccess {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 let r = if self.readable { "R" } else { "" };
120 let w = if self.writable { "W" } else { "" };
121 let o = if self.optional { "O" } else { "" };
122 write!(f, "{r}{w}{o}")
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct PropertySupport {
129 pub property_id: PropertyIdentifier,
131 pub access: PropertyAccess,
133 pub written_through: Option<PropertyIdentifier>,
137}
138
139#[derive(Debug, Clone)]
141pub struct ObjectTypeSupport {
142 pub object_type: ObjectType,
144 pub createable: bool,
146 pub creation_only_properties: Vec<PropertyIdentifier>,
152 pub deleteable: bool,
154 pub supported_properties: Vec<PropertySupport>,
156}
157
158#[derive(Debug, Clone)]
160pub struct ServiceSupport {
161 pub service_name: String,
163 pub initiator: bool,
165 pub executor: bool,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
174#[non_exhaustive]
175pub enum DataLinkSupport {
176 BipV4,
178 BipV4NatTraversal,
182 BipV6,
184 Mstp,
186 Ethernet,
188 BacnetSc,
190}
191
192impl fmt::Display for DataLinkSupport {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Self::BipV4 => f.write_str("BACnet/IP (Annex J)"),
196 Self::BipV4NatTraversal => f.write_str("BACnet/IP with NAT traversal (Annex J.7.5)"),
197 Self::BipV6 => f.write_str("BACnet/IPv6 (Annex U)"),
198 Self::Mstp => f.write_str("MS/TP (Clause 9)"),
199 Self::Ethernet => f.write_str("BACnet Ethernet (Clause 7)"),
200 Self::BacnetSc => f.write_str("BACnet/SC (Annex AB)"),
201 }
202 }
203}
204
205#[derive(Debug, Clone)]
207pub struct NetworkLayerSupport {
208 pub router: bool,
210 pub bbmd: bool,
212 pub foreign_device: bool,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
223#[repr(u8)]
224pub enum CharacterSet {
225 Utf8 = charset::UTF8,
227 IbmMicrosoftDbcs = charset::IBM_MICROSOFT_DBCS,
229 JisX0208 = charset::JIS_X_0208,
231 Ucs4 = charset::UCS4,
233 Ucs2 = charset::UCS2,
235 Iso8859_1 = charset::ISO_8859_1,
237}
238
239impl CharacterSet {
240 pub const ALL: [Self; 6] = [
242 Self::Utf8,
243 Self::IbmMicrosoftDbcs,
244 Self::JisX0208,
245 Self::Ucs4,
246 Self::Ucs2,
247 Self::Iso8859_1,
248 ];
249
250 pub const fn code(self) -> u8 {
252 self as u8
253 }
254}
255
256impl fmt::Display for CharacterSet {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 f.write_str(match self {
259 Self::Utf8 => "ISO 10646 (UTF-8)",
260 Self::IbmMicrosoftDbcs => "IBM/Microsoft DBCS",
261 Self::JisX0208 => "JIS X 0208",
262 Self::Ucs4 => "ISO 10646 (UCS-4)",
263 Self::Ucs2 => "ISO 10646 (UCS-2)",
264 Self::Iso8859_1 => "ISO 8859-1",
265 })
266 }
267}
268
269#[derive(Debug, Clone)]
273pub struct PicsConfig {
274 pub device_profile: DeviceProfile,
276 pub data_link_layers: Vec<DataLinkSupport>,
278 pub network_layer: NetworkLayerSupport,
280 pub character_sets: Vec<CharacterSet>,
282 pub special_functionality: Vec<String>,
284}
285
286impl Default for PicsConfig {
287 fn default() -> Self {
288 Self {
289 device_profile: DeviceProfile::BAsc,
290 data_link_layers: vec![DataLinkSupport::BipV4],
291 network_layer: NetworkLayerSupport {
292 router: false,
293 bbmd: false,
294 foreign_device: false,
295 },
296 character_sets: vec![CharacterSet::Utf8],
297 special_functionality: Vec::new(),
298 }
299 }
300}
301
302pub struct PicsGenerator<'a> {
308 db: &'a ObjectDatabase,
309 server_config: &'a ServerConfig,
310 pics_config: &'a PicsConfig,
311 served: bool,
312}
313
314impl<'a> PicsGenerator<'a> {
315 pub fn new(
317 db: &'a ObjectDatabase,
318 server_config: &'a ServerConfig,
319 pics_config: &'a PicsConfig,
320 ) -> Self {
321 Self {
322 db,
323 server_config,
324 pics_config,
325 served: false,
326 }
327 }
328
329 pub(crate) fn for_server(mut self) -> Self {
330 self.served = true;
331 self
332 }
333
334 pub fn generate(&self) -> Pics {
352 Pics {
353 vendor_info: self.build_vendor_info(),
354 device_profile: self.pics_config.device_profile.clone(),
355 supported_object_types: self.build_object_types(),
356 supported_services: self.build_services(),
357 data_link_layers: self.pics_config.data_link_layers.clone(),
358 network_layer: self.pics_config.network_layer.clone(),
359 character_sets: self.pics_config.character_sets.clone(),
360 special_functionality: self.pics_config.special_functionality.clone(),
361 }
362 }
363
364 fn build_vendor_info(&self) -> VendorInfo {
365 let device = (self.served)
371 .then(|| announcement_device(self.db, &self.server_config.device))
372 .flatten()
373 .or_else(|| self.db.selected_device())
374 .and_then(|oid| self.db.get(&oid));
375 let view = crate::device_view::DeviceReadContext::new(
376 self.db,
377 crate::device_view::DeviceExecution::FullServer,
378 )
379 .with_identity(self.served.then_some(&self.server_config.device));
380 let read = |property| device.map(|d| view.object(d).read_property(property, None));
381 let unsigned = |property, fallback| match read(property) {
382 Some(Ok(PropertyValue::Unsigned(raw))) => u16::try_from(raw).unwrap_or(fallback),
383 _ => fallback,
384 };
385 let text = |property| match read(property) {
386 Some(Ok(PropertyValue::CharacterString(value))) => value,
387 _ => String::new(),
388 };
389 VendorInfo {
390 vendor_id: unsigned(
391 PropertyIdentifier::VENDOR_IDENTIFIER,
392 self.server_config.device.vendor_id(),
393 ),
394 vendor_name: text(PropertyIdentifier::VENDOR_NAME),
395 model_name: text(PropertyIdentifier::MODEL_NAME),
396 firmware_revision: text(PropertyIdentifier::FIRMWARE_REVISION),
397 application_software_version: text(PropertyIdentifier::APPLICATION_SOFTWARE_VERSION),
398 protocol_version: unsigned(PropertyIdentifier::PROTOCOL_VERSION, PROTOCOL_VERSION),
399 protocol_revision: unsigned(PropertyIdentifier::PROTOCOL_REVISION, PROTOCOL_REVISION),
400 }
401 }
402
403 fn build_object_types(&self) -> Vec<ObjectTypeSupport> {
404 let mut by_type: BTreeMap<u32, Vec<&dyn bacnet_objects::traits::BACnetObject>> =
405 BTreeMap::new();
406 for (_oid, obj) in self.db.iter_objects() {
407 by_type
408 .entry(obj.object_identifier().object_type().to_raw())
409 .or_default()
410 .push(obj);
411 }
412
413 let mut result = Vec::with_capacity(by_type.len());
414 for (raw_type, objects) in &by_type {
415 let object_type = ObjectType::from_raw(*raw_type);
416 let representative = objects[0];
417 let view = (self.served && object_type == ObjectType::DEVICE).then(|| {
418 crate::device_view::DeviceReadContext::new(
419 self.db,
420 crate::device_view::DeviceExecution::FullServer,
421 )
422 .with_identity(Some(&self.server_config.device))
423 });
424 let supported_properties =
425 Self::union_property_support(objects.iter().flat_map(|object| {
426 if let Some(view) = &view {
427 Self::object_property_support(&view.object(*object))
428 } else {
429 Self::object_property_support(*object)
430 }
431 }));
432
433 let createable = representative.is_createable();
437 let deleteable = representative.is_deleteable();
438 let written_through = |property: &PropertyIdentifier| {
439 supported_properties
440 .iter()
441 .any(|row| row.property_id == *property && row.written_through.is_some())
442 };
443 let creation_only_properties = if createable {
444 let mut properties = representative.creation_only_properties().to_vec();
445 properties.retain(|property| !written_through(property));
446 properties
447 } else {
448 Vec::new()
449 };
450
451 result.push(ObjectTypeSupport {
452 object_type,
453 createable,
454 creation_only_properties,
455 deleteable,
456 supported_properties,
457 });
458 }
459 result
460 }
461
462 fn object_property_support(
463 object: &dyn bacnet_objects::traits::BACnetObject,
464 ) -> Vec<PropertySupport> {
465 let metadata = object.property_metadata();
466 if metadata.is_empty() {
467 let all_props = object.property_list();
468 let required = object.required_properties();
469 all_props
470 .iter()
471 .map(|&property_id| PropertySupport {
472 property_id,
473 access: PropertyAccess {
474 readable: true,
475 writable: object.is_writable_property(property_id),
476 optional: !required.contains(&property_id),
477 },
478 written_through: None,
479 })
480 .collect()
481 } else {
482 metadata
483 .iter()
484 .map(|row| PropertySupport {
485 property_id: row.property_identifier,
486 access: PropertyAccess {
487 readable: true,
488 writable: row.write_capability.is_writable(),
489 optional: !row.is_required(),
490 },
491 written_through: row.write_capability.written_through(),
492 })
493 .collect()
494 }
495 }
496
497 fn union_property_support(
498 rows: impl IntoIterator<Item = PropertySupport>,
499 ) -> Vec<PropertySupport> {
500 let mut properties: BTreeMap<u32, PropertySupport> = BTreeMap::new();
504 for row in rows {
505 properties
506 .entry(row.property_id.to_raw())
507 .and_modify(|existing| {
508 existing.access.readable |= row.access.readable;
509 existing.access.writable |= row.access.writable;
510 existing.access.optional &= row.access.optional;
511 existing.written_through = existing.written_through.or(row.written_through);
512 })
513 .or_insert(row);
514 }
515 properties.into_values().collect()
516 }
517
518 pub const INITIATED_SERVICES: &'static [ServiceSupported] = &[
532 ServiceSupported::I_AM,
533 ServiceSupported::I_HAVE,
534 ServiceSupported::CONFIRMED_COV_NOTIFICATION,
535 ServiceSupported::CONFIRMED_EVENT_NOTIFICATION,
536 ServiceSupported::UNCONFIRMED_COV_NOTIFICATION,
537 ServiceSupported::UNCONFIRMED_EVENT_NOTIFICATION,
538 ServiceSupported::CONFIRMED_COV_NOTIFICATION_MULTIPLE,
539 ServiceSupported::UNCONFIRMED_COV_NOTIFICATION_MULTIPLE,
540 ServiceSupported::CONFIRMED_AUDIT_NOTIFICATION,
541 ServiceSupported::UNCONFIRMED_AUDIT_NOTIFICATION,
542 ServiceSupported::READ_PROPERTY,
543 ServiceSupported::WRITE_PROPERTY,
544 ServiceSupported::WHO_IS,
545 ];
546
547 fn build_services(&self) -> Vec<ServiceSupport> {
548 let effective_executed = (!self.served)
553 .then(|| {
554 let device = self.db.get(&self.db.selected_device()?)?;
555 match device
556 .read_property(PropertyIdentifier::PROTOCOL_SERVICES_SUPPORTED, None)
557 .ok()?
558 {
559 PropertyValue::BitString { data, .. } => {
560 Some(ServicesSupported::from_bacnet(&data))
561 }
562 _ => None,
563 }
564 })
565 .flatten();
566 let mut service_map: BTreeMap<&'static str, (bool, bool)> = BTreeMap::new();
567 let clock_available = self.db.clock_frame().is_some();
568 let executed: Box<dyn Iterator<Item = ServiceSupported> + '_> = match &effective_executed {
569 Some(services) => Box::new(services.iter()),
570 None => {
571 Box::new(crate::device_view::DeviceExecution::FullServer.services(clock_available))
572 }
573 };
574 for service in executed {
575 service_map
576 .entry(service_display_name(service).unwrap_or("Unknown"))
577 .or_default()
578 .1 = true;
579 }
580 for service in Self::INITIATED_SERVICES {
581 service_map
582 .entry(service_display_name(*service).unwrap_or("Unknown"))
583 .or_default()
584 .0 = true;
585 }
586
587 service_map
588 .into_iter()
589 .map(|(name, (initiator, executor))| ServiceSupport {
590 service_name: name.to_string(),
591 initiator,
592 executor,
593 })
594 .collect()
595 }
596}
597
598pub fn generate_pics(
604 db: &ObjectDatabase,
605 server_config: &ServerConfig,
606 pics_config: &PicsConfig,
607) -> Pics {
608 PicsGenerator::new(db, server_config, pics_config).generate()
609}
610
611pub fn service_display_name(service: ServiceSupported) -> Option<&'static str> {
615 Some(match service.to_raw() {
616 0 => "AcknowledgeAlarm",
617 1 => "ConfirmedCOVNotification",
618 2 => "ConfirmedEventNotification",
619 3 => "GetAlarmSummary",
620 4 => "GetEnrollmentSummary",
621 5 => "SubscribeCOV",
622 6 => "AtomicReadFile",
623 7 => "AtomicWriteFile",
624 8 => "AddListElement",
625 9 => "RemoveListElement",
626 10 => "CreateObject",
627 11 => "DeleteObject",
628 12 => "ReadProperty",
629 14 => "ReadPropertyMultiple",
630 15 => "WriteProperty",
631 16 => "WritePropertyMultiple",
632 17 => "DeviceCommunicationControl",
633 18 => "ConfirmedPrivateTransfer",
634 19 => "ConfirmedTextMessage",
635 20 => "ReinitializeDevice",
636 21 => "VT-Open",
637 22 => "VT-Close",
638 23 => "VT-Data",
639 26 => "I-Am",
640 27 => "I-Have",
641 28 => "UnconfirmedCOVNotification",
642 29 => "UnconfirmedEventNotification",
643 30 => "UnconfirmedPrivateTransfer",
644 31 => "UnconfirmedTextMessage",
645 32 => "TimeSynchronization",
646 33 => "WhoHas",
647 34 => "WhoIs",
648 35 => "ReadRange",
649 36 => "UTCTimeSynchronization",
650 37 => "LifeSafetyOperation",
651 38 => "SubscribeCOVProperty",
652 39 => "GetEventInformation",
653 40 => "WriteGroup",
654 41 => "SubscribeCOVPropertyMultiple",
655 42 => "ConfirmedCOVNotificationMultiple",
656 43 => "UnconfirmedCOVNotificationMultiple",
657 44 => "ConfirmedAuditNotification",
658 45 => "AuditLogQuery",
659 46 => "UnconfirmedAuditNotification",
660 47 => "Who-Am-I",
661 48 => "You-Are",
662 _ => return None,
663 })
664}
665
666mod device_text;
667mod render;
668
669#[cfg(test)]
672mod tests;
673
674#[cfg(test)]
675mod property_metadata_tests;
676
677#[cfg(test)]
678mod acked_transitions_policy_tests;
679
680#[cfg(test)]
681mod truth_source_tests;
682
683#[cfg(test)]
684mod protocol_revision_tests;
685
686#[cfg(test)]
687mod identity_text_tests;
688
689#[cfg(test)]
690mod property_union_tests;
691
692#[cfg(test)]
693mod character_set_tests;
694
695#[cfg(test)]
696mod selected_device_tests;
697
698#[cfg(test)]
699mod creation_only_tests;
700
701#[cfg(test)]
702mod data_link_tests;
703
704#[cfg(test)]
705mod initiated_tests;