Version 1.0.0
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
//! AM/OOK demodulator for extracting level+duration pairs from raw IQ samples.
|
||||
//!
|
||||
//! This demodulator converts raw IQ samples into a stream of (level, duration_us) pairs
|
||||
//! that can be processed by protocol decoders, similar to how the Flipper Zero SubGHz
|
||||
//! system works.
|
||||
|
||||
/// A single level+duration pair representing one segment of the signal
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LevelDuration {
|
||||
/// Signal level (true = high, false = low)
|
||||
pub level: bool,
|
||||
/// Duration in microseconds
|
||||
pub duration_us: u32,
|
||||
}
|
||||
|
||||
impl LevelDuration {
|
||||
pub fn new(level: bool, duration_us: u32) -> Self {
|
||||
Self { level, duration_us }
|
||||
}
|
||||
}
|
||||
|
||||
/// Demodulator for processing raw IQ samples into level+duration pairs
|
||||
pub struct Demodulator {
|
||||
/// Sample rate in Hz
|
||||
#[allow(dead_code)]
|
||||
sample_rate: u32,
|
||||
/// Samples per microsecond
|
||||
samples_per_us: f64,
|
||||
/// Current threshold for high/low detection
|
||||
threshold: f32,
|
||||
/// Adaptive threshold - high level estimate
|
||||
high_level: f32,
|
||||
/// Adaptive threshold - low level estimate
|
||||
low_level: f32,
|
||||
/// Current signal state (high or low)
|
||||
current_level: bool,
|
||||
/// Sample count at current level
|
||||
level_sample_count: u64,
|
||||
/// Accumulated level+duration pairs
|
||||
pairs: Vec<LevelDuration>,
|
||||
/// Total samples processed
|
||||
total_samples: u64,
|
||||
/// Minimum duration to consider valid (in µs)
|
||||
min_duration_us: u32,
|
||||
/// Maximum gap before considering signal complete (in µs)
|
||||
max_gap_us: u32,
|
||||
/// Samples since last edge
|
||||
samples_since_edge: u64,
|
||||
}
|
||||
|
||||
impl Demodulator {
|
||||
/// Create a new demodulator
|
||||
pub fn new(sample_rate: u32) -> Self {
|
||||
Self {
|
||||
sample_rate,
|
||||
samples_per_us: sample_rate as f64 / 1_000_000.0,
|
||||
threshold: 0.15,
|
||||
high_level: 0.3,
|
||||
low_level: 0.05,
|
||||
current_level: false,
|
||||
level_sample_count: 0,
|
||||
pairs: Vec::with_capacity(2048),
|
||||
total_samples: 0,
|
||||
min_duration_us: 50, // Minimum 50µs pulse
|
||||
max_gap_us: 10_000, // 10ms gap = end of signal
|
||||
samples_since_edge: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Process raw IQ samples and return level+duration pairs if signal complete
|
||||
///
|
||||
/// Returns None if still accumulating, Some(pairs) when a complete signal is detected
|
||||
pub fn process_samples(&mut self, samples: &[i8]) -> Option<Vec<LevelDuration>> {
|
||||
// Process each IQ sample pair
|
||||
for chunk in samples.chunks(2) {
|
||||
if chunk.len() < 2 {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate magnitude (AM envelope detection)
|
||||
let i = chunk[0] as f32 / 128.0;
|
||||
let q = chunk[1] as f32 / 128.0;
|
||||
let magnitude = (i * i + q * q).sqrt();
|
||||
|
||||
// Update adaptive threshold
|
||||
self.update_threshold(magnitude);
|
||||
|
||||
// Detect level
|
||||
let is_high = magnitude > self.threshold;
|
||||
|
||||
// Check for level change
|
||||
if is_high != self.current_level && self.level_sample_count > 0 {
|
||||
// Calculate duration of the previous level
|
||||
let duration_us = (self.level_sample_count as f64 / self.samples_per_us) as u32;
|
||||
|
||||
// Only record if above minimum duration (noise filtering)
|
||||
if duration_us >= self.min_duration_us {
|
||||
self.pairs.push(LevelDuration::new(self.current_level, duration_us));
|
||||
self.samples_since_edge = 0;
|
||||
}
|
||||
|
||||
self.current_level = is_high;
|
||||
self.level_sample_count = 1;
|
||||
} else {
|
||||
self.level_sample_count += 1;
|
||||
self.samples_since_edge += 1;
|
||||
}
|
||||
|
||||
self.total_samples += 1;
|
||||
}
|
||||
|
||||
// Check if we have a complete signal (long gap detected)
|
||||
let gap_samples = (self.max_gap_us as f64 * self.samples_per_us) as u64;
|
||||
|
||||
if !self.pairs.is_empty() && self.samples_since_edge > gap_samples {
|
||||
// Add the final level duration
|
||||
let duration_us = (self.level_sample_count as f64 / self.samples_per_us) as u32;
|
||||
if duration_us >= self.min_duration_us {
|
||||
self.pairs.push(LevelDuration::new(self.current_level, duration_us));
|
||||
}
|
||||
|
||||
// Return the pairs and reset
|
||||
let result = std::mem::take(&mut self.pairs);
|
||||
self.reset_state();
|
||||
|
||||
if result.len() >= 10 {
|
||||
return Some(result);
|
||||
}
|
||||
}
|
||||
|
||||
// Limit buffer size
|
||||
if self.pairs.len() > 4096 {
|
||||
self.reset_state();
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Update adaptive threshold based on signal levels
|
||||
fn update_threshold(&mut self, magnitude: f32) {
|
||||
const ALPHA: f32 = 0.001; // Slow adaptation
|
||||
|
||||
if magnitude > self.threshold {
|
||||
// Update high level estimate
|
||||
self.high_level = self.high_level * (1.0 - ALPHA) + magnitude * ALPHA;
|
||||
} else {
|
||||
// Update low level estimate
|
||||
self.low_level = self.low_level * (1.0 - ALPHA) + magnitude * ALPHA;
|
||||
}
|
||||
|
||||
// Threshold is midpoint between low and high
|
||||
self.threshold = (self.low_level + self.high_level) / 2.0;
|
||||
|
||||
// Ensure reasonable bounds
|
||||
self.threshold = self.threshold.max(0.05).min(0.5);
|
||||
}
|
||||
|
||||
/// Reset the demodulator state
|
||||
fn reset_state(&mut self) {
|
||||
self.pairs.clear();
|
||||
self.level_sample_count = 0;
|
||||
self.samples_since_edge = 0;
|
||||
self.current_level = false;
|
||||
}
|
||||
|
||||
/// Reset completely (including threshold adaptation)
|
||||
#[allow(dead_code)]
|
||||
pub fn reset(&mut self) {
|
||||
self.reset_state();
|
||||
self.threshold = 0.15;
|
||||
self.high_level = 0.3;
|
||||
self.low_level = 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: duration_diff macro is defined in protocols/mod.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_demodulator_creation() {
|
||||
let demod = Demodulator::new(2_000_000);
|
||||
assert_eq!(demod.sample_rate, 2_000_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_level_duration() {
|
||||
let ld = LevelDuration::new(true, 500);
|
||||
assert!(ld.level);
|
||||
assert_eq!(ld.duration_us, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
//! HackRF device control.
|
||||
//!
|
||||
//! This module provides a high-level interface for controlling HackRF devices
|
||||
//! using the `libhackrf` crate. Falls back to demo mode at runtime if no
|
||||
//! HackRF hardware is detected.
|
||||
|
||||
use anyhow::Result;
|
||||
use std::sync::mpsc::Sender;
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
};
|
||||
use std::thread::{self, JoinHandle};
|
||||
|
||||
use crate::app::RadioEvent;
|
||||
use crate::capture::Capture;
|
||||
|
||||
use super::demodulator::Demodulator;
|
||||
use super::demodulator::LevelDuration;
|
||||
|
||||
/// Sample rate for HackRF (2 MHz is good for keyfob signals)
|
||||
const SAMPLE_RATE: u32 = 2_000_000;
|
||||
|
||||
/// HackRF controller for receiving and transmitting signals
|
||||
pub struct HackRfController {
|
||||
/// Event sender for notifying the app
|
||||
event_tx: Sender<RadioEvent>,
|
||||
/// Whether we're currently receiving
|
||||
receiving: Arc<AtomicBool>,
|
||||
/// Receiver thread handle
|
||||
rx_thread: Option<JoinHandle<()>>,
|
||||
/// Current frequency
|
||||
frequency: Arc<Mutex<u32>>,
|
||||
/// Demodulator for processing samples
|
||||
demodulator: Arc<Mutex<Demodulator>>,
|
||||
/// Whether HackRF is available
|
||||
hackrf_available: bool,
|
||||
}
|
||||
|
||||
impl HackRfController {
|
||||
/// Create a new HackRF controller
|
||||
pub fn new(event_tx: Sender<RadioEvent>) -> Result<Self> {
|
||||
let demodulator = Demodulator::new(SAMPLE_RATE);
|
||||
|
||||
// Check if HackRF is available
|
||||
let hackrf_available = check_hackrf_available();
|
||||
|
||||
if hackrf_available {
|
||||
tracing::info!("HackRF device detected");
|
||||
} else {
|
||||
tracing::warn!("HackRF not detected - running in demo mode");
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
event_tx,
|
||||
receiving: Arc::new(AtomicBool::new(false)),
|
||||
rx_thread: None,
|
||||
frequency: Arc::new(Mutex::new(433_920_000)),
|
||||
demodulator: Arc::new(Mutex::new(demodulator)),
|
||||
hackrf_available,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if HackRF is available
|
||||
#[allow(dead_code)]
|
||||
pub fn is_available(&self) -> bool {
|
||||
self.hackrf_available
|
||||
}
|
||||
|
||||
/// Start receiving at the specified frequency
|
||||
pub fn start_receiving(&mut self, frequency: u32) -> Result<()> {
|
||||
if self.receiving.load(Ordering::SeqCst) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
*self.frequency.lock().unwrap() = frequency;
|
||||
self.receiving.store(true, Ordering::SeqCst);
|
||||
|
||||
let receiving = self.receiving.clone();
|
||||
let event_tx = self.event_tx.clone();
|
||||
let freq = self.frequency.clone();
|
||||
let demodulator = self.demodulator.clone();
|
||||
let hackrf_available = self.hackrf_available;
|
||||
|
||||
self.rx_thread = Some(thread::spawn(move || {
|
||||
if hackrf_available {
|
||||
if let Err(e) =
|
||||
run_receiver_hackrf(receiving.clone(), event_tx.clone(), freq, demodulator)
|
||||
{
|
||||
let _ = event_tx.send(RadioEvent::Error(format!("Receiver error: {}", e)));
|
||||
}
|
||||
} else {
|
||||
run_demo_receiver(receiving, event_tx, freq);
|
||||
}
|
||||
}));
|
||||
|
||||
tracing::info!("Started receiving at {} Hz", frequency);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop receiving
|
||||
pub fn stop_receiving(&mut self) -> Result<()> {
|
||||
self.receiving.store(false, Ordering::SeqCst);
|
||||
|
||||
if let Some(handle) = self.rx_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
|
||||
tracing::info!("Stopped receiving");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set the receive frequency
|
||||
pub fn set_frequency(&mut self, frequency: u32) -> Result<()> {
|
||||
*self.frequency.lock().unwrap() = frequency;
|
||||
tracing::info!("Set frequency to {} Hz", frequency);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Transmit a signal
|
||||
pub fn transmit(&mut self, signal: &[LevelDuration], frequency: u32) -> Result<()> {
|
||||
if !self.hackrf_available {
|
||||
tracing::warn!("HackRF not available - simulating transmission");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Stop receiving first if we are
|
||||
let was_receiving = self.receiving.load(Ordering::SeqCst);
|
||||
if was_receiving {
|
||||
self.stop_receiving()?;
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"Transmitting {} level/duration pairs at {} Hz",
|
||||
signal.len(),
|
||||
frequency
|
||||
);
|
||||
|
||||
transmit_signal_hackrf(signal, frequency)?;
|
||||
|
||||
// Resume receiving if we were before
|
||||
if was_receiving {
|
||||
let freq = *self.frequency.lock().unwrap();
|
||||
self.start_receiving(freq)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set LNA gain (0-40 dB, 8 dB steps)
|
||||
pub fn set_lna_gain(&mut self, gain: u32) -> Result<()> {
|
||||
tracing::info!("Set LNA gain to {} dB", gain);
|
||||
// Note: gain changes take effect on next start_receiving
|
||||
// For now, just log - actual application happens in run_receiver_hackrf
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Set VGA gain (0-62 dB, 2 dB steps)
|
||||
pub fn set_vga_gain(&mut self, gain: u32) -> Result<()> {
|
||||
tracing::info!("Set VGA gain to {} dB", gain);
|
||||
// Note: gain changes take effect on next start_receiving
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enable/disable the RF amplifier
|
||||
pub fn set_amp_enable(&mut self, enabled: bool) -> Result<()> {
|
||||
tracing::info!("Set amp enable to {}", enabled);
|
||||
// Note: amp changes take effect on next start_receiving
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HackRfController {
|
||||
fn drop(&mut self) {
|
||||
self.receiving.store(false, Ordering::SeqCst);
|
||||
if let Some(handle) = self.rx_thread.take() {
|
||||
let _ = handle.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if HackRF is available
|
||||
fn check_hackrf_available() -> bool {
|
||||
// Try to open a HackRF device
|
||||
match libhackrf::HackRf::open() {
|
||||
Ok(_) => {
|
||||
tracing::debug!("HackRF opened successfully");
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::debug!("HackRF not available: {:?}", e);
|
||||
// Fallback: check via hackrf_info command
|
||||
match std::process::Command::new("hackrf_info")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
{
|
||||
Ok(status) => status.success(),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a demo receiver (no actual HackRF)
|
||||
fn run_demo_receiver(
|
||||
receiving: Arc<AtomicBool>,
|
||||
_event_tx: Sender<RadioEvent>,
|
||||
_frequency: Arc<Mutex<u32>>,
|
||||
) {
|
||||
tracing::info!("Demo receiver thread started (no HackRF)");
|
||||
|
||||
while receiving.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
tracing::info!("Demo receiver thread stopped");
|
||||
}
|
||||
|
||||
/// Shared state for RX callback (libhackrf requires fn pointers, not closures)
|
||||
struct RxState {
|
||||
receiving: Arc<AtomicBool>,
|
||||
event_tx: Sender<RadioEvent>,
|
||||
frequency: Arc<Mutex<u32>>,
|
||||
demodulator: Arc<Mutex<Demodulator>>,
|
||||
capture_id: std::sync::atomic::AtomicU32,
|
||||
}
|
||||
|
||||
/// RX callback function for libhackrf
|
||||
fn rx_callback(
|
||||
_hackrf: &libhackrf::HackRf,
|
||||
buffer: &[num_complex::Complex<i8>],
|
||||
user_data: &dyn std::any::Any,
|
||||
) {
|
||||
use crate::capture::StoredLevelDuration;
|
||||
|
||||
// Downcast user_data to our state
|
||||
let state = match user_data.downcast_ref::<RxState>() {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if !state.receiving.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
let current_freq = *state.frequency.lock().unwrap();
|
||||
|
||||
// Convert Complex<i8> samples to i8 pairs for demodulator
|
||||
let samples: Vec<i8> = buffer.iter()
|
||||
.flat_map(|c| [c.re, c.im])
|
||||
.collect();
|
||||
|
||||
// Process through demodulator
|
||||
if let Ok(mut demod) = state.demodulator.lock() {
|
||||
if let Some(pairs) = demod.process_samples(&samples) {
|
||||
// Convert to storable format
|
||||
let stored_pairs: Vec<StoredLevelDuration> = pairs
|
||||
.iter()
|
||||
.map(|p| StoredLevelDuration { level: p.level, duration_us: p.duration_us })
|
||||
.collect();
|
||||
|
||||
let id = state.capture_id.fetch_add(1, Ordering::SeqCst);
|
||||
let capture = Capture::from_pairs(id, current_freq, stored_pairs);
|
||||
let _ = state.event_tx.send(RadioEvent::SignalCaptured(capture));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the receiver loop with actual HackRF using libhackrf
|
||||
fn run_receiver_hackrf(
|
||||
receiving: Arc<AtomicBool>,
|
||||
event_tx: Sender<RadioEvent>,
|
||||
frequency: Arc<Mutex<u32>>,
|
||||
demodulator: Arc<Mutex<Demodulator>>,
|
||||
) -> Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
tracing::info!("HackRF receiver thread starting...");
|
||||
|
||||
// Open HackRF device
|
||||
let hackrf = libhackrf::HackRf::open()
|
||||
.context("Failed to open HackRF device")?;
|
||||
|
||||
let freq = *frequency.lock().unwrap();
|
||||
tracing::info!("Configuring HackRF: freq={} Hz, sample_rate={} Hz", freq, SAMPLE_RATE);
|
||||
|
||||
// Configure HackRF
|
||||
hackrf.set_sample_rate(SAMPLE_RATE)
|
||||
.context("Failed to set sample rate")?;
|
||||
|
||||
hackrf.set_freq(freq as u64)
|
||||
.context("Failed to set frequency")?;
|
||||
|
||||
hackrf.set_lna_gain(32)
|
||||
.context("Failed to set LNA gain")?;
|
||||
|
||||
hackrf.set_rxvga_gain(20)
|
||||
.context("Failed to set RXVGA gain")?;
|
||||
|
||||
hackrf.set_amp_enable(true)
|
||||
.context("Failed to enable amp")?;
|
||||
|
||||
tracing::info!("HackRF configured, starting RX...");
|
||||
|
||||
// Create state for callback
|
||||
let state = RxState {
|
||||
receiving: receiving.clone(),
|
||||
event_tx: event_tx.clone(),
|
||||
frequency: frequency.clone(),
|
||||
demodulator,
|
||||
capture_id: std::sync::atomic::AtomicU32::new(0),
|
||||
};
|
||||
|
||||
// Start receiving
|
||||
hackrf.start_rx(rx_callback, state)
|
||||
.context("Failed to start RX")?;
|
||||
|
||||
// Wait until receiving is stopped
|
||||
while receiving.load(Ordering::SeqCst) {
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
// Stop receiving
|
||||
hackrf.stop_rx().context("Failed to stop RX")?;
|
||||
|
||||
tracing::info!("HackRF receiver thread stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Shared state for TX callback
|
||||
struct TxState {
|
||||
samples: Vec<(i8, i8)>,
|
||||
sample_index: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
/// TX callback function for libhackrf
|
||||
fn tx_callback(
|
||||
_hackrf: &libhackrf::HackRf,
|
||||
buffer: &mut [num_complex::Complex<i8>],
|
||||
user_data: &dyn std::any::Any,
|
||||
) {
|
||||
use num_complex::Complex;
|
||||
|
||||
// Downcast user_data to our state
|
||||
let state = match user_data.downcast_ref::<TxState>() {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let total = state.samples.len();
|
||||
|
||||
for sample in buffer.iter_mut() {
|
||||
let idx = state.sample_index.fetch_add(1, Ordering::SeqCst);
|
||||
if idx < total {
|
||||
let (i, q) = state.samples[idx];
|
||||
*sample = Complex::new(i, q);
|
||||
} else {
|
||||
*sample = Complex::new(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Transmit a signal via HackRF
|
||||
fn transmit_signal_hackrf(signal: &[LevelDuration], frequency: u32) -> Result<()> {
|
||||
use anyhow::Context;
|
||||
|
||||
tracing::info!("Starting HackRF transmission at maximum power...");
|
||||
|
||||
// Open HackRF device
|
||||
let hackrf = libhackrf::HackRf::open()
|
||||
.context("Failed to open HackRF device")?;
|
||||
|
||||
// Configure for TX with MAXIMUM POWER
|
||||
hackrf.set_sample_rate(SAMPLE_RATE)
|
||||
.context("Failed to set sample rate")?;
|
||||
|
||||
hackrf.set_freq(frequency as u64)
|
||||
.context("Failed to set frequency")?;
|
||||
|
||||
// Set TX VGA gain to maximum (47 dB is the max for HackRF)
|
||||
hackrf.set_txvga_gain(47)
|
||||
.context("Failed to set TXVGA gain")?;
|
||||
|
||||
// Enable the RF amplifier for +14dB additional gain
|
||||
hackrf.set_amp_enable(true)
|
||||
.context("Failed to enable amp")?;
|
||||
|
||||
// Generate TX samples
|
||||
let tx_samples = generate_tx_samples(signal, SAMPLE_RATE);
|
||||
let total_samples = tx_samples.len();
|
||||
|
||||
tracing::debug!("Generated {} TX samples", total_samples);
|
||||
|
||||
// Create state for callback
|
||||
let state = TxState {
|
||||
samples: tx_samples,
|
||||
sample_index: std::sync::atomic::AtomicUsize::new(0),
|
||||
};
|
||||
|
||||
// Start transmitting
|
||||
hackrf.start_tx(tx_callback, state)
|
||||
.context("Failed to start TX")?;
|
||||
|
||||
// Wait for transmission to complete (check sample_index through a loop)
|
||||
// We can't easily check completion with this API, so just wait based on expected time
|
||||
let duration_us: u32 = signal.iter().map(|s| s.duration_us).sum();
|
||||
let wait_ms = (duration_us / 1000).max(100);
|
||||
std::thread::sleep(std::time::Duration::from_millis(wait_ms as u64 + 100));
|
||||
|
||||
// Stop transmitting
|
||||
hackrf.stop_tx().context("Failed to stop TX")?;
|
||||
|
||||
tracing::info!("Transmission complete");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generate TX samples from level/duration pairs
|
||||
fn generate_tx_samples(signal: &[LevelDuration], sample_rate: u32) -> Vec<(i8, i8)> {
|
||||
let mut samples = Vec::new();
|
||||
let samples_per_us = sample_rate as f64 / 1_000_000.0;
|
||||
|
||||
for ld in signal {
|
||||
let num_samples = (ld.duration_us as f64 * samples_per_us) as usize;
|
||||
let value: i8 = if ld.level { 127 } else { 0 };
|
||||
|
||||
// IQ samples
|
||||
for _ in 0..num_samples {
|
||||
samples.push((value, 0)); // I, Q (Q=0 for OOK)
|
||||
}
|
||||
}
|
||||
|
||||
samples
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//! Radio subsystem for HackRF control.
|
||||
|
||||
pub mod demodulator;
|
||||
mod hackrf;
|
||||
mod modulator;
|
||||
|
||||
pub use demodulator::LevelDuration;
|
||||
pub use hackrf::HackRfController;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use demodulator::Demodulator;
|
||||
#[allow(unused_imports)]
|
||||
pub use modulator::Modulator;
|
||||
@@ -0,0 +1,147 @@
|
||||
//! Signal modulator for generating TX waveforms.
|
||||
|
||||
use super::demodulator::LevelDuration;
|
||||
|
||||
/// Modulator for generating transmission waveforms
|
||||
#[allow(dead_code)]
|
||||
pub struct Modulator {
|
||||
/// Time element in microseconds (base timing unit)
|
||||
pub te: u32,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Modulator {
|
||||
/// Create a new modulator with the given time element
|
||||
pub fn new(te: u32) -> Self {
|
||||
Self { te }
|
||||
}
|
||||
|
||||
/// Generate a preamble (alternating pattern)
|
||||
pub fn generate_preamble(&self, count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(count * 2);
|
||||
for _ in 0..count {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Generate a sync pattern
|
||||
pub fn generate_sync(&self, high_te: u32, low_te: u32) -> Vec<LevelDuration> {
|
||||
vec![
|
||||
LevelDuration::new(true, self.te * high_te),
|
||||
LevelDuration::new(false, self.te * low_te),
|
||||
]
|
||||
}
|
||||
|
||||
/// Encode data using PWM (Pulse Width Modulation)
|
||||
/// bit 0: short high, long low
|
||||
/// bit 1: long high, short low
|
||||
pub fn encode_pwm(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(bit_count * 2);
|
||||
|
||||
for i in 0..bit_count {
|
||||
let byte_idx = i / 8;
|
||||
let bit_idx = 7 - (i % 8);
|
||||
let bit = (data[byte_idx] >> bit_idx) & 1;
|
||||
|
||||
if bit == 0 {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te * 3));
|
||||
} else {
|
||||
result.push(LevelDuration::new(true, self.te * 3));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Encode data using Manchester encoding
|
||||
/// bit 0: high then low
|
||||
/// bit 1: low then high
|
||||
pub fn encode_manchester(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(bit_count * 2);
|
||||
|
||||
for i in 0..bit_count {
|
||||
let byte_idx = i / 8;
|
||||
let bit_idx = 7 - (i % 8);
|
||||
let bit = (data[byte_idx] >> bit_idx) & 1;
|
||||
|
||||
if bit == 0 {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
} else {
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Encode data using inverted Manchester encoding
|
||||
/// bit 0: low then high
|
||||
/// bit 1: high then low
|
||||
pub fn encode_manchester_inverted(&self, data: &[u8], bit_count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(bit_count * 2);
|
||||
|
||||
for i in 0..bit_count {
|
||||
let byte_idx = i / 8;
|
||||
let bit_idx = 7 - (i % 8);
|
||||
let bit = (data[byte_idx] >> bit_idx) & 1;
|
||||
|
||||
if bit == 0 {
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
} else {
|
||||
result.push(LevelDuration::new(true, self.te));
|
||||
result.push(LevelDuration::new(false, self.te));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Generate a trailer (final low period)
|
||||
pub fn generate_trailer(&self, te_count: u32) -> Vec<LevelDuration> {
|
||||
vec![LevelDuration::new(false, self.te * te_count)]
|
||||
}
|
||||
|
||||
/// Combine multiple signal parts into one
|
||||
pub fn combine(parts: Vec<Vec<LevelDuration>>) -> Vec<LevelDuration> {
|
||||
parts.into_iter().flatten().collect()
|
||||
}
|
||||
|
||||
/// Repeat a signal pattern multiple times
|
||||
pub fn repeat(signal: &[LevelDuration], count: usize) -> Vec<LevelDuration> {
|
||||
let mut result = Vec::with_capacity(signal.len() * count);
|
||||
for _ in 0..count {
|
||||
result.extend_from_slice(signal);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_pwm_encoding() {
|
||||
let mod_ = Modulator::new(400);
|
||||
let data = vec![0b10101010];
|
||||
let encoded = mod_.encode_pwm(&data, 8);
|
||||
|
||||
assert_eq!(encoded.len(), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manchester_encoding() {
|
||||
let mod_ = Modulator::new(400);
|
||||
let data = vec![0b10101010];
|
||||
let encoded = mod_.encode_manchester(&data, 8);
|
||||
|
||||
assert_eq!(encoded.len(), 16);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user