Skip to main content

rs_matter/transport/
mrp.rs

1/*
2 *
3 *    Copyright (c) 2022-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
18use embassy_time::Instant;
19
20use crate::dm::clusters::basic_info::BasicInfoConfig;
21use crate::error::{Error, ErrorCode};
22
23use super::{plain_hdr::PlainHdr, proto_hdr::ProtoHdr};
24
25/// Emit an MRP-diagnostic log message.
26///
27/// A number of log call sites in the transport layer fire in normal
28/// operation on lossy channels — packet retransmissions, duplicate
29/// packet drops, retrans / ACK mismatches, orphaned or late packets.
30/// They are useful when investigating packet loss on Thread or Wi-Fi
31/// but are noise otherwise.
32///
33/// This macro:
34/// - expands to [`debug!`] by default — keeping this noise out of a
35///   typical `info`-level log,
36/// - expands to [`warn!`] when the `log-mrp` Cargo feature is
37///   enabled — making these events visible under any reasonable log
38///   filter (including the compile-time filters used by `defmt` and
39///   `esp-println` in MCU firmwares).
40///
41/// Terminal / genuinely-erroneous conditions caused by noise (e.g.
42/// "Too many retransmissions. Giving up") intentionally stay at
43/// [`error!`] and are not routed through this macro.
44macro_rules! mrp_log {
45    ($s:literal $(, $x:expr)* $(,)?) => {{
46        #[cfg(feature = "log-mrp")]
47        { warn!($s $(, $x)*); }
48        #[cfg(not(feature = "log-mrp"))]
49        { debug!($s $(, $x)*); }
50    }};
51}
52
53pub(crate) use mrp_log;
54
55//const MRP_STANDALONE_ACK_TIMEOUT_MS: u64 = 200;   // TODO: Use to pro-actively send ACKs
56const MRP_BASE_RETRY_INTERVAL_MS: u32 = 300;
57const MRP_MAX_TRANSMISSIONS: u16 = 10;
58const MRP_BACKOFF_THRESHOLD: u16 = 1;
59const MRP_BACKOFF_BASE: (u64, u64) = (16, 10); // 1.6
60const MRP_BACKOFF_JITTER: (u64, u64) = (25, 100); // 0.25
61const MRP_BACKOFF_MARGIN: (u64, u64) = (11, 10); // 1.1
62const MRP_JITTER_RAND_MAX: u8 = u8::MAX;
63
64/// Fallback for `MRP_SESSION_IDLE_INTERVAL` when neither the peer nor our
65/// own `BasicInfoConfig::sii` advertised a value. Matches the docstring
66/// default on [`BasicInfoConfig::sii`].
67const MRP_DEFAULT_IDLE_INTERVAL_MS: u32 = 5000;
68
69/// Fallback for `MRP_SESSION_ACTIVE_THRESHOLD` when the peer didn't
70/// advertise one. Matter Core spec default.
71const MRP_DEFAULT_ACTIVE_THRESHOLD_MS: u16 = 4000;
72
73/// Resolve the default peer-MRP timing for a freshly-created [`Session`]
74/// from our own [`BasicInfoConfig`] (Matter Core spec): a peer
75/// that never advertises `session_parameters` will be addressed using
76/// our own SAI / SII as a reasonable approximation. Returns
77/// `(active_interval_ms, idle_interval_ms, active_threshold_ms)`.
78///
79/// `Some(0)` is treated identically to `None` — an interval of zero
80/// would collapse the MRP backoff to a tight retransmit loop and is
81/// never a valid configuration, so the fallback constants are used
82/// instead.
83///
84/// [`Session`]: crate::transport::session::Session
85pub fn default_peer_mrp_params(dev_det: &BasicInfoConfig<'_>) -> (u32, u32, u16) {
86    (
87        dev_det
88            .sai
89            .filter(|&v| v > 0)
90            .unwrap_or(MRP_BASE_RETRY_INTERVAL_MS),
91        dev_det
92            .sii
93            .filter(|&v| v > 0)
94            .unwrap_or(MRP_DEFAULT_IDLE_INTERVAL_MS),
95        MRP_DEFAULT_ACTIVE_THRESHOLD_MS,
96    )
97}
98
99#[derive(Debug)]
100#[cfg_attr(feature = "defmt", derive(defmt::Format))]
101pub struct RetransEntry {
102    /// The retransmission delay interval in milliseconds
103    base_delay_interval_ms: u32,
104    // The msg counter that we are waiting to be acknowledged
105    msg_ctr: u32,
106    // The retransmission counter
107    counter: u16,
108}
109
110impl RetransEntry {
111    pub fn new(base_delay_interval_ms: Option<u32>, msg_ctr: u32) -> Self {
112        // Defence-in-depth: a future code path that bypasses the
113        // peer-side / dev_det-side zero filters must never collapse the
114        // backoff into a zero-delay retransmit loop. Treat `Some(0)`
115        // identically to `None`.
116        let base_delay_interval_ms = base_delay_interval_ms
117            .filter(|&v| v > 0)
118            .unwrap_or(MRP_BASE_RETRY_INTERVAL_MS);
119        Self {
120            base_delay_interval_ms,
121            msg_ctr,
122            counter: 0,
123        }
124    }
125
126    pub fn get_msg_ctr(&self) -> u32 {
127        self.msg_ctr
128    }
129
130    /// Return how much to delay before (re)transmitting the message
131    /// based on the number of re-transmissions so far
132    pub fn delay_ms(&self, jitter_rand: u8) -> u64 {
133        self.delay_ms_counter(self.counter, jitter_rand)
134    }
135
136    /// Maximum delay before giving up on retransmitting the message
137    pub fn max_delay_ms(&self) -> u64 {
138        self.delay_ms_counter(MRP_MAX_TRANSMISSIONS, MRP_JITTER_RAND_MAX)
139    }
140
141    /// Return how much to delay before (re)transmitting the message
142    /// based on the provided number of re-transmissions so far
143    pub fn delay_ms_counter(&self, counter: u16, jitter_rand: u8) -> u64 {
144        let mut delay =
145            self.base_delay_interval_ms as u64 * MRP_BACKOFF_MARGIN.0 / MRP_BACKOFF_MARGIN.1;
146
147        if counter > MRP_BACKOFF_THRESHOLD {
148            for _ in 0..counter - MRP_BACKOFF_THRESHOLD {
149                delay = delay * MRP_BACKOFF_BASE.0 / MRP_BACKOFF_BASE.1;
150            }
151        }
152
153        delay + (delay * jitter_rand as u64 * MRP_BACKOFF_JITTER.0) / (255 * MRP_BACKOFF_JITTER.1)
154    }
155
156    pub fn pre_send(&mut self, ctr: u32) -> Result<(), Error> {
157        if self.msg_ctr == ctr {
158            if self.counter < MRP_MAX_TRANSMISSIONS {
159                self.counter += 1;
160                Ok(())
161            } else {
162                Err(ErrorCode::TxTimeout.into())
163            }
164        } else {
165            // This indicates there was some existing entry for same sess-id/exch-id, which shouldn't happen
166            panic!("Previous retrans entry for this exchange already exists");
167        }
168    }
169}
170
171#[derive(Debug, Clone)]
172#[cfg_attr(feature = "defmt", derive(defmt::Format))]
173pub struct AckEntry {
174    // The msg counter that we should acknowledge
175    pub(crate) msg_ctr: u32,
176    // Whether the message was acknowledged at least once
177    pub(crate) acknowledged: bool,
178}
179
180impl AckEntry {
181    pub fn new(msg_ctr: u32) -> Result<Self, Error> {
182        Ok(Self {
183            msg_ctr,
184            acknowledged: false,
185        })
186    }
187
188    pub fn get_msg_ctr(&self) -> u32 {
189        self.msg_ctr
190    }
191}
192
193#[derive(Default, Debug)]
194#[cfg_attr(feature = "defmt", derive(defmt::Format))]
195pub struct ReliableMessage {
196    pub(crate) retrans: Option<RetransEntry>,
197    pub(crate) ack: Option<AckEntry>,
198    pub(crate) received_at: Option<Instant>,
199}
200
201impl ReliableMessage {
202    pub fn new() -> Self {
203        Default::default()
204    }
205
206    pub fn is_retrans_pending(&self) -> bool {
207        self.retrans.is_some()
208    }
209
210    pub fn is_ack_pending(&self) -> bool {
211        self.ack
212            .as_ref()
213            .map(|ack| !ack.acknowledged)
214            .unwrap_or(false)
215    }
216
217    pub fn has_rx_timed_out(&self, timeout_ms: u64) -> bool {
218        self.received_at
219            .map(|received_at| {
220                let deadline =
221                    received_at.saturating_add(embassy_time::Duration::from_millis(timeout_ms));
222                Instant::now() >= deadline
223            })
224            .unwrap_or(false)
225    }
226
227    pub fn pre_send(
228        &mut self,
229        tx_plain: &PlainHdr,
230        tx_proto: &mut ProtoHdr,
231        session_active_interval_ms: Option<u32>,
232        // TODO: Need to make use of it in future,
233        // once we detect idle vs active devices
234        _session_idle_interval_ms: Option<u32>,
235    ) -> Result<(), Error> {
236        // Check if any acknowledgements are pending for this exchange,
237        if let Some(ack) = &mut self.ack {
238            // if so, piggy back in the encoded header here
239            tx_proto.set_ack(Some(ack.get_msg_ctr()));
240            ack.acknowledged = true;
241        }
242
243        if tx_proto.is_reliable() {
244            if let Some(retrans) = &mut self.retrans {
245                if retrans.pre_send(tx_plain.ctr).is_err() {
246                    // Too many retransmissions, give up
247                    error!(
248                        "Packet {}{}: Too many retransmissions. Giving up",
249                        tx_plain, tx_proto
250                    );
251
252                    self.retrans = None;
253                    self.ack = None;
254                }
255            } else {
256                self.retrans = Some(RetransEntry::new(session_active_interval_ms, tx_plain.ctr));
257            }
258        }
259
260        self.received_at = None;
261
262        Ok(())
263    }
264
265    /// This method will update the state of the rentransmission and ACK tables
266    /// with the data from the incoming packet.
267    ///
268    /// The method will return `Ok` if the message needs to be processed by the
269    /// exchange layer, and an error if it needs to be dropped.
270    ///
271    /// A note about Message ACKs, it is a bit asymmetric in the sense that:
272    /// - there can be only one pending ACK per exchange (so this is per-exchange)
273    /// - there can be only one pending retransmission per exchange (so this is per-exchange)
274    /// - duplicate detection should happen per session (obviously), so that part is per-session
275    pub fn post_recv(&mut self, rx_plain: &PlainHdr, rx_proto: &ProtoHdr) -> Result<(), Error> {
276        if let Some(ack_msg_ctr) = rx_proto.get_ack() {
277            // Handle received Acks
278            if let Some(entry) = &self.retrans {
279                if entry.get_msg_ctr() != ack_msg_ctr {
280                    mrp_log!("Mismatch in retrans-table's msg counter and received msg counter: received {:x}, expected {:x}.", ack_msg_ctr, entry.msg_ctr);
281
282                    // This can actually happen on a noisy channel, where we've just sent a reply to a message
283                    // - yet - the other side is still retransmitting the original message and thus acknowledging
284                    // an earlier counter we've sent.
285
286                    // In this case, we should ignore the ACK and not process this message any further, as it is
287                    // a duplicate.
288                    Err(ErrorCode::Duplicate)?;
289                }
290
291                self.retrans = None;
292                self.ack = None;
293            }
294        }
295
296        if rx_proto.is_reliable() {
297            if let Some(ack) = &self.ack {
298                // This indicates there was some existing entry for same sess-id/exch-id, which shouldnt happen
299                // TODO: As per the spec if this happens, we need to send out the previous ACK and note this new ACK
300                error!(
301                    "Previous ACK entry {:x} for this exchange already exists",
302                    ack.get_msg_ctr()
303                );
304            }
305
306            self.ack = Some(AckEntry::new(rx_plain.ctr)?);
307        }
308
309        self.received_at = Some(Instant::now());
310
311        Ok(())
312    }
313}