#!/usr/bin/env python3 """ Unit Tests for Preamble Detection and Frequency Fingerprinting """ import pytest from src.matcher.preamble_detector import PreambleDetector, PreamblePattern from src.matcher.frequency_fingerprint import FrequencyFingerprinter class TestPreambleDetection: """Test preamble detection algorithms""" def test_long_burst_detection(self): """Test 1: Detect long HIGH burst preamble (Princeton style)""" detector = PreambleDetector() # Long burst: 4000us followed by normal pulses (need at least 10 pulses) pulses = [4000, -500, 500, -500, 1000, -500, 500, -500, 1000, -500, 500, -500] pattern = detector.detect(pulses, timing_short=500, timing_long=1000) assert pattern is not None, "Should detect long burst" assert pattern.type == 'long_burst', f"Expected long_burst, got {pattern.type}" assert pattern.confidence > 0.7, f"Expected confidence >0.7, got {pattern.confidence}" def test_alternating_detection(self): """Test 2: Detect alternating 01010101 pattern (Manchester/Oregon)""" detector = PreambleDetector() # Alternating pattern: 500us/1000us pulses # Pattern: 01010101 (8 alternations) pulses = [ 500, -500, # 0 1000, -500, # 1 500, -500, # 0 1000, -500, # 1 500, -500, # 0 1000, -500, # 1 500, -500, # 0 1000, -500, # 1 ] * 2 # Repeat for emphasis pattern = detector.detect(pulses, timing_short=500, timing_long=1000) assert pattern is not None, "Should detect alternating pattern" assert pattern.type == 'alternating', f"Expected alternating, got {pattern.type}" assert pattern.repetitions >= 4, f"Expected >=4 repetitions, got {pattern.repetitions}" def test_sync_word_detection(self): """Test 3: Detect known sync word (e.g., '1000' for Oregon Scientific)""" detector = PreambleDetector() # Sync word "1000" (need at least 10 pulses) pulses = [ 1000, -500, # 1 500, -500, # 0 500, -500, # 0 500, -500, # 0 # Followed by data (add more to meet 10 pulse minimum) 1000, -500, 500, -500, 1000, -500, 500, -500, ] pattern = detector.detect(pulses, timing_short=500, timing_long=1000) assert pattern is not None, "Should detect sync word" assert pattern.type == 'sync_word', f"Expected sync_word, got {pattern.type}" assert '1000' in pattern.pattern, "Pattern should contain '1000'" def test_no_preamble(self): """Test 4: Handle signals without clear preamble""" detector = PreambleDetector() # Random-looking pattern pulses = [600, -400, 800, -600, 550, -450, 900, -500] pattern = detector.detect(pulses, timing_short=500, timing_long=1000) # May or may not detect - should not crash assert True, "Should handle no preamble gracefully" def test_preamble_matching_exact(self): """Test 5: Exact preamble match scoring""" detector = PreambleDetector() # Detected alternating pattern detected = PreamblePattern( type='alternating', pattern='10101010', length=8, repetitions=4, confidence=0.9, details={} ) # Protocol expects alternating match = detector.match_against_protocol( detected, protocol_preamble='"10" * 4', # "10101010" protocol_sync=None ) assert match.match_type == 'exact', f"Expected exact match, got {match.match_type}" assert match.similarity >= 0.9, f"Expected similarity >=0.9, got {match.similarity}" def test_preamble_matching_partial(self): """Test 6: Partial preamble match (type similarity)""" detector = PreambleDetector() detected = PreamblePattern( type='alternating', pattern='0101', length=4, repetitions=2, confidence=0.8, details={} ) # Protocol also uses alternating (but different exact pattern) match = detector.match_against_protocol( detected, protocol_preamble='"01" * 8', protocol_sync=None ) # Should match type even if exact pattern differs assert match.similarity >= 0.5, f"Expected partial match, got {match.similarity}" def test_preamble_mismatch(self): """Test 7: Preamble mismatch penalty""" detector = PreambleDetector() detected = PreamblePattern( type='long_burst', pattern='1111', length=4, repetitions=1, confidence=0.8, details={} ) # Protocol expects alternating (mismatch) match = detector.match_against_protocol( detected, protocol_preamble='"10" * 4', protocol_sync=None ) assert match.similarity < 0.5, f"Expected low similarity for mismatch, got {match.similarity}" class TestFrequencyFingerprinting: """Test frequency-based filtering and scoring""" def test_ism_band_detection_433mhz(self): """Test 8: Detect 433 MHz ISM band""" fingerprinter = FrequencyFingerprinter() freq = 433920000 # 433.92 MHz fingerprint = fingerprinter.analyze(freq) assert fingerprint.ism_band == '433MHz', f"Expected 433MHz band, got {fingerprint.ism_band}" assert fingerprint.center_freq == freq assert fingerprint.confidence > 0.8 def test_ism_band_detection_315mhz(self): """Test 9: Detect 315 MHz ISM band""" fingerprinter = FrequencyFingerprinter() freq = 315000000 # 315 MHz fingerprint = fingerprinter.analyze(freq) assert fingerprint.ism_band == '315MHz', f"Expected 315MHz band, got {fingerprint.ism_band}" def test_frequency_exact_match(self): """Test 10: Perfect frequency match scoring""" fingerprinter = FrequencyFingerprinter() match = fingerprinter.score_frequency_match( signal_frequency=433920000, protocol_frequency=433920000, protocol_tolerance=100000 ) assert match.score == 1.0, f"Expected score 1.0 for exact match, got {match.score}" assert match.within_tolerance is True assert match.frequency_error == 0 def test_frequency_within_tolerance(self): """Test 11: Frequency match within tolerance""" fingerprinter = FrequencyFingerprinter() # 50 kHz offset (within ±100 kHz tolerance) match = fingerprinter.score_frequency_match( signal_frequency=433970000, protocol_frequency=433920000, protocol_tolerance=100000 ) assert match.within_tolerance is True assert match.score >= 0.8, f"Expected score >=0.8 within tolerance, got {match.score}" def test_frequency_outside_tolerance(self): """Test 12: Frequency mismatch outside tolerance""" fingerprinter = FrequencyFingerprinter() # 500 kHz offset (outside ±100 kHz tolerance) match = fingerprinter.score_frequency_match( signal_frequency=434420000, protocol_frequency=433920000, protocol_tolerance=100000 ) assert match.within_tolerance is False assert match.score < 0.6, f"Expected low score outside tolerance, got {match.score}" def test_frequency_filtering(self): """Test 13: Filter protocols by frequency""" fingerprinter = FrequencyFingerprinter() # Mock protocols class MockProtocol: def __init__(self, name, freq): self.name = name self.frequency = freq self.frequency_tolerance = 100000 protocols = [ MockProtocol('433MHz Device', 433920000), MockProtocol('315MHz Device', 315000000), MockProtocol('868MHz Device', 868000000), MockProtocol('433MHz Device 2', 433850000), # Close to 433.92 ] # Filter for 433 MHz signal filtered = fingerprinter.filter_protocols_by_frequency( protocols, signal_frequency=433920000, tolerance_hz=200000 ) # Should get both 433 MHz devices assert len(filtered) == 2, f"Expected 2 protocols, got {len(filtered)}" assert all('433MHz' in p.name for p in filtered) def test_protocol_grouping_by_band(self): """Test 14: Group protocols by ISM band""" fingerprinter = FrequencyFingerprinter() class MockProtocol: def __init__(self, name, freq): self.name = name self.frequency = freq protocols = [ MockProtocol('Dev1', 433920000), MockProtocol('Dev2', 315000000), MockProtocol('Dev3', 433850000), MockProtocol('Dev4', 868000000), ] grouped = fingerprinter.group_protocols_by_band(protocols) assert '433MHz' in grouped assert '315MHz' in grouped assert '868MHz' in grouped assert len(grouped['433MHz']) == 2, "Should have 2 protocols in 433MHz band" def test_band_statistics(self): """Test 15: Get protocol count per band""" fingerprinter = FrequencyFingerprinter() class MockProtocol: def __init__(self, freq): self.frequency = freq protocols = [ MockProtocol(433920000), MockProtocol(433850000), MockProtocol(315000000), ] stats = fingerprinter.get_band_statistics(protocols) assert stats.get('433MHz', 0) == 2 assert stats.get('315MHz', 0) == 1 if __name__ == '__main__': pytest.main([__file__, '-v'])