Files
giglez/static/decoder-demo.html
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

581 lines
17 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GigLez Pattern Decoder - Browser Demo</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.header p {
font-size: 1.1em;
opacity: 0.9;
}
.content {
padding: 30px;
}
.upload-section {
background: #f7f9fc;
border: 2px dashed #667eea;
border-radius: 8px;
padding: 40px;
text-align: center;
margin-bottom: 30px;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-section:hover {
border-color: #764ba2;
background: #eef2f7;
}
.upload-section.dragover {
background: #e3e9f3;
border-color: #764ba2;
}
.upload-icon {
font-size: 3em;
margin-bottom: 15px;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 30px;
font-size: 1em;
border-radius: 6px;
cursor: pointer;
transition: transform 0.2s ease;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
.file-input {
display: none;
}
.results-section {
display: none;
}
.results-section.visible {
display: block;
}
.file-info {
background: #f7f9fc;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.file-info h3 {
color: #667eea;
margin-bottom: 15px;
}
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
background: white;
padding: 12px;
border-radius: 6px;
}
.info-item label {
font-size: 0.85em;
color: #666;
display: block;
margin-bottom: 5px;
}
.info-item value {
font-weight: 600;
color: #333;
font-size: 1.1em;
}
.matches-container {
margin-top: 20px;
}
.matches-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.matches-header h3 {
color: #667eea;
}
.badge {
background: #667eea;
color: white;
padding: 5px 12px;
border-radius: 20px;
font-size: 0.9em;
}
.match-card {
background: white;
border: 2px solid #e3e9f3;
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
transition: all 0.3s ease;
}
.match-card:hover {
border-color: #667eea;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
}
.match-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 15px;
}
.device-name {
font-size: 1.3em;
font-weight: 600;
color: #333;
}
.manufacturer {
color: #666;
font-size: 0.95em;
margin-top: 5px;
}
.confidence-badge {
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 0.9em;
}
.confidence-high {
background: #10b981;
color: white;
}
.confidence-medium {
background: #f59e0b;
color: white;
}
.confidence-low {
background: #6b7280;
color: white;
}
.match-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin-top: 15px;
padding-top: 15px;
border-top: 1px solid #e3e9f3;
}
.detail-item {
font-size: 0.9em;
}
.detail-label {
color: #666;
margin-bottom: 3px;
}
.detail-value {
color: #333;
font-weight: 600;
}
.no-matches {
text-align: center;
padding: 40px;
color: #666;
}
.no-matches-icon {
font-size: 3em;
margin-bottom: 15px;
opacity: 0.3;
}
.debug-section {
margin-top: 30px;
background: #f7f9fc;
border-radius: 8px;
padding: 20px;
}
.debug-section h3 {
color: #667eea;
margin-bottom: 15px;
}
.debug-output {
background: #1e293b;
color: #e2e8f0;
padding: 15px;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 0.85em;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
background: white;
padding: 15px;
border-radius: 6px;
text-align: center;
}
.stat-value {
font-size: 2em;
font-weight: 700;
color: #667eea;
}
.stat-label {
color: #666;
font-size: 0.9em;
margin-top: 5px;
}
.loading {
text-align: center;
padding: 40px;
color: #667eea;
}
.spinner {
border: 4px solid #f3f4f6;
border-top: 4px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🔬 GigLez Pattern Decoder</h1>
<p>Client-Side RF Signal Analysis for Sub-GHz Devices</p>
</div>
<div class="content">
<!-- Upload Section -->
<div class="upload-section" id="uploadSection">
<div class="upload-icon">📡</div>
<h2>Drop .sub file or click to upload</h2>
<p style="margin: 15px 0;">Supports Flipper Zero and LilyGo T-Embed RAW captures</p>
<button class="btn" onclick="document.getElementById('fileInput').click()">
Choose File
</button>
<input type="file" id="fileInput" class="file-input" accept=".sub,.txt">
</div>
<!-- Loading -->
<div class="loading" id="loadingSection" style="display: none;">
<div class="spinner"></div>
<p>Analyzing RF signal...</p>
</div>
<!-- Results Section -->
<div class="results-section" id="resultsSection">
<!-- File Info -->
<div class="file-info">
<h3>📄 File Information</h3>
<div class="info-grid" id="fileInfoGrid"></div>
</div>
<!-- Decoder Stats -->
<div class="stats-grid" id="statsGrid"></div>
<!-- Matches -->
<div class="matches-container">
<div class="matches-header">
<h3>🎯 Device Matches</h3>
<span class="badge" id="matchCount">0 matches</span>
</div>
<div id="matchesContainer"></div>
</div>
<!-- Debug Section -->
<div class="debug-section">
<h3>🐛 Debug Output</h3>
<div class="debug-output" id="debugOutput"></div>
</div>
</div>
</div>
</div>
<script type="module">
import { decodeFromFile, parseSubFile, getVersion } from './js/decoder/index.js';
let debugLog = [];
// Override console.log for debug output
const originalLog = console.log;
console.log = function(...args) {
debugLog.push(args.join(' '));
originalLog.apply(console, args);
};
// Print version info
const version = getVersion();
console.log(`${version.name} v${version.version}`);
console.log(`Loaded ${version.protocols} protocols`);
// File upload handling
const fileInput = document.getElementById('fileInput');
const uploadSection = document.getElementById('uploadSection');
const loadingSection = document.getElementById('loadingSection');
const resultsSection = document.getElementById('resultsSection');
fileInput.addEventListener('change', handleFileSelect);
// Drag and drop
uploadSection.addEventListener('dragover', (e) => {
e.preventDefault();
uploadSection.classList.add('dragover');
});
uploadSection.addEventListener('dragleave', () => {
uploadSection.classList.remove('dragover');
});
uploadSection.addEventListener('drop', (e) => {
e.preventDefault();
uploadSection.classList.remove('dragover');
const files = e.dataTransfer.files;
if (files.length > 0) {
handleFile(files[0]);
}
});
async function handleFileSelect(e) {
const file = e.target.files[0];
if (file) {
await handleFile(file);
}
}
async function handleFile(file) {
debugLog = [];
console.log(`\n=== Analyzing ${file.name} ===\n`);
// Show loading
uploadSection.style.display = 'none';
loadingSection.style.display = 'block';
resultsSection.classList.remove('visible');
try {
// Parse file
const content = await file.text();
const metadata = parseSubFile(content);
console.log('Parsed metadata:', metadata);
// Decode with debug enabled
const matches = await decodeFromFile(file, { debug: true, minConfidence: 0.3 });
console.log(`\nFound ${matches.length} device matches\n`);
// Display results
displayResults(file.name, metadata, matches);
} catch (error) {
console.error('Error:', error);
alert('Failed to decode file: ' + error.message);
uploadSection.style.display = 'block';
loadingSection.style.display = 'none';
}
}
function displayResults(filename, metadata, matches) {
// Hide loading, show results
loadingSection.style.display = 'none';
resultsSection.classList.add('visible');
// File info
const fileInfoGrid = document.getElementById('fileInfoGrid');
fileInfoGrid.innerHTML = `
<div class="info-item">
<label>Filename</label>
<value>${filename}</value>
</div>
<div class="info-item">
<label>Protocol</label>
<value>${metadata.protocol || 'Unknown'}</value>
</div>
<div class="info-item">
<label>Frequency</label>
<value>${metadata.frequency ? (metadata.frequency / 1e6).toFixed(2) + ' MHz' : 'Unknown'}</value>
</div>
<div class="info-item">
<label>Preset</label>
<value>${metadata.preset || 'Unknown'}</value>
</div>
<div class="info-item">
<label>Pulses</label>
<value>${metadata.raw_data?.length || 0}</value>
</div>
`;
// Stats
const statsGrid = document.getElementById('statsGrid');
const avgConfidence = matches.length > 0
? (matches.reduce((sum, m) => sum + m.confidence, 0) / matches.length * 100).toFixed(1)
: 0;
statsGrid.innerHTML = `
<div class="stat-card">
<div class="stat-value">${matches.length}</div>
<div class="stat-label">Matches Found</div>
</div>
<div class="stat-card">
<div class="stat-value">${avgConfidence}%</div>
<div class="stat-label">Avg Confidence</div>
</div>
<div class="stat-card">
<div class="stat-value">${matches.length > 0 ? (matches[0].confidence * 100).toFixed(1) + '%' : 'N/A'}</div>
<div class="stat-label">Best Match</div>
</div>
`;
// Match count badge
document.getElementById('matchCount').textContent = `${matches.length} match${matches.length !== 1 ? 'es' : ''}`;
// Matches
const matchesContainer = document.getElementById('matchesContainer');
if (matches.length === 0) {
matchesContainer.innerHTML = `
<div class="no-matches">
<div class="no-matches-icon">🔍</div>
<p>No device matches found</p>
<p style="margin-top: 10px; font-size: 0.9em;">Try adjusting the minimum confidence threshold or check the debug output below.</p>
</div>
`;
} else {
matchesContainer.innerHTML = matches.map((match, index) => {
const confidence = (match.confidence * 100).toFixed(1);
const confidenceClass = confidence >= 70 ? 'confidence-high' :
confidence >= 50 ? 'confidence-medium' : 'confidence-low';
const details = Object.entries(match.matchDetails || {})
.map(([key, value]) => `
<div class="detail-item">
<div class="detail-label">${key.replace(/_/g, ' ')}</div>
<div class="detail-value">${value}</div>
</div>
`).join('');
return `
<div class="match-card">
<div class="match-header">
<div>
<div class="device-name">${index + 1}. ${match.deviceName}</div>
<div class="manufacturer">${match.manufacturer}</div>
</div>
<div class="confidence-badge ${confidenceClass}">
${confidence}%
</div>
</div>
<div style="margin-bottom: 10px;">
<span style="background: #e3e9f3; padding: 4px 10px; border-radius: 12px; font-size: 0.85em; color: #667eea;">
${match.matchMethod}
</span>
</div>
${details ? `<div class="match-details">${details}</div>` : ''}
</div>
`;
}).join('');
}
// Debug output
document.getElementById('debugOutput').textContent = debugLog.join('\n');
}
// Allow reset
document.getElementById('resultsSection').addEventListener('click', (e) => {
if (e.target.classList.contains('btn')) {
uploadSection.style.display = 'block';
resultsSection.classList.remove('visible');
}
});
</script>
</body>
</html>