Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions crates/ziggurat-driver/src/zigbee_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -788,8 +788,29 @@ pub enum ZigbeeNotification {
/// A unique trust center link key was negotiated with a device; the client should
/// persist it so the device survives a stack restart
LinkKeyUpdate { ieee: Eui64, key: Key },
/// A device joined or rejoined the network, directly or through a router
DeviceJoined { nwk: Nwk, ieee: Eui64, parent: Nwk },
/// A device joined or rejoined the network, directly or through a router. The
/// capability is known for direct association/rejoin (our children) and `None` for
/// joins learned second-hand via an APS Update-Device from a router.
DeviceJoined {
nwk: Nwk,
ieee: Eui64,
parent: Nwk,
device_type: Option<NwkDeviceType>,
rx_on_when_idle: bool,
},
/// A routing table entry's active route changed.
RouteChanged {
destination: Nwk,
next_hop: Nwk,
path_cost: u8,
},
/// A routing table entry was removed; the client drops it from its persisted cache
RouteRemoved { destination: Nwk },
/// A source route (relay list) to a destination was learned or cleared
RouteRecord { destination: Nwk, relays: Vec<Nwk> },
/// The outgoing APS security frame counter has advanced; the client persists it to
/// prevent a rollback on restart
ApsFrameCounterUpdate { frame_counter: u32 },
/// A device left the network. The EUI64 is unknown when the leaving device never
/// made it into the address map. `reason` records how we learned of the departure.
DeviceLeft {
Expand Down Expand Up @@ -920,6 +941,9 @@ pub struct ZigbeeStack<P: RadioPhy, R: Runtime = crate::runtime::DefaultRuntime>
pub(crate) pending_route_wake: Notify,
/// Monotonic tiebreaker giving equal-priority sends FIFO order in `send_queue`.
pub(crate) send_seq: AtomicU32,
/// The outgoing APS frame counter last streamed to the client, so a persist
/// notification only fires every `FRAME_COUNTER_NOTIFY_INTERVAL` frames.
last_persisted_aps_counter: AtomicU32,

/// Spawns and owns the stack's background tasks, so that a replaced stack can be fully
/// stopped: a leaked background task would keep the replaced stack processing frames
Expand Down Expand Up @@ -1015,6 +1039,7 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
send_wake: Notify::new(),
pending_route_wake: Notify::new(),
send_seq: AtomicU32::new(0),
last_persisted_aps_counter: AtomicU32::new(0),
spawner,
cancels: Mutex::new(FlatMap::new()),
next_task_id: AtomicU32::new(0),
Expand Down Expand Up @@ -1091,6 +1116,34 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
self.notification_wake.notify_one();
}

/// The `(device_type, rx_on_when_idle)` for a `DeviceJoined` notification, read back
/// from the neighbor entry. Known for a direct association/rejoin (the device is our
/// child); `(None, true)` for a join learned second-hand via a router's Update-Device.
fn device_join_capability(&self, ieee: Eui64) -> (Option<NwkDeviceType>, bool) {
let device_capability = self.core().nib.neighbors.device_capability(ieee);
match device_capability {
Some((device_type, rx_on_when_idle)) => (Some(device_type), rx_on_when_idle),
None => (None, true),
}
}

/// Stream the outgoing APS frame counter every `FRAME_COUNTER_NOTIFY_INTERVAL`
/// frames so a restart can't roll it back. Called after each APS encryption.
fn maybe_notify_aps_frame_counter(&self) {
let counter = self.core().aib.aps_security.outgoing_frame_counter();
let last = self
.last_persisted_aps_counter
.load(AtomicOrdering::Relaxed);

if counter.wrapping_sub(last) >= FRAME_COUNTER_NOTIFY_INTERVAL {
self.last_persisted_aps_counter
.store(counter, AtomicOrdering::Relaxed);
self.push_notification(ZigbeeNotification::ApsFrameCounterUpdate {
frame_counter: counter,
});
}
}

/// Wait for and take all queued network events.
pub async fn next_notifications(&self) -> Vec<ZigbeeNotification> {
loop {
Expand Down
6 changes: 5 additions & 1 deletion crates/ziggurat-driver/src/zigbee_stack/aps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
return;
};

self.maybe_notify_aps_frame_counter();
encrypted.to_bytes()
} else {
ack_frame.to_bytes()
Expand Down Expand Up @@ -261,7 +262,10 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
.aps_security
.encrypt_data(destination_eui64, &aps_frame);
match encrypted {
Some(encrypted) => encrypted.to_bytes(),
Some(encrypted) => {
self.maybe_notify_aps_frame_counter();
encrypted.to_bytes()
}
None => return Err(ZigbeeStackError::ApsSecurityFailed),
}
} else {
Expand Down
4 changes: 3 additions & 1 deletion crates/ziggurat-driver/src/zigbee_stack/indirect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
// The address map entry and any negotiated link key are kept so that the
// device can rejoin later (mirrors `handle_leave`)
self.drop_indirect_transactions(Some(eui64), nwk);
self.core().nib.routing.remove_route(nwk);
if self.core().nib.routing.remove_route(nwk) {
self.push_notification(ZigbeeNotification::RouteRemoved { destination: nwk });
}

self.push_notification(ZigbeeNotification::DeviceLeft {
nwk,
Expand Down
35 changes: 31 additions & 4 deletions crates/ziggurat-driver/src/zigbee_stack/joining.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,9 +248,16 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {

// Routers resolve their own conflicts after hearing the notification; our
// mapping for the address is ambiguous until the keeper re-announces
let mut core = self.core();
core.nib.address_map.forget_address(address);
core.nib.routing.remove_route(address);
let removed = {
let mut core = self.core();
core.nib.address_map.forget_address(address);
core.nib.routing.remove_route(address)
};
if removed {
self.push_notification(ZigbeeNotification::RouteRemoved {
destination: address,
});
}
}

/// The address-conflict report reactor: a single long-lived task owning the
Expand Down Expand Up @@ -494,10 +501,13 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {

self.background_send_nwk_frame(nwk_frame, NwkSecurityMode::Unsecured, SendMode::Direct);

let (device_type, rx_on_when_idle) = self.device_join_capability(destination_eui64);
self.push_notification(ZigbeeNotification::DeviceJoined {
nwk: destination,
ieee: destination_eui64,
parent: self.state.network_address,
device_type,
rx_on_when_idle,
});
}

Expand Down Expand Up @@ -917,6 +927,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
nwk: update.device_short_address,
ieee: update.device_address,
parent: router_nwk,
// Learned via the router's Update-Device, which carries no capability
device_type: None,
rx_on_when_idle: true,
});
}
ApsUpdateDeviceStatus::StandardDeviceTrustCenterRejoin => {
Expand All @@ -938,6 +951,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
nwk: update.device_short_address,
ieee: update.device_address,
parent: router_nwk,
// Learned via the router's Update-Device, which carries no capability
device_type: None,
rx_on_when_idle: true,
});
}
ApsUpdateDeviceStatus::StandardDeviceSecuredRejoin => {
Expand All @@ -947,6 +963,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
nwk: update.device_short_address,
ieee: update.device_address,
parent: router_nwk,
// Learned via the router's Update-Device, which carries no capability
device_type: None,
rx_on_when_idle: true,
});
}
ApsUpdateDeviceStatus::DeviceLeft => {
Expand Down Expand Up @@ -1139,10 +1158,14 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
// `send_network_key` also emits the join notification
self.send_network_key(assigned_nwk, source_ieee, JoinKind::Rejoin);
} else {
let (device_type, rx_on_when_idle) = self.device_join_capability(source_ieee);

self.push_notification(ZigbeeNotification::DeviceJoined {
nwk: assigned_nwk,
ieee: source_ieee,
parent: self.state.network_address,
device_type,
rx_on_when_idle,
});
}
}
Expand Down Expand Up @@ -1207,7 +1230,11 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
// The address map entry and any negotiated link key are kept around so that the
// device can rejoin later
self.drop_indirect_transactions(source_ieee, source);
self.core().nib.routing.remove_route(source);
if self.core().nib.routing.remove_route(source) {
self.push_notification(ZigbeeNotification::RouteRemoved {
destination: source,
});
}

self.push_notification(ZigbeeNotification::DeviceLeft {
nwk: source,
Expand Down
6 changes: 4 additions & 2 deletions crates/ziggurat-driver/src/zigbee_stack/neighbor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ziggurat_zigbee::nwk::frame::{BROADCAST_ALL_ROUTERS_AND_COORDINATOR, NwkFram

use crate::frame_token::TrafficClass;

use super::{NwkSecurityMode, TxPolicy, TxPriority, ZigbeeStack};
use super::{NwkSecurityMode, TxPolicy, TxPriority, ZigbeeNotification, ZigbeeStack};

/// Maximum number of link status entries that can be carried in a single frame.
const MAX_LINK_STATUSES: usize = 7;
Expand All @@ -33,7 +33,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
tracing::info!("Child {eui64:?} ({nwk:?}) is now parented by {new_parent:?}");

self.drop_indirect_transactions(Some(eui64), nwk);
self.core().nib.routing.remove_route(nwk);
if self.core().nib.routing.remove_route(nwk) {
self.push_notification(ZigbeeNotification::RouteRemoved { destination: nwk });
}
}

/// Drop our child entry for a device known to have attached to another parent.
Expand Down
32 changes: 29 additions & 3 deletions crates/ziggurat-driver/src/zigbee_stack/nwk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ use ziggurat_zigbee::nwk::frame::{
NwkSecurityLevel, NwkSourceRoute,
};

use super::routing::{Route, Status as RouteStatus};
use super::routing::{Route, RouteUpdate, Status as RouteStatus};
use super::{
AddrConflictSource, BroadcastSchedule, IndirectFrame, IndirectPayload, JoinKind, MAX_DEPTH,
NwkSecurityMode, PROTOCOL_VERSION, PendingBroadcast, PendingFrame, PendingRoute,
Expand Down Expand Up @@ -412,10 +412,19 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
NwkCommand::RouteReply(cmd) => self.handle_route_reply(nwk_frame, cmd.clone()),
NwkCommand::RouteRecord(cmd) => {
tracing::trace!("Route record command frame received: {cmd:?}");
self.core()
let source = nwk_frame.nwk_header.source;
let changed = self
.core()
.nib
.routing
.store_route_record(nwk_frame.nwk_header.source, cmd.relays.clone());
.store_route_record(source, cmd.relays.clone());

if changed {
self.push_notification(ZigbeeNotification::RouteRecord {
destination: source,
relays: cmd.relays.clone(),
});
}
}
NwkCommand::RouteRequest(cmd) => {
self.handle_route_request(nwk_frame, cmd.clone(), sender_nwk);
Expand Down Expand Up @@ -451,6 +460,23 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
}
}

/// Notify the client of an established or re-pointed route so it can update its
/// warm-start next-hop cache in real time.
pub(crate) fn notify_route_update(&self, update: Option<RouteUpdate>) {
if let Some(RouteUpdate {
destination,
next_hop,
path_cost,
}) = update
{
self.push_notification(ZigbeeNotification::RouteChanged {
destination,
next_hop,
path_cost,
});
}
}

/// A NWK command frame originated by us, with stack-wide defaults: secured, route
/// discovery suppressed, radius `2 * max_depth`, sequence number assigned on send,
/// our EUI64 as the extended source. Deviations chain `with_*` overrides.
Expand Down
20 changes: 15 additions & 5 deletions crates/ziggurat-driver/src/zigbee_stack/route.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ use ziggurat_zigbee::nwk::frame::{BROADCAST_ALL_ROUTERS_AND_COORDINATOR, NwkFram
use super::routing::RouteReplyDisposition;
use crate::frame_token::TrafficClass;

use super::{AddrConflictSource, NwkSecurityMode, SendMode, TxPolicy, TxPriority, ZigbeeStack};
use super::{
AddrConflictSource, NwkSecurityMode, SendMode, TxPolicy, TxPriority, ZigbeeNotification,
ZigbeeStack,
};

impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
#[allow(clippy::significant_drop_tightening)]
Expand Down Expand Up @@ -44,7 +47,7 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
// responder seeds the first hop and each relay adds the link it forwards across.
let updated_path_cost = route_reply_cmd.path_cost;

let disposition = self.core().nib.routing.accept_route_reply(
let outcome = self.core().nib.routing.accept_route_reply(
self.state.network_address,
route_reply_cmd.originator_nwk,
route_reply_cmd.route_request_identifier,
Expand All @@ -53,7 +56,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
updated_path_cost,
);

let (next_hop_nwk, path_cost) = match disposition {
self.notify_route_update(outcome.update);

let (next_hop_nwk, path_cost) = match outcome.disposition {
RouteReplyDisposition::Drop => return,
RouteReplyDisposition::Established => {
self.pending_route_wake.notify_one();
Expand Down Expand Up @@ -143,7 +148,7 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
// Deduplicate route requests and track the best path back to the originator.
// Only requests advertising a strictly better forward cost are processed
// further; this also stops our own requests from echoing back at us.
let accepted = self.core().nib.routing.accept_route_request(
let outcome = self.core().nib.routing.accept_route_request(
nwk_frame.nwk_header.source,
route_request_cmd.route_request_identifier,
route_request_cmd.destination_address,
Expand All @@ -154,7 +159,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
self.tunables.route_discovery_time(),
);

if !accepted {
self.notify_route_update(outcome.update);

if !outcome.accepted {
return;
}

Expand Down Expand Up @@ -422,6 +429,9 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
"Removed failed route to {:?}",
network_status_cmd.network_address
);
self.push_notification(ZigbeeNotification::RouteRemoved {
destination: network_status_cmd.network_address,
});
}
if removed_record {
tracing::info!(
Expand Down
Loading