Skip to main content

matter_interaction/
subscription.rs

1//! `SubscribeRequestMessage` / `SubscribeResponseMessage` / `StatusResponseMessage`
2//! framing — Matter §10.6 (subscription interaction).
3//!
4//! Byte-parity with matter.js is enforced by `tests/im_byte_parity.rs`
5//! against fixtures captured via `cargo xtask capture-im`.
6
7#![forbid(unsafe_code)]
8
9use crate::error::ImError;
10use crate::event::{EventFilter, EventPath};
11use crate::path::ReadPath;
12use crate::{expect_message_struct, skip_container, IM_REVISION};
13use matter_codec::{Element, Tag, TlvReader, TlvWriter};
14
15/// Parameters for a subscription request.
16///
17/// Encodes as a `SubscribeRequestMessage` (Matter §10.6.6):
18/// `keepSubscriptions` (ctx 0), `minIntervalFloor` (ctx 1),
19/// `maxIntervalCeiling` (ctx 2), `attributeRequests` array (ctx 3),
20/// `isFabricFiltered` (ctx 7), `interactionModelRevision` (ctx 0xFF).
21#[derive(Clone, Debug, PartialEq)]
22pub struct SubscribeRequest {
23    /// Whether to keep existing subscriptions alive when this one is
24    /// established. `false` is the typical controller-side value.
25    pub keep_subscriptions: bool,
26    /// Minimum reporting interval floor in seconds.
27    pub min_interval_floor: u16,
28    /// Maximum reporting interval ceiling in seconds.
29    pub max_interval_ceiling: u16,
30    /// Attribute paths to subscribe to. Each [`ReadPath`] field that is
31    /// `Some` is emitted as a context-tagged member of the
32    /// `AttributePathIB` list (endpoint=2, cluster=3, attribute=4);
33    /// `None` fields are omitted (wildcard).
34    pub paths: Vec<ReadPath>,
35    /// Event paths to subscribe to (`EventRequests`, context tag 4). Empty ⇒
36    /// the array is omitted.
37    pub event_paths: Vec<EventPath>,
38    /// Event filters (`EventFilters`, context tag 5) — report only events with
39    /// number `>= event_min`. Empty ⇒ the array is omitted.
40    pub event_filters: Vec<EventFilter>,
41}
42
43/// Parsed `SubscribeResponseMessage` — the device's subscription confirmation.
44///
45/// Contains the server-assigned `subscription_id` (opaque, stable for the
46/// lifetime of the subscription) and the negotiated `max_interval`.
47#[derive(Clone, Debug, PartialEq)]
48#[non_exhaustive]
49pub struct SubscribeResponse {
50    /// Server-assigned subscription identifier.
51    pub subscription_id: u32,
52    /// Negotiated maximum reporting interval in seconds.
53    pub max_interval: u16,
54}
55
56/// Build a `SubscribeRequestMessage` for the given subscription parameters.
57///
58/// Encodes to the wire format that byte-matches matter.js
59/// `TlvSubscribeRequest.encode(...)` for the same input (verified by
60/// `tests/im_byte_parity.rs`).
61///
62/// The path array reuses the same `AttributePathIB` list encoding as
63/// [`crate::read::build_read_request_paths`]: each path is an anonymous list
64/// with context tags 2/3/4 for endpoint/cluster/attribute; `None` components
65/// are omitted (wildcard).
66#[must_use]
67#[allow(clippy::expect_used, clippy::missing_panics_doc)] // Vec-backed TlvWriter is infallible.
68pub fn build_subscribe_request(req: &SubscribeRequest) -> Vec<u8> {
69    let mut buf = Vec::with_capacity(48 + req.paths.len() * 24 + req.event_paths.len() * 24);
70    let mut w = TlvWriter::new(&mut buf);
71
72    w.start_structure(Tag::Anonymous)
73        .expect("infallible: vec writer");
74
75    // ctx[0]: keepSubscriptions (bool)
76    w.put_bool(Tag::Context(0), req.keep_subscriptions)
77        .expect("infallible: vec writer");
78
79    // ctx[1]: minIntervalFloorSeconds (uint)
80    w.put_uint(Tag::Context(1), u64::from(req.min_interval_floor))
81        .expect("infallible: vec writer");
82
83    // ctx[2]: maxIntervalCeilingSeconds (uint)
84    w.put_uint(Tag::Context(2), u64::from(req.max_interval_ceiling))
85        .expect("infallible: vec writer");
86
87    // ctx[3]: attributeRequests (array of AttributePathIB lists)
88    w.start_array(Tag::Context(3))
89        .expect("infallible: vec writer");
90    for p in &req.paths {
91        w.start_list(Tag::Anonymous)
92            .expect("infallible: vec writer");
93        if let Some(ep) = p.endpoint {
94            w.put_uint(Tag::Context(2), u64::from(ep))
95                .expect("infallible: vec writer");
96        }
97        if let Some(cl) = p.cluster {
98            w.put_uint(Tag::Context(3), u64::from(cl))
99                .expect("infallible: vec writer");
100        }
101        if let Some(at) = p.attribute {
102            w.put_uint(Tag::Context(4), u64::from(at))
103                .expect("infallible: vec writer");
104        }
105        w.end_container().expect("infallible: vec writer");
106    }
107    w.end_container().expect("infallible: vec writer"); // attributeRequests array
108
109    // ctx[4]: eventRequests (array of EventPathIB lists) — omitted when empty.
110    if !req.event_paths.is_empty() {
111        w.start_array(Tag::Context(4))
112            .expect("infallible: vec writer");
113        for p in &req.event_paths {
114            p.write(&mut w).expect("infallible: vec writer");
115        }
116        w.end_container().expect("infallible: vec writer");
117    }
118
119    // ctx[5]: eventFilters (array of EventFilterIB structs) — omitted when empty.
120    if !req.event_filters.is_empty() {
121        w.start_array(Tag::Context(5))
122            .expect("infallible: vec writer");
123        for f in &req.event_filters {
124            f.write(&mut w).expect("infallible: vec writer");
125        }
126        w.end_container().expect("infallible: vec writer");
127    }
128
129    // ctx[7]: isFabricFiltered (bool) — always false for controller-side reads.
130    // ctx[4]=eventRequests / ctx[5]=eventFilters are emitted above when present;
131    // ctx[6] (dataVersionFilters) is unused.
132    w.put_bool(Tag::Context(7), false)
133        .expect("infallible: vec writer");
134
135    // ctx[0xFF]: interactionModelRevision (uint)
136    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
137        .expect("infallible: vec writer");
138
139    w.end_container().expect("infallible: vec writer");
140    buf
141}
142
143/// Parse a `SubscribeResponseMessage` into a [`SubscribeResponse`].
144///
145/// Extracts `subscriptionId` (ctx 0, uint32) and `maxInterval` (ctx 2,
146/// uint16). The `interactionModelRevision` (ctx 0xFF) is read and discarded.
147///
148/// # Errors
149///
150/// Returns [`ImError`] if the message is not a struct, or if
151/// `subscriptionId` / `maxInterval` are absent or out of range.
152pub fn parse_subscribe_response(bytes: &[u8]) -> Result<SubscribeResponse, ImError> {
153    let mut r = TlvReader::new(bytes);
154    expect_message_struct(&mut r)?;
155
156    let mut subscription_id: Option<u32> = None;
157    let mut max_interval: Option<u16> = None;
158
159    loop {
160        match r.next()? {
161            None | Some(Element::ContainerEnd) => break,
162            Some(Element::Scalar {
163                tag: Tag::Context(0),
164                value: matter_codec::Value::Uint(n),
165            }) => {
166                subscription_id = Some(u32::try_from(n).map_err(|_| {
167                    ImError::UnexpectedValue("SubscribeResponse.subscriptionId exceeds u32")
168                })?);
169            }
170            Some(Element::Scalar {
171                tag: Tag::Context(2),
172                value: matter_codec::Value::Uint(n),
173            }) => {
174                max_interval = Some(u16::try_from(n).map_err(|_| {
175                    ImError::UnexpectedValue("SubscribeResponse.maxInterval exceeds u16")
176                })?);
177            }
178            Some(Element::ContainerStart { .. }) => skip_container(&mut r)?,
179            Some(_) => {}
180        }
181    }
182
183    Ok(SubscribeResponse {
184        subscription_id: subscription_id
185            .ok_or(ImError::MissingField("SubscribeResponse.subscriptionId"))?,
186        max_interval: max_interval.ok_or(ImError::MissingField("SubscribeResponse.maxInterval"))?,
187    })
188}
189
190/// Build a `StatusResponseMessage` with the given `status` code.
191///
192/// Used to acknowledge a `ReportData` during a subscription. `status = 0`
193/// is the success ack. `interactionModelRevision` is always [`IM_REVISION`].
194///
195/// Encodes to the wire format that byte-matches matter.js
196/// `TlvStatusResponse.encode({ status, interactionModelRevision: 11 })`.
197#[must_use]
198#[allow(clippy::expect_used, clippy::missing_panics_doc)] // Vec-backed TlvWriter is infallible.
199pub fn build_status_response(status: u8) -> Vec<u8> {
200    let mut buf = Vec::with_capacity(16);
201    let mut w = TlvWriter::new(&mut buf);
202
203    w.start_structure(Tag::Anonymous)
204        .expect("infallible: vec writer");
205    w.put_uint(Tag::Context(0), u64::from(status))
206        .expect("infallible: vec writer");
207    w.put_uint(Tag::Context(0xFF), u64::from(IM_REVISION))
208        .expect("infallible: vec writer");
209    w.end_container().expect("infallible: vec writer");
210    buf
211}
212
213#[cfg(test)]
214mod tests {
215    #![allow(clippy::unwrap_used)]
216    use super::*;
217    use matter_codec::ContainerKind;
218
219    #[test]
220    fn status_response_success_has_expected_structure() {
221        let bytes = build_status_response(0);
222        let mut r = TlvReader::new(&bytes);
223        // anon struct
224        assert!(matches!(
225            r.next().unwrap(),
226            Some(Element::ContainerStart {
227                tag: Tag::Anonymous,
228                kind: ContainerKind::Structure
229            })
230        ));
231        // ctx[0] = uint 0
232        assert!(matches!(
233            r.next().unwrap(),
234            Some(Element::Scalar {
235                tag: Tag::Context(0),
236                value: matter_codec::Value::Uint(0)
237            })
238        ));
239        // ctx[255] = uint 11
240        assert!(matches!(
241            r.next().unwrap(),
242            Some(Element::Scalar {
243                tag: Tag::Context(0xFF),
244                value: matter_codec::Value::Uint(11)
245            })
246        ));
247    }
248
249    #[test]
250    fn subscribe_request_has_expected_structure() {
251        let req = SubscribeRequest {
252            keep_subscriptions: false,
253            min_interval_floor: 1,
254            max_interval_ceiling: 30,
255            paths: vec![ReadPath::concrete(1, 0x06, 0x0000)],
256            event_paths: vec![],
257            event_filters: vec![],
258        };
259        let bytes = build_subscribe_request(&req);
260        let mut r = TlvReader::new(&bytes);
261        // anon struct
262        assert!(matches!(
263            r.next().unwrap(),
264            Some(Element::ContainerStart {
265                tag: Tag::Anonymous,
266                kind: ContainerKind::Structure
267            })
268        ));
269        // ctx[0] = keepSubscriptions = false
270        assert!(matches!(
271            r.next().unwrap(),
272            Some(Element::Scalar {
273                tag: Tag::Context(0),
274                value: matter_codec::Value::Bool(false)
275            })
276        ));
277        // ctx[1] = minIntervalFloor = 1
278        assert!(matches!(
279            r.next().unwrap(),
280            Some(Element::Scalar {
281                tag: Tag::Context(1),
282                value: matter_codec::Value::Uint(1)
283            })
284        ));
285        // ctx[2] = maxIntervalCeiling = 30
286        assert!(matches!(
287            r.next().unwrap(),
288            Some(Element::Scalar {
289                tag: Tag::Context(2),
290                value: matter_codec::Value::Uint(30)
291            })
292        ));
293        // ctx[3] = array of paths
294        assert!(matches!(
295            r.next().unwrap(),
296            Some(Element::ContainerStart {
297                tag: Tag::Context(3),
298                kind: ContainerKind::Array
299            })
300        ));
301    }
302
303    #[test]
304    fn parse_subscribe_response_roundtrip() {
305        // Hand-encode a SubscribeResponse and parse it back.
306        let mut buf = Vec::new();
307        let mut w = TlvWriter::new(&mut buf);
308        w.start_structure(Tag::Anonymous).unwrap();
309        w.put_uint(Tag::Context(0), 0x1234_5678_u64).unwrap(); // subscriptionId
310        w.put_uint(Tag::Context(2), 30_u64).unwrap(); // maxInterval
311        w.put_uint(Tag::Context(0xFF), 11_u64).unwrap(); // revision
312        w.end_container().unwrap();
313
314        let result = parse_subscribe_response(&buf).unwrap();
315        assert_eq!(result.subscription_id, 0x1234_5678);
316        assert_eq!(result.max_interval, 30);
317    }
318}