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