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:
2026-01-12 18:21:11 -08:00
parent bba30ad2da
commit 48fcb00241
39 changed files with 10138 additions and 12 deletions
+356
View File
@@ -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