4237c4bdb8
## Phase 1: Code Cleanup (~1,859 lines removed) **Deleted Redundant Matchers:** - ❌ strategies_orm.py (356 lines) - Old ORM-based strategies - ❌ simple_matcher.py (351 lines) - Replaced by strategies.py - ❌ rtl433_matcher.py (352 lines) - Replaced by strategies.py **Archived Old Scripts:** - Moved 11 one-time analysis/import scripts to scripts/archive/ - Scripts: analyze_flipper_signatures, analyze_tembed_files, identify_tembed_devices, import_flipper_sqlite, import_tembed_signatures, match_tembed_with_db, match_with_flipper_db, rematch_captures, test_gps_extraction, test_tembed_matching, test_wardriving_import **Consolidated API:** - Renamed main.py → main_orm_legacy.py (archived old ORM-based API) - main_simple.py is now the primary production API ## Phase 2: Pattern Decoder Integration ✅ **CRITICAL FIX: Pattern decoder now integrated into production API!** **Changes:** 1. Updated main_simple.py to use unified SignatureMatcher 2. Added 6 strategies to matcher pipeline: - ExactMatcher (protocol + frequency) - FrequencyMatcher (frequency-based) - BitPatternMatcher (data patterns) - TimingMatcher (timing-based) - RTL433DecoderStrategy (RTL_433 decoder) - PatternBasedStrategy (NEW! Pattern decoder for short captures) 3. Created MockDB class for simplified mode (no real database) 4. Replaced old get_matcher() with get_matcher_engine() 5. Updated upload handler to use MatchResult format 6. All matches now include confidence scores and match methods **Result:** - Pattern decoder is NOW ACTIVE in production 🎉 - Unified matching pipeline with 6 strategies - Cleaner codebase (-1,859 lines) - Single source of truth for matching logic 🎉 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1104 lines
30 KiB
Markdown
1104 lines
30 KiB
Markdown
# 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
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>GigLez Pattern Decoder</title>
|
|
</head>
|
|
<body>
|
|
<h1>Decode .sub File</h1>
|
|
<input type="file" id="fileInput" accept=".sub">
|
|
<div id="results"></div>
|
|
|
|
<script type="module">
|
|
import PatternDecoder from './src/matcher/js/index.js';
|
|
|
|
const decoder = new PatternDecoder();
|
|
|
|
document.getElementById('fileInput').addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
const text = await file.text();
|
|
|
|
// Parse .sub file (simple parser)
|
|
const metadata = parseSubFile(text);
|
|
|
|
// Decode
|
|
const matches = decoder.decode(metadata);
|
|
|
|
// Display results
|
|
const resultsDiv = document.getElementById('results');
|
|
resultsDiv.innerHTML = `
|
|
<h2>Found ${matches.length} matches:</h2>
|
|
<ul>
|
|
${matches.map(m => `
|
|
<li>
|
|
<strong>${m.name}</strong> (${m.manufacturer || 'Unknown'})
|
|
<br>Confidence: ${(m.confidence * 100).toFixed(1)}%
|
|
<br>Method: ${m.matchMethod}
|
|
</li>
|
|
`).join('')}
|
|
</ul>
|
|
`;
|
|
});
|
|
|
|
function parseSubFile(text) {
|
|
// Simple .sub parser (extract frequency and RAW_Data)
|
|
const frequencyMatch = text.match(/Frequency: (\d+)/);
|
|
const rawDataMatch = text.match(/RAW_Data: ([\d\s-]+)/);
|
|
|
|
if (!frequencyMatch || !rawDataMatch) {
|
|
throw new Error('Invalid .sub file format');
|
|
}
|
|
|
|
const frequency = parseInt(frequencyMatch[1]);
|
|
const raw_data = rawDataMatch[1].trim().split(/\s+/).map(Number);
|
|
|
|
return { frequency, raw_data };
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
#### 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 (
|
|
<div>
|
|
<input type="file" onChange={handleFileUpload} accept=".sub" />
|
|
{matches.length > 0 && (
|
|
<div>
|
|
<h2>Decoded Devices ({matches.length})</h2>
|
|
{matches.map((match, i) => (
|
|
<div key={i} className="match-card">
|
|
<h3>{match.name}</h3>
|
|
<p>Manufacturer: {match.manufacturer || 'Unknown'}</p>
|
|
<p>Category: {match.category}</p>
|
|
<p>Confidence: {(match.confidence * 100).toFixed(1)}%</p>
|
|
<p>Method: {match.matchMethod}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 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)
|