-
-
Notifications
You must be signed in to change notification settings - Fork 0
Driver Development
whisprer edited this page Nov 18, 2025
·
1 revision
This guide explains how to add support for new Zigbee hardware to the analyzer.
Adding a new hardware driver involves:
-
Implement the HAL traits (
ZigbeeCapture, optionallyPacketInjection) - Handle device-specific protocol (serial, USB bulk, etc.)
- Register the driver in the driver registry
- Test thoroughly with real hardware
- Document capabilities and limitations
- Rust programming: Intermediate level
- USB/Serial communication: Understanding of device protocols
- Protocol analysis: Ability to reverse-engineer if no docs
- IEEE 802.15.4: Basic understanding of Zigbee frames
- Hardware: The device you're adding support for
- USB sniffer (optional): Wireshark + USBPcap, or hardware USB analyzer
- Logic analyzer (optional): For serial protocol analysis
- Documentation: Device datasheet, if available
-
USB IDs:
lsusb -v | grep -A 20 "your device"
-
Serial protocol (if using serial):
- Baud rate
- Data bits / parity / stop bits
- Flow control
-
Existing software:
- Official tools
- Open source projects using the same hardware
- Firmware documentation
# Capture USB traffic
1. Install USBPcap
2. Start Wireshark
3. Select USB interface
4. Use official software while capturing
5. Analyze captured packets# Enable usbmon
sudo modprobe usbmon
# Find bus number
lsusb | grep "your device"
# Output: Bus 003 Device 005: ID 0451:16ae
# Capture
sudo cat /sys/kernel/debug/usb/usbmon/3u > capture.log
# Use official software, then analyze capture.log# Pattern recognition in captured data
ffead010: 4c80 0c00 0000 0000 5300 0000 0001 0000 L.......S.......
ffead020: 0001 0000 = ....
# Looks like:
# 0xFE = Start of frame
# 0xAD = Length
# 0x01 = Command
# ...
# In zigbee-drivers/src/
touch your_device.rsuse zigbee_core::packet::RawPacket;
use zigbee_hal::{
traits::{ZigbeeCapture, PacketInjection},
capabilities::DeviceCapabilities,
error::{HalError, HalResult},
};
use async_trait::async_trait;
use serialport::SerialPort;
use std::time::{Duration, SystemTime};
/// Your device driver
pub struct YourDevice {
port: Option<Box<dyn SerialPort>>,
port_name: String,
channel: u8,
active: bool,
capabilities: DeviceCapabilities,
buffer: Vec<u8>,
}
// Protocol constants
const SOF: u8 = 0xFE; // Start of Frame
const CMD_START: u8 = 0x22; // Your device's start command
const CMD_STOP: u8 = 0x23; // Stop command
const CMD_SET_CHANNEL: u8 = 0x24; // Set channel command
const CMD_PACKET: u8 = 0x80; // Packet indication
// USB IDs
const VENDOR_ID: u16 = 0x????;
const PRODUCT_ID: u16 = 0x????;impl YourDevice {
/// Create new driver instance
pub fn new() -> HalResult<Self> {
// Find device by USB VID/PID
let port_name = Self::find_device()?;
Ok(Self {
port: None,
port_name,
channel: 11,
active: false,
capabilities: DeviceCapabilities {
packet_injection: false, // Adjust based on hardware
promiscuous_mode: true,
supported_channels: (11..=26).collect(),
max_sample_rate: 1000,
hardware_filtering: false,
},
buffer: Vec::with_capacity(256),
})
}
/// Find device on USB bus
fn find_device() -> HalResult<String> {
let ports = serialport::available_ports()
.map_err(|e| HalError::DeviceNotFound)?;
for port in ports {
if let serialport::SerialPortType::UsbPort(info) = &port.port_type {
if info.vid == VENDOR_ID && info.pid == PRODUCT_ID {
return Ok(port.port_name);
}
}
}
Err(HalError::DeviceNotFound)
}
/// Send command to device
fn send_command(&mut self, cmd: u8, data: &[u8]) -> HalResult<()> {
let port = self.port.as_mut()
.ok_or(HalError::InitializationFailed)?;
// Build frame: [SOF] [LEN] [CMD] [DATA...] [FCS]
let mut frame = vec![SOF, data.len() as u8, cmd];
frame.extend_from_slice(data);
// Calculate checksum (XOR of all bytes after SOF)
let fcs = frame[1..].iter().fold(0u8, |acc, &b| acc ^ b);
frame.push(fcs);
port.write_all(&frame)
.map_err(|e| HalError::CommunicationError(e.to_string()))?;
Ok(())
}
/// Read response from device
fn read_response(&mut self, timeout: Duration) -> HalResult<Vec<u8>> {
let port = self.port.as_mut()
.ok_or(HalError::InitializationFailed)?;
port.set_timeout(timeout)
.map_err(|e| HalError::CommunicationError(e.to_string()))?;
// Read until we get a complete frame
let mut response = Vec::new();
let mut buf = [0u8; 1];
// Wait for SOF
loop {
port.read_exact(&mut buf)
.map_err(|e| HalError::Timeout)?;
if buf[0] == SOF {
break;
}
}
// Read length
port.read_exact(&mut buf)
.map_err(|e| HalError::Timeout)?;
let len = buf[0] as usize;
// Read rest of frame
response.resize(len + 2, 0); // +2 for CMD and FCS
port.read_exact(&mut response)
.map_err(|e| HalError::Timeout)?;
// Verify checksum
let received_fcs = response.pop().unwrap();
let calculated_fcs = response.iter()
.chain(std::iter::once(&(len as u8)))
.fold(0u8, |acc, &b| acc ^ b);
if received_fcs != calculated_fcs {
return Err(HalError::CommunicationError("Checksum mismatch".into()));
}
Ok(response)
}
}#[async_trait]
impl ZigbeeCapture for YourDevice {
async fn initialize(&mut self) -> HalResult<()> {
// Open serial port
let port = serialport::new(&self.port_name, 115200)
.timeout(Duration::from_secs(1))
.open()
.map_err(|e| HalError::InitializationFailed)?;
self.port = Some(port);
// Reset device (device-specific)
self.send_command(CMD_RESET, &[])?;
tokio::time::sleep(Duration::from_millis(500)).await;
// Verify device is responding
self.send_command(CMD_PING, &[])?;
let response = self.read_response(Duration::from_secs(1))?;
if response[0] != CMD_PING {
return Err(HalError::InitializationFailed);
}
Ok(())
}
async fn set_channel(&mut self, channel: u8) -> HalResult<()> {
if !(11..=26).contains(&channel) {
return Err(HalError::InvalidChannel(channel));
}
self.send_command(CMD_SET_CHANNEL, &[channel])?;
let response = self.read_response(Duration::from_millis(100))?;
if response[0] == CMD_SET_CHANNEL && response[1] == 0x00 {
self.channel = channel;
Ok(())
} else {
Err(HalError::CommunicationError("Failed to set channel".into()))
}
}
fn get_channel(&self) -> HalResult<u8> {
Ok(self.channel)
}
async fn capture_packet(&mut self) -> HalResult<RawPacket> {
if !self.active {
// Start capture if not already active
self.send_command(CMD_START, &[self.channel])?;
self.active = true;
}
// Read packet indication
let response = self.read_response(Duration::from_secs(10))?;
if response[0] != CMD_PACKET {
return Err(HalError::CommunicationError("Unexpected response".into()));
}
// Parse packet from response
// Format (example): [CMD] [LEN] [RSSI] [LQI] [DATA...]
let rssi = response[2] as i8;
let lqi = response[3];
let data = response[4..].to_vec();
Ok(RawPacket {
timestamp: SystemTime::now(),
channel: self.channel,
rssi,
lqi,
data,
})
}
fn capabilities(&self) -> &DeviceCapabilities {
&self.capabilities
}
fn device_name(&self) -> &str {
"Your Device Name"
}
async fn shutdown(&mut self) -> HalResult<()> {
if self.active {
self.send_command(CMD_STOP, &[])?;
self.active = false;
}
self.port = None;
Ok(())
}
}#[async_trait]
impl PacketInjection for YourDevice {
async fn inject_packet(&mut self, packet: &RawPacket) -> HalResult<()> {
// Check if supported
if !self.capabilities.packet_injection {
return Err(HalError::NotSupported);
}
// Build inject command
let mut payload = vec![packet.channel];
payload.extend_from_slice(&packet.data);
self.send_command(CMD_INJECT, &payload)?;
// Wait for confirmation
let response = self.read_response(Duration::from_millis(100))?;
if response[0] == CMD_INJECT && response[1] == 0x00 {
Ok(())
} else {
Err(HalError::CommunicationError("Injection failed".into()))
}
}
async fn set_tx_power(&mut self, dbm: i8) -> HalResult<()> {
self.send_command(CMD_SET_POWER, &[dbm as u8])?;
Ok(())
}
}In zigbee-drivers/src/registry.rs:
impl DriverRegistry {
pub fn new() -> Self {
Self {
drivers: vec![
// Existing drivers...
DriverInfo {
name: "Your Device",
description: "Description of your device",
vid: 0x????,
pid: 0x????,
factory: || Ok(Box::new(crate::your_device::YourDevice::new()?)),
},
],
}
}
}In zigbee-drivers/src/lib.rs:
pub mod your_device;
// Re-export
pub use your_device::YourDevice;Create zigbee-drivers/src/your_device_tests.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_parsing() {
// Test your protocol parsing
let data = vec![0xFE, 0x03, 0x80, 0x01, 0x02, 0x03, 0x82];
// Parse and verify
}
#[test]
fn test_checksum() {
// Test checksum calculation
}
#[tokio::test]
async fn test_initialization() {
// Mock hardware test
}
}Create tests/your_device_integration.rs:
use zigbee_drivers::YourDevice;
use zigbee_hal::traits::ZigbeeCapture;
#[tokio::test]
#[ignore] // Only run with real hardware
async fn test_real_hardware() {
let mut device = YourDevice::new().unwrap();
device.initialize().await.unwrap();
device.set_channel(15).await.unwrap();
// Capture a few packets
for _ in 0..10 {
let packet = device.capture_packet().await.unwrap();
assert_eq!(packet.channel, 15);
assert!(!packet.data.is_empty());
}
device.shutdown().await.unwrap();
}- Driver compiles without warnings
- Unit tests pass
- Device is detected by registry
- Initialization succeeds
- Channel setting works (all channels 11-26)
- Packets are captured correctly
- RSSI/LQI values are reasonable
- Graceful error handling (unplug device, etc.)
- No memory leaks (run under valgrind)
- Performance meets expectations
Update wiki/Hardware-Support.md:
## Your Device Name
### Overview
Brief description of the hardware.
### Specifications
- Chipset:
- Frequency:
- Channels:
- Sensitivity:
- Interface:
### Features
✅ Supported:
- Feature 1
- Feature 2
❌ Not Supported:
- Feature 3
### Setup
Instructions for users...If you had to reverse-engineer the protocol, document it!
// In your driver file, add comprehensive comments
/// Protocol Frame Format:
///
/// ```text
/// ┌────┬────┬────┬─────┬────┐
/// │SOF │LEN │CMD │DATA │FCS │
/// ├────┼────┼────┼─────┼────┤
/// │0xFE│1 B │1 B │N B │1 B │
/// └────┴────┴────┴─────┴────┘
/// ```
///
/// Commands:
/// - 0x22: Start capture
/// - 0x23: Stop capture
/// - 0x24: Set channel
/// - 0x80: Packet indication
///
/// Checksum: XOR of all bytes after SOF
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio_serial::SerialStream;
async fn read_frame_async(&mut self) -> HalResult<Vec<u8>> {
let mut buf = [0u8; 1];
// Use async I/O
self.port.read_exact(&mut buf).await
.map_err(|e| HalError::Timeout)?;
// ...
}use rusb::{Context, Device, DeviceHandle};
fn bulk_read(&self, endpoint: u8, buf: &mut [u8]) -> HalResult<usize> {
let timeout = Duration::from_secs(1);
self.handle
.read_bulk(endpoint, buf, timeout)
.map_err(|e| HalError::CommunicationError(e.to_string()))
}use std::collections::VecDeque;
struct PacketBuffer {
queue: VecDeque<RawPacket>,
max_size: usize,
}
impl PacketBuffer {
fn push(&mut self, packet: RawPacket) {
if self.queue.len() >= self.max_size {
self.queue.pop_front();
}
self.queue.push_back(packet);
}
}RUST_LOG=zigbee_drivers=trace cargo runIn your code:
use tracing::{trace, debug, info, warn, error};
debug!("Sending command: {:02x}", cmd);
trace!("Raw data: {:02x?}", data);# Linux: Monitor specific device
sudo usbmon -i 3 -s 0 # Bus 3
# Windows: Wireshark + [USBPcap](https://desowin.org/usbpcap/)Device not detected:
- Check USB IDs match
- Verify udev rules (Linux)
- Check Windows drivers
Communication errors:
- Verify baud rate
- Check flow control
- Add delays between commands
- Increase timeout values
Packet corruption:
- Verify checksum calculation
- Check for buffer overflows
- Ensure proper frame synchronization
use nom::{IResult, bytes::complete::take};
fn parse_frame(input: &[u8]) -> IResult<&[u8], Frame> {
let (input, header) = take(10usize)(input)?;
// Parse without copying
// ...
}async fn capture_batch(&mut self, count: usize) -> HalResult<Vec<RawPacket>> {
let mut packets = Vec::with_capacity(count);
for _ in 0..count {
packets.push(self.capture_packet().await?);
}
Ok(packets)
}- Code follows project style (run
cargo fmt) - No clippy warnings (
cargo clippy) - All tests pass
- Documentation complete
- Hardware tested thoroughly
- Example code provided
- Fork the repository
- Create feature branch:
git checkout -b driver/your-device - Commit changes:
git commit -am 'Add YourDevice driver' - Push:
git push origin driver/your-device - Open Pull Request with:
- Description of hardware
- Testing performed
- Known limitations
- Photos/links (if possible)
USB Tools:
- USBPcap - Windows USB capture
- [Wireshark](https://www.wireshark.org/) - Protocol analyzer
- [sigrok/PulseView](https://sigrok.org/) - Logic analyzer software
Rust Libraries:
- [serialport-rs](https://crates.io/crates/serialport) - Serial port
- [rusb](https://crates.io/crates/rusb) - USB (libusb wrapper)
- [tokio-serial](https://crates.io/crates/tokio-serial) - Async serial
Reference Drivers:
- Look at
ti_cc2531.rs- Simple serial protocol - Look at
ti_cc2652.rs- More complex device
Need Help?
- Open a [Discussion](https://github.com/yourusername/zigbee-analyzer/discussions)
- Check existing [Issues](https://github.com/yourusername/zigbee-analyzer/issues)
- Ask in the driver-dev channel
See Also:
- Hardware Support - Supported devices
- Architecture - HAL design
- Contributing - General contribution guide