Skip to main content

rustbac_client/
simulator.rs

1//! Lightweight simulated BACnet device.
2//!
3//! [`SimulatedDevice`] responds to Who-Is, ReadProperty, and WriteProperty
4//! requests. Useful for testing and development without physical hardware.
5
6use crate::{ClientDataValue, ClientError};
7use rustbac_core::apdu::{
8    ApduType, ComplexAckHeader, ConfirmedRequestHeader, SimpleAck, UnconfirmedRequestHeader,
9};
10use rustbac_core::encoding::{
11    primitives::{decode_unsigned, encode_ctx_unsigned},
12    reader::Reader,
13    tag::Tag,
14    writer::Writer,
15};
16use rustbac_core::npdu::Npdu;
17use rustbac_core::services::i_am::IAmRequest;
18use rustbac_core::services::read_property::SERVICE_READ_PROPERTY;
19use rustbac_core::services::value_codec::encode_application_data_value;
20use rustbac_core::services::write_property::SERVICE_WRITE_PROPERTY;
21use rustbac_core::types::{DataValue, ObjectId, ObjectType, PropertyId};
22use rustbac_datalink::{DataLink, DataLinkAddress};
23use std::collections::HashMap;
24use std::sync::Arc;
25use tokio::sync::RwLock;
26
27/// A simulated BACnet device.
28pub struct SimulatedDevice<D: DataLink> {
29    pub device_id: ObjectId,
30    objects: Arc<RwLock<HashMap<ObjectId, HashMap<PropertyId, ClientDataValue>>>>,
31    datalink: D,
32}
33
34impl<D: DataLink> SimulatedDevice<D> {
35    /// Create a new simulated device with the given instance number.
36    pub fn new(instance: u32, datalink: D) -> Self {
37        let device_id = ObjectId::new(ObjectType::Device, instance);
38        let mut device_props = HashMap::new();
39        device_props.insert(
40            PropertyId::ObjectIdentifier,
41            ClientDataValue::ObjectId(device_id),
42        );
43        device_props.insert(
44            PropertyId::ObjectName,
45            ClientDataValue::CharacterString(format!("SimDevice-{instance}")),
46        );
47        device_props.insert(
48            PropertyId::ObjectType,
49            ClientDataValue::Enumerated(ObjectType::Device.to_u16() as u32),
50        );
51
52        let mut objects = HashMap::new();
53        objects.insert(device_id, device_props);
54
55        Self {
56            device_id,
57            objects: Arc::new(RwLock::new(objects)),
58            datalink,
59        }
60    }
61
62    /// Add an object with its properties to the simulated device.
63    pub async fn add_object(&self, id: ObjectId, properties: HashMap<PropertyId, ClientDataValue>) {
64        self.objects.write().await.insert(id, properties);
65    }
66
67    /// Run the device loop, responding to incoming requests until stopped.
68    pub async fn run(&self) -> Result<(), ClientError> {
69        let mut buf = [0u8; 1500];
70        loop {
71            let (n, source) = self.datalink.recv(&mut buf).await?;
72            if let Err(e) = self.handle_frame(&buf[..n], source).await {
73                log::debug!("simulator: error handling frame: {e}");
74            }
75        }
76    }
77
78    async fn handle_frame(&self, frame: &[u8], source: DataLinkAddress) -> Result<(), ClientError> {
79        let mut r = Reader::new(frame);
80        let _npdu = Npdu::decode(&mut r)?;
81
82        if r.is_empty() {
83            return Ok(());
84        }
85
86        let first = r.peek_u8()?;
87        let apdu_type = ApduType::from_u8(first >> 4);
88
89        match apdu_type {
90            Some(ApduType::UnconfirmedRequest) => {
91                let header = UnconfirmedRequestHeader::decode(&mut r)?;
92                if header.service_choice == 0x08 {
93                    // Who-Is — decode optional limits from remaining payload.
94                    let who_is_limits = self.decode_who_is_limits(&mut r);
95                    if self.matches_who_is(who_is_limits) {
96                        self.send_i_am(source).await?;
97                    }
98                }
99            }
100            Some(ApduType::ConfirmedRequest) => {
101                let header = ConfirmedRequestHeader::decode(&mut r)?;
102                match header.service_choice {
103                    SERVICE_READ_PROPERTY => {
104                        self.handle_read_property(&mut r, header.invoke_id, source)
105                            .await?;
106                    }
107                    SERVICE_WRITE_PROPERTY => {
108                        self.handle_write_property(&mut r, header.invoke_id, source)
109                            .await?;
110                    }
111                    _ => {
112                        // Unknown service — ignore.
113                    }
114                }
115            }
116            _ => {}
117        }
118
119        Ok(())
120    }
121
122    fn decode_who_is_limits(&self, r: &mut Reader<'_>) -> Option<(u32, u32)> {
123        // Who-Is has optional [0] low-limit, [1] high-limit.
124        if r.is_empty() {
125            return None; // Global Who-Is — no limits.
126        }
127        let tag0 = Tag::decode(r).ok()?;
128        let low = match tag0 {
129            Tag::Context { tag_num: 0, len } => decode_unsigned(r, len as usize).ok()?,
130            _ => return None,
131        };
132        let tag1 = Tag::decode(r).ok()?;
133        let high = match tag1 {
134            Tag::Context { tag_num: 1, len } => decode_unsigned(r, len as usize).ok()?,
135            _ => return None,
136        };
137        Some((low, high))
138    }
139
140    fn matches_who_is(&self, limits: Option<(u32, u32)>) -> bool {
141        let instance = self.device_id.instance();
142        match limits {
143            None => true, // Global Who-Is.
144            Some((low, high)) => instance >= low && instance <= high,
145        }
146    }
147
148    async fn send_i_am(&self, target: DataLinkAddress) -> Result<(), ClientError> {
149        let req = IAmRequest {
150            device_id: self.device_id,
151            max_apdu: 1476,
152            segmentation: 3, // no-segmentation
153            vendor_id: 0,
154        };
155
156        let mut buf = [0u8; 128];
157        let mut w = Writer::new(&mut buf);
158        Npdu::new(0).encode(&mut w)?;
159        req.encode(&mut w)?;
160        let data = w.as_written();
161        self.datalink.send(target, data).await?;
162        Ok(())
163    }
164
165    async fn handle_read_property(
166        &self,
167        r: &mut Reader<'_>,
168        invoke_id: u8,
169        source: DataLinkAddress,
170    ) -> Result<(), ClientError> {
171        // ReadPropertyRequest has no decode method — decode manually.
172        let object_id = crate::decode_ctx_object_id(r)?;
173        let property_id = PropertyId::from_u32(crate::decode_ctx_unsigned(r)?);
174        let objects = self.objects.read().await;
175
176        let value = objects
177            .get(&object_id)
178            .and_then(|props| props.get(&property_id));
179
180        match value {
181            Some(val) => {
182                let borrowed = client_value_to_borrowed(val);
183                let mut buf = [0u8; 1400];
184                let mut w = Writer::new(&mut buf);
185                Npdu::new(0).encode(&mut w)?;
186                ComplexAckHeader {
187                    segmented: false,
188                    more_follows: false,
189                    invoke_id,
190                    sequence_number: None,
191                    proposed_window_size: None,
192                    service_choice: SERVICE_READ_PROPERTY,
193                }
194                .encode(&mut w)?;
195                // Encode the ReadPropertyAck payload manually.
196                encode_ctx_unsigned(&mut w, 0, object_id.raw())?;
197                encode_ctx_unsigned(&mut w, 1, property_id.to_u32())?;
198                Tag::Opening { tag_num: 3 }.encode(&mut w)?;
199                encode_application_data_value(&mut w, &borrowed)?;
200                Tag::Closing { tag_num: 3 }.encode(&mut w)?;
201                let data = w.as_written();
202                self.datalink.send(source, data).await?;
203            }
204            None => {
205                // Send error: unknown-property.
206                let mut buf = [0u8; 64];
207                let mut w = Writer::new(&mut buf);
208                Npdu::new(0).encode(&mut w)?;
209                // BACnet Error PDU: type=5, invoke_id, service_choice, error_class, error_code
210                w.write_u8(0x50)?; // Error PDU type (5 << 4)
211                w.write_u8(invoke_id)?;
212                w.write_u8(SERVICE_READ_PROPERTY)?;
213                // error-class: property (2), error-code: unknown-property (32)
214                Tag::Application {
215                    tag: rustbac_core::encoding::tag::AppTag::Enumerated,
216                    len: 1,
217                }
218                .encode(&mut w)?;
219                w.write_u8(2)?; // property
220                Tag::Application {
221                    tag: rustbac_core::encoding::tag::AppTag::Enumerated,
222                    len: 1,
223                }
224                .encode(&mut w)?;
225                w.write_u8(32)?; // unknown-property
226                let data = w.as_written();
227                self.datalink.send(source, data).await?;
228            }
229        }
230
231        Ok(())
232    }
233
234    async fn handle_write_property(
235        &self,
236        r: &mut Reader<'_>,
237        invoke_id: u8,
238        source: DataLinkAddress,
239    ) -> Result<(), ClientError> {
240        // Decode object_id [0], property_id [1], optional array_index [2], value [3]
241        let object_id = crate::decode_ctx_object_id(r)?;
242        let property_id_raw = crate::decode_ctx_unsigned(r)?;
243        let property_id = PropertyId::from_u32(property_id_raw);
244
245        let next_tag = Tag::decode(r)?;
246        let value_start_tag = match next_tag {
247            Tag::Context { tag_num: 2, len } => {
248                let _array_index = decode_unsigned(r, len as usize)?;
249                Tag::decode(r)?
250            }
251            other => other,
252        };
253        if value_start_tag != (Tag::Opening { tag_num: 3 }) {
254            return Err(rustbac_core::DecodeError::InvalidTag.into());
255        }
256        let val = rustbac_core::services::value_codec::decode_application_data_value(r)?;
257        match Tag::decode(r)? {
258            Tag::Closing { tag_num: 3 } => {}
259            _ => return Err(rustbac_core::DecodeError::InvalidTag.into()),
260        }
261
262        let client_val = crate::data_value_to_client(val);
263        let mut objects = self.objects.write().await;
264        if let Some(props) = objects.get_mut(&object_id) {
265            props.insert(property_id, client_val);
266        }
267
268        // Send SimpleAck
269        let mut buf = [0u8; 32];
270        let mut w = Writer::new(&mut buf);
271        Npdu::new(0).encode(&mut w)?;
272        SimpleAck {
273            invoke_id,
274            service_choice: SERVICE_WRITE_PROPERTY,
275        }
276        .encode(&mut w)?;
277        let data = w.as_written();
278        self.datalink.send(source, data).await?;
279
280        Ok(())
281    }
282}
283
284/// Convert an owned ClientDataValue to a borrowed DataValue.
285///
286/// This is a shallow conversion — strings and byte arrays reference the owned data.
287fn client_value_to_borrowed(val: &ClientDataValue) -> DataValue<'_> {
288    match val {
289        ClientDataValue::Null => DataValue::Null,
290        ClientDataValue::Boolean(v) => DataValue::Boolean(*v),
291        ClientDataValue::Unsigned(v) => DataValue::Unsigned(*v),
292        ClientDataValue::Signed(v) => DataValue::Signed(*v),
293        ClientDataValue::Real(v) => DataValue::Real(*v),
294        ClientDataValue::Double(v) => DataValue::Double(*v),
295        ClientDataValue::OctetString(v) => DataValue::OctetString(v),
296        ClientDataValue::CharacterString(v) => DataValue::CharacterString(v),
297        ClientDataValue::BitString { unused_bits, data } => {
298            DataValue::BitString(rustbac_core::types::BitString {
299                unused_bits: *unused_bits,
300                data,
301            })
302        }
303        ClientDataValue::Enumerated(v) => DataValue::Enumerated(*v),
304        ClientDataValue::Date(v) => DataValue::Date(*v),
305        ClientDataValue::Time(v) => DataValue::Time(*v),
306        ClientDataValue::ObjectId(v) => DataValue::ObjectId(*v),
307        ClientDataValue::Constructed { tag_num, values } => DataValue::Constructed {
308            tag_num: *tag_num,
309            values: values.iter().map(client_value_to_borrowed).collect(),
310        },
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use rustbac_core::encoding::{primitives::encode_ctx_unsigned, reader::Reader, writer::Writer};
318    use std::sync::{Arc, Mutex};
319
320    #[derive(Clone, Default)]
321    struct MockDataLink {
322        sent: Arc<Mutex<Vec<(DataLinkAddress, Vec<u8>)>>>,
323    }
324
325    impl DataLink for MockDataLink {
326        async fn send(
327            &self,
328            address: DataLinkAddress,
329            payload: &[u8],
330        ) -> Result<(), rustbac_datalink::DataLinkError> {
331            self.sent
332                .lock()
333                .expect("poisoned lock")
334                .push((address, payload.to_vec()));
335            Ok(())
336        }
337
338        async fn recv(
339            &self,
340            _buf: &mut [u8],
341        ) -> Result<(usize, DataLinkAddress), rustbac_datalink::DataLinkError> {
342            Err(rustbac_datalink::DataLinkError::InvalidFrame)
343        }
344    }
345
346    #[tokio::test]
347    async fn handle_write_property_accepts_optional_array_index() {
348        let dl = MockDataLink::default();
349        let sent = dl.sent.clone();
350        let sim = SimulatedDevice::new(1, dl);
351
352        let mut payload = [0u8; 256];
353        let mut w = Writer::new(&mut payload);
354        encode_ctx_unsigned(&mut w, 0, sim.device_id.raw()).unwrap();
355        encode_ctx_unsigned(&mut w, 1, PropertyId::ObjectName.to_u32()).unwrap();
356        encode_ctx_unsigned(&mut w, 2, 0).unwrap();
357        Tag::Opening { tag_num: 3 }.encode(&mut w).unwrap();
358        rustbac_core::services::value_codec::encode_application_data_value(
359            &mut w,
360            &DataValue::CharacterString("updated-name"),
361        )
362        .unwrap();
363        Tag::Closing { tag_num: 3 }.encode(&mut w).unwrap();
364
365        let source = DataLinkAddress::Ip("127.0.0.1:47808".parse().unwrap());
366        let mut r = Reader::new(w.as_written());
367        sim.handle_write_property(&mut r, 9, source).await.unwrap();
368
369        let objects = sim.objects.read().await;
370        let props = objects.get(&sim.device_id).unwrap();
371        assert_eq!(
372            props.get(&PropertyId::ObjectName),
373            Some(&ClientDataValue::CharacterString(
374                "updated-name".to_string()
375            ))
376        );
377
378        let sent = sent.lock().expect("poisoned lock");
379        assert_eq!(sent.len(), 1);
380        let mut ack_reader = Reader::new(&sent[0].1);
381        let _npdu = Npdu::decode(&mut ack_reader).unwrap();
382        let ack = SimpleAck::decode(&mut ack_reader).unwrap();
383        assert_eq!(ack.invoke_id, 9);
384        assert_eq!(ack.service_choice, SERVICE_WRITE_PROPERTY);
385    }
386}