Phase 3 Complete: Web Interface MVP
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>
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
"""
|
||||
Signature matching strategies using SQLAlchemy ORM
|
||||
|
||||
These strategies use the ORM models for cleaner database access
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from loguru import logger
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .engine import MatchStrategy, MatchResult
|
||||
from ..parser.metadata import SignalMetadata
|
||||
from ..database.models import Device, Signature
|
||||
|
||||
|
||||
class FrequencyMatcherORM(MatchStrategy):
|
||||
"""
|
||||
Match by frequency proximity (for RAW signals without protocol)
|
||||
|
||||
Confidence: 0.5-0.8 based on frequency proximity and additional factors
|
||||
"""
|
||||
|
||||
def __init__(self, session: Session, tolerance_hz: int = 10000):
|
||||
"""
|
||||
Initialize frequency matcher
|
||||
|
||||
Args:
|
||||
session: SQLAlchemy session
|
||||
tolerance_hz: Frequency tolerance in Hz (default 10kHz)
|
||||
"""
|
||||
self.session = session
|
||||
self.tolerance = tolerance_hz
|
||||
|
||||
def match(self, metadata: SignalMetadata, db=None) -> List[MatchResult]:
|
||||
"""Match by frequency proximity"""
|
||||
matches = []
|
||||
|
||||
freq_min = metadata.frequency - self.tolerance
|
||||
freq_max = metadata.frequency + self.tolerance
|
||||
|
||||
# Query signatures within frequency range
|
||||
signatures = self.session.query(Signature).filter(
|
||||
Signature.frequency.between(freq_min, freq_max)
|
||||
).all()
|
||||
|
||||
logger.debug(f"FrequencyMatcher: Found {len(signatures)} signatures in range")
|
||||
|
||||
for sig in signatures:
|
||||
device = sig.device
|
||||
|
||||
# Calculate confidence based on frequency difference
|
||||
freq_diff = abs(sig.frequency - metadata.frequency)
|
||||
base_confidence = 0.8 * (1.0 - (freq_diff / self.tolerance))
|
||||
base_confidence = max(0.5, min(0.8, base_confidence))
|
||||
|
||||
# Bonus for exact frequency match
|
||||
if freq_diff == 0:
|
||||
base_confidence = 0.9
|
||||
|
||||
matches.append(MatchResult(
|
||||
device_id=device.id,
|
||||
device_name=device.device_name or device.model,
|
||||
manufacturer=device.manufacturer or 'Unknown',
|
||||
confidence=base_confidence,
|
||||
match_method='frequency',
|
||||
match_details={
|
||||
'signature_id': sig.id,
|
||||
'frequency': sig.frequency,
|
||||
'frequency_diff_hz': freq_diff,
|
||||
'tolerance_hz': self.tolerance
|
||||
}
|
||||
))
|
||||
|
||||
logger.debug(f"FrequencyMatcher: Returning {len(matches)} matches")
|
||||
return matches
|
||||
|
||||
|
||||
class TimingMatcherORM(MatchStrategy):
|
||||
"""
|
||||
Timing pattern matching for RAW signals
|
||||
|
||||
Compares RAW_Data timing patterns to find similar signals
|
||||
Confidence: 0.6-0.9 based on timing similarity
|
||||
"""
|
||||
|
||||
def __init__(self, session: Session):
|
||||
"""Initialize timing matcher"""
|
||||
self.session = session
|
||||
|
||||
def match(self, metadata: SignalMetadata, db=None) -> List[MatchResult]:
|
||||
"""Match by timing pattern characteristics"""
|
||||
matches = []
|
||||
|
||||
if not metadata.raw_data or len(metadata.raw_data) == 0:
|
||||
logger.debug("TimingMatcher: No RAW data to match")
|
||||
return matches
|
||||
|
||||
# Calculate statistics from input signal
|
||||
abs_timings = [abs(t) for t in metadata.raw_data]
|
||||
avg_timing = sum(abs_timings) / len(abs_timings)
|
||||
min_timing = min(abs_timings)
|
||||
max_timing = max(abs_timings)
|
||||
|
||||
logger.debug(f"TimingMatcher: Input stats - avg:{avg_timing:.1f}, "
|
||||
f"min:{min_timing}, max:{max_timing}, samples:{len(metadata.raw_data)}")
|
||||
|
||||
# Query signatures with timing information at same frequency
|
||||
signatures = self.session.query(Signature).filter(
|
||||
Signature.frequency == metadata.frequency,
|
||||
Signature.timing_min.isnot(None)
|
||||
).all()
|
||||
|
||||
logger.debug(f"TimingMatcher: Found {len(signatures)} signatures with timing data")
|
||||
|
||||
for sig in signatures:
|
||||
device = sig.device
|
||||
|
||||
# Check if our timing characteristics overlap
|
||||
timing_overlap = self._check_timing_overlap(
|
||||
min_timing, max_timing, avg_timing,
|
||||
sig.timing_min, sig.timing_max
|
||||
)
|
||||
|
||||
if timing_overlap > 0:
|
||||
confidence = 0.6 + (timing_overlap * 0.3) # 0.6-0.9 range
|
||||
|
||||
matches.append(MatchResult(
|
||||
device_id=device.id,
|
||||
device_name=device.device_name or device.model,
|
||||
manufacturer=device.manufacturer or 'Unknown',
|
||||
confidence=confidence,
|
||||
match_method='timing',
|
||||
match_details={
|
||||
'signature_id': sig.id,
|
||||
'input_avg_timing': avg_timing,
|
||||
'input_range': (min_timing, max_timing),
|
||||
'signature_range': (sig.timing_min, sig.timing_max),
|
||||
'overlap_score': timing_overlap
|
||||
}
|
||||
))
|
||||
|
||||
logger.debug(f"TimingMatcher: Returning {len(matches)} matches")
|
||||
return matches
|
||||
|
||||
def _check_timing_overlap(self, in_min, in_max, in_avg, sig_min, sig_max) -> float:
|
||||
"""
|
||||
Check how well timing ranges overlap
|
||||
|
||||
Returns:
|
||||
Overlap score 0.0-1.0
|
||||
"""
|
||||
# Check if ranges overlap at all
|
||||
if in_max < sig_min or in_min > sig_max:
|
||||
return 0.0
|
||||
|
||||
# Calculate overlap percentage
|
||||
overlap_min = max(in_min, sig_min)
|
||||
overlap_max = min(in_max, sig_max)
|
||||
overlap_size = overlap_max - overlap_min
|
||||
|
||||
input_size = in_max - in_min
|
||||
sig_size = sig_max - sig_min
|
||||
|
||||
# Overlap as percentage of smallest range
|
||||
min_size = min(input_size, sig_size)
|
||||
if min_size == 0:
|
||||
# Exact match if both are single values
|
||||
return 1.0 if in_avg == sig_min else 0.0
|
||||
|
||||
overlap_pct = overlap_size / min_size
|
||||
|
||||
# Bonus if average falls within signature range
|
||||
if sig_min <= in_avg <= sig_max:
|
||||
overlap_pct = min(1.0, overlap_pct * 1.2)
|
||||
|
||||
return overlap_pct
|
||||
|
||||
|
||||
class RAWPatternMatcherORM(MatchStrategy):
|
||||
"""
|
||||
Advanced RAW pattern matching using sequence comparison
|
||||
|
||||
Compares actual RAW_Data sequences for similarity
|
||||
Confidence: 0.7-0.95 based on pattern similarity
|
||||
"""
|
||||
|
||||
def __init__(self, session: Session, min_samples: int = 10):
|
||||
"""
|
||||
Initialize RAW pattern matcher
|
||||
|
||||
Args:
|
||||
session: SQLAlchemy session
|
||||
min_samples: Minimum RAW samples needed for matching
|
||||
"""
|
||||
self.session = session
|
||||
self.min_samples = min_samples
|
||||
|
||||
def match(self, metadata: SignalMetadata, db=None) -> List[MatchResult]:
|
||||
"""Match by RAW pattern similarity"""
|
||||
matches = []
|
||||
|
||||
if not metadata.raw_data or len(metadata.raw_data) < self.min_samples:
|
||||
logger.debug(f"RAWPatternMatcher: Not enough samples "
|
||||
f"({len(metadata.raw_data) if metadata.raw_data else 0})")
|
||||
return matches
|
||||
|
||||
# Query signatures with RAW patterns at same frequency
|
||||
signatures = self.session.query(Signature).filter(
|
||||
Signature.frequency == metadata.frequency,
|
||||
Signature.raw_pattern.isnot(None)
|
||||
).all()
|
||||
|
||||
logger.debug(f"RAWPatternMatcher: Comparing against {len(signatures)} signatures")
|
||||
|
||||
for sig in signatures:
|
||||
device = sig.device
|
||||
|
||||
# Parse stored RAW pattern
|
||||
try:
|
||||
sig_raw_data = [int(x) for x in sig.raw_pattern.split(',')]
|
||||
except (ValueError, AttributeError) as e:
|
||||
logger.warning(f"Could not parse raw_pattern for signature {sig.id}: {e}")
|
||||
continue
|
||||
|
||||
if len(sig_raw_data) < self.min_samples:
|
||||
continue
|
||||
|
||||
# Compare patterns
|
||||
similarity = self._compare_raw_sequences(
|
||||
metadata.raw_data,
|
||||
sig_raw_data
|
||||
)
|
||||
|
||||
if similarity > 0.5: # Minimum threshold
|
||||
confidence = 0.7 + (similarity * 0.25) # 0.7-0.95 range
|
||||
|
||||
matches.append(MatchResult(
|
||||
device_id=device.id,
|
||||
device_name=device.device_name or device.model,
|
||||
manufacturer=device.manufacturer or 'Unknown',
|
||||
confidence=confidence,
|
||||
match_method='raw_pattern',
|
||||
match_details={
|
||||
'signature_id': sig.id,
|
||||
'similarity': similarity,
|
||||
'input_samples': len(metadata.raw_data),
|
||||
'signature_samples': len(sig_raw_data)
|
||||
}
|
||||
))
|
||||
|
||||
logger.debug(f"RAWPatternMatcher: Returning {len(matches)} matches")
|
||||
return matches
|
||||
|
||||
def _compare_raw_sequences(self, seq1: List[int], seq2: List[int]) -> float:
|
||||
"""
|
||||
Compare two RAW timing sequences
|
||||
|
||||
Uses normalized cross-correlation approach
|
||||
|
||||
Returns:
|
||||
Similarity score 0.0-1.0
|
||||
"""
|
||||
# Use shorter sequence as reference
|
||||
if len(seq1) > len(seq2):
|
||||
seq1, seq2 = seq2, seq1
|
||||
|
||||
# Normalize sequences (convert to relative timings)
|
||||
norm_seq1 = self._normalize_sequence(seq1)
|
||||
norm_seq2 = self._normalize_sequence(seq2)
|
||||
|
||||
# Find best alignment using sliding window
|
||||
best_similarity = 0.0
|
||||
window_size = min(len(norm_seq1), 50) # Limit comparison window
|
||||
|
||||
for offset in range(max(1, len(norm_seq2) - len(norm_seq1))):
|
||||
similarity = self._compare_windows(
|
||||
norm_seq1[:window_size],
|
||||
norm_seq2[offset:offset+window_size]
|
||||
)
|
||||
best_similarity = max(best_similarity, similarity)
|
||||
|
||||
return best_similarity
|
||||
|
||||
def _normalize_sequence(self, seq: List[int]) -> List[float]:
|
||||
"""
|
||||
Normalize a timing sequence
|
||||
|
||||
Converts absolute timings to relative values (0.0-1.0 range)
|
||||
"""
|
||||
abs_seq = [abs(x) for x in seq]
|
||||
max_val = max(abs_seq) if abs_seq else 1
|
||||
return [x / max_val for x in abs_seq]
|
||||
|
||||
def _compare_windows(self, window1: List[float], window2: List[float]) -> float:
|
||||
"""
|
||||
Compare two timing windows
|
||||
|
||||
Returns similarity score 0.0-1.0
|
||||
"""
|
||||
min_len = min(len(window1), len(window2))
|
||||
if min_len == 0:
|
||||
return 0.0
|
||||
|
||||
# Calculate normalized difference
|
||||
diff_sum = sum(abs(window1[i] - window2[i]) for i in range(min_len))
|
||||
avg_diff = diff_sum / min_len
|
||||
|
||||
# Convert to similarity (0.0 = identical, higher = more different)
|
||||
similarity = max(0.0, 1.0 - avg_diff)
|
||||
|
||||
return similarity
|
||||
|
||||
|
||||
class ExactMatcherORM(MatchStrategy):
|
||||
"""
|
||||
Exact protocol + frequency matching
|
||||
|
||||
For decoded signals with known protocols
|
||||
Confidence: 1.0 for perfect matches
|
||||
"""
|
||||
|
||||
def __init__(self, session: Session):
|
||||
"""Initialize exact matcher"""
|
||||
self.session = session
|
||||
|
||||
def match(self, metadata: SignalMetadata, db=None) -> List[MatchResult]:
|
||||
"""Match by exact protocol and frequency"""
|
||||
matches = []
|
||||
|
||||
if not metadata.protocol or metadata.protocol == 'RAW':
|
||||
return matches
|
||||
|
||||
# Query for exact matches
|
||||
signatures = self.session.query(Signature).filter(
|
||||
Signature.protocol == metadata.protocol,
|
||||
Signature.frequency == metadata.frequency
|
||||
).all()
|
||||
|
||||
for sig in signatures:
|
||||
device = sig.device
|
||||
|
||||
matches.append(MatchResult(
|
||||
device_id=device.id,
|
||||
device_name=device.device_name or device.model,
|
||||
manufacturer=device.manufacturer or 'Unknown',
|
||||
confidence=1.0,
|
||||
match_method='exact',
|
||||
match_details={
|
||||
'signature_id': sig.id,
|
||||
'protocol': sig.protocol,
|
||||
'frequency': sig.frequency
|
||||
}
|
||||
))
|
||||
|
||||
logger.debug(f"ExactMatcher: Returning {len(matches)} matches")
|
||||
return matches
|
||||
Reference in New Issue
Block a user