48fcb00241
Major Achievements: - ✅ Full web interface (1,520+ lines of frontend code) - ✅ Interactive Leaflet.js map with marker clustering - ✅ Drag-and-drop upload system with GPS input - ✅ Search & filter UI with multi-criteria - ✅ Statistics dashboard with Chart.js - ✅ Responsive mobile-friendly design Backend: - ✅ FastAPI static file serving - ✅ Simplified server mode (main_simple.py) - ✅ Improved startup script with port auto-selection - ✅ PostgreSQL schema ready (requires setup) Database: - ✅ SQLite populated with 85 Flipper Zero signatures - ✅ Device matching system operational - ✅ Frequency-based search working Documentation: - ✅ PHASE_3_COMPLETE.md - Technical summary - ✅ WEB_INTERFACE_README.md - User guide - ✅ WEBAPP_STARTUP_GUIDE.md - Troubleshooting - ✅ POSTGRESQL_SETUP_EXPLANATION.md - DB setup guide - ✅ DATABASE_POPULATION_SUCCESS.md - Import report - ✅ DEVICE_IDENTIFICATION_REPORT.md - Matching analysis Files Created: - templates/index.html (260 lines) - static/css/main.css (500 lines) - static/js/*.js (760 lines total) - src/api/main_simple.py (simplified server) - start_web.sh (auto port selection) Status: Production MVP Ready Next: Phase 4 - API & Integration 🛰️ Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
443 lines
16 KiB
Python
Executable File
443 lines
16 KiB
Python
Executable File
#!/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())
|