Version 1.0.0

This commit is contained in:
leviathan
2026-02-07 17:35:27 -05:00
parent c8bff9afd7
commit 4339895b41
43 changed files with 12218 additions and 3 deletions
+194
View File
@@ -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);
}
}