Skip to main content

zigbee_core/nwk/nlme/
mod.rs

1//! Network Management Entity
2//!
3//! The NLME shall provide a management service to allow an application to
4//! interact with the stack.
5//!
6//! it provides:
7//! * configuring a new device
8//! * starting a network
9//! * joining, rejoining and leaving a network
10//! * addressing
11//! * neighbor discovery
12//! * route discovery
13//! * reception control
14//! * routing
15
16use core::slice;
17use core::sync::atomic::AtomicU8;
18use core::sync::atomic::AtomicU32;
19use core::sync::atomic::Ordering;
20
21use byte::BytesExt;
22use byte::TryRead;
23use management::NlmeEdScanConfirm;
24use management::NlmeEdScanRequest;
25use management::NlmeJoinConfirm;
26use management::NlmeJoinRequest;
27use management::NlmeJoinStatus;
28use management::NlmeLeaveConfirm;
29use management::NlmeLeaveIndication;
30use management::NlmeLeaveRequest;
31use management::NlmeLeaveStatus;
32use management::NlmeNetworkDiscoveryConfirm;
33use management::NlmeNetworkFormationConfirm;
34use management::NlmeNetworkFormationRequest;
35use management::NlmeNwkStatusIndication;
36use management::NlmePermitJoiningConfirm;
37use management::NlmePermitJoiningRequest;
38use management::NlmeStartRouterConfirm;
39use management::NlmeStartRouterRequest;
40use management::RejoinNetwork;
41use thiserror::Error;
42use zigbee_mac::Address;
43use zigbee_mac::AssociationStatus;
44use zigbee_mac::MacShortAddress;
45use zigbee_mac::PanId;
46use zigbee_mac::mlme::MacConfig;
47use zigbee_mac::mlme::MacError;
48use zigbee_mac::mlme::Mlme;
49use zigbee_mac::mlme::ScanType;
50use zigbee_types::IeeeAddress;
51use zigbee_types::ShortAddress;
52use zigbee_types::StorageVec;
53use zigbee_types::sync::Signal;
54use zigbee_types::sync::with_timeout;
55
56use crate::nwk::frame::CommandFrame as NwkCommandFrame;
57use crate::nwk::frame::DataFrame as NwkDataFrame;
58use crate::nwk::frame::Frame as NwkFrame;
59use crate::nwk::frame::command::Command;
60use crate::nwk::frame::command::end_device_timeout_request;
61use crate::nwk::frame::command::end_device_timeout_request::EndDeviceTimeoutRequest;
62use crate::nwk::frame::command::end_device_timeout_response::EndDeviceTimeoutResponse;
63use crate::nwk::frame::command::leave::CommandOptions as LeaveCommandOptions;
64use crate::nwk::frame::command::leave::Leave;
65use crate::nwk::frame::command::network_status::NetworkStatusCode;
66use crate::nwk::frame::command::rejoin_request;
67use crate::nwk::frame::command::rejoin_request::RejoinRequest;
68use crate::nwk::frame::command::rejoin_response::RejoinResponse;
69use crate::nwk::frame::frame_control::DiscoverRoute;
70use crate::nwk::frame::frame_control::FrameControl as NwkFrameControl;
71use crate::nwk::frame::frame_control::FrameType as NwkFrameType;
72use crate::nwk::frame::header::Header as NwkHeader;
73use crate::nwk::nib;
74use crate::nwk::nib::CapabilityInformation;
75use crate::nwk::nib::DeviceType;
76use crate::nwk::nib::MAX_PARENT_LINK_COST;
77use crate::nwk::nib::NWK_BROADCAST_ADDRESS_MIN;
78use crate::nwk::nib::NWK_BROADCAST_ALL;
79use crate::nwk::nib::NWK_COORDINATOR_ADDRESS;
80use crate::nwk::nib::NWK_UNASSIGNED_ADDRESS;
81use crate::nwk::nib::Nib;
82use crate::nwk::nib::NwkNeighbor;
83use crate::nwk::nib::link_cost_from_lqi;
84use crate::nwk::nib::relationship;
85use crate::security::SecurityContext;
86
87/// Network management entity
88pub mod management;
89
90// sentinel for `pending_timeout_request`: no request outstanding
91const NO_PENDING_TIMEOUT: u8 = 0xff;
92
93// poll attempts while waiting for the parent to deliver a buffered Rejoin
94// Response (3.4.7.1: indirect transmission for a sleepy child)
95const REJOIN_RESPONSE_POLL_RETRIES: u8 = 20;
96
97// poll attempts covering macResponseWaitTime while waiting for the keepalive's
98// End Device Timeout Response (3.6.10.3)
99const TIMEOUT_RESPONSE_POLL_RETRIES: u8 = 3;
100
101// `parent_timeout_remaining_ms` value meaning "not tracking": either no
102// timeout was negotiated or it already expired
103const PARENT_TIMEOUT_INACTIVE: u32 = 0;
104
105// consecutive transmit failures classifying the link to the parent as failed
106// (3.6.3.7 leaves the counting scheme to the implementer)
107const MAX_PARENT_TRANSMIT_FAILURES: u8 = 3;
108
109#[derive(Debug, Error)]
110pub enum NetworkError {
111    #[error("mac error: {0}")]
112    MacError(#[from] MacError),
113    #[error("not joined to a network")]
114    NotJoined,
115    #[error("no transport key received from coordinator")]
116    NoTransportKey,
117    #[error("frame parse error")]
118    ParseError,
119    #[error("invalid frame")]
120    InvalidFrame,
121    #[error("frame does not fit a single APS frame")]
122    FrameTooLong,
123    #[error("no APS acknowledgement received")]
124    AckTimeout,
125    #[error("parent link failure")]
126    ParentLinkFailure,
127    #[error("security error: {0}")]
128    SecurityError(#[from] crate::security::SecurityError),
129}
130
131impl From<byte::Error> for NetworkError {
132    fn from(_: byte::Error) -> Self {
133        Self::ParseError
134    }
135}
136
137/// Network Layer Management Entity (3.2.2).
138///
139/// Provides the management service access point (NLME-SAP) that allows
140/// the next higher layer to interact with the NWK layer: network
141/// discovery, formation, joining, rejoining, data transmission, etc.
142pub struct Nlme<M> {
143    mac: M,
144    nwk_seq: AtomicU8,
145    // requested timeout enum awaiting a response (3.6.10.2); 0xff = none
146    pending_timeout_request: AtomicU8,
147    // remaining time before the parent is assumed to have aged this device out
148    // (3.6.10.6); the parent's neighbor entry is the only one an end device
149    // tracks, and the counter is volatile, so it lives here rather than in the
150    // flash-backed neighbor table
151    parent_timeout_remaining_ms: AtomicU32,
152    // Rejoin Response (3.4.7) handed from the receive path to the rejoin
153    // procedure waiting for it
154    rejoin_response: Signal<RejoinResponse>,
155    // End Device Timeout Response (3.4.12) handed from the receive path to the
156    // keepalive waiting for it
157    timeout_response: Signal<EndDeviceTimeoutResponse>,
158    // NLME-LEAVE.indication (3.2.2.19) raised by the receive path for the
159    // higher layer, which decides whether to re-commission or rejoin
160    leave_indication: Signal<NlmeLeaveIndication>,
161    // NLME-NWK-STATUS.indication (3.2.2.32) reporting a network failure to the
162    // higher layer
163    nwk_status_indication: Signal<NlmeNwkStatusIndication>,
164}
165
166/// Keepalive method negotiated with the router parent (3.6.10.3).
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub enum KeepaliveMethod {
169    /// The parent tracks a MAC data poll as a keepalive
170    /// (`nwkParentInformation` bit 0).
171    MacDataPoll,
172    /// The parent expects an End Device Timeout Request
173    /// (`nwkParentInformation` bit 1).
174    TimeoutRequest,
175    /// No timeout was negotiated: the parent ages this device out on its own
176    /// terms and no keepalive is sent.
177    None,
178}
179
180impl<M> Nlme<M>
181where
182    M: Mlme,
183{
184    /// Creates a new instance owning the given MAC.
185    ///
186    /// Mirrors the MAC's hardware-provisioned IEEE address into
187    /// `nwkIeeeAddress`, so it is valid even when resuming on a network
188    /// without re-associating. The information bases must be initialized
189    /// first.
190    pub fn new(mac: M) -> Self {
191        nib::get_ref().update_ieee_address(|value| *value = mac.ieee_address());
192        Self {
193            mac,
194            nwk_seq: AtomicU8::new(0),
195            pending_timeout_request: AtomicU8::new(NO_PENDING_TIMEOUT),
196            parent_timeout_remaining_ms: AtomicU32::new(PARENT_TIMEOUT_INACTIVE),
197            rejoin_response: Signal::new(),
198            timeout_response: Signal::new(),
199            leave_indication: Signal::new(),
200            nwk_status_indication: Signal::new(),
201        }
202    }
203
204    /// 3.2.2.19 - take a pending NLME-LEAVE.indication, if any.
205    ///
206    /// Reports that this device was removed from the network (device address
207    /// `None`), or that a neighbor left it. The indication stays pending until
208    /// taken.
209    pub fn take_leave_indication(&self) -> Option<NlmeLeaveIndication> {
210        self.leave_indication.try_take()
211    }
212
213    /// 3.2.2.19 - wait for the next NLME-LEAVE.indication.
214    pub async fn wait_leave_indication(&self) -> NlmeLeaveIndication {
215        self.leave_indication.wait().await
216    }
217
218    /// 3.2.2.32 - take a pending NLME-NWK-STATUS.indication, if any.
219    ///
220    /// Reports a network failure to the higher layer; the indication stays
221    /// pending until taken. A status of
222    /// [`NetworkStatusCode::ParentLinkFailure`] means the link to the parent
223    /// is gone and the device should rejoin (3.6.10.10).
224    pub fn take_nwk_status_indication(&self) -> Option<NlmeNwkStatusIndication> {
225        self.nwk_status_indication.try_take()
226    }
227
228    /// 3.2.2.32 - wait for the next NLME-NWK-STATUS.indication.
229    pub async fn wait_nwk_status_indication(&self) -> NlmeNwkStatusIndication {
230        self.nwk_status_indication.wait().await
231    }
232
233    fn next_nwk_seq(&self) -> u8 {
234        self.nwk_seq.fetch_add(1, Ordering::Relaxed).wrapping_add(1)
235    }
236
237    /// Build a NWK data frame into `buf`, returning the total frame length.
238    ///
239    /// When `secure` is true the frame is encrypted with the active
240    /// network key.
241    fn build_nwk_data_frame(
242        &self,
243        destination: ShortAddress,
244        secure: bool,
245        payload: &[u8],
246        buf: &mut [u8],
247    ) -> Result<usize, NetworkError> {
248        let nib = self.nib();
249        let frame_control = NwkFrameControl(0)
250            .set_frame_type(NwkFrameType::Data)
251            .set_protocol_version(2)
252            .set_discover_route(DiscoverRoute::Suppress)
253            .set_security_flag(secure);
254
255        let seq = self.next_nwk_seq();
256        let header = NwkHeader {
257            frame_control,
258            destination,
259            source: ShortAddress(*nib.network_address()),
260            radius: 30,
261            sequence_number: seq,
262            destination_ieee: None,
263            source_ieee: None,
264            multicast_control: None,
265            source_route_subframe: None,
266        };
267
268        if secure {
269            let nwk_frame = NwkFrame::Data(NwkDataFrame { header, payload });
270            let cx = SecurityContext::get();
271            let len = cx.encrypt_nwk_frame_in_place(nwk_frame, buf)?;
272            Ok(len)
273        } else {
274            let offset = &mut 0;
275            buf.write_with(offset, header, ())?;
276            let hdr_len = *offset;
277            let payload_len = payload.len().min(buf.len() - hdr_len);
278            buf[hdr_len..hdr_len + payload_len].copy_from_slice(&payload[..payload_len]);
279            Ok(hdr_len + payload_len)
280        }
281    }
282
283    /// Build a NWK command frame into `buf`, returning the total frame length
284    /// (3.3.2).
285    ///
286    /// When `secure` is false the frame is sent in the clear, as required for
287    /// a Trust Center rejoin (4.6.3.3.2).
288    fn build_nwk_command_frame(
289        &self,
290        destination: ShortAddress,
291        command: Command<'_>,
292        secure: bool,
293        buf: &mut [u8],
294    ) -> Result<usize, NetworkError> {
295        let nib = self.nib();
296        // the destination IEEE address is carried whenever it is known
297        // (3.4.6.2, 3.4.11.2)
298        let destination_ieee = self.neighbor_ieee_address(destination);
299        let frame_control = NwkFrameControl(0)
300            .set_frame_type(NwkFrameType::NwkCommand)
301            .set_protocol_version(2)
302            .set_discover_route(DiscoverRoute::Suppress)
303            .set_security_flag(secure)
304            .set_destination_ieee_flag(destination_ieee.is_some())
305            .set_source_ieee_flag(true);
306
307        let header = NwkHeader {
308            frame_control,
309            destination,
310            source: ShortAddress(*nib.network_address()),
311            radius: 1,
312            sequence_number: self.next_nwk_seq(),
313            destination_ieee,
314            source_ieee: Some(*nib.ieee_address()),
315            multicast_control: None,
316            source_route_subframe: None,
317        };
318
319        if !secure {
320            let offset = &mut 0;
321            buf.write_with(offset, header, ())?;
322            buf.write_with(offset, command, ())?;
323            return Ok(*offset);
324        }
325
326        let nwk_frame = NwkFrame::NwkCommand(NwkCommandFrame { header, command });
327        let cx = SecurityContext::get();
328        let len = cx.encrypt_nwk_frame_in_place(nwk_frame, buf)?;
329        Ok(len)
330    }
331
332    /// Build a failed NLME-JOIN.confirm (3.2.2.13.3).
333    fn join_failure(status: NlmeJoinStatus) -> NlmeJoinConfirm {
334        NlmeJoinConfirm {
335            status,
336            network_address: ShortAddress(0xffff),
337            extended_pan_id: IeeeAddress(0u64),
338            channel: 0,
339            enhanced_beacon_type: false,
340            mac_interface_index: 0u8,
341        }
342    }
343
344    /// Send an End Device Timeout Request to the parent (3.6.10.2).
345    ///
346    /// Requests the timeout enumeration from `nwkEndDeviceTimeoutDefault`.
347    /// The parent's response is consumed by the receive path and updates
348    /// `nwkParentInformation` and the negotiated timeout in the NIB; no
349    /// response means a legacy parent and leaves the NIB untouched.
350    pub async fn send_end_device_timeout_request(&self) -> Result<(), NetworkError> {
351        let requested_timeout = *self.nib().end_device_timeout_default();
352        let command = Command::EndDeviceTimeoutRequest(EndDeviceTimeoutRequest {
353            requested_timeout,
354            // all bits reserved, must be 0 (3.4.11.3.2)
355            end_device_configuration: 0x00,
356        });
357
358        let mut buf = [0u8; 256];
359        let len =
360            self.build_nwk_command_frame(self.parent_short_address()?, command, true, &mut buf)?;
361        self.pending_timeout_request
362            .store(requested_timeout, Ordering::Relaxed);
363        self.mac
364            .transmit_data(self.parent_address()?, &buf[..len])
365            .await?;
366        Ok(())
367    }
368
369    /// Program the MAC from the restored information base and report whether
370    /// this device is resuming on a network (3.6.8).
371    ///
372    /// A device that was reset while joined keeps its addresses in the NIB but
373    /// the radio comes up unconfigured: the PAN id, short address, channel and
374    /// the filtering that goes with membership have to be applied before it
375    /// can poll its parent again. A device that is not on a network only gets
376    /// the channel it will look for one on, leaving the rest to the join.
377    pub async fn resume(&self, channel: u8) -> bool {
378        let nib = self.nib();
379        let network_address = *nib.network_address();
380        let on_a_network = network_address != NWK_UNASSIGNED_ADDRESS;
381
382        let config = if on_a_network {
383            MacConfig::joined(channel, PanId(*nib.panid()), ShortAddress(network_address))
384        } else {
385            MacConfig::channel(channel)
386        };
387        self.mac.configure(config).await;
388
389        on_a_network
390    }
391
392    /// Negotiate the end-device timeout with the parent (3.6.10.2).
393    ///
394    /// Sends the request and waits for the parent's response, which stores the
395    /// negotiated timeout and the parent's keepalive methods in the NIB.
396    /// Returns whether a timeout was negotiated: a legacy parent never answers
397    /// and keeps its own default, leaving this device without a keepalive
398    /// method.
399    pub async fn negotiate_end_device_timeout(&self) -> Result<bool, NetworkError> {
400        // arm before transmitting: the response may be delivered by a
401        // concurrently running receive loop
402        self.timeout_response.reset();
403        self.send_end_device_timeout_request().await?;
404
405        let response = with_timeout(
406            self.timeout_response.wait(),
407            self.poll_until_timeout_response(TIMEOUT_RESPONSE_POLL_RETRIES),
408        )
409        .await
410        .or_else(|| self.timeout_response.try_take());
411
412        match response {
413            Some(response) if response.status == EndDeviceTimeoutResponse::STATUS_SUCCESS => {
414                Ok(true)
415            }
416            Some(response) => {
417                log::warn!(
418                    "[NWK] end-device timeout rejected (status={:#04x})",
419                    response.status
420                );
421                Ok(false)
422            }
423            None => {
424                log::warn!("[NWK] no end-device timeout response, assuming legacy parent");
425                Ok(false)
426            }
427        }
428    }
429
430    /// Recommended maximum poll interval in milliseconds, allowing three
431    /// keepalives per negotiated timeout period (3.6.10.3).
432    ///
433    /// Returns `None` when no timeout was negotiated or the parent does not
434    /// support the MAC data poll keepalive method.
435    pub fn negotiated_poll_interval_ms(&self) -> Option<u32> {
436        if self.keepalive_method() != KeepaliveMethod::MacDataPoll {
437            return None;
438        }
439        self.keepalive_interval_ms()
440    }
441
442    /// Keepalive method the parent expects (3.6.10.3).
443    ///
444    /// Bit 0 of `nwkParentInformation` takes precedence over bit 1; without a
445    /// negotiated timeout no keepalive is sent.
446    pub fn keepalive_method(&self) -> KeepaliveMethod {
447        let nib = self.nib();
448        if end_device_timeout_request::timeout_seconds(*nib.end_device_timeout()).is_none() {
449            return KeepaliveMethod::None;
450        }
451        let parent_information = *nib.parent_information();
452        if parent_information & EndDeviceTimeoutResponse::MAC_DATA_POLL_KEEPALIVE != 0 {
453            KeepaliveMethod::MacDataPoll
454        } else if parent_information & EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE != 0 {
455            KeepaliveMethod::TimeoutRequest
456        } else {
457            KeepaliveMethod::None
458        }
459    }
460
461    /// Recommended interval between keepalives in milliseconds: three
462    /// keepalives per negotiated Device Timeout period (3.6.10.3).
463    ///
464    /// Returns `None` when no timeout was negotiated with the parent.
465    pub fn keepalive_interval_ms(&self) -> Option<u32> {
466        Some(self.negotiated_timeout_ms()? / 3)
467    }
468
469    // negotiated Device Timeout period in milliseconds (Table 3.52)
470    fn negotiated_timeout_ms(&self) -> Option<u32> {
471        let seconds =
472            end_device_timeout_request::timeout_seconds(*self.nib().end_device_timeout())?;
473        Some(seconds.saturating_mul(1000))
474    }
475
476    /// Send one keepalive to the parent (3.6.10.3).
477    ///
478    /// The method follows `nwkParentInformation`: a MAC data poll, or an End
479    /// Device Timeout Request whose response is awaited. A failed timeout
480    /// request raises an NLME-NWK-STATUS.indication with
481    /// [`NetworkStatusCode::ParentLinkFailure`] and returns
482    /// [`NetworkError::ParentLinkFailure`]; a successful keepalive restarts
483    /// the local timeout (3.6.10.6).
484    pub async fn send_keepalive(&self) -> Result<(), NetworkError> {
485        match self.keepalive_method() {
486            KeepaliveMethod::MacDataPoll => self.mac_data_poll_keepalive().await,
487            KeepaliveMethod::TimeoutRequest => self.timeout_request_keepalive().await,
488            KeepaliveMethod::None => Ok(()),
489        }
490    }
491
492    // 3.6.10.3: a data poll refreshes the parent's timeout on its own; any
493    // frame it retrieves belongs to the receive loop, which is not running
494    // when the higher layer drives the keepalive itself
495    async fn mac_data_poll_keepalive(&self) -> Result<(), NetworkError> {
496        let mut buf = [0u8; 256];
497        match self.poll_nwk_frame(&mut buf).await {
498            Ok(Some(_)) => log::debug!("[NWK] keepalive poll dropped a data frame"),
499            Ok(None) => (),
500            Err(e) => return Err(e),
501        }
502        self.refresh_parent_timeout();
503        Ok(())
504    }
505
506    // 3.6.10.3: unicast an End Device Timeout Request and wait
507    // macResponseWaitTime for the response; anything else is a parent link
508    // failure
509    async fn timeout_request_keepalive(&self) -> Result<(), NetworkError> {
510        // arm before transmitting: the response may be delivered by a
511        // concurrently running receive loop
512        self.timeout_response.reset();
513        if let Err(e) = self.send_end_device_timeout_request().await {
514            log::warn!("[NWK] keepalive transmission failed ({e:?})");
515            return Err(self.parent_link_failure());
516        }
517
518        let response = with_timeout(
519            self.timeout_response.wait(),
520            self.poll_until_timeout_response(TIMEOUT_RESPONSE_POLL_RETRIES),
521        )
522        .await
523        // the response may have arrived in the very poll that ended the polling
524        .or_else(|| self.timeout_response.try_take());
525
526        match response {
527            Some(response) if response.status == EndDeviceTimeoutResponse::STATUS_SUCCESS => {
528                self.refresh_parent_timeout();
529                Ok(())
530            }
531            Some(response) => {
532                log::warn!("[NWK] keepalive rejected (status={:#04x})", response.status);
533                Err(self.parent_link_failure())
534            }
535            None => {
536                log::warn!("[NWK] no keepalive response from parent");
537                Err(self.parent_link_failure())
538            }
539        }
540    }
541
542    // poll the parent until the End Device Timeout Response has been processed
543    // or `retries` polls went unanswered; a sleepy child only receives the
544    // response by polling for it
545    async fn poll_until_timeout_response(&self, retries: u8) {
546        let mut buf = [0u8; 128];
547        for _ in 0..retries {
548            if self.timeout_response.is_signaled() {
549                return;
550            }
551            match self.poll_parent(&mut buf).await {
552                Ok(Some(len)) => match self.process_received_nwk_frame(&mut buf[..len]).await {
553                    Ok(Some(_)) => log::debug!("[NWK] keepalive poll dropped a data frame"),
554                    Ok(None) => (),
555                    Err(e) => log::debug!("[NWK] keepalive frame not processed: {e:?}"),
556                },
557                Ok(None) => (),
558                Err(e) => log::debug!("[NWK] keepalive poll failed: {e:?}"),
559            }
560        }
561    }
562
563    /// Restart the local end-device timeout after evidence that the parent
564    /// still holds this device in its neighbor table (3.6.10.6).
565    pub fn refresh_parent_timeout(&self) {
566        let remaining = self
567            .negotiated_timeout_ms()
568            .unwrap_or(PARENT_TIMEOUT_INACTIVE);
569        self.parent_timeout_remaining_ms
570            .store(remaining, Ordering::Relaxed);
571    }
572
573    /// Advance the local end-device timeout by `elapsed_ms` (3.6.10.6).
574    ///
575    /// Returns `true` once the timeout expires: the parent is assumed to have
576    /// aged this device out, which is reported to the higher layer as an
577    /// NLME-NWK-STATUS.indication with
578    /// [`NetworkStatusCode::ParentLinkFailure`]. Tracking then stops until the
579    /// next successful keepalive.
580    pub fn tick_parent_timeout(&self, elapsed_ms: u32) -> bool {
581        let remaining = self.parent_timeout_remaining_ms.load(Ordering::Relaxed);
582        if remaining == PARENT_TIMEOUT_INACTIVE {
583            return false;
584        }
585        let remaining = remaining.saturating_sub(elapsed_ms.max(1));
586        self.parent_timeout_remaining_ms
587            .store(remaining, Ordering::Relaxed);
588        if remaining != PARENT_TIMEOUT_INACTIVE {
589            return false;
590        }
591        log::warn!("[NWK] end-device timeout elapsed, assuming aged out by parent");
592        self.parent_link_failure();
593        true
594    }
595
596    /// Transmit a built frame through the parent, tracking the link
597    /// (3.6.3.7).
598    ///
599    /// Every neighbor with an outgoing link carries a failure counter; once
600    /// the link to the parent counts as failed, the higher layer is informed
601    /// with an NLME-NWK-STATUS.indication of `ParentLinkFailure` (3.6.3.7.1)
602    /// and [`NetworkError::ParentLinkFailure`] is returned.
603    async fn transmit_via_parent(&self, buf: &[u8]) -> Result<(), NetworkError> {
604        let parent = self.parent_address()?;
605        let Err(e) = self.mac.transmit_data(parent, buf).await else {
606            // an acknowledged frame proves the parent still holds this device
607            // in its neighbor table (3.6.10.6)
608            self.update_parent_neighbor(|neighbor| neighbor.transmit_failure = 0);
609            self.refresh_parent_timeout();
610            return Ok(());
611        };
612
613        let mut failures = 0;
614        self.update_parent_neighbor(|neighbor| {
615            neighbor.transmit_failure = neighbor.transmit_failure.saturating_add(1);
616            failures = neighbor.transmit_failure;
617        });
618        log::debug!("[NWK] transmission via parent failed ({e:?}), failures={failures}");
619        if failures < MAX_PARENT_TRANSMIT_FAILURES {
620            return Err(e.into());
621        }
622
623        self.update_parent_neighbor(|neighbor| neighbor.transmit_failure = 0);
624        Err(self.parent_link_failure())
625    }
626
627    // apply `update` to the parent's neighbor table entry, if there is one
628    fn update_parent_neighbor(&self, update: impl FnOnce(&mut NwkNeighbor)) {
629        self.nib().update_neighbor_table(|table| {
630            if let Some(parent) = table
631                .iter_mut()
632                .find(|n| n.relationship == relationship::PARENT)
633            {
634                update(parent);
635            }
636        });
637    }
638
639    // raise NLME-NWK-STATUS.indication 0x09 (3.6.3.7.1, 3.6.10.3, 3.2.2.32)
640    fn parent_link_failure(&self) -> NetworkError {
641        let network_address = self
642            .parent_short_address()
643            .unwrap_or(ShortAddress(NWK_UNASSIGNED_ADDRESS));
644        self.nwk_status_indication.signal(NlmeNwkStatusIndication {
645            status: NetworkStatusCode::ParentLinkFailure,
646            network_address,
647        });
648        NetworkError::ParentLinkFailure
649    }
650
651    /// Returns a reference to the global NIB singleton.
652    pub fn nib(&self) -> &'static Nib {
653        nib::get_ref()
654    }
655
656    /// Select parent candidates for an association join (3.6.1.4.1.1).
657    fn select_parent_candidates(
658        &self,
659        extended_pan_id: IeeeAddress,
660        join_as_router: bool,
661    ) -> heapless::Vec<usize, 16> {
662        self.select_candidates(extended_pan_id, join_as_router, true)
663    }
664
665    /// Select parent candidates for a rejoin (3.6.1.4.2).
666    ///
667    /// Same selection as for a join except that the network need not permit
668    /// joining: a rejoin reconnects a device the network already knows.
669    fn select_rejoin_candidates(
670        &self,
671        extended_pan_id: IeeeAddress,
672        join_as_router: bool,
673    ) -> heapless::Vec<usize, 16> {
674        self.select_candidates(extended_pan_id, join_as_router, false)
675    }
676
677    fn select_candidates(
678        &self,
679        extended_pan_id: IeeeAddress,
680        join_as_router: bool,
681        require_permit_joining: bool,
682    ) -> heapless::Vec<usize, 16> {
683        let table = self.nib().neighbor_table();
684        log::debug!("[NWK-JOIN] neighbor table: {table:#?}");
685        let stack_profile = self.nib().stack_profile();
686        let permits = |n: &NwkNeighbor| !require_permit_joining || n.permit_joining;
687
688        // 3.6.1.4.1.1: the update id wraps back to zero, so it is treated as
689        // a modular counter - an id `b` is newer than `a` when the signed
690        // difference `(b - a) as i8` is positive
691        let mut matching = table.iter().filter(|n| {
692            n.extended_pan_id == extended_pan_id && permits(n) && n.potential_parent == 1
693        });
694
695        let Some(first) = matching.next() else {
696            return heapless::Vec::new();
697        };
698
699        let best_update_id = matching
700            .map(|n| n.update_id)
701            .fold(first.update_id, |best, id| {
702                // positive signed difference means id is newer
703                if id.wrapping_sub(best).cast_signed() > 0 {
704                    id
705                } else {
706                    best
707                }
708            });
709
710        let mut candidates: heapless::Vec<usize, 16> = table
711            .iter()
712            .enumerate()
713            .filter(|(_, n)| {
714                // correct network, accepting joins of the right type, low
715                // enough link cost, a potential parent, and on the most
716                // recent update id
717                n.extended_pan_id == extended_pan_id
718                    && permits(n)
719                    && if join_as_router {
720                        n.router_capacity
721                    } else {
722                        n.end_device_capacity
723                    }
724                    && link_cost_from_lqi(n.lqi) <= MAX_PARENT_LINK_COST
725                    && n.potential_parent == 1
726                    && n.update_id == best_update_id
727            })
728            .map(|(i, _)| i)
729            .collect();
730
731        // nwkStackProfile == 1 prefers minimum depth (3.6.1.4.1.1)
732        if *stack_profile == 1 {
733            candidates.sort_unstable_by_key(|&i| table[i].depth);
734        }
735
736        candidates
737    }
738
739    /// Build the IEEE 802.15.4 MAC `CapabilityInformation` from the NWK
740    /// layer `CapabilityInformation` bitmap (Table 3-62).
741    fn build_mac_capabilities(cap: &CapabilityInformation) -> zigbee_mac::CapabilityInformation {
742        zigbee_mac::CapabilityInformation {
743            // Bit 1 — Device type: 1 if joining as router (FFD)
744            full_function_device: cap.device_type(),
745            // Bit 2 — Power source
746            mains_power: cap.power_source(),
747            // Bit 3 — Receiver on when idle
748            idle_receive: cap.receiver_on_when_idle(),
749            // Bit 6 — Security capability
750            frame_protection: cap.security_capability(),
751            // Bit 7 — Allocate address
752            allocate_address: cap.allocate_address(),
753        }
754    }
755
756    /// Clear parent information and the negotiated end-device timeout; a
757    /// fresh join invalidates both (3.6.1.4.1.1, 3.6.10.2).
758    fn reset_parent_negotiation(&self) {
759        let nib = self.nib();
760        nib.update_parent_information(|value| *value = 0);
761        nib.update_end_device_timeout(|value| *value = NO_PENDING_TIMEOUT);
762        self.parent_timeout_remaining_ms
763            .store(PARENT_TIMEOUT_INACTIVE, Ordering::Relaxed);
764    }
765
766    /// Find the parent's MAC address from the neighbor table.
767    fn parent_address(&self) -> Result<Address, NetworkError> {
768        let addr = Address::Short(
769            PanId(*self.nib().panid()),
770            MacShortAddress(self.parent_short_address()?.0),
771        );
772        Ok(addr)
773    }
774
775    /// The IEEE address of a neighbor, once it has been learned from a
776    /// received frame carrying the source IEEE address field.
777    fn neighbor_ieee_address(&self, network_address: ShortAddress) -> Option<IeeeAddress> {
778        self.nib()
779            .neighbor_table()
780            .iter()
781            .find(|n| n.network_address == network_address)
782            .map(|n| n.extended_address)
783            .filter(|ieee| ieee.0 != 0)
784    }
785
786    /// The network address of a neighbor whose IEEE address has been learned
787    /// from a received frame (3.4.6.2).
788    pub(crate) fn short_address_for_ieee(&self, ieee: IeeeAddress) -> Option<ShortAddress> {
789        self.nib()
790            .neighbor_table()
791            .iter()
792            .find(|n| n.extended_address == ieee)
793            .map(|n| n.network_address)
794    }
795
796    /// Record the IEEE address a received frame reported for its source
797    /// (3.4.6.2: it lets us address later commands by both addresses).
798    fn learn_neighbor_ieee_address(&self, header: &NwkHeader<'_>) {
799        let Some(source_ieee) = header.source_ieee else {
800            return;
801        };
802        self.nib().update_neighbor_table(|table| {
803            if let Some(neighbor) = table
804                .iter_mut()
805                .find(|n| n.network_address == header.source)
806            {
807                neighbor.extended_address = source_ieee;
808            }
809        });
810    }
811
812    /// Find the parent's network address from the neighbor table.
813    fn parent_short_address(&self) -> Result<ShortAddress, NetworkError> {
814        let table = self.nib().neighbor_table();
815        let parent = table
816            .iter()
817            .find(|n| n.relationship == relationship::PARENT)
818            .ok_or(NetworkError::NotJoined)?;
819        Ok(parent.network_address)
820    }
821
822    /// Issue one MLME-POLL to the parent (3.6.6).
823    ///
824    /// Returns the raw MAC payload length, or `None` when nothing was buffered.
825    async fn poll_parent(&self, buf: &mut [u8]) -> Result<Option<usize>, NetworkError> {
826        let coord_addr = self.parent_address()?;
827        match self.mac.poll_data(coord_addr, buf).await {
828            Ok((len, _lqi)) => Ok(Some(len)),
829            Err(MacError::NoData) => Ok(None),
830            Err(e) => Err(e.into()),
831        }
832    }
833
834    /// Poll the parent once (MLME-POLL, 3.6.6) and process the retrieved NWK
835    /// frame, if any.
836    ///
837    /// A sleepy end device only receives buffered unicasts by polling.
838    /// Returns `Ok(None)` when nothing was buffered or the frame carried no
839    /// application data.
840    pub async fn poll_nwk_frame<'a>(
841        &self,
842        buf: &'a mut [u8],
843    ) -> Result<Option<NwkDataFrame<'a>>, NetworkError> {
844        let Some(len) = self.poll_parent(buf).await? else {
845            return Ok(None);
846        };
847        self.process_received_nwk_frame(&mut buf[..len]).await
848    }
849
850    /// Passively wait for the next inbound NWK frame and process it.
851    ///
852    /// For devices with `rxOnWhenIdle = TRUE`, which receive directly instead
853    /// of polling the parent for buffered unicasts. Returns `Ok(None)` when
854    /// the frame carried no application data.
855    pub async fn receive_nwk_frame<'a>(
856        &self,
857        buf: &'a mut [u8],
858    ) -> Result<Option<NwkDataFrame<'a>>, NetworkError> {
859        let (len, _lqi) = self.mac.receive(buf).await?;
860        self.process_received_nwk_frame(&mut buf[..len]).await
861    }
862
863    /// Decrypt one inbound NWK frame and hand NWK commands to the NWK layer.
864    async fn process_received_nwk_frame<'a>(
865        &self,
866        frame_buf: &'a mut [u8],
867    ) -> Result<Option<NwkDataFrame<'a>>, NetworkError> {
868        let cx = SecurityContext::get();
869        let nwk_frame = cx.decrypt_nwk_frame_in_place(frame_buf)?;
870
871        match nwk_frame {
872            NwkFrame::Data(data_frame) => {
873                self.learn_neighbor_ieee_address(&data_frame.header);
874                Ok(Some(data_frame))
875            }
876            // NWK command frames (link status, route requests, rejoin, ...) ride
877            // the same receive path; let the NWK layer process them, then report
878            // "no data" so the APS/ZDO caller skips this frame
879            NwkFrame::NwkCommand(command_frame) => {
880                self.learn_neighbor_ieee_address(&command_frame.header);
881                self.handle_nwk_command(&command_frame.header, command_frame.command)
882                    .await;
883                Ok(None)
884            }
885            _ => Ok(None),
886        }
887    }
888
889    /// Process an inbound NWK command frame (3.4).
890    ///
891    /// Scaffolding extension point: every NWK command variant is matched so a
892    /// handler can be filled in. Most are not yet acted upon — they are logged
893    /// and ignored. Returning unit keeps the caller's receive loop simple; add
894    /// a result type here if a handler needs to surface failures.
895    async fn handle_nwk_command(&self, header: &NwkHeader<'_>, command: Command<'_>) {
896        match command {
897            // routing (3.4.1-3.4.2): a sleepy end device routes via its parent,
898            // so route discovery is currently a no-op
899            Command::RouteRequest(_) => log::trace!("[NWK] route request (ignored)"),
900            Command::RouteReply(_) => log::trace!("[NWK] route reply (ignored)"),
901            Command::RouteRecord(_) => log::trace!("[NWK] route record (ignored)"),
902            // network maintenance (3.4.3, 3.4.9, 3.4.10)
903            Command::NetworkStatus(_) => log::trace!("[NWK] network status (ignored)"),
904            Command::NetworkReport(_) => log::trace!("[NWK] network report (ignored)"),
905            Command::NetworkUpdate(_) => log::trace!("[NWK] network update (ignored)"),
906            Command::Leave(leave) => self.handle_leave_command(header, &leave).await,
907            // rejoin (3.4.6–3.4.7): a request is parent-side only; a response
908            // is handed to the rejoin procedure waiting for it, which may be
909            // driven by another task than this receive path
910            Command::RejoinRequest(_) => log::trace!("[NWK] rejoin request (ignored)"),
911            Command::RejoinResponse(response) => {
912                log::debug!(
913                    "[NWK] rejoin response: address={:?}, status={:#04x}",
914                    response.network_address,
915                    response.status
916                );
917                if response.status == u8::from(AssociationStatus::Successful) {
918                    // 3.4.7.3.1: the parent may hand out a different address
919                    // than the one the device rejoined with
920                    self.nib()
921                        .update_network_address(|value| *value = response.network_address.0);
922                }
923                self.rejoin_response.signal(response);
924            }
925            // link management (3.4.8, 3.4.13)
926            Command::LinkStatus(_) => log::trace!("[NWK] link status (ignored)"),
927            Command::LinkPowerDelta(_) => log::trace!("[NWK] link power delta (ignored)"),
928            // end-device timeout requests are parent-side only (3.4.11)
929            Command::EndDeviceTimeoutRequest(_) => {
930                log::trace!("[NWK] end-device timeout request (ignored)");
931            }
932            // 3.6.10.2: on success store the parent information bitmask and
933            // the negotiated timeout, otherwise the parent keeps its default
934            Command::EndDeviceTimeoutResponse(response) => {
935                let pending = self
936                    .pending_timeout_request
937                    .swap(NO_PENDING_TIMEOUT, Ordering::Relaxed);
938                if response.status == EndDeviceTimeoutResponse::STATUS_SUCCESS
939                    && pending != NO_PENDING_TIMEOUT
940                {
941                    let nib = self.nib();
942                    nib.update_parent_information(|value| *value = response.parent_information);
943                    nib.update_end_device_timeout(|value| *value = pending);
944                    // the parent restarted its timeout counter for this device
945                    // (3.6.10.5), so the local one follows (3.6.10.6)
946                    self.refresh_parent_timeout();
947                    log::info!(
948                        "[NWK] end-device timeout negotiated: enum={pending}, parent_information={:#04x}",
949                        response.parent_information
950                    );
951                } else {
952                    log::warn!(
953                        "[NWK] end-device timeout response rejected or unexpected (status={:#04x})",
954                        response.status
955                    );
956                }
957                // hand it to a keepalive waiting for it (3.6.10.3)
958                self.timeout_response.signal(response);
959            }
960            Command::Reserved(id) => log::trace!("[NWK] reserved command {id:#04x} (ignored)"),
961        }
962    }
963
964    /// Process an inbound Leave command frame (3.6.1.10.3).
965    ///
966    /// TODO: a router receiving a notification from its parent with the remove
967    /// children sub-field set must rebroadcast the leave to its own children.
968    async fn handle_leave_command(&self, header: &NwkHeader<'_>, leave: &Leave) {
969        let options = leave.command_options;
970        let is_from_parent = self
971            .parent_short_address()
972            .is_ok_and(|parent| parent == header.source);
973
974        if !options.request() {
975            // notification: the sender left the network on its own initiative —
976            // it is no longer a neighbor regardless of the rejoin flag
977            self.nib().update_neighbor_table(|table| {
978                table.retain(|n| n.network_address != header.source);
979            });
980
981            if is_from_parent {
982                // our parent has dropped us: we are no longer on the network,
983                // and the indication carries a NULL device address (3.6.1.10.3)
984                self.nib().update_extended_panid(|value| *value = 0);
985                log::info!("[NWK] removed by parent (leave notification)");
986                self.leave_indication.signal(NlmeLeaveIndication {
987                    device_address: None,
988                    rejoin: options.rejoin(),
989                });
990            } else {
991                log::debug!(
992                    "[NWK] neighbor {:?} left the network (rejoin={})",
993                    header.source_ieee,
994                    options.rejoin()
995                );
996                self.leave_indication.signal(NlmeLeaveIndication {
997                    device_address: header.source_ieee,
998                    rejoin: options.rejoin(),
999                });
1000            }
1001            return;
1002        }
1003
1004        // request == 1: someone is asking us to leave (3.6.1.10.3.1). A leave
1005        // request is sent by the parent itself, so its NWK source address is
1006        // the sender the spec tests
1007        let from_parent = self
1008            .parent_short_address()
1009            .is_ok_and(|parent| parent == header.source);
1010        if !self.accepts_leave_request(header.destination, from_parent) {
1011            return;
1012        }
1013
1014        if let Err(e) = self
1015            .leave_network(options.remove_children(), options.rejoin())
1016            .await
1017        {
1018            log::warn!("[NWK] failed to announce leave: {e:?}");
1019        }
1020
1021        // 3.2.2.19: removed by another device, so the indication carries a NULL
1022        // device address; with the rejoin flag it asks the higher layer to come
1023        // back onto the network (3.6.1.10.4)
1024        self.leave_indication.signal(NlmeLeaveIndication {
1025            device_address: None,
1026            rejoin: options.rejoin(),
1027        });
1028    }
1029
1030    /// Validate a leave request against 3.6.1.10.3.1, which governs both the
1031    /// NWK Leave (request) command and the ZDO Mgmt_Leave_req.
1032    ///
1033    /// `destination` is the address the request was addressed to.
1034    /// `from_parent` says whether it reached this device over the parent link:
1035    /// the spec tests the MAC source address of the delivering frame, which is
1036    /// the parent for anything a child receives — not the originator of a
1037    /// relayed ZDP request.
1038    pub fn accepts_leave_request(&self, destination: ShortAddress, from_parent: bool) -> bool {
1039        // step 1: the coordinator never leaves, and a broadcast request is
1040        // dropped without further processing
1041        if *self.nib().network_address() == NWK_COORDINATOR_ADDRESS
1042            || destination.0 >= NWK_BROADCAST_ADDRESS_MIN
1043        {
1044            log::trace!("[NWK] leave request dropped (coordinator or broadcast destination)");
1045            return false;
1046        }
1047
1048        // step 2: a router honors any sender while nwkLeaveRequestAllowed is
1049        // set, ignoring the neighbor relationship
1050        if self.nib().capability_information().device_type() {
1051            let allowed = *self.nib().leave_request_allowed();
1052            if !allowed {
1053                log::trace!("[NWK] leave request refused (nwkLeaveRequestAllowed is FALSE)");
1054            }
1055            return allowed;
1056        }
1057
1058        // step 3: an end device is only removed by its parent
1059        if !from_parent {
1060            log::trace!("[NWK] leave request refused (sender is not our parent)");
1061        }
1062        from_parent
1063    }
1064
1065    /// 3.2.2.18
1066    ///
1067    /// Only self-removal (`device_address` `None` or this device's own IEEE
1068    /// address) is implemented: removing a child requires acting as its
1069    /// parent, which this stack does not yet support.
1070    pub async fn leave(&self, request: NlmeLeaveRequest) -> NlmeLeaveConfirm {
1071        if *self.nib().network_address() == NWK_UNASSIGNED_ADDRESS {
1072            return NlmeLeaveConfirm {
1073                status: NlmeLeaveStatus::InvalidRequest,
1074                device_address: request.device_address,
1075            };
1076        }
1077
1078        let is_self = request
1079            .device_address
1080            .is_none_or(|addr| addr == *self.nib().ieee_address());
1081        if !is_self {
1082            return NlmeLeaveConfirm {
1083                status: NlmeLeaveStatus::UnknownDevice,
1084                device_address: request.device_address,
1085            };
1086        }
1087
1088        let status = match self
1089            .leave_network(request.remove_children, request.rejoin)
1090            .await
1091        {
1092            Ok(()) => NlmeLeaveStatus::Success,
1093            Err(_) => NlmeLeaveStatus::MacError,
1094        };
1095        NlmeLeaveConfirm {
1096            status,
1097            device_address: None,
1098        }
1099    }
1100
1101    /// 3.6.1.10.1 + 3.6.1.10.4: announce this device's own removal from the
1102    /// network, then apply the local leave procedure.
1103    ///
1104    /// The local leave runs whether or not the announcement made it onto the
1105    /// air; the transmit result only feeds the NLME-LEAVE.confirm status.
1106    async fn leave_network(&self, remove_children: bool, rejoin: bool) -> Result<(), NetworkError> {
1107        let result = self.announce_leave(remove_children, rejoin).await;
1108        self.local_leave(rejoin);
1109        result
1110    }
1111
1112    /// 3.6.1.10.1: transmit this device's own leave command frame, with the
1113    /// request sub-field set to 0 — a notification, never a request addressed
1114    /// to someone else.
1115    async fn announce_leave(
1116        &self,
1117        remove_children: bool,
1118        rejoin: bool,
1119    ) -> Result<(), NetworkError> {
1120        let is_router_or_coordinator = self.nib().capability_information().device_type()
1121            || *self.nib().network_address() == NWK_COORDINATOR_ADDRESS;
1122
1123        let mut buf = [0u8; 64];
1124        if is_router_or_coordinator {
1125            let command = Command::Leave(Leave {
1126                command_options: LeaveCommandOptions(0)
1127                    .set_remove_children(remove_children)
1128                    .set_rejoin(rejoin),
1129            });
1130            let len = self.build_nwk_command_frame(
1131                ShortAddress(NWK_BROADCAST_ALL),
1132                command,
1133                true,
1134                &mut buf,
1135            )?;
1136            self.mac
1137                .transmit_data(
1138                    Address::Short(
1139                        PanId(*self.nib().panid()),
1140                        MacShortAddress(NWK_BROADCAST_ALL),
1141                    ),
1142                    &buf[..len],
1143                )
1144                .await?;
1145            return Ok(());
1146        }
1147
1148        // an end device unicasts to its parent and sends the remove children
1149        // sub-field as 0; only the rejoin flag is carried over
1150        let command = Command::Leave(Leave {
1151            command_options: LeaveCommandOptions(0).set_rejoin(rejoin),
1152        });
1153        let len =
1154            self.build_nwk_command_frame(self.parent_short_address()?, command, true, &mut buf)?;
1155        let result = self
1156            .mac
1157            .transmit_data(self.parent_address()?, &buf[..len])
1158            .await;
1159        // 3.6.1.10.1: an end device clears the extended PAN id right after
1160        // transmitting, ahead of the rest of the local leave process
1161        self.nib().update_extended_panid(|value| *value = 0);
1162        result.map_err(Into::into)
1163    }
1164
1165    /// 3.6.1.10.4: the local half of the leave procedure.
1166    fn local_leave(&self, rejoin: bool) {
1167        if rejoin {
1168            // step 1: with Rejoin set the NIB is kept, so the remembered
1169            // network is still there for a later NLME-JOIN.request with
1170            // `RejoinNetwork::NwkRejoin`; driving that is the higher layer's
1171            // call, which the NLME-LEAVE.indication/confirm hands it
1172            log::info!("[NWK] left the network, rejoin requested");
1173            return;
1174        }
1175
1176        self.clear_nib_on_leave();
1177    }
1178
1179    /// 3.6.1.10.4 (Rejoin = FALSE): clear the NIB attributes describing
1180    /// network membership, leaving the device unjoined.
1181    fn clear_nib_on_leave(&self) {
1182        let nib = self.nib();
1183        nib.update_neighbor_table(|value| value.clear());
1184        nib.update_route_table(|value| value.clear());
1185        nib.update_manager_addr(|value| *value = 0x0000);
1186        nib.update_update_id(|value| *value = 0x00);
1187        nib.update_network_address(|value| *value = NWK_UNASSIGNED_ADDRESS);
1188        nib.update_group_idtable(|value| value.clear());
1189        nib.update_extended_panid(|value| *value = 0);
1190        nib.update_route_record_table(|value| value.clear());
1191        nib.update_is_concentrator(|value| *value = false);
1192        nib.update_concentrator_radius(|value| *value = 0);
1193        nib.update_security_material_set(|value| value.clear());
1194        nib.update_active_key_seq_number(|value| *value = 0x00);
1195        nib.update_address_map(|value| value.clear());
1196        nib.update_panid(|value| *value = 0xffff);
1197        nib.update_tx_total(|value| *value = 0);
1198        nib.update_parent_information(|value| *value = 0x00);
1199    }
1200
1201    /// 3.2.2.3
1202    pub async fn network_discovery(
1203        &self,
1204        channels: core::ops::Range<u8>,
1205        duration: u8,
1206    ) -> Result<NlmeNetworkDiscoveryConfirm, NetworkError> {
1207        let scan_result = self
1208            .mac
1209            .scan_network(ScanType::Active, channels, duration)
1210            .await?;
1211
1212        // populate the neighbor table with mandatory fields (Table 3-63)
1213        // and optional discovery-time fields (Table 3-64)
1214        let neighbor_table = scan_result
1215            .pan_descriptor
1216            .iter()
1217            .filter_map(|pd| match pd.coord_address {
1218                Address::Short(_pan_id, short_address) => Some(NwkNeighbor {
1219                    // a beacon carries no IEEE address; learned later from
1220                    // received frames
1221                    extended_address: IeeeAddress(0),
1222                    network_address: ShortAddress(short_address.0),
1223                    device_type: if short_address.0 == NWK_COORDINATOR_ADDRESS {
1224                        DeviceType::Coordinator
1225                    } else {
1226                        DeviceType::Router
1227                    },
1228                    rx_on_when_idle: false,
1229                    end_device_configuration: 0,
1230                    relationship: relationship::NONE,
1231                    transmit_failure: 0,
1232                    lqi: pd.link_quality,
1233                    outgoing_cost: 0,
1234                    age: 0,
1235                    keepalive_received: false,
1236                    // Table 3-64: optional discovery-time fields
1237                    extended_pan_id: pd.zigbee_beacon.extended_pan_id,
1238                    logical_channel: pd.channel,
1239                    depth: pd.zigbee_beacon.stack_profile.device_depth(),
1240                    permit_joining: pd.superframe_spec.association_permit,
1241                    // a device is a potential parent if it can accept a child of
1242                    // either type; select_parent_candidates applies the
1243                    // join-type-specific capacity check (Table 3-64)
1244                    potential_parent: u8::from(
1245                        pd.zigbee_beacon.stack_profile.router_capacity()
1246                            || pd.zigbee_beacon.stack_profile.end_device_capacity()
1247                            || short_address.0 == NWK_COORDINATOR_ADDRESS,
1248                    ),
1249                    router_capacity: pd.zigbee_beacon.stack_profile.router_capacity(),
1250                    end_device_capacity: pd.zigbee_beacon.stack_profile.end_device_capacity(),
1251                    update_id: pd.zigbee_beacon.update_id,
1252                    pan_id: pd.coord_pan_id.0,
1253                }),
1254                Address::Extended(_, _) => None,
1255            })
1256            .collect();
1257
1258        self.nib()
1259            .update_neighbor_table(|value| *value = StorageVec(neighbor_table));
1260
1261        // build network descriptors for the confirm primitive
1262        let network_descriptors = scan_result
1263            .pan_descriptor
1264            .into_iter()
1265            .map(From::from)
1266            .collect();
1267
1268        Ok(NlmeNetworkDiscoveryConfirm {
1269            network_descriptor: network_descriptors,
1270        })
1271    }
1272
1273    /// 3.2.2.5
1274    #[allow(clippy::unused_async)]
1275    pub async fn network_formation(
1276        &self,
1277        _request: NlmeNetworkFormationRequest,
1278    ) -> NlmeNetworkFormationConfirm {
1279        todo!()
1280    }
1281
1282    /// 3.2.2.7
1283    // figure 3-39
1284    #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
1285    pub async fn permit_joining(
1286        &self,
1287        _request: NlmePermitJoiningRequest,
1288    ) -> NlmePermitJoiningConfirm {
1289        NlmePermitJoiningConfirm {
1290            status: NlmeJoinStatus::InvalidRequest,
1291        }
1292    }
1293
1294    /// 3.2.2.9
1295    #[allow(clippy::unused_async)]
1296    pub async fn start_router(&self, _request: NlmeStartRouterRequest) -> NlmeStartRouterConfirm {
1297        todo!()
1298    }
1299
1300    /// 3.2.2.11
1301    #[allow(clippy::unused_async)]
1302    pub async fn ed_scan(&self, _request: NlmeEdScanRequest) -> NlmeEdScanConfirm {
1303        todo!()
1304    }
1305
1306    /// 3.2.2.13
1307    // the association candidate loop is a single state machine; splitting it
1308    // up would scatter the 3.6.1.4.1.1 bookkeeping across functions
1309    #[allow(clippy::too_many_lines)]
1310    pub async fn join(&self, request: NlmeJoinRequest) -> NlmeJoinConfirm {
1311        if request.rejoin_network == RejoinNetwork::NwkRejoin {
1312            return self.nwk_rejoin(request).await;
1313        }
1314        let fail = Self::join_failure;
1315
1316        // validate the request (3.2.2.13.3); orphan (0x01) and channel
1317        // change (0x03) are not yet implemented
1318        if request.rejoin_network != RejoinNetwork::Association {
1319            return fail(NlmeJoinStatus::InvalidRequest);
1320        }
1321
1322        // a device already joined must not re-associate (3.6.1.4.1.1)
1323        if *self.nib().network_address() != 0xffff {
1324            return fail(NlmeJoinStatus::InvalidRequest);
1325        }
1326
1327        // parent selection (3.6.1.4.1.1): reset nwkParentInformation before
1328        // searching, per spec
1329        self.reset_parent_negotiation();
1330
1331        let join_as_router = request.capability_information.device_type();
1332
1333        let candidates = self.select_parent_candidates(request.extended_pan_id, join_as_router);
1334
1335        if candidates.is_empty() {
1336            log::warn!("[NWK-JOIN] no suitable neighbors");
1337            return fail(NlmeJoinStatus::NotPermitted);
1338        }
1339
1340        log::debug!("[NWK-JOIN] neighbor candidates: {candidates:?}");
1341
1342        // build MAC CapabilityInformation from NWK CapabilityInformation
1343        // bitmap (Table 3-62)
1344        let mac_caps = Self::build_mac_capabilities(&request.capability_information);
1345
1346        // 3.6.1.4.1.1: the capability information shall be stored as the
1347        // value of the nwkCapabilityInformation NIB attribute
1348        self.nib()
1349            .update_capability_information(|value| *value = request.capability_information);
1350
1351        // try each candidate in order (3.6.1.4.1.1)
1352        let mut last_status = NlmeJoinStatus::NotPermitted;
1353
1354        for &candidate_idx in &candidates {
1355            let table = self.nib().neighbor_table();
1356            let neighbor = &table[candidate_idx];
1357            let channel = neighbor.logical_channel;
1358            let pan_id = PanId(neighbor.pan_id);
1359            let dest = Address::Short(pan_id, MacShortAddress(neighbor.network_address.0));
1360            drop(table);
1361
1362            match self.mac.associate(channel, dest, mac_caps).await {
1363                Ok(response) => {
1364                    match response.status {
1365                        AssociationStatus::Successful => {
1366                            let assigned_addr = response.association_address;
1367                            self.nib()
1368                                .update_network_address(|value| *value = assigned_addr.0);
1369                            self.nib()
1370                                .update_ieee_address(|value| *value = response.device_address);
1371                            self.nib()
1372                                .update_extended_panid(|value| *value = request.extended_pan_id.0);
1373                            self.nib().update_panid(|value| *value = pan_id.0);
1374
1375                            // read parent fields before the clearing loop
1376                            // zeroes them (3.6.1.4.1.1)
1377                            let parent_update_id =
1378                                self.nib().neighbor_table()[candidate_idx].update_id;
1379                            let parent_channel =
1380                                self.nib().neighbor_table()[candidate_idx].logical_channel;
1381                            self.nib()
1382                                .update_update_id(|value| *value = parent_update_id);
1383
1384                            // set the relationship field to parent and clear
1385                            // optional Table 3-64 fields on all entries, which
1386                            // should not be retained after joining
1387                            // TODO: retain only entries belonging to the joined network
1388                            self.nib().update_neighbor_table(|table| {
1389                                table[candidate_idx].relationship = relationship::PARENT;
1390                                for neighbor in table.iter_mut() {
1391                                    neighbor.extended_pan_id = IeeeAddress(0);
1392                                    neighbor.logical_channel = 0;
1393                                    neighbor.depth = 0;
1394                                    neighbor.permit_joining = false;
1395                                    neighbor.potential_parent = 0;
1396                                    neighbor.router_capacity = false;
1397                                    neighbor.end_device_capacity = false;
1398                                    neighbor.update_id = 0;
1399                                    neighbor.pan_id = 0xffff;
1400                                }
1401                            });
1402
1403                            return NlmeJoinConfirm {
1404                                status: NlmeJoinStatus::Success,
1405                                network_address: assigned_addr,
1406                                extended_pan_id: request.extended_pan_id,
1407                                channel: parent_channel,
1408                                enhanced_beacon_type: false,
1409                                mac_interface_index: 0u8,
1410                            };
1411                        }
1412                        AssociationStatus::NetworkAtCapacity => {
1413                            // mark this neighbor as not a potential parent so
1414                            // we don't retry (3.6.1.4.1.1)
1415                            self.nib().update_neighbor_table(|table| {
1416                                table[candidate_idx].potential_parent = 0;
1417                            });
1418                            last_status = NlmeJoinStatus::PanAtCapacity;
1419                        }
1420                        AssociationStatus::AccessDenied => {
1421                            self.nib().update_neighbor_table(|table| {
1422                                table[candidate_idx].potential_parent = 0;
1423                            });
1424                            last_status = NlmeJoinStatus::PanAccessDenied;
1425                        }
1426                        // other status codes are treated as a generic MAC-level failure
1427                        _ => {
1428                            last_status = NlmeJoinStatus::MacError;
1429                        }
1430                    }
1431                }
1432                Err(_mac_err) => {
1433                    last_status = NlmeJoinStatus::MacError;
1434                }
1435            }
1436        }
1437
1438        // all candidates exhausted
1439        fail(last_status)
1440    }
1441
1442    /// NLME-JOIN.request with `RejoinNetwork::NwkRejoin` (3.6.1.4.2).
1443    ///
1444    /// Reconnects to a network this device remembers — extended PAN id,
1445    /// network key — by unicasting a Rejoin Request (3.4.6) and waiting for
1446    /// the Rejoin Response (3.4.7). The remembered parent is tried first; if
1447    /// it does not answer, `request.scan_channels` are scanned for another
1448    /// router of the same network to rejoin through, which may operate on a
1449    /// different channel.
1450    ///
1451    /// `request.security_enabled` selects the rejoin flavour: `true` secures
1452    /// the Rejoin Request with the active network key (Secure Rejoin,
1453    /// 4.6.3.3.1), `false` sends it unsecured (Trust Center Rejoin,
1454    /// 4.6.3.3.2) for a device that no longer holds the key — the caller is
1455    /// then responsible for obtaining the current network key from the Trust
1456    /// Center.
1457    async fn nwk_rejoin(&self, request: NlmeJoinRequest) -> NlmeJoinConfirm {
1458        let fail = Self::join_failure;
1459
1460        // a rejoin only makes sense against a network this device already
1461        // remembers (BDB 7.1 step 4 only reaches here when
1462        // bdbNodeIsOnANetwork is TRUE and the extended PAN id is known). A
1463        // leave zeroes nwkExtendedPANId (3.6.1.10.1) without ending the rejoin
1464        // path, so an unknown extended PAN id takes the requested one
1465        let remembered_epid = *self.nib().extended_panid();
1466        if *self.nib().network_address() == 0xffff
1467            || (remembered_epid != 0 && remembered_epid != request.extended_pan_id.0)
1468        {
1469            return fail(NlmeJoinStatus::InvalidRequest);
1470        }
1471
1472        // whether joining or rejoining, nwkParentInformation is reset
1473        // (3.2.2.13.3)
1474        self.reset_parent_negotiation();
1475        self.nib()
1476            .update_capability_information(|value| *value = request.capability_information);
1477
1478        // the remembered parent needs no scan: try it before the discovery
1479        // below rebuilds the neighbor table from fresh beacons
1480        let mut last_status = NlmeJoinStatus::InvalidRequest;
1481        if let Ok(parent) = self.parent_short_address() {
1482            match self.rejoin_via_parent(parent, &request).await {
1483                // the channel is whatever the radio is already tuned to
1484                Ok(response) => return self.rejoin_confirm(response, &request, 0).await,
1485                Err(status) => {
1486                    log::debug!("[NWK-REJOIN] remembered parent failed ({status:?})");
1487                    last_status = status;
1488                }
1489            }
1490        }
1491
1492        if request.scan_channels.is_empty() {
1493            return fail(last_status);
1494        }
1495
1496        log::debug!(
1497            "[NWK-REJOIN] scanning {:?} for another parent",
1498            request.scan_channels
1499        );
1500        if let Err(e) = self
1501            .network_discovery(request.scan_channels.clone(), request.scan_duration)
1502            .await
1503        {
1504            log::warn!("[NWK-REJOIN] scan failed: {e:?}");
1505            return fail(NlmeJoinStatus::MacError);
1506        }
1507
1508        let join_as_router = request.capability_information.device_type();
1509        let candidates = self.select_rejoin_candidates(request.extended_pan_id, join_as_router);
1510        if candidates.is_empty() {
1511            log::warn!("[NWK-REJOIN] no parent candidate found");
1512            return fail(NlmeJoinStatus::NotPermitted);
1513        }
1514
1515        for candidate_idx in candidates {
1516            let Some((parent, channel)) = self.adopt_parent(candidate_idx).await else {
1517                continue;
1518            };
1519            match self.rejoin_via_parent(parent, &request).await {
1520                Ok(response) => return self.rejoin_confirm(response, &request, channel).await,
1521                Err(status) => {
1522                    log::debug!("[NWK-REJOIN] candidate {parent:?} failed ({status:?})");
1523                    last_status = status;
1524                }
1525            }
1526        }
1527
1528        fail(last_status)
1529    }
1530
1531    /// One Rejoin Request/Response exchange with `parent` (3.4.6, 3.4.7).
1532    async fn rejoin_via_parent(
1533        &self,
1534        parent: ShortAddress,
1535        request: &NlmeJoinRequest,
1536    ) -> Result<RejoinResponse, NlmeJoinStatus> {
1537        let command = Command::RejoinRequest(RejoinRequest {
1538            // the Capability Information bit layout (Table 3-62) is shared
1539            // with `nwk::nib::CapabilityInformation`
1540            capability_information: rejoin_request::CapabilityInformation(
1541                request.capability_information.0,
1542            ),
1543        });
1544
1545        let mut buf = [0u8; 128];
1546        let len = self
1547            .build_nwk_command_frame(parent, command, request.security_enabled, &mut buf)
1548            .map_err(|_| NlmeJoinStatus::MacError)?;
1549        let parent_addr = self
1550            .parent_address()
1551            .map_err(|_| NlmeJoinStatus::InvalidRequest)?;
1552
1553        // arm before transmitting: the response is delivered by the receive
1554        // path, which may be this procedure's own poll or a concurrent loop
1555        self.rejoin_response.reset();
1556        self.mac
1557            .transmit_data(parent_addr, &buf[..len])
1558            .await
1559            .map_err(|_| NlmeJoinStatus::MacError)?;
1560
1561        let response = self
1562            .await_rejoin_response(REJOIN_RESPONSE_POLL_RETRIES)
1563            .await
1564            .ok_or(NlmeJoinStatus::MacError)?;
1565
1566        match response.status {
1567            status if status == u8::from(AssociationStatus::Successful) => Ok(response),
1568            status if status == u8::from(AssociationStatus::NetworkAtCapacity) => {
1569                Err(NlmeJoinStatus::PanAtCapacity)
1570            }
1571            status if status == u8::from(AssociationStatus::AccessDenied) => {
1572                Err(NlmeJoinStatus::PanAccessDenied)
1573            }
1574            _ => Err(NlmeJoinStatus::MacError),
1575        }
1576    }
1577
1578    /// Build the successful NLME-JOIN.confirm of a rejoin (3.2.2.15).
1579    async fn rejoin_confirm(
1580        &self,
1581        response: RejoinResponse,
1582        request: &NlmeJoinRequest,
1583        channel: u8,
1584    ) -> NlmeJoinConfirm {
1585        // the receive path stored the assigned address; the hardware filter has
1586        // to follow it
1587        self.mac
1588            .configure(MacConfig::short_address(response.network_address))
1589            .await;
1590        // 3.2.2.13.3: nwkExtendedPANId names the network we are on again
1591        self.nib()
1592            .update_extended_panid(|value| *value = request.extended_pan_id.0);
1593
1594        NlmeJoinConfirm {
1595            status: NlmeJoinStatus::Success,
1596            network_address: response.network_address,
1597            extended_pan_id: request.extended_pan_id,
1598            channel,
1599            enhanced_beacon_type: false,
1600            mac_interface_index: 0u8,
1601        }
1602    }
1603
1604    /// Make the neighbor at `candidate_idx` this device's parent and tune the
1605    /// radio to its channel, returning its address and channel.
1606    ///
1607    /// A rejoin may land on a router other than the remembered parent, and the
1608    /// network may meanwhile have moved to another channel (3.6.1.4.2).
1609    async fn adopt_parent(&self, candidate_idx: usize) -> Option<(ShortAddress, u8)> {
1610        let nib = self.nib();
1611        let (parent, channel, pan_id, update_id) = {
1612            let table = nib.neighbor_table();
1613            let neighbor = table.get(candidate_idx)?;
1614            (
1615                neighbor.network_address,
1616                neighbor.logical_channel,
1617                neighbor.pan_id,
1618                neighbor.update_id,
1619            )
1620        };
1621
1622        nib.update_neighbor_table(|table| {
1623            for (index, neighbor) in table.iter_mut().enumerate() {
1624                neighbor.relationship = if index == candidate_idx {
1625                    relationship::PARENT
1626                } else {
1627                    relationship::NONE
1628                };
1629            }
1630        });
1631        nib.update_panid(|value| *value = pan_id);
1632        nib.update_update_id(|value| *value = update_id);
1633        self.mac.configure(MacConfig::channel(channel)).await;
1634
1635        Some((parent, channel))
1636    }
1637
1638    /// Wait for the receive path to deliver the Rejoin Response (3.4.7).
1639    ///
1640    /// A concurrently running receive loop may deliver it; otherwise the
1641    /// bounded polling below does.
1642    async fn await_rejoin_response(&self, retries: u8) -> Option<RejoinResponse> {
1643        with_timeout(
1644            self.rejoin_response.wait(),
1645            self.poll_until_rejoin_response(retries),
1646        )
1647        .await
1648        // the response may have arrived in the very poll that ended the polling
1649        .or_else(|| self.rejoin_response.try_take())
1650    }
1651
1652    /// Poll the parent until the Rejoin Response has been processed or
1653    /// `retries` polls went unanswered.
1654    ///
1655    /// Drives reception for a caller whose receive loop is not running yet; a
1656    /// data frame cannot be dispatched from here and is left to that loop.
1657    async fn poll_until_rejoin_response(&self, retries: u8) {
1658        let mut buf = [0u8; 128];
1659        for _ in 0..retries {
1660            if self.rejoin_response.is_signaled() {
1661                return;
1662            }
1663            match self.poll_parent(&mut buf).await {
1664                Ok(Some(len)) => match self.process_received_nwk_frame(&mut buf[..len]).await {
1665                    Ok(Some(_)) => log::debug!("[NWK-REJOIN] polled data frame dropped"),
1666                    Ok(None) => (),
1667                    Err(e) => log::debug!("[NWK-REJOIN] polled frame not processed: {e:?}"),
1668                },
1669                Ok(None) => (),
1670                Err(e) => log::debug!("[NWK-REJOIN] poll failed: {e:?}"),
1671            }
1672        }
1673    }
1674
1675    /// Poll the coordinator for pending data, strip the NWK header, and
1676    /// return the APS payload (3.6.2).
1677    pub async fn poll_nwk_data<'a>(
1678        &self,
1679        buf: &'a mut [u8],
1680        retries: u8,
1681    ) -> Result<NwkDataFrame<'a>, NetworkError> {
1682        for _ in 0..retries {
1683            // SAFETY: `buf` is mutably borrowed once at a time
1684            // need to get rid of the 'a lifetime in the loop
1685            // &mut buf is still guaranteed within 'a
1686            let buf = unsafe { slice::from_raw_parts_mut(buf.as_mut_ptr(), buf.len()) };
1687            match self.poll_nwk_frame(buf).await {
1688                Ok(Some(data_frame)) => {
1689                    return Ok(data_frame);
1690                }
1691                // keep polling: nothing buffered, a NWK command frame, or ambient
1692                // traffic the pre-key joiner cannot decode (SecurityError/ParseError)
1693                // the NWK-unsecured transport-key (4.6.3.7.2) stays buffered for a
1694                // later poll
1695                Ok(None) | Err(NetworkError::SecurityError(_) | NetworkError::ParseError) => (),
1696                Err(e) => return Err(e),
1697            }
1698        }
1699
1700        Err(NetworkError::MacError(MacError::NoData))
1701    }
1702
1703    /// Broadcast an NWK data frame (3.6.5).
1704    ///
1705    /// Wraps `payload` in a NWK header addressed to `destination` and
1706    /// transmits it as a MAC broadcast.
1707    ///
1708    /// When `secure` is true the NWK frame is encrypted with the
1709    /// active network key.
1710    pub async fn broadcast_data(
1711        &self,
1712        destination: ShortAddress,
1713        secure: bool,
1714        payload: &[u8],
1715    ) -> Result<(), NetworkError> {
1716        let mut buf = [0u8; 256];
1717        let total_len = self.build_nwk_data_frame(destination, secure, payload, &mut buf)?;
1718        // 3.6.5: a sleepy end device unicasts broadcasts to its parent,
1719        // which relays them into the network on its behalf
1720        self.transmit_via_parent(&buf[..total_len]).await
1721    }
1722
1723    /// Send an NWK data frame to a specific destination (3.6.3).
1724    ///
1725    /// Wraps `payload` in a NWK header addressed to `destination` and
1726    /// transmits it via the parent (for end devices) or directly.
1727    ///
1728    /// When `secure` is true the NWK frame is encrypted with the
1729    /// active network key.
1730    pub async fn send_data(
1731        &self,
1732        destination: ShortAddress,
1733        secure: bool,
1734        payload: &[u8],
1735    ) -> Result<(), NetworkError> {
1736        let mut buf = [0u8; 256];
1737        let total_len = self.build_nwk_data_frame(destination, secure, payload, &mut buf)?;
1738        // end devices route via parent
1739        self.transmit_via_parent(&buf[..total_len]).await
1740    }
1741}
1742
1743#[cfg(test)]
1744mod tests {
1745    use core::future::Future;
1746
1747    use zigbee_mac::AssociationStatus;
1748    use zigbee_mac::mlme::AssociationResponse;
1749    use zigbee_mac::mlme::MacConfig;
1750    use zigbee_mac::mlme::MacError;
1751    use zigbee_mac::mlme::ScanResult;
1752    use zigbee_mac::mlme::ScanType;
1753
1754    use super::*;
1755    // tests share a global NIB singleton — serialize against every other
1756    // module that touches it, not just this one
1757    use crate::TEST_MUTEX;
1758
1759    // minimal async block_on — the mock futures resolve immediately so a
1760    // single poll is sufficient
1761    #[allow(clippy::panic)]
1762    fn block_on<F: Future>(f: F) -> F::Output {
1763        use core::pin::pin;
1764        use core::task::Context;
1765        use core::task::Poll;
1766        use core::task::RawWaker;
1767        use core::task::RawWakerVTable;
1768        use core::task::Waker;
1769
1770        fn noop(_: *const ()) {}
1771        fn clone(p: *const ()) -> RawWaker {
1772            RawWaker::new(p, &VTABLE)
1773        }
1774        static VTABLE: RawWakerVTable = RawWakerVTable::new(clone, noop, noop, noop);
1775
1776        let waker = unsafe { Waker::from_raw(RawWaker::new(core::ptr::null(), &VTABLE)) };
1777        let mut cx = Context::from_waker(&waker);
1778        let mut f = pin!(f);
1779
1780        match f.as_mut().poll(&mut cx) {
1781            Poll::Ready(val) => val,
1782            Poll::Pending => panic!("block_on: future returned Pending"),
1783        }
1784    }
1785
1786    mockall::mock! {
1787        Mlme {}
1788        impl Mlme for Mlme {
1789            fn ieee_address(&self) -> IeeeAddress;
1790            async fn configure(&self, config: MacConfig);
1791            async fn scan_network(
1792                &self,
1793                ty: ScanType,
1794                channels: core::ops::Range<u8>,
1795                duration: u8,
1796            ) -> Result<ScanResult, MacError>;
1797            async fn associate(
1798                &self,
1799                channel: u8,
1800                dest: Address,
1801                capabilities: zigbee_mac::CapabilityInformation,
1802            ) -> Result<AssociationResponse, MacError>;
1803            async fn poll_data(
1804                &self,
1805                coord_address: Address,
1806                buf: &mut [u8],
1807            ) -> Result<(usize, u8), MacError>;
1808            async fn receive(
1809                &self,
1810                buf: &mut [u8],
1811            ) -> Result<(usize, u8), MacError>;
1812            async fn transmit_data(
1813                &self,
1814                dest: Address,
1815                payload: &[u8],
1816            ) -> Result<(), MacError>;
1817        }
1818    }
1819
1820    // creates a default `NwkNeighbor` pre-filled for parent selection
1821    fn make_neighbor(pan_id: u16, short_addr: u16, epid: u64, lqi: u8, depth: u8) -> NwkNeighbor {
1822        NwkNeighbor {
1823            extended_address: IeeeAddress(0),
1824            network_address: ShortAddress(short_addr),
1825            device_type: if short_addr == 0 {
1826                DeviceType::Coordinator
1827            } else {
1828                DeviceType::Router
1829            },
1830            rx_on_when_idle: false,
1831            end_device_configuration: 0,
1832            relationship: 0x03,
1833            transmit_failure: 0,
1834            lqi,
1835            outgoing_cost: 0,
1836            age: 0,
1837            keepalive_received: false,
1838            extended_pan_id: IeeeAddress(epid),
1839            logical_channel: 11,
1840            depth,
1841            permit_joining: true,
1842            potential_parent: 1,
1843            router_capacity: true,
1844            end_device_capacity: true,
1845            update_id: 0,
1846            pan_id,
1847        }
1848    }
1849
1850    fn make_nlme(mut mac: MockMlme) -> (std::sync::MutexGuard<'static, ()>, Nlme<MockMlme>) {
1851        let guard = TEST_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
1852        use crate::nwk::nib;
1853        nib::try_init();
1854        nib::reset();
1855        // the security context used by frame builders also needs the AIB
1856        crate::aps::aib::try_init();
1857        // reset() only rewrites fields with a declared default; StorageVec fields
1858        // (no default) persist across tests in the global NIB, so clear explicitly
1859        nib::get_ref().update_neighbor_table(|value| *value = StorageVec::new());
1860        mac.expect_ieee_address()
1861            .return_const(IeeeAddress(0xa4c1_0000_0000_0001));
1862        (guard, Nlme::new(mac))
1863    }
1864
1865    fn default_join_request(epid: u64) -> NlmeJoinRequest {
1866        NlmeJoinRequest {
1867            extended_pan_id: IeeeAddress(epid),
1868            rejoin_network: RejoinNetwork::Association,
1869            capability_information: CapabilityInformation(0x80),
1870            security_enabled: false,
1871            scan_channels: 0..0,
1872            scan_duration: 0,
1873        }
1874    }
1875
1876    #[test]
1877    fn select_parent_no_neighbors() {
1878        let (_guard, nlme) = make_nlme(MockMlme::new());
1879        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
1880        assert!(candidates.is_empty());
1881    }
1882
1883    #[test]
1884    fn select_parent_filters_by_extended_pan_id() {
1885        let (_guard, nlme) = make_nlme(MockMlme::new());
1886
1887        let mut table = StorageVec::new();
1888        // neighbor on the correct network
1889        table
1890            .push(make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0))
1891            .unwrap();
1892        // neighbor on a different network
1893        table
1894            .push(make_neighbor(0xBBBB, 0x0001, 0x9999, 200, 0))
1895            .unwrap();
1896        nlme.nib().update_neighbor_table(|value| *value = table);
1897
1898        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
1899        assert_eq!(candidates.len(), 1);
1900        assert_eq!(candidates[0], 0);
1901    }
1902
1903    #[test]
1904    fn select_parent_filters_by_link_cost() {
1905        let (_guard, nlme) = make_nlme(MockMlme::new());
1906
1907        let mut table = StorageVec::new();
1908        // good LQI => low cost => eligible
1909        table
1910            .push(make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0))
1911            .unwrap();
1912        // bad LQI => high cost => filtered out
1913        table
1914            .push(make_neighbor(0xAAAA, 0x0001, 0x1234, 10, 0))
1915            .unwrap();
1916        nlme.nib().update_neighbor_table(|value| *value = table);
1917
1918        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
1919        assert_eq!(candidates.len(), 1);
1920        assert_eq!(candidates[0], 0);
1921    }
1922
1923    #[test]
1924    fn select_parent_filters_by_end_device_capacity() {
1925        let (_guard, nlme) = make_nlme(MockMlme::new());
1926
1927        let mut table = StorageVec::new();
1928        let mut n = make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0);
1929        n.end_device_capacity = false;
1930        table.push(n).unwrap();
1931        nlme.nib().update_neighbor_table(|value| *value = table);
1932
1933        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
1934        assert!(candidates.is_empty());
1935    }
1936
1937    #[test]
1938    fn select_parent_filters_by_router_capacity() {
1939        let (_guard, nlme) = make_nlme(MockMlme::new());
1940
1941        let mut table = StorageVec::new();
1942        let mut n = make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0);
1943        n.router_capacity = false;
1944        table.push(n).unwrap();
1945        nlme.nib().update_neighbor_table(|value| *value = table);
1946
1947        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), true);
1948        assert!(candidates.is_empty());
1949    }
1950
1951    #[test]
1952    fn select_parent_end_device_accepts_router_without_router_capacity() {
1953        // a router parent that accepts end devices but not routers
1954        // (router_capacity == false, end_device_capacity == true) must remain a
1955        // valid parent for an end-device join
1956        let (_guard, nlme) = make_nlme(MockMlme::new());
1957
1958        let mut table = StorageVec::new();
1959        let mut n = make_neighbor(0xAAAA, 0x1234, 0x1234, 200, 0);
1960        n.router_capacity = false;
1961        n.end_device_capacity = true;
1962        table.push(n).unwrap();
1963        nlme.nib().update_neighbor_table(|value| *value = table);
1964
1965        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
1966        assert_eq!(candidates.len(), 1);
1967    }
1968
1969    #[test]
1970    fn select_parent_sorts_by_depth_for_stack_profile_1() {
1971        let (_guard, nlme) = make_nlme(MockMlme::new());
1972        nlme.nib().update_stack_profile(|value| *value = 1);
1973
1974        let mut table = StorageVec::new();
1975        table
1976            .push(make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 3))
1977            .unwrap();
1978        table
1979            .push(make_neighbor(0xAAAA, 0x0001, 0x1234, 200, 1))
1980            .unwrap();
1981        table
1982            .push(make_neighbor(0xAAAA, 0x0002, 0x1234, 200, 2))
1983            .unwrap();
1984        nlme.nib().update_neighbor_table(|value| *value = table);
1985
1986        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
1987        assert_eq!(candidates.len(), 3);
1988        // sorted: depth 1 (idx 1), depth 2 (idx 2), depth 3 (idx 0)
1989        assert_eq!(candidates[0], 1);
1990        assert_eq!(candidates[1], 2);
1991        assert_eq!(candidates[2], 0);
1992    }
1993
1994    #[test]
1995    fn select_parent_filters_not_permitting_join() {
1996        let (_guard, nlme) = make_nlme(MockMlme::new());
1997
1998        let mut table = StorageVec::new();
1999        let mut n = make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0);
2000        n.permit_joining = false;
2001        table.push(n).unwrap();
2002        nlme.nib().update_neighbor_table(|value| *value = table);
2003
2004        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
2005        assert!(candidates.is_empty());
2006    }
2007
2008    #[test]
2009    fn select_parent_filters_non_potential_parent() {
2010        let (_guard, nlme) = make_nlme(MockMlme::new());
2011
2012        let mut table = StorageVec::new();
2013        let mut n = make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0);
2014        n.potential_parent = 0;
2015        table.push(n).unwrap();
2016        nlme.nib().update_neighbor_table(|value| *value = table);
2017
2018        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
2019        assert!(candidates.is_empty());
2020    }
2021
2022    #[test]
2023    fn select_parent_prefers_most_recent_update_id() {
2024        let (_guard, nlme) = make_nlme(MockMlme::new());
2025
2026        let mut table = StorageVec::new();
2027        let mut n1 = make_neighbor(0xAAAA, 0x0000, 0x1234, 200, 0);
2028        n1.update_id = 5;
2029        table.push(n1).unwrap();
2030        let mut n2 = make_neighbor(0xAAAA, 0x0001, 0x1234, 200, 0);
2031        n2.update_id = 3;
2032        table.push(n2).unwrap();
2033        nlme.nib().update_neighbor_table(|value| *value = table);
2034
2035        let candidates = nlme.select_parent_candidates(IeeeAddress(0x1234), false);
2036        assert_eq!(candidates.len(), 1);
2037        assert_eq!(candidates[0], 0);
2038    }
2039
2040    #[test]
2041    fn join_successful_association() {
2042        let mut mac = MockMlme::new();
2043        mac.expect_associate().returning(|_, _, _| {
2044            Ok(AssociationResponse {
2045                device_address: IeeeAddress(0),
2046                association_address: ShortAddress(0x1234),
2047                status: AssociationStatus::Successful,
2048            })
2049        });
2050
2051        let (_guard, nlme) = make_nlme(mac);
2052
2053        let mut table = StorageVec::new();
2054        table
2055            .push(make_neighbor(0xAAAA, 0x0000, 0xDEAD, 200, 0))
2056            .unwrap();
2057        nlme.nib().update_neighbor_table(|value| *value = table);
2058
2059        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2060
2061        assert_eq!(confirm.status, NlmeJoinStatus::Success);
2062        assert_eq!(confirm.network_address.0, 0x1234);
2063        assert_eq!(confirm.extended_pan_id.0, 0xDEAD);
2064        assert_eq!(confirm.channel, 11);
2065
2066        assert_eq!(*nlme.nib().network_address(), 0x1234);
2067        assert_eq!(*nlme.nib().extended_panid(), 0xDEAD);
2068        assert_eq!(*nlme.nib().panid(), 0xAAAA);
2069        assert_eq!(*nlme.nib().update_id(), 0);
2070
2071        let table = nlme.nib().neighbor_table();
2072        assert_eq!(table[0].relationship, 0x00);
2073    }
2074
2075    #[test]
2076    fn join_sets_nwk_update_id_from_parent() {
2077        let mut mac = MockMlme::new();
2078        mac.expect_associate().returning(|_, _, _| {
2079            Ok(AssociationResponse {
2080                device_address: IeeeAddress(0),
2081                association_address: ShortAddress(0x1234),
2082                status: AssociationStatus::Successful,
2083            })
2084        });
2085
2086        let (_guard, nlme) = make_nlme(mac);
2087
2088        let mut n = make_neighbor(0xAAAA, 0x0000, 0xDEAD, 200, 0);
2089        n.update_id = 7;
2090        let mut table = StorageVec::new();
2091        table.push(n).unwrap();
2092        nlme.nib().update_neighbor_table(|value| *value = table);
2093
2094        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2095        assert_eq!(confirm.status, NlmeJoinStatus::Success);
2096        assert_eq!(*nlme.nib().update_id(), 7);
2097    }
2098
2099    #[test]
2100    fn join_fails_when_no_candidates() {
2101        let mac = MockMlme::new();
2102        let (_guard, nlme) = make_nlme(mac);
2103        nlme.nib()
2104            .update_neighbor_table(|value| *value = StorageVec::new());
2105        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2106        assert_eq!(confirm.status, NlmeJoinStatus::NotPermitted);
2107    }
2108
2109    #[test]
2110    fn join_fails_when_already_joined() {
2111        let mac = MockMlme::new();
2112        let (_guard, nlme) = make_nlme(mac);
2113        nlme.nib().update_network_address(|value| *value = 0x0001);
2114
2115        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2116        assert_eq!(confirm.status, NlmeJoinStatus::InvalidRequest);
2117    }
2118
2119    #[test]
2120    fn join_skips_capacity_rejected_parent_tries_next() {
2121        let mut mac = MockMlme::new();
2122        let mut seq = mockall::Sequence::new();
2123        mac.expect_associate()
2124            .times(1)
2125            .in_sequence(&mut seq)
2126            .returning(|_, _, _| {
2127                Ok(AssociationResponse {
2128                    device_address: IeeeAddress(0),
2129                    association_address: ShortAddress(0),
2130                    status: AssociationStatus::NetworkAtCapacity,
2131                })
2132            });
2133        mac.expect_associate()
2134            .times(1)
2135            .in_sequence(&mut seq)
2136            .returning(|_, _, _| {
2137                Ok(AssociationResponse {
2138                    device_address: IeeeAddress(0),
2139                    association_address: ShortAddress(0x5678),
2140                    status: AssociationStatus::Successful,
2141                })
2142            });
2143
2144        let (_guard, nlme) = make_nlme(mac);
2145
2146        let mut table = StorageVec::new();
2147        table
2148            .push(make_neighbor(0xAAAA, 0x0000, 0xDEAD, 200, 0))
2149            .unwrap();
2150        table
2151            .push(make_neighbor(0xAAAA, 0x0001, 0xDEAD, 200, 0))
2152            .unwrap();
2153        nlme.nib().update_neighbor_table(|value| *value = table);
2154
2155        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2156        assert_eq!(confirm.status, NlmeJoinStatus::Success);
2157        assert_eq!(confirm.network_address.0, 0x5678);
2158
2159        let table = nlme.nib().neighbor_table();
2160        assert_eq!(table[0].potential_parent, 0);
2161        assert_eq!(table[1].relationship, 0x00);
2162    }
2163
2164    #[test]
2165    fn join_all_candidates_rejected() {
2166        let mut mac = MockMlme::new();
2167        mac.expect_associate().returning(|_, _, _| {
2168            Ok(AssociationResponse {
2169                device_address: IeeeAddress(0),
2170                association_address: ShortAddress(0),
2171                status: AssociationStatus::AccessDenied,
2172            })
2173        });
2174
2175        let (_guard, nlme) = make_nlme(mac);
2176
2177        let mut table = StorageVec::new();
2178        table
2179            .push(make_neighbor(0xAAAA, 0x0000, 0xDEAD, 200, 0))
2180            .unwrap();
2181        nlme.nib().update_neighbor_table(|value| *value = table);
2182
2183        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2184        assert_eq!(confirm.status, NlmeJoinStatus::PanAccessDenied);
2185        assert_eq!(confirm.network_address.0, 0xffff);
2186    }
2187
2188    #[test]
2189    fn join_mac_error_reported() {
2190        let mut mac = MockMlme::new();
2191        mac.expect_associate()
2192            .returning(|_, _, _| Err(MacError::NoAck));
2193
2194        let (_guard, nlme) = make_nlme(mac);
2195
2196        let mut table = StorageVec::new();
2197        table
2198            .push(make_neighbor(0xAAAA, 0x0000, 0xDEAD, 200, 0))
2199            .unwrap();
2200        nlme.nib().update_neighbor_table(|value| *value = table);
2201
2202        let confirm = block_on(nlme.join(default_join_request(0xDEAD)));
2203        assert_eq!(confirm.status, NlmeJoinStatus::MacError);
2204    }
2205
2206    #[test]
2207    fn join_invalid_rejoin_network() {
2208        let mac = MockMlme::new();
2209        let (_guard, nlme) = make_nlme(mac);
2210
2211        let mut req = default_join_request(0xDEAD);
2212        req.rejoin_network = RejoinNetwork::Orphan;
2213
2214        let confirm = block_on(nlme.join(req));
2215        assert_eq!(confirm.status, NlmeJoinStatus::InvalidRequest);
2216    }
2217
2218    fn rejoin_request(epid: u64) -> NlmeJoinRequest {
2219        NlmeJoinRequest {
2220            extended_pan_id: IeeeAddress(epid),
2221            rejoin_network: RejoinNetwork::NwkRejoin,
2222            capability_information: CapabilityInformation(0x80),
2223            security_enabled: true,
2224            // no scan: these exercise the remembered-parent path
2225            scan_channels: 0..0,
2226            scan_duration: 0,
2227        }
2228    }
2229
2230    /// Build the encrypted bytes of an inbound Rejoin Response command frame,
2231    /// as `poll_data` would deliver it from the parent.
2232    fn encrypted_rejoin_response(network_address: u16, status: u8) -> heapless::Vec<u8, 128> {
2233        let nib = nib::get_ref();
2234        let header = NwkHeader {
2235            frame_control: NwkFrameControl(0)
2236                .set_frame_type(NwkFrameType::NwkCommand)
2237                .set_protocol_version(2)
2238                .set_discover_route(DiscoverRoute::Suppress)
2239                .set_security_flag(true)
2240                .set_source_ieee_flag(true),
2241            destination: ShortAddress(*nib.network_address()),
2242            source: ShortAddress(0x0000),
2243            radius: 1,
2244            sequence_number: 0,
2245            destination_ieee: None,
2246            source_ieee: Some(*nib.ieee_address()),
2247            multicast_control: None,
2248            source_route_subframe: None,
2249        };
2250        let command = Command::RejoinResponse(RejoinResponse {
2251            network_address: ShortAddress(network_address),
2252            status,
2253        });
2254        let nwk_frame = NwkFrame::NwkCommand(NwkCommandFrame { header, command });
2255        let mut buf = heapless::Vec::<u8, 128>::new();
2256        buf.resize(128, 0).unwrap();
2257        let len = SecurityContext::get()
2258            .encrypt_nwk_frame_in_place(nwk_frame, &mut buf)
2259            .unwrap();
2260        buf.truncate(len);
2261        buf
2262    }
2263
2264    #[test]
2265    fn nwk_rejoin_not_joined_is_invalid() {
2266        let (_guard, nlme) = make_nlme(MockMlme::new());
2267
2268        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
2269        assert_eq!(confirm.status, NlmeJoinStatus::InvalidRequest);
2270    }
2271
2272    #[test]
2273    fn nwk_rejoin_without_security_is_sent_unsecured() {
2274        // 4.6.3.3.2: a Trust Center rejoin carries no NWK security
2275        let mut mac = MockMlme::new();
2276        mac.expect_transmit_data()
2277            .times(1)
2278            .withf(|_dest, payload| {
2279                let (header, _) = NwkHeader::try_read(payload, ()).unwrap();
2280                !header.frame_control.security_flag()
2281            })
2282            .returning(|_, _| Ok(()));
2283        mac.expect_poll_data()
2284            .returning(|_, _| Err(MacError::NoData));
2285
2286        let (_guard, nlme) = make_nlme(mac);
2287        seed_joined_nib(&nlme);
2288
2289        let mut req = rejoin_request(0xDEAD);
2290        req.security_enabled = false;
2291
2292        let confirm = block_on(nlme.join(req));
2293        assert_eq!(confirm.status, NlmeJoinStatus::MacError);
2294    }
2295
2296    #[test]
2297    fn nwk_rejoin_epid_mismatch_is_invalid() {
2298        let (_guard, nlme) = make_nlme(MockMlme::new());
2299        seed_joined_nib(&nlme);
2300
2301        let confirm = block_on(nlme.join(rejoin_request(0xBEEF)));
2302        assert_eq!(confirm.status, NlmeJoinStatus::InvalidRequest);
2303    }
2304
2305    #[test]
2306    fn nwk_rejoin_success_sends_request_and_updates_address() {
2307        let mut mac = MockMlme::new();
2308        mac.expect_transmit_data()
2309            .times(1)
2310            .withf(|dest, payload| {
2311                let mut buf = [0u8; 256];
2312                buf[..payload.len()].copy_from_slice(payload);
2313                let Ok(NwkFrame::NwkCommand(command_frame)) =
2314                    SecurityContext::get().decrypt_nwk_frame_in_place(&mut buf[..payload.len()])
2315                else {
2316                    return false;
2317                };
2318                let Command::RejoinRequest(request) = command_frame.command else {
2319                    return false;
2320                };
2321                *dest == Address::Short(PanId(0xAAAA), MacShortAddress(0x0000))
2322                    // rejoin_request() uses CapabilityInformation(0x80):
2323                    // allocate address set, device_type (end device) clear
2324                    && request.capability_information.device_type() == 0
2325                    && request.capability_information.allocate_address() == 1
2326            })
2327            .returning(|_, _| Ok(()));
2328        mac.expect_poll_data().times(1).returning(|_, buf| {
2329            let response = encrypted_rejoin_response(0x5678, 0x00);
2330            buf[..response.len()].copy_from_slice(&response);
2331            Ok((response.len(), 200u8))
2332        });
2333        // the assigned address has to reach the hardware filter
2334        mac.expect_configure()
2335            .times(1)
2336            .withf(|config| config.short_address == Some(ShortAddress(0x5678)))
2337            .returning(|_| ());
2338
2339        let (_guard, nlme) = make_nlme(mac);
2340        seed_joined_nib(&nlme);
2341
2342        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
2343        assert_eq!(confirm.status, NlmeJoinStatus::Success);
2344        assert_eq!(confirm.network_address, ShortAddress(0x5678));
2345        assert_eq!(*nlme.nib().network_address(), 0x5678);
2346    }
2347
2348    #[test]
2349    fn rejoin_response_from_receive_path_applies_address_and_signals() {
2350        // a concurrent receive loop may retrieve the response instead of the
2351        // rejoin procedure's own poll
2352        let (_guard, nlme) = make_nlme(MockMlme::new());
2353        seed_joined_nib(&nlme);
2354
2355        block_on(nlme.handle_nwk_command(
2356            &dummy_header(0x0000),
2357            Command::RejoinResponse(RejoinResponse {
2358                network_address: ShortAddress(0x5678),
2359                status: 0x00,
2360            }),
2361        ));
2362
2363        assert_eq!(*nlme.nib().network_address(), 0x5678);
2364        block_on(nlme.rejoin_response.wait());
2365    }
2366
2367    #[test]
2368    fn nwk_rejoin_network_at_capacity() {
2369        let mut mac = MockMlme::new();
2370        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2371        mac.expect_poll_data().times(1).returning(|_, buf| {
2372            let response = encrypted_rejoin_response(0xffff, 0x01);
2373            buf[..response.len()].copy_from_slice(&response);
2374            Ok((response.len(), 200u8))
2375        });
2376
2377        let (_guard, nlme) = make_nlme(mac);
2378        seed_joined_nib(&nlme);
2379
2380        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
2381        assert_eq!(confirm.status, NlmeJoinStatus::PanAtCapacity);
2382    }
2383
2384    #[test]
2385    fn nwk_rejoin_no_response_is_mac_error() {
2386        let mut mac = MockMlme::new();
2387        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2388        mac.expect_poll_data()
2389            .returning(|_, _| Err(MacError::NoData));
2390
2391        let (_guard, nlme) = make_nlme(mac);
2392        seed_joined_nib(&nlme);
2393
2394        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
2395        assert_eq!(confirm.status, NlmeJoinStatus::MacError);
2396    }
2397
2398    #[test]
2399    fn nwk_rejoin_transmit_failure_is_mac_error() {
2400        let mut mac = MockMlme::new();
2401        mac.expect_transmit_data()
2402            .times(1)
2403            .returning(|_, _| Err(MacError::NoAck));
2404
2405        let (_guard, nlme) = make_nlme(mac);
2406        seed_joined_nib(&nlme);
2407
2408        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
2409        assert_eq!(confirm.status, NlmeJoinStatus::MacError);
2410    }
2411
2412    /// A minimal NWK header for feeding `handle_nwk_command` in tests; the
2413    /// source address is the only field most handlers inspect.
2414    fn dummy_header(source: u16) -> NwkHeader<'static> {
2415        NwkHeader {
2416            frame_control: NwkFrameControl(0).set_frame_type(NwkFrameType::NwkCommand),
2417            destination: ShortAddress(0x0000),
2418            source: ShortAddress(source),
2419            radius: 1,
2420            sequence_number: 0,
2421            destination_ieee: None,
2422            source_ieee: None,
2423            multicast_control: None,
2424            source_route_subframe: None,
2425        }
2426    }
2427
2428    /// Seed the global NIB with a joined-network state: parent neighbor,
2429    /// addresses, and NWK security material.
2430    fn seed_joined_nib(nlme: &Nlme<MockMlme>) {
2431        use zigbee_types::ByteArray;
2432
2433        use crate::nwk::nib::NetworkSecurityMaterialDescriptor;
2434
2435        let nib = nlme.nib();
2436        nib.update_panid(|value| *value = 0xAAAA);
2437        nib.update_network_address(|value| *value = 0x1234);
2438        nib.update_extended_panid(|value| *value = 0xDEAD);
2439
2440        let mut table = StorageVec::new();
2441        let mut parent = make_neighbor(0xAAAA, 0x0000, 0xDEAD, 200, 0);
2442        parent.relationship = relationship::PARENT;
2443        table.push(parent).unwrap();
2444        nib.update_neighbor_table(|value| *value = table);
2445
2446        let mut set = StorageVec::new();
2447        set.push(NetworkSecurityMaterialDescriptor {
2448            key_seq_number: 0,
2449            outgoing_frame_counter: 1,
2450            incoming_frame_counter_set: StorageVec::new(),
2451            key: ByteArray([0x42; 16]),
2452            network_key_type: 0,
2453        })
2454        .unwrap();
2455        nib.update_security_material_set(|value| *value = set);
2456    }
2457
2458    #[test]
2459    fn send_end_device_timeout_request_encodes_command() {
2460        let mut mac = MockMlme::new();
2461        mac.expect_transmit_data()
2462            .times(1)
2463            .withf(|dest, payload| {
2464                assert_eq!(
2465                    *dest,
2466                    Address::Short(PanId(0xAAAA), MacShortAddress(0x0000))
2467                );
2468                // header is unencrypted; verify it directly
2469                let (header, _) = NwkHeader::try_read(payload, ()).unwrap();
2470                assert_eq!(header.frame_control.frame_type(), NwkFrameType::NwkCommand);
2471                assert!(header.frame_control.security_flag());
2472                assert!(header.frame_control.source_ieee_flag());
2473                assert_eq!(header.radius, 1);
2474                assert_eq!(header.destination, ShortAddress(0x0000));
2475                assert_eq!(header.source, ShortAddress(0x1234));
2476
2477                // decrypt in place to verify the command payload
2478                let mut buf = [0u8; 256];
2479                buf[..payload.len()].copy_from_slice(payload);
2480                let frame = SecurityContext::get()
2481                    .decrypt_nwk_frame_in_place(&mut buf[..payload.len()])
2482                    .unwrap();
2483                let NwkFrame::NwkCommand(command_frame) = frame else {
2484                    panic!("expected NWK command frame");
2485                };
2486                let Command::EndDeviceTimeoutRequest(request) = command_frame.command else {
2487                    panic!("expected end device timeout request");
2488                };
2489                assert_eq!(request.requested_timeout, 0x08);
2490                assert_eq!(request.end_device_configuration, 0x00);
2491                true
2492            })
2493            .returning(|_, _| Ok(()));
2494
2495        let (_guard, nlme) = make_nlme(mac);
2496        seed_joined_nib(&nlme);
2497
2498        block_on(nlme.send_end_device_timeout_request()).unwrap();
2499        assert_eq!(
2500            nlme.pending_timeout_request.load(Ordering::Relaxed),
2501            *nlme.nib().end_device_timeout_default()
2502        );
2503    }
2504
2505    #[test]
2506    fn send_end_device_timeout_request_not_joined() {
2507        let (_guard, nlme) = make_nlme(MockMlme::new());
2508        let result = block_on(nlme.send_end_device_timeout_request());
2509        assert!(matches!(result, Err(NetworkError::NotJoined)));
2510    }
2511
2512    #[test]
2513    fn end_device_timeout_response_success_updates_nib() {
2514        let (_guard, nlme) = make_nlme(MockMlme::new());
2515        nlme.pending_timeout_request.store(0x03, Ordering::Relaxed);
2516
2517        block_on(nlme.handle_nwk_command(
2518            &dummy_header(0x0000),
2519            Command::EndDeviceTimeoutResponse(EndDeviceTimeoutResponse {
2520                status: EndDeviceTimeoutResponse::STATUS_SUCCESS,
2521                parent_information: 0b011,
2522            }),
2523        ));
2524
2525        let nib = nlme.nib();
2526        assert_eq!(*nib.parent_information(), 0b011);
2527        assert_eq!(*nib.end_device_timeout(), 0x03);
2528        // pending request consumed
2529        assert_eq!(
2530            nlme.pending_timeout_request.load(Ordering::Relaxed),
2531            NO_PENDING_TIMEOUT
2532        );
2533    }
2534
2535    #[test]
2536    fn end_device_timeout_response_rejected_leaves_nib_untouched() {
2537        let (_guard, nlme) = make_nlme(MockMlme::new());
2538        nlme.pending_timeout_request.store(0x03, Ordering::Relaxed);
2539
2540        block_on(nlme.handle_nwk_command(
2541            &dummy_header(0x0000),
2542            Command::EndDeviceTimeoutResponse(EndDeviceTimeoutResponse {
2543                status: EndDeviceTimeoutResponse::STATUS_INCORRECT_VALUE,
2544                parent_information: 0b001,
2545            }),
2546        ));
2547
2548        let nib = nlme.nib();
2549        assert_eq!(*nib.parent_information(), 0x00);
2550        assert_eq!(*nib.end_device_timeout(), NO_PENDING_TIMEOUT);
2551    }
2552
2553    #[test]
2554    fn end_device_timeout_response_unsolicited_ignored() {
2555        let (_guard, nlme) = make_nlme(MockMlme::new());
2556
2557        block_on(nlme.handle_nwk_command(
2558            &dummy_header(0x0000),
2559            Command::EndDeviceTimeoutResponse(EndDeviceTimeoutResponse {
2560                status: EndDeviceTimeoutResponse::STATUS_SUCCESS,
2561                parent_information: 0b001,
2562            }),
2563        ));
2564
2565        let nib = nlme.nib();
2566        assert_eq!(*nib.parent_information(), 0x00);
2567        assert_eq!(*nib.end_device_timeout(), NO_PENDING_TIMEOUT);
2568    }
2569
2570    #[test]
2571    fn leave_not_joined_is_invalid() {
2572        let (_guard, nlme) = make_nlme(MockMlme::new());
2573
2574        let confirm = block_on(nlme.leave(NlmeLeaveRequest {
2575            device_address: None,
2576            remove_children: false,
2577            rejoin: false,
2578        }));
2579        assert_eq!(confirm.status, NlmeLeaveStatus::InvalidRequest);
2580    }
2581
2582    #[test]
2583    fn leave_other_device_is_unknown() {
2584        let (_guard, nlme) = make_nlme(MockMlme::new());
2585        seed_joined_nib(&nlme);
2586
2587        let confirm = block_on(nlme.leave(NlmeLeaveRequest {
2588            device_address: Some(IeeeAddress(0x1111_1111_1111_1111)),
2589            remove_children: false,
2590            rejoin: false,
2591        }));
2592        assert_eq!(confirm.status, NlmeLeaveStatus::UnknownDevice);
2593    }
2594
2595    #[test]
2596    fn leave_self_end_device_unicasts_to_parent_and_clears_nib() {
2597        let mut mac = MockMlme::new();
2598        mac.expect_transmit_data()
2599            .times(1)
2600            .withf(|dest, payload| {
2601                assert_eq!(
2602                    *dest,
2603                    Address::Short(PanId(0xAAAA), MacShortAddress(0x0000))
2604                );
2605                let mut buf = [0u8; 256];
2606                buf[..payload.len()].copy_from_slice(payload);
2607                let frame = SecurityContext::get()
2608                    .decrypt_nwk_frame_in_place(&mut buf[..payload.len()])
2609                    .unwrap();
2610                let NwkFrame::NwkCommand(command_frame) = frame else {
2611                    panic!("expected NWK command frame");
2612                };
2613                let Command::Leave(leave) = command_frame.command else {
2614                    panic!("expected leave command");
2615                };
2616                assert!(!leave.command_options.request());
2617                assert!(!leave.command_options.rejoin());
2618                assert!(!leave.command_options.remove_children());
2619                true
2620            })
2621            .returning(|_, _| Ok(()));
2622
2623        let (_guard, nlme) = make_nlme(mac);
2624        seed_joined_nib(&nlme);
2625
2626        // 3.6.1.10.1: an end device sends the remove children sub-field as 0
2627        // even when the request asked for it
2628        let confirm = block_on(nlme.leave(NlmeLeaveRequest {
2629            device_address: None,
2630            remove_children: true,
2631            rejoin: false,
2632        }));
2633        assert_eq!(confirm.status, NlmeLeaveStatus::Success);
2634        assert_eq!(confirm.device_address, None);
2635
2636        let nib = nlme.nib();
2637        assert_eq!(*nib.network_address(), 0xffff);
2638        assert_eq!(*nib.extended_panid(), 0);
2639        assert!(nib.neighbor_table().is_empty());
2640        assert!(nib.security_material_set().is_empty());
2641    }
2642
2643    #[test]
2644    fn leave_self_clears_nib_even_when_transmit_fails() {
2645        let mut mac = MockMlme::new();
2646        mac.expect_transmit_data()
2647            .times(1)
2648            .returning(|_, _| Err(MacError::NoAck));
2649
2650        let (_guard, nlme) = make_nlme(mac);
2651        seed_joined_nib(&nlme);
2652
2653        let confirm = block_on(nlme.leave(NlmeLeaveRequest {
2654            device_address: None,
2655            remove_children: false,
2656            rejoin: false,
2657        }));
2658        // 3.6.1.10.1: the local leave runs regardless of the confirm status
2659        assert_eq!(confirm.status, NlmeLeaveStatus::MacError);
2660        assert_eq!(*nlme.nib().network_address(), NWK_UNASSIGNED_ADDRESS);
2661        assert!(nlme.nib().security_material_set().is_empty());
2662    }
2663
2664    #[test]
2665    fn leave_request_to_broadcast_address_is_dropped() {
2666        let (_guard, nlme) = make_nlme(MockMlme::new());
2667        seed_joined_nib(&nlme);
2668
2669        let header = NwkHeader {
2670            destination: ShortAddress(0xfffd),
2671            ..dummy_header(0x0000)
2672        };
2673        let leave = Leave {
2674            command_options: LeaveCommandOptions(0).set_request(true),
2675        };
2676        // no transmit_data expectation set: a call here would panic the mock
2677        block_on(nlme.handle_nwk_command(&header, Command::Leave(leave)));
2678
2679        assert_eq!(*nlme.nib().network_address(), 0x1234);
2680    }
2681
2682    #[test]
2683    fn leave_notification_from_parent_raises_indication() {
2684        let (_guard, nlme) = make_nlme(MockMlme::new());
2685        seed_joined_nib(&nlme);
2686
2687        let leave = Leave {
2688            command_options: LeaveCommandOptions(0).set_rejoin(true),
2689        };
2690        block_on(nlme.handle_nwk_command(&dummy_header(0x0000), Command::Leave(leave)));
2691
2692        // 3.6.1.10.3: removed by the parent -> NULL device address
2693        assert_eq!(
2694            nlme.take_leave_indication(),
2695            Some(NlmeLeaveIndication {
2696                device_address: None,
2697                rejoin: true,
2698            })
2699        );
2700    }
2701
2702    #[test]
2703    fn leave_self_with_rejoin_leaves_nib_untouched() {
2704        let mut mac = MockMlme::new();
2705        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2706
2707        let (_guard, nlme) = make_nlme(mac);
2708        seed_joined_nib(&nlme);
2709
2710        let confirm = block_on(nlme.leave(NlmeLeaveRequest {
2711            device_address: None,
2712            remove_children: false,
2713            rejoin: true,
2714        }));
2715        assert_eq!(confirm.status, NlmeLeaveStatus::Success);
2716
2717        let nib = nlme.nib();
2718        // rejoin requested: the NIB is left intact for the (not yet
2719        // implemented) rejoin procedure, aside from the extended PAN id
2720        // which 3.6.1.10.1 clears unconditionally for an end device
2721        assert_eq!(*nib.network_address(), 0x1234);
2722        assert_eq!(*nib.extended_panid(), 0);
2723        assert!(!nib.neighbor_table().is_empty());
2724    }
2725
2726    #[test]
2727    fn leave_notification_removes_only_the_notifying_neighbor() {
2728        let (_guard, nlme) = make_nlme(MockMlme::new());
2729        seed_joined_nib(&nlme);
2730        nlme.nib().update_neighbor_table(|table| {
2731            let _ = table.push(make_neighbor(0xAAAA, 0x5678, 0xBEEF, 200, 1));
2732        });
2733        assert_eq!(nlme.nib().neighbor_table().len(), 2);
2734
2735        let leave = Leave {
2736            command_options: LeaveCommandOptions(0),
2737        };
2738        block_on(nlme.handle_nwk_command(&dummy_header(0x5678), Command::Leave(leave)));
2739
2740        let nib = nlme.nib();
2741        assert_eq!(nib.neighbor_table().len(), 1);
2742        assert_eq!(
2743            nib.neighbor_table()[0].network_address,
2744            ShortAddress(0x0000)
2745        );
2746        // not our parent -> we are still joined
2747        assert_ne!(*nib.network_address(), 0xffff);
2748    }
2749
2750    #[test]
2751    fn leave_notification_from_parent_marks_device_removed() {
2752        let (_guard, nlme) = make_nlme(MockMlme::new());
2753        seed_joined_nib(&nlme);
2754
2755        let leave = Leave {
2756            command_options: LeaveCommandOptions(0),
2757        };
2758        block_on(nlme.handle_nwk_command(&dummy_header(0x0000), Command::Leave(leave)));
2759
2760        let nib = nlme.nib();
2761        assert_eq!(*nib.extended_panid(), 0);
2762        assert!(nib.neighbor_table().is_empty());
2763    }
2764
2765    #[test]
2766    fn leave_request_from_parent_triggers_self_removal() {
2767        let mut mac = MockMlme::new();
2768        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2769
2770        let (_guard, nlme) = make_nlme(mac);
2771        seed_joined_nib(&nlme);
2772
2773        let leave = Leave {
2774            command_options: LeaveCommandOptions(0).set_request(true),
2775        };
2776        block_on(nlme.handle_nwk_command(&dummy_header(0x0000), Command::Leave(leave)));
2777
2778        assert_eq!(*nlme.nib().network_address(), 0xffff);
2779    }
2780
2781    #[test]
2782    fn leave_request_from_non_parent_is_ignored() {
2783        let (_guard, nlme) = make_nlme(MockMlme::new());
2784        seed_joined_nib(&nlme);
2785
2786        let leave = Leave {
2787            command_options: LeaveCommandOptions(0).set_request(true),
2788        };
2789        // no transmit_data expectation set: a call here would panic the mock
2790        block_on(nlme.handle_nwk_command(&dummy_header(0x9999), Command::Leave(leave)));
2791
2792        assert_eq!(*nlme.nib().network_address(), 0x1234);
2793    }
2794
2795    #[test]
2796    fn leave_request_dropped_for_coordinator() {
2797        let (_guard, nlme) = make_nlme(MockMlme::new());
2798        seed_joined_nib(&nlme);
2799        nlme.nib().update_network_address(|value| *value = 0x0000);
2800
2801        let leave = Leave {
2802            command_options: LeaveCommandOptions(0).set_request(true),
2803        };
2804        // no transmit_data expectation set: a call here would panic the mock
2805        block_on(nlme.handle_nwk_command(&dummy_header(0x0000), Command::Leave(leave)));
2806
2807        assert_eq!(*nlme.nib().network_address(), 0x0000);
2808    }
2809
2810    #[test]
2811    fn negotiated_poll_interval() {
2812        let (_guard, nlme) = make_nlme(MockMlme::new());
2813        let nib = nlme.nib();
2814
2815        // not negotiated
2816        assert_eq!(nlme.negotiated_poll_interval_ms(), None);
2817
2818        // negotiated 2 min with MAC data poll keepalive: 120 s / 3 = 40 s
2819        nib.update_end_device_timeout(|value| *value = 1);
2820        nib.update_parent_information(|value| {
2821            *value = EndDeviceTimeoutResponse::MAC_DATA_POLL_KEEPALIVE;
2822        });
2823        assert_eq!(nlme.negotiated_poll_interval_ms(), Some(40_000));
2824
2825        // parent without MAC data poll keepalive support
2826        nib.update_parent_information(|value| {
2827            *value = EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE;
2828        });
2829        assert_eq!(nlme.negotiated_poll_interval_ms(), None);
2830    }
2831
2832    #[test]
2833    fn keepalive_method_follows_parent_information() {
2834        let (_guard, nlme) = make_nlme(MockMlme::new());
2835        let nib = nlme.nib();
2836
2837        // no negotiated timeout
2838        assert_eq!(nlme.keepalive_method(), KeepaliveMethod::None);
2839
2840        nib.update_end_device_timeout(|value| *value = 1);
2841        nib.update_parent_information(|value| {
2842            *value = EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE;
2843        });
2844        assert_eq!(nlme.keepalive_method(), KeepaliveMethod::TimeoutRequest);
2845
2846        // bit 0 takes precedence over bit 1
2847        nib.update_parent_information(|value| {
2848            *value = EndDeviceTimeoutResponse::MAC_DATA_POLL_KEEPALIVE
2849                | EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE;
2850        });
2851        assert_eq!(nlme.keepalive_method(), KeepaliveMethod::MacDataPoll);
2852    }
2853
2854    #[test]
2855    fn local_timeout_expiry_raises_parent_link_failure() {
2856        let (_guard, nlme) = make_nlme(MockMlme::new());
2857        seed_joined_nib(&nlme);
2858        let nib = nlme.nib();
2859
2860        // no negotiated timeout: nothing to track
2861        assert!(!nlme.tick_parent_timeout(10_000));
2862        assert!(nlme.take_nwk_status_indication().is_none());
2863
2864        // 10 s timeout, refreshed by the negotiation
2865        nib.update_end_device_timeout(|value| *value = 0);
2866        nib.update_parent_information(|value| {
2867            *value = EndDeviceTimeoutResponse::MAC_DATA_POLL_KEEPALIVE;
2868        });
2869        nlme.refresh_parent_timeout();
2870
2871        assert!(!nlme.tick_parent_timeout(4_000));
2872        assert!(!nlme.tick_parent_timeout(4_000));
2873        assert!(nlme.tick_parent_timeout(4_000));
2874
2875        let indication = nlme
2876            .take_nwk_status_indication()
2877            .expect("indication raised");
2878        assert_eq!(indication.status, NetworkStatusCode::ParentLinkFailure);
2879        assert_eq!(indication.network_address, ShortAddress(0x0000));
2880
2881        // expiry stops the tracking: it fires once, until the next keepalive
2882        assert!(!nlme.tick_parent_timeout(4_000));
2883        assert!(nlme.take_nwk_status_indication().is_none());
2884    }
2885
2886    #[test]
2887    fn keepalive_without_response_raises_parent_link_failure() {
2888        let mut mac = MockMlme::new();
2889        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2890        mac.expect_poll_data()
2891            .returning(|_, _| Err(MacError::NoData));
2892
2893        let (_guard, nlme) = make_nlme(mac);
2894        seed_joined_nib(&nlme);
2895        let nib = nlme.nib();
2896        nib.update_end_device_timeout(|value| *value = 1);
2897        nib.update_parent_information(|value| {
2898            *value = EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE;
2899        });
2900
2901        let result = block_on(nlme.send_keepalive());
2902        assert!(matches!(result, Err(NetworkError::ParentLinkFailure)));
2903        assert_eq!(
2904            nlme.take_nwk_status_indication().map(|i| i.status),
2905            Some(NetworkStatusCode::ParentLinkFailure)
2906        );
2907    }
2908
2909    #[test]
2910    fn keepalive_response_refreshes_local_timeout() {
2911        // the parent answers the keepalive on the next poll
2912        let mut mac = MockMlme::new();
2913        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2914        mac.expect_poll_data().returning(|_, buf| {
2915            let frame = encrypted_timeout_response(EndDeviceTimeoutResponse::STATUS_SUCCESS);
2916            buf[..frame.len()].copy_from_slice(&frame);
2917            Ok((frame.len(), 200))
2918        });
2919
2920        let (_guard, nlme) = make_nlme(mac);
2921        seed_joined_nib(&nlme);
2922        let nib = nlme.nib();
2923        // request and negotiate the 120 s timeout enumeration
2924        nib.update_end_device_timeout_default(|value| *value = 1);
2925        nib.update_end_device_timeout(|value| *value = 1);
2926        nib.update_parent_information(|value| {
2927            *value = EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE;
2928        });
2929
2930        assert!(block_on(nlme.send_keepalive()).is_ok());
2931        assert!(nlme.take_nwk_status_indication().is_none());
2932        // the negotiated 120 s are being tracked again
2933        assert!(!nlme.tick_parent_timeout(119_000));
2934        assert!(nlme.tick_parent_timeout(1_000));
2935    }
2936
2937    #[test]
2938    fn repeated_transmit_failures_report_parent_link_failure() {
2939        let mut mac = MockMlme::new();
2940        mac.expect_transmit_data()
2941            .returning(|_, _| Err(MacError::NoAck));
2942
2943        let (_guard, nlme) = make_nlme(mac);
2944        seed_joined_nib(&nlme);
2945
2946        // 3.6.3.7: a single failure does not condemn the link
2947        for _ in 1..MAX_PARENT_TRANSMIT_FAILURES {
2948            assert!(matches!(
2949                block_on(nlme.send_data(ShortAddress(0x0000), true, &[1, 2, 3])),
2950                Err(NetworkError::MacError(_))
2951            ));
2952            assert!(nlme.take_nwk_status_indication().is_none());
2953        }
2954
2955        // 3.6.3.7.1: the failed link to the parent is reported as 0x09
2956        assert!(matches!(
2957            block_on(nlme.send_data(ShortAddress(0x0000), true, &[1, 2, 3])),
2958            Err(NetworkError::ParentLinkFailure)
2959        ));
2960        assert_eq!(
2961            nlme.take_nwk_status_indication().map(|i| i.status),
2962            Some(NetworkStatusCode::ParentLinkFailure)
2963        );
2964        // the counter restarts, so the next failure is not reported at once
2965        assert!(matches!(
2966            block_on(nlme.send_data(ShortAddress(0x0000), true, &[1, 2, 3])),
2967            Err(NetworkError::MacError(_))
2968        ));
2969    }
2970
2971    #[test]
2972    fn successful_transmission_clears_the_failure_counter() {
2973        let mut mac = MockMlme::new();
2974        mac.expect_transmit_data()
2975            .times(1)
2976            .returning(|_, _| Err(MacError::NoAck));
2977        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
2978
2979        let (_guard, nlme) = make_nlme(mac);
2980        seed_joined_nib(&nlme);
2981
2982        let _ = block_on(nlme.send_data(ShortAddress(0x0000), true, &[1, 2, 3]));
2983        assert!(block_on(nlme.send_data(ShortAddress(0x0000), true, &[1, 2, 3])).is_ok());
2984
2985        let table = nlme.nib().neighbor_table();
2986        let parent = table
2987            .iter()
2988            .find(|n| n.relationship == relationship::PARENT)
2989            .expect("parent");
2990        assert_eq!(parent.transmit_failure, 0);
2991    }
2992
2993    #[test]
2994    fn select_rejoin_candidates_ignores_permit_joining() {
2995        let (_guard, nlme) = make_nlme(MockMlme::new());
2996        let mut closed = make_neighbor(0xAAAA, 0x1234, 0xDEAD, 200, 1);
2997        closed.permit_joining = false;
2998        nlme.nib().update_neighbor_table(|table| {
2999            let _ = table.push(closed);
3000        });
3001
3002        // a closed network still accepts a device it already knows (3.6.1.4.2)
3003        assert!(
3004            nlme.select_parent_candidates(IeeeAddress(0xDEAD), false)
3005                .is_empty()
3006        );
3007        assert_eq!(
3008            nlme.select_rejoin_candidates(IeeeAddress(0xDEAD), false),
3009            heapless::Vec::<usize, 16>::from_slice(&[0]).unwrap()
3010        );
3011    }
3012
3013    #[test]
3014    fn rejoin_scans_when_the_known_parent_is_gone() {
3015        let mut mac = MockMlme::new();
3016        // the remembered parent no longer answers, so a scan follows
3017        mac.expect_transmit_data()
3018            .times(1)
3019            .returning(|_, _| Err(MacError::NoAck));
3020        mac.expect_scan_network().times(1).returning(|_, _, _| {
3021            Ok(ScanResult {
3022                scan_type: ScanType::Active,
3023                pan_descriptor: Default::default(),
3024            })
3025        });
3026
3027        let (_guard, nlme) = make_nlme(mac);
3028        seed_joined_nib(&nlme);
3029
3030        let mut request = rejoin_request(0xDEAD);
3031        request.scan_channels = 20..21;
3032        let confirm = block_on(nlme.join(request));
3033
3034        // the scan found no beacon of the remembered network
3035        assert_eq!(confirm.status, NlmeJoinStatus::NotPermitted);
3036    }
3037
3038    #[test]
3039    fn adopt_parent_follows_the_candidate_channel() {
3040        let mut mac = MockMlme::new();
3041        mac.expect_configure()
3042            .times(1)
3043            .withf(|config| config.channel == Some(20))
3044            .returning(|_| ());
3045
3046        let (_guard, nlme) = make_nlme(mac);
3047        seed_joined_nib(&nlme);
3048        let mut candidate = make_neighbor(0xAAAA, 0x1234, 0xDEAD, 200, 1);
3049        candidate.logical_channel = 20;
3050        candidate.update_id = 7;
3051        nlme.nib().update_neighbor_table(|table| {
3052            let _ = table.push(candidate);
3053        });
3054
3055        let adopted = block_on(nlme.adopt_parent(1));
3056
3057        assert_eq!(adopted, Some((ShortAddress(0x1234), 20)));
3058        assert_eq!(nlme.parent_short_address().unwrap(), ShortAddress(0x1234));
3059        assert_eq!(*nlme.nib().update_id(), 7);
3060        // the previous parent is demoted: only one entry is the parent
3061        let table = nlme.nib().neighbor_table();
3062        assert_eq!(
3063            table
3064                .iter()
3065                .filter(|n| n.relationship == relationship::PARENT)
3066                .count(),
3067            1
3068        );
3069    }
3070
3071    #[test]
3072    fn rejoin_without_scan_channels_keeps_the_remembered_parent_failure() {
3073        let mut mac = MockMlme::new();
3074        mac.expect_transmit_data()
3075            .times(1)
3076            .returning(|_, _| Err(MacError::NoAck));
3077        // no scan_network expectation: a call here would panic the mock
3078
3079        let (_guard, nlme) = make_nlme(mac);
3080        seed_joined_nib(&nlme);
3081
3082        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
3083        assert_eq!(confirm.status, NlmeJoinStatus::MacError);
3084    }
3085
3086    #[test]
3087    fn leave_request_from_parent_raises_indication_with_rejoin_flag() {
3088        let mut mac = MockMlme::new();
3089        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
3090
3091        let (_guard, nlme) = make_nlme(mac);
3092        seed_joined_nib(&nlme);
3093
3094        let leave = Leave {
3095            command_options: LeaveCommandOptions(0).set_request(true).set_rejoin(true),
3096        };
3097        block_on(nlme.handle_nwk_command(&dummy_header(0x0000), Command::Leave(leave)));
3098
3099        // 3.2.2.19: removed by the parent -> NULL device address; the higher
3100        // layer drives the rejoin (3.6.1.10.4 step 1)
3101        assert_eq!(
3102            nlme.take_leave_indication(),
3103            Some(NlmeLeaveIndication {
3104                device_address: None,
3105                rejoin: true,
3106            })
3107        );
3108    }
3109
3110    #[test]
3111    fn rejoin_after_leave_accepts_the_requested_extended_pan_id() {
3112        let mut mac = MockMlme::new();
3113        mac.expect_transmit_data().times(1).returning(|_, _| Ok(()));
3114        mac.expect_poll_data().returning(|_, buf| {
3115            let frame = encrypted_rejoin_response(0x5678, 0x00);
3116            buf[..frame.len()].copy_from_slice(&frame);
3117            Ok((frame.len(), 200))
3118        });
3119        mac.expect_configure().times(1).returning(|_| ());
3120
3121        let (_guard, nlme) = make_nlme(mac);
3122        seed_joined_nib(&nlme);
3123        // a leave zeroed the extended PAN id (3.6.1.10.1)
3124        nlme.nib().update_extended_panid(|value| *value = 0);
3125
3126        let confirm = block_on(nlme.join(rejoin_request(0xDEAD)));
3127
3128        assert_eq!(confirm.status, NlmeJoinStatus::Success);
3129        assert_eq!(*nlme.nib().extended_panid(), 0xDEAD);
3130    }
3131
3132    /// An encrypted End Device Timeout Response as the parent sends it.
3133    fn encrypted_timeout_response(status: u8) -> heapless::Vec<u8, 128> {
3134        let nib = nib::get_ref();
3135        let header = NwkHeader {
3136            frame_control: NwkFrameControl(0)
3137                .set_frame_type(NwkFrameType::NwkCommand)
3138                .set_protocol_version(2)
3139                .set_discover_route(DiscoverRoute::Suppress)
3140                .set_security_flag(true)
3141                .set_source_ieee_flag(true),
3142            destination: ShortAddress(*nib.network_address()),
3143            source: ShortAddress(0x0000),
3144            radius: 1,
3145            sequence_number: 0,
3146            destination_ieee: None,
3147            source_ieee: Some(*nib.ieee_address()),
3148            multicast_control: None,
3149            source_route_subframe: None,
3150        };
3151        let command = Command::EndDeviceTimeoutResponse(EndDeviceTimeoutResponse {
3152            status,
3153            parent_information: EndDeviceTimeoutResponse::TIMEOUT_REQUEST_KEEPALIVE,
3154        });
3155        let nwk_frame = NwkFrame::NwkCommand(NwkCommandFrame { header, command });
3156        let mut buf = heapless::Vec::<u8, 128>::new();
3157        buf.resize(128, 0).unwrap();
3158        let len = SecurityContext::get()
3159            .encrypt_nwk_frame_in_place(nwk_frame, &mut buf)
3160            .unwrap();
3161        buf.truncate(len);
3162        buf
3163    }
3164}