rs_matter/im/client.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//! Interaction Model Client implementation.
19//!
20//! This module provides client-side functionality for sending IM requests
21//! (Read, Write, Invoke, Subscribe) to Matter devices and processing their
22//! responses.
23//!
24//! Subscribe support covers the *establishment* phase only — the
25//! `SubscribeRequest`, the priming `ReportData` chunks and the
26//! terminal `SubscribeResponse`. Server-initiated post-establishment
27//! reports arrive on new exchanges over the same session and require
28//! a separate listening abstraction layered on top of the transport.
29
30use either::Either;
31
32pub use super::{AttrId, ClusterId, EndptId};
33
34use crate::error::{Error, ErrorCode};
35#[cfg(feature = "groups")]
36use crate::tlv::TLVWriteParent;
37use crate::tlv::{FromTLV, TLVBuilderParent, TLVElement, TLVTag, TLVWrite, TagType, ToTLV};
38use crate::transport::exchange::{Exchange, OwnedSender, OwnedSenderTx};
39#[cfg(feature = "groups")]
40use crate::utils::storage::WriteBuf;
41
42use super::{
43 IMStatusCode, InvReqBuilder, InvokeResp, OpCode, ReadReqBuilder, ReportDataResp, StatusResp,
44 SubscribeReqBuilder, SubscribeResp, TimedReq, WriteReqBuilder, WriteResp, IM_REVISION,
45};
46
47/// IM Client trait — extension over an [`Exchange`] that adds the
48/// Matter Interaction Model client operations.
49///
50/// Implemented for [`Exchange<'a>`]; user code just `use`s this trait
51/// to get method-syntax access on any exchange handle. Two flavours
52/// of method live on this trait:
53///
54/// - `*_sender` — hands the caller a typed `*Sender` they drive
55/// manually. Maximum control, full visibility into the retransmit
56/// loop and chunked response iteration.
57/// - `*_with` / `*_with_async` — takes a build closure that
58/// writes the request straight into the TX buffer; the retransmit
59/// loop is handled internally and the first response chunk is
60/// handed back for the caller to iterate via `complete()`.
61///
62/// On top of these, the codegen-emitted per-cluster
63/// `<ClusterName>Client<'a>` traits add high-level single-shot
64/// methods (`<cluster>_<command>` / `<cluster>_<attr>_read` /
65/// `<cluster>_<attr>_write`) that bake in the cluster/attr/cmd IDs
66/// and the chunk-drain / status-to-error conversion for the common
67/// case.
68///
69/// The trait sits over `Self: Into<Exchange<'a>>` so any type that
70/// converts to an exchange can opt in via a one-line blanket impl;
71/// `Exchange<'a>` itself implements `Into<Exchange<'a>>` for free via
72/// the standard-library identity impl.
73///
74/// # Lifecycle
75///
76/// Every method **consumes** the exchange (`self` by value) — one
77/// exchange is one IM transaction, end of story. After the method
78/// returns, the exchange is closed and the slot is released; callers
79/// wanting to issue another transaction must initiate a fresh
80/// exchange.
81pub trait ImClient<'a>: Sized + Into<Exchange<'a>> {
82 /// Perform an IM read transaction.
83 ///
84 /// # Arguments
85 /// - `build` closure that writes the `ReadRequestMessage` TLV body
86 /// NOTE: The closure is `FnMut` because the MRP layer may retransmit the
87 /// request multiple times; it MUST produce the same TLV output on every call.
88 ///
89 /// # Returns
90 /// - `Ok(ReadRespChunk)` for the first response chunk; multi-chunk
91 /// `ReportData` streams iterate via `ReadRespChunk::complete()`
92 /// - `Err` if the transaction fails at any point (request build,
93 /// I/O, response parsing, etc.)
94 async fn read_with<B>(self, mut build: B) -> Result<ReadRespChunk<'a>, Error>
95 where
96 B: FnMut(ReadReqBuilder<ReadSender<'a>>) -> Result<ReadSender<'a>, Error>,
97 {
98 // Drives the retransmit loop on the caller's behalf
99 // (build closure idempotency contract — same TLV bytes on
100 // every call). First response chunk handed back; the caller
101 // iterates further chunks via `chunk.complete()`.
102 let mut sender = self.read_sender().await?;
103 loop {
104 match sender.tx().await? {
105 TxOutcome::BuildRequest(builder) => {
106 sender = build(builder)?;
107 }
108 TxOutcome::GotResponse(chunk) => return Ok(chunk),
109 }
110 }
111 }
112
113 /// Perform an IM read transaction without using a closure.
114 ///
115 /// # Returns
116 /// - `Ok(ReadSender)` ready for the caller to drive manually via `ReadSender::tx()`
117 /// The first call to [`ReadSender::tx`] yields the initial builder.
118 /// See [`invoke_sender`](Self::invoke_sender) for the full pattern.
119 /// - `Err` if the transaction fails at any point (I/O, etc.)
120 async fn read_sender(self) -> Result<ReadSender<'a>, Error> {
121 let exchange: Exchange<'a> = self.into();
122 let sender = exchange.into_sender()?;
123 Ok(ReadSender {
124 state: ReadSenderState::Ready(sender),
125 })
126 }
127
128 /// Perform an IM write transaction.
129 ///
130 /// # Arguments
131 /// - `build` closure that writes the `WriteRequestMessage` TLV body.
132 /// NOTE: the closure is `FnMut` because the MRP layer may retransmit the
133 /// request multiple times; it MUST produce the same TLV output on every call.
134 ///
135 /// # Returns
136 /// - `Ok(WriteRespHandle)` once the request is ACK-ed and the response is parsed; call
137 /// `WriteRespHandle::response()` to inspect the parsed `WriteResp`.
138 /// - `Err` if the transaction fails at any point (request build, I/O, response parsing, etc.)
139 async fn write_with<B>(
140 self,
141 timed_timeout_ms: Option<u16>,
142 mut build: B,
143 ) -> Result<WriteRespHandle<'a>, Error>
144 where
145 B: FnMut(WriteReqBuilder<WriteSender<'a>>) -> Result<WriteSender<'a>, Error>,
146 {
147 let mut sender = self.write_sender(timed_timeout_ms).await?;
148 loop {
149 match sender.tx().await? {
150 TxOutcome::BuildRequest(builder) => {
151 sender = build(builder)?;
152 }
153 TxOutcome::GotResponse(handle) => return Ok(handle),
154 }
155 }
156 }
157
158 /// Perform an IM write transaction without using a closure.
159 ///
160 /// # Arguments
161 /// - `timed_timeout_ms` if `Some`, perform the initial handshake via a `TimedRequest` with the given timeout (in milliseconds)
162 ///
163 /// # Returns
164 /// - `Ok(WriteSender)` ready for the caller to drive manually via `WriteSender::tx()`
165 /// The first call to [`WriteSender::tx`] yields the initial builder.
166 /// See [`invoke_sender`](Self::invoke_sender) for the full pattern.
167 /// - `Err` if the transaction fails at any point (I/O, etc.)
168 async fn write_sender(self, timed_timeout_ms: Option<u16>) -> Result<WriteSender<'a>, Error> {
169 let mut exchange: Exchange<'a> = self.into();
170 if let Some(timeout_ms) = timed_timeout_ms {
171 send_timed_request(&mut exchange, timeout_ms).await?;
172 }
173 let sender = exchange.into_sender()?;
174 Ok(WriteSender {
175 state: WriteSenderState::Ready(sender),
176 })
177 }
178
179 /// Perform an IM invoke transaction.
180 ///
181 /// # Arguments
182 /// - `timed_timeout_ms` if `Some`, perform the initial handshake via a `TimedRequest` with the given timeout (in milliseconds)
183 /// - `build` closure that writes the `InvokeRequestMessage` TLV body
184 /// NOTE: The closure is `FnMut` because the MRP layer may retransmit the
185 /// request multiple times; it MUST produce the same TLV output on every call.
186 ///
187 /// # Returns
188 /// - `Ok(InvokeRespChunk)` once the request is ACK-ed and the first response chunk is parsed;
189 /// multi-chunk `InvokeResponse` streams iterate via `InvokeRespChunk::complete()`.
190 /// - `Err` if the transaction fails at any point (request build, I/O, response parsing, etc.)
191 async fn invoke_with<B>(
192 self,
193 timed_timeout_ms: Option<u16>,
194 mut build: B,
195 ) -> Result<InvokeRespChunk<'a>, Error>
196 where
197 B: FnMut(InvReqBuilder<InvokeSender<'a>>) -> Result<InvokeSender<'a>, Error>,
198 {
199 // Drives the retransmit loop on the caller's behalf:
200 // the `build` closure is (re-)run on every framework attempt
201 // (so it must remain idempotent — same TLV bytes on every
202 // call), and the first response chunk is returned to the
203 // caller for direct inspection / `complete()` iteration.
204 let mut sender = self.invoke_sender(timed_timeout_ms).await?;
205 loop {
206 match sender.tx().await? {
207 TxOutcome::BuildRequest(builder) => {
208 sender = build(builder)?;
209 }
210 TxOutcome::GotResponse(chunk) => return Ok(chunk),
211 }
212 }
213 }
214
215 /// Perform an IM invoke transaction without using a closure.
216 ///
217 /// # Arguments
218 /// - `timed_timeout_ms` if `Some`, perform the initial handshake via a `TimedRequest` with the given timeout (in milliseconds)
219 ///
220 /// # Returns
221 /// - `Ok(InvokeSender)` ready for the caller to drive manually via `InvokeSender::tx()`.
222 /// The first call to [`InvokeSender::tx`] yields the initial builder.
223 /// - `Err` if the transaction fails at any point (I/O, etc.)
224 ///
225 /// # Lifecycle
226 ///
227 /// 1. `let mut sender = exchange.invoke_sender(None).await?;`
228 /// 2. `loop { match sender.tx().await? { TxOutcome::BuildRequest(b) => sender = build(b)?, TxOutcome::GotResponse(c) => break c } }`
229 /// 3. `loop { let resp = chunk.response()?; …; match chunk.complete().await? { … } }`
230 async fn invoke_sender(self, timed_timeout_ms: Option<u16>) -> Result<InvokeSender<'a>, Error> {
231 let mut exchange: Exchange<'a> = self.into();
232 if let Some(timeout_ms) = timed_timeout_ms {
233 send_timed_request(&mut exchange, timeout_ms).await?;
234 }
235 let sender = exchange.into_sender()?;
236 Ok(InvokeSender {
237 state: InvokeSenderState::Ready(sender),
238 })
239 }
240
241 /// Perform the *establishment* phase of an IM subscribe
242 /// transaction.
243 ///
244 /// On the wire the establishment is a sequence of priming
245 /// `ReportData` chunks (each ACK-ed by the client with
246 /// `StatusResponse(Success)`) followed by a single
247 /// `SubscribeResponse` carrying `subscription_id` and the chosen
248 /// `max_int`. This method drives the request side and hands the
249 /// caller back the first priming chunk; the caller iterates
250 /// further priming chunks (and gets the terminal
251 /// [`SubscribeEstablished`]) via [`SubscribePrimingChunk::complete`].
252 ///
253 /// # Arguments
254 /// - `build` — closure that writes the `SubscribeRequestMessage`
255 /// TLV body via the streaming [`SubscribeReqBuilder`]. NOTE:
256 /// `FnMut` because the MRP layer may retransmit the request;
257 /// it MUST produce the same TLV output on every call.
258 ///
259 /// # Returns
260 /// - `Ok(SubscribePrimingChunk)` for the first priming chunk;
261 /// walk the chunk loop via [`SubscribePrimingChunk::complete`].
262 /// - `Err` on any failure (request build, I/O, response parsing,
263 /// peer-side validation `StatusResponse(non-Success)`, …)
264 ///
265 /// # Scope: establishment only
266 ///
267 /// The *active* subscription phase — server-initiated
268 /// `ReportData` messages arriving on new exchanges throughout
269 /// the lifetime of the subscription — is **not** covered by
270 /// this method. That requires a listening loop on the
271 /// fabric/peer-node pair and is a separate piece of
272 /// infrastructure to layer on top. Once the
273 /// [`SubscribeEstablished`] is returned, the
274 /// fabric+peer+subscription-id triple identifies the active
275 /// subscription for any such future incoming reports.
276 async fn subscribe_with<B>(self, mut build: B) -> Result<SubscribePrimingChunk<'a>, Error>
277 where
278 B: FnMut(SubscribeReqBuilder<SubscribeSender<'a>>) -> Result<SubscribeSender<'a>, Error>,
279 {
280 let mut sender = self.subscribe_sender().await?;
281 loop {
282 match sender.tx().await? {
283 TxOutcome::BuildRequest(builder) => {
284 sender = build(builder)?;
285 }
286 TxOutcome::GotResponse(chunk) => return Ok(chunk),
287 }
288 }
289 }
290
291 /// Perform the establishment phase of an IM subscribe transaction
292 /// without using a closure.
293 ///
294 /// # Returns
295 /// - `Ok(SubscribeSender)` ready to be driven manually via
296 /// [`SubscribeSender::tx`]. The first call yields the initial
297 /// [`SubscribeReqBuilder`].
298 /// - `Err` if the underlying exchange handoff fails.
299 async fn subscribe_sender(self) -> Result<SubscribeSender<'a>, Error> {
300 let exchange: Exchange<'a> = self.into();
301 let sender = exchange.into_sender()?;
302 Ok(SubscribeSender {
303 state: SubscribeSenderState::Ready(sender),
304 })
305 }
306
307 /// Perform a fire-and-forget IM invoke — for group (multicast) exchanges
308 /// opened with [`Exchange::initiate_group`].
309 ///
310 /// Sends a single `InvokeRequestMessage` with `SuppressResponse = true`
311 /// and completes as soon as the message is out: groupcast invokes elicit
312 /// no responses and group messages carry no MRP, so there is nothing to
313 /// wait for. Push the command entries with endpoint-less paths
314 /// ([`CmdDataBuilder::path_from`] with an endpoint-less
315 /// [`crate::im::CmdPath`]) — receivers apply them to their group-member
316 /// endpoints.
317 #[cfg(feature = "groups")]
318 async fn group_invoke_with<B>(self, mut build: B) -> Result<(), Error>
319 where
320 B: for<'x, 'y> FnMut(
321 GroupInvokeRequestsBuilder<'x, 'y>,
322 ) -> Result<GroupInvokeRequestsBuilder<'x, 'y>, Error>,
323 {
324 let mut exchange: Exchange<'a> = self.into();
325
326 exchange
327 .send_with(|_, wb| {
328 let builder =
329 InvReqBuilder::new(TLVWriteParent::new("GroupInvoke", wb), &TLVTag::Anonymous)?
330 .suppress_response(true)?
331 .invoke_requests()?;
332
333 let builder = build(builder)?;
334
335 builder.end()?.end()?;
336
337 Ok(Some(OpCode::InvokeRequest.meta().reliable(false)))
338 })
339 .await
340 }
341}
342
343/// The builder handed to [`ImClient::group_invoke_with`]'s closure: the
344/// `InvokeRequests` array of a `SuppressResponse` invoke, writing into the
345/// exchange's TX buffer.
346#[cfg(feature = "groups")]
347pub type GroupInvokeRequestsBuilder<'x, 'y> = crate::im::CmdDataArrayBuilder<
348 InvReqBuilder<TLVWriteParent<&'static str, &'x mut WriteBuf<'y>>, 3>,
349>;
350
351/// Blanket impl so any [`Exchange<'a>`] is an [`ImClient<'a>`] when
352/// the trait is `use`d. The default-method bodies do all the work;
353/// this impl just opts the type in.
354impl<'a> ImClient<'a> for Exchange<'a> {}
355
356/// Outcome of calling `.tx()` on a transaction sender (`*Sender`).
357#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
358#[cfg_attr(feature = "defmt", derive(defmt::Format))]
359pub enum TxOutcome<F, S> {
360 /// Framework needs the request bytes (re-)built into a fresh TX slot;
361 BuildRequest(F),
362 /// Framework has received the peer's ACK; here's the first response chunk.
363 GotResponse(S),
364}
365
366// =====================================================================
367// Transaction types for the `read` opcode.
368//
369// Mirrors the `invoke` set: `ReadSender` drives the MRP retransmit
370// loop; the codegen `ReadReqBuilder` writes through `ReadSenderSlot`
371// while the slot is live; `ReadRespChunk` gives chunk-by-chunk
372// access to the resulting `ReportData` stream.
373// =====================================================================
374
375/// Cornerstone `read` transaction. See module docs for the
376/// pattern. Returned by [`ImClient::read_sender`].
377pub struct ReadSender<'a> {
378 state: ReadSenderState<'a>,
379}
380
381enum ReadSenderState<'a> {
382 Ready(OwnedSender<'a>),
383 Slot(ReadSenderSlot<'a>),
384}
385
386impl<'a> ReadSender<'a> {
387 /// Drive one round of the MRP retransmit loop. See
388 /// [`InvokeSender::tx`] for the full contract; the read variant is
389 /// identical except the right arm holds a [`ReadRespChunk`].
390 pub async fn tx(
391 mut self,
392 ) -> Result<TxOutcome<ReadReqBuilder<ReadSender<'a>>, ReadRespChunk<'a>>, Error> {
393 let sender = match self.state {
394 ReadSenderState::Slot(slot) => slot.commit()?,
395 ReadSenderState::Ready(s) => s,
396 };
397
398 match sender.tx().await? {
399 Either::Left(tx) => {
400 self.state = ReadSenderState::Slot(ReadSenderSlot { tx, cursor: 0 });
401 let builder = ReadReqBuilder::new(self, &TLVTag::Anonymous)?;
402 Ok(TxOutcome::BuildRequest(builder))
403 }
404 Either::Right(exchange) => Ok(TxOutcome::GotResponse(
405 ReadRespChunk::receive(exchange).await?,
406 )),
407 }
408 }
409}
410
411impl<'a> TLVBuilderParent for ReadSender<'a> {
412 type Write = ReadSenderSlot<'a>;
413
414 fn writer(&mut self) -> &mut Self::Write {
415 match &mut self.state {
416 ReadSenderState::Slot(slot) => slot,
417 ReadSenderState::Ready(_) => panic!(
418 "ReadSender::writer() called outside the build phase — \
419 only reachable via a ReadReqBuilder yielded by ReadSender::tx."
420 ),
421 }
422 }
423}
424
425impl<'a> core::fmt::Debug for ReadSender<'a> {
426 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
427 write!(f, "ReadSender")
428 }
429}
430
431#[cfg(feature = "defmt")]
432impl<'a> defmt::Format for ReadSender<'a> {
433 fn format(&self, f: defmt::Formatter<'_>) {
434 defmt::write!(f, "ReadSender")
435 }
436}
437
438/// Internal serialization handle for the in-flight build of a
439/// [`ReadSender`]. See [`InvokeSenderSlot`] for the design rationale —
440/// this type exists only because [`TLVBuilderParent`] requires the
441/// `Write` associated type to be a named type.
442pub struct ReadSenderSlot<'a> {
443 tx: OwnedSenderTx<'a>,
444 cursor: usize,
445}
446
447impl<'a> ReadSenderSlot<'a> {
448 fn commit(self) -> Result<OwnedSender<'a>, Error> {
449 self.tx.complete(0, self.cursor, OpCode::ReadRequest.into())
450 }
451}
452
453impl<'a> TLVWrite for ReadSenderSlot<'a> {
454 type Position = usize;
455
456 fn write(&mut self, byte: u8) -> Result<(), Error> {
457 let payload = self.tx.payload();
458 if self.cursor >= payload.len() {
459 return Err(ErrorCode::NoSpace.into());
460 }
461 payload[self.cursor] = byte;
462 self.cursor += 1;
463 Ok(())
464 }
465
466 fn get_tail(&self) -> Self::Position {
467 self.cursor
468 }
469
470 fn rewind_to(&mut self, pos: Self::Position) {
471 self.cursor = pos;
472 }
473}
474
475impl<'a> core::fmt::Debug for ReadSenderSlot<'a> {
476 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
477 write!(f, "ReadSenderSlot({})", self.cursor)
478 }
479}
480
481#[cfg(feature = "defmt")]
482impl<'a> defmt::Format for ReadSenderSlot<'a> {
483 fn format(&self, f: defmt::Formatter<'_>) {
484 defmt::write!(f, "ReadSenderSlot({})", self.cursor)
485 }
486}
487
488/// First (possibly only) response chunk of a `read`
489/// transaction. Returned by [`ReadSender::tx`] once the peer has ACK-ed
490/// the request and the first `ReportData` chunk is parsed.
491///
492/// Multi-chunk `ReportData` streams iterate via
493/// [`complete`](Self::complete) — same shape as [`InvokeRespChunk`].
494pub struct ReadRespChunk<'a> {
495 exchange: Exchange<'a>,
496}
497
498impl<'a> ReadRespChunk<'a> {
499 async fn receive(mut exchange: Exchange<'a>) -> Result<Self, Error> {
500 exchange.recv_fetch().await?;
501 {
502 let rx = exchange.rx()?;
503 check_opcode(rx.meta().proto_opcode, OpCode::ReportData)?;
504 }
505 Ok(Self { exchange })
506 }
507
508 /// Borrowed access to the parsed `ReportDataResp` for this chunk.
509 pub fn response(&self) -> Result<ReportDataResp<'_>, Error> {
510 let rx = self.exchange.rx()?;
511 let element = TLVElement::new(rx.payload());
512 ReportDataResp::from_tlv(&element)
513 }
514
515 /// ACK the current chunk; if `more_chunks=true`, fetch + parse
516 /// the next chunk and return it as `Some(next)`. Otherwise drop
517 /// the exchange and return `None`.
518 pub async fn complete(mut self) -> Result<Option<Self>, Error> {
519 let (more_chunks, suppress_response) = {
520 let resp = self.response()?;
521 (
522 resp.more_chunks.unwrap_or(false),
523 resp.suppress_response.unwrap_or(false),
524 )
525 };
526
527 if more_chunks {
528 // Request next chunk.
529 self.exchange
530 .send_with(|_, wb| {
531 StatusResp::write(wb, IMStatusCode::Success)?;
532 Ok(Some(OpCode::StatusResponse.into()))
533 })
534 .await?;
535
536 self.exchange.recv_fetch().await?;
537 {
538 let rx = self.exchange.rx()?;
539 check_opcode(rx.meta().proto_opcode, OpCode::ReportData)?;
540 }
541
542 Ok(Some(self))
543 } else {
544 if !suppress_response {
545 self.exchange
546 .send_with(|_, wb| {
547 StatusResp::write(wb, IMStatusCode::Success)?;
548 Ok(Some(OpCode::StatusResponse.into()))
549 })
550 .await?;
551 } else {
552 self.exchange.acknowledge().await?;
553 }
554 Ok(None)
555 }
556 }
557}
558
559// =====================================================================
560// Transaction types for the `write` opcode.
561//
562// Mirrors the `invoke` / `read` sets. `WriteResponseMessage`
563// is single-message per spec (no chunking), so the receive side has
564// a [`WriteRespHandle`] with just a [`response()`](WriteRespHandle::response)
565// method — no `complete()` iteration.
566// =====================================================================
567
568/// Cornerstone `write` transaction. See module docs for the
569/// pattern. Returned by [`ImClient::write_sender`].
570pub struct WriteSender<'a> {
571 state: WriteSenderState<'a>,
572}
573
574enum WriteSenderState<'a> {
575 Ready(OwnedSender<'a>),
576 Slot(WriteSenderSlot<'a>),
577}
578
579impl<'a> WriteSender<'a> {
580 /// Drive one round of the MRP retransmit loop. Mirrors
581 /// [`InvokeSender::tx`] / [`ReadSender::tx`] except the right arm
582 /// returns a [`WriteRespHandle`] (no chunking on write).
583 pub async fn tx(
584 mut self,
585 ) -> Result<TxOutcome<WriteReqBuilder<WriteSender<'a>>, WriteRespHandle<'a>>, Error> {
586 let sender = match self.state {
587 WriteSenderState::Slot(slot) => slot.commit()?,
588 WriteSenderState::Ready(s) => s,
589 };
590
591 match sender.tx().await? {
592 Either::Left(tx) => {
593 self.state = WriteSenderState::Slot(WriteSenderSlot { tx, cursor: 0 });
594 let builder = WriteReqBuilder::new(self, &TLVTag::Anonymous)?;
595 Ok(TxOutcome::BuildRequest(builder))
596 }
597 Either::Right(exchange) => Ok(TxOutcome::GotResponse(
598 WriteRespHandle::receive(exchange).await?,
599 )),
600 }
601 }
602}
603
604impl<'a> TLVBuilderParent for WriteSender<'a> {
605 type Write = WriteSenderSlot<'a>;
606
607 fn writer(&mut self) -> &mut Self::Write {
608 match &mut self.state {
609 WriteSenderState::Slot(slot) => slot,
610 WriteSenderState::Ready(_) => panic!(
611 "WriteSender::writer() called outside the build phase — \
612 only reachable via a WriteReqBuilder yielded by WriteSender::tx."
613 ),
614 }
615 }
616}
617
618impl<'a> core::fmt::Debug for WriteSender<'a> {
619 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
620 write!(f, "WriteSender")
621 }
622}
623
624#[cfg(feature = "defmt")]
625impl<'a> defmt::Format for WriteSender<'a> {
626 fn format(&self, f: defmt::Formatter<'_>) {
627 defmt::write!(f, "WriteSender")
628 }
629}
630
631/// Internal serialization handle for the in-flight build of a
632/// [`WriteSender`]. See [`InvokeSenderSlot`] for the design rationale.
633pub struct WriteSenderSlot<'a> {
634 tx: OwnedSenderTx<'a>,
635 cursor: usize,
636}
637
638impl<'a> WriteSenderSlot<'a> {
639 fn commit(self) -> Result<OwnedSender<'a>, Error> {
640 self.tx
641 .complete(0, self.cursor, OpCode::WriteRequest.into())
642 }
643}
644
645impl<'a> TLVWrite for WriteSenderSlot<'a> {
646 type Position = usize;
647
648 fn write(&mut self, byte: u8) -> Result<(), Error> {
649 let payload = self.tx.payload();
650 if self.cursor >= payload.len() {
651 return Err(ErrorCode::NoSpace.into());
652 }
653 payload[self.cursor] = byte;
654 self.cursor += 1;
655 Ok(())
656 }
657
658 fn get_tail(&self) -> Self::Position {
659 self.cursor
660 }
661
662 fn rewind_to(&mut self, pos: Self::Position) {
663 self.cursor = pos;
664 }
665}
666
667impl<'a> core::fmt::Debug for WriteSenderSlot<'a> {
668 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
669 write!(f, "WriteSenderSlot({})", self.cursor)
670 }
671}
672
673#[cfg(feature = "defmt")]
674impl<'a> defmt::Format for WriteSenderSlot<'a> {
675 fn format(&self, f: defmt::Formatter<'_>) {
676 defmt::write!(f, "WriteSenderSlot({})", self.cursor)
677 }
678}
679
680/// Handle to the (single, non-chunked) response of a `write`
681/// transaction. Returned by [`WriteSender::tx`] once the peer has
682/// ACK-ed the request and the response is parsed.
683///
684/// Unlike [`InvokeRespChunk`] / [`ReadRespChunk`], `WriteResponse`
685/// is a single message per Matter Core spec — no chunk
686/// iteration is needed; just call [`response`](Self::response) to
687/// inspect the parsed [`WriteResp`].
688pub struct WriteRespHandle<'a> {
689 exchange: Exchange<'a>,
690}
691
692impl<'a> WriteRespHandle<'a> {
693 async fn receive(mut exchange: Exchange<'a>) -> Result<Self, Error> {
694 exchange.recv_fetch().await?;
695 {
696 let rx = exchange.rx()?;
697 check_opcode(rx.meta().proto_opcode, OpCode::WriteResponse)?;
698 }
699 // ACK here (via standalone `acknowledge()`, not `send_with`)
700 // because `send_with` would clear the RX buffer and break
701 // zero-copy access from `response()` below.
702 exchange.acknowledge().await?;
703 Ok(Self { exchange })
704 }
705
706 /// Borrowed access to the parsed `WriteResp`. The returned value
707 /// points into the exchange's RX buffer, which stays valid until
708 /// this handle is dropped.
709 pub fn response(&self) -> Result<WriteResp<'_>, Error> {
710 let rx = self.exchange.rx()?;
711 WriteResp::from_tlv(&TLVElement::new(rx.payload()))
712 }
713}
714
715// =====================================================================
716// Transaction types for the `invoke` opcode.
717//
718// `InvokeSender` is the cornerstone of the closure-free, scratch-buffer-
719// free IM client. It owns the exchange end-to-end (via `OwnedSender`
720// internally) and exposes a `tx().await` method that drives one
721// round of the MRP retransmit loop. The user matches on the result:
722//
723// - `TxOutcome::BuildRequest(builder)` → (re-)build the request bytes via the
724// typestate builder; `.end()` returns the `InvokeSender` back for the
725// next round.
726// - `TxOutcome::GotResponse(chunk)` → the request has been ACK-ed; here's
727// the first response chunk. Iterate via `chunk.complete().await`.
728//
729// Closure-based and scratch-buffer-based variants
730// will be layered on top, mirroring how `Exchange::send_with` and
731// `Exchange::send` are layered on top of `Exchange::sender`.
732// =====================================================================
733
734/// Cornerstone `invoke` transaction. See module docs for the
735/// pattern. Returned by [`ImClient::invoke_sender`].
736///
737/// Public surface is intentionally narrow: a single async
738/// [`tx`](Self::tx) method that drives one round of the MRP loop.
739/// The TLV-serialization plumbing the codegen request builder uses
740/// to fill the request bytes lives in a separate
741/// [`InvokeSenderSlot`] type accessed via [`TLVBuilderParent::writer`],
742/// so that `u8` / `start_struct` / etc. don't appear directly on
743/// `InvokeSender` and tempt users to drive the TX buffer by hand.
744pub struct InvokeSender<'a> {
745 state: InvokeSenderState<'a>,
746}
747
748enum InvokeSenderState<'a> {
749 /// Between rounds: own a sender, no slot. The first `tx()` call
750 /// from this state acquires a slot and hands back a builder. The
751 /// `n`-th call (n ≥ 1) here means the previous round's bytes are
752 /// in flight; we wait for the next framework event.
753 Ready(OwnedSender<'a>),
754 /// During build: own a fully-prepared [`InvokeSenderSlot`] (TX slot
755 /// plus cursor) that the codegen builder writes into via
756 /// [`TLVBuilderParent::writer`]. The next `tx()` call commits
757 /// `slot`'s bytes via [`OwnedSenderTx::complete`] and transitions
758 /// back to `Ready`.
759 Slot(InvokeSenderSlot<'a>),
760}
761
762impl<'a> InvokeSender<'a> {
763 /// Drive one round of the MRP retransmit loop.
764 ///
765 /// - Returns `TxOutcome::BuildRequest(builder)` when the framework needs
766 /// the request bytes (re-)built into a fresh TX slot. The
767 /// builder's `P` parent is this `InvokeSender`; calling `.end()`
768 /// on the message builder hands the `InvokeSender` back, ready
769 /// for the next `tx()` call.
770 /// - Returns `TxOutcome::GotResponse(chunk)` once the framework has
771 /// received the peer's ACK; iterate the chunk loop via
772 /// [`InvokeRespChunk::complete`].
773 ///
774 /// The first call after [`ImClient::invoke_sender`] is guaranteed
775 /// to yield `TxOutcome::BuildRequest(builder)` because no message has been sent yet.
776 pub async fn tx(
777 mut self,
778 ) -> Result<TxOutcome<InvReqBuilder<InvokeSender<'a>>, InvokeRespChunk<'a>>, Error> {
779 // 1. If we're in Slot state, commit the bytes we just built.
780 let sender = match self.state {
781 InvokeSenderState::Slot(slot) => slot.commit()?,
782 InvokeSenderState::Ready(s) => s,
783 };
784
785 // 2. Ask the framework for the next event.
786 match sender.tx().await? {
787 Either::Left(tx) => {
788 // Re-build needed (initial or retransmit). Move to
789 // Slot state and hand back a fresh builder.
790 self.state = InvokeSenderState::Slot(InvokeSenderSlot { tx, cursor: 0 });
791 let builder = InvReqBuilder::new(self, &TLVTag::Anonymous)?;
792 Ok(TxOutcome::BuildRequest(builder))
793 }
794 Either::Right(exchange) => {
795 // ACK received — fetch and parse the first response chunk.
796 Ok(TxOutcome::GotResponse(
797 InvokeRespChunk::receive(exchange).await?,
798 ))
799 }
800 }
801 }
802}
803
804impl<'a> TLVBuilderParent for InvokeSender<'a> {
805 type Write = InvokeSenderSlot<'a>;
806
807 fn writer(&mut self) -> &mut Self::Write {
808 match &mut self.state {
809 InvokeSenderState::Slot(slot) => slot,
810 // The only way to reach `writer()` on an `InvokeSender` is
811 // through the codegen builder constructed inside
812 // [`InvokeSender::tx`]'s `TxOutcome::BuildRequest` arm, which transitions to
813 // `Slot` state before yielding the builder. Hitting this
814 // branch means the invariant was violated externally —
815 // panic rather than corrupt the TX buffer silently.
816 InvokeSenderState::Ready(_) => panic!(
817 "InvokeSender::writer() called outside the build phase \
818 (state = Ready); only reachable via an InvReqBuilder \
819 yielded by InvokeSender::tx — see module docs."
820 ),
821 }
822 }
823}
824
825impl<'a> core::fmt::Debug for InvokeSender<'a> {
826 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
827 write!(f, "InvokeSender")
828 }
829}
830
831#[cfg(feature = "defmt")]
832impl<'a> defmt::Format for InvokeSender<'a> {
833 fn format(&self, f: defmt::Formatter<'_>) {
834 defmt::write!(f, "InvokeSender")
835 }
836}
837
838/// Internal serialization handle for the in-flight build of an
839/// [`InvokeSender`].
840///
841/// This type only exists because [`TLVBuilderParent`] requires the
842/// `Write` associated type to be a named type (so the codegen request
843/// builders can write through it). Users should not interact with it
844/// directly — go through [`InvokeSender::tx`] and the typed
845/// [`InvReqBuilder`] it returns.
846///
847/// Fields are private to enforce that the only way to drive
848/// `cursor` forward is by writing TLV through the
849/// [`TLVWrite`] impl below.
850pub struct InvokeSenderSlot<'a> {
851 tx: OwnedSenderTx<'a>,
852 cursor: usize,
853}
854
855impl<'a> InvokeSenderSlot<'a> {
856 /// Consume the slot — commit the bytes accumulated in `cursor`
857 /// via [`OwnedSenderTx::complete`] and return the
858 /// [`OwnedSender`] for the next retransmit-loop iteration.
859 fn commit(self) -> Result<OwnedSender<'a>, Error> {
860 self.tx
861 .complete(0, self.cursor, OpCode::InvokeRequest.into())
862 }
863}
864
865impl<'a> TLVWrite for InvokeSenderSlot<'a> {
866 type Position = usize;
867
868 fn write(&mut self, byte: u8) -> Result<(), Error> {
869 let payload = self.tx.payload();
870 if self.cursor >= payload.len() {
871 return Err(ErrorCode::NoSpace.into());
872 }
873 payload[self.cursor] = byte;
874 self.cursor += 1;
875 Ok(())
876 }
877
878 /// Byte offset into the active TX-slot payload at which the next
879 /// `write()` would land. Used by the derived `ToTLV` impls (and
880 /// similar helpers) to mark a rollback anchor before composing a
881 /// TLV structure.
882 fn get_tail(&self) -> Self::Position {
883 self.cursor
884 }
885
886 /// Roll the cursor back to a position previously returned by
887 /// [`get_tail`]. Used by derived `ToTLV` impls to unwind a
888 /// partially-written TLV structure on error.
889 fn rewind_to(&mut self, pos: Self::Position) {
890 self.cursor = pos;
891 }
892}
893
894impl<'a> core::fmt::Debug for InvokeSenderSlot<'a> {
895 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
896 write!(f, "InvokeSenderSlot({})", self.cursor)
897 }
898}
899
900#[cfg(feature = "defmt")]
901impl<'a> defmt::Format for InvokeSenderSlot<'a> {
902 fn format(&self, f: defmt::Formatter<'_>) {
903 defmt::write!(f, "InvokeSenderSlot({})", self.cursor)
904 }
905}
906
907/// First (possibly only) response chunk of an `invoke`
908/// transaction. Returned by [`InvokeSender::tx`] once the peer has
909/// ACK-ed the request.
910///
911/// The borrowed [`response`](Self::response) method gives zero-copy
912/// access to the parsed [`InvokeResp`] backed by the exchange's RX
913/// buffer; the buffer stays valid until [`complete`](Self::complete)
914/// is called.
915///
916/// For multi-chunk responses (the server signals `more_chunks=true`),
917/// [`complete`](Self::complete) returns `Some(next_chunk)` so the
918/// caller can iterate; otherwise it returns `None` and drops the
919/// exchange.
920///
921/// Per Matter Core spec, a server MAY reply to a command
922/// declared with `DefaultSuccess` (no explicit response struct) by
923/// sending a plain `StatusResponse(Success)` instead of a full
924/// `InvokeResponse`. In that case the chunk is *status-only*:
925/// [`response`](Self::response) returns `None`, and
926/// [`complete`](Self::complete) is terminal (returns `None`).
927pub struct InvokeRespChunk<'a> {
928 exchange: Exchange<'a>,
929 /// `true` when the peer replied with `StatusResponse(Success)`
930 /// instead of a real `InvokeResponse` (DefaultSuccess commands).
931 status_only: bool,
932}
933
934impl<'a> InvokeRespChunk<'a> {
935 async fn receive(mut exchange: Exchange<'a>) -> Result<Self, Error> {
936 exchange.recv_fetch().await?;
937 let opcode = exchange.rx()?.meta().proto_opcode;
938
939 if opcode == OpCode::InvokeResponse as u8 {
940 Ok(Self {
941 exchange,
942 status_only: false,
943 })
944 } else if opcode == OpCode::StatusResponse as u8 {
945 // DefaultSuccess command — server replied with a plain
946 // StatusResponse. Translate non-Success codes to errors;
947 // otherwise treat as an empty (status-only) chunk.
948 let status = {
949 let rx = exchange.rx()?;
950 let element = TLVElement::new(rx.payload());
951 StatusResp::from_tlv(&element)?.status
952 };
953 if status == IMStatusCode::Success {
954 Ok(Self {
955 exchange,
956 status_only: true,
957 })
958 } else {
959 error!("Invoke reply: StatusResponse({:?})", status);
960 Err(status.to_error_code().unwrap_or(ErrorCode::Failure).into())
961 }
962 } else {
963 Err(ErrorCode::InvalidOpcode.into())
964 }
965 }
966
967 /// Whether the peer replied with `StatusResponse(Success)`
968 /// (DefaultSuccess command) rather than a real `InvokeResponse`.
969 /// In that case [`response`](Self::response) returns `None`.
970 pub fn is_status_only(&self) -> bool {
971 self.status_only
972 }
973
974 /// Borrowed access to the parsed `InvokeResp` for this chunk —
975 /// `None` if the chunk is status-only (see [`is_status_only`]).
976 /// The returned value points into the exchange's RX buffer, so
977 /// its lifetime is the borrow of this `InvokeRespChunk`.
978 pub fn response(&self) -> Result<Option<InvokeResp<'_>>, Error> {
979 if self.status_only {
980 return Ok(None);
981 }
982 let rx = self.exchange.rx()?;
983 let element = TLVElement::new(rx.payload());
984 InvokeResp::from_tlv(&element).map(Some)
985 }
986
987 /// ACK the current chunk and, if the server signalled
988 /// `more_chunks=true`, fetch + parse the next chunk and return
989 /// it as `Some(next)`. Otherwise (final chunk, or status-only)
990 /// drop the exchange and return `None`.
991 ///
992 /// Chunking flow control (Matter Core): on receipt of
993 /// any `InvokeResponseMessage` with `MoreChunkedResponses=true`,
994 /// the receiver SHALL reply with `StatusResponse(Success)` and
995 /// the sender SHALL await that ACK before transmitting the next
996 /// chunk. On the **final** chunk (`MoreChunks=false`) no trailer
997 /// is sent — per spec the `SuppressResponse` field on an
998 /// `InvokeResponseMessage` is *ignored by the client*, and Matter
999 /// "does not support responses to InvokeResponse actions" at the
1000 /// action layer. So the terminal-chunk branch is MRP-ack only,
1001 /// regardless of the `SuppressResponse` value the server echoed.
1002 pub async fn complete(mut self) -> Result<Option<Self>, Error> {
1003 if self.status_only {
1004 // Status-only chunks are terminal — no chunking, no
1005 // additional StatusResponse round-trip needed. Just ACK
1006 // the message at the MRP layer and we're done.
1007 self.exchange.acknowledge().await?;
1008 return Ok(None);
1009 }
1010
1011 let (more_chunks, suppress_response) = {
1012 let resp = self
1013 .response()?
1014 .expect("status_only checked above; response() must be Some");
1015 (
1016 resp.more_chunks.unwrap_or(false),
1017 resp.suppress_response.unwrap_or(false),
1018 )
1019 };
1020
1021 if more_chunks {
1022 // If MoreChunkedMessages is true,
1023 // SuppressResponse SHALL be false. A peer that
1024 // violates this is malformed — abort the chain.
1025 if suppress_response {
1026 send_abort(&mut self.exchange).await?;
1027 return Err(ErrorCode::InvalidData.into());
1028 }
1029
1030 // Per the spec — flow-control ACK between chunks.
1031 self.exchange
1032 .send_with(|_, wb| {
1033 StatusResp::write(wb, IMStatusCode::Success)?;
1034 Ok(Some(OpCode::StatusResponse.into()))
1035 })
1036 .await?;
1037
1038 self.exchange.recv_fetch().await?;
1039 {
1040 let rx = self.exchange.rx()?;
1041 check_opcode(rx.meta().proto_opcode, OpCode::InvokeResponse)?;
1042 }
1043
1044 Ok(Some(self))
1045 } else {
1046 // Final (or only) chunk. Per the spec, Matter does not
1047 // support responses to InvokeResponse actions — the
1048 // SuppressResponse field is ignored by the client. MRP
1049 // -ack only.
1050 self.exchange.acknowledge().await?;
1051 Ok(None)
1052 }
1053 }
1054}
1055
1056// =====================================================================
1057// Transaction types for the `subscribe` opcode.
1058//
1059// On the wire the establishment of a subscription is:
1060// 1. Client → SubscribeRequest
1061// 2. Server → ReportData (priming, with `more_chunks=true` until
1062// the last chunk has `more_chunks=false`); client ACKs each
1063// with `StatusResponse(Success)`
1064// 3. Server → SubscribeResponse (carries `subscription_id` and
1065// the chosen `max_int`)
1066//
1067// `SubscribeSender` drives the request side; `SubscribePrimingChunk`
1068// owns the response stream during priming. The terminal
1069// `complete()` returns either another priming chunk (more reports
1070// coming) or `SubscribeEstablished` carrying the subscription id /
1071// max interval. The exchange is dropped at that point; ongoing
1072// (post-establishment) report messages arrive on server-initiated
1073// exchanges and require a separate listening abstraction.
1074// =====================================================================
1075
1076/// Cornerstone `subscribe` transaction. See module docs for the
1077/// pattern. Returned by [`ImClient::subscribe_sender`].
1078pub struct SubscribeSender<'a> {
1079 state: SubscribeSenderState<'a>,
1080}
1081
1082enum SubscribeSenderState<'a> {
1083 Ready(OwnedSender<'a>),
1084 Slot(SubscribeSenderSlot<'a>),
1085}
1086
1087impl<'a> SubscribeSender<'a> {
1088 /// Drive one round of the MRP retransmit loop. Same shape as
1089 /// [`ReadSender::tx`] except the right arm holds a
1090 /// [`SubscribePrimingChunk`] (the first priming `ReportData`).
1091 pub async fn tx(
1092 mut self,
1093 ) -> Result<TxOutcome<SubscribeReqBuilder<SubscribeSender<'a>>, SubscribePrimingChunk<'a>>, Error>
1094 {
1095 let sender = match self.state {
1096 SubscribeSenderState::Slot(slot) => slot.commit()?,
1097 SubscribeSenderState::Ready(s) => s,
1098 };
1099
1100 match sender.tx().await? {
1101 Either::Left(tx) => {
1102 self.state = SubscribeSenderState::Slot(SubscribeSenderSlot { tx, cursor: 0 });
1103 let builder = SubscribeReqBuilder::new(self, &TLVTag::Anonymous)?;
1104 Ok(TxOutcome::BuildRequest(builder))
1105 }
1106 Either::Right(exchange) => Ok(TxOutcome::GotResponse(
1107 SubscribePrimingChunk::receive(exchange).await?,
1108 )),
1109 }
1110 }
1111}
1112
1113impl<'a> TLVBuilderParent for SubscribeSender<'a> {
1114 type Write = SubscribeSenderSlot<'a>;
1115
1116 fn writer(&mut self) -> &mut Self::Write {
1117 match &mut self.state {
1118 SubscribeSenderState::Slot(slot) => slot,
1119 SubscribeSenderState::Ready(_) => panic!(
1120 "SubscribeSender::writer() called outside the build phase — \
1121 only reachable via a SubscribeReqBuilder yielded by SubscribeSender::tx."
1122 ),
1123 }
1124 }
1125}
1126
1127impl<'a> core::fmt::Debug for SubscribeSender<'a> {
1128 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1129 write!(f, "SubscribeSender")
1130 }
1131}
1132
1133#[cfg(feature = "defmt")]
1134impl<'a> defmt::Format for SubscribeSender<'a> {
1135 fn format(&self, f: defmt::Formatter<'_>) {
1136 defmt::write!(f, "SubscribeSender")
1137 }
1138}
1139
1140/// Internal serialization handle for the in-flight build of a
1141/// [`SubscribeSender`]. Same role as [`InvokeSenderSlot`] —
1142/// see its docs for why this type exists.
1143pub struct SubscribeSenderSlot<'a> {
1144 tx: OwnedSenderTx<'a>,
1145 cursor: usize,
1146}
1147
1148impl<'a> SubscribeSenderSlot<'a> {
1149 fn commit(self) -> Result<OwnedSender<'a>, Error> {
1150 self.tx
1151 .complete(0, self.cursor, OpCode::SubscribeRequest.into())
1152 }
1153}
1154
1155impl<'a> TLVWrite for SubscribeSenderSlot<'a> {
1156 type Position = usize;
1157
1158 fn write(&mut self, byte: u8) -> Result<(), Error> {
1159 let payload = self.tx.payload();
1160 if self.cursor >= payload.len() {
1161 return Err(ErrorCode::NoSpace.into());
1162 }
1163 payload[self.cursor] = byte;
1164 self.cursor += 1;
1165 Ok(())
1166 }
1167
1168 fn get_tail(&self) -> Self::Position {
1169 self.cursor
1170 }
1171
1172 fn rewind_to(&mut self, pos: Self::Position) {
1173 self.cursor = pos;
1174 }
1175}
1176
1177impl<'a> core::fmt::Debug for SubscribeSenderSlot<'a> {
1178 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1179 write!(f, "SubscribeSenderSlot({})", self.cursor)
1180 }
1181}
1182
1183#[cfg(feature = "defmt")]
1184impl<'a> defmt::Format for SubscribeSenderSlot<'a> {
1185 fn format(&self, f: defmt::Formatter<'_>) {
1186 defmt::write!(f, "SubscribeSenderSlot({})", self.cursor)
1187 }
1188}
1189
1190/// First (possibly only) priming `ReportData` chunk of a subscribe
1191/// transaction. Returned by [`SubscribeSender::tx`] once the peer has
1192/// ACK-ed the `SubscribeRequest` and the first `ReportData` is
1193/// parsed. Same `response()` shape as [`ReadRespChunk`].
1194///
1195/// Walk the priming sequence — and pick up the final
1196/// [`SubscribeEstablished`] — via [`Self::complete`].
1197pub struct SubscribePrimingChunk<'a> {
1198 exchange: Exchange<'a>,
1199}
1200
1201impl<'a> core::fmt::Debug for SubscribePrimingChunk<'a> {
1202 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1203 write!(f, "SubscribePrimingChunk")
1204 }
1205}
1206
1207#[cfg(feature = "defmt")]
1208impl<'a> defmt::Format for SubscribePrimingChunk<'a> {
1209 fn format(&self, f: defmt::Formatter<'_>) {
1210 defmt::write!(f, "SubscribePrimingChunk")
1211 }
1212}
1213
1214impl<'a> SubscribePrimingChunk<'a> {
1215 async fn receive(mut exchange: Exchange<'a>) -> Result<Self, Error> {
1216 exchange.recv_fetch().await?;
1217 {
1218 let rx = exchange.rx()?;
1219 check_opcode(rx.meta().proto_opcode, OpCode::ReportData)?;
1220 }
1221 Ok(Self { exchange })
1222 }
1223
1224 /// Borrowed access to the parsed `ReportDataResp` for this
1225 /// priming chunk. The returned value points into the exchange's
1226 /// RX buffer; its lifetime is the borrow of this chunk.
1227 pub fn response(&self) -> Result<ReportDataResp<'_>, Error> {
1228 let rx = self.exchange.rx()?;
1229 let element = TLVElement::new(rx.payload());
1230 ReportDataResp::from_tlv(&element)
1231 }
1232
1233 /// ACK the current priming chunk and advance to the next stage:
1234 ///
1235 /// - If the chunk's `more_chunks=true`: send
1236 /// `StatusResponse(Success)`, fetch the next priming
1237 /// `ReportData`, and return `Ok(NextChunk(self))`.
1238 /// - If `more_chunks=false`: send the trailing
1239 /// `StatusResponse(Success)`, then await + parse the peer's
1240 /// `SubscribeResponse`, and return `Ok(Established(...))` with
1241 /// the subscription id and chosen max interval.
1242 /// - If the priming stream is aborted (peer sends
1243 /// `StatusResponse(non-Success)` instead of either `ReportData`
1244 /// or `SubscribeResponse`), return `Err`.
1245 pub async fn complete(mut self) -> Result<SubscribeOutcome<'a>, Error> {
1246 let (more_chunks, suppress_response) = {
1247 let resp = self.response()?;
1248 (
1249 resp.more_chunks.unwrap_or(false),
1250 resp.suppress_response.unwrap_or(false),
1251 )
1252 };
1253
1254 if more_chunks {
1255 // Spec forbids suppress_response=true alongside
1256 // more_chunks=true (same constraint as ReadRespChunk).
1257 if suppress_response {
1258 send_abort(&mut self.exchange).await?;
1259 return Err(ErrorCode::InvalidData.into());
1260 }
1261
1262 // ACK with StatusResponse(Success), fetch next ReportData.
1263 self.exchange
1264 .send_with(|_, wb| {
1265 StatusResp::write(wb, IMStatusCode::Success)?;
1266 Ok(Some(OpCode::StatusResponse.into()))
1267 })
1268 .await?;
1269
1270 self.exchange.recv_fetch().await?;
1271 {
1272 let rx = self.exchange.rx()?;
1273 check_opcode(rx.meta().proto_opcode, OpCode::ReportData)?;
1274 }
1275
1276 Ok(SubscribeOutcome::NextChunk(self))
1277 } else {
1278 // Last priming ReportData. Send the trailing
1279 // StatusResponse(Success) (unless the server explicitly
1280 // suppressed it — unusual for subscribe but legal) and
1281 // wait for the peer's SubscribeResponse.
1282 if !suppress_response {
1283 self.exchange
1284 .send_with(|_, wb| {
1285 StatusResp::write(wb, IMStatusCode::Success)?;
1286 Ok(Some(OpCode::StatusResponse.into()))
1287 })
1288 .await?;
1289 }
1290
1291 self.exchange.recv_fetch().await?;
1292 let opcode = self.exchange.rx()?.meta().proto_opcode;
1293
1294 if opcode == OpCode::SubscribeResponse as u8 {
1295 let (subscription_id, max_int) = {
1296 let rx = self.exchange.rx()?;
1297 let resp = SubscribeResp::from_tlv(&TLVElement::new(rx.payload()))?;
1298 (resp.subs_id, resp.max_int)
1299 };
1300 // ACK the SubscribeResponse at the MRP layer. After
1301 // this the establishment exchange is terminal; the
1302 // ongoing subscription lives on the (fab, peer, sub_id)
1303 // triple via server-initiated future exchanges.
1304 self.exchange.acknowledge().await?;
1305 Ok(SubscribeOutcome::Established(SubscribeEstablished {
1306 subscription_id,
1307 max_int,
1308 }))
1309 } else if opcode == OpCode::StatusResponse as u8 {
1310 // Peer aborted the establishment after the last
1311 // priming chunk — e.g. ran out of subscription
1312 // slots. Translate the status into an Error.
1313 let status = {
1314 let rx = self.exchange.rx()?;
1315 StatusResp::from_tlv(&TLVElement::new(rx.payload()))?.status
1316 };
1317 self.exchange.acknowledge().await?;
1318 error!(
1319 "Subscribe establishment aborted: StatusResponse({:?})",
1320 status
1321 );
1322 Err(status.to_error_code().unwrap_or(ErrorCode::Failure).into())
1323 } else {
1324 Err(ErrorCode::InvalidOpcode.into())
1325 }
1326 }
1327 }
1328}
1329
1330/// What [`SubscribePrimingChunk::complete`] returns: either the
1331/// next priming chunk in the sequence, or the terminal
1332/// [`SubscribeEstablished`] carrying the negotiated subscription
1333/// identity.
1334#[derive(Debug)]
1335#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1336pub enum SubscribeOutcome<'a> {
1337 /// More priming `ReportData` chunks coming — process this one
1338 /// and call `complete()` again on it.
1339 NextChunk(SubscribePrimingChunk<'a>),
1340 /// Establishment complete: subscription is active on the peer.
1341 /// The exchange is no longer needed (it has been dropped); the
1342 /// `(fabric, peer_node_id, subscription_id)` triple identifies
1343 /// the subscription for any server-initiated future reports.
1344 Established(SubscribeEstablished),
1345}
1346
1347/// Result of a successful subscribe-establishment: the
1348/// subscription-identifier issued by the peer plus the maximum
1349/// reporting interval (seconds) the peer committed to.
1350#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
1351#[cfg_attr(feature = "defmt", derive(defmt::Format))]
1352pub struct SubscribeEstablished {
1353 /// Subscription identifier chosen by the peer (Matter Core spec).
1354 /// Combined with the accessing fabric and the peer
1355 /// node id, this is the lookup key for the active subscription.
1356 pub subscription_id: u32,
1357 /// Maximum reporting interval (seconds) the peer committed to.
1358 /// The peer MUST report no less frequently than this — see
1359 /// Matter Core spec. Use this to drive a watchdog if the
1360 /// caller wants to detect a silently-broken subscription.
1361 pub max_int: u16,
1362}
1363
1364// =====================================================================
1365// Module-private helpers shared by trait default impls.
1366//
1367// The IM-client trait below has default-impl methods that drive each
1368// IM transaction end-to-end. They share several response-loop bodies
1369// (chunked-response handling for read/invoke, single-response handling
1370// for write, the timed-request handshake, the abort path); those live
1371// here as freestanding `pub(crate)` fns rather than trait methods so
1372// that we don't have to expose them as required trait items the way
1373// trait inheritance would force.
1374// =====================================================================
1375
1376/// Send a timed-request handshake and wait for `StatusResponse(Success)`.
1377/// Used before timed writes/invokes.
1378async fn send_timed_request(exchange: &mut Exchange<'_>, timeout_ms: u16) -> Result<(), Error> {
1379 let req = TimedReq {
1380 timeout: timeout_ms,
1381 interaction_model_revision: Some(IM_REVISION),
1382 };
1383
1384 exchange
1385 .send_with(|_, wb| {
1386 req.to_tlv(&TagType::Anonymous, wb)?;
1387 Ok(Some(OpCode::TimedRequest.into()))
1388 })
1389 .await?;
1390
1391 exchange.recv_fetch().await?;
1392
1393 let rx = exchange.rx()?;
1394 check_opcode(rx.meta().proto_opcode, OpCode::StatusResponse)?;
1395
1396 let status_resp = StatusResp::from_tlv(&TLVElement::new(rx.payload()))?;
1397 if status_resp.status != IMStatusCode::Success {
1398 error!("TimedRequest failed with status: {:?}", status_resp.status);
1399 return Err(status_resp
1400 .status
1401 .to_error_code()
1402 .unwrap_or(ErrorCode::Failure)
1403 .into());
1404 }
1405
1406 Ok(())
1407}
1408
1409/// Abort a chunked transaction by sending `StatusResponse(Failure)`.
1410///
1411/// This tells the server we are not continuing the transaction, preventing
1412/// it from waiting indefinitely for the next `StatusResponse(Success)`.
1413async fn send_abort(exchange: &mut Exchange<'_>) -> Result<(), Error> {
1414 exchange
1415 .send_with(|_, wb| {
1416 StatusResp::write(wb, IMStatusCode::Failure)?;
1417 Ok(Some(OpCode::StatusResponse.into()))
1418 })
1419 .await
1420}
1421
1422/// Check that the received opcode matches the expected one.
1423fn check_opcode(received: u8, expected: OpCode) -> Result<(), Error> {
1424 if received != expected as u8 {
1425 error!(
1426 "Unexpected IM opcode: received {}, expected {:?}",
1427 received, expected
1428 );
1429 Err(ErrorCode::InvalidOpcode.into())
1430 } else {
1431 Ok(())
1432 }
1433}