Skip to main content

rs_matter/im/encoding/
invoke_builder.rs

1/*
2 *
3 *    Copyright (c) 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//! Streaming TLV builders for `InvokeRequestMessage` and its
19//! sub-structures.
20//!
21//! This is the analog of [`crate::im::encoding::attr::write_builder`] for
22//! command invokes — and the genuine MCU win for client clusters: a
23//! switch wanting to send `OnOff::Toggle` to a bound bulb constructs
24//! the command-request payload directly into the TX `WriteBuf` via
25//! [`CmdDataBuilder::data`], no sibling buffer needed.
26//!
27//! # Layout
28//!
29//! Per Matter Core spec `InvokeRequestMessage` is an
30//! anonymous-tagged struct with three fields:
31//!
32//! | Tag | Field            | Type           | Required |
33//! |-----|------------------|----------------|----------|
34//! | 0   | SuppressResponse | bool           | **yes**  |
35//! | 1   | TimedRequest     | bool           | **yes**  |
36//! | 2   | InvokeRequests   | array[CmdData] | **yes**  |
37//!
38//! All three are mandatory on the wire per the spec — and Matter 1.5
39//! strictly-validating peers like SmartThings reject requests with
40//! either bool field absent — so the builder always emits all three.
41//! At the API level, however, the two booleans are *optional*: the
42//! caller may skip `suppress_response()` and/or `timed_request()` and
43//! the builder will fill them in with their default value (`false`)
44//! automatically. The default matches what every normal client
45//! wants — receive a response, no timed-request handshake — so the
46//! common-case ceremony collapses to just `invoke_requests()?`.
47//!
48//! `CmdData` (`CommandDataIB`) is a struct with:
49//!
50//! | Tag | Field      | Type        | Required |
51//! |-----|------------|-------------|----------|
52//! | 0   | Path       | CmdPath     | yes      |
53//! | 1   | Data       | any TLV     | yes      |
54//! | 2   | CommandRef | u16         | conditional (mandatory when invoke is batched) |
55//!
56//! `CmdPath` (`CommandPathIB`) is a TLV *list* with optional fields
57//! at tags 0,1,2 (endpoint, cluster, cmd). For client-cluster sends
58//! the path is always concrete `(endpoint, cluster, cmd)`; the
59//! builder requires all three.
60//!
61//! # Usage
62//!
63//! ```ignore
64//! exchange.send_with(|_, wb| {
65//!     let parent = TLVWriteParent::new("InvokeRequest", wb);
66//!     // `suppress_response` and `timed_request` are skipped here —
67//!     // the builder fills them in as `false` on the wire.
68//!     InvReqBuilder::new(parent)?
69//!         .invoke_requests()?
70//!             .push()?
71//!                 .path(1, 0x0006 /* OnOff */, 0x02 /* Toggle */)?
72//!                 .data(|w| {
73//!                     // Toggle's request body is empty:
74//!                     w.start_struct(&TLVTag::Context(CmdDataTag::Data as u8))?;
75//!                     w.end_container()
76//!                 })?
77//!             .end()?
78//!         .end()?
79//!         .end()?;
80//!     Ok(Some(OpCode::InvokeRequest.into()))
81//! }).await
82//! ```
83
84use core::marker::PhantomData;
85
86use crate::error::Error;
87use crate::im::encoding::{ClusterId, CmdId, EndptId};
88use crate::im::{CmdDataTag, CmdPathTag, InvReqTag, IM_REVISION};
89use crate::tlv::{TLVBuilder, TLVBuilderParent, TLVTag, TLVWrite, ToTLV};
90
91/// Streaming builder for an `InvokeRequestMessage`. Type-state-tagged
92/// so the compiler enforces in-order field writes.
93///
94/// All three top-level fields (`SuppressResponse`, `TimedRequest`,
95/// `InvokeRequests`) are mandatory on the wire per Matter Core spec,
96/// and the builder always emits all three. The two booleans
97/// are *optional at the API level* though — skipping either setter
98/// causes the builder to write the field with its default value
99/// (`false`) before opening the next state. This matches the
100/// `write_builder` ergonomics and keeps the common-case ceremony to
101/// `invoke_requests()?` only. The CmdData entries inside the array
102/// have an optional `CommandRef`, also implicitly skippable.
103///
104/// Field-state values:
105/// - `0`: nothing written yet
106/// - `1`: past `SuppressResponse`
107/// - `2`: past `TimedRequest`
108/// - `3`: past `InvokeRequests` array
109/// - `4`: past `InteractionModelRevision` (auto-injected at default
110///   value [`IM_REVISION`] by `end()` if the optional setter wasn't
111///   called)
112pub struct InvReqBuilder<P, const F: usize = 0> {
113    p: P,
114}
115
116impl<P> InvReqBuilder<P, 0>
117where
118    P: TLVBuilderParent,
119{
120    /// Begin a new `InvokeRequestMessage` — opens a struct at the
121    /// given tag. For top-level use (the usual case) pass
122    /// `&TLVTag::Anonymous`.
123    pub fn new(mut p: P, tag: &TLVTag) -> Result<Self, Error> {
124        p.writer().start_struct(tag)?;
125        Ok(Self { p })
126    }
127}
128
129impl<P> TLVBuilder<P> for InvReqBuilder<P, 0>
130where
131    P: TLVBuilderParent,
132{
133    fn new(parent: P, tag: &TLVTag) -> Result<Self, Error> {
134        Self::new(parent, tag)
135    }
136
137    fn unchecked_into_parent(self) -> P {
138        self.p
139    }
140}
141
142// ---------------------------------------------------------------------
143// `suppress_response` — optional in the *API* but mandatory on the
144// *wire*. Settable from state 0 (advances to state 1). Skipping —
145// going straight to `timed_request` or `invoke_requests` — causes the
146// builder to emit the field with its default value (`false`)
147// automatically, so strictly-validating peers (e.g. Matter 1.5
148// SmartThings) still see all three top-level fields present.
149// ---------------------------------------------------------------------
150impl<P> InvReqBuilder<P, 0>
151where
152    P: TLVBuilderParent,
153{
154    /// Write the `SuppressResponse` field. Omitting this call is
155    /// equivalent to `suppress_response(false)` (i.e. the typical
156    /// case — most clients want a response); the builder emits the
157    /// default automatically when `timed_request` or
158    /// `invoke_requests` is called from state 0. Set to `true` only
159    /// for fire-and-forget commands.
160    pub fn suppress_response(mut self, value: bool) -> Result<InvReqBuilder<P, 1>, Error> {
161        self.p
162            .writer()
163            .bool(&TLVTag::Context(InvReqTag::SupressResponse as u8), value)?;
164        Ok(InvReqBuilder { p: self.p })
165    }
166}
167
168// ---------------------------------------------------------------------
169// `timed_request` — same shape: optional in the API (defaults to
170// `false`), mandatory on the wire. Settable from state 0 or 1.
171// ---------------------------------------------------------------------
172impl<P> InvReqBuilder<P, 0>
173where
174    P: TLVBuilderParent,
175{
176    /// Write the `TimedRequest` field, implicitly emitting
177    /// `SuppressResponse(false)` first (the common-case default).
178    pub fn timed_request(self, value: bool) -> Result<InvReqBuilder<P, 2>, Error> {
179        self.suppress_response(false)?.timed_request(value)
180    }
181}
182
183impl<P> InvReqBuilder<P, 1>
184where
185    P: TLVBuilderParent,
186{
187    /// Write the `TimedRequest` field. Omitting this call is
188    /// equivalent to `timed_request(false)`; the builder emits the
189    /// default automatically when `invoke_requests` is called from
190    /// state 1. Set to `true` only when the surrounding flow sent a
191    /// `TimedRequest` IM message first (some commands like ACL
192    /// writes require this).
193    pub fn timed_request(mut self, value: bool) -> Result<InvReqBuilder<P, 2>, Error> {
194        self.p
195            .writer()
196            .bool(&TLVTag::Context(InvReqTag::TimedReq as u8), value)?;
197        Ok(InvReqBuilder { p: self.p })
198    }
199}
200
201// ---------------------------------------------------------------------
202// `invoke_requests` — required; openable from state 0, 1, or 2.
203// Calling from 0 or 1 fills in the missing default fields first so
204// the wire layout always contains all three top-level fields.
205// ---------------------------------------------------------------------
206impl<P> InvReqBuilder<P, 0>
207where
208    P: TLVBuilderParent,
209{
210    /// Open the `InvokeRequests` array, implicitly emitting
211    /// `SuppressResponse(false)` and `TimedRequest(false)` first.
212    pub fn invoke_requests(self) -> Result<CmdDataArrayBuilder<InvReqBuilder<P, 3>>, Error> {
213        self.suppress_response(false)?.invoke_requests()
214    }
215}
216
217impl<P> InvReqBuilder<P, 1>
218where
219    P: TLVBuilderParent,
220{
221    /// Open the `InvokeRequests` array, implicitly emitting
222    /// `TimedRequest(false)` first.
223    pub fn invoke_requests(self) -> Result<CmdDataArrayBuilder<InvReqBuilder<P, 3>>, Error> {
224        self.timed_request(false)?.invoke_requests()
225    }
226}
227
228impl<P> InvReqBuilder<P, 2>
229where
230    P: TLVBuilderParent,
231{
232    /// Open the `InvokeRequests` array. Each `.push()` starts one
233    /// [`CmdDataBuilder`]; close with `.end()` to return to the
234    /// message builder.
235    pub fn invoke_requests(self) -> Result<CmdDataArrayBuilder<InvReqBuilder<P, 3>>, Error> {
236        CmdDataArrayBuilder::new(
237            InvReqBuilder { p: self.p },
238            &TLVTag::Context(InvReqTag::InvokeRequests as u8),
239        )
240    }
241}
242
243impl<P> InvReqBuilder<P, 3>
244where
245    P: TLVBuilderParent,
246{
247    /// Write the mandatory-on-the-wire `InteractionModelRevision`
248    /// field (Matter Core: value is `13` since Matter
249    /// 1.3, unchanged in 1.4 and 1.5). Optional at the API level —
250    /// omit and `end()` injects [`IM_REVISION`] automatically.
251    pub fn interaction_model_revision(mut self, value: u8) -> Result<InvReqBuilder<P, 4>, Error> {
252        self.p.writer().u8(
253            &TLVTag::Context(crate::im::encoding::IM_REVISION_TAG),
254            value,
255        )?;
256        Ok(InvReqBuilder { p: self.p })
257    }
258
259    /// Close the message struct, auto-injecting
260    /// `InteractionModelRevision` at its default value
261    /// [`IM_REVISION`]. Returns the parent.
262    pub fn end(self) -> Result<P, Error> {
263        self.interaction_model_revision(IM_REVISION)?.end()
264    }
265}
266
267impl<P> InvReqBuilder<P, 4>
268where
269    P: TLVBuilderParent,
270{
271    /// Close the message struct and return the parent.
272    pub fn end(mut self) -> Result<P, Error> {
273        self.p.writer().end_container()?;
274        Ok(self.p)
275    }
276}
277
278impl<P, const F: usize> TLVBuilderParent for InvReqBuilder<P, F>
279where
280    P: TLVBuilderParent,
281{
282    type Write = P::Write;
283
284    fn writer(&mut self) -> &mut Self::Write {
285        self.p.writer()
286    }
287}
288
289impl<P, const F: usize> core::fmt::Debug for InvReqBuilder<P, F>
290where
291    P: core::fmt::Debug,
292{
293    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
294        write!(f, "{:?}::InvokeRequestMessage<{}>", self.p, F)
295    }
296}
297
298#[cfg(feature = "defmt")]
299impl<P, const F: usize> defmt::Format for InvReqBuilder<P, F>
300where
301    P: defmt::Format,
302{
303    fn format(&self, fmt: defmt::Formatter<'_>) {
304        defmt::write!(fmt, "{:?}::InvokeRequestMessage<{}>", self.p, F);
305    }
306}
307
308// =====================================================================
309// CmdData array sub-builder
310// =====================================================================
311
312/// Array builder for the `InvokeRequests` field. Opened by
313/// [`InvReqBuilder::invoke_requests`]; close with
314/// `.end()` to return to the message builder.
315pub struct CmdDataArrayBuilder<P> {
316    p: P,
317}
318
319impl<P> CmdDataArrayBuilder<P>
320where
321    P: TLVBuilderParent,
322{
323    /// Begin a new `CmdData` array — opens an array at the given tag.
324    pub fn new(mut p: P, tag: &TLVTag) -> Result<Self, Error> {
325        p.writer().start_array(tag)?;
326        Ok(Self { p })
327    }
328
329    /// Start a new `CmdData` entry. The returned [`CmdDataBuilder`]
330    /// terminates with `.end()` which returns this array builder.
331    pub fn push(self) -> Result<CmdDataBuilder<Self, 0>, Error> {
332        CmdDataBuilder::new(self, &TLVTag::Anonymous)
333    }
334
335    /// Close the array and return the message builder.
336    pub fn end(mut self) -> Result<P, Error> {
337        self.p.writer().end_container()?;
338        Ok(self.p)
339    }
340}
341
342impl<P> TLVBuilder<P> for CmdDataArrayBuilder<P>
343where
344    P: TLVBuilderParent,
345{
346    fn new(parent: P, tag: &TLVTag) -> Result<Self, Error> {
347        Self::new(parent, tag)
348    }
349
350    fn unchecked_into_parent(self) -> P {
351        self.p
352    }
353}
354
355impl<P> TLVBuilderParent for CmdDataArrayBuilder<P>
356where
357    P: TLVBuilderParent,
358{
359    type Write = P::Write;
360
361    fn writer(&mut self) -> &mut Self::Write {
362        self.p.writer()
363    }
364}
365
366impl<P> core::fmt::Debug for CmdDataArrayBuilder<P>
367where
368    P: core::fmt::Debug,
369{
370    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
371        write!(f, "{:?}[]", self.p)
372    }
373}
374
375#[cfg(feature = "defmt")]
376impl<P> defmt::Format for CmdDataArrayBuilder<P>
377where
378    P: defmt::Format,
379{
380    fn format(&self, fmt: defmt::Formatter<'_>) {
381        defmt::write!(fmt, "{:?}[]", self.p);
382    }
383}
384
385// =====================================================================
386// CmdData entry builder
387// =====================================================================
388
389/// Streaming builder for one `CmdData` entry inside the
390/// `InvokeRequests` array.
391///
392/// Field-state values:
393/// - `0`: nothing written yet
394/// - `1`: past `Path`
395/// - `2`: past `Data` (struct can be closed)
396/// - `3`: past `CommandRef`
397pub struct CmdDataBuilder<P, const F: usize = 0> {
398    p: P,
399    _f: PhantomData<[(); F]>,
400}
401
402impl<P> CmdDataBuilder<P, 0>
403where
404    P: TLVBuilderParent,
405{
406    /// Begin a new `CmdData` entry — opens a struct at the given tag.
407    /// Use `&TLVTag::Anonymous` when pushed into an `InvokeRequests`
408    /// array (the typical case).
409    pub fn new(mut p: P, tag: &TLVTag) -> Result<Self, Error> {
410        p.writer().start_struct(tag)?;
411        Ok(Self { p, _f: PhantomData })
412    }
413}
414
415impl<P> TLVBuilder<P> for CmdDataBuilder<P, 0>
416where
417    P: TLVBuilderParent,
418{
419    fn new(parent: P, tag: &TLVTag) -> Result<Self, Error> {
420        Self::new(parent, tag)
421    }
422
423    fn unchecked_into_parent(self) -> P {
424        self.p
425    }
426}
427
428// ---- path ------------------------------------------------------------
429impl<P> CmdDataBuilder<P, 0>
430where
431    P: TLVBuilderParent,
432{
433    /// Write the concrete `(endpoint, cluster, command)` path.
434    /// Wildcards aren't meaningful for invokes — every send targets a
435    /// concrete `(endpoint, cluster, command)` triple. The path is
436    /// encoded as a *list* per spec `CommandPathIB`.
437    pub fn path(
438        mut self,
439        endpoint: EndptId,
440        cluster: ClusterId,
441        cmd: CmdId,
442    ) -> Result<CmdDataBuilder<P, 1>, Error> {
443        let w = self.p.writer();
444        w.start_list(&TLVTag::Context(CmdDataTag::Path as u8))?;
445        w.u16(&TLVTag::Context(CmdPathTag::Endpoint as u8), endpoint)?;
446        w.u32(&TLVTag::Context(CmdPathTag::Cluster as u8), cluster)?;
447        w.u32(&TLVTag::Context(CmdPathTag::Command as u8), cmd)?;
448        w.end_container()?;
449        Ok(CmdDataBuilder {
450            p: self.p,
451            _f: PhantomData,
452        })
453    }
454
455    /// Write the path from an existing [`crate::im::CmdPath`]. Used
456    /// by the snapshot→streaming bridge in `ImClient::invoke`.
457    pub fn path_from(mut self, path: &crate::im::CmdPath) -> Result<CmdDataBuilder<P, 1>, Error> {
458        path.to_tlv(&TLVTag::Context(CmdDataTag::Path as u8), self.p.writer())?;
459        Ok(CmdDataBuilder {
460            p: self.p,
461            _f: PhantomData,
462        })
463    }
464}
465
466// ---- data ------------------------------------------------------------
467impl<P> CmdDataBuilder<P, 1>
468where
469    P: TLVBuilderParent,
470{
471    /// Write the command request body into the `Data` slot.
472    ///
473    /// Per Matter Core spec `CommandDataIB.Data` is the
474    /// command's request payload — any TLV element tagged
475    /// `TLVTag::Context(1)` (= `CmdDataTag::Data`). For commands
476    /// with no request fields (e.g. `OnOff::On`, `OnOff::Toggle`)
477    /// the data slot is still required: write an empty struct.
478    ///
479    /// Idiomatic call patterns:
480    ///
481    /// ```ignore
482    /// // No-payload command (e.g. OnOff::Toggle):
483    /// .data(|w| {
484    ///     w.start_struct(&TLVTag::Context(CmdDataTag::Data as u8))?;
485    ///     w.end_container()
486    /// })?
487    ///
488    /// // Any `T: ToTLV` request body:
489    /// .data(|w| req.to_tlv(&TLVTag::Context(CmdDataTag::Data as u8), w))?
490    ///
491    /// // Via a codegen-emitted typed request builder:
492    /// .data(|w| MoveToHueRequestBuilder::new(w, ...)?
493    ///             .hue(180)? .direction(...)? .end())?
494    /// ```
495    pub fn data<F>(mut self, f: F) -> Result<CmdDataBuilder<P, 2>, Error>
496    where
497        F: FnOnce(&mut P::Write) -> Result<(), Error>,
498    {
499        f(self.p.writer())?;
500        Ok(CmdDataBuilder {
501            p: self.p,
502            _f: PhantomData,
503        })
504    }
505
506    /// Open the `Data` slot as a typed sub-builder.
507    ///
508    /// Closure-free counterpart to [`data`](Self::data) — hand back
509    /// the codegen-emitted request builder for the command, already
510    /// opened at `CmdDataTag::Data`. The caller fills the request
511    /// fields, then calls `.end()` on the sub-builder; that close
512    /// writes `Data`'s closing tag and yields a
513    /// [`CmdDataBuilder<P, 2>`]. The caller then `.end()`s once more
514    /// to close the `CmdData` entry struct itself (the "double-end"
515    /// pattern of the IM-client glue).
516    ///
517    /// Soundness of the phantom typestate advance: `B::new` is
518    /// contractually required (by [`TLVBuilder`]) to open exactly one
519    /// container at the supplied tag, and `B`'s terminal `.end()`
520    /// closes it. So by the time the caller observes the returned
521    /// `CmdDataBuilder<P, 2>`, the `Data` field has been fully written
522    /// and the typestate matches the wire state.
523    pub fn data_builder<B>(self) -> Result<B, Error>
524    where
525        B: TLVBuilder<CmdDataBuilder<P, 2>>,
526    {
527        let advanced = CmdDataBuilder {
528            p: self.p,
529            _f: PhantomData,
530        };
531        B::new(advanced, &TLVTag::Context(CmdDataTag::Data as u8))
532    }
533}
534
535// ---- command_ref -----------------------------------------------------
536impl<P> CmdDataBuilder<P, 2>
537where
538    P: TLVBuilderParent,
539{
540    /// Write the optional `CommandRef` field. **Mandatory** when the
541    /// `InvokeRequests` array carries more than one entry (per spec -
542    /// the server echoes this back so the client can
543    /// correlate responses to requests). For single-command invokes,
544    /// omit (go straight to `.end()`).
545    pub fn command_ref(mut self, value: u16) -> Result<CmdDataBuilder<P, 3>, Error> {
546        self.p
547            .writer()
548            .u16(&TLVTag::Context(CmdDataTag::CommandRef as u8), value)?;
549        Ok(CmdDataBuilder {
550            p: self.p,
551            _f: PhantomData,
552        })
553    }
554}
555
556// ---- end -------------------------------------------------------------
557// Closable from state 2 (CommandRef implicitly skipped) or state 3.
558impl<P> CmdDataBuilder<P, 2>
559where
560    P: TLVBuilderParent,
561{
562    /// Close the `CmdData` struct, implicitly skipping `CommandRef`.
563    /// Returns the array builder so the caller can `.push()` another
564    /// entry or `.end()` the array.
565    pub fn end(self) -> Result<P, Error> {
566        CmdDataBuilder::<P, 3> {
567            p: self.p,
568            _f: PhantomData,
569        }
570        .end()
571    }
572}
573
574impl<P> CmdDataBuilder<P, 3>
575where
576    P: TLVBuilderParent,
577{
578    /// Close the `CmdData` struct and return the array builder.
579    pub fn end(mut self) -> Result<P, Error> {
580        self.p.writer().end_container()?;
581        Ok(self.p)
582    }
583}
584
585impl<P, const F: usize> TLVBuilderParent for CmdDataBuilder<P, F>
586where
587    P: TLVBuilderParent,
588{
589    type Write = P::Write;
590
591    fn writer(&mut self) -> &mut Self::Write {
592        self.p.writer()
593    }
594}
595
596impl<P, const F: usize> core::fmt::Debug for CmdDataBuilder<P, F>
597where
598    P: core::fmt::Debug,
599{
600    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
601        write!(f, "{:?}::CmdData<{}>", self.p, F)
602    }
603}
604
605#[cfg(feature = "defmt")]
606impl<P, const F: usize> defmt::Format for CmdDataBuilder<P, F>
607where
608    P: defmt::Format,
609{
610    fn format(&self, fmt: defmt::Formatter<'_>) {
611        defmt::write!(fmt, "{:?}::CmdData<{}>", self.p, F);
612    }
613}