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}