Skip to main content

mabi_bacnet/
device.rs

1//! BACnet device implementation integrating with the core Device trait.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use parking_lot::RwLock;
8use tokio::sync::broadcast;
9
10use mabi_core::{
11    device::{Device, DeviceInfo, DeviceState, DeviceStatistics},
12    error::{Error, Result},
13    protocol::Protocol,
14    types::{AccessMode, DataPoint, DataPointDef, DataPointId, DataType},
15    value::Value,
16};
17
18use crate::config::BacnetServerConfig;
19use crate::object::property::{BACnetValue, PropertyId};
20use crate::object::registry::ObjectRegistry;
21use crate::object::standard::{
22    AnalogInput, AnalogOutput, AnalogValue, BinaryInput, BinaryOutput, BinaryValue,
23};
24use crate::object::traits::{ArcObject, BACnetObject};
25use crate::object::types::{ObjectId, ObjectType};
26
27/// BACnet device implementation.
28///
29/// This device wraps an ObjectRegistry and implements the core Device trait,
30/// providing integration between the BACnet object model and the simulator framework.
31pub struct BacnetDevice {
32    info: DeviceInfo,
33    device_instance: u32,
34    objects: Arc<ObjectRegistry>,
35    point_defs: RwLock<HashMap<String, DataPointDef>>,
36    object_to_point: RwLock<HashMap<ObjectId, String>>,
37    stats: RwLock<DeviceStatistics>,
38    event_tx: broadcast::Sender<DataPoint>,
39}
40
41impl BacnetDevice {
42    /// Create a new BACnet device.
43    pub fn new(config: &BacnetServerConfig) -> Self {
44        let (event_tx, _) = broadcast::channel(1000);
45
46        let info = DeviceInfo::new(
47            format!("bacnet-{}", config.device_instance),
48            &config.device_name,
49            Protocol::BacnetIp,
50        )
51        .with_metadata("device_instance", config.device_instance.to_string())
52        .with_metadata("vendor_id", config.vendor_id.to_string());
53
54        Self {
55            info,
56            device_instance: config.device_instance,
57            objects: Arc::new(ObjectRegistry::new()),
58            point_defs: RwLock::new(HashMap::new()),
59            object_to_point: RwLock::new(HashMap::new()),
60            stats: RwLock::new(DeviceStatistics::default()),
61            event_tx,
62        }
63    }
64
65    /// Create a new BACnet device with existing object registry.
66    pub fn with_registry(config: &BacnetServerConfig, registry: ObjectRegistry) -> Self {
67        let mut device = Self::new(config);
68        device.objects = Arc::new(registry);
69        device.sync_point_definitions();
70        device
71    }
72
73    /// Get device instance number.
74    pub fn device_instance(&self) -> u32 {
75        self.device_instance
76    }
77
78    /// Get the object registry.
79    pub fn objects(&self) -> &Arc<ObjectRegistry> {
80        &self.objects
81    }
82
83    /// Register an object and create its point definition.
84    pub fn register_object(&self, object: ArcObject) {
85        let object_id = object.object_identifier();
86        let object_name = object.object_name().to_string();
87
88        // Create point ID
89        let point_id = format!("{}:{}", object_id.object_type as u16, object_id.instance);
90
91        // Determine data type based on object type
92        let data_type = match object_id.object_type {
93            ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
94                DataType::Float32
95            }
96            ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue => {
97                DataType::Bool
98            }
99            ObjectType::MultiStateInput
100            | ObjectType::MultiStateOutput
101            | ObjectType::MultiStateValue => DataType::UInt32,
102            _ => DataType::String,
103        };
104
105        // Determine access mode
106        let access = match object_id.object_type {
107            ObjectType::AnalogOutput
108            | ObjectType::AnalogValue
109            | ObjectType::BinaryOutput
110            | ObjectType::BinaryValue
111            | ObjectType::MultiStateOutput
112            | ObjectType::MultiStateValue => AccessMode::ReadWrite,
113            _ => AccessMode::ReadOnly,
114        };
115
116        let def = DataPointDef::new(&point_id, &object_name, data_type).with_access(access);
117
118        // Register object
119        self.objects.register(object);
120
121        // Add point definition
122        self.object_to_point
123            .write()
124            .insert(object_id, point_id.clone());
125        self.point_defs.write().insert(point_id, def);
126    }
127
128    /// Add an analog input object.
129    pub fn add_analog_input(&self, instance: u32, name: &str) -> ObjectId {
130        let object = Arc::new(AnalogInput::new(instance, name));
131        let id = object.object_identifier();
132        self.register_object(object);
133        id
134    }
135
136    /// Add an analog output object.
137    pub fn add_analog_output(&self, instance: u32, name: &str) -> ObjectId {
138        let object = Arc::new(AnalogOutput::new(instance, name));
139        let id = object.object_identifier();
140        self.register_object(object);
141        id
142    }
143
144    /// Add an analog value object.
145    pub fn add_analog_value(&self, instance: u32, name: &str) -> ObjectId {
146        let object = Arc::new(AnalogValue::new(instance, name));
147        let id = object.object_identifier();
148        self.register_object(object);
149        id
150    }
151
152    /// Add a binary input object.
153    pub fn add_binary_input(&self, instance: u32, name: &str) -> ObjectId {
154        let object = Arc::new(BinaryInput::new(instance, name));
155        let id = object.object_identifier();
156        self.register_object(object);
157        id
158    }
159
160    /// Add a binary output object.
161    pub fn add_binary_output(&self, instance: u32, name: &str) -> ObjectId {
162        let object = Arc::new(BinaryOutput::new(instance, name));
163        let id = object.object_identifier();
164        self.register_object(object);
165        id
166    }
167
168    /// Add a binary value object.
169    pub fn add_binary_value(&self, instance: u32, name: &str) -> ObjectId {
170        let object = Arc::new(BinaryValue::new(instance, name));
171        let id = object.object_identifier();
172        self.register_object(object);
173        id
174    }
175
176    /// Sync point definitions from the object registry.
177    fn sync_point_definitions(&self) {
178        for object in self.objects.iter() {
179            let object_id = object.object_identifier();
180            let object_name = object.object_name().to_string();
181            let point_id = format!("{}:{}", object_id.object_type as u16, object_id.instance);
182
183            let data_type = match object_id.object_type {
184                ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
185                    DataType::Float32
186                }
187                ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue => {
188                    DataType::Bool
189                }
190                ObjectType::MultiStateInput
191                | ObjectType::MultiStateOutput
192                | ObjectType::MultiStateValue => DataType::UInt32,
193                _ => DataType::String,
194            };
195
196            let access = match object_id.object_type {
197                ObjectType::AnalogOutput
198                | ObjectType::AnalogValue
199                | ObjectType::BinaryOutput
200                | ObjectType::BinaryValue
201                | ObjectType::MultiStateOutput
202                | ObjectType::MultiStateValue => AccessMode::ReadWrite,
203                _ => AccessMode::ReadOnly,
204            };
205
206            let def = DataPointDef::new(&point_id, &object_name, data_type).with_access(access);
207
208            self.object_to_point
209                .write()
210                .insert(object_id, point_id.clone());
211            self.point_defs.write().insert(point_id, def);
212        }
213    }
214
215    /// Convert point ID to object ID.
216    fn point_id_to_object_id(&self, point_id: &str) -> Option<ObjectId> {
217        let parts: Vec<&str> = point_id.split(':').collect();
218        if parts.len() != 2 {
219            return None;
220        }
221
222        let type_code: u16 = parts[0].parse().ok()?;
223        let instance: u32 = parts[1].parse().ok()?;
224        let object_type = ObjectType::from_u16(type_code)?;
225
226        Some(ObjectId::new(object_type, instance))
227    }
228
229    /// Convert BACnet value to core Value.
230    fn bacnet_to_core_value(bacnet: &BACnetValue) -> Option<Value> {
231        match bacnet {
232            BACnetValue::Null => Some(Value::Null),
233            BACnetValue::Boolean(b) => Some(Value::Bool(*b)),
234            BACnetValue::Unsigned(u) => Some(Value::U32(*u)),
235            BACnetValue::Signed(i) => Some(Value::I32(*i)),
236            BACnetValue::Real(f) => Some(Value::F32(*f)),
237            BACnetValue::Double(d) => Some(Value::F64(*d)),
238            BACnetValue::CharacterString(s) => Some(Value::String(s.clone())),
239            BACnetValue::Enumerated(e) => Some(Value::U32(*e)),
240            _ => None,
241        }
242    }
243
244    /// Convert core Value to BACnet value.
245    fn core_to_bacnet_value(value: &Value) -> Option<BACnetValue> {
246        match value {
247            Value::Null => Some(BACnetValue::Null),
248            Value::Bool(b) => Some(BACnetValue::Boolean(*b)),
249            Value::U8(u) => Some(BACnetValue::Unsigned(*u as u32)),
250            Value::U16(u) => Some(BACnetValue::Unsigned(*u as u32)),
251            Value::U32(u) => Some(BACnetValue::Unsigned(*u)),
252            Value::U64(u) => Some(BACnetValue::Unsigned(*u as u32)),
253            Value::I8(i) => Some(BACnetValue::Signed(*i as i32)),
254            Value::I16(i) => Some(BACnetValue::Signed(*i as i32)),
255            Value::I32(i) => Some(BACnetValue::Signed(*i)),
256            Value::I64(i) => Some(BACnetValue::Signed(*i as i32)),
257            Value::F32(f) => Some(BACnetValue::Real(*f)),
258            Value::F64(d) => Some(BACnetValue::Double(*d)),
259            Value::String(s) => Some(BACnetValue::CharacterString(s.clone())),
260            _ => None,
261        }
262    }
263}
264
265#[async_trait]
266impl Device for BacnetDevice {
267    fn info(&self) -> &DeviceInfo {
268        &self.info
269    }
270
271    async fn initialize(&mut self) -> Result<()> {
272        self.info.state = DeviceState::Online;
273        self.info.point_count = self.point_defs.read().len();
274        Ok(())
275    }
276
277    async fn start(&mut self) -> Result<()> {
278        self.info.state = DeviceState::Online;
279        Ok(())
280    }
281
282    async fn stop(&mut self) -> Result<()> {
283        self.info.state = DeviceState::Offline;
284        Ok(())
285    }
286
287    async fn tick(&mut self) -> Result<()> {
288        Ok(())
289    }
290
291    fn point_definitions(&self) -> Vec<&DataPointDef> {
292        // Limitation: returning empty since we use RwLock
293        // In practice, use the objects() method to query directly
294        vec![]
295    }
296
297    fn point_definition(&self, _point_id: &str) -> Option<&DataPointDef> {
298        // Same limitation
299        None
300    }
301
302    async fn read(&self, point_id: &str) -> Result<DataPoint> {
303        let object_id = self
304            .point_id_to_object_id(point_id)
305            .ok_or_else(|| Error::point_not_found(&self.info.id, point_id))?;
306
307        let bacnet_value = self
308            .objects
309            .read_property(&object_id, PropertyId::PresentValue)
310            .map_err(|e| Error::point_not_found(&self.info.id, &e.to_string()))?;
311
312        let value = Self::bacnet_to_core_value(&bacnet_value)
313            .ok_or_else(|| Error::point_not_found(&self.info.id, "Cannot convert BACnet value"))?;
314
315        self.stats.write().record_read();
316        let id = DataPointId::new(&self.info.id, point_id);
317        Ok(DataPoint::new(id, value))
318    }
319
320    async fn write(&mut self, point_id: &str, value: Value) -> Result<()> {
321        let object_id = self
322            .point_id_to_object_id(point_id)
323            .ok_or_else(|| Error::point_not_found(&self.info.id, point_id))?;
324
325        // Check if writable
326        let is_writable = matches!(
327            object_id.object_type,
328            ObjectType::AnalogOutput
329                | ObjectType::AnalogValue
330                | ObjectType::BinaryOutput
331                | ObjectType::BinaryValue
332                | ObjectType::MultiStateOutput
333                | ObjectType::MultiStateValue
334        );
335
336        if !is_writable {
337            return Err(Error::NotSupported(format!(
338                "Object {} is read-only",
339                point_id
340            )));
341        }
342
343        let bacnet_value = Self::core_to_bacnet_value(&value)
344            .ok_or_else(|| Error::NotSupported("Cannot convert value to BACnet format".into()))?;
345
346        self.objects
347            .write_property(&object_id, PropertyId::PresentValue, bacnet_value)
348            .map_err(|e| Error::point_not_found(&self.info.id, &e.to_string()))?;
349
350        self.stats.write().record_write();
351
352        let id = DataPointId::new(&self.info.id, point_id);
353        let _ = self.event_tx.send(DataPoint::new(id, value));
354
355        Ok(())
356    }
357
358    fn subscribe(&self) -> Option<broadcast::Receiver<DataPoint>> {
359        Some(self.event_tx.subscribe())
360    }
361
362    fn statistics(&self) -> DeviceStatistics {
363        self.stats.read().clone()
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[tokio::test]
372    async fn test_bacnet_device() {
373        let config = BacnetServerConfig::default();
374        let device = BacnetDevice::new(&config);
375
376        device.add_analog_input(1, "Temperature");
377        device.add_analog_output(1, "Setpoint");
378
379        assert_eq!(device.objects.len(), 2);
380    }
381
382    #[tokio::test]
383    async fn test_bacnet_read_write() {
384        let config = BacnetServerConfig::default();
385        let mut device = BacnetDevice::new(&config);
386
387        let id = device.add_analog_output(1, "Setpoint");
388        device.initialize().await.unwrap();
389
390        // Write using object registry directly first
391        if let Some(object) = device.objects.get(&id) {
392            let _ = object.write_property(PropertyId::PresentValue, BACnetValue::Real(21.5));
393        }
394
395        // Read
396        let dp = device.read("1:1").await.unwrap();
397        assert!((dp.value.as_f64().unwrap() - 21.5).abs() < 0.01);
398    }
399}