Files
giglez/static/js/decoder/pulse-analyzer.js
Trilltechnician 01b06fadc6 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>
2026-01-14 23:08:53 -08:00

238 lines
7.0 KiB
JavaScript

/**
* GigLez Pulse Width Analysis using K-means clustering
* JavaScript port of Python pattern_decoder.py pulse analysis
*
* @module pulse-analyzer
*/
/**
* Simple K-means clustering for 1D data
* Used to identify SHORT and LONG pulse widths
*
* @param {number[]} data - Array of pulse widths
* @param {number} k - Number of clusters (default: 2 for SHORT/LONG)
* @param {number} maxIterations - Maximum iterations (default: 100)
* @returns {number[]} Array of cluster centers
*/
export function kMeans(data, k = 2, maxIterations = 100) {
if (data.length < k) {
return data.map(v => v);
}
// Initialize centroids using min and max values
const sorted = [...data].sort((a, b) => a - b);
let centroids = k === 2
? [sorted[0], sorted[sorted.length - 1]]
: Array.from({ length: k }, (_, i) =>
sorted[Math.floor((i / k) * sorted.length)]
);
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 (mean of each cluster)
const newCentroids = clusters.map(cluster => {
if (cluster.length === 0) return centroids[0]; // Handle empty cluster
return cluster.reduce((sum, val) => sum + val, 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
* Uses K-means clustering to automatically detect the two pulse durations
*
* @param {number[]} pulses - Raw pulse data (positive = HIGH, negative = LOW)
* @returns {Object} { shortPulse, longPulse, shortGap, longGap }
*/
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
let shortPulse = 0, longPulse = 0;
if (highPulses.length >= 2) {
const [short, long] = kMeans(highPulses, 2);
shortPulse = short;
longPulse = long;
} else if (highPulses.length === 1) {
shortPulse = longPulse = highPulses[0];
}
let shortGap = 0, longGap = 0;
if (lowPulses.length >= 2) {
const [short, long] = kMeans(lowPulses, 2);
shortGap = short;
longGap = long;
} else if (lowPulses.length === 1) {
shortGap = longGap = lowPulses[0];
}
return { shortPulse, longPulse, shortGap, longGap };
}
/**
* Decode pulse train to binary string using PWM encoding
* SHORT pulse = 0, LONG pulse = 1
*
* @param {number[]} pulses - Raw pulse data
* @param {number} shortPulse - SHORT pulse width (microseconds)
* @param {number} longPulse - LONG pulse width (microseconds)
* @returns {string} Binary string (e.g., "101010110")
*/
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
const duration = Math.abs(pulse);
bits.push(duration < threshold ? '0' : '1');
}
}
return bits.join('');
}
/**
* Analyze pulse train and extract timing information
* Returns comprehensive timing analysis for debugging
*
* @param {number[]} pulses - Raw pulse data
* @returns {Object} Timing analysis including pulse widths, counts, patterns
*/
export function analyzePulseTrain(pulses) {
const highPulses = pulses.filter(p => p > 0).map(Math.abs);
const lowPulses = pulses.filter(p => p < 0).map(Math.abs);
const { shortPulse, longPulse, shortGap, longGap } = identifyPulseWidths(pulses);
const bitPattern = decodeToBits(pulses, shortPulse, longPulse);
return {
// Pulse width identification
shortPulse,
longPulse,
shortGap,
longGap,
// Bit pattern
bitPattern,
bitCount: bitPattern.length,
// Counts
totalPulses: pulses.length,
highPulseCount: highPulses.length,
lowPulseCount: lowPulses.length,
// High pulse statistics
highPulseStats: {
min: Math.min(...highPulses),
max: Math.max(...highPulses),
mean: highPulses.reduce((a, b) => a + b, 0) / highPulses.length,
median: highPulses.sort((a, b) => a - b)[Math.floor(highPulses.length / 2)]
},
// Low pulse (gap) statistics
lowPulseStats: {
min: Math.min(...lowPulses),
max: Math.max(...lowPulses),
mean: lowPulses.reduce((a, b) => a + b, 0) / lowPulses.length,
median: lowPulses.sort((a, b) => a - b)[Math.floor(lowPulses.length / 2)]
}
};
}
/**
* Validate pulse data quality
* Checks for common issues in RF captures
*
* @param {number[]} pulses - Raw pulse data
* @returns {Object} { valid: boolean, issues: string[] }
*/
export function validatePulseData(pulses) {
const issues = [];
if (!pulses || pulses.length === 0) {
return { valid: false, issues: ['No pulse data provided'] };
}
if (pulses.length < 20) {
issues.push(`Very short capture (${pulses.length} pulses) - may not be decodable`);
}
const allPositive = pulses.every(p => p > 0);
const allNegative = pulses.every(p => p < 0);
if (allPositive || allNegative) {
issues.push('Pulse data is all positive or all negative - should alternate');
}
const highPulses = pulses.filter(p => p > 0).map(Math.abs);
const lowPulses = pulses.filter(p => p < 0).map(Math.abs);
if (highPulses.length < 5) {
issues.push('Too few HIGH pulses for reliable analysis');
}
if (lowPulses.length < 5) {
issues.push('Too few LOW pulses (gaps) for reliable analysis');
}
// Check for outliers (pulses > 100ms are suspicious)
const outliers = pulses.filter(p => Math.abs(p) > 100000);
if (outliers.length > 0) {
issues.push(`${outliers.length} suspiciously long pulses (>100ms) detected`);
}
return {
valid: issues.length === 0,
issues
};
}
/**
* Calculate Signal-to-Noise Ratio (SNR) estimate
* Based on pulse width consistency
*
* @param {number[]} pulses - Raw pulse data
* @returns {number} SNR estimate (0-1, higher is better)
*/
export function estimateSNR(pulses) {
const { shortPulse, longPulse } = identifyPulseWidths(pulses);
const highPulses = pulses.filter(p => p > 0).map(Math.abs);
if (highPulses.length < 10) return 0;
// Count how many pulses are close to SHORT or LONG
const threshold = (shortPulse + longPulse) / 2;
const shortTolerance = shortPulse * 0.3;
const longTolerance = longPulse * 0.3;
const consistentPulses = highPulses.filter(p => {
const nearShort = Math.abs(p - shortPulse) <= shortTolerance;
const nearLong = Math.abs(p - longPulse) <= longTolerance;
return nearShort || nearLong;
});
return consistentPulses.length / highPulses.length;
}