01b06fadc6
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>
410 lines
9.9 KiB
JavaScript
410 lines
9.9 KiB
JavaScript
/**
|
|
* 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();
|