use std::collections::HashMap;
use std::sync::Arc;
use async_trait::async_trait;
use parking_lot::RwLock;
use tokio::sync::broadcast;
use mabi_core::{
device::{Device, DeviceInfo, DeviceState, DeviceStatistics},
error::{Error, Result},
protocol::Protocol,
types::{AccessMode, DataPoint, DataPointDef, DataPointId, DataType},
value::Value,
};
use crate::config::BacnetServerConfig;
use crate::object::property::{BACnetValue, PropertyId};
use crate::object::registry::ObjectRegistry;
use crate::object::standard::{
AnalogInput, AnalogOutput, AnalogValue, BinaryInput, BinaryOutput, BinaryValue,
};
use crate::object::traits::{ArcObject, BACnetObject};
use crate::object::types::{ObjectId, ObjectType};
pub struct BacnetDevice {
info: DeviceInfo,
device_instance: u32,
objects: Arc<ObjectRegistry>,
point_defs: RwLock<HashMap<String, DataPointDef>>,
object_to_point: RwLock<HashMap<ObjectId, String>>,
stats: RwLock<DeviceStatistics>,
event_tx: broadcast::Sender<DataPoint>,
}
impl BacnetDevice {
pub fn new(config: &BacnetServerConfig) -> Self {
let (event_tx, _) = broadcast::channel(1000);
let info = DeviceInfo::new(
format!("bacnet-{}", config.device_instance),
&config.device_name,
Protocol::BacnetIp,
)
.with_metadata("device_instance", config.device_instance.to_string())
.with_metadata("vendor_id", config.vendor_id.to_string());
Self {
info,
device_instance: config.device_instance,
objects: Arc::new(ObjectRegistry::new()),
point_defs: RwLock::new(HashMap::new()),
object_to_point: RwLock::new(HashMap::new()),
stats: RwLock::new(DeviceStatistics::default()),
event_tx,
}
}
pub fn with_registry(config: &BacnetServerConfig, registry: ObjectRegistry) -> Self {
let mut device = Self::new(config);
device.objects = Arc::new(registry);
device.sync_point_definitions();
device
}
pub fn device_instance(&self) -> u32 {
self.device_instance
}
pub fn objects(&self) -> &Arc<ObjectRegistry> {
&self.objects
}
pub fn register_object(&self, object: ArcObject) {
let object_id = object.object_identifier();
let object_name = object.object_name().to_string();
let point_id = format!("{}:{}", object_id.object_type as u16, object_id.instance);
let data_type = match object_id.object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
DataType::Float32
}
ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue => {
DataType::Bool
}
ObjectType::MultiStateInput
| ObjectType::MultiStateOutput
| ObjectType::MultiStateValue => DataType::UInt32,
_ => DataType::String,
};
let access = match object_id.object_type {
ObjectType::AnalogOutput
| ObjectType::AnalogValue
| ObjectType::BinaryOutput
| ObjectType::BinaryValue
| ObjectType::MultiStateOutput
| ObjectType::MultiStateValue => AccessMode::ReadWrite,
_ => AccessMode::ReadOnly,
};
let def = DataPointDef::new(&point_id, &object_name, data_type).with_access(access);
self.objects.register(object);
self.object_to_point
.write()
.insert(object_id, point_id.clone());
self.point_defs.write().insert(point_id, def);
}
pub fn add_analog_input(&self, instance: u32, name: &str) -> ObjectId {
let object = Arc::new(AnalogInput::new(instance, name));
let id = object.object_identifier();
self.register_object(object);
id
}
pub fn add_analog_output(&self, instance: u32, name: &str) -> ObjectId {
let object = Arc::new(AnalogOutput::new(instance, name));
let id = object.object_identifier();
self.register_object(object);
id
}
pub fn add_analog_value(&self, instance: u32, name: &str) -> ObjectId {
let object = Arc::new(AnalogValue::new(instance, name));
let id = object.object_identifier();
self.register_object(object);
id
}
pub fn add_binary_input(&self, instance: u32, name: &str) -> ObjectId {
let object = Arc::new(BinaryInput::new(instance, name));
let id = object.object_identifier();
self.register_object(object);
id
}
pub fn add_binary_output(&self, instance: u32, name: &str) -> ObjectId {
let object = Arc::new(BinaryOutput::new(instance, name));
let id = object.object_identifier();
self.register_object(object);
id
}
pub fn add_binary_value(&self, instance: u32, name: &str) -> ObjectId {
let object = Arc::new(BinaryValue::new(instance, name));
let id = object.object_identifier();
self.register_object(object);
id
}
fn sync_point_definitions(&self) {
for object in self.objects.iter() {
let object_id = object.object_identifier();
let object_name = object.object_name().to_string();
let point_id = format!("{}:{}", object_id.object_type as u16, object_id.instance);
let data_type = match object_id.object_type {
ObjectType::AnalogInput | ObjectType::AnalogOutput | ObjectType::AnalogValue => {
DataType::Float32
}
ObjectType::BinaryInput | ObjectType::BinaryOutput | ObjectType::BinaryValue => {
DataType::Bool
}
ObjectType::MultiStateInput
| ObjectType::MultiStateOutput
| ObjectType::MultiStateValue => DataType::UInt32,
_ => DataType::String,
};
let access = match object_id.object_type {
ObjectType::AnalogOutput
| ObjectType::AnalogValue
| ObjectType::BinaryOutput
| ObjectType::BinaryValue
| ObjectType::MultiStateOutput
| ObjectType::MultiStateValue => AccessMode::ReadWrite,
_ => AccessMode::ReadOnly,
};
let def = DataPointDef::new(&point_id, &object_name, data_type).with_access(access);
self.object_to_point
.write()
.insert(object_id, point_id.clone());
self.point_defs.write().insert(point_id, def);
}
}
fn point_id_to_object_id(&self, point_id: &str) -> Option<ObjectId> {
let parts: Vec<&str> = point_id.split(':').collect();
if parts.len() != 2 {
return None;
}
let type_code: u16 = parts[0].parse().ok()?;
let instance: u32 = parts[1].parse().ok()?;
let object_type = ObjectType::from_u16(type_code)?;
Some(ObjectId::new(object_type, instance))
}
fn bacnet_to_core_value(bacnet: &BACnetValue) -> Option<Value> {
match bacnet {
BACnetValue::Null => Some(Value::Null),
BACnetValue::Boolean(b) => Some(Value::Bool(*b)),
BACnetValue::Unsigned(u) => Some(Value::U32(*u)),
BACnetValue::Signed(i) => Some(Value::I32(*i)),
BACnetValue::Real(f) => Some(Value::F32(*f)),
BACnetValue::Double(d) => Some(Value::F64(*d)),
BACnetValue::CharacterString(s) => Some(Value::String(s.clone())),
BACnetValue::Enumerated(e) => Some(Value::U32(*e)),
_ => None,
}
}
fn core_to_bacnet_value(value: &Value) -> Option<BACnetValue> {
match value {
Value::Null => Some(BACnetValue::Null),
Value::Bool(b) => Some(BACnetValue::Boolean(*b)),
Value::U8(u) => Some(BACnetValue::Unsigned(*u as u32)),
Value::U16(u) => Some(BACnetValue::Unsigned(*u as u32)),
Value::U32(u) => Some(BACnetValue::Unsigned(*u)),
Value::U64(u) => Some(BACnetValue::Unsigned(*u as u32)),
Value::I8(i) => Some(BACnetValue::Signed(*i as i32)),
Value::I16(i) => Some(BACnetValue::Signed(*i as i32)),
Value::I32(i) => Some(BACnetValue::Signed(*i)),
Value::I64(i) => Some(BACnetValue::Signed(*i as i32)),
Value::F32(f) => Some(BACnetValue::Real(*f)),
Value::F64(d) => Some(BACnetValue::Double(*d)),
Value::String(s) => Some(BACnetValue::CharacterString(s.clone())),
_ => None,
}
}
}
#[async_trait]
impl Device for BacnetDevice {
fn info(&self) -> &DeviceInfo {
&self.info
}
async fn initialize(&mut self) -> Result<()> {
self.info.state = DeviceState::Online;
self.info.point_count = self.point_defs.read().len();
Ok(())
}
async fn start(&mut self) -> Result<()> {
self.info.state = DeviceState::Online;
Ok(())
}
async fn stop(&mut self) -> Result<()> {
self.info.state = DeviceState::Offline;
Ok(())
}
async fn tick(&mut self) -> Result<()> {
Ok(())
}
fn point_definitions(&self) -> Vec<&DataPointDef> {
vec![]
}
fn point_definition(&self, _point_id: &str) -> Option<&DataPointDef> {
None
}
async fn read(&self, point_id: &str) -> Result<DataPoint> {
let object_id = self
.point_id_to_object_id(point_id)
.ok_or_else(|| Error::point_not_found(&self.info.id, point_id))?;
let bacnet_value = self
.objects
.read_property(&object_id, PropertyId::PresentValue)
.map_err(|e| Error::point_not_found(&self.info.id, &e.to_string()))?;
let value = Self::bacnet_to_core_value(&bacnet_value)
.ok_or_else(|| Error::point_not_found(&self.info.id, "Cannot convert BACnet value"))?;
self.stats.write().record_read();
let id = DataPointId::new(&self.info.id, point_id);
Ok(DataPoint::new(id, value))
}
async fn write(&mut self, point_id: &str, value: Value) -> Result<()> {
let object_id = self
.point_id_to_object_id(point_id)
.ok_or_else(|| Error::point_not_found(&self.info.id, point_id))?;
let is_writable = matches!(
object_id.object_type,
ObjectType::AnalogOutput
| ObjectType::AnalogValue
| ObjectType::BinaryOutput
| ObjectType::BinaryValue
| ObjectType::MultiStateOutput
| ObjectType::MultiStateValue
);
if !is_writable {
return Err(Error::NotSupported(format!(
"Object {} is read-only",
point_id
)));
}
let bacnet_value = Self::core_to_bacnet_value(&value)
.ok_or_else(|| Error::NotSupported("Cannot convert value to BACnet format".into()))?;
self.objects
.write_property(&object_id, PropertyId::PresentValue, bacnet_value)
.map_err(|e| Error::point_not_found(&self.info.id, &e.to_string()))?;
self.stats.write().record_write();
let id = DataPointId::new(&self.info.id, point_id);
let _ = self.event_tx.send(DataPoint::new(id, value));
Ok(())
}
fn subscribe(&self) -> Option<broadcast::Receiver<DataPoint>> {
Some(self.event_tx.subscribe())
}
fn statistics(&self) -> DeviceStatistics {
self.stats.read().clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_bacnet_device() {
let config = BacnetServerConfig::default();
let device = BacnetDevice::new(&config);
device.add_analog_input(1, "Temperature");
device.add_analog_output(1, "Setpoint");
assert_eq!(device.objects.len(), 2);
}
#[tokio::test]
async fn test_bacnet_read_write() {
let config = BacnetServerConfig::default();
let mut device = BacnetDevice::new(&config);
let id = device.add_analog_output(1, "Setpoint");
device.initialize().await.unwrap();
if let Some(object) = device.objects.get(&id) {
let _ = object.write_property(PropertyId::PresentValue, BACnetValue::Real(21.5));
}
let dp = device.read("1:1").await.unwrap();
assert!((dp.value.as_f64().unwrap() - 21.5).abs() < 0.01);
}
}