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}