#!/usr/bin/env python3 """ Deep RF Signal Analysis and Device Identification Analyzes T-Embed captures and identifies likely devices based on: - Frequency band - Timing patterns - Pulse characteristics - Known device signatures in the 915 MHz ISM band """ import sys from pathlib import Path from typing import List, Dict, Any, Tuple import statistics # Add project root to path sys.path.insert(0, str(Path(__file__).parent.parent)) from src.parser.sub_parser import SubFileParser class RFSignalAnalyzer: """Deep analysis of RF signals to identify device types""" # Known 915 MHz ISM band devices and their characteristics KNOWN_915MHZ_DEVICES = { 'wireless_sensor': { 'name': 'Wireless Sensor (Temperature/Humidity)', 'timing_range': (50, 1500), 'avg_pulse_range': (200, 600), 'pulse_count_range': (40, 100), 'characteristics': ['Regular pulses', 'Short transmission bursts'], 'manufacturers': ['Acurite', 'La Crosse', 'Oregon Scientific', 'Generic'], 'confidence_multiplier': 0.9 }, 'tpms': { 'name': 'Tire Pressure Monitoring System (TPMS)', 'timing_range': (30, 800), 'avg_pulse_range': (100, 400), 'pulse_count_range': (50, 150), 'characteristics': ['Periodic transmission', 'Short data packets'], 'manufacturers': ['Schrader', 'Continental', 'Sensata'], 'confidence_multiplier': 0.85 }, 'door_window_sensor': { 'name': 'Door/Window Security Sensor', 'timing_range': (100, 2000), 'avg_pulse_range': (300, 800), 'pulse_count_range': (20, 80), 'characteristics': ['On-demand transmission', 'Low duty cycle'], 'manufacturers': ['SimpliSafe', 'Ring', 'ADT', 'Generic'], 'confidence_multiplier': 0.8 }, 'utility_meter': { 'name': 'Smart Utility Meter', 'timing_range': (200, 3000), 'avg_pulse_range': (400, 1200), 'pulse_count_range': (100, 300), 'characteristics': ['Regular interval transmission', 'Long packets'], 'manufacturers': ['Itron', 'Landis+Gyr', 'Sensus'], 'confidence_multiplier': 0.75 }, 'motion_sensor': { 'name': 'Motion Detector / PIR Sensor', 'timing_range': (50, 1000), 'avg_pulse_range': (150, 500), 'pulse_count_range': (30, 90), 'characteristics': ['Event-triggered', 'Quick bursts'], 'manufacturers': ['Generic', 'Smart Home Brands'], 'confidence_multiplier': 0.7 }, 'remote_control': { 'name': '915MHz Remote Control', 'timing_range': (100, 2500), 'avg_pulse_range': (250, 900), 'pulse_count_range': (20, 70), 'characteristics': ['Manual trigger', 'Short commands'], 'manufacturers': ['Generic', 'Industrial'], 'confidence_multiplier': 0.65 }, 'iot_generic': { 'name': 'Generic IoT Device', 'timing_range': (10, 5000), 'avg_pulse_range': (50, 2000), 'pulse_count_range': (10, 500), 'characteristics': ['Variable patterns'], 'manufacturers': ['Various'], 'confidence_multiplier': 0.5 } } def __init__(self): self.parser = SubFileParser() def analyze_file(self, file_path: Path) -> Dict[str, Any]: """Perform deep analysis on a .sub file""" print(f"\n{'='*80}") print(f"ANALYZING: {file_path.name}") print(f"{'='*80}\n") # Parse file try: metadata = self.parser.parse(str(file_path)) except Exception as e: return {'error': str(e)} # Check if valid if metadata.frequency == 0 or (metadata.file_format == 'RAW' and not metadata.raw_data): return {'skipped': True, 'reason': 'Empty capture'} # Basic info print("BASIC SIGNAL INFORMATION") print("-" * 80) print(f"File Type: {metadata.file_type}") print(f"Frequency: {metadata.frequency/1e6:.3f} MHz ({metadata.frequency} Hz)") print(f"Protocol: {metadata.protocol or 'RAW (undecoded)'}") print(f"Format: {metadata.file_format}") print(f"Modulation: {metadata.modulation or 'Unknown'}") # Analyze RAW data if not metadata.raw_data: print("\nNo RAW data to analyze") return {'error': 'No RAW data'} analysis = self._analyze_timing(metadata.raw_data) print(f"\nRAW TIMING ANALYSIS") print("-" * 80) print(f"Total Samples: {analysis['total_samples']}") print(f"Pulse Count: {analysis['pulse_count']} (positive values)") print(f"Gap Count: {analysis['gap_count']} (negative values)") print(f"\nTiming Statistics (microseconds):") print(f" Min: {analysis['timing_min']} μs") print(f" Max: {analysis['timing_max']} μs") print(f" Average: {analysis['timing_avg']:.2f} μs") print(f" Median: {analysis['timing_median']:.2f} μs") print(f" Std Dev: {analysis['timing_stddev']:.2f} μs") print(f"\nPulse Width Analysis:") print(f" Avg Pulse: {analysis['avg_pulse_width']:.2f} μs") print(f" Avg Gap: {analysis['avg_gap_width']:.2f} μs") print(f" Pulse/Gap: {analysis['pulse_gap_ratio']:.2f}") # Pattern analysis pattern_analysis = self._analyze_pattern(metadata.raw_data) print(f"\nPATTERN CHARACTERISTICS") print("-" * 80) print(f"Repeating Patterns: {pattern_analysis['has_repetition']}") print(f"Pattern Regularity: {pattern_analysis['regularity']}") print(f"Transmission Type: {pattern_analysis['transmission_type']}") # Device identification print(f"\n{'='*80}") print("DEVICE IDENTIFICATION") print(f"{'='*80}\n") # Match against known devices matches = self._identify_device(metadata.frequency, analysis, pattern_analysis) if matches: print(f"Found {len(matches)} potential match(es):\n") for i, match in enumerate(matches, 1): print(f"{i}. {match['name']}") print(f" Confidence: {match['confidence']:.1%}") print(f" Match Score: {match['score']:.2f}/1.0") print(f" Manufacturers: {', '.join(match['manufacturers'])}") print(f" Characteristics: {', '.join(match['characteristics'])}") print(f"\n Match Details:") for detail_key, detail_val in match['match_details'].items(): print(f" {detail_key}: {detail_val}") print() # Best match best = matches[0] print(f"{'='*80}") print(f"MOST LIKELY DEVICE: {best['name']}") print(f"Confidence: {best['confidence']:.1%}") print(f"{'='*80}") else: print("❌ No matches found in known device database") print("\nThis could be:") print(" - A custom/proprietary device") print(" - A new/unknown protocol") print(" - Interference or noise") return { 'file': file_path.name, 'frequency': metadata.frequency, 'analysis': analysis, 'pattern': pattern_analysis, 'matches': matches } def _analyze_timing(self, raw_data: List[int]) -> Dict[str, Any]: """Analyze timing characteristics""" abs_timings = [abs(t) for t in raw_data] pulses = [t for t in raw_data if t > 0] gaps = [abs(t) for t in raw_data if t < 0] analysis = { 'total_samples': len(raw_data), 'pulse_count': len(pulses), 'gap_count': len(gaps), 'timing_min': min(abs_timings), 'timing_max': max(abs_timings), 'timing_avg': statistics.mean(abs_timings), 'timing_median': statistics.median(abs_timings), 'timing_stddev': statistics.stdev(abs_timings) if len(abs_timings) > 1 else 0, } if pulses: analysis['avg_pulse_width'] = statistics.mean(pulses) else: analysis['avg_pulse_width'] = 0 if gaps: analysis['avg_gap_width'] = statistics.mean(gaps) else: analysis['avg_gap_width'] = 0 if analysis['avg_gap_width'] > 0: analysis['pulse_gap_ratio'] = analysis['avg_pulse_width'] / analysis['avg_gap_width'] else: analysis['pulse_gap_ratio'] = 0 return analysis def _analyze_pattern(self, raw_data: List[int]) -> Dict[str, Any]: """Analyze signal patterns""" # Check for repetition has_repetition = self._check_repetition(raw_data) # Calculate regularity (coefficient of variation) abs_timings = [abs(t) for t in raw_data] avg = statistics.mean(abs_timings) stddev = statistics.stdev(abs_timings) if len(abs_timings) > 1 else 0 cv = (stddev / avg) if avg > 0 else 0 if cv < 0.5: regularity = "High (uniform timing)" elif cv < 1.5: regularity = "Moderate (some variation)" else: regularity = "Low (highly variable)" # Determine transmission type if cv < 0.7 and has_repetition: transmission_type = "Periodic (sensor/beacon)" elif cv > 2.0: transmission_type = "Bursty (on-demand)" else: transmission_type = "Mixed (varies)" return { 'has_repetition': has_repetition, 'regularity': regularity, 'coefficient_variation': cv, 'transmission_type': transmission_type } def _check_repetition(self, raw_data: List[int], window_size: int = 10) -> bool: """Check if pattern has repetition""" if len(raw_data) < window_size * 2: return False # Simple check: see if first window repeats window1 = raw_data[:window_size] for i in range(window_size, len(raw_data) - window_size): window2 = raw_data[i:i+window_size] # Check similarity matches = sum(1 for j in range(window_size) if abs(window1[j] - window2[j]) < abs(window1[j]) * 0.2) if matches >= window_size * 0.7: # 70% similarity return True return False def _identify_device(self, frequency: int, timing_analysis: Dict, pattern_analysis: Dict) -> List[Dict[str, Any]]: """Identify device based on RF characteristics""" freq_mhz = frequency / 1e6 # Only process 915 MHz ISM band if not (900 <= freq_mhz <= 930): return [] matches = [] for device_key, device_info in self.KNOWN_915MHZ_DEVICES.items(): score = 0.0 match_details = {} # Check timing range timing_match = self._check_range_match( timing_analysis['timing_avg'], device_info['timing_range'] ) score += timing_match * 0.3 match_details['Timing Match'] = f"{timing_match:.1%}" # Check average pulse pulse_match = self._check_range_match( timing_analysis['avg_pulse_width'], device_info['avg_pulse_range'] ) score += pulse_match * 0.3 match_details['Pulse Match'] = f"{pulse_match:.1%}" # Check pulse count pulse_count_match = self._check_range_match( timing_analysis['pulse_count'], device_info['pulse_count_range'] ) score += pulse_count_match * 0.2 match_details['Count Match'] = f"{pulse_count_match:.1%}" # Pattern characteristics bonus if 'Periodic' in pattern_analysis['transmission_type'] and 'sensor' in device_key: score += 0.1 match_details['Pattern Bonus'] = 'Periodic transmission (sensor-like)' if 'Bursty' in pattern_analysis['transmission_type'] and 'remote' in device_key: score += 0.1 match_details['Pattern Bonus'] = 'Bursty transmission (control-like)' # Only include if reasonable match if score > 0.3: confidence = score * device_info['confidence_multiplier'] matches.append({ 'device_key': device_key, 'name': device_info['name'], 'confidence': confidence, 'score': score, 'manufacturers': device_info['manufacturers'], 'characteristics': device_info['characteristics'], 'match_details': match_details }) # Sort by confidence matches.sort(key=lambda x: x['confidence'], reverse=True) return matches def _check_range_match(self, value: float, range_tuple: Tuple[float, float]) -> float: """ Check how well a value fits within a range Returns: 0.0-1.0 score """ min_val, max_val = range_tuple if min_val <= value <= max_val: # Value is within range center = (min_val + max_val) / 2 distance = abs(value - center) range_size = (max_val - min_val) / 2 # Score decreases as we move from center score = 1.0 - (distance / range_size) if range_size > 0 else 1.0 return max(0.5, score) # At least 0.5 if in range elif value < min_val: # Below range distance = min_val - value return max(0.0, 1.0 - (distance / min_val)) else: # Above range distance = value - max_val return max(0.0, 1.0 - (distance / max_val)) def main(): """Main entry point""" print("="*80) print("T-EMBED RF DEVICE IDENTIFICATION") print("Deep Signal Analysis & Device Detection") print("="*80) # Find T-Embed files tembed_dir = Path(__file__).parent.parent / 'signatures' / 't-embed-rf' if not tembed_dir.exists(): print(f"❌ Directory not found: {tembed_dir}") return 1 sub_files = sorted(tembed_dir.glob('*.sub')) print(f"\nFound {len(sub_files)} .sub files to analyze\n") analyzer = RFSignalAnalyzer() results = [] # Analyze each file for sub_file in sub_files: result = analyzer.analyze_file(sub_file) if 'error' not in result and 'skipped' not in result: results.append(result) # Final summary print(f"\n{'='*80}") print("SUMMARY: DEVICES DETECTED") print(f"{'='*80}\n") if results: for i, result in enumerate(results, 1): print(f"{i}. {result['file']}") print(f" Frequency: {result['frequency']/1e6:.2f} MHz") if result['matches']: best_match = result['matches'][0] print(f" Identified: {best_match['name']}") print(f" Confidence: {best_match['confidence']:.1%}") print(f" Likely Manufacturer: {best_match['manufacturers'][0]}") else: print(f" Identified: Unknown device") print() else: print("No valid devices detected (all files were empty or parse errors)\n") print("="*80) return 0 if __name__ == '__main__': sys.exit(main())