Skip to content

Driver Development

whisprer edited this page Nov 18, 2025 · 1 revision

Driver Development

This guide explains how to add support for new Zigbee hardware to the analyzer.

Overview

Adding a new hardware driver involves:

  1. Implement the HAL traits (ZigbeeCapture, optionally PacketInjection)
  2. Handle device-specific protocol (serial, USB bulk, etc.)
  3. Register the driver in the driver registry
  4. Test thoroughly with real hardware
  5. Document capabilities and limitations

Prerequisites

Knowledge Required

  • 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

Tools Needed

  • 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

Step 1: Study the Hardware

Gather Information

  1. USB IDs:

    lsusb -v | grep -A 20 "your device"
  2. Serial protocol (if using serial):

    • Baud rate
    • Data bits / parity / stop bits
    • Flow control
  3. Existing software:

    • Official tools
    • Open source projects using the same hardware
    • Firmware documentation

Reverse Engineer Protocol (if needed)

Using Wireshark + USBPcap (Windows):

# Capture USB traffic
1. Install USBPcap
2. Start Wireshark
3. Select USB interface
4. Use official software while capturing
5. Analyze captured packets

Using usbmon (Linux):

# 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

Example Analysis:

# 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
# ...

Step 2: Implement the Driver

Create Driver File

# In zigbee-drivers/src/
touch your_device.rs

Basic Structure

use 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????;

Implement Constructor

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)
    }
}

Implement ZigbeeCapture Trait

#[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(())
    }
}

Optional: Implement PacketInjection

#[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(())
    }
}

Step 3: Register the Driver

Update Registry

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()?)),
                },
            ],
        }
    }
}

Export Module

In zigbee-drivers/src/lib.rs:

pub mod your_device;

// Re-export
pub use your_device::YourDevice;

Step 4: Testing

Unit Tests

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
    }
}

Integration Tests

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();
}

Testing Checklist

  • 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

Step 5: Documentation

Add Hardware Documentation

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...

Document Protocol

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

Common Patterns

Async Serial Reading

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)?;
    
    // ...
}

USB Bulk Transfers

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()))
}

Buffer Management

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);
    }
}

Debugging Tips

Enable Logging

RUST_LOG=zigbee_drivers=trace cargo run

In your code:

use tracing::{trace, debug, info, warn, error};

debug!("Sending command: {:02x}", cmd);
trace!("Raw data: {:02x?}", data);

USB Traffic Analysis

# Linux: Monitor specific device
sudo usbmon -i 3 -s 0  # Bus 3

# Windows: Wireshark + [USBPcap](https://desowin.org/usbpcap/)

Common Issues

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

Performance Optimization

Zero-Copy Parsing

use nom::{IResult, bytes::complete::take};

fn parse_frame(input: &[u8]) -> IResult<&[u8], Frame> {
    let (input, header) = take(10usize)(input)?;
    // Parse without copying
    // ...
}

Batch Processing

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)
}

Submitting Your Driver

Pre-submission Checklist

  • Code follows project style (run cargo fmt)
  • No clippy warnings (cargo clippy)
  • All tests pass
  • Documentation complete
  • Hardware tested thoroughly
  • Example code provided

Pull Request

  1. Fork the repository
  2. Create feature branch: git checkout -b driver/your-device
  3. Commit changes: git commit -am 'Add YourDevice driver'
  4. Push: git push origin driver/your-device
  5. Open Pull Request with:
    • Description of hardware
    • Testing performed
    • Known limitations
    • Photos/links (if possible)

Resources

USB Tools:

Rust Libraries:

Reference Drivers:

  • Look at ti_cc2531.rs - Simple serial protocol
  • Look at ti_cc2652.rs - More complex device

Need Help?


See Also:

Clone this wiki locally