Skip to main content

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