diff --git a/docs/CLEANUP_AND_OPTIMIZATION_PLAN.md b/docs/CLEANUP_AND_OPTIMIZATION_PLAN.md
new file mode 100644
index 0000000..baf173e
--- /dev/null
+++ b/docs/CLEANUP_AND_OPTIMIZATION_PLAN.md
@@ -0,0 +1,1103 @@
+# GigLez Cleanup & Optimization Plan
+
+**Date:** 2026-01-14
+**Purpose:** Identify bloat, unused code, and plan optimizations + JavaScript implementation
+
+---
+
+## Part 1: Code Bloat Analysis
+
+### A. Duplicate/Redundant Matcher Implementations
+
+#### ๐ด HIGH PRIORITY - Consolidate Matchers
+
+**Current State:**
+```
+src/matcher/
+โโโ engine.py # Base MatchStrategy interface (3.4 KB)
+โโโ strategies.py # NEW unified strategy system (610 lines) โ
+โโโ strategies_orm.py # OLD ORM-based strategies (356 lines) โ REDUNDANT
+โโโ simple_matcher.py # Standalone matcher (351 lines) โ REDUNDANT
+โโโ rtl433_matcher.py # Standalone RTL_433 (352 lines) โ REDUNDANT
+โโโ rtl433_decoder.py # RTL_433 decoder (385 lines) โ
USED
+โโโ pattern_decoder.py # Pattern decoder (461 lines) โ
USED
+โโโ protocol_database.py # Protocol DB (435 lines) โ
USED
+```
+
+**Problem:**
+- **3 different matcher implementations** doing similar things
+- `strategies.py` is the NEW unified system (includes all strategies)
+- `strategies_orm.py`, `simple_matcher.py`, `rtl433_matcher.py` are OLD and redundant
+
+**Evidence:**
+```python
+# main_simple.py uses:
+from src.matcher.simple_matcher import get_matcher # OLD
+from src.matcher.rtl433_decoder import get_decoder # OK
+
+# strategies.py contains:
+class ExactMatcher(MatchStrategy) # Replaces simple_matcher
+class FrequencyMatcher(MatchStrategy) # Replaces simple_matcher
+class BitPatternMatcher(MatchStrategy) # Replaces simple_matcher
+class TimingMatcher(MatchStrategy) # Replaces simple_matcher
+class RTL433DecoderStrategy(MatchStrategy) # Wraps rtl433_decoder
+class PatternBasedStrategy(MatchStrategy) # NEW - pattern_decoder
+```
+
+**Action Plan:**
+1. โ
**Keep**: `strategies.py` (unified system)
+2. โ
**Keep**: `rtl433_decoder.py` (used by RTL433DecoderStrategy)
+3. โ
**Keep**: `pattern_decoder.py` (used by PatternBasedStrategy)
+4. โ
**Keep**: `protocol_database.py` (used by pattern_decoder)
+5. โ
**Keep**: `engine.py` (base interfaces)
+6. โ **DELETE**: `strategies_orm.py` (old, unused)
+7. โ **DELETE**: `simple_matcher.py` (replaced by strategies.py)
+8. โ **DELETE**: `rtl433_matcher.py` (replaced by strategies.py)
+
+**Impact:**
+- Remove ~1,059 lines of redundant code
+- Eliminate maintenance burden of 3 parallel implementations
+- Simplify API by using only `strategies.py`
+
+---
+
+### B. Dual API Implementations
+
+#### ๐ก MEDIUM PRIORITY - Consolidate APIs
+
+**Current State:**
+```
+src/api/
+โโโ main.py # Full ORM-based API (280 lines)
+โโโ main_simple.py # Simplified API (536 lines) โ
CURRENTLY USED
+```
+
+**Problem:**
+- Two FastAPI implementations
+- `main_simple.py` is actively developed and used
+- `main.py` uses old ORM approach (SQLAlchemy)
+- Confusion about which is "production"
+
+**Recommendation:**
+1. โ
**Keep**: `main_simple.py` (rename to `main.py` or `app.py`)
+2. โ **Archive**: Current `main.py` โ `main_orm_legacy.py` or delete
+
+**Reason:**
+- `main_simple.py` has RTL_433 integration
+- `main_simple.py` uses modern psycopg2 approach
+- Less abstraction = easier debugging
+
+---
+
+### C. Unused/Redundant Scripts
+
+#### ๐ข LOW PRIORITY - Archive Old Scripts
+
+**Current State:** 19 Python scripts in `scripts/`
+
+**Categories:**
+
+**1. Active/Essential (Keep):**
+- โ
`test_pattern_decoder.py` - New pattern decoder tests
+- โ
`test_real_weather_sensors.py` - RTL_433 validation
+- โ
`test_rtl433_with_known_devices.py` - RTL_433 tests
+- โ
`parse_rtl433_devices.py` - RTL_433 device catalog builder
+- โ
`download_rf_test_datasets.sh` - Test data fetcher
+
+**2. Utility/Maintenance (Keep):**
+- โ
`cleanup_database.py` - Database maintenance
+- โ
`import_wardriving_data.py` - CSV import for wardriving
+- โ
`create_test_dataset.py` - Test data generator
+
+**3. One-Time/Experimental (Archive):**
+- ๐ `analyze_flipper_signatures.py` - One-time analysis
+- ๐ `analyze_tembed_files.py` - One-time analysis
+- ๐ `identify_tembed_devices.py` - One-time analysis
+- ๐ `import_flipper_sqlite.py` - One-time import
+- ๐ `import_tembed_signatures.py` - One-time import
+- ๐ `match_tembed_with_db.py` - Exploratory
+- ๐ `match_with_flipper_db.py` - Exploratory
+- ๐ `rematch_captures.py` - One-time rematch
+- ๐ `test_gps_extraction.py` - Early prototype
+- ๐ `test_tembed_matching.py` - Early prototype
+- ๐ `test_wardriving_import.py` - Import test
+
+**Action Plan:**
+Create `scripts/archive/` directory and move 11 one-time scripts there.
+
+**Impact:**
+- Cleaner `scripts/` directory (8 files vs 19)
+- Keep history but reduce clutter
+- Easy to restore if needed
+
+---
+
+### D. Unused Parser Modules
+
+#### ๐ข LOW PRIORITY - Review Parsers
+
+**Current State:**
+```
+src/parser/
+โโโ sub_parser.py # Main .sub parser โ
USED
+โโโ rtl433_converter.py # RAW โ RTL_433 converter โ
USED
+โโโ metadata.py # SignalMetadata class โ
USED
+โโโ raw_parser.py # RAW signal analyzer (326 lines) ?
+โโโ gps_extractor.py # GPS from filenames (241 lines) ?
+โโโ wardriving_importer.py # CSV wardriving import (333 lines) ?
+```
+
+**Questions:**
+1. Is `raw_parser.py` still used? (Seems superseded by pattern_decoder.py)
+2. Is `gps_extractor.py` actively used?
+3. Is `wardriving_importer.py` production-ready?
+
+**Recommendation:**
+- Check imports/usage in API
+- If unused, move to `deprecated/` folder
+- Document if they're experimental
+
+---
+
+### E. Database Connection Complexity
+
+#### ๐ก MEDIUM PRIORITY - Simplify Database Layer
+
+**Current State:**
+```
+Database layer has 3 approaches:
+1. config/database.py # Psycopg2 connection pooling
+2. src/database/connection.py # SQLAlchemy ORM
+3. src/database/models.py # SQLAlchemy models (564 lines)
+```
+
+**Problem:**
+- `main_simple.py` uses psycopg2 approach (simpler)
+- `main.py` uses SQLAlchemy ORM approach (complex)
+- `strategies_orm.py` uses SQLAlchemy models
+- Dual maintenance burden
+
+**Recommendation:**
+Since we're keeping `main_simple.py`:
+1. โ
**Keep**: `config/database.py` (psycopg2 pooling)
+2. ๐ **Archive**: `src/database/connection.py` (if not used elsewhere)
+3. ๐ **Archive**: `src/database/models.py` (if only used by old main.py)
+
+---
+
+## Part 2: Optimization Opportunities
+
+### A. Pattern Decoder Enhancements
+
+#### ๐ต ENHANCEMENT - Improve Decode Rate
+
+**Current Performance:**
+- 44.4% decode rate (4/9 files)
+- Confidence range: 33-76%
+
+**Optimization Ideas:**
+
+**1. Lower Confidence Thresholds (Easy - 30 min)**
+```python
+# Current
+proto.min_confidence = 0.6 # Too strict?
+
+# Proposed
+proto.min_confidence = 0.4 # Allow more matches
+```
+
+**2. Add More Protocols (Medium - 2 hours)**
+- Currently: 18 protocols
+- Target: 50+ protocols
+- Source: Flipper Zero firmware protocol definitions
+
+**3. Improve Timing Tolerance (Easy - 1 hour)**
+```python
+# Current
+timing_tolerance = 0.2 # ยฑ20%
+
+# Proposed: Adaptive tolerance based on pulse count
+if pulse_count < 150:
+ timing_tolerance = 0.3 # ยฑ30% for short captures
+else:
+ timing_tolerance = 0.2 # ยฑ20% for longer captures
+```
+
+**4. Add Preamble Pattern Matching (Medium - 2 hours)**
+```python
+# Currently not fully utilized
+if proto.preamble_pattern and proto.preamble_pattern in bit_pattern:
+ pattern_confidence = 1.0
+```
+
+Enhance to use fuzzy pattern matching (Levenshtein distance).
+
+---
+
+### B. API Integration of Pattern Decoder
+
+#### ๐ด CRITICAL - Pattern Decoder Not Integrated!
+
+**Problem:**
+Pattern decoder exists in `strategies.py` but is **NOT** used by API!
+
+**Current API Flow:**
+```python
+# main_simple.py
+from src.matcher.simple_matcher import get_matcher # OLD
+from src.matcher.rtl433_decoder import get_decoder # OK
+
+# Upload handler uses:
+matcher = get_matcher() # Returns OLD simple_matcher
+matches = matcher.match_device(metadata) # DOESN'T use strategies.py!
+
+decoder = get_rtl433_decoder() # Separate call
+rtl433_devices = decoder.decode(metadata)
+```
+
+**What's Missing:**
+The new unified `strategies.py` system with `PatternBasedStrategy` is **NOT CONNECTED** to the API!
+
+**Fix Required:**
+```python
+# NEW approach (use strategies.py):
+from src.matcher.engine import MatchEngine
+from src.matcher.strategies import (
+ ExactMatcher,
+ FrequencyMatcher,
+ BitPatternMatcher,
+ TimingMatcher,
+ RTL433DecoderStrategy,
+ PatternBasedStrategy # NEW!
+)
+
+# Initialize engine
+engine = MatchEngine()
+engine.add_strategy(ExactMatcher())
+engine.add_strategy(FrequencyMatcher())
+engine.add_strategy(BitPatternMatcher())
+engine.add_strategy(TimingMatcher())
+engine.add_strategy(RTL433DecoderStrategy()) # Uses rtl433_decoder
+engine.add_strategy(PatternBasedStrategy()) # Uses pattern_decoder
+
+# In upload handler:
+matches = engine.match(metadata, db) # Returns all matches from all strategies
+```
+
+**Impact:**
+- Pattern decoder will finally be used in production!
+- Unified matching pipeline
+- Easier to add new strategies
+
+---
+
+### C. Frontend JavaScript Implementation
+
+#### ๐ต NEW FEATURE - Client-Side Pattern Decoder
+
+**Why JavaScript?**
+1. Decode signals client-side before upload (privacy)
+2. Real-time feedback during capture
+3. Reduce server load
+4. Works offline (PWA capability)
+
+**Use Cases:**
+1. **Web Interface**: Decode .sub files in browser before upload
+2. **Mobile App**: Real-time decoding during T-Embed capture
+3. **Offline Mode**: Decode signals without server
+
+---
+
+## Part 3: JavaScript Implementation Plan
+
+### Architecture: Port Pattern Decoder to JavaScript
+
+#### Target: Node.js + Browser-Compatible Module
+
+**File Structure:**
+```
+src/matcher/js/
+โโโ protocol-database.js # Protocol signatures (ES6 module)
+โโโ pattern-decoder.js # Core decoder logic
+โโโ pulse-analyzer.js # K-means clustering for pulse widths
+โโโ fingerprint.js # Statistical fingerprinting
+โโโ index.js # Main export
+```
+
+---
+
+### JavaScript Implementation: Core Components
+
+#### 1. Protocol Database (protocol-database.js)
+
+```javascript
+/**
+ * RF Protocol Signature Database
+ * Port of protocol_database.py
+ */
+
+export class ProtocolSignature {
+ constructor({
+ name,
+ category,
+ manufacturer = null,
+ frequency = 433920000,
+ shortPulseUs = 500,
+ longPulseUs = 1000,
+ timingTolerance = 0.2,
+ preamblePattern = null,
+ syncPattern = null,
+ minBits = 24,
+ maxBits = 64,
+ typicalPulseCount = 100
+ }) {
+ this.name = name;
+ this.category = category;
+ this.manufacturer = manufacturer;
+ this.frequency = frequency;
+ this.shortPulseUs = shortPulseUs;
+ this.longPulseUs = longPulseUs;
+ this.timingTolerance = timingTolerance;
+ this.preamblePattern = preamblePattern;
+ this.syncPattern = syncPattern;
+ this.minBits = minBits;
+ this.maxBits = maxBits;
+ this.typicalPulseCount = typicalPulseCount;
+ }
+
+ matchesTiming(shortUs, longUs) {
+ const shortMin = this.shortPulseUs * (1 - this.timingTolerance);
+ const shortMax = this.shortPulseUs * (1 + this.timingTolerance);
+ const longMin = this.longPulseUs * (1 - this.timingTolerance);
+ const longMax = this.longPulseUs * (1 + this.timingTolerance);
+
+ return (shortMin <= shortUs && shortUs <= shortMax) &&
+ (longMin <= longUs && longUs <= longMax);
+ }
+
+ matchesFrequency(freq) {
+ const tolerance = 100000; // ยฑ100 kHz
+ return Math.abs(freq - this.frequency) <= tolerance;
+ }
+}
+
+// Weather Sensors
+export const WEATHER_SENSORS = [
+ new ProtocolSignature({
+ name: 'Oregon Scientific v2.1',
+ category: 'Weather Sensor',
+ manufacturer: 'Oregon Scientific',
+ shortPulseUs: 488,
+ longPulseUs: 976,
+ preamblePattern: '10101010'.repeat(4),
+ minBits: 64,
+ maxBits: 128,
+ typicalPulseCount: 200
+ }),
+ new ProtocolSignature({
+ name: 'Acurite Tower Sensor',
+ category: 'Weather Sensor',
+ manufacturer: 'Acurite',
+ shortPulseUs: 220,
+ longPulseUs: 440,
+ minBits: 56,
+ maxBits: 64,
+ typicalPulseCount: 130
+ }),
+ new ProtocolSignature({
+ name: 'LaCrosse TX141TH-Bv2',
+ category: 'Weather Sensor',
+ manufacturer: 'LaCrosse',
+ shortPulseUs: 500,
+ longPulseUs: 1000,
+ minBits: 40,
+ maxBits: 48,
+ typicalPulseCount: 100
+ }),
+ // ... add all 18 protocols
+];
+
+export const ALL_PROTOCOLS = [
+ ...WEATHER_SENSORS,
+ // ...GARAGE_DOOR_OPENERS,
+ // ...DOORBELLS,
+ // ...TIRE_PRESSURE,
+ // ...SECURITY_SENSORS,
+ // ...REMOTE_CONTROLS
+];
+
+export class ProtocolDatabase {
+ constructor() {
+ this.protocols = ALL_PROTOCOLS;
+ this._indexProtocols();
+ }
+
+ _indexProtocols() {
+ this.byCategory = {};
+ this.byFrequency = {};
+
+ for (const proto of this.protocols) {
+ // Index by category
+ if (!this.byCategory[proto.category]) {
+ this.byCategory[proto.category] = [];
+ }
+ this.byCategory[proto.category].push(proto);
+
+ // Index by frequency (MHz)
+ const freqMHz = Math.round(proto.frequency / 1_000_000);
+ if (!this.byFrequency[freqMHz]) {
+ this.byFrequency[freqMHz] = [];
+ }
+ this.byFrequency[freqMHz].push(proto);
+ }
+ }
+
+ findByTiming(shortUs, longUs, frequency = null) {
+ const matches = [];
+ let candidates = this.protocols;
+
+ if (frequency) {
+ const freqMHz = Math.round(frequency / 1_000_000);
+ candidates = this.byFrequency[freqMHz] || this.protocols;
+ }
+
+ for (const proto of candidates) {
+ if (proto.matchesTiming(shortUs, longUs)) {
+ if (!frequency || proto.matchesFrequency(frequency)) {
+ matches.push(proto);
+ }
+ }
+ }
+
+ return matches;
+ }
+
+ findByFrequency(frequency) {
+ return this.protocols.filter(p => p.matchesFrequency(frequency));
+ }
+}
+```
+
+---
+
+#### 2. Pulse Analyzer (pulse-analyzer.js)
+
+```javascript
+/**
+ * Pulse Width Analysis using K-means clustering
+ * Port of PatternDecoder._identify_pulse_widths()
+ */
+
+/**
+ * Simple K-means clustering for 1D data
+ */
+export function kMeans(data, k = 2, maxIterations = 100) {
+ if (data.length < k) {
+ return data.map(v => [v]);
+ }
+
+ // Initialize centroids (use min and max)
+ const sorted = [...data].sort((a, b) => a - b);
+ let centroids = [sorted[0], sorted[sorted.length - 1]];
+
+ for (let iter = 0; iter < maxIterations; iter++) {
+ // Assign points to nearest centroid
+ const clusters = Array.from({ length: k }, () => []);
+
+ for (const point of data) {
+ const distances = centroids.map(c => Math.abs(point - c));
+ const nearestIdx = distances.indexOf(Math.min(...distances));
+ clusters[nearestIdx].push(point);
+ }
+
+ // Update centroids
+ const newCentroids = clusters.map(cluster => {
+ if (cluster.length === 0) return centroids[0];
+ return cluster.reduce((a, b) => a + b, 0) / cluster.length;
+ });
+
+ // Check convergence
+ const converged = centroids.every((c, i) =>
+ Math.abs(c - newCentroids[i]) < 1
+ );
+
+ centroids = newCentroids;
+
+ if (converged) break;
+ }
+
+ return centroids.sort((a, b) => a - b);
+}
+
+/**
+ * Identify SHORT and LONG pulse widths from pulse train
+ */
+export function identifyPulseWidths(pulses) {
+ // Separate HIGH pulses (positive) and LOW gaps (negative)
+ const highPulses = pulses.filter(p => p > 0).map(Math.abs);
+ const lowPulses = pulses.filter(p => p < 0).map(Math.abs);
+
+ // Cluster into SHORT/LONG
+ const [shortPulse, longPulse] = highPulses.length >= 2
+ ? kMeans(highPulses, 2)
+ : [highPulses[0] || 0, highPulses[0] || 0];
+
+ const [shortGap, longGap] = lowPulses.length >= 2
+ ? kMeans(lowPulses, 2)
+ : [lowPulses[0] || 0, lowPulses[0] || 0];
+
+ return { shortPulse, longPulse, shortGap, longGap };
+}
+
+/**
+ * Decode pulse train to binary string (PWM encoding)
+ */
+export function decodeToBits(pulses, shortPulse, longPulse) {
+ const threshold = (shortPulse + longPulse) / 2;
+ const bits = [];
+
+ for (const pulse of pulses) {
+ if (pulse > 0) { // Only decode HIGH pulses
+ bits.push(Math.abs(pulse) < threshold ? '0' : '1');
+ }
+ }
+
+ return bits.join('');
+}
+```
+
+---
+
+#### 3. Statistical Fingerprinting (fingerprint.js)
+
+```javascript
+/**
+ * Extract statistical fingerprint from pulse data
+ */
+
+export function extractFingerprint(pulses) {
+ const highPulses = pulses.filter(p => p > 0);
+ const lowPulses = pulses.filter(p => p < 0).map(Math.abs);
+
+ // Calculate statistics
+ const meanPulse = mean(highPulses);
+ const stdPulse = std(highPulses);
+ const meanGap = mean(lowPulses);
+ const stdGap = std(lowPulses);
+
+ const totalHigh = sum(highPulses);
+ const totalLow = sum(lowPulses);
+ const totalTime = totalHigh + totalLow;
+
+ const pulseGapRatio = meanGap > 0 ? meanPulse / meanGap : 0;
+ const dutyCycle = totalTime > 0 ? totalHigh / totalTime : 0;
+
+ return {
+ meanPulseWidth: meanPulse,
+ stdPulseWidth: stdPulse,
+ meanGapWidth: meanGap,
+ stdGapWidth: stdGap,
+ pulseGapRatio,
+ dutyCycle,
+ pulseCount: pulses.length,
+ minPulse: Math.min(...highPulses),
+ maxPulse: Math.max(...highPulses),
+ minGap: Math.min(...lowPulses),
+ maxGap: Math.max(...lowPulses)
+ };
+}
+
+// Helper functions
+function mean(arr) {
+ return arr.length > 0 ? sum(arr) / arr.length : 0;
+}
+
+function sum(arr) {
+ return arr.reduce((a, b) => a + b, 0);
+}
+
+function std(arr) {
+ const m = mean(arr);
+ const variance = arr.reduce((acc, val) => acc + Math.pow(val - m, 2), 0) / arr.length;
+ return Math.sqrt(variance);
+}
+```
+
+---
+
+#### 4. Pattern Decoder (pattern-decoder.js)
+
+```javascript
+/**
+ * Main Pattern Decoder
+ * Port of PatternDecoder class
+ */
+
+import { ProtocolDatabase } from './protocol-database.js';
+import { identifyPulseWidths, decodeToBits } from './pulse-analyzer.js';
+import { extractFingerprint } from './fingerprint.js';
+
+export class DeviceMatch {
+ constructor(protocol, confidence, matchMethod, details) {
+ this.protocol = protocol;
+ this.confidence = confidence;
+ this.matchMethod = matchMethod;
+ this.details = details;
+ }
+
+ get name() { return this.protocol.name; }
+ get manufacturer() { return this.protocol.manufacturer; }
+ get category() { return this.protocol.category; }
+}
+
+export class PatternDecoder {
+ constructor() {
+ this.protocolDb = new ProtocolDatabase();
+ }
+
+ /**
+ * Decode device from signal metadata
+ * @param {Object} metadata - { frequency, raw_data: [] }
+ * @returns {DeviceMatch[]}
+ */
+ decode(metadata) {
+ if (!metadata.raw_data || metadata.raw_data.length === 0) {
+ return [];
+ }
+
+ const pulses = metadata.raw_data;
+ const frequency = metadata.frequency;
+
+ const matches = [];
+
+ // Strategy 1: Timing Pattern Analysis
+ matches.push(...this._decodeTimingPatterns(pulses, frequency));
+
+ // Strategy 2: Statistical Fingerprinting
+ const fingerprint = extractFingerprint(pulses);
+ matches.push(...this._matchFingerprint(fingerprint, frequency));
+
+ // Deduplicate and rank
+ return this._rankMatches(matches);
+ }
+
+ _decodeTimingPatterns(pulses, frequency) {
+ const matches = [];
+
+ // Identify pulse widths
+ const { shortPulse, longPulse } = identifyPulseWidths(pulses);
+
+ if (shortPulse === 0 || longPulse === 0) {
+ return [];
+ }
+
+ // Decode to binary
+ const bitPattern = decodeToBits(pulses, shortPulse, longPulse);
+
+ // Find matching protocols
+ const protocolMatches = this.protocolDb.findByTiming(
+ shortPulse,
+ longPulse,
+ frequency
+ );
+
+ for (const proto of protocolMatches) {
+ // Calculate confidence
+ const timingError = Math.abs(proto.shortPulseUs - shortPulse) / proto.shortPulseUs;
+ const timingConfidence = Math.max(0, 1.0 - timingError);
+
+ const bitCount = bitPattern.length;
+ const bitCountMatch = (proto.minBits <= bitCount && bitCount <= proto.maxBits);
+ const bitConfidence = bitCountMatch ? 1.0 : 0.5;
+
+ let patternConfidence = 0.7;
+ if (proto.preamblePattern && bitPattern.includes(proto.preamblePattern)) {
+ patternConfidence = 1.0;
+ } else if (proto.syncPattern && bitPattern.includes(proto.syncPattern)) {
+ patternConfidence = 0.9;
+ }
+
+ const overallConfidence = (
+ timingConfidence * 0.4 +
+ bitConfidence * 0.3 +
+ patternConfidence * 0.3
+ );
+
+ if (overallConfidence >= 0.6) {
+ matches.push(new DeviceMatch(
+ proto,
+ overallConfidence,
+ 'timing_pattern',
+ {
+ shortPulseUs: shortPulse,
+ longPulseUs: longPulse,
+ bitCount,
+ bitPattern: bitPattern.substring(0, 64),
+ timingError: `${(timingError * 100).toFixed(2)}%`
+ }
+ ));
+ }
+ }
+
+ return matches;
+ }
+
+ _matchFingerprint(fingerprint, frequency) {
+ const matches = [];
+ const candidates = this.protocolDb.findByFrequency(frequency);
+
+ for (const proto of candidates) {
+ const expectedMeanPulse = (proto.shortPulseUs + proto.longPulseUs) / 2;
+ const pulseError = Math.abs(expectedMeanPulse - fingerprint.meanPulseWidth) / expectedMeanPulse;
+
+ const expectedCount = proto.typicalPulseCount;
+ const countError = Math.abs(expectedCount - fingerprint.pulseCount) / expectedCount;
+
+ const pulseSimilarity = Math.max(0, 1.0 - pulseError);
+ const countSimilarity = Math.max(0, 1.0 - countError);
+
+ const overallConfidence = (pulseSimilarity * 0.6 + countSimilarity * 0.4) * 0.8;
+
+ if (overallConfidence >= 0.4) {
+ matches.push(new DeviceMatch(
+ proto,
+ overallConfidence,
+ 'fingerprint',
+ {
+ meanPulseUs: fingerprint.meanPulseWidth.toFixed(1),
+ pulseCount: fingerprint.pulseCount,
+ dutyCycle: `${(fingerprint.dutyCycle * 100).toFixed(2)}%`,
+ pulseError: `${(pulseError * 100).toFixed(2)}%`,
+ countError: `${(countError * 100).toFixed(2)}%`
+ }
+ ));
+ }
+ }
+
+ return matches;
+ }
+
+ _rankMatches(matches) {
+ // Group by protocol name
+ const byProtocol = {};
+
+ for (const match of matches) {
+ const name = match.protocol.name;
+ if (!byProtocol[name]) {
+ byProtocol[name] = [];
+ }
+ byProtocol[name].push(match);
+ }
+
+ // Keep best match per protocol
+ const bestMatches = [];
+ for (const protoMatches of Object.values(byProtocol)) {
+ const best = protoMatches.reduce((a, b) =>
+ a.confidence > b.confidence ? a : b
+ );
+ bestMatches.push(best);
+ }
+
+ // Sort by confidence
+ return bestMatches.sort((a, b) => b.confidence - a.confidence);
+ }
+}
+```
+
+---
+
+#### 5. Main Export (index.js)
+
+```javascript
+/**
+ * GigLez Pattern Decoder - JavaScript Implementation
+ * Browser and Node.js compatible
+ */
+
+export { PatternDecoder, DeviceMatch } from './pattern-decoder.js';
+export { ProtocolDatabase, ProtocolSignature } from './protocol-database.js';
+export { identifyPulseWidths, decodeToBits, kMeans } from './pulse-analyzer.js';
+export { extractFingerprint } from './fingerprint.js';
+
+// Default export for convenience
+import { PatternDecoder } from './pattern-decoder.js';
+export default PatternDecoder;
+```
+
+---
+
+### JavaScript Integration: Usage Examples
+
+#### Example 1: Node.js (Backend)
+
+```javascript
+import PatternDecoder from './src/matcher/js/index.js';
+
+// Parse .sub file (assume you have this)
+const metadata = {
+ frequency: 433920000,
+ raw_data: [788, -1004, 1194, -68, 3094, -162, 786, -66, 164, -200, ...]
+};
+
+// Decode
+const decoder = new PatternDecoder();
+const matches = decoder.decode(metadata);
+
+console.log(`Found ${matches.length} matches:`);
+for (const match of matches) {
+ console.log(` ${match.name} (${match.manufacturer}) - ${(match.confidence * 100).toFixed(1)}%`);
+}
+```
+
+#### Example 2: Browser (Frontend)
+
+```html
+
+
+
+ GigLez Pattern Decoder
+
+
+ Decode .sub File
+
+
+
+
+
+
+```
+
+#### Example 3: React Component
+
+```jsx
+import React, { useState } from 'react';
+import PatternDecoder from '../matcher/js/index.js';
+
+export function SubFileDecoder() {
+ const [matches, setMatches] = useState([]);
+ const decoder = new PatternDecoder();
+
+ const handleFileUpload = async (e) => {
+ const file = e.target.files[0];
+ const text = await file.text();
+ const metadata = parseSubFile(text);
+ const results = decoder.decode(metadata);
+ setMatches(results);
+ };
+
+ return (
+
+
+ {matches.length > 0 && (
+
+
Decoded Devices ({matches.length})
+ {matches.map((match, i) => (
+
+
{match.name}
+
Manufacturer: {match.manufacturer || 'Unknown'}
+
Category: {match.category}
+
Confidence: {(match.confidence * 100).toFixed(1)}%
+
Method: {match.matchMethod}
+
+ ))}
+
+ )}
+
+ );
+}
+```
+
+---
+
+## Part 4: Implementation Roadmap
+
+### Phase 1: Cleanup (1 day)
+
+**Priority: Remove redundant code**
+
+1. โ
**Delete redundant matchers** (~2 hours)
+ - Remove `strategies_orm.py`
+ - Remove `simple_matcher.py`
+ - Remove `rtl433_matcher.py`
+ - Update imports across codebase
+
+2. โ
**Consolidate API** (~2 hours)
+ - Rename `main_simple.py` โ `app.py` (or keep as `main_simple.py`)
+ - Archive old `main.py` โ `main_orm_legacy.py`
+ - Update `start_web.sh` to use correct file
+
+3. โ
**Archive old scripts** (~1 hour)
+ - Create `scripts/archive/` directory
+ - Move 11 one-time scripts
+ - Update `scripts/README.md`
+
+4. โ
**Review parser usage** (~1 hour)
+ - Check if `raw_parser.py`, `gps_extractor.py`, `wardriving_importer.py` are used
+ - Move to `deprecated/` if unused
+
+**Outcome:** ~1,500 lines of code removed, cleaner structure
+
+---
+
+### Phase 2: Fix Pattern Decoder Integration (2 hours)
+
+**Priority: Make pattern decoder work in production**
+
+1. โ
**Update API to use strategies.py** (~1 hour)
+ ```python
+ # Replace simple_matcher with MatchEngine
+ from src.matcher.engine import MatchEngine
+ from src.matcher.strategies import *
+
+ engine = MatchEngine()
+ engine.add_strategy(ExactMatcher())
+ engine.add_strategy(FrequencyMatcher())
+ engine.add_strategy(PatternBasedStrategy()) # NEW!
+ engine.add_strategy(RTL433DecoderStrategy())
+ ```
+
+2. โ
**Test pattern decoder in API** (~1 hour)
+ - Upload test .sub files
+ - Verify pattern matches appear in results
+ - Check confidence scores
+
+**Outcome:** Pattern decoder fully integrated and working
+
+---
+
+### Phase 3: JavaScript Implementation (3-5 days)
+
+**Priority: Port pattern decoder to JavaScript**
+
+1. โ
**Core implementation** (~2 days)
+ - `protocol-database.js` (port 18 protocols)
+ - `pulse-analyzer.js` (K-means, pulse detection)
+ - `fingerprint.js` (statistics)
+ - `pattern-decoder.js` (main logic)
+ - `index.js` (exports)
+
+2. โ
**Testing** (~1 day)
+ - Unit tests (Jest or Mocha)
+ - Test with same .sub files as Python
+ - Verify decode rate matches (44.4%)
+
+3. โ
**Browser integration** (~1-2 days)
+ - Build .sub file parser
+ - Create demo HTML page
+ - Optimize for bundle size
+ - Add to GigLez frontend
+
+**Outcome:** Client-side decoding capability
+
+---
+
+### Phase 4: Optimization (1-2 days)
+
+**Priority: Improve decode rate**
+
+1. โ
**Lower confidence thresholds** (~30 min)
+2. โ
**Add more protocols** (~2 hours)
+3. โ
**Adaptive timing tolerance** (~1 hour)
+4. โ
**Enhance pattern matching** (~2 hours)
+
+**Target:** 60%+ decode rate (vs current 44.4%)
+
+---
+
+## Summary
+
+### Code to Remove
+
+| File | Lines | Reason |
+|------|-------|--------|
+| `strategies_orm.py` | 356 | Replaced by strategies.py |
+| `simple_matcher.py` | 351 | Replaced by strategies.py |
+| `rtl433_matcher.py` | 352 | Replaced by strategies.py |
+| 11 archived scripts | ~800 | One-time use, move to archive/ |
+| **Total** | **~1,859 lines** | **19% reduction** |
+
+### Code to Add
+
+| Component | Lines (estimate) | Purpose |
+|-----------|------------------|---------|
+| JavaScript protocol DB | ~300 | Client-side decoding |
+| JavaScript decoder | ~400 | Core logic |
+| JavaScript utilities | ~200 | K-means, fingerprinting |
+| **Total** | **~900 lines** | **New capability** |
+
+### Net Result
+
+- **Remove:** 1,859 lines of redundant Python
+- **Add:** 900 lines of new JavaScript
+- **Net reduction:** 959 lines (-10%)
+- **New capability:** Client-side decoding
+- **Improved maintainability:** Single matcher system
+
+---
+
+## Next Steps
+
+**Immediate (Today):**
+1. Review this plan
+2. Get approval for deletions
+3. Start Phase 1: Cleanup
+
+**Short-term (This Week):**
+1. Complete Phase 1 & 2
+2. Start Phase 3: JavaScript implementation
+
+**Medium-term (Next Week):**
+1. Complete JavaScript implementation
+2. Test in browser
+3. Optimize decoder performance
+
+---
+
+**Status:** Plan Complete - Ready for Implementation
+**Approval Needed:** Yes (for deletions)
+**Risk Level:** Low (all changes are backwards-compatible or cleanup)
diff --git a/scripts/analyze_flipper_signatures.py b/scripts/archive/analyze_flipper_signatures.py
similarity index 100%
rename from scripts/analyze_flipper_signatures.py
rename to scripts/archive/analyze_flipper_signatures.py
diff --git a/scripts/analyze_tembed_files.py b/scripts/archive/analyze_tembed_files.py
similarity index 100%
rename from scripts/analyze_tembed_files.py
rename to scripts/archive/analyze_tembed_files.py
diff --git a/scripts/identify_tembed_devices.py b/scripts/archive/identify_tembed_devices.py
similarity index 100%
rename from scripts/identify_tembed_devices.py
rename to scripts/archive/identify_tembed_devices.py
diff --git a/scripts/import_flipper_sqlite.py b/scripts/archive/import_flipper_sqlite.py
similarity index 100%
rename from scripts/import_flipper_sqlite.py
rename to scripts/archive/import_flipper_sqlite.py
diff --git a/scripts/import_tembed_signatures.py b/scripts/archive/import_tembed_signatures.py
similarity index 100%
rename from scripts/import_tembed_signatures.py
rename to scripts/archive/import_tembed_signatures.py
diff --git a/scripts/match_tembed_with_db.py b/scripts/archive/match_tembed_with_db.py
similarity index 100%
rename from scripts/match_tembed_with_db.py
rename to scripts/archive/match_tembed_with_db.py
diff --git a/scripts/match_with_flipper_db.py b/scripts/archive/match_with_flipper_db.py
similarity index 100%
rename from scripts/match_with_flipper_db.py
rename to scripts/archive/match_with_flipper_db.py
diff --git a/scripts/rematch_captures.py b/scripts/archive/rematch_captures.py
similarity index 100%
rename from scripts/rematch_captures.py
rename to scripts/archive/rematch_captures.py
diff --git a/scripts/test_gps_extraction.py b/scripts/archive/test_gps_extraction.py
similarity index 100%
rename from scripts/test_gps_extraction.py
rename to scripts/archive/test_gps_extraction.py
diff --git a/scripts/test_tembed_matching.py b/scripts/archive/test_tembed_matching.py
similarity index 100%
rename from scripts/test_tembed_matching.py
rename to scripts/archive/test_tembed_matching.py
diff --git a/scripts/test_wardriving_import.py b/scripts/archive/test_wardriving_import.py
similarity index 100%
rename from scripts/test_wardriving_import.py
rename to scripts/archive/test_wardriving_import.py
diff --git a/src/api/main.py b/src/api/main_orm_legacy.py
similarity index 100%
rename from src/api/main.py
rename to src/api/main_orm_legacy.py
diff --git a/src/api/main_simple.py b/src/api/main_simple.py
index 142c589..ac37390 100644
--- a/src/api/main_simple.py
+++ b/src/api/main_simple.py
@@ -21,8 +21,14 @@ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from src.parser.sub_parser import SubFileParser
from src.parser.gps_extractor import GPSFilenameExtractor
-from src.matcher.simple_matcher import get_matcher
-from src.matcher.rtl433_decoder import get_decoder as get_rtl433_decoder
+from src.matcher.strategies import (
+ ExactMatcher,
+ FrequencyMatcher,
+ BitPatternMatcher,
+ TimingMatcher,
+ RTL433DecoderStrategy,
+ PatternBasedStrategy
+)
# =============================================================================
# APPLICATION INSTANCE
@@ -49,6 +55,39 @@ STORAGE_FILE.parent.mkdir(exist_ok=True)
captures_storage = []
upload_counter = 0
+# Initialize unified matcher engine with all strategies
+_matcher_engine = None
+
+# Mock database class for simplified mode (no actual database)
+class MockDB:
+ """Mock database for simplified mode - strategies that need DB will gracefully fail"""
+ def execute(self, query, params=None):
+ return []
+
+ def query(self, *args, **kwargs):
+ return []
+
+def get_matcher_engine():
+ """Get or create unified matcher engine with all strategies"""
+ global _matcher_engine
+ if _matcher_engine is None:
+ # Create mock database for strategies that need it
+ mock_db = MockDB()
+
+ # Initialize SignatureMatcher from engine.py
+ from src.matcher.engine import SignatureMatcher
+ _matcher_engine = SignatureMatcher(mock_db)
+
+ # Add all strategies in order of confidence
+ _matcher_engine.add_strategy(ExactMatcher()) # Exact protocol + frequency
+ _matcher_engine.add_strategy(FrequencyMatcher()) # Frequency-based
+ _matcher_engine.add_strategy(BitPatternMatcher()) # Bit pattern matching
+ _matcher_engine.add_strategy(TimingMatcher()) # Timing-based
+ _matcher_engine.add_strategy(RTL433DecoderStrategy()) # RTL_433 decoder
+ _matcher_engine.add_strategy(PatternBasedStrategy()) # Pattern decoder (NEW!)
+ print("โ
Initialized SignatureMatcher with 6 strategies (including Pattern Decoder)")
+ return _matcher_engine
+
def load_captures():
"""Load captures from JSON file"""
@@ -382,41 +421,13 @@ async def upload_captures(
protocol = metadata.protocol if hasattr(metadata, 'protocol') else "RAW"
preset = metadata.preset if hasattr(metadata, 'preset') else "Unknown"
- # Extract RAW_Data (convert list to string format for timing analysis)
- raw_data = None
- if hasattr(metadata, 'raw_data') and metadata.raw_data:
- # Convert list of ints to space-separated string
- raw_data = ' '.join(map(str, metadata.raw_data))
+ # Perform unified device matching with all strategies
+ # This includes: Exact, Frequency, BitPattern, Timing, RTL_433, and Pattern Decoder!
+ matcher_engine = get_matcher_engine()
+ match_results = matcher_engine.match(metadata, max_results=10)
- # Perform device matching (now with RAW data for timing analysis)
- matcher = get_matcher()
- matches = matcher.match(frequency, protocol, preset, raw_data=raw_data)
-
- # Try RTL_433 decoding for RAW signals
- rtl433_devices = []
- try:
- if hasattr(metadata, 'has_raw_data') and metadata.has_raw_data:
- decoder = get_rtl433_decoder()
- if decoder._check_rtl433_available():
- rtl433_devices = decoder.decode(metadata, enable_all_protocols=True)
- except Exception as e:
- print(f"RTL_433 decode error for {file.filename}: {e}")
-
- # Combine all matches
- all_matches = matches.copy() if matches else []
-
- # Add RTL_433 decoded devices to matches
- for rtl_dev in rtl433_devices:
- all_matches.insert(0, type('obj', (object,), {
- 'device_name': rtl_dev.model,
- 'device_category': 'RTL_433 Decoded',
- 'confidence': rtl_dev.confidence,
- 'match_method': 'rtl433_decode',
- 'description': f"{rtl_dev.manufacturer or 'Unknown'} {rtl_dev.model} (Protocol {rtl_dev.protocol_id})"
- })())
-
- # Get best match (prioritize RTL_433 if available)
- best_match = all_matches[0] if all_matches else None
+ # Get best match from unified results
+ best_match = match_results[0] if match_results else None
# Use GPS from manifest or filename
global upload_counter
@@ -434,34 +445,23 @@ async def upload_captures(
"timestamp": manifest_data.get("captures", [{}])[0].get("timestamp", ""),
"data_source": manifest_data.get("data_source", "production"), # production, test, or mock
"session_id": manifest_data.get("session_uuid", ""),
- # Device identification fields
+ # Device identification fields (from unified matcher)
"device_name": best_match.device_name if best_match else None,
- "device_category": best_match.device_category if best_match else None,
+ "device_category": f"{best_match.manufacturer} - {best_match.device_name}" if best_match else None,
"match_confidence": best_match.confidence if best_match else None,
"match_method": best_match.match_method if best_match else None,
- "device_description": best_match.description if best_match else None,
- # RTL_433 decoded devices
- "rtl433_decoded": [
- {
- "model": d.model,
- "manufacturer": d.manufacturer,
- "device_id": d.device_id,
- "protocol_id": d.protocol_id,
- "confidence": d.confidence
- }
- for d in rtl433_devices
- ] if rtl433_devices else [],
- # All matched devices (signature + RTL_433)
+ "device_description": f"{best_match.manufacturer} {best_match.device_name}" if best_match else None,
+ # All matched devices from unified matcher (includes RTL_433, Pattern Decoder, etc.)
"matched_devices": [
{
"device_name": m.device_name,
- "category": m.device_category,
+ "manufacturer": m.manufacturer,
"confidence": m.confidence,
"method": m.match_method,
- "description": m.description
+ "details": m.match_details
}
- for m in all_matches[:10] # Top 10 matches (including RTL_433)
- ] if all_matches else []
+ for m in match_results[:10] # Top 10 matches
+ ] if match_results else []
}
# Store in memory
diff --git a/src/matcher/rtl433_matcher.py b/src/matcher/rtl433_matcher.py
deleted file mode 100644
index be72ed5..0000000
--- a/src/matcher/rtl433_matcher.py
+++ /dev/null
@@ -1,352 +0,0 @@
-"""
-RTL_433 Device Matcher
-
-Loads and indexes the RTL_433 protocol database for high-accuracy device matching.
-Provides fuzzy name matching, timing-based matching, and category lookups.
-
-Author: GigLez Team
-Date: January 2026
-"""
-
-import json
-import logging
-from pathlib import Path
-from typing import List, Dict, Optional, Tuple
-from dataclasses import dataclass
-from difflib import SequenceMatcher
-
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class RTL433Match:
- """Match result from RTL_433 database"""
- device_id: str
- device_name: str
- category: str
- manufacturer: Optional[str]
- modulation: Optional[str]
- confidence: float
- match_method: str
- timing_data: Optional[Dict] = None
-
-
-class RTL433Matcher:
- """
- High-performance matcher using RTL_433 protocol database
-
- Features:
- - Fast device ID and name lookups
- - Fuzzy name matching for partial matches
- - Modulation-based filtering
- - Timing signature matching
- - Category-based fallbacks
- """
-
- def __init__(self, database_path: str = "data/rtl_433_protocols.json"):
- """
- Initialize RTL_433 matcher with protocol database
-
- Args:
- database_path: Path to rtl_433_protocols.json
- """
- self.database_path = Path(database_path)
- self.devices: List[Dict] = []
- self.device_by_id: Dict[str, Dict] = {}
- self.device_by_name: Dict[str, Dict] = {}
- self.devices_by_category: Dict[str, List[Dict]] = {}
- self.devices_by_modulation: Dict[str, List[Dict]] = {}
-
- self._load_database()
- self._build_indexes()
-
- logger.info(f"RTL_433 matcher initialized with {len(self.devices)} devices")
-
- def _load_database(self):
- """Load RTL_433 protocol database from JSON"""
- try:
- if not self.database_path.exists():
- logger.warning(f"RTL_433 database not found: {self.database_path}")
- return
-
- with open(self.database_path, 'r') as f:
- data = json.load(f)
- self.devices = data.get('devices', [])
-
- logger.info(f"Loaded {len(self.devices)} RTL_433 protocols")
-
- except Exception as e:
- logger.error(f"Failed to load RTL_433 database: {e}")
- self.devices = []
-
- def _build_indexes(self):
- """Build search indexes for fast lookups"""
- for device in self.devices:
- # Index by device ID
- device_id = device.get('device_id', '').lower()
- if device_id:
- self.device_by_id[device_id] = device
-
- # Index by device name (lowercase for case-insensitive)
- device_name = device.get('name', '').lower()
- if device_name:
- self.device_by_name[device_name] = device
-
- # Index by category
- category = device.get('category', 'other')
- if category not in self.devices_by_category:
- self.devices_by_category[category] = []
- self.devices_by_category[category].append(device)
-
- # Index by modulation
- modulation = device.get('modulation')
- if modulation:
- if modulation not in self.devices_by_modulation:
- self.devices_by_modulation[modulation] = []
- self.devices_by_modulation[modulation].append(device)
-
- logger.info(f"Built indexes: {len(self.device_by_id)} IDs, "
- f"{len(self.devices_by_category)} categories, "
- f"{len(self.devices_by_modulation)} modulations")
-
- def match_by_protocol_name(self, protocol: str) -> List[RTL433Match]:
- """
- Match by protocol/device name
-
- Tries:
- 1. Exact device ID match
- 2. Exact device name match
- 3. Fuzzy name match (similarity > 0.6)
-
- Args:
- protocol: Protocol name from .sub file (e.g., "acurite_rain_896", "Oregon")
-
- Returns:
- List of matches sorted by confidence
- """
- matches = []
- protocol_lower = protocol.lower()
-
- # 1. Exact device ID match
- if protocol_lower in self.device_by_id:
- device = self.device_by_id[protocol_lower]
- matches.append(RTL433Match(
- device_id=device['device_id'],
- device_name=device['name'],
- category=device.get('category', 'other'),
- manufacturer=device.get('manufacturer'),
- modulation=device.get('modulation'),
- confidence=0.95,
- match_method='rtl433_exact_id'
- ))
- return matches
-
- # 2. Exact device name match
- if protocol_lower in self.device_by_name:
- device = self.device_by_name[protocol_lower]
- matches.append(RTL433Match(
- device_id=device['device_id'],
- device_name=device['name'],
- category=device.get('category', 'other'),
- manufacturer=device.get('manufacturer'),
- modulation=device.get('modulation'),
- confidence=0.90,
- match_method='rtl433_exact_name'
- ))
- return matches
-
- # 3. Fuzzy matching - check all devices
- for device in self.devices:
- device_id = device.get('device_id', '').lower()
- device_name = device.get('name', '').lower()
-
- # Calculate similarity scores
- id_similarity = SequenceMatcher(None, protocol_lower, device_id).ratio()
- name_similarity = SequenceMatcher(None, protocol_lower, device_name).ratio()
-
- # Also check if protocol is substring
- substring_match = protocol_lower in device_id or protocol_lower in device_name
-
- # Take best similarity
- similarity = max(id_similarity, name_similarity)
- if substring_match:
- similarity = max(similarity, 0.7)
-
- if similarity > 0.6:
- matches.append(RTL433Match(
- device_id=device['device_id'],
- device_name=device['name'],
- category=device.get('category', 'other'),
- manufacturer=device.get('manufacturer'),
- modulation=device.get('modulation'),
- confidence=0.70 + (similarity * 0.15), # 0.70-0.85 range
- match_method='rtl433_fuzzy'
- ))
-
- # Sort by confidence
- matches.sort(key=lambda x: x.confidence, reverse=True)
- return matches[:5] # Top 5 matches
-
- def match_by_modulation(self, modulation: str, category: Optional[str] = None) -> List[RTL433Match]:
- """
- Match devices by modulation type
-
- Args:
- modulation: Modulation type (OOK, FSK, etc.)
- category: Optional category filter
-
- Returns:
- List of matches
- """
- matches = []
-
- devices = self.devices_by_modulation.get(modulation, [])
-
- # Filter by category if provided
- if category:
- devices = [d for d in devices if d.get('category') == category]
-
- # Return top matches
- for device in devices[:10]:
- matches.append(RTL433Match(
- device_id=device['device_id'],
- device_name=device['name'],
- category=device.get('category', 'other'),
- manufacturer=device.get('manufacturer'),
- modulation=device.get('modulation'),
- confidence=0.55, # Lower confidence for modulation-only match
- match_method='rtl433_modulation'
- ))
-
- return matches
-
- def match_by_timing(self, short_pulse: int, long_pulse: int,
- gap_limit: Optional[int] = None,
- tolerance: float = 0.15) -> List[RTL433Match]:
- """
- Match devices by timing signature
-
- Args:
- short_pulse: Short pulse width in microseconds
- long_pulse: Long pulse width in microseconds
- gap_limit: Gap limit in microseconds (optional)
- tolerance: Matching tolerance (default 15%)
-
- Returns:
- List of matches sorted by timing similarity
- """
- matches = []
-
- for device in self.devices:
- device_short = device.get('short_width')
- device_long = device.get('long_width')
- device_gap = device.get('gap_limit')
-
- if not (device_short and device_long):
- continue
-
- # Calculate timing similarity
- short_diff = abs(short_pulse - device_short) / device_short
- long_diff = abs(long_pulse - device_long) / device_long
-
- # Both must be within tolerance
- if short_diff <= tolerance and long_diff <= tolerance:
- # Calculate confidence based on closeness
- similarity = 1.0 - ((short_diff + long_diff) / 2)
-
- # Bonus for gap match
- gap_bonus = 0.0
- if gap_limit and device_gap:
- gap_diff = abs(gap_limit - device_gap) / device_gap
- if gap_diff <= tolerance:
- gap_bonus = 0.05
-
- confidence = 0.70 + (similarity * 0.20) + gap_bonus
-
- matches.append(RTL433Match(
- device_id=device['device_id'],
- device_name=device['name'],
- category=device.get('category', 'other'),
- manufacturer=device.get('manufacturer'),
- modulation=device.get('modulation'),
- confidence=min(confidence, 0.95),
- match_method='rtl433_timing',
- timing_data={
- 'short_pulse': device_short,
- 'long_pulse': device_long,
- 'gap_limit': device_gap,
- 'similarity': similarity
- }
- ))
-
- # Sort by confidence
- matches.sort(key=lambda x: x.confidence, reverse=True)
- return matches[:5]
-
- def get_devices_by_category(self, category: str) -> List[Dict]:
- """Get all devices in a category"""
- return self.devices_by_category.get(category, [])
-
- def get_statistics(self) -> Dict:
- """Get database statistics"""
- return {
- 'total_devices': len(self.devices),
- 'categories': {cat: len(devs) for cat, devs in self.devices_by_category.items()},
- 'modulations': {mod: len(devs) for mod, devs in self.devices_by_modulation.items()},
- 'manufacturers': len(set(d.get('manufacturer') for d in self.devices if d.get('manufacturer')))
- }
-
-
-# Global singleton instance
-_rtl433_matcher: Optional[RTL433Matcher] = None
-
-
-def get_rtl433_matcher() -> RTL433Matcher:
- """Get or create RTL_433 matcher singleton"""
- global _rtl433_matcher
- if _rtl433_matcher is None:
- _rtl433_matcher = RTL433Matcher()
- return _rtl433_matcher
-
-
-if __name__ == '__main__':
- # Test the matcher
- logging.basicConfig(level=logging.INFO)
-
- matcher = RTL433Matcher()
-
- print("\n" + "="*60)
- print("RTL_433 MATCHER TEST")
- print("="*60)
-
- # Test 1: Exact match
- print("\n1. Exact Protocol Match: 'acurite_rain_896'")
- matches = matcher.match_by_protocol_name('acurite_rain_896')
- for match in matches:
- print(f" {match.device_name} ({match.confidence:.2f}) - {match.match_method}")
-
- # Test 2: Fuzzy match
- print("\n2. Fuzzy Match: 'Oregon'")
- matches = matcher.match_by_protocol_name('Oregon')
- for match in matches:
- print(f" {match.device_name} ({match.confidence:.2f}) - {match.match_method}")
-
- # Test 3: Modulation match
- print("\n3. Modulation Match: 'OOK'")
- matches = matcher.match_by_modulation('OOK', category='weather')[:3]
- for match in matches:
- print(f" {match.device_name} ({match.confidence:.2f})")
-
- # Test 4: Timing match
- print("\n4. Timing Match: short=1000ยตs, long=2000ยตs")
- matches = matcher.match_by_timing(1000, 2000)
- for match in matches:
- print(f" {match.device_name} ({match.confidence:.2f})")
-
- # Statistics
- print("\n" + "="*60)
- stats = matcher.get_statistics()
- print(f"Total devices: {stats['total_devices']}")
- print(f"Categories: {len(stats['categories'])}")
- print(f"Modulations: {len(stats['modulations'])}")
- print("="*60)
diff --git a/src/matcher/simple_matcher.py b/src/matcher/simple_matcher.py
deleted file mode 100644
index e0335df..0000000
--- a/src/matcher/simple_matcher.py
+++ /dev/null
@@ -1,351 +0,0 @@
-"""
-Enhanced Device Matcher - RTL_433 + Timing Analysis + Frequency-based categorization
-
-Integrates:
-- RTL_433 protocol database (286 devices)
-- RAW signal timing analysis
-- Frequency-based categorization
-- Protocol pattern matching
-"""
-
-import logging
-from typing import List, Dict, Tuple, Optional
-from dataclasses import dataclass
-
-# Import enhanced matchers
-try:
- from src.matcher.rtl433_matcher import get_rtl433_matcher, RTL433Match
- from src.parser.raw_parser import parse_raw_data
- RTL433_AVAILABLE = True
-except ImportError:
- RTL433_AVAILABLE = False
- logging.warning("RTL_433 matcher not available, using fallback matching")
-
-logger = logging.getLogger(__name__)
-
-
-@dataclass
-class DeviceMatch:
- """Device identification result"""
- device_name: str
- device_category: str
- confidence: float
- match_method: str
- description: str
-
-
-class SimpleDeviceMatcher:
- """
- Lightweight device matcher using frequency-based categorization
-
- Based on research from FREQUENCY_DEVICE_CHART.md and industry standards
- """
-
- # Frequency ranges and associated device types
- FREQUENCY_PATTERNS = {
- # 313-316 MHz - North America TPMS
- (313_000_000, 316_000_000): [
- ("TPMS Sensor", "Automotive", 0.7, "Tire Pressure Monitoring System"),
- ("Car Key Fob", "Automotive", 0.5, "Vehicle remote"),
- ],
-
- # 315 MHz - North America generic
- (314_500_000, 315_500_000): [
- ("Garage Door Opener", "Home Automation", 0.6, "Generic 315MHz remote"),
- ("Wireless Doorbell", "Home Automation", 0.5, "Simple doorbell"),
- ("Security Sensor", "Security", 0.5, "Door/window sensor"),
- ],
-
- # 319.5 MHz - GE/Interlogix
- (319_000_000, 320_000_000): [
- ("GE Security Sensor", "Security", 0.8, "GE/Interlogix professional sensor"),
- ],
-
- # 345 MHz - Honeywell
- (344_000_000, 346_000_000): [
- ("Honeywell Security Sensor", "Security", 0.9, "Honeywell door/window sensor"),
- ],
-
- # 390 MHz - Chamberlain
- (389_000_000, 391_000_000): [
- ("Chamberlain Garage Door", "Home Automation", 0.9, "Chamberlain Security+ opener"),
- ],
-
- # 433.05-434.79 MHz - Primary ISM band
- (433_000_000, 435_000_000): [
- ("Generic 433MHz Device", "Consumer RF", 0.4, "Unidentified 433MHz device"),
- ("Remote Control", "Consumer RF", 0.5, "Generic remote control"),
- ("Wireless Sensor", "Sensors", 0.5, "Temperature/humidity sensor"),
- ],
-
- # 433.42 MHz - Somfy RTS
- (433_410_000, 433_430_000): [
- ("Somfy RTS Blind", "Home Automation", 0.95, "Somfy motorized blind/shutter"),
- ],
-
- # 433.92 MHz - Most common
- (433_910_000, 433_930_000): [
- ("Weather Station", "Sensors", 0.6, "433MHz weather station"),
- ("Car Key Fob", "Automotive", 0.5, "Vehicle remote control"),
- ("Gate/Garage Opener", "Home Automation", 0.5, "Nice Flor-S / FAAC"),
- ],
-
- # 868 MHz - Europe smart meters/LoRa
- (868_000_000, 870_000_000): [
- ("Smart Meter", "Utility", 0.7, "European electricity/gas meter"),
- ("LoRa Sensor", "IoT", 0.6, "Long-range IoT sensor"),
- ("Z-Wave Device", "Home Automation", 0.5, "Z-Wave smart home device"),
- ],
-
- # 915 MHz - North America IoT
- (902_000_000, 928_000_000): [
- ("RFID Tag", "Industrial", 0.6, "UHF RFID asset tag"),
- ("Smart Meter", "Utility", 0.6, "North America meter"),
- ("LoRa Sensor", "IoT", 0.5, "Long-range IoT sensor"),
- ("Industrial Sensor", "Industrial", 0.5, "SCADA/telemetry"),
- ],
- }
-
- # Protocol-specific device identification
- PROTOCOL_PATTERNS = {
- "Princeton": [
- ("Princeton Remote", "Consumer RF", 0.7, "PT2260/PT2262 generic remote"),
- ],
- "EV1527": [
- ("EV1527 Remote/Sensor", "Consumer RF", 0.8, "Cheap Chinese RF device"),
- ],
- "Keeloq": [
- ("Keeloq Remote", "Automotive", 0.9, "Encrypted rolling code (car/garage)"),
- ],
- "HCS301": [
- ("HCS301 Key Fob", "Automotive", 0.9, "Microchip encrypted remote"),
- ],
- "Oregon": [
- ("Oregon Scientific Weather Station", "Sensors", 0.95, "Oregon Scientific weather sensor"),
- ],
- "OregonScientific": [
- ("Oregon Scientific Weather Station", "Sensors", 0.95, "Oregon Scientific weather sensor"),
- ],
- "Acurite": [
- ("Acurite Weather Sensor", "Sensors", 0.95, "Acurite temperature/humidity sensor"),
- ],
- "LaCrosse": [
- ("LaCrosse Sensor", "Sensors", 0.95, "LaCrosse temperature sensor"),
- ],
- "Nexus": [
- ("Nexus Sensor", "Sensors", 0.9, "Nexus outdoor sensor"),
- ],
- "Somfy": [
- ("Somfy RTS", "Home Automation", 0.95, "Somfy motorized blind"),
- ],
- "Nice": [
- ("Nice Gate Opener", "Home Automation", 0.9, "Nice Flor-S gate remote"),
- ],
- "FAAC": [
- ("FAAC Gate Opener", "Home Automation", 0.9, "FAAC gate remote"),
- ],
- }
-
- # Modulation + frequency patterns
- MODULATION_PATTERNS = {
- ("OOK", 315_000_000, 316_000_000): [
- ("Generic 315MHz Remote", "Consumer RF", 0.6, "Simple OOK device"),
- ],
- ("OOK", 433_000_000, 435_000_000): [
- ("Generic 433MHz Remote", "Consumer RF", 0.6, "Simple OOK device"),
- ],
- ("FSK", 868_000_000, 870_000_000): [
- ("Smart Device (868MHz FSK)", "IoT", 0.7, "Advanced IoT device"),
- ],
- ("FSK", 902_000_000, 928_000_000): [
- ("Smart Device (915MHz FSK)", "IoT", 0.7, "Advanced IoT device"),
- ],
- }
-
- def match(self, frequency: int, protocol: str = None, preset: str = None,
- raw_data: str = None) -> List[DeviceMatch]:
- """
- Enhanced device matching with RTL_433 and timing analysis
-
- Args:
- frequency: Frequency in Hz
- protocol: Protocol name (e.g., "Princeton", "RAW")
- preset: Preset/modulation (e.g., "FuriHalSubGhzPresetOok270Async")
- raw_data: RAW_Data string for timing analysis (optional)
-
- Returns:
- List of DeviceMatch objects sorted by confidence
- """
- matches = []
-
- # PHASE 2: RTL_433 Protocol Database Matching (NEW!)
- if RTL433_AVAILABLE and protocol and protocol != "RAW":
- try:
- rtl433_matcher = get_rtl433_matcher()
- rtl433_matches = rtl433_matcher.match_by_protocol_name(protocol)
-
- for rtl_match in rtl433_matches:
- matches.append(DeviceMatch(
- device_name=rtl_match.device_name,
- device_category=rtl_match.category,
- confidence=rtl_match.confidence,
- match_method=rtl_match.match_method,
- description=f"{rtl_match.manufacturer or 'Unknown'} - {rtl_match.modulation or 'Unknown'} modulation"
- ))
- logger.info(f"RTL_433 match: {rtl_match.device_name} ({rtl_match.confidence:.2f})")
- except Exception as e:
- logger.warning(f"RTL_433 matching failed: {e}")
-
- # PHASE 3: Timing Analysis for RAW captures (NEW!)
- if RTL433_AVAILABLE and raw_data and protocol == "RAW":
- try:
- # Parse RAW signal
- timing_sig = parse_raw_data(raw_data)
- if timing_sig:
- logger.info(f"Timing signature: {timing_sig.short_pulse}ยตs/{timing_sig.long_pulse}ยตs, "
- f"encoding={timing_sig.encoding_type}")
-
- # Match against RTL_433 timing signatures
- rtl433_matcher = get_rtl433_matcher()
- timing_matches = rtl433_matcher.match_by_timing(
- timing_sig.short_pulse,
- timing_sig.long_pulse,
- timing_sig.gap,
- tolerance=0.15
- )
-
- for rtl_match in timing_matches:
- matches.append(DeviceMatch(
- device_name=rtl_match.device_name,
- device_category=rtl_match.category,
- confidence=rtl_match.confidence,
- match_method=rtl_match.match_method,
- description=f"Timing match: {rtl_match.timing_data['similarity']:.2%} similarity"
- ))
- logger.info(f"Timing match: {rtl_match.device_name} ({rtl_match.confidence:.2f})")
- except Exception as e:
- logger.warning(f"Timing analysis failed: {e}")
-
- # 1. Protocol-based matching (original, lower confidence)
- if protocol and protocol != "RAW":
- protocol_matches = self._match_by_protocol(protocol)
- matches.extend(protocol_matches)
-
- # 2. Exact frequency matching
- freq_matches = self._match_by_frequency(frequency)
- matches.extend(freq_matches)
-
- # 3. Modulation + frequency matching
- if preset:
- modulation = self._extract_modulation(preset)
- mod_matches = self._match_by_modulation(frequency, modulation)
- matches.extend(mod_matches)
-
- # 4. Deduplicate and sort by confidence
- unique_matches = self._deduplicate_matches(matches)
- return sorted(unique_matches, key=lambda x: x.confidence, reverse=True)
-
- def _match_by_protocol(self, protocol: str) -> List[DeviceMatch]:
- """Match by protocol name"""
- matches = []
-
- for proto_pattern, devices in self.PROTOCOL_PATTERNS.items():
- if proto_pattern.lower() in protocol.lower():
- for device_name, category, confidence, description in devices:
- matches.append(DeviceMatch(
- device_name=device_name,
- device_category=category,
- confidence=confidence,
- match_method="protocol",
- description=description
- ))
-
- return matches
-
- def _match_by_frequency(self, frequency: int) -> List[DeviceMatch]:
- """Match by frequency range"""
- matches = []
-
- for (freq_min, freq_max), devices in self.FREQUENCY_PATTERNS.items():
- if freq_min <= frequency <= freq_max:
- for device_name, category, confidence, description in devices:
- # Adjust confidence based on frequency precision
- freq_center = (freq_min + freq_max) / 2
- freq_range = freq_max - freq_min
- distance_from_center = abs(frequency - freq_center)
-
- # Reduce confidence if far from center
- if freq_range > 1_000_000: # Wide range (> 1 MHz)
- confidence_adj = confidence * (1.0 - (distance_from_center / freq_range) * 0.3)
- else: # Narrow range
- confidence_adj = confidence
-
- matches.append(DeviceMatch(
- device_name=device_name,
- device_category=category,
- confidence=max(0.3, confidence_adj), # Min 0.3
- match_method="frequency",
- description=description
- ))
-
- return matches
-
- def _match_by_modulation(self, frequency: int, modulation: str) -> List[DeviceMatch]:
- """Match by modulation + frequency"""
- matches = []
-
- for (mod, freq_min, freq_max), devices in self.MODULATION_PATTERNS.items():
- if mod == modulation and freq_min <= frequency <= freq_max:
- for device_name, category, confidence, description in devices:
- matches.append(DeviceMatch(
- device_name=device_name,
- device_category=category,
- confidence=confidence,
- match_method="modulation+frequency",
- description=description
- ))
-
- return matches
-
- def _extract_modulation(self, preset: str) -> Optional[str]:
- """Extract modulation type from preset string"""
- preset_lower = preset.lower()
-
- if "ook" in preset_lower:
- return "OOK"
- elif "fsk" in preset_lower:
- return "FSK"
- elif "ask" in preset_lower:
- return "ASK"
- else:
- return None
-
- def _deduplicate_matches(self, matches: List[DeviceMatch]) -> List[DeviceMatch]:
- """Remove duplicate device names, keeping highest confidence"""
- seen = {}
-
- for match in matches:
- if match.device_name not in seen or match.confidence > seen[match.device_name].confidence:
- seen[match.device_name] = match
-
- return list(seen.values())
-
- def get_best_match(self, frequency: int, protocol: str = None, preset: str = None) -> Optional[DeviceMatch]:
- """Get single best match"""
- matches = self.match(frequency, protocol, preset)
- return matches[0] if matches else None
-
- def format_device_string(self, match: DeviceMatch) -> str:
- """Format device match as human-readable string"""
- return f"{match.device_name} ({match.device_category})"
-
-
-# Singleton instance
-_matcher = None
-
-def get_matcher() -> SimpleDeviceMatcher:
- """Get singleton matcher instance"""
- global _matcher
- if _matcher is None:
- _matcher = SimpleDeviceMatcher()
- return _matcher
diff --git a/src/matcher/strategies_orm.py b/src/matcher/strategies_orm.py
deleted file mode 100644
index 0932bba..0000000
--- a/src/matcher/strategies_orm.py
+++ /dev/null
@@ -1,356 +0,0 @@
-"""
-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