1use rustbac_core::types::ObjectType;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SegmentationSupport {
11 Both,
13 Transmit,
15 Receive,
17 None,
19}
20
21impl SegmentationSupport {
22 fn as_str(&self) -> &'static str {
23 match self {
24 Self::Both => "Both",
25 Self::Transmit => "Transmit",
26 Self::Receive => "Receive",
27 Self::None => "None",
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
34pub struct SupportedService {
35 pub name: String,
37 pub service_choice: u8,
39 pub initiate: bool,
41 pub execute: bool,
43}
44
45#[derive(Debug, Clone)]
47pub struct SupportedObjectType {
48 pub object_type: ObjectType,
50 pub createable: bool,
52 pub deletable: bool,
54}
55
56#[derive(Debug, Clone)]
60pub struct PicsDocument {
61 pub vendor_name: String,
62 pub product_name: String,
63 pub product_model_number: String,
64 pub firmware_revision: String,
65 pub protocol_version: u8,
66 pub protocol_revision: u16,
67 pub max_apdu_length: u16,
68 pub segmentation_supported: SegmentationSupport,
69 pub services: Vec<SupportedService>,
70 pub object_types: Vec<SupportedObjectType>,
71}
72
73impl PicsDocument {
74 pub fn to_text(&self) -> String {
76 let mut out = String::new();
77 out.push_str("==============================================================================\n");
78 out.push_str(" BACnet Protocol Implementation Conformance Statement (PICS)\n");
79 out.push_str("==============================================================================\n\n");
80
81 out.push_str("-- Product Identification --\n\n");
82 out.push_str(&format!(" Vendor Name: {}\n", self.vendor_name));
83 out.push_str(&format!(" Product Name: {}\n", self.product_name));
84 out.push_str(&format!(" Product Model Number: {}\n", self.product_model_number));
85 out.push_str(&format!(" Firmware Revision: {}\n", self.firmware_revision));
86 out.push_str("\n");
87
88 out.push_str("-- BACnet Protocol --\n\n");
89 out.push_str(&format!(" BACnet Protocol Version: {}\n", self.protocol_version));
90 out.push_str(&format!(" BACnet Protocol Revision: {}\n", self.protocol_revision));
91 out.push_str(&format!(" Max APDU Length Accepted: {}\n", self.max_apdu_length));
92 out.push_str(&format!(" Segmentation Supported: {}\n", self.segmentation_supported.as_str()));
93 out.push_str("\n");
94
95 out.push_str("-- Supported Services --\n\n");
96 out.push_str(" Service Initiate Execute\n");
97 out.push_str(" ------------------------------------ -------- -------\n");
98 for svc in &self.services {
99 let init = if svc.initiate { "Yes" } else { "No" };
100 let exec = if svc.execute { "Yes" } else { "No" };
101 out.push_str(&format!(" {:<38} {:<8} {}\n", svc.name, init, exec));
102 }
103 out.push_str("\n");
104
105 out.push_str("-- Supported Object Types --\n\n");
106 out.push_str(" Object Type Createable Deletable\n");
107 out.push_str(" ------------------------------------ ---------- ---------\n");
108 for ot in &self.object_types {
109 let name = format!("{:?}", ot.object_type);
110 let create = if ot.createable { "Yes" } else { "No" };
111 let delete = if ot.deletable { "Yes" } else { "No" };
112 out.push_str(&format!(" {:<38} {:<10} {}\n", name, create, delete));
113 }
114
115 out
116 }
117
118 pub fn to_json(&self) -> String {
120 let services_json: Vec<String> = self
121 .services
122 .iter()
123 .map(|s| {
124 format!(
125 r#" {{ "name": "{}", "service_choice": {}, "initiate": {}, "execute": {} }}"#,
126 s.name, s.service_choice, s.initiate, s.execute
127 )
128 })
129 .collect();
130
131 let objects_json: Vec<String> = self
132 .object_types
133 .iter()
134 .map(|o| {
135 format!(
136 r#" {{ "object_type": "{:?}", "createable": {}, "deletable": {} }}"#,
137 o.object_type, o.createable, o.deletable
138 )
139 })
140 .collect();
141
142 format!(
143 r#"{{
144 "vendor_name": "{}",
145 "product_name": "{}",
146 "product_model_number": "{}",
147 "firmware_revision": "{}",
148 "protocol_version": {},
149 "protocol_revision": {},
150 "max_apdu_length": {},
151 "segmentation_supported": "{}",
152 "services": [
153{}
154 ],
155 "object_types": [
156{}
157 ]
158}}"#,
159 self.vendor_name,
160 self.product_name,
161 self.product_model_number,
162 self.firmware_revision,
163 self.protocol_version,
164 self.protocol_revision,
165 self.max_apdu_length,
166 self.segmentation_supported.as_str(),
167 services_json.join(",\n"),
168 objects_json.join(",\n"),
169 )
170 }
171}
172
173pub fn default_rustbac_pics() -> PicsDocument {
176 PicsDocument {
177 vendor_name: "rust-bac".to_string(),
178 product_name: "rust-bac BACnet Stack".to_string(),
179 product_model_number: "rustbac-0.4".to_string(),
180 firmware_revision: env!("CARGO_PKG_VERSION").to_string(),
181 protocol_version: 1,
182 protocol_revision: 24,
183 max_apdu_length: 1476,
184 segmentation_supported: SegmentationSupport::Both,
185 services: vec![
186 SupportedService {
187 name: "ReadProperty".to_string(),
188 service_choice: 0x0C,
189 initiate: true,
190 execute: true,
191 },
192 SupportedService {
193 name: "ReadPropertyMultiple".to_string(),
194 service_choice: 0x0E,
195 initiate: true,
196 execute: true,
197 },
198 SupportedService {
199 name: "WriteProperty".to_string(),
200 service_choice: 0x0F,
201 initiate: true,
202 execute: true,
203 },
204 SupportedService {
205 name: "WritePropertyMultiple".to_string(),
206 service_choice: 0x10,
207 initiate: true,
208 execute: true,
209 },
210 SupportedService {
211 name: "Who-Is".to_string(),
212 service_choice: 0x08,
213 initiate: true,
214 execute: true,
215 },
216 SupportedService {
217 name: "I-Am".to_string(),
218 service_choice: 0x00,
219 initiate: true,
220 execute: false,
221 },
222 SupportedService {
223 name: "Who-Has".to_string(),
224 service_choice: 0x07,
225 initiate: true,
226 execute: false,
227 },
228 SupportedService {
229 name: "I-Have".to_string(),
230 service_choice: 0x01,
231 initiate: true,
232 execute: false,
233 },
234 SupportedService {
235 name: "SubscribeCOV".to_string(),
236 service_choice: 0x05,
237 initiate: true,
238 execute: true,
239 },
240 SupportedService {
241 name: "ConfirmedCOVNotification".to_string(),
242 service_choice: 0x01,
243 initiate: true,
244 execute: true,
245 },
246 SupportedService {
247 name: "UnconfirmedCOVNotification".to_string(),
248 service_choice: 0x02,
249 initiate: true,
250 execute: true,
251 },
252 SupportedService {
253 name: "SubscribeCOVProperty".to_string(),
254 service_choice: 0x1C,
255 initiate: true,
256 execute: false,
257 },
258 SupportedService {
259 name: "CreateObject".to_string(),
260 service_choice: 0x0A,
261 initiate: true,
262 execute: true,
263 },
264 SupportedService {
265 name: "DeleteObject".to_string(),
266 service_choice: 0x0B,
267 initiate: true,
268 execute: true,
269 },
270 SupportedService {
271 name: "GetAlarmSummary".to_string(),
272 service_choice: 0x03,
273 initiate: true,
274 execute: false,
275 },
276 SupportedService {
277 name: "GetEnrollmentSummary".to_string(),
278 service_choice: 0x04,
279 initiate: true,
280 execute: false,
281 },
282 SupportedService {
283 name: "GetEventInformation".to_string(),
284 service_choice: 0x1D,
285 initiate: true,
286 execute: false,
287 },
288 SupportedService {
289 name: "AcknowledgeAlarm".to_string(),
290 service_choice: 0x00,
291 initiate: true,
292 execute: false,
293 },
294 SupportedService {
295 name: "ConfirmedEventNotification".to_string(),
296 service_choice: 0x02,
297 initiate: false,
298 execute: true,
299 },
300 SupportedService {
301 name: "UnconfirmedEventNotification".to_string(),
302 service_choice: 0x03,
303 initiate: false,
304 execute: true,
305 },
306 SupportedService {
307 name: "AtomicReadFile".to_string(),
308 service_choice: 0x06,
309 initiate: true,
310 execute: false,
311 },
312 SupportedService {
313 name: "AtomicWriteFile".to_string(),
314 service_choice: 0x07,
315 initiate: true,
316 execute: false,
317 },
318 SupportedService {
319 name: "AddListElement".to_string(),
320 service_choice: 0x08,
321 initiate: true,
322 execute: false,
323 },
324 SupportedService {
325 name: "RemoveListElement".to_string(),
326 service_choice: 0x09,
327 initiate: true,
328 execute: false,
329 },
330 SupportedService {
331 name: "ReadRange".to_string(),
332 service_choice: 0x1A,
333 initiate: true,
334 execute: false,
335 },
336 SupportedService {
337 name: "DeviceCommunicationControl".to_string(),
338 service_choice: 0x11,
339 initiate: true,
340 execute: false,
341 },
342 SupportedService {
343 name: "ReinitializeDevice".to_string(),
344 service_choice: 0x14,
345 initiate: true,
346 execute: false,
347 },
348 SupportedService {
349 name: "TimeSynchronization".to_string(),
350 service_choice: 0x06,
351 initiate: true,
352 execute: false,
353 },
354 SupportedService {
355 name: "ConfirmedPrivateTransfer".to_string(),
356 service_choice: 0x12,
357 initiate: true,
358 execute: false,
359 },
360 ],
361 object_types: vec![
362 SupportedObjectType {
363 object_type: ObjectType::Device,
364 createable: false,
365 deletable: false,
366 },
367 SupportedObjectType {
368 object_type: ObjectType::AnalogInput,
369 createable: true,
370 deletable: true,
371 },
372 SupportedObjectType {
373 object_type: ObjectType::AnalogOutput,
374 createable: true,
375 deletable: true,
376 },
377 SupportedObjectType {
378 object_type: ObjectType::AnalogValue,
379 createable: true,
380 deletable: true,
381 },
382 SupportedObjectType {
383 object_type: ObjectType::BinaryInput,
384 createable: true,
385 deletable: true,
386 },
387 SupportedObjectType {
388 object_type: ObjectType::BinaryOutput,
389 createable: true,
390 deletable: true,
391 },
392 SupportedObjectType {
393 object_type: ObjectType::BinaryValue,
394 createable: true,
395 deletable: true,
396 },
397 SupportedObjectType {
398 object_type: ObjectType::MultiStateInput,
399 createable: true,
400 deletable: true,
401 },
402 SupportedObjectType {
403 object_type: ObjectType::MultiStateOutput,
404 createable: true,
405 deletable: true,
406 },
407 SupportedObjectType {
408 object_type: ObjectType::MultiStateValue,
409 createable: true,
410 deletable: true,
411 },
412 SupportedObjectType {
413 object_type: ObjectType::Schedule,
414 createable: false,
415 deletable: false,
416 },
417 SupportedObjectType {
418 object_type: ObjectType::Calendar,
419 createable: false,
420 deletable: false,
421 },
422 SupportedObjectType {
423 object_type: ObjectType::TrendLog,
424 createable: false,
425 deletable: false,
426 },
427 SupportedObjectType {
428 object_type: ObjectType::File,
429 createable: false,
430 deletable: false,
431 },
432 SupportedObjectType {
433 object_type: ObjectType::NotificationClass,
434 createable: false,
435 deletable: false,
436 },
437 ],
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444
445 #[test]
446 fn default_pics_has_services() {
447 let pics = default_rustbac_pics();
448 assert!(!pics.services.is_empty());
449 assert!(pics
450 .services
451 .iter()
452 .any(|s| s.name == "ReadProperty" && s.initiate && s.execute));
453 assert!(pics
454 .services
455 .iter()
456 .any(|s| s.name == "WriteProperty" && s.initiate && s.execute));
457 assert!(pics
458 .services
459 .iter()
460 .any(|s| s.name == "Who-Is" && s.initiate && s.execute));
461 }
462
463 #[test]
464 fn default_pics_has_object_types() {
465 let pics = default_rustbac_pics();
466 assert!(!pics.object_types.is_empty());
467 assert!(pics
468 .object_types
469 .iter()
470 .any(|o| o.object_type == ObjectType::Device && !o.createable));
471 assert!(pics
472 .object_types
473 .iter()
474 .any(|o| o.object_type == ObjectType::AnalogValue && o.createable));
475 }
476
477 #[test]
478 fn to_text_contains_vendor() {
479 let pics = default_rustbac_pics();
480 let text = pics.to_text();
481 assert!(text.contains("rust-bac"));
482 assert!(text.contains("ReadProperty"));
483 assert!(text.contains("Supported Services"));
484 assert!(text.contains("Supported Object Types"));
485 }
486
487 #[test]
488 fn to_json_parses() {
489 let pics = default_rustbac_pics();
490 let json = pics.to_json();
491 assert!(json.contains("\"vendor_name\""));
492 assert!(json.contains("\"services\""));
493 assert!(json.contains("\"object_types\""));
494 assert!(json.starts_with('{'));
496 assert!(json.ends_with('}'));
497 }
498}