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}