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}