Skip to main content

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}