rs_matter/transport/network/btp.rs
1/*
2 *
3 * Copyright (c) 2024-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//! An implementation of the Matter BTP protocol over BLE, using GATT as the underlying transport.
19
20use core::future::Future;
21
22use embassy_futures::select::select;
23use embassy_time::{Instant, Timer};
24
25use session::{BTP_ACK_TIMEOUT_SECS, BTP_CONN_IDLE_TIMEOUT_SECS};
26
27use crate::error::{Error, ErrorCode};
28use crate::transport::network::btp::session::Session;
29use crate::transport::network::{Address, BtAddr, NetworkReceive, NetworkSend, MAX_TX_PACKET_SIZE};
30use crate::utils::cell::RefCell;
31use crate::utils::init::{init, Init};
32use crate::utils::storage::Vec;
33use crate::utils::sync::blocking::Mutex;
34use crate::utils::sync::Notification;
35
36pub use gatt::*;
37
38mod gatt;
39mod session;
40
41/// The maximum size of a BTP segment.
42pub(crate) const MAX_BTP_SEGMENT_SIZE: usize = 244;
43/// The size of the GATT header. `MAX_BTP_SEGMENT_SIZE` + `GATT_HEADER_SIZE` is 247 bytes, which is the maximum ATT MTU size supported by the BTP protocol.
44pub(crate) const GATT_HEADER_SIZE: usize = 3;
45
46/// The minimum MTU that can be used as per specification.
47pub(crate) const MIN_MTU: u16 = (20 + GATT_HEADER_SIZE) as u16;
48/// The maximum MTU that can be used as per specification.
49pub(crate) const MAX_MTU: u16 = (MAX_BTP_SEGMENT_SIZE + GATT_HEADER_SIZE) as u16;
50
51/// An implementation of the Matter BTP protocol.
52/// This is a low-level protocol that is used to send and receive Matter messages over BLE.
53///
54/// For the BTP protocol to function, it is expected that a GATT Peripheral (or a GATT Central, when rs-matter plays the Controller role)
55/// is setup in some OS-specific way by the user, where the GATT peripheral app is configured to contain the C1 and C2 characteristics
56/// on a service with the the Matter Service UUID.
57///
58/// The OS-specific GATT Peripheral or Central is supposed to call the following two methods:
59///
60/// - For the case where we take the GATT Peripheral role (i.e. rs-matter is the Accessory/Device, and BTP is initialized with `Btp::set_initiator(false)`):
61/// - `Btp::process_incoming` - when a GATT Write request is received on the C1 characteristic
62/// - `Btp::process_outgoing` - periodically and when `Btp::wait_outgoing` is notified, to check if there is any data to send to the peer
63/// The data is to be send via a GATT indication on the C2 characteristic.
64///
65/// - For the case where we take the GATT Central role (i.e. rs-matter is the Controller, and BTP is initialized with `Btp::set_initiator(true)`):
66/// - `Btp::process_incoming` - when a GATT indication is received on the C2 characteristic
67/// - `Btp::process_outgoing` - periodically and when `Btp::wait_outgoing` is notified, to check if there is any data to send to the peer
68/// The data is to be send via a GATT Write request on the C1 characteristic.
69pub struct Btp {
70 /// The inner state of the BTP protocol, containing the session state, the outgoing SDU buffer, and the timeouts configuration.
71 inner: Mutex<RefCell<BtpInner>>,
72 /// Notification triggered when (potentially!) a new Matter packet (BTP SDU) is assembled and available for processing.
73 recv_notif: Notification,
74 /// Notification triggered when (potentially!) there is now space for buffering a new outgoing Matter packet (BTP SDU).
75 send_notif: Notification,
76 /// Notification triggered when (potentially!) there is new outgoing data to be sent to the peer, which can be either a handshake packet or a BTP SDU segment.
77 outg_notif: Notification,
78}
79
80impl Btp {
81 /// Construct a new BTP instance.
82 #[inline(always)]
83 pub const fn new() -> Self {
84 Self {
85 inner: Mutex::new(RefCell::new(BtpInner::new())),
86 recv_notif: Notification::new(),
87 send_notif: Notification::new(),
88 outg_notif: Notification::new(),
89 }
90 }
91
92 /// Create an in-place initializer for a BTP instance.
93 pub fn init() -> impl Init<Self> {
94 init!(Self {
95 inner <- Mutex::init(RefCell::init(BtpInner::init())),
96 recv_notif <- Notification::init(),
97 send_notif <- Notification::init(),
98 outg_notif <- Notification::init(),
99 })
100 }
101
102 /// Reset the BTP state, by clearing all sessions and buffers, and resetting the timeouts to their default values.
103 pub fn reset(&self) {
104 self.inner.lock(|inner| {
105 inner.borrow_mut().reset();
106
107 self.recv_notif.notify();
108 self.send_notif.notify();
109 self.outg_notif.notify();
110 });
111 }
112
113 /// Set the relaxed MTU negotiation mode, which changes the behavior of the MTU negotiation in the handshake phase
114 /// when there is a mismatch between the GATT MTU and the peer-reported MTU.
115 pub fn set_relaxed_mtu_nego(&self, relaxed_mtu_nego: bool) {
116 self.inner
117 .lock(|inner| inner.borrow_mut().set_relaxed_mtu_nego(relaxed_mtu_nego));
118 }
119
120 /// Set the BTP role.
121 ///
122 /// - `false` (the default) - the GATT Peripheral role, i.e. rs-matter is the Accessory/Device.
123 /// The peer (Commissioner) initiates the BTP handshake by writing a Handshake Request on `C1`;
124 /// we respond with a Handshake Response indicated on `C2`.
125 /// - `true` - the GATT Central role, i.e. rs-matter is the Controller/Commissioner.
126 /// We initiate the BTP handshake ourselves by writing a Handshake Request on the peer's `C1`
127 /// characteristic (surfaced via `process_outgoing`), and the peer responds with a Handshake
128 /// Response, which we consume via `process_incoming` on the peer's `C2` indications.
129 ///
130 /// This must be called (with `true`) *before* the first `process_outgoing`/`process_incoming`
131 /// when acting as a Central, so that the pending initial handshake is emitted.
132 ///
133 /// See the [`Btp`] type documentation for the full Peripheral vs Central contract.
134 pub fn set_initiator(&self, initiator: bool) {
135 self.inner.lock(|inner| {
136 inner.borrow_mut().session.set_initiator(initiator);
137
138 // A freshly-flagged initiator has a handshake pending to emit.
139 self.outg_notif.notify();
140 });
141 }
142
143 /// Set the BTP timeouts, by configuring the ACK timeout and the connection idle timeout.
144 ///
145 /// Only used by the unit tests.
146 #[allow(dead_code)]
147 pub(crate) fn set_timeouts(&self, ack_timeout_secs: u8, conn_idle_timeout_secs: u8) {
148 self.inner.lock(|inner| {
149 inner
150 .borrow_mut()
151 .set_timeouts(ack_timeout_secs, conn_idle_timeout_secs);
152
153 self.recv_notif.notify();
154 self.send_notif.notify();
155 self.outg_notif.notify();
156 });
157 }
158
159 /// Check if the session has timed out due to inactivity (connection timeout).
160 pub fn timeout(&self) -> bool {
161 self.inner.lock(|inner| inner.borrow().timeout())
162 }
163
164 /// Wait until the session has timed out due to inactivity (connection timeout).
165 pub async fn wait_timeout(&self) {
166 // Check every 2 seconds
167 // The connection timeout should be at least 1 second, but in production
168 // use cases is anyway hard-coded to `BTP_CONN_IDLE_TIMEOUT_SECS`
169 //
170 // And even for testing, timeouting an extra second late is not a prob
171 const TIMEOUT_CHECK_SECS: u64 = 2;
172
173 while !self.timeout() {
174 Timer::after_secs(TIMEOUT_CHECK_SECS).await;
175 }
176 }
177
178 /// Process an incoming BLE packet
179 ///
180 /// This method is expected to be called by the OS-specific GATT Peripheral or Central when a packet is received from the peer.
181 ///
182 /// # Arguments
183 /// - `gatt_mtu`: the GATT MTU (if known) to be used for processing the incoming packet.
184 /// This is needed in order to properly process the BTP handshake packets, which contain the peer's supported MTU.
185 /// If `None` is provided, the processing will assume that the GATT MTU is unknown, and will use the minimum MTU for processing the handshake packets.
186 /// - `addr`: the address of the peer from where the packet originates
187 /// - `data`: the incoming packet data, which is expected to be the payload of a GATT Write request (when we are the Peripheral) or a GATT indication (when we are the Central)
188 pub fn process_incoming(
189 &self,
190 gatt_mtu: Option<u16>,
191 addr: BtAddr,
192 data: &[u8],
193 ) -> Result<(), Error> {
194 self.inner.lock(|inner| {
195 inner.borrow_mut().process_incoming(gatt_mtu, addr, data)?;
196
197 self.recv_notif.notify();
198 self.outg_notif.notify();
199
200 Ok(())
201 })
202 }
203
204 /// Process outgoing data and prepare it to be sent to the peer.
205 ///
206 /// This method is expected to be called by the OS-specific GATT Peripheral or Central periodically and when `Btp::wait_outgoing` is notified,
207 /// in order to check if there is any data to send to the peer.
208 ///
209 /// The data to be sent is expected to be sent by the OS-specific GATT Peripheral or Central via a GATT indication (when we are the Peripheral)
210 /// or a GATT Write request (when we are the Central).
211 ///
212 /// # Arguments
213 /// - `gatt_mtu`: the GATT MTU (if known) to be used for processing the outgoing packet.
214 /// This is needed in order to properly process the BTP handshake packets, which require to know the MTU in order to properly segment the outgoing data.
215 /// If `None` is provided, the processing will assume that the GATT MTU is unknown, and will use the minimum MTU for processing the handshake packets.
216 /// - `buf`: the buffer to be used for preparing the outgoing packet data, which is expected to be the payload of a GATT indication (when we are the Peripheral)
217 /// or a GATT Write request (when we are the Central). A size of 512 (max MTU) should be enough.
218 ///
219 /// # Returns
220 /// - `Ok(len)` if there is data to be sent to the peer, where `len` is the size of the prepared packet data to be sent.
221 /// The prepared packet data will be written to the provided `buf` buffer.
222 /// - `Ok(0)` if there is no data to be sent to the peer at the moment.
223 /// - `Err` if there was an error during processing the outgoing data.
224 pub fn process_outgoing(&self, gatt_mtu: Option<u16>, buf: &mut [u8]) -> Result<usize, Error> {
225 self.inner.lock(|inner| {
226 let mut inner = inner.borrow_mut();
227
228 let len = inner.process_outgoing(gatt_mtu, buf)?;
229
230 if inner.outgoing_sdu.buf.is_empty() {
231 self.send_notif.notify();
232 }
233
234 Ok(len)
235 })
236 }
237
238 /// Wait until there is at least one packet to be sent to the peer,
239 /// by waiting for a notification that is triggered when there is new outgoing data to be sent.
240 pub async fn wait_outgoing(&self) {
241 // Check every second
242 // The ack timeout should be at least 1 second, but in production
243 // use cases is anyway hard-coded to `BTP_ACK_TIMEOUT_SECS`
244 const ACK_TIMEOUT_CHECK_SECS: u64 = 1;
245
246 select(
247 self.outg_notif.wait(),
248 Timer::after_secs(ACK_TIMEOUT_CHECK_SECS),
249 )
250 .await;
251 }
252
253 /// Wait until there is at least one Matter (a.k.a. BTP SDU) packet available for consumption.
254 pub async fn wait_available(&self) -> Result<(), Error> {
255 loop {
256 let available = self.inner.lock(|inner| inner.borrow().available());
257
258 if available {
259 break;
260 }
261
262 self.recv_notif.wait().await;
263 }
264
265 Ok(())
266 }
267
268 /// Receive a Matter (a.k.a. BTP SDU) packet.
269 ///
270 /// If there is no packet available, this method will block asynchronously until a packet is available.
271 /// Returns the size of the received packet, as well as the address of the BLE peer from where the packet originates.
272 pub async fn recv(&self, buf: &mut [u8]) -> Result<(usize, BtAddr), Error> {
273 loop {
274 let result = self.inner.lock(|inner| {
275 let result = inner.borrow_mut().recv(buf)?;
276
277 if result.is_some() {
278 self.outg_notif.notify();
279 }
280
281 Ok::<_, Error>(result)
282 })?;
283
284 if let Some(result) = result {
285 break Ok(result);
286 }
287
288 self.recv_notif.wait().await;
289 }
290 }
291
292 /// Send a Matter (a.k.a. BTP SDU) packet to the specified BLE peer.
293 pub async fn send(&self, data: &[u8], addr: BtAddr) -> Result<(), Error> {
294 loop {
295 let sent = self.inner.lock(|inner| {
296 let sent = inner.borrow_mut().send(data, addr)?;
297
298 if sent {
299 self.outg_notif.notify();
300 }
301
302 Ok::<_, Error>(sent)
303 })?;
304
305 if sent {
306 break Ok(());
307 }
308
309 self.send_notif.wait().await;
310 }
311 }
312}
313
314impl Default for Btp {
315 fn default() -> Self {
316 Self::new()
317 }
318}
319
320impl NetworkSend for &Btp {
321 async fn send_to(&mut self, data: &[u8], addr: Address) -> Result<(), Error> {
322 (*self)
323 .send(data, addr.btp().ok_or(ErrorCode::NoNetworkInterface)?)
324 .await
325 }
326}
327
328impl NetworkReceive for &Btp {
329 fn wait_available(&mut self) -> impl Future<Output = Result<(), Error>> {
330 (*self).wait_available()
331 }
332
333 async fn recv_from(&mut self, buffer: &mut [u8]) -> Result<(usize, Address), Error> {
334 (*self)
335 .recv(buffer)
336 .await
337 .map(|(len, addr)| (len, Address::Btp(addr)))
338 }
339}
340
341impl NetworkSend for Btp {
342 async fn send_to(&mut self, data: &[u8], addr: Address) -> Result<(), Error> {
343 (&*self).send_to(data, addr).await
344 }
345}
346
347impl NetworkReceive for Btp {
348 fn wait_available(&mut self) -> impl Future<Output = Result<(), Error>> {
349 (*self).wait_available()
350 }
351
352 async fn recv_from(&mut self, buffer: &mut [u8]) -> Result<(usize, Address), Error> {
353 (&*self).recv_from(buffer).await
354 }
355}
356
357/// The inner state of the BTP protocol, containing the session state, the outgoing SDU buffer, and the timeouts configuration.
358struct BtpInner {
359 session: Session,
360 outgoing_sdu: OutgoingSdu,
361 ack_timeout_secs: u8,
362 conn_idle_timeout_secs: u8,
363}
364
365impl BtpInner {
366 /// Construct a new BtpInner instance with default values.
367 const fn new() -> Self {
368 Self {
369 session: Session::new(),
370 outgoing_sdu: OutgoingSdu::new(),
371 ack_timeout_secs: BTP_ACK_TIMEOUT_SECS,
372 conn_idle_timeout_secs: BTP_CONN_IDLE_TIMEOUT_SECS,
373 }
374 }
375
376 /// Create an in-place initializer for a BtpInner instance.
377 fn init() -> impl Init<Self> {
378 init!(Self {
379 session <- Session::init(),
380 outgoing_sdu <- OutgoingSdu::init(),
381 ack_timeout_secs: BTP_ACK_TIMEOUT_SECS,
382 conn_idle_timeout_secs: BTP_CONN_IDLE_TIMEOUT_SECS,
383 })
384 }
385
386 /// Reset the BtpInner state, by resetting the session, clearing the outgoing SDU buffer, and resetting the timeouts to their default values.
387 fn reset(&mut self) {
388 self.session.reset();
389 self.outgoing_sdu.reset();
390 self.ack_timeout_secs = BTP_ACK_TIMEOUT_SECS;
391 self.conn_idle_timeout_secs = BTP_CONN_IDLE_TIMEOUT_SECS;
392 }
393
394 /// Set the relaxed MTU negotiation mode, which changes the behavior of the MTU negotiation in the handshake phase
395 /// when there is a mismatch between the GATT MTU and the peer-reported MTU.
396 fn set_relaxed_mtu_nego(&mut self, relaxed_mtu_nego: bool) {
397 self.session.set_relaxed_mtu_nego(relaxed_mtu_nego);
398 }
399
400 /// Set the BTP timeouts, by configuring the ACK timeout and the connection idle timeout.
401 /// Only used by the unit tests.
402 fn set_timeouts(&mut self, ack_timeout_secs: u8, conn_idle_timeout_secs: u8) {
403 self.ack_timeout_secs = ack_timeout_secs;
404 self.conn_idle_timeout_secs = conn_idle_timeout_secs;
405 }
406
407 /// Process an incoming BLE packet
408 fn process_incoming(
409 &mut self,
410 gatt_mtu: Option<u16>,
411 addr: BtAddr,
412 data: &[u8],
413 ) -> Result<(), Error> {
414 self.session.process_rx(gatt_mtu, addr, data)
415 }
416
417 /// Process outgoing data and prepare it to be sent to the peer.
418 fn process_outgoing(&mut self, gatt_mtu: Option<u16>, buf: &mut [u8]) -> Result<usize, Error> {
419 let len = self.session.prep_tx_handshake(gatt_mtu, buf)?;
420 if len > 0 {
421 return Ok(len);
422 }
423
424 if !self.outgoing_sdu.buf.is_empty() {
425 if self.outgoing_sdu.address == self.session.address() {
426 let len = self.session.prep_tx_data(
427 &self.outgoing_sdu.buf,
428 &mut self.outgoing_sdu.buf_offset,
429 buf,
430 )?;
431 if len > 0 {
432 if self.outgoing_sdu.buf_offset == self.outgoing_sdu.buf.len() {
433 self.outgoing_sdu.reset();
434 }
435
436 return Ok(len);
437 }
438 } else if self.session.is_established() {
439 // The queued SDU is addressed to a *different* peer than the
440 // current session - it is stale, drop it.
441 //
442 // We must only do this once the session is established. As an
443 // initiator (GATT Central), an SDU (e.g. the PASE
444 // PBKDFParamRequest) can be queued *before* the BTP handshake
445 // completes, i.e. while the session address is still unset
446 // (all-zero). Dropping it then would silently lose the first
447 // Matter message and stall commissioning; instead we keep it
448 // queued until the handshake response establishes the session,
449 // and send it afterwards.
450 self.outgoing_sdu.reset();
451 }
452 }
453
454 if self
455 .session
456 .is_ack_due(Instant::now(), self.ack_timeout_secs as _)
457 {
458 let len = self.session.prep_tx_data(&[], &mut 0, buf)?;
459 assert!(len > 0);
460
461 return Ok(len);
462 }
463
464 Ok(0)
465 }
466
467 /// Check if there is at least one Matter (a.k.a. BTP SDU) packet available for consumption.
468 fn available(&self) -> bool {
469 self.session.message_available()
470 }
471
472 /// Receive a Matter (a.k.a. BTP SDU) packet.
473 ///
474 /// Returns the size of the received packet, as well as the address of the BLE peer from where the packet originates,
475 /// or 0 if there is no packet available for reception.
476 fn recv(&mut self, buf: &mut [u8]) -> Result<Option<(usize, BtAddr)>, Error> {
477 if self.session.message_available() {
478 let len = self.session.fetch_message(buf)?;
479
480 Ok(Some((len, self.session.address())))
481 } else {
482 Ok(None)
483 }
484 }
485
486 /// Send a Matter (a.k.a. BTP SDU) packet to the specified BLE peer.
487 ///
488 /// Returns `Ok(true)` if the packet was successfully buffered for sending,
489 /// `Ok(false)` if there is already an outgoing packet being buffered (i.e. the caller should retry later),
490 /// or an error if there was an error during buffering the packet for sending.
491 fn send(&mut self, data: &[u8], addr: BtAddr) -> Result<bool, Error> {
492 if data.is_empty() || data.len() > MAX_TX_PACKET_SIZE {
493 Err(ErrorCode::InvalidArgument.into())
494 } else if self.outgoing_sdu.buf.is_empty() {
495 self.outgoing_sdu.address = addr;
496 self.outgoing_sdu.buf_offset = 0;
497 unwrap!(self.outgoing_sdu.buf.extend_from_slice(data));
498
499 Ok(true)
500 } else {
501 Ok(false)
502 }
503 }
504
505 /// Check if the session has timed out due to inactivity (connection timeout).
506 fn timeout(&self) -> bool {
507 self.session
508 .is_timed_out(Instant::now(), self.conn_idle_timeout_secs as _)
509 }
510}
511
512/// The state of an outgoing BTP SDU, containing the peer address, the SDU data buffer, and the current offset in the buffer for sending.
513struct OutgoingSdu {
514 address: BtAddr,
515 buf: Vec<u8, MAX_TX_PACKET_SIZE>,
516 buf_offset: usize,
517}
518
519impl OutgoingSdu {
520 const fn new() -> Self {
521 Self {
522 address: BtAddr([0; 6]),
523 buf: Vec::new(),
524 buf_offset: 0,
525 }
526 }
527
528 fn init() -> impl Init<Self> {
529 init!(Self {
530 address: BtAddr([0; 6]),
531 buf <- crate::utils::storage::Vec::init(),
532 buf_offset: 0,
533 })
534 }
535
536 fn reset(&mut self) {
537 self.address = BtAddr([0; 6]);
538 self.buf.clear();
539 self.buf_offset = 0;
540 }
541}
542
543#[cfg(test)]
544mod test {
545 use super::*;
546
547 const PEER_ADDR: BtAddr = BtAddr([1, 2, 3, 4, 5, 6]);
548
549 fn incoming(btp: &Btp, data: &[u8]) {
550 incoming_mtu(btp, None, data)
551 }
552
553 fn expect_outgoing(btp: &Btp, data: &[u8]) {
554 expect_outgoing_mtu(btp, None, data)
555 }
556
557 /// Generate `GattPeripheralEvent::Write` event for the peer
558 fn incoming_mtu(btp: &Btp, gatt_mtu: Option<u16>, data: &[u8]) {
559 btp.process_incoming(gatt_mtu, PEER_ADDR, data).unwrap();
560 }
561
562 /// Expect to receive the provided data from the peer as if the BTP protocol
563 /// did call `indicate`
564 fn expect_outgoing_mtu(btp: &Btp, gatt_mtu: Option<u16>, data: &[u8]) {
565 let mut buf = [0; 512];
566
567 let len = btp.process_outgoing(gatt_mtu, &mut buf).unwrap();
568
569 assert_eq!(&buf[..len], data);
570 }
571
572 fn send(btp: &Btp, data: &[u8]) {
573 embassy_futures::block_on(btp.send(data, PEER_ADDR)).unwrap();
574 }
575
576 fn expect_recv(btp: &Btp, data: &[u8]) {
577 let mut buf = [0; 2048];
578
579 let (len, addr) = embassy_futures::block_on(btp.recv(&mut buf)).unwrap();
580
581 assert_eq!(addr, PEER_ADDR);
582 assert_eq!(&buf[..len], data);
583 }
584
585 #[test]
586 fn test_mtu_timeout() {
587 #[cfg(all(feature = "std", not(target_os = "espidf")))]
588 {
589 let _ = env_logger::try_init_from_env(
590 env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
591 );
592 }
593
594 let btp = Btp::new();
595 btp.set_timeouts(1, 2);
596
597 incoming_mtu(
598 &btp,
599 Some(0xc8),
600 &[0x65, 0x6c, 0x54, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x05],
601 );
602
603 // Expected MTU in response is 0xc8 - 3 = 0xc5
604 expect_outgoing(&btp, &[0x65, 0x6c, 0x05, 0xc5, 0x00, 0x05]);
605
606 embassy_futures::block_on(Timer::after_secs(3));
607
608 assert!(btp.timeout());
609
610 /////////////////////////////////
611
612 btp.reset();
613
614 // GATT MTU is unknown
615 incoming_mtu(
616 &btp,
617 None,
618 &[0x65, 0x6c, 0x54, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x05],
619 );
620
621 // Expected MTU is the minimum one (0x14)
622 expect_outgoing(&btp, &[0x65, 0x6c, 0x05, 0x14, 0x00, 0x05]);
623 }
624
625 // Utility to do the negotiation phase with a minumum MTU
626 fn nego_min_mtu() -> Btp {
627 let btp = Btp::new();
628
629 incoming(
630 &btp,
631 &[0x65, 0x6c, 0x54, 0x00, 0x00, 0x00, 0xc8, 0x00, 0x05],
632 );
633
634 // Peer window = 1 because of this handshake resp
635 expect_outgoing(&btp, &[0x65, 0x6c, 0x05, 0x14, 0x00, 0x05]);
636
637 btp
638 }
639
640 #[test]
641 fn test_short_read() {
642 let btp = nego_min_mtu();
643
644 send(&btp, &[0, 1, 2, 3]);
645
646 expect_outgoing(&btp, &[5, 1, 4, 0, 0, 1, 2, 3]);
647 }
648
649 #[test]
650 fn test_short_write() {
651 let btp = nego_min_mtu();
652
653 incoming(&btp, &[5, 0, 3, 0, 1, 2, 3]);
654
655 expect_recv(&btp, &[1, 2, 3]);
656 }
657
658 #[test]
659 fn test_long_read() {
660 let btp = nego_min_mtu();
661
662 send(&btp, &[0; 52]);
663
664 // Long msg beginning
665 expect_outgoing(
666 &btp,
667 &[1, 1, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
668 );
669
670 // Long msg continue
671 expect_outgoing(
672 &btp,
673 &[2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
674 );
675
676 // Long msg end
677 expect_outgoing(
678 &btp,
679 &[6, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
680 );
681 }
682
683 #[test]
684 fn test_long_write() {
685 let btp = nego_min_mtu();
686
687 // Beginning
688 incoming(
689 &btp,
690 &[
691 1, 0, 30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
692 ],
693 );
694
695 // End
696 incoming(
697 &btp,
698 &[4, 1, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
699 );
700
701 expect_recv(
702 &btp,
703 &[
704 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
705 24, 25, 26, 27, 28, 29, 30,
706 ],
707 );
708 }
709
710 #[test]
711 fn test_long_read_ack() {
712 let btp = nego_min_mtu();
713
714 // A short message, to pump up the ack window
715 send(&btp, &[0, 1, 2, 3]);
716
717 // Peer window = 2
718 expect_outgoing(&btp, &[5, 1, 4, 0, 0, 1, 2, 3]);
719
720 send(&btp, &[0; 100]);
721
722 // Long msg beginning
723 // Peer window = 3
724 expect_outgoing(
725 &btp,
726 &[1, 2, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
727 );
728
729 // Long msg continue
730 // Peer window = 4
731 expect_outgoing(
732 &btp,
733 &[2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
734 );
735
736 // Send ACK from the peer as its window is full by now (5 - 1) = 4
737 incoming(&btp, &[8, 3, 0]);
738
739 // Long msg end + ACK
740 // Peer window = 0, final packet
741 expect_outgoing(
742 &btp,
743 &[10, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
744 );
745 }
746
747 #[test]
748 fn test_long_write_ack() {
749 let btp = nego_min_mtu();
750
751 // Beginning
752 incoming(
753 &btp,
754 &[1, 0, 44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
755 );
756
757 // Continue
758 incoming(
759 &btp,
760 &[2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
761 );
762
763 // End
764 incoming(&btp, &[4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
765
766 expect_recv(&btp, &[0; 44]);
767 }
768
769 #[test]
770 fn test_idle_ping_pong() {
771 let btp = nego_min_mtu();
772
773 btp.set_timeouts(1, 10);
774
775 // The peripheral should send the first ACK for the handshake response
776 incoming(&btp, &[8, 0, 0]);
777
778 embassy_futures::block_on(Timer::after_secs(1));
779
780 // BTP should - in X seconds - ACK our message so that the session does not timeout
781 expect_outgoing(&btp, &[8, 0, 1]);
782
783 // The peripheral should ACK it
784 incoming(&btp, &[8, 1, 1]);
785
786 embassy_futures::block_on(Timer::after_secs(1));
787
788 // BTP should - in X seconds - ACK again
789 expect_outgoing(&btp, &[8, 1, 2]);
790 }
791
792 /// Pump any pending outgoing frame from `from` into `to`. Returns whether a
793 /// frame was moved.
794 fn pump(from: &Btp, from_addr: BtAddr, to: &Btp, gatt_mtu: Option<u16>) -> bool {
795 let mut buf = [0u8; 512];
796 let len = from.process_outgoing(gatt_mtu, &mut buf).unwrap();
797 if len == 0 {
798 return false;
799 }
800 to.process_incoming(gatt_mtu, from_addr, &buf[..len])
801 .unwrap();
802 true
803 }
804
805 /// End-to-end BTP handshake + first data packet between an **initiator**
806 /// (GATT Central) and a **responder** (GATT Peripheral), both in-process.
807 ///
808 /// This is the regression test for the controller-side (initiator) BTP path:
809 /// the initiator queues a Matter SDU *before* the handshake has completed
810 /// (exactly as a commissioner does when it sends the PASE PBKDFParamRequest),
811 /// and that SDU must survive the handshake and be delivered to the responder.
812 #[test]
813 fn test_initiator_handshake_then_data() {
814 const INITIATOR_ADDR: BtAddr = BtAddr([0xaa; 6]);
815 const RESPONDER_ADDR: BtAddr = BtAddr([0xbb; 6]);
816
817 let gatt_mtu = Some(0xc8);
818
819 let initiator = Btp::new();
820 initiator.set_initiator(true);
821
822 let responder = Btp::new();
823
824 // The commissioner queues its first Matter message (a PASE
825 // PBKDFParamRequest, here stubbed) *before* the BTP handshake completes.
826 let sdu: &[u8] = &[1, 2, 3, 4, 5];
827 send_to(&initiator, RESPONDER_ADDR, sdu);
828
829 // Handshake: initiator -> request -> responder.
830 assert!(pump(&initiator, INITIATOR_ADDR, &responder, gatt_mtu));
831 // Responder -> response -> initiator.
832 assert!(pump(&responder, RESPONDER_ADDR, &initiator, gatt_mtu));
833
834 // Now the initiator's queued SDU must flow to the responder. Pump every
835 // frame the initiator has to send (data segments + acks) into the
836 // responder until the responder can deliver the full SDU.
837 for _ in 0..8 {
838 if !pump(&initiator, INITIATOR_ADDR, &responder, gatt_mtu) {
839 break;
840 }
841 }
842
843 let mut buf = [0u8; 512];
844 let (len, addr) = embassy_futures::block_on(responder.recv(&mut buf)).unwrap();
845 assert_eq!(addr, INITIATOR_ADDR);
846 assert_eq!(&buf[..len], sdu);
847
848 // Now the reverse direction: the responder replies with its own SDU (as a
849 // device would with the PASE PBKDFParamResponse). This exercises the
850 // initiator's receive-sequence handling - the responder's Handshake
851 // Response was its seq 0, so its first data segment is seq 1, and the
852 // initiator must accept it rather than rejecting it as out-of-sequence.
853 let reply: &[u8] = &[9, 8, 7, 6];
854 send_to(&responder, INITIATOR_ADDR, reply);
855
856 for _ in 0..8 {
857 if !pump(&responder, RESPONDER_ADDR, &initiator, gatt_mtu) {
858 break;
859 }
860 }
861
862 let (len, addr) = embassy_futures::block_on(initiator.recv(&mut buf)).unwrap();
863 assert_eq!(addr, RESPONDER_ADDR);
864 assert_eq!(&buf[..len], reply);
865 }
866
867 fn send_to(btp: &Btp, addr: BtAddr, data: &[u8]) {
868 embassy_futures::block_on(btp.send(data, addr)).unwrap();
869 }
870
871 /// An initiator must survive a peer/stack reporting a nonsensically small GATT
872 /// MTU. BlueZ hands us whatever it has in `GattCharacteristic1.MTU`, and an MTU
873 /// below the BTP header size once left the proposed window size dividing by
874 /// zero - a panic on external input.
875 #[test]
876 fn test_initiator_handshake_tolerates_a_tiny_gatt_mtu() {
877 for gatt_mtu in [Some(0), Some(1), Some(3), Some(20), Some(0xFFFF), None] {
878 let btp = Btp::new();
879 btp.set_initiator(true);
880
881 let mut buf = [0u8; 512];
882 let len = btp
883 .process_outgoing(gatt_mtu, &mut buf)
884 .unwrap_or_else(|e| panic!("gatt_mtu {gatt_mtu:?} failed: {e:?}"));
885
886 // Whatever the peer claimed, we must emit a well-formed handshake
887 // request proposing a usable (non-zero) window.
888 assert!(len > 0, "gatt_mtu {gatt_mtu:?} produced no handshake");
889 }
890 }
891}