Phase 3: JavaScript pattern decoder implementation
Completed JavaScript port of pattern_decoder.py with full client-side
RF signal analysis capability for Sub-GHz devices.
## New Files Created (8 files, ~2,100 lines):
### Core Decoder Modules:
- static/js/decoder/protocol-database.js (410 lines)
* 18 RF protocol signatures (Weather, Garage, TPMS, Doorbells, Security, Remotes)
* ProtocolSignature class with timing/frequency matching
* ProtocolDatabase with indexed queries
* Categories: Weather Sensors (7), Garage Doors (3), TPMS (2), etc.
- static/js/decoder/pulse-analyzer.js (238 lines)
* K-means clustering for SHORT/LONG pulse detection
* identifyPulseWidths() using k-means
* decodeToBits() for PWM encoding (SHORT=0, LONG=1)
* analyzePulseTrain() for comprehensive timing analysis
* validatePulseData() quality checks
* estimateSNR() signal quality estimation
- static/js/decoder/fingerprint.js (312 lines)
* PulseFingerprint class for statistical characteristics
* extractFingerprint() - mean, std, duty cycle, pulse/gap ratio
* calculateSimilarity() - 0-1 similarity score with weights
* compareToProtocol() - protocol library matching
* classifySignal() - heuristic device type classification
* generateReport() - debugging output
- static/js/decoder/pattern-decoder.js (457 lines)
* DeviceMatch class (standardized result format)
* PatternDecoder main decoder class
* Multi-strategy decoding:
- Strategy 1: Timing pattern analysis (K-means + protocol DB)
- Strategy 2: Statistical fingerprint matching
- Strategy 3: Heuristic classification (fallback)
* Match deduplication and ranking
* Statistics tracking (success rate, avg confidence)
- static/js/decoder/index.js (226 lines)
* ES6 module exports for all decoder components
* parseSubFile() - Flipper Zero .sub parser
* quickDecode() - convenience API
* decodeFromURL() - fetch and decode remote files
* decodeFromFile() - browser File API support
* getVersion() - version and feature info
### Demo & Testing:
- static/decoder-demo.html (374 lines)
* Beautiful drag-and-drop UI for .sub file upload
* Real-time client-side decoding (no server needed!)
* Interactive results with confidence badges
* Signal quality visualization
* Debug console output
* Mobile-responsive design
- static/js/decoder/test.js (120 lines)
* Node.js test suite for decoder
* Tests all 4 strategies (timing, fingerprint, protocol DB, heuristics)
* Validates parsing, analysis, and matching
* Example output shows 7 matches @ 43.3% confidence
- package.json
* Enable ES6 modules ("type": "module")
* NPM script: "test:decoder"
## Test Results:
✅ JavaScript decoder successfully tested with Node.js
✅ Found 7 device matches on test .sub file (43.3% best confidence)
✅ All modules working: protocol DB (18 protocols), pulse analysis, fingerprinting, decoding
✅ Browser demo ready at /static/decoder-demo.html
## Features:
- 🔬 K-means pulse width clustering
- 📊 Statistical fingerprinting
- 📚 Protocol library matching (18 protocols)
- 🎯 Multi-strategy matching
- 📱 Client-side decoding (privacy-focused)
- 🌐 Browser and Node.js compatible
- ⚡ Single-transmission decoding (no repetitions needed)
## Next Steps:
- Integrate decoder into main upload UI
- Test with T-Embed and Flipper datasets
- Optimize for larger .sub files
- Add more protocol signatures
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* GigLez RF Protocol Database
|
||||
* JavaScript port of Python protocol_database.py
|
||||
*
|
||||
* Contains timing signatures for 18 known RF protocols
|
||||
* @module protocol-database
|
||||
*/
|
||||
|
||||
export class ProtocolSignature {
|
||||
constructor({
|
||||
name,
|
||||
category,
|
||||
manufacturer = null,
|
||||
model = null,
|
||||
frequency = 433920000,
|
||||
frequencyTolerance = 100000,
|
||||
shortPulseUs = 500,
|
||||
longPulseUs = 1000,
|
||||
timingTolerance = 0.2,
|
||||
preamblePattern = null,
|
||||
syncPattern = null,
|
||||
minBits = 24,
|
||||
maxBits = 64,
|
||||
typicalPulseCount = 100,
|
||||
minConfidence = 0.6
|
||||
}) {
|
||||
this.name = name;
|
||||
this.category = category;
|
||||
this.manufacturer = manufacturer;
|
||||
this.model = model;
|
||||
this.frequency = frequency;
|
||||
this.frequencyTolerance = frequencyTolerance;
|
||||
this.shortPulseUs = shortPulseUs;
|
||||
this.longPulseUs = longPulseUs;
|
||||
this.timingTolerance = timingTolerance;
|
||||
this.preamblePattern = preamblePattern;
|
||||
this.syncPattern = syncPattern;
|
||||
this.minBits = minBits;
|
||||
this.maxBits = maxBits;
|
||||
this.typicalPulseCount = typicalPulseCount;
|
||||
this.minConfidence = minConfidence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if observed timing matches this protocol
|
||||
* @param {number} shortUs - Observed short pulse width (microseconds)
|
||||
* @param {number} longUs - Observed long pulse width (microseconds)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if frequency matches this protocol
|
||||
* @param {number} freq - Observed frequency (Hz)
|
||||
* @returns {boolean}
|
||||
*/
|
||||
matchesFrequency(freq) {
|
||||
return Math.abs(freq - this.frequency) <= this.frequencyTolerance;
|
||||
}
|
||||
}
|
||||
|
||||
// Weather Sensor Protocols
|
||||
export const WEATHER_SENSORS = [
|
||||
new ProtocolSignature({
|
||||
name: 'Oregon Scientific v2.1',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'Oregon Scientific',
|
||||
shortPulseUs: 488,
|
||||
longPulseUs: 976,
|
||||
preamblePattern: '10101010101010101010101010101010', // 32-bit preamble
|
||||
syncPattern: '1000',
|
||||
minBits: 64,
|
||||
maxBits: 128,
|
||||
typicalPulseCount: 200
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Oregon Scientific v3.0',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'Oregon Scientific',
|
||||
shortPulseUs: 500,
|
||||
longPulseUs: 1000,
|
||||
preamblePattern: '101010101010101010101010101010101010101010101010', // 48-bit preamble
|
||||
syncPattern: '1000',
|
||||
minBits: 64,
|
||||
maxBits: 128,
|
||||
typicalPulseCount: 250
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Acurite Tower Sensor',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'Acurite',
|
||||
shortPulseUs: 220,
|
||||
longPulseUs: 440,
|
||||
syncPattern: '10',
|
||||
minBits: 56,
|
||||
maxBits: 64,
|
||||
typicalPulseCount: 130
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Acurite 5n1 Weather Station',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'Acurite',
|
||||
shortPulseUs: 220,
|
||||
longPulseUs: 440,
|
||||
minBits: 64,
|
||||
maxBits: 80,
|
||||
typicalPulseCount: 160
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'LaCrosse TX141TH-Bv2',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'LaCrosse',
|
||||
shortPulseUs: 500,
|
||||
longPulseUs: 1000,
|
||||
preamblePattern: '10101010', // 8-bit preamble
|
||||
minBits: 40,
|
||||
maxBits: 48,
|
||||
typicalPulseCount: 100
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Nexus Temperature/Humidity',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'Nexus',
|
||||
shortPulseUs: 500,
|
||||
longPulseUs: 1000,
|
||||
preamblePattern: '11111111', // 8-bit preamble
|
||||
minBits: 36,
|
||||
maxBits: 40,
|
||||
typicalPulseCount: 90
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Ambient Weather F007TH',
|
||||
category: 'Weather Sensor',
|
||||
manufacturer: 'Ambient Weather',
|
||||
shortPulseUs: 500,
|
||||
longPulseUs: 1000,
|
||||
minBits: 64,
|
||||
maxBits: 72,
|
||||
typicalPulseCount: 150
|
||||
})
|
||||
];
|
||||
|
||||
// Garage Door Opener Protocols
|
||||
export const GARAGE_DOOR_OPENERS = [
|
||||
new ProtocolSignature({
|
||||
name: 'Princeton',
|
||||
category: 'Garage Door Opener',
|
||||
manufacturer: null,
|
||||
shortPulseUs: 400,
|
||||
longPulseUs: 1200,
|
||||
preamblePattern: '1111', // 4-bit preamble
|
||||
syncPattern: '10',
|
||||
minBits: 24,
|
||||
maxBits: 32,
|
||||
typicalPulseCount: 60
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Chamberlain/LiftMaster',
|
||||
category: 'Garage Door Opener',
|
||||
manufacturer: 'Chamberlain',
|
||||
shortPulseUs: 300,
|
||||
longPulseUs: 900,
|
||||
minBits: 32,
|
||||
maxBits: 40,
|
||||
typicalPulseCount: 80,
|
||||
frequency: 315000000 // 315 MHz
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Linear MegaCode',
|
||||
category: 'Garage Door Opener',
|
||||
manufacturer: 'Linear',
|
||||
shortPulseUs: 250,
|
||||
longPulseUs: 500,
|
||||
minBits: 32,
|
||||
maxBits: 32,
|
||||
typicalPulseCount: 70,
|
||||
frequency: 318000000 // 318 MHz
|
||||
})
|
||||
];
|
||||
|
||||
// Doorbell Protocols
|
||||
export const DOORBELLS = [
|
||||
new ProtocolSignature({
|
||||
name: 'Honeywell Doorbell',
|
||||
category: 'Doorbell',
|
||||
manufacturer: 'Honeywell',
|
||||
shortPulseUs: 175,
|
||||
longPulseUs: 340,
|
||||
minBits: 48,
|
||||
maxBits: 48,
|
||||
typicalPulseCount: 100
|
||||
})
|
||||
];
|
||||
|
||||
// TPMS Protocols
|
||||
export const TIRE_PRESSURE = [
|
||||
new ProtocolSignature({
|
||||
name: 'Toyota TPMS',
|
||||
category: 'TPMS',
|
||||
manufacturer: 'Toyota',
|
||||
shortPulseUs: 50,
|
||||
longPulseUs: 100,
|
||||
minBits: 64,
|
||||
maxBits: 80,
|
||||
typicalPulseCount: 160,
|
||||
frequency: 315000000 // 315 MHz
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'Schrader TPMS',
|
||||
category: 'TPMS',
|
||||
manufacturer: 'Schrader',
|
||||
shortPulseUs: 50,
|
||||
longPulseUs: 100,
|
||||
minBits: 64,
|
||||
maxBits: 80,
|
||||
typicalPulseCount: 160,
|
||||
frequency: 433920000 // 433.92 MHz
|
||||
})
|
||||
];
|
||||
|
||||
// Security Sensor Protocols
|
||||
export const SECURITY_SENSORS = [
|
||||
new ProtocolSignature({
|
||||
name: 'Magellan',
|
||||
category: 'Security Sensor',
|
||||
manufacturer: 'Paradox',
|
||||
shortPulseUs: 250,
|
||||
longPulseUs: 500,
|
||||
minBits: 32,
|
||||
maxBits: 48,
|
||||
typicalPulseCount: 80,
|
||||
frequency: 433920000
|
||||
})
|
||||
];
|
||||
|
||||
// Remote Control Protocols
|
||||
export const REMOTE_CONTROLS = [
|
||||
new ProtocolSignature({
|
||||
name: 'PT2262',
|
||||
category: 'Remote Control',
|
||||
manufacturer: null,
|
||||
shortPulseUs: 350,
|
||||
longPulseUs: 1050,
|
||||
preamblePattern: '1111', // 4-bit preamble
|
||||
minBits: 24,
|
||||
maxBits: 24,
|
||||
typicalPulseCount: 50
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'PT2260',
|
||||
category: 'Remote Control',
|
||||
manufacturer: null,
|
||||
shortPulseUs: 300,
|
||||
longPulseUs: 900,
|
||||
minBits: 24,
|
||||
maxBits: 24,
|
||||
typicalPulseCount: 50
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'EV1527',
|
||||
category: 'Remote Control',
|
||||
manufacturer: null,
|
||||
shortPulseUs: 300,
|
||||
longPulseUs: 900,
|
||||
minBits: 24,
|
||||
maxBits: 24,
|
||||
typicalPulseCount: 50
|
||||
}),
|
||||
new ProtocolSignature({
|
||||
name: 'HCS301',
|
||||
category: 'Remote Control',
|
||||
manufacturer: 'Microchip',
|
||||
shortPulseUs: 400,
|
||||
longPulseUs: 800,
|
||||
minBits: 66,
|
||||
maxBits: 66,
|
||||
typicalPulseCount: 140
|
||||
})
|
||||
];
|
||||
|
||||
// Combined protocol list
|
||||
export const ALL_PROTOCOLS = [
|
||||
...WEATHER_SENSORS,
|
||||
...GARAGE_DOOR_OPENERS,
|
||||
...DOORBELLS,
|
||||
...TIRE_PRESSURE,
|
||||
...SECURITY_SENSORS,
|
||||
...REMOTE_CONTROLS
|
||||
];
|
||||
|
||||
/**
|
||||
* Protocol Database class for querying and indexing protocols
|
||||
*/
|
||||
export class ProtocolDatabase {
|
||||
constructor() {
|
||||
this.protocols = ALL_PROTOCOLS;
|
||||
this._byCategory = {};
|
||||
this._byFrequency = {};
|
||||
this._indexProtocols();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build indexes for fast lookup
|
||||
* @private
|
||||
*/
|
||||
_indexProtocols() {
|
||||
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 (rounded to MHz)
|
||||
const freqMHz = Math.round(proto.frequency / 1_000_000);
|
||||
if (!this._byFrequency[freqMHz]) {
|
||||
this._byFrequency[freqMHz] = [];
|
||||
}
|
||||
this._byFrequency[freqMHz].push(proto);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find protocols matching timing characteristics
|
||||
* @param {number} shortUs - Short pulse width (microseconds)
|
||||
* @param {number} longUs - Long pulse width (microseconds)
|
||||
* @param {number} [frequency] - Optional frequency filter (Hz)
|
||||
* @returns {ProtocolSignature[]}
|
||||
*/
|
||||
findByTiming(shortUs, longUs, frequency = null) {
|
||||
let candidates = this.protocols;
|
||||
|
||||
if (frequency) {
|
||||
const freqMHz = Math.round(frequency / 1_000_000);
|
||||
candidates = this._byFrequency[freqMHz] || this.protocols;
|
||||
}
|
||||
|
||||
const matches = [];
|
||||
for (const proto of candidates) {
|
||||
if (proto.matchesTiming(shortUs, longUs)) {
|
||||
if (!frequency || proto.matchesFrequency(frequency)) {
|
||||
matches.push(proto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find protocols by category
|
||||
* @param {string} category - Category name
|
||||
* @returns {ProtocolSignature[]}
|
||||
*/
|
||||
findByCategory(category) {
|
||||
return this._byCategory[category] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find protocols by frequency
|
||||
* @param {number} frequency - Frequency in Hz
|
||||
* @returns {ProtocolSignature[]}
|
||||
*/
|
||||
findByFrequency(frequency) {
|
||||
const matches = [];
|
||||
for (const proto of this.protocols) {
|
||||
if (proto.matchesFrequency(frequency)) {
|
||||
matches.push(proto);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all protocols
|
||||
* @returns {ProtocolSignature[]}
|
||||
*/
|
||||
getAll() {
|
||||
return this.protocols;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database statistics
|
||||
* @returns {Object}
|
||||
*/
|
||||
getStatistics() {
|
||||
return {
|
||||
totalProtocols: this.protocols.length,
|
||||
categories: Object.keys(this._byCategory),
|
||||
byCategory: Object.fromEntries(
|
||||
Object.entries(this._byCategory).map(([cat, protos]) => [cat, protos.length])
|
||||
),
|
||||
frequencyBands: [...new Set(
|
||||
this.protocols.map(p => Math.round(p.frequency / 1_000_000))
|
||||
)].sort((a, b) => a - b)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const protocolDb = new ProtocolDatabase();
|
||||
Reference in New Issue
Block a user