Skip to main content

zigbee_mac/esp/
mod.rs

1use alloc::vec::Vec;
2
3use byte::BytesExt;
4use embassy_futures::select::Either;
5use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
6use embassy_sync::mutex::Mutex;
7use embassy_time::Timer;
8use esp_radio::ieee802154::Config;
9use esp_radio::ieee802154::Frame;
10use esp_radio::ieee802154::Ieee802154;
11use esp_radio::ieee802154::ReceivedFrame;
12use ieee802154::mac::Address;
13use ieee802154::mac::FrameContent;
14use ieee802154::mac::FrameType;
15use ieee802154::mac::FrameVersion;
16use ieee802154::mac::Header;
17use ieee802154::mac::PanId;
18use ieee802154::mac::command::CapabilityInformation;
19use ieee802154::mac::command::Command;
20use ieee802154::mac::security::SecurityContext;
21
22use crate::esp::driver::Ieee802154Driver;
23use crate::mlme::A_BASE_SUPER_FRAME_DURATION;
24use crate::mlme::A_MAX_FRAME_RETRIES;
25use crate::mlme::A_RESPONSE_WAIT_TIME;
26use crate::mlme::AssociationResponse;
27use crate::mlme::MAX_IEEE802154_CHANNELS;
28use crate::mlme::MacConfig;
29use crate::mlme::MacError;
30use crate::mlme::Mlme;
31use crate::mlme::PanDescriptor;
32use crate::mlme::PanDescriptorList;
33use crate::mlme::ScanResult;
34use crate::mlme::ScanType;
35
36mod driver;
37
38// higher-layer retries of the whole association handshake; aMaxFrameRetries
39// ack-retransmission already covers a lost request or poll, this is a safety
40// net for a parent that accepts the request but is slow to respond
41const ASSOCIATE_REQUEST_RETRIES: u8 = 3;
42
43// number of times the association response is polled per request attempt
44const ASSOCIATE_POLL_RETRIES: u8 = 5;
45
46// number of poll rounds per steady-state MLME-POLL before reporting no data
47const POLL_DATA_RETRIES: u8 = 5;
48
49/// ESP32-C6 [`Mlme`] implementation.
50///
51/// The radio is a single shared resource: the inner state is held behind an
52/// async mutex so the trait's `&self` methods can be driven concurrently from a
53/// receive task and a transmit path. The extended address is cached so it can
54/// be read without locking.
55pub struct EspMlme<'a> {
56    inner: Mutex<CriticalSectionRawMutex, EspMlmeInner<'a>>,
57    ieee_address: u64,
58}
59
60impl<'a> EspMlme<'a> {
61    pub fn new(ieee802154: Ieee802154<'a>, config: Config) -> Self {
62        // seed the MAC seq number randomly (7.2.1.2): a fixed start re-uses low
63        // numbers each reboot, which a parent's duplicate filter drops as stale
64        let inner = EspMlmeInner {
65            driver: Ieee802154Driver::new(ieee802154, config),
66            seq_number: (esp_hal::rng::Rng::new().random() & 0xff) as u8,
67        };
68        let ieee_address = inner.driver.ieee_address().0;
69        Self {
70            inner: Mutex::new(inner),
71            ieee_address,
72        }
73    }
74
75    /// The device's IEEE 802.15.4 extended (EUI-64) address.
76    pub fn ieee_address(&self) -> u64 {
77        self.ieee_address
78    }
79}
80
81struct EspMlmeInner<'a> {
82    driver: Ieee802154Driver<'a>,
83    seq_number: u8,
84}
85
86impl EspMlmeInner<'_> {
87    fn sequence_number(&mut self) -> u8 {
88        self.seq_number = self.seq_number.wrapping_add(1);
89        self.seq_number
90    }
91
92    // retransmits up to aMaxFrameRetries times if unacknowledged (IEEE 802.15.4
93    // 7.5.6.4); returns MacError::NoAck if every attempt goes unacknowledged
94    async fn transmit_acked(&mut self, frame: &[u8]) -> Result<(), MacError> {
95        for _ in 0..=A_MAX_FRAME_RETRIES {
96            match self.driver.transmit(frame).await {
97                Ok(()) if self.driver.last_tx_acked() => return Ok(()),
98                Ok(()) => {}
99                // no ack / channel busy: retransmit (7.5.6.4)
100                Err(MacError::TxFailed) => {}
101                // a lost radio interrupt: retry rather than wedge the caller
102                Err(MacError::TxTimeout) => log::warn!("[MLME] tx-done timeout, retrying"),
103                Err(e) => return Err(e),
104            }
105        }
106        Err(MacError::NoAck)
107    }
108
109    fn flush(&mut self) {
110        while self.driver.poll_received().is_some() {}
111    }
112
113    fn poll_frame(&mut self) -> Option<Result<ReceivedFrame, MacError>> {
114        self.driver
115            .poll_received()
116            .map(|r| r.map_err(MacError::RadioError))
117    }
118
119    async fn next_frame(&mut self) -> Result<ReceivedFrame, MacError> {
120        loop {
121            // wait for a fully-received frame before draining: draining re-issues
122            // RxStart and aborts a frame still on the air (dropped a fast ~2ms
123            // indirect response). signal is level-held, so drain until empty then reset
124            self.driver.wait_rx_available().await;
125            if let Some(result) = self.poll_frame() {
126                return result;
127            }
128            self.driver.reset_rx_signal();
129        }
130    }
131
132    fn try_drain(&mut self, buf: &mut [u8]) -> Result<Option<(usize, u8)>, MacError> {
133        while let Some(result) = self.poll_frame() {
134            if let Some(received) = copy_data_payload(result?, buf) {
135                return Ok(Some(received));
136            }
137        }
138        Ok(None)
139    }
140
141    fn beacon_request_frame(&mut self) -> [u8; 10] {
142        let seq_number = self.sequence_number();
143        [0x3, 0x8, seq_number, 0xff, 0xff, 0xff, 0xff, 0x7, 0x0, 0x0]
144    }
145
146    async fn scan_channel_active(
147        &mut self,
148        channel: u8,
149        duration: u8,
150    ) -> Result<Option<PanDescriptorList>, MacError> {
151        self.flush();
152        self.driver.update_driver_config(|config| {
153            config.promiscuous = false;
154            config.channel = channel;
155        });
156        self.driver.start_receive();
157
158        let frame = self.beacon_request_frame();
159        if let Err(e) = self.driver.transmit(&frame).await {
160            log::error!("[MLME-SCAN]: error transmitting beacon: {e}");
161        }
162
163        log::debug!("[MLME-SCAN] sent beacon frame to channel {channel}, waiting for messages...");
164
165        let delay_us: u64 = calculate_scan_duration_max_us(duration).into();
166        log::debug!("[MLME-SCAN] waiting for response for {delay_us}us");
167
168        let mut pds = Vec::new();
169        let deadline = Timer::after_micros(delay_us);
170        let mut deadline = core::pin::pin!(deadline);
171
172        loop {
173            match embassy_futures::select::select(&mut deadline, self.next_frame()).await {
174                Either::First(_) => break,
175                Either::Second(Ok(frame)) => {
176                    if let Some(pd) = self.parse_beacon(frame) {
177                        pds.push(pd);
178                    }
179                }
180                Either::Second(Err(_)) => continue,
181            }
182        }
183
184        Ok(Some(pds))
185    }
186
187    fn parse_beacon(&self, received: ReceivedFrame) -> Option<PanDescriptor> {
188        match received {
189            ReceivedFrame {
190                frame:
191                    Frame {
192                        header:
193                            hdr @ Header {
194                                source: Some(source),
195                                ..
196                            },
197                        content: FrameContent::Beacon(beacon_content),
198                        payload,
199                        ..
200                    },
201                channel,
202                lqi,
203                ..
204            } => {
205                log::debug!("[MLME-SCAN] received beacon frame on channel {channel}");
206
207                let zigbee_beacon = match payload.read_with(&mut 0, ()) {
208                    Ok(zb) => zb,
209                    Err(e) => {
210                        log::warn!("[MLME-SCAN] failed to parse zigbee beacon: {e:?}");
211                        return None;
212                    }
213                };
214
215                Some(PanDescriptor {
216                    channel,
217                    coord_addr_mode: match source {
218                        Address::Short(_, _) => 0x2,
219                        Address::Extended(_, _) => 0x3,
220                    },
221                    coord_pan_id: source.pan_id().0.into(),
222                    coord_address: source,
223                    superframe_spec: beacon_content.superframe_spec,
224                    link_quality: lqi,
225                    security_use: hdr.has_security(),
226                    zigbee_beacon,
227                })
228            }
229            other => {
230                log::debug!("[MLME-SCAN] received non-beacon frame: {other:?}");
231                None
232            }
233        }
234    }
235
236    // MAC data request command frame (IEEE 802.15.4 7.3.4). returns the buffer
237    // and on-air length (written bytes + 2 for the FCS the radio appends); the
238    // source address mode varies (extended before a short address is assigned,
239    // short after) so the length is not fixed — caller must transmit exactly
240    // &buf[..len], not the whole fixed-size array
241    fn data_request_frame(&mut self, dest: Address) -> Result<([u8; 20], usize), MacError> {
242        let seq = self.sequence_number();
243        let source = match self.driver.short_address() {
244            Some(short) => Address::Short(dest.pan_id(), ieee802154::mac::ShortAddress(short)),
245            None => Address::Extended(dest.pan_id(), self.driver.ieee_address()),
246        };
247        let frame_header = Header {
248            frame_type: FrameType::MacCommand,
249            frame_pending: false,
250            ack_request: true,
251            pan_id_compress: true,
252            seq_no_suppress: false,
253            ie_present: false,
254            version: FrameVersion::Ieee802154_2003,
255            seq,
256            destination: Some(dest),
257            source: Some(source),
258            auxiliary_security_header: None,
259        };
260        let frame_content = FrameContent::Command(Command::DataRequest);
261
262        let mut buf = [0u8; 20];
263        let offset = &mut 0;
264        buf.write_with(
265            offset,
266            frame_header,
267            &Some(&mut SecurityContext::no_security()),
268        )?;
269        buf.write_with(offset, frame_content, ())?;
270
271        Ok((buf, *offset + 2))
272    }
273
274    // listens up to timeout_us for a MAC association response, draining and
275    // discarding any other frame. does not flush, so a response already
276    // queued (a parent that sent it directly) is not lost
277    async fn recv_association_response(
278        &mut self,
279        timeout_us: u64,
280    ) -> Result<Option<AssociationResponse>, MacError> {
281        // wait ~4ms before each drain (> a frame's ~0.8ms air time): draining
282        // mid-reception re-issues RxStart and aborts the frame. time-based, not
283        // signal-based: the RX signal may not fire even when the frame is queued.
284        // no logging in the loop — the UART critical section stalls the RX ISR
285        let timeout = Timer::after_micros(timeout_us);
286        let receive = async {
287            loop {
288                Timer::after_micros(4000).await;
289                while let Some(result) = self.poll_frame() {
290                    if let ReceivedFrame {
291                        frame:
292                            Frame {
293                                content:
294                                    FrameContent::Command(Command::AssociationResponse(
295                                        short_addr,
296                                        status,
297                                    )),
298                                ..
299                            },
300                        ..
301                    } = result?
302                    {
303                        return Ok::<_, MacError>(AssociationResponse {
304                            device_address: zigbee_types::IeeeAddress(self.driver.ieee_address().0),
305                            association_address: zigbee_types::ShortAddress(short_addr.0),
306                            status,
307                        });
308                    }
309                }
310            }
311        };
312        match embassy_futures::select::select(timeout, receive).await {
313            Either::First(_) => Ok(None),
314            Either::Second(Ok(r)) => Ok(Some(r)),
315            Either::Second(Err(e)) => Err(e),
316        }
317    }
318
319    // an indirect (poll) response is always a unicast, so ambient broadcasts
320    // never shadow it
321    fn take_unicast_data(&mut self, buf: &mut [u8]) -> Result<Option<(usize, u8)>, MacError> {
322        while let Some(result) = self.poll_frame() {
323            let received = result?;
324            if matches!(
325                received.frame.header.destination,
326                Some(Address::Short(_, ieee802154::mac::ShortAddress(d))) if d >= 0xfff8
327            ) {
328                continue;
329            }
330            if let Some(data) = copy_data_payload(received, buf) {
331                return Ok(Some(data));
332            }
333        }
334        Ok(None)
335    }
336
337    // shares recv_association_response's timing discipline: waits ~4ms before
338    // each drain (the RX signal may not fire even when a frame is queued) and
339    // never logs in the loop (the UART critical section stalls the RX ISR)
340    async fn recv_data_response(
341        &mut self,
342        timeout_us: u64,
343        buf: &mut [u8],
344    ) -> Result<Option<(usize, u8)>, MacError> {
345        let timeout = Timer::after_micros(timeout_us);
346        let receive = async {
347            loop {
348                Timer::after_micros(4000).await;
349                if let Some(data) = self.take_unicast_data(buf)? {
350                    return Ok::<_, MacError>(Some(data));
351                }
352            }
353        };
354        match embassy_futures::select::select(timeout, receive).await {
355            Either::First(_) => Ok(None),
356            Either::Second(result) => result,
357        }
358    }
359
360    fn association_request_frame(
361        &mut self,
362        dest: Address,
363        src: Option<Address>,
364        capabilities: CapabilityInformation,
365    ) -> Result<[u8; 21], MacError> {
366        let seq = self.sequence_number();
367        let frame_header = Header {
368            frame_type: FrameType::MacCommand,
369            frame_pending: false,
370            ack_request: true,
371            pan_id_compress: false,
372            seq_no_suppress: false,
373            ie_present: false,
374            version: FrameVersion::Ieee802154_2003,
375            seq,
376            destination: Some(dest),
377            source: src,
378            auxiliary_security_header: None,
379        };
380        let frame_content = FrameContent::Command(Command::AssociationRequest(capabilities));
381
382        let mut buf = [0u8; 21];
383        let offset = &mut 0;
384        buf.write_with(
385            offset,
386            frame_header,
387            &Some(&mut SecurityContext::no_security()),
388        )?;
389        buf.write_with(offset, frame_content, ())?;
390
391        Ok(buf)
392    }
393}
394
395// non-data frames (commands, beacons, acks) yield None
396fn copy_data_payload(received: ReceivedFrame, buf: &mut [u8]) -> Option<(usize, u8)> {
397    let ReceivedFrame {
398        frame:
399            Frame {
400                content: FrameContent::Data,
401                payload,
402                ..
403            },
404        lqi,
405        ..
406    } = received
407    else {
408        return None;
409    };
410    let len = payload.len().min(buf.len());
411    buf[..len].copy_from_slice(&payload[..len]);
412    Some((len, lqi))
413}
414
415fn calculate_scan_duration_max_us(duration: u8) -> u32 {
416    // we assume a symbol period of 16us (QPSK, 2.4Ghz)
417    16 * A_BASE_SUPER_FRAME_DURATION * (2 * (duration as u32) + 1)
418}
419
420impl EspMlmeInner<'_> {
421    async fn scan_network(
422        &mut self,
423        scan_type: ScanType,
424        channels: core::ops::Range<u8>,
425        duration: u8,
426    ) -> Result<ScanResult, MacError> {
427        if !matches!(scan_type, ScanType::Active) {
428            return Err(MacError::InvalidScanParams);
429        }
430
431        log::debug!("[MLME-SCAN] start scan");
432
433        let mut pan_descriptor = Vec::new();
434        for c in channels {
435            if (c as usize) >= MAX_IEEE802154_CHANNELS {
436                continue;
437            }
438
439            match self.scan_channel_active(c, duration).await {
440                Ok(Some(mut pd)) => {
441                    pan_descriptor.append(&mut pd);
442                }
443                Err(e) => {
444                    log::error!("[MLME-SCAN] error on channel {c}: {e}");
445                }
446                _ => (),
447            }
448        }
449
450        log::debug!("[MLME-SCAN] success");
451
452        Ok(ScanResult {
453            scan_type,
454            pan_descriptor,
455        })
456    }
457
458    async fn associate(
459        &mut self,
460        channel: u8,
461        dest: Address,
462        capabilities: CapabilityInformation,
463    ) -> Result<AssociationResponse, MacError> {
464        // filter on our extended address: the association response is an indirect
465        // tx addressed to it (7.5.6.4). auto_ack_rx must stay on to ack it —
466        // promiscuous suppresses the ack and floods the RX queue with broadcasts
467        self.driver.update_driver_config(|config| {
468            *config = Default::default();
469            config.channel = channel;
470            config.pan_id = Some(dest.pan_id().0);
471            config.auto_ack_tx = true;
472            config.auto_ack_rx = true;
473            config.promiscuous = false;
474        });
475        // arm RX after the config change: otherwise general RX is off until a
476        // later TX triggers rx-on-when-idle and the response is missed
477        self.driver.start_receive();
478
479        let ext_addr = self.driver.ieee_address();
480        // 7.3.1.1: source PAN is the broadcast PAN (0xffff), source is the ext addr
481        let src = Address::Extended(PanId::broadcast(), ext_addr);
482        let timeout_us = (A_RESPONSE_WAIT_TIME as u64) * 16;
483
484        // retry the full handshake (7.5.3.1): a lost/unacked request leaves the
485        // parent with nothing to buffer, so re-send each round. within a round
486        // listen first (rx-on-when-idle parent replies directly) then poll for
487        // indirect delivery. never flush — a direct response may already be queued
488        let mut response = None;
489        'association: for _ in 0..ASSOCIATE_REQUEST_RETRIES {
490            let frame = self.association_request_frame(dest, Some(src), capabilities)?;
491            match self.transmit_acked(&frame).await {
492                Ok(()) => {}
493                Err(MacError::NoAck) => {
494                    log::debug!("[MLME-ASSOCIATE] request not acked, retrying");
495                    continue;
496                }
497                Err(e) => return Err(e),
498            }
499            log::debug!(
500                "[MLME-ASSOCIATE] request acked, ack_pending={:?}",
501                self.driver.last_ack_frame_pending()
502            );
503
504            // catch a directly-sent response before spending a poll round-trip
505            if let Some(r) = self.recv_association_response(timeout_us).await? {
506                response = Some(r);
507                break 'association;
508            }
509
510            for _ in 0..ASSOCIATE_POLL_RETRIES {
511                let (data_req, len) = self.data_request_frame(dest)?;
512                match self.transmit_acked(&data_req[..len]).await {
513                    Ok(()) | Err(MacError::NoAck) => {}
514                    Err(e) => return Err(e),
515                }
516                // arm RX for the indirect response (~2-3ms after the poll ack);
517                // start_receive is a no-op if already receiving. no log here — the
518                // UART critical section stalls the RX ISR and a TI parent replies once
519                self.driver.start_receive();
520                if let Some(r) = self.recv_association_response(timeout_us).await? {
521                    response = Some(r);
522                    break 'association;
523                }
524            }
525        }
526        let response = response.ok_or(MacError::NoData)?;
527
528        log::debug!(
529            "[MLME-ASSOCIATE] success, short_addr={:?}",
530            response.association_address
531        );
532
533        // set the assigned short address so the hw filter accepts our unicasts
534        let short = response.association_address.0;
535        self.driver.update_driver_config(|config| {
536            config.promiscuous = false;
537            config.short_addr = Some(short);
538        });
539
540        Ok(response)
541    }
542
543    async fn poll_data(
544        &mut self,
545        coord_address: Address,
546        buf: &mut [u8],
547    ) -> Result<(usize, u8), MacError> {
548        // a response elicited by an earlier poll may have landed after its
549        // listen window closed; deliver it instead of flushing it away
550        if let Some((len, lqi)) = self.take_unicast_data(buf)? {
551            log::debug!("[MLME-POLL] rx buffered data len={len}");
552            return Ok((len, lqi));
553        }
554        let timeout_us = (A_RESPONSE_WAIT_TIME as u64) * 16;
555
556        // retry the poll handshake (7.5.6.3)
557        let mut acked = false;
558        for _ in 0..POLL_DATA_RETRIES {
559            let (data_req, len) = self.data_request_frame(coord_address)?;
560            match self.transmit_acked(&data_req[..len]).await {
561                // no ack after retries: the parent may be busy; keep listening
562                Ok(()) => acked = true,
563                Err(MacError::NoAck) => {}
564                Err(e) => return Err(e),
565            }
566            // arm RX for the indirect response (~2-3ms after the poll ack);
567            // start_receive is a no-op if already receiving
568            self.driver.start_receive();
569            if let Some((len, lqi)) = self.recv_data_response(timeout_us, buf).await? {
570                log::debug!("[MLME-POLL] rx data len={len}");
571                return Ok((len, lqi));
572            }
573        }
574        // an unacknowledged poll says nothing about buffered data: the parent
575        // itself is unreachable, which the NWK layer treats as a missed
576        // keepalive (3.6.10.3)
577        if acked {
578            Err(MacError::NoData)
579        } else {
580            Err(MacError::NoAck)
581        }
582    }
583
584    async fn transmit_data(&mut self, dest: Address, payload: &[u8]) -> Result<(), MacError> {
585        let seq = self.sequence_number();
586
587        // NWK broadcast addresses (0xfff8-0xffff) map to the MAC broadcast
588        // address 0xffff, which is never acknowledged (IEEE 802.15.4 7.2.1.1.2)
589        let is_broadcast = matches!(dest, Address::Short(_, sa) if sa.0 >= 0xfff8);
590        let dest = if is_broadcast {
591            Address::Short(dest.pan_id(), ieee802154::mac::ShortAddress(0xffff))
592        } else {
593            dest
594        };
595
596        let source = Some(match self.driver.short_address() {
597            Some(short) => Address::Short(dest.pan_id(), ieee802154::mac::ShortAddress(short)),
598            None => Address::Extended(dest.pan_id(), self.driver.ieee_address()),
599        });
600
601        let frame_header = Header {
602            frame_type: FrameType::Data,
603            frame_pending: false,
604            ack_request: !is_broadcast,
605            pan_id_compress: source.is_some(),
606            seq_no_suppress: false,
607            ie_present: false,
608            version: FrameVersion::Ieee802154_2003,
609            seq,
610            destination: Some(dest),
611            source,
612            auxiliary_security_header: None,
613        };
614
615        let mut frame_buf = [0u8; 127];
616        let offset = &mut 0;
617        frame_buf.write_with(
618            offset,
619            frame_header,
620            &Some(&mut SecurityContext::no_security()),
621        )?;
622        let hdr_len = *offset;
623        let payload_len = payload.len().min(frame_buf.len() - hdr_len - 2);
624        frame_buf[hdr_len..hdr_len + payload_len].copy_from_slice(&payload[..payload_len]);
625        // 2-byte FCS placeholder (IEEE 802.15.4 7.2.1.8) — the hardware
626        // computes the actual CRC-16 over the frame and overwrites these
627        // bytes during transmission
628        let total_len = hdr_len + payload_len + 2;
629
630        // retransmit unicasts per 7.5.6.4: a single CCA-busy or lost ack must
631        // not drop a ZDO/APS response — the coordinator treats the silence as
632        // an interview failure
633        if is_broadcast {
634            self.driver.transmit(&frame_buf[..total_len]).await?;
635        } else {
636            self.transmit_acked(&frame_buf[..total_len]).await?;
637        }
638        log::debug!("[MLME] tx data, len={total_len}");
639
640        Ok(())
641    }
642}
643
644impl Mlme for EspMlme<'_> {
645    fn ieee_address(&self) -> zigbee_types::IeeeAddress {
646        zigbee_types::IeeeAddress(self.ieee_address)
647    }
648
649    async fn configure(&self, config: MacConfig) {
650        let mut inner = self.inner.lock().await;
651        inner.driver.update_driver_config(|driver| {
652            if let Some(channel) = config.channel {
653                driver.channel = channel;
654            }
655            if let Some(pan_id) = config.pan_id {
656                driver.pan_id = Some(pan_id.0);
657            }
658            if let Some(short_address) = config.short_address {
659                driver.short_addr = Some(short_address.0);
660            }
661            if let Some(promiscuous) = config.promiscuous {
662                driver.promiscuous = promiscuous;
663            }
664            if let Some(auto_ack_rx) = config.auto_ack_rx {
665                driver.auto_ack_rx = auto_ack_rx;
666            }
667            if let Some(auto_ack_tx) = config.auto_ack_tx {
668                driver.auto_ack_tx = auto_ack_tx;
669            }
670        });
671        // arm RX for the new configuration: otherwise reception stays off
672        // until the next transmit triggers rx-on-when-idle
673        inner.driver.start_receive();
674    }
675
676    async fn scan_network(
677        &self,
678        ty: ScanType,
679        channels: core::ops::Range<u8>,
680        duration: u8,
681    ) -> Result<ScanResult, MacError> {
682        self.inner
683            .lock()
684            .await
685            .scan_network(ty, channels, duration)
686            .await
687    }
688
689    async fn associate(
690        &self,
691        channel: u8,
692        dest: Address,
693        capabilities: CapabilityInformation,
694    ) -> Result<AssociationResponse, MacError> {
695        self.inner
696            .lock()
697            .await
698            .associate(channel, dest, capabilities)
699            .await
700    }
701
702    async fn poll_data(
703        &self,
704        coord_address: Address,
705        buf: &mut [u8],
706    ) -> Result<(usize, u8), MacError> {
707        self.inner.lock().await.poll_data(coord_address, buf).await
708    }
709
710    async fn receive(&self, buf: &mut [u8]) -> Result<(usize, u8), MacError> {
711        loop {
712            // drain under a brief lock, then idle-wait lock-free so a concurrent
713            // transmit can acquire the radio while we wait for the next frame
714            {
715                let mut inner = self.inner.lock().await;
716                if let Some(received) = inner.try_drain(buf)? {
717                    return Ok(received);
718                }
719            }
720            driver::wait_rx_signal().await;
721        }
722    }
723
724    async fn transmit_data(&self, dest: Address, payload: &[u8]) -> Result<(), MacError> {
725        self.inner.lock().await.transmit_data(dest, payload).await
726    }
727}