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
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ in terms of functionality exposed by consumer Zigbee devices:
- [x] Source routing
- [x] Device joining
- [x] Child device message holding and management
- [ ] Zigbee Green Power (TODO)
- [x] Zigbee Green Power: direct reception of GPDFs, which are passed unprocessed to zigpy
- [ ] Zigbee Green Power: transmission of GPDFs (needed for bidirectional GPDs)

Zigbee R23 and R23.1 support (i.e. Zigbee 4.0) are low on the list but planned for the
future. Keep in mind Zigbee is both forward and backward compatible and practically nothing
Expand All @@ -90,22 +91,21 @@ Ziggurat's public API is a simple WebSocket server, which can be run locally ins
in a Home Assistant addon:

```bash
cargo run --bin ziggurat /dev/cu.SLAB_USBtoUART --baudrate 460800 --host 0.0.0.0:9999
cargo run --bin ziggurat -- --device /dev/cu.SLAB_USBtoUART --listen 0.0.0.0:9999
```

The adapter itself is stateless and ZHA will push all network configuration to it on startup.


#### Future work
- Ziggurat currently statically allocates most protocol-level parsing buffers, there are only
a few places left in the stack (e.g. `HashMap`) that rely on dynamic allocation. This is
in preparation for eventually trying to run Ziggurat on a microcontroller directly, probably
the ESP32-C6.
a few places left in the stack (e.g. `HashMap`) that rely on dynamic allocation.
- For testing purposes (especially CI) it would be convenient to allow Ziggurat to have a
dynamically configurable role on startup. Adding the ability to join a network as a router
would allow for some useful end-to-end testing scenarios, like a synthetic Spinel layer
that can connect multiple Ziggurat instances together.
- Zigbee Green Power.
- Zigbee Green Power transmission: a GPD only listens 20-25 ms after it transmits, too
short for a round trip to zigpy, so Ziggurat has to queue the response and send it itself.
- Exploring more routing algorithms.

---
Expand Down
10 changes: 10 additions & 0 deletions crates/ziggurat-driver/src/zigbee_stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -869,6 +869,16 @@ pub enum ZigbeeNotification {
aps_encrypted: bool,
data: Vec<u8>,
},
/// A Green Power frame received directly from a GPD, without any processing: the
/// stub NWK header onward, including the MIC (GP A.1.2.1.3). Of the MAC header,
/// only the fields GP uses are kept.
ReceivedGpdf {
mac_sequence_number: u8,
source_ieee: Option<Eui64>,
lqi: u8,
rssi: i8,
data: Vec<u8>,
},
/// The outgoing NWK security frame counter has advanced; the client should persist
/// it to prevent a rollback on restart
FrameCounterUpdate { frame_counter: u32 },
Expand Down
47 changes: 44 additions & 3 deletions crates/ziggurat-driver/src/zigbee_stack/mac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ use crate::ziggurat_ieee_802154::{
Ieee802154Frame, Ieee802154FrameControl, Ieee802154FrameHeader, Ieee802154FrameType,
Ieee802154FrameVersion,
};
use abstract_bits::AbstractBits;
use abstract_bits::{AbstractBits, BitReader};
use alloc::vec::Vec;
use ziggurat_ieee_802154::types::{Nwk, PanId};
use ziggurat_phy::{RadioPhy, TxFrame, TxResult};
use ziggurat_zigbee::beacon::ZigbeeBeacon;
use ziggurat_zigbee::constants::GP_PROTOCOL_VERSION;
use ziggurat_zigbee::gp::GpNwkFrameControl;
use ziggurat_zigbee::nwk::frame::{
BROADCAST_ALL_ROUTERS_AND_COORDINATOR, EncryptedNwkFrame, NwkFrame, NwkPayload,
NwkSecurityHeaderKeyId, NwkSecurityLevel,
Expand All @@ -20,7 +22,7 @@ use crate::frame_token::TrafficClass;

use super::{
DeliveryError, NwkDeviceType, PROTOCOL_VERSION, STACK_PROFILE, SendKind, TxOutcome, TxPolicy,
TxPriority, ZigbeeStack,
TxPriority, ZigbeeNotification, ZigbeeStack,
};

/// Spacing between sprayed beacons while a [`hack_beacon_spam_duration`] window is open.
Expand Down Expand Up @@ -232,9 +234,17 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
return None;
}

// Only process packets destined for our PAN ID
let pan_id = self.core().mac.pan_id;

// A GPDF carries a stub NWK header instead (GP A.1.4.1), for the client to process
if GpNwkFrameControl::read_abstract_bits(&mut BitReader::from(&data_frame.payload[..]))
.is_ok_and(|control| control.protocol_version == GP_PROTOCOL_VERSION)
{
self.process_gpdf(data_frame, pan_id, lqi, rssi);
return None;
}

// Only process packets destined for our PAN ID
match data_frame.header.dest_pan_id {
None => {
tracing::debug!("Ignoring frame, destination PAN ID is not present");
Expand Down Expand Up @@ -367,6 +377,37 @@ impl<P: RadioPhy, R: Runtime> ZigbeeStack<P, R> {
Some(decrypted_nwk_frame)
}

fn process_gpdf(&self, data_frame: &Ieee802154DataFrame, pan_id: PanId, lqi: u8, rssi: i8) {
// A GPD is not part of the PAN and usually sends to the broadcast PAN ID
// (GP A.1.4.1.1)
match data_frame.header.dest_pan_id {
Some(dest_pan_id) if dest_pan_id == pan_id || dest_pan_id == PanId(0xFFFF) => (),
dest_pan_id => {
tracing::debug!("Ignoring GPDF, destination PAN ID {dest_pan_id:?} is not ours");
return;
}
}

let Some(mac_sequence_number) = data_frame.header.sequence_number else {
tracing::debug!("Ignoring GPDF, MAC sequence number is suppressed");
return;
};

// Only an IEEE source identifies the GPD, for ApplicationID 0b010 (GP A.1.4.1.1)
let source_ieee = match data_frame.header.src_address {
Some(Ieee802154Address::Eui64(eui64)) => Some(eui64),
_ => None,
};

self.push_notification(ZigbeeNotification::ReceivedGpdf {
mac_sequence_number,
source_ieee,
lqi,
rssi,
data: data_frame.payload.to_vec(),
});
}

pub(super) async fn send_802154_frame<Payload: ziggurat_ieee_802154::FramePayload>(
&self,
frame: Ieee802154Frame<Payload>,
Expand Down
14 changes: 14 additions & 0 deletions crates/ziggurat-protocol/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,20 @@ pub fn notification_frame(update: &ZigbeeNotification) -> Option<Vec<u8>> {
router_ieee: router_ieee.unwrap_or(Eui64([0; 8])),
})
}
ZigbeeNotification::ReceivedGpdf {
mac_sequence_number,
source_ieee,
lqi,
rssi,
data,
} => Notification::ReceivedGpdf(ReceivedGpdfPayload {
mac_sequence_number: *mac_sequence_number,
has_source_ieee: source_ieee.is_some(),
source_ieee: source_ieee.unwrap_or(Eui64([0; 8])),
lqi: *lqi,
rssi: *rssi,
data: data.clone(),
}),
ZigbeeNotification::FrameCounterUpdate { frame_counter } => {
Notification::FrameCounter(FrameCounterPayload {
frame_counter: *frame_counter,
Expand Down
20 changes: 20 additions & 0 deletions crates/ziggurat-protocol/src/wire.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub enum NotificationCommand {
SendConfirm = 0x11,
ApsAckConfirm = 0x12,
BroadcastConfirm = 0x13,
ReceivedGpdf = 0x14,
// Network membership.
DeviceJoined = 0x20,
DeviceLeft = 0x21,
Expand Down Expand Up @@ -676,6 +677,22 @@ pub struct ReceivedApsPayload {
pub data: Vec<u8>,
}

/// A Green Power frame received directly from a GPD, unprocessed: `data` is the stub NWK
/// header onward, including the MIC.
#[abstract_bits]
#[derive(Debug, Clone)]
pub struct ReceivedGpdfPayload {
pub mac_sequence_number: u8,
pub has_source_ieee: bool,
pub reserved: u7,
pub source_ieee: Eui64, // only for ApplicationID 0b010
pub lqi: u8,
pub rssi: i8,
pub data_len: u16,
#[abstract_bits(length_from = data_len)]
pub data: Vec<u8>,
}

/// A send's terminal verdict, carried in its `SendConfirm` / `ApsAckConfirm` /
/// `BroadcastConfirm` notification. Mirrors the driver's `DeliveryError`; admission
/// failures ride the synchronous `Error` frame's [`Status`] instead.
Expand Down Expand Up @@ -965,6 +982,7 @@ pub enum Notification {
SendConfirm(RequestId, SendConfirmPayload),
ApsAckConfirm(RequestId, ApsAckConfirmPayload),
BroadcastConfirm(RequestId, BroadcastConfirmPayload),
ReceivedGpdf(ReceivedGpdfPayload),
DeviceJoined(DeviceJoinedPayload),
DeviceLeft(DeviceLeftPayload),
FrameCounter(FrameCounterPayload),
Expand All @@ -985,6 +1003,7 @@ impl Notification {
Self::BroadcastConfirm(request_id, _) => {
(NotificationCommand::BroadcastConfirm, *request_id)
}
Self::ReceivedGpdf(_) => (NotificationCommand::ReceivedGpdf, 0),
Self::DeviceJoined(_) => (NotificationCommand::DeviceJoined, 0),
Self::DeviceLeft(_) => (NotificationCommand::DeviceLeft, 0),
Self::FrameCounter(_) => (NotificationCommand::FrameCounter, 0),
Expand All @@ -1001,6 +1020,7 @@ impl Notification {
Self::SendConfirm(_, payload) => append(&mut bytes, payload),
Self::ApsAckConfirm(_, payload) => append(&mut bytes, payload),
Self::BroadcastConfirm(_, payload) => append(&mut bytes, payload),
Self::ReceivedGpdf(payload) => append(&mut bytes, payload),
Self::DeviceJoined(payload) => append(&mut bytes, payload),
Self::DeviceLeft(payload) => append(&mut bytes, payload),
Self::FrameCounter(payload) => append(&mut bytes, payload),
Expand Down
3 changes: 3 additions & 0 deletions crates/ziggurat-zigbee/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ use crate::nwk::commands::EndDeviceTimeout;
/// `nwkcProtocolVersion`: the NWK protocol version carried in every frame.
pub const PROTOCOL_VERSION: u8 = 2;

/// The protocol version in the stub NWK header of a Green Power frame (GP A.1.4.1.2).
pub const GP_PROTOCOL_VERSION: u8 = 3;

/// The stack profile advertised in beacons: 2 is Zigbee PRO.
pub const STACK_PROFILE: u8 = 2;

Expand Down
45 changes: 45 additions & 0 deletions crates/ziggurat-zigbee/src/gp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use abstract_bits::abstract_bits;
use num_enum::TryFromPrimitive;

#[abstract_bits(bits = 2)]
#[derive(Debug, Eq, PartialEq, TryFromPrimitive, Clone, Copy)]
#[repr(u8)]
pub enum GpFrameType {
Data = 0b00,
Maintenance = 0b01,
}

/// The NWK Frame Control field of a GPDF (GP A.1.4.1.2). It shares the position of
/// the protocol version with the NWK frame control of a Zigbee frame.
#[abstract_bits]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpNwkFrameControl {
pub frame_type: GpFrameType,
pub protocol_version: u4,
pub auto_commissioning: bool,
pub extended: bool,
}

#[cfg(test)]
mod test {
use super::*;
use abstract_bits::{AbstractBits, BitReader};
use hex_literal::hex;

#[test]
fn test_gp_nwk_frame_control() {
// The transmitted packet for SecurityLevel 0b10 (GP A.1.5.4.2.1)
let bytes = hex!("8C 10 21436587 02000000 20 CF787E72");

let mut reader = BitReader::from(&bytes[..]);
assert_eq!(
GpNwkFrameControl::read_abstract_bits(&mut reader).unwrap(),
GpNwkFrameControl {
frame_type: GpFrameType::Data,
protocol_version: 3,
auto_commissioning: false,
extended: true,
}
);
}
}
1 change: 1 addition & 0 deletions crates/ziggurat-zigbee/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod beacon;
pub mod constants;
pub mod crypto;
pub mod flat_map;
pub mod gp;
pub mod indirect;
pub mod nwk;
pub mod ring_buffer;
Expand Down
Loading