feat: robust timing analyzer for RF device identification - iteration 2/5

- Implemented multi-method timing extraction (K-means, histogram, percentile)
- Added intelligent outlier removal with IQR method (2.5x threshold)
- Integrated timing analyzer into pattern_decoder.py
- Created comprehensive scoring system against protocol signatures
- Handles noisy/imperfect captures with tolerance windows

Components:
  - RobustTimingAnalyzer: Multi-strategy timing extraction
  - TimingCharacteristics: Extracted pulse/gap/ratio data
  - TimingScore: Similarity scoring with weighted components
  - TimingMatchStrategy: Integration with engine.py

Features:
  - Separates HIGH/LOW pulses before outlier removal (preserves alternating pattern)
  - Multi-method ensemble: tries K-means → histogram → percentile
  - Confidence scoring based on clustering quality
  - Timing ratios (short/long pulse ratios) for better matching
  - Duty cycle calculation
  - Configurable tolerance windows (default ±25%)

Test Coverage:
  - 15 new unit tests in tests/unit/test_timing_analyzer.py
  - All 41 tests passing (26 original + 15 new)
  - Coverage: clean signals, noisy signals, outliers, multi-level, scoring, real-world LaCrosse

Expected Impact:
  - Improved single-transmission accuracy (20% → 55% projected)
  - Better noise tolerance for Flipper Zero captures
  - More accurate protocol matching with 299 signatures
This commit is contained in:
leetcrypt
2026-02-14 19:06:13 -08:00
parent 9f73595b20
commit af9942f822
3 changed files with 966 additions and 11 deletions
+11 -11
View File
@@ -20,6 +20,7 @@ from src.matcher.protocol_database import (
ProtocolDatabase,
get_protocol_database
)
from src.matcher.timing_analyzer import get_timing_analyzer
@dataclass
@@ -82,6 +83,7 @@ class PatternDecoder:
def __init__(self, protocol_db: Optional[ProtocolDatabase] = None):
self.protocol_db = protocol_db or get_protocol_database()
self.timing_analyzer = get_timing_analyzer()
def decode(self, metadata: SignalMetadata) -> List[DeviceMatch]:
"""
@@ -118,7 +120,7 @@ class PatternDecoder:
def _identify_pulse_widths(self, pulses: List[int]) -> Tuple[int, int, int, int]:
"""
Identify SHORT/LONG pulse and gap durations using K-means clustering
Identify SHORT/LONG pulse and gap durations using robust timing analyzer
Returns:
(short_pulse, long_pulse, short_gap, long_gap) in microseconds
@@ -126,17 +128,15 @@ class PatternDecoder:
if not pulses:
return (0, 0, 0, 0)
# Separate positive (HIGH pulses) and negative (LOW gaps)
high_pulses = [abs(p) for p in pulses if p > 0]
low_pulses = [abs(p) for p in pulses if p < 0]
# Use robust timing analyzer for improved extraction
timing = self.timing_analyzer.extract_timing(pulses)
# Cluster high pulses into SHORT/LONG
short_pulse, long_pulse = self._cluster_durations(high_pulses)
# Cluster low pulses into SHORT/LONG
short_gap, long_gap = self._cluster_durations(low_pulses)
return (short_pulse, long_pulse, short_gap, long_gap)
return (
timing.short_pulse_us,
timing.long_pulse_us,
timing.short_gap_us,
timing.long_gap_us
)
def _cluster_durations(self, durations: List[int]) -> Tuple[int, int]:
"""