Skip to main content

rs_matter/im/encoding/
invoke.rs

1/*
2 *
3 *    Copyright (c) 2025-2026 Project CHIP Authors
4 *
5 *    Licensed under the Apache License, Version 2.0 (the "License");
6 *    you may not use this file except in compliance with the License.
7 *    You may obtain a copy of the License at
8 *
9 *        http://www.apache.org/licenses/LICENSE-2.0
10 *
11 *    Unless required by applicable law or agreed to in writing, software
12 *    distributed under the License is distributed on an "AS IS" BASIS,
13 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *    See the License for the specific language governing permissions and
15 *    limitations under the License.
16 */
17
18//! This module contains types related to command invocations in the Interaction Model.
19
20use core::fmt;
21
22use crate::error::{Error, ErrorCode};
23use crate::tlv::{FromTLV, TLVArray, TLVElement, ToTLV};
24
25use super::{ClusterId, CmdId, EndptId, GenericPath, IMStatusCode, Status};
26
27/// A path to a command in the Interaction Model.
28///
29/// Corresponds to the `CommandPathIB` block in the Matter Core spec.
30#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
31#[tlvargs(datatype = "list")]
32#[cfg_attr(feature = "defmt", derive(defmt::Format))]
33pub struct CmdPath {
34    /// The endpoint ID, if specified, otherwise `None` for wildcard
35    pub endpoint: Option<EndptId>,
36    /// The cluster ID, if specified, otherwise `None` for wildcard
37    pub cluster: Option<ClusterId>,
38    /// The command ID, if specified, otherwise `None` for wildcard
39    pub cmd: Option<CmdId>,
40}
41
42/// Tags corresponding to the fields in the `CommandPathIB` TLV
43/// structure (Matter Core spec). `CmdPath` is encoded as a
44/// TLV *list* with positional context tags 0..2. Used by callers that
45/// need to perform low-level TLV serde on `CmdPath` data.
46#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
47#[cfg_attr(feature = "defmt", derive(defmt::Format))]
48#[repr(u8)]
49pub enum CmdPathTag {
50    Endpoint = 0,
51    Cluster = 1,
52    Command = 2,
53}
54
55impl CmdPath {
56    /// Create a new instance from the given IDs.
57    pub const fn new(
58        endpoint: Option<EndptId>,
59        cluster: Option<ClusterId>,
60        cmd: Option<CmdId>,
61    ) -> Self {
62        Self {
63            endpoint,
64            cluster,
65            cmd,
66        }
67    }
68
69    /// Create a new instance from the given `GenericPath`.
70    pub const fn from_gp(path: &GenericPath) -> Self {
71        Self {
72            endpoint: path.endpoint,
73            cluster: path.cluster,
74            cmd: path.leaf,
75        }
76    }
77
78    /// Convert this command path to a `GenericPath`.
79    pub const fn to_gp(&self) -> GenericPath {
80        GenericPath::new(self.endpoint, self.cluster, self.cmd)
81    }
82
83    /// Return true, if the path is wildcard
84    pub const fn is_wildcard(&self) -> bool {
85        self.endpoint.is_none() || self.cluster.is_none() || self.cmd.is_none()
86    }
87}
88
89/// Status of a command invocation.
90///
91/// Returned when a command invocation does not have a specific generated-command
92/// response.
93///
94/// Corresponds to the `CommandStatusIB` block in the Matter Core spec.
95#[derive(Debug, Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
96#[cfg_attr(feature = "defmt", derive(defmt::Format))]
97pub struct CmdStatus {
98    /// The command path associated with this status.
99    pub path: CmdPath,
100    /// The status of the command invocation.
101    pub status: Status,
102    /// The CommandRef echoed from the corresponding `CommandDataIB`.
103    /// Required when the request was part of a batched (multi-path) invoke.
104    pub command_ref: Option<u16>,
105}
106
107impl CmdStatus {
108    /// Create a new command status with the given path, status code, optional cluster status,
109    /// and optional CommandRef (echoed from the request when batched).
110    pub const fn new(
111        path: CmdPath,
112        status: IMStatusCode,
113        cluster_status: Option<u16>,
114        command_ref: Option<u16>,
115    ) -> Self {
116        Self {
117            path,
118            status: Status {
119                status,
120                cluster_status,
121            },
122            command_ref,
123        }
124    }
125}
126
127/// Data associated with a command invocation.
128///
129/// Corresponds to the `CommandDataIB` struct in the Matter Core spec.
130#[derive(Debug, Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
131#[cfg_attr(feature = "defmt", derive(defmt::Format))]
132#[tlvargs(lifetime = "'a")]
133pub struct CmdData<'a> {
134    pub path: CmdPath,
135    pub data: TLVElement<'a>,
136    /// CommandRef set by the requester to correlate batched invokes with their responses.
137    /// Mandatory when the `InvokeRequestMessage` carries more than one `CommandDataIB`.
138    pub command_ref: Option<u16>,
139}
140
141impl<'a> CmdData<'a> {
142    /// Create a new command data instance with the specified path, data, and optional CommandRef.
143    pub const fn new(path: CmdPath, data: TLVElement<'a>, command_ref: Option<u16>) -> Self {
144        Self {
145            path,
146            data,
147            command_ref,
148        }
149    }
150}
151
152/// Tags corresponding to the fields in the `CmdData` struct.
153///
154/// Used when there is a need to perform low-level TLV serde on
155/// `CmdData` data.
156pub enum CmdDataTag {
157    Path = 0,
158    Data = 1,
159    CommandRef = 2,
160}
161
162/// Response to a command invocation.
163///
164/// Corresponds to the `InvokeResponseIB` struct in the Matter Core spec.
165#[derive(Clone, FromTLV, ToTLV, Debug)]
166#[cfg_attr(feature = "defmt", derive(defmt::Format))]
167#[tlvargs(lifetime = "'a")]
168pub enum CmdResp<'a> {
169    Cmd(CmdData<'a>),
170    Status(CmdStatus),
171}
172
173impl CmdResp<'_> {
174    /// Create the `Status` variant of a command response
175    /// with the given command path, status code, optional cluster status, and optional CommandRef.
176    pub const fn status_new(
177        cmd_path: CmdPath,
178        status: IMStatusCode,
179        cluster_status: Option<u16>,
180        command_ref: Option<u16>,
181    ) -> Self {
182        Self::Status(CmdStatus {
183            path: cmd_path,
184            status: Status::new(status, cluster_status),
185            command_ref,
186        })
187    }
188}
189
190impl<'a> From<CmdData<'a>> for CmdResp<'a> {
191    fn from(value: CmdData<'a>) -> Self {
192        Self::Cmd(value)
193    }
194}
195
196/// Tags corresponding to the fields in the `CmdResp` enum.
197///
198/// Used when there is a need to perform low-level TLV serde on
199/// `CmdResp` data.
200#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
201#[cfg_attr(feature = "defmt", derive(defmt::Format))]
202#[repr(u8)]
203pub enum CmdRespTag {
204    Cmd = 0,
205    Status = 1,
206}
207
208impl From<CmdStatus> for CmdResp<'_> {
209    fn from(value: CmdStatus) -> Self {
210        Self::Status(value)
211    }
212}
213
214/// A request to invoke commands in the Interaction Model.
215///
216/// Corresponds to the `InvokeRequestMessage` struct in the Matter Core spec.
217#[derive(Clone, PartialEq, Eq, Hash, FromTLV, ToTLV)]
218#[tlvargs(lifetime = "'a")]
219pub struct InvReq<'a>(TLVElement<'a>);
220
221impl<'a> InvReq<'a> {
222    /// Create a new `InvReq` instance from the given TLV element.
223    pub const fn new(element: TLVElement<'a>) -> Self {
224        Self(element)
225    }
226
227    /// Return `true` if the request indicates that the response should be suppressed.
228    pub fn suppress_response(&self) -> Result<bool, Error> {
229        self.0
230            .r#struct()?
231            .find_ctx(0)?
232            .non_empty()
233            .map(|t| t.bool())
234            .unwrap_or(Ok(false))
235    }
236
237    /// Return `true` if the request indicates that it is a timed request.
238    pub fn timed_request(&self) -> Result<bool, Error> {
239        self.0
240            .r#struct()?
241            .find_ctx(1)?
242            .non_empty()
243            .map(|t| t.bool())
244            .unwrap_or(Ok(false))
245    }
246
247    /// Return the invocation requests contained in this request.
248    pub fn inv_requests(&self) -> Result<Option<TLVArray<'a, CmdData<'a>>>, Error> {
249        Option::from_tlv(&self.0.r#struct()?.find_ctx(2)?)
250    }
251}
252
253impl fmt::Debug for InvReq<'_> {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        f.debug_struct("InvReqRef")
256            .field("suppress_response", &self.suppress_response())
257            .field("timed_request", &self.timed_request())
258            .field("inv_requests", &self.inv_requests())
259            .finish()
260    }
261}
262
263#[cfg(feature = "defmt")]
264impl defmt::Format for InvReq<'_> {
265    fn format(&self, f: defmt::Formatter<'_>) {
266        defmt::write!(f,
267            "InvReqRef {{\n  suppress_response: {:?},\n  timed_request: {:?},\n  inv_requests: {:?},\n}}",
268            self.suppress_response(),
269            self.timed_request(),
270            self.inv_requests(),
271        )
272    }
273}
274
275/// Tags corresponding to the fields in the `InvReq` struct.
276///
277/// Used when there is a need to perform low-level TLV serde on
278/// `InvReq` data.
279#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
280#[cfg_attr(feature = "defmt", derive(defmt::Format))]
281#[repr(u8)]
282pub enum InvReqTag {
283    SupressResponse = 0,
284    TimedReq = 1,
285    InvokeRequests = 2,
286}
287
288/// Tags corresponding to the fields in the `InvokeResponseMessage`
289/// IM struct.
290///
291/// Used when there is a need to perform low-level TLV serde on
292/// `InvokeResponseMessage` data.
293#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
294#[cfg_attr(feature = "defmt", derive(defmt::Format))]
295#[repr(u8)]
296pub enum InvRespTag {
297    SupressResponse = 0,
298    InvokeResponses = 1,
299}
300
301/// A response to an invoke request in the Interaction Model.
302///
303/// Corresponds to the `InvokeResponseMessage` TLV structure in the Interaction Model.
304/// Used by clients to parse invoke responses from devices.
305#[derive(Debug, Clone, FromTLV, ToTLV)]
306#[cfg_attr(feature = "defmt", derive(defmt::Format))]
307#[tlvargs(lifetime = "'a")]
308pub struct InvokeResp<'a> {
309    /// Whether the response should be suppressed (echo from request)
310    pub suppress_response: Option<bool>,
311    /// The list of invoke responses
312    pub invoke_responses: Option<TLVArray<'a, CmdResp<'a>>>,
313    /// Whether there are more chunked messages coming
314    pub more_chunks: Option<bool>,
315    /// `interactionModelRevision` (TLV context tag `0xFF`). Mandatory in
316    /// every IM message we send; modelled as `Option<u8>` so we tolerate
317    /// peers that omit it (the C++ SDK is tolerant in practice).
318    #[tagval(crate::im::encoding::IM_REVISION_TAG)]
319    pub interaction_model_revision: Option<u8>,
320}
321
322impl<'a> InvokeResp<'a> {
323    /// Iterate the entries in `invoke_responses` whose path matches
324    /// the given `(cluster, cmd)` pair, in `(endpoint, result)` form.
325    ///
326    /// - **`Ok(R)`** — `CmdResp::Cmd` entry; the embedded `data` is
327    ///   decoded via `FromTLV` into `R`.
328    /// - **`Err(_)`** — `CmdResp::Status` entry; the `IMStatusCode` is
329    ///   converted to an [`Error`]. This covers access-check failures
330    ///   (`UnsupportedAccess` etc.) and all `Unsupported*` cases
331    ///   (`UnsupportedEndpoint`, `UnsupportedCluster`, `UnsupportedCommand`)
332    ///   uniformly — the peer echoes the requested path on status, so
333    ///   the filter still catches them.
334    /// - Entries with a non-matching cluster/cmd are silently filtered
335    ///   out (they belong to a *different* `.responses(...)` call).
336    /// - Entries with an absent endpoint in the path are skipped
337    ///   (the wire spec requires concrete paths on invoke responses;
338    ///   a missing endpoint indicates a malformed response).
339    ///
340    /// `R = ()` for `DefaultSuccess` commands. Codegen-emitted
341    /// response structs (e.g. `MoveToLevelResponse<'a>`) implement
342    /// `FromTLV` over `'a` and plug in directly.
343    ///
344    /// Multi-response: single-command invokes per Matter Core spec
345    /// carry concrete paths only, but batched invokes
346    /// (multiple `CommandDataIB`s in one `InvokeRequestMessage`) can
347    /// produce multiple matching entries — the iterator yields one
348    /// per match, in wire order.
349    pub fn responses<R>(
350        &self,
351        cluster: ClusterId,
352        cmd: CmdId,
353    ) -> impl Iterator<Item = (EndptId, Result<R, Error>)> + use<'_, 'a, R>
354    where
355        R: FromTLV<'a> + 'a,
356    {
357        self.invoke_responses
358            .as_ref()
359            .into_iter()
360            .flat_map(|arr| arr.iter())
361            .filter_map(move |resp| filter_cmd_resp::<R>(resp.ok()?, cluster, cmd))
362    }
363
364    /// Counterpart of [`Self::responses`] for `DefaultSuccess`
365    /// commands — the ones whose IDL `output` is `DefaultSuccess` and
366    /// thus carry no per-command response payload. Filters the
367    /// `invoke_responses` list by `(cluster, cmd)` and yields
368    /// `(endpoint, Result<(), Error>)`:
369    ///
370    /// - **`Ok(())`** — a `CmdResp::Status(Success)` entry for the
371    ///   given path (this is what a batched DefaultSuccess command
372    ///   produces on the wire).
373    /// - **`Err(_)`** — a non-`Success` `CmdResp::Status`, with the
374    ///   same `IMStatusCode`-to-[`Error`] mapping as
375    ///   [`Self::responses`].
376    /// - `CmdResp::Cmd` entries (which would indicate the peer
377    ///   replied with payload data for a command we asked to be
378    ///   DefaultSuccess) are skipped silently.
379    /// - Entries with non-matching cluster/cmd or absent endpoint
380    ///   are skipped as in [`Self::responses`].
381    ///
382    /// Note: a *single-command* DefaultSuccess invoke produces a
383    /// top-level `StatusResponse(Success)` instead of an
384    /// `InvokeResponseMessage`, so the response array is absent
385    /// entirely and this iterator yields nothing — use
386    /// [`crate::im::client::InvokeRespChunk::is_status_only`] to
387    /// detect that case. The iterator here is only useful for
388    /// *batched* invokes that mix DefaultSuccess and response-bearing
389    /// commands.
390    pub fn statuses(
391        &self,
392        cluster: ClusterId,
393        cmd: CmdId,
394    ) -> impl Iterator<Item = (EndptId, Result<(), Error>)> + '_ {
395        self.invoke_responses
396            .as_ref()
397            .into_iter()
398            .flat_map(|arr| arr.iter())
399            .filter_map(move |resp| match resp.ok()? {
400                CmdResp::Status(s) => {
401                    if s.path.cluster != Some(cluster) || s.path.cmd != Some(cmd) {
402                        return None;
403                    }
404                    let endpoint = s.path.endpoint?;
405                    let result = if s.status.status == IMStatusCode::Success {
406                        Ok(())
407                    } else {
408                        let err: Error = s
409                            .status
410                            .status
411                            .to_error_code()
412                            .unwrap_or(ErrorCode::Failure)
413                            .into();
414                        Err(err)
415                    };
416                    Some((endpoint, result))
417                }
418                CmdResp::Cmd(_) => None,
419            })
420    }
421}
422
423/// Helper for [`InvokeResp::responses`] — extracts `(endpoint,
424/// Result<R, Error>)` from a single `CmdResp` if it matches the
425/// requested `(cluster, cmd)` filter.
426fn filter_cmd_resp<'a, R>(
427    resp: CmdResp<'a>,
428    cluster: ClusterId,
429    cmd: CmdId,
430) -> Option<(EndptId, Result<R, Error>)>
431where
432    R: FromTLV<'a>,
433{
434    match resp {
435        CmdResp::Cmd(data) => {
436            if data.path.cluster != Some(cluster) || data.path.cmd != Some(cmd) {
437                return None;
438            }
439            let endpoint = data.path.endpoint?;
440            Some((endpoint, R::from_tlv(&data.data)))
441        }
442        CmdResp::Status(s) => {
443            if s.path.cluster != Some(cluster) || s.path.cmd != Some(cmd) {
444                return None;
445            }
446            let endpoint = s.path.endpoint?;
447            let err: Error = s
448                .status
449                .status
450                .to_error_code()
451                .unwrap_or(ErrorCode::Failure)
452                .into();
453            Some((endpoint, Err(err)))
454        }
455    }
456}