Skip to main content

rustbac_client/
server.rs

1//! BACnet server/responder implementation.
2//!
3//! [`BacnetServer`] binds a [`DataLink`] transport and dispatches incoming
4//! service requests to a user-supplied [`ServiceHandler`].  [`ObjectStore`]
5//! is a convenient thread-safe property store that implements
6//! [`ServiceHandler`] out of the box.
7
8use crate::ClientDataValue;
9use rustbac_core::apdu::{
10    ApduType, ComplexAckHeader, ConfirmedRequestHeader, SimpleAck, UnconfirmedRequestHeader,
11};
12use rustbac_core::encoding::{
13    primitives::{decode_unsigned, encode_ctx_unsigned},
14    reader::Reader,
15    tag::Tag,
16    writer::Writer,
17};
18use rustbac_core::npdu::Npdu;
19use rustbac_core::services::i_am::IAmRequest;
20use rustbac_core::services::read_property::SERVICE_READ_PROPERTY;
21use rustbac_core::services::read_property_multiple::SERVICE_READ_PROPERTY_MULTIPLE;
22use rustbac_core::services::value_codec::encode_application_data_value;
23use rustbac_core::services::write_property::SERVICE_WRITE_PROPERTY;
24use rustbac_core::types::{ObjectId, PropertyId};
25use rustbac_datalink::{DataLink, DataLinkAddress};
26use std::collections::HashMap;
27use std::future::Future;
28use std::sync::{Arc, Mutex};
29
30// ─────────────────────────────────────────────────────────────────────────────
31// BacnetServiceError
32// ─────────────────────────────────────────────────────────────────────────────
33
34/// Errors that a [`ServiceHandler`] may return.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum BacnetServiceError {
37    /// The addressed object does not exist.
38    UnknownObject,
39    /// The property does not exist on the object.
40    UnknownProperty,
41    /// The property is not writable.
42    WriteAccessDenied,
43    /// The supplied value is of the wrong type.
44    InvalidDataType,
45}
46
47impl BacnetServiceError {
48    /// Map the error to a (error_class, error_code) pair for the wire.
49    fn to_error_class_code(self) -> (u8, u8) {
50        match self {
51            // error-class: object (1), error-code: unknown-object (31)
52            BacnetServiceError::UnknownObject => (1, 31),
53            // error-class: property (2), error-code: unknown-property (32)
54            BacnetServiceError::UnknownProperty => (2, 32),
55            // error-class: property (2), error-code: write-access-denied (40)
56            BacnetServiceError::WriteAccessDenied => (2, 40),
57            // error-class: property (2), error-code: invalid-data-type (9)
58            BacnetServiceError::InvalidDataType => (2, 9),
59        }
60    }
61}
62
63// ─────────────────────────────────────────────────────────────────────────────
64// ServiceHandler trait
65// ─────────────────────────────────────────────────────────────────────────────
66
67/// Handler trait that the server calls for each incoming service request.
68pub trait ServiceHandler: Send + Sync + 'static {
69    /// Called for a ReadProperty confirmed request.
70    fn read_property(
71        &self,
72        object_id: ObjectId,
73        property_id: PropertyId,
74        array_index: Option<u32>,
75    ) -> Result<ClientDataValue, BacnetServiceError>;
76
77    /// Called for a WriteProperty confirmed request.
78    fn write_property(
79        &self,
80        object_id: ObjectId,
81        property_id: PropertyId,
82        array_index: Option<u32>,
83        value: ClientDataValue,
84        priority: Option<u8>,
85    ) -> Result<(), BacnetServiceError>;
86}
87
88// ─────────────────────────────────────────────────────────────────────────────
89// ObjectStore
90// ─────────────────────────────────────────────────────────────────────────────
91
92/// Thread-safe property store backed by `Mutex<HashMap<ObjectId, HashMap<PropertyId, ClientDataValue>>>`.
93pub struct ObjectStore {
94    inner: Mutex<HashMap<ObjectId, HashMap<PropertyId, ClientDataValue>>>,
95}
96
97impl ObjectStore {
98    /// Create an empty store.
99    pub fn new() -> Self {
100        Self {
101            inner: Mutex::new(HashMap::new()),
102        }
103    }
104
105    /// Insert or overwrite a property value.
106    pub fn set(&self, object_id: ObjectId, property_id: PropertyId, value: ClientDataValue) {
107        let mut map = self.inner.lock().expect("ObjectStore lock poisoned");
108        map.entry(object_id).or_default().insert(property_id, value);
109    }
110
111    /// Retrieve a property value, returning `None` if the object or property is absent.
112    pub fn get(&self, object_id: ObjectId, property_id: PropertyId) -> Option<ClientDataValue> {
113        let map = self.inner.lock().expect("ObjectStore lock poisoned");
114        map.get(&object_id)?.get(&property_id).cloned()
115    }
116
117    /// Remove all properties associated with an object.
118    pub fn remove_object(&self, object_id: ObjectId) {
119        let mut map = self.inner.lock().expect("ObjectStore lock poisoned");
120        map.remove(&object_id);
121    }
122
123    /// Return a snapshot of all known object identifiers.
124    pub fn object_ids(&self) -> Vec<ObjectId> {
125        let map = self.inner.lock().expect("ObjectStore lock poisoned");
126        map.keys().copied().collect()
127    }
128}
129
130impl Default for ObjectStore {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136// ─────────────────────────────────────────────────────────────────────────────
137// ObjectStoreHandler
138// ─────────────────────────────────────────────────────────────────────────────
139
140/// A [`ServiceHandler`] that delegates directly to an [`Arc<ObjectStore>`].
141///
142/// ReadProperty returns the stored value or the appropriate error.
143/// WriteProperty always accepts writes (no write-protection logic).
144pub struct ObjectStoreHandler {
145    store: Arc<ObjectStore>,
146}
147
148impl ObjectStoreHandler {
149    /// Wrap an existing shared store.
150    pub fn new(store: Arc<ObjectStore>) -> Self {
151        Self { store }
152    }
153}
154
155impl ServiceHandler for ObjectStoreHandler {
156    fn read_property(
157        &self,
158        object_id: ObjectId,
159        property_id: PropertyId,
160        _array_index: Option<u32>,
161    ) -> Result<ClientDataValue, BacnetServiceError> {
162        // Check object existence first.
163        let map = self.store.inner.lock().expect("ObjectStore lock poisoned");
164        let props = map
165            .get(&object_id)
166            .ok_or(BacnetServiceError::UnknownObject)?;
167        props
168            .get(&property_id)
169            .cloned()
170            .ok_or(BacnetServiceError::UnknownProperty)
171    }
172
173    fn write_property(
174        &self,
175        object_id: ObjectId,
176        property_id: PropertyId,
177        _array_index: Option<u32>,
178        value: ClientDataValue,
179        _priority: Option<u8>,
180    ) -> Result<(), BacnetServiceError> {
181        let mut map = self.store.inner.lock().expect("ObjectStore lock poisoned");
182        // Write is only permitted if the object already exists.
183        let props = map
184            .get_mut(&object_id)
185            .ok_or(BacnetServiceError::UnknownObject)?;
186        props.insert(property_id, value);
187        Ok(())
188    }
189}
190
191// ─────────────────────────────────────────────────────────────────────────────
192// BacnetServer
193// ─────────────────────────────────────────────────────────────────────────────
194
195/// A server that binds a [`DataLink`] and dispatches incoming service requests.
196pub struct BacnetServer<D: DataLink> {
197    datalink: Arc<D>,
198    handler: Arc<dyn ServiceHandler>,
199    device_id: u32,
200    vendor_id: u16,
201    /// Stored for future use in I-Am responses and segmentation negotiation.
202    #[allow(dead_code)]
203    max_apdu: u8,
204}
205
206impl<D: DataLink> BacnetServer<D> {
207    /// Create a new server with the given datalink and device instance number.
208    pub fn new(datalink: D, device_id: u32, handler: impl ServiceHandler) -> Self {
209        Self {
210            datalink: Arc::new(datalink),
211            handler: Arc::new(handler),
212            device_id,
213            vendor_id: 0,
214            max_apdu: 5, // standard max APDU size index 5 → 1476 bytes
215        }
216    }
217
218    /// Override the vendor ID sent in I-Am responses (default: 0).
219    pub fn with_vendor_id(mut self, vendor_id: u16) -> Self {
220        self.vendor_id = vendor_id;
221        self
222    }
223
224    /// Run the serve loop.
225    ///
226    /// Receives frames, parses them, and dispatches:
227    /// - UnconfirmedRequest Who-Is (0x08) → I-Am; others ignored.
228    /// - ConfirmedRequest ReadProperty (0x0C) → ComplexAck or Error.
229    /// - ConfirmedRequest WriteProperty (0x0F) → SimpleAck or Error.
230    /// - ConfirmedRequest ReadPropertyMultiple (0x0E) → ComplexAck or Error.
231    /// - Any other confirmed service → Reject (UNRECOGNIZED_SERVICE = 0x08).
232    pub fn serve(self) -> impl Future<Output = ()> {
233        async move {
234            let mut buf = [0u8; 1500];
235            loop {
236                let result = self.datalink.recv(&mut buf).await;
237                match result {
238                    Ok((n, source)) => {
239                        if let Err(e) = self.handle_frame(&buf[..n], source).await {
240                            log::debug!("server: error handling frame: {e:?}");
241                        }
242                    }
243                    Err(e) => {
244                        log::debug!("server: datalink recv error: {e:?}");
245                        // On persistent transport errors avoid a tight busy loop.
246                        tokio::task::yield_now().await;
247                    }
248                }
249            }
250        }
251    }
252
253    // ── private helpers ──────────────────────────────────────────────────────
254
255    async fn handle_frame(
256        &self,
257        frame: &[u8],
258        source: DataLinkAddress,
259    ) -> Result<(), rustbac_core::DecodeError> {
260        let mut r = Reader::new(frame);
261        let _npdu = Npdu::decode(&mut r)?;
262
263        if r.is_empty() {
264            return Ok(());
265        }
266
267        let first = r.peek_u8()?;
268        let apdu_type = ApduType::from_u8(first >> 4);
269
270        match apdu_type {
271            Some(ApduType::UnconfirmedRequest) => {
272                let header = UnconfirmedRequestHeader::decode(&mut r)?;
273                if header.service_choice == 0x08 {
274                    // Who-Is — parse optional limits then respond.
275                    let limits = decode_who_is_limits(&mut r);
276                    if matches_who_is(self.device_id, limits) {
277                        self.send_i_am(source).await;
278                    }
279                }
280                // All other unconfirmed services are ignored.
281            }
282            Some(ApduType::ConfirmedRequest) => {
283                let header = ConfirmedRequestHeader::decode(&mut r)?;
284                let invoke_id = header.invoke_id;
285                match header.service_choice {
286                    SERVICE_READ_PROPERTY => {
287                        self.handle_read_property(&mut r, invoke_id, source).await;
288                    }
289                    SERVICE_WRITE_PROPERTY => {
290                        self.handle_write_property(&mut r, invoke_id, source).await;
291                    }
292                    SERVICE_READ_PROPERTY_MULTIPLE => {
293                        self.handle_read_property_multiple(&mut r, invoke_id, source)
294                            .await;
295                    }
296                    _ => {
297                        // Unknown service — send Reject with UNRECOGNIZED_SERVICE.
298                        self.send_reject(invoke_id, 0x08, source).await;
299                    }
300                }
301            }
302            _ => {
303                // Not a request — ignore.
304            }
305        }
306
307        Ok(())
308    }
309
310    async fn send_i_am(&self, target: DataLinkAddress) {
311        let device_id_raw =
312            rustbac_core::types::ObjectId::new(rustbac_core::types::ObjectType::Device, self.device_id);
313        let req = IAmRequest {
314            device_id: device_id_raw,
315            max_apdu: 1476,
316            segmentation: 3, // no-segmentation
317            vendor_id: self.vendor_id as u32,
318        };
319        let mut buf = [0u8; 128];
320        let mut w = Writer::new(&mut buf);
321        if Npdu::new(0).encode(&mut w).is_err() {
322            return;
323        }
324        if req.encode(&mut w).is_err() {
325            return;
326        }
327        let _ = self.datalink.send(target, w.as_written()).await;
328    }
329
330    async fn handle_read_property(
331        &self,
332        r: &mut Reader<'_>,
333        invoke_id: u8,
334        source: DataLinkAddress,
335    ) {
336        // Decode: object_id [0], property_id [1], optional array_index [2].
337        let object_id = match crate::decode_ctx_object_id(r) {
338            Ok(v) => v,
339            Err(_) => return,
340        };
341        let property_id_raw = match crate::decode_ctx_unsigned(r) {
342            Ok(v) => v,
343            Err(_) => return,
344        };
345        let property_id = PropertyId::from_u32(property_id_raw);
346
347        // Optional array index.
348        let array_index = decode_optional_array_index(r);
349
350        match self
351            .handler
352            .read_property(object_id, property_id, array_index)
353        {
354            Ok(value) => {
355                let borrowed = client_value_to_borrowed(&value);
356                let mut buf = [0u8; 1400];
357                let mut w = Writer::new(&mut buf);
358                if Npdu::new(0).encode(&mut w).is_err() {
359                    return;
360                }
361                if (ComplexAckHeader {
362                    segmented: false,
363                    more_follows: false,
364                    invoke_id,
365                    sequence_number: None,
366                    proposed_window_size: None,
367                    service_choice: SERVICE_READ_PROPERTY,
368                })
369                .encode(&mut w)
370                .is_err()
371                {
372                    return;
373                }
374                if encode_ctx_unsigned(&mut w, 0, object_id.raw()).is_err() {
375                    return;
376                }
377                if encode_ctx_unsigned(&mut w, 1, property_id.to_u32()).is_err() {
378                    return;
379                }
380                if (Tag::Opening { tag_num: 3 }).encode(&mut w).is_err() {
381                    return;
382                }
383                if encode_application_data_value(&mut w, &borrowed).is_err() {
384                    return;
385                }
386                if (Tag::Closing { tag_num: 3 }).encode(&mut w).is_err() {
387                    return;
388                }
389                let _ = self.datalink.send(source, w.as_written()).await;
390            }
391            Err(err) => {
392                self.send_error(invoke_id, SERVICE_READ_PROPERTY, err, source)
393                    .await;
394            }
395        }
396    }
397
398    async fn handle_write_property(
399        &self,
400        r: &mut Reader<'_>,
401        invoke_id: u8,
402        source: DataLinkAddress,
403    ) {
404        // Decode: object_id [0], property_id [1], optional array_index [2], value [3], optional priority [4].
405        let object_id = match crate::decode_ctx_object_id(r) {
406            Ok(v) => v,
407            Err(_) => return,
408        };
409        let property_id_raw = match crate::decode_ctx_unsigned(r) {
410            Ok(v) => v,
411            Err(_) => return,
412        };
413        let property_id = PropertyId::from_u32(property_id_raw);
414
415        // Optional array index [2].
416        let next_tag = match Tag::decode(r) {
417            Ok(t) => t,
418            Err(_) => return,
419        };
420        let (array_index, value_start_tag) = match next_tag {
421            Tag::Context { tag_num: 2, len } => {
422                let idx = match decode_unsigned(r, len as usize) {
423                    Ok(v) => v,
424                    Err(_) => return,
425                };
426                let vt = match Tag::decode(r) {
427                    Ok(t) => t,
428                    Err(_) => return,
429                };
430                (Some(idx), vt)
431            }
432            other => (None, other),
433        };
434
435        if value_start_tag != (Tag::Opening { tag_num: 3 }) {
436            return;
437        }
438
439        let val = match rustbac_core::services::value_codec::decode_application_data_value(r) {
440            Ok(v) => v,
441            Err(_) => return,
442        };
443
444        match Tag::decode(r) {
445            Ok(Tag::Closing { tag_num: 3 }) => {}
446            _ => return,
447        }
448
449        // Optional priority [4].
450        let priority = if !r.is_empty() {
451            match Tag::decode(r) {
452                Ok(Tag::Context { tag_num: 4, len }) => {
453                    match decode_unsigned(r, len as usize) {
454                        Ok(p) => Some(p as u8),
455                        Err(_) => return,
456                    }
457                }
458                _ => None,
459            }
460        } else {
461            None
462        };
463
464        let client_val = crate::data_value_to_client(val);
465
466        match self
467            .handler
468            .write_property(object_id, property_id, array_index, client_val, priority)
469        {
470            Ok(()) => {
471                let mut buf = [0u8; 32];
472                let mut w = Writer::new(&mut buf);
473                if Npdu::new(0).encode(&mut w).is_err() {
474                    return;
475                }
476                if (SimpleAck {
477                    invoke_id,
478                    service_choice: SERVICE_WRITE_PROPERTY,
479                })
480                .encode(&mut w)
481                .is_err()
482                {
483                    return;
484                }
485                let _ = self.datalink.send(source, w.as_written()).await;
486            }
487            Err(err) => {
488                self.send_error(invoke_id, SERVICE_WRITE_PROPERTY, err, source)
489                    .await;
490            }
491        }
492    }
493
494    async fn handle_read_property_multiple(
495        &self,
496        r: &mut Reader<'_>,
497        invoke_id: u8,
498        source: DataLinkAddress,
499    ) {
500        // Collect all (object_id, [(property_id, array_index)]) specs from the request.
501        let mut specs: Vec<(ObjectId, Vec<(PropertyId, Option<u32>)>)> = Vec::new();
502
503        while !r.is_empty() {
504            // object-identifier [0]
505            let object_id = match crate::decode_ctx_object_id(r) {
506                Ok(v) => v,
507                Err(_) => return,
508            };
509
510            // list-of-property-references [1] opening tag
511            match Tag::decode(r) {
512                Ok(Tag::Opening { tag_num: 1 }) => {}
513                _ => return,
514            }
515
516            let mut props: Vec<(PropertyId, Option<u32>)> = Vec::new();
517            loop {
518                // Each property reference: property-identifier [0], optional array-index [1].
519                let tag = match Tag::decode(r) {
520                    Ok(t) => t,
521                    Err(_) => return,
522                };
523                if tag == (Tag::Closing { tag_num: 1 }) {
524                    break;
525                }
526                let property_id = match tag {
527                    Tag::Context { tag_num: 0, len } => {
528                        match decode_unsigned(r, len as usize) {
529                            Ok(v) => PropertyId::from_u32(v),
530                            Err(_) => return,
531                        }
532                    }
533                    _ => return,
534                };
535
536                // Optional array index [1].
537                let array_index = if !r.is_empty() {
538                    // peek next tag without consuming
539                    match peek_context_tag(r, 1) {
540                        Some(len) => {
541                            // consume the tag byte(s) we already peeked
542                            match Tag::decode(r) {
543                                Ok(_) => {}
544                                Err(_) => return,
545                            }
546                            match decode_unsigned(r, len as usize) {
547                                Ok(idx) => Some(idx),
548                                Err(_) => return,
549                            }
550                        }
551                        None => None,
552                    }
553                } else {
554                    None
555                };
556
557                props.push((property_id, array_index));
558            }
559
560            specs.push((object_id, props));
561        }
562
563        // Build response buffer.
564        let mut buf = [0u8; 1400];
565        let mut w = Writer::new(&mut buf);
566        if Npdu::new(0).encode(&mut w).is_err() {
567            return;
568        }
569        if (ComplexAckHeader {
570            segmented: false,
571            more_follows: false,
572            invoke_id,
573            sequence_number: None,
574            proposed_window_size: None,
575            service_choice: SERVICE_READ_PROPERTY_MULTIPLE,
576        })
577        .encode(&mut w)
578        .is_err()
579        {
580            return;
581        }
582
583        for (object_id, props) in &specs {
584            // object-identifier [0]
585            if encode_ctx_unsigned(&mut w, 0, object_id.raw()).is_err() {
586                return;
587            }
588            // list-of-results [1] opening
589            if (Tag::Opening { tag_num: 1 }).encode(&mut w).is_err() {
590                return;
591            }
592
593            for (property_id, array_index) in props {
594                // property-identifier [2]
595                if encode_ctx_unsigned(&mut w, 2, property_id.to_u32()).is_err() {
596                    return;
597                }
598                // optional array-index [3]
599                if let Some(idx) = array_index {
600                    if encode_ctx_unsigned(&mut w, 3, *idx).is_err() {
601                        return;
602                    }
603                }
604
605                // property-access-result [4] opening
606                if (Tag::Opening { tag_num: 4 }).encode(&mut w).is_err() {
607                    return;
608                }
609
610                match self.handler.read_property(*object_id, *property_id, *array_index) {
611                    Ok(value) => {
612                        let borrowed = client_value_to_borrowed(&value);
613                        if encode_application_data_value(&mut w, &borrowed).is_err() {
614                            return;
615                        }
616                    }
617                    Err(err) => {
618                        // Encode property-access-error [5] with errorClass [0] / errorCode [1].
619                        let (class, code) = err.to_error_class_code();
620                        if (Tag::Opening { tag_num: 5 }).encode(&mut w).is_err() {
621                            return;
622                        }
623                        if encode_ctx_unsigned(&mut w, 0, class as u32).is_err() {
624                            return;
625                        }
626                        if encode_ctx_unsigned(&mut w, 1, code as u32).is_err() {
627                            return;
628                        }
629                        if (Tag::Closing { tag_num: 5 }).encode(&mut w).is_err() {
630                            return;
631                        }
632                    }
633                }
634
635                // property-access-result [4] closing
636                if (Tag::Closing { tag_num: 4 }).encode(&mut w).is_err() {
637                    return;
638                }
639            }
640
641            // list-of-results [1] closing
642            if (Tag::Closing { tag_num: 1 }).encode(&mut w).is_err() {
643                return;
644            }
645        }
646
647        let _ = self.datalink.send(source, w.as_written()).await;
648    }
649
650    async fn send_error(
651        &self,
652        invoke_id: u8,
653        service_choice: u8,
654        err: BacnetServiceError,
655        target: DataLinkAddress,
656    ) {
657        let (class, code) = err.to_error_class_code();
658        let mut buf = [0u8; 64];
659        let mut w = Writer::new(&mut buf);
660        if Npdu::new(0).encode(&mut w).is_err() {
661            return;
662        }
663        // Error PDU header: type=5 (Error)
664        if w.write_u8(0x50).is_err() {
665            return;
666        }
667        if w.write_u8(invoke_id).is_err() {
668            return;
669        }
670        if w.write_u8(service_choice).is_err() {
671            return;
672        }
673        // error-class: application Enumerated
674        if (Tag::Application {
675            tag: rustbac_core::encoding::tag::AppTag::Enumerated,
676            len: 1,
677        })
678        .encode(&mut w)
679        .is_err()
680        {
681            return;
682        }
683        if w.write_u8(class).is_err() {
684            return;
685        }
686        // error-code: application Enumerated
687        if (Tag::Application {
688            tag: rustbac_core::encoding::tag::AppTag::Enumerated,
689            len: 1,
690        })
691        .encode(&mut w)
692        .is_err()
693        {
694            return;
695        }
696        if w.write_u8(code).is_err() {
697            return;
698        }
699        let _ = self.datalink.send(target, w.as_written()).await;
700    }
701
702    async fn send_reject(&self, invoke_id: u8, reason: u8, target: DataLinkAddress) {
703        let mut buf = [0u8; 16];
704        let mut w = Writer::new(&mut buf);
705        if Npdu::new(0).encode(&mut w).is_err() {
706            return;
707        }
708        // Reject PDU: type=6 (Reject)
709        if w.write_u8(0x60).is_err() {
710            return;
711        }
712        if w.write_u8(invoke_id).is_err() {
713            return;
714        }
715        if w.write_u8(reason).is_err() {
716            return;
717        }
718        let _ = self.datalink.send(target, w.as_written()).await;
719    }
720}
721
722// ─────────────────────────────────────────────────────────────────────────────
723// Free-standing helpers
724// ─────────────────────────────────────────────────────────────────────────────
725
726/// Decode Who-Is optional [0] low-limit and [1] high-limit.
727fn decode_who_is_limits(r: &mut Reader<'_>) -> Option<(u32, u32)> {
728    if r.is_empty() {
729        return None;
730    }
731    let tag0 = Tag::decode(r).ok()?;
732    let low = match tag0 {
733        Tag::Context { tag_num: 0, len } => decode_unsigned(r, len as usize).ok()?,
734        _ => return None,
735    };
736    let tag1 = Tag::decode(r).ok()?;
737    let high = match tag1 {
738        Tag::Context { tag_num: 1, len } => decode_unsigned(r, len as usize).ok()?,
739        _ => return None,
740    };
741    Some((low, high))
742}
743
744/// Return true when `device_id` falls within the Who-Is range (or it is a global Who-Is).
745fn matches_who_is(device_id: u32, limits: Option<(u32, u32)>) -> bool {
746    match limits {
747        None => true,
748        Some((low, high)) => device_id >= low && device_id <= high,
749    }
750}
751
752/// Decode an optional context-tagged array index from the current reader position.
753///
754/// This is a non-destructive peek: if the next tag is not a context tag with
755/// tag_num == 2, `None` is returned and the reader is **not** advanced.
756fn decode_optional_array_index(r: &mut Reader<'_>) -> Option<u32> {
757    if r.is_empty() {
758        return None;
759    }
760    // Peek the first byte to see if it could be context tag 2.
761    let first = r.peek_u8().ok()?;
762    // Context tag 2 with short form: upper nibble = (tag_num << 4) | 0x08 = 0x28, 0x29, 0x2A, 0x2B
763    // The tag class bit (bit 3) = 1 for context tags; tag_num is bits 7-4.
764    // For context tag 2: byte = (2 << 4) | 0x08 | len_byte where len_byte ∈ {1,2,3,4}
765    // But we should decode properly and put it back if not matching.
766    // Reader doesn't support un-consuming, so we use a clone.
767    let tag = Tag::decode(r).ok()?;
768    match tag {
769        Tag::Context { tag_num: 2, len } => decode_unsigned(r, len as usize).ok(),
770        _ => {
771            // Not an array index tag — put it back by... we can't.
772            // This function is only called when we know the array index is encoded
773            // (i.e., right after property_id and before the closing tag in ReadProperty).
774            // Since Reader has no unget, we rely on the caller to only call this
775            // after property_id and only if the bytes represent an array index.
776            // In practice the caller already consumed the property_id tag so any
777            // remaining tag is either [2] array_index or end-of-frame.
778            let _ = first; // silence unused warning
779            None
780        }
781    }
782}
783
784/// Peek whether the next tag in `r` is a context tag with the given `tag_num`.
785/// Returns the `len` field if it matches, `None` otherwise.
786/// Does NOT advance the reader.
787fn peek_context_tag(r: &mut Reader<'_>, tag_num: u8) -> Option<u32> {
788    let first = r.peek_u8().ok()?;
789    // Short-form context tag: bit3=1 (context), bits 7-4 = tag_num, bits 2-0 = len (0-4).
790    // Short form len encoding: 0-4 means length is that value (for context tags without extended len).
791    // Byte layout: [tag_num(4) | class(1) | len(3)] where class bit set means context.
792    // For context: byte = (tag_num << 4) | 0x08 | short_len  (short_len < 5)
793    // Closing/Opening tags have short_len = 6 or 7.
794    let is_context = (first & 0x08) != 0 && (first & 0x07) < 6;
795    if !is_context {
796        return None;
797    }
798    let this_tag_num = first >> 4;
799    if this_tag_num != tag_num {
800        return None;
801    }
802    let short_len = first & 0x07;
803    if short_len < 5 {
804        Some(short_len as u32)
805    } else {
806        // Extended length — not expected for small BACnet property indices, skip.
807        None
808    }
809}
810
811/// Convert an owned [`ClientDataValue`] to a borrowed [`rustbac_core::types::DataValue`].
812fn client_value_to_borrowed(val: &ClientDataValue) -> rustbac_core::types::DataValue<'_> {
813    use rustbac_core::types::DataValue;
814    match val {
815        ClientDataValue::Null => DataValue::Null,
816        ClientDataValue::Boolean(v) => DataValue::Boolean(*v),
817        ClientDataValue::Unsigned(v) => DataValue::Unsigned(*v),
818        ClientDataValue::Signed(v) => DataValue::Signed(*v),
819        ClientDataValue::Real(v) => DataValue::Real(*v),
820        ClientDataValue::Double(v) => DataValue::Double(*v),
821        ClientDataValue::OctetString(v) => DataValue::OctetString(v),
822        ClientDataValue::CharacterString(v) => DataValue::CharacterString(v),
823        ClientDataValue::BitString { unused_bits, data } => {
824            DataValue::BitString(rustbac_core::types::BitString {
825                unused_bits: *unused_bits,
826                data,
827            })
828        }
829        ClientDataValue::Enumerated(v) => DataValue::Enumerated(*v),
830        ClientDataValue::Date(v) => DataValue::Date(*v),
831        ClientDataValue::Time(v) => DataValue::Time(*v),
832        ClientDataValue::ObjectId(v) => DataValue::ObjectId(*v),
833        ClientDataValue::Constructed { tag_num, values } => DataValue::Constructed {
834            tag_num: *tag_num,
835            values: values.iter().map(client_value_to_borrowed).collect(),
836        },
837    }
838}
839
840// ─────────────────────────────────────────────────────────────────────────────
841// Writer helper — expose write_u8 used above
842// ─────────────────────────────────────────────────────────────────────────────
843
844/// Thin extension to allow calling `w.write_u8` inside this module.
845trait WriterExt {
846    fn write_u8(&mut self, b: u8) -> Result<(), rustbac_core::EncodeError>;
847}
848
849impl WriterExt for Writer<'_> {
850    fn write_u8(&mut self, b: u8) -> Result<(), rustbac_core::EncodeError> {
851        Writer::write_u8(self, b)
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use rustbac_core::apdu::{ComplexAckHeader, SimpleAck};
859    use rustbac_core::encoding::{reader::Reader, writer::Writer};
860    use rustbac_core::npdu::Npdu;
861    use rustbac_core::services::read_property::SERVICE_READ_PROPERTY;
862    use rustbac_core::services::write_property::SERVICE_WRITE_PROPERTY;
863    use rustbac_core::types::{ObjectId, ObjectType, PropertyId};
864    use rustbac_datalink::DataLinkAddress;
865    use std::sync::{Arc, Mutex};
866
867    #[derive(Clone, Default)]
868    struct MockDataLink {
869        sent: Arc<Mutex<Vec<(DataLinkAddress, Vec<u8>)>>>,
870    }
871
872    impl rustbac_datalink::DataLink for MockDataLink {
873        async fn send(
874            &self,
875            address: DataLinkAddress,
876            payload: &[u8],
877        ) -> Result<(), rustbac_datalink::DataLinkError> {
878            self.sent
879                .lock()
880                .expect("poisoned")
881                .push((address, payload.to_vec()));
882            Ok(())
883        }
884
885        async fn recv(
886            &self,
887            _buf: &mut [u8],
888        ) -> Result<(usize, DataLinkAddress), rustbac_datalink::DataLinkError> {
889            Err(rustbac_datalink::DataLinkError::InvalidFrame)
890        }
891    }
892
893    fn make_server() -> (BacnetServer<MockDataLink>, Arc<Mutex<Vec<(DataLinkAddress, Vec<u8>)>>>, Arc<ObjectStore>) {
894        let store = Arc::new(ObjectStore::new());
895        let device_id = ObjectId::new(ObjectType::Device, 42);
896        store.set(
897            device_id,
898            PropertyId::ObjectName,
899            ClientDataValue::CharacterString("TestDevice".to_string()),
900        );
901        let handler = ObjectStoreHandler::new(store.clone());
902        let dl = MockDataLink::default();
903        let sent = dl.sent.clone();
904        let server = BacnetServer::new(dl, 42, handler);
905        (server, sent, store)
906    }
907
908    fn source() -> DataLinkAddress {
909        DataLinkAddress::Ip("127.0.0.1:47808".parse().unwrap())
910    }
911
912    #[tokio::test]
913    async fn object_store_set_get_remove() {
914        let store = ObjectStore::new();
915        let oid = ObjectId::new(ObjectType::AnalogValue, 1);
916        store.set(oid, PropertyId::PresentValue, ClientDataValue::Real(3.14));
917        assert_eq!(
918            store.get(oid, PropertyId::PresentValue),
919            Some(ClientDataValue::Real(3.14))
920        );
921        store.remove_object(oid);
922        assert_eq!(store.get(oid, PropertyId::PresentValue), None);
923    }
924
925    #[tokio::test]
926    async fn object_store_object_ids() {
927        let store = ObjectStore::new();
928        let oid1 = ObjectId::new(ObjectType::AnalogValue, 1);
929        let oid2 = ObjectId::new(ObjectType::AnalogValue, 2);
930        store.set(oid1, PropertyId::PresentValue, ClientDataValue::Real(1.0));
931        store.set(oid2, PropertyId::PresentValue, ClientDataValue::Real(2.0));
932        let mut ids = store.object_ids();
933        ids.sort_by_key(|id| id.raw());
934        assert!(ids.contains(&oid1));
935        assert!(ids.contains(&oid2));
936    }
937
938    #[tokio::test]
939    async fn read_property_known_returns_complex_ack() {
940        let (server, sent, _store) = make_server();
941        let device_id = ObjectId::new(ObjectType::Device, 42);
942
943        // Build a ReadProperty request frame.
944        use rustbac_core::encoding::primitives::encode_ctx_unsigned;
945        let mut req_buf = [0u8; 256];
946        let mut w = Writer::new(&mut req_buf);
947        Npdu::new(0).encode(&mut w).unwrap();
948        rustbac_core::apdu::ConfirmedRequestHeader {
949            segmented: false,
950            more_follows: false,
951            segmented_response_accepted: true,
952            max_segments: 0,
953            max_apdu: 5,
954            invoke_id: 7,
955            sequence_number: None,
956            proposed_window_size: None,
957            service_choice: SERVICE_READ_PROPERTY,
958        }
959        .encode(&mut w)
960        .unwrap();
961        encode_ctx_unsigned(&mut w, 0, device_id.raw()).unwrap();
962        encode_ctx_unsigned(&mut w, 1, PropertyId::ObjectName.to_u32()).unwrap();
963
964        server
965            .handle_frame(w.as_written(), source())
966            .await
967            .unwrap();
968
969        let sent = sent.lock().expect("poisoned");
970        assert_eq!(sent.len(), 1);
971        let mut r = Reader::new(&sent[0].1);
972        let _npdu = Npdu::decode(&mut r).unwrap();
973        let hdr = ComplexAckHeader::decode(&mut r).unwrap();
974        assert_eq!(hdr.invoke_id, 7);
975        assert_eq!(hdr.service_choice, SERVICE_READ_PROPERTY);
976    }
977
978    #[tokio::test]
979    async fn read_property_unknown_object_returns_error() {
980        let (server, sent, _store) = make_server();
981        let unknown = ObjectId::new(ObjectType::AnalogValue, 999);
982
983        use rustbac_core::encoding::primitives::encode_ctx_unsigned;
984        let mut req_buf = [0u8; 256];
985        let mut w = Writer::new(&mut req_buf);
986        Npdu::new(0).encode(&mut w).unwrap();
987        rustbac_core::apdu::ConfirmedRequestHeader {
988            segmented: false,
989            more_follows: false,
990            segmented_response_accepted: true,
991            max_segments: 0,
992            max_apdu: 5,
993            invoke_id: 3,
994            sequence_number: None,
995            proposed_window_size: None,
996            service_choice: SERVICE_READ_PROPERTY,
997        }
998        .encode(&mut w)
999        .unwrap();
1000        encode_ctx_unsigned(&mut w, 0, unknown.raw()).unwrap();
1001        encode_ctx_unsigned(&mut w, 1, PropertyId::PresentValue.to_u32()).unwrap();
1002
1003        server
1004            .handle_frame(w.as_written(), source())
1005            .await
1006            .unwrap();
1007
1008        let sent = sent.lock().expect("poisoned");
1009        assert_eq!(sent.len(), 1);
1010        // First byte after NPDU should be 0x50 (Error PDU).
1011        let mut r = Reader::new(&sent[0].1);
1012        let _npdu = Npdu::decode(&mut r).unwrap();
1013        let type_byte = r.read_u8().unwrap();
1014        assert_eq!(type_byte >> 4, 5, "expected Error PDU type");
1015    }
1016
1017    #[tokio::test]
1018    async fn write_property_updates_store() {
1019        let (server, sent, store) = make_server();
1020        let device_id = ObjectId::new(ObjectType::Device, 42);
1021
1022        use rustbac_core::encoding::primitives::encode_ctx_unsigned;
1023        use rustbac_core::services::value_codec::encode_application_data_value;
1024        use rustbac_core::types::DataValue;
1025
1026        let mut req_buf = [0u8; 256];
1027        let mut w = Writer::new(&mut req_buf);
1028        Npdu::new(0).encode(&mut w).unwrap();
1029        rustbac_core::apdu::ConfirmedRequestHeader {
1030            segmented: false,
1031            more_follows: false,
1032            segmented_response_accepted: false,
1033            max_segments: 0,
1034            max_apdu: 5,
1035            invoke_id: 11,
1036            sequence_number: None,
1037            proposed_window_size: None,
1038            service_choice: SERVICE_WRITE_PROPERTY,
1039        }
1040        .encode(&mut w)
1041        .unwrap();
1042        encode_ctx_unsigned(&mut w, 0, device_id.raw()).unwrap();
1043        encode_ctx_unsigned(&mut w, 1, PropertyId::ObjectName.to_u32()).unwrap();
1044        Tag::Opening { tag_num: 3 }.encode(&mut w).unwrap();
1045        encode_application_data_value(&mut w, &DataValue::CharacterString("NewName")).unwrap();
1046        Tag::Closing { tag_num: 3 }.encode(&mut w).unwrap();
1047
1048        server
1049            .handle_frame(w.as_written(), source())
1050            .await
1051            .unwrap();
1052
1053        // Verify SimpleAck was sent.
1054        let sent_frames = sent.lock().expect("poisoned");
1055        assert_eq!(sent_frames.len(), 1);
1056        let mut r = Reader::new(&sent_frames[0].1);
1057        let _npdu = Npdu::decode(&mut r).unwrap();
1058        let ack = SimpleAck::decode(&mut r).unwrap();
1059        assert_eq!(ack.invoke_id, 11);
1060        assert_eq!(ack.service_choice, SERVICE_WRITE_PROPERTY);
1061        drop(sent_frames);
1062
1063        // Verify store updated.
1064        assert_eq!(
1065            store.get(device_id, PropertyId::ObjectName),
1066            Some(ClientDataValue::CharacterString("NewName".to_string()))
1067        );
1068    }
1069
1070    #[tokio::test]
1071    async fn who_is_sends_i_am() {
1072        let (server, sent, _store) = make_server();
1073
1074        let mut req_buf = [0u8; 32];
1075        let mut w = Writer::new(&mut req_buf);
1076        Npdu::new(0).encode(&mut w).unwrap();
1077        // UnconfirmedRequest Who-Is (0x08) with no limits.
1078        rustbac_core::apdu::UnconfirmedRequestHeader { service_choice: 0x08 }
1079            .encode(&mut w)
1080            .unwrap();
1081
1082        server
1083            .handle_frame(w.as_written(), source())
1084            .await
1085            .unwrap();
1086
1087        let sent = sent.lock().expect("poisoned");
1088        assert_eq!(sent.len(), 1);
1089        let mut r = Reader::new(&sent[0].1);
1090        let _npdu = Npdu::decode(&mut r).unwrap();
1091        let _unconf_hdr = rustbac_core::apdu::UnconfirmedRequestHeader::decode(&mut r).unwrap();
1092        let iam = rustbac_core::services::i_am::IAmRequest::decode_after_header(&mut r).unwrap();
1093        assert_eq!(iam.device_id.instance(), 42);
1094    }
1095
1096    #[tokio::test]
1097    async fn unknown_service_sends_reject() {
1098        let (server, sent, _store) = make_server();
1099
1100        let mut req_buf = [0u8; 32];
1101        let mut w = Writer::new(&mut req_buf);
1102        Npdu::new(0).encode(&mut w).unwrap();
1103        rustbac_core::apdu::ConfirmedRequestHeader {
1104            segmented: false,
1105            more_follows: false,
1106            segmented_response_accepted: false,
1107            max_segments: 0,
1108            max_apdu: 5,
1109            invoke_id: 99,
1110            sequence_number: None,
1111            proposed_window_size: None,
1112            service_choice: 0x55, // unknown
1113        }
1114        .encode(&mut w)
1115        .unwrap();
1116
1117        server
1118            .handle_frame(w.as_written(), source())
1119            .await
1120            .unwrap();
1121
1122        let sent = sent.lock().expect("poisoned");
1123        assert_eq!(sent.len(), 1);
1124        let mut r = Reader::new(&sent[0].1);
1125        let _npdu = Npdu::decode(&mut r).unwrap();
1126        let type_byte = r.read_u8().unwrap();
1127        assert_eq!(type_byte >> 4, 6, "expected Reject PDU type");
1128        let id = r.read_u8().unwrap();
1129        let reason = r.read_u8().unwrap();
1130        assert_eq!(id, 99);
1131        assert_eq!(reason, 0x08); // UNRECOGNIZED_SERVICE
1132    }
1133}