feat: add Generic OOK Signal Decoder module

New 'OOK Decoder' mode for capturing and decoding arbitrary OOK/ASK
signals using rtl_433's flex decoder with fully configurable pulse
timing. Covers PWM, PPM, and Manchester encoding schemes.

Backend (utils/ook.py, routes/ook.py):
- Configurable modulation: OOK_PWM, OOK_PPM, OOK_MC_ZEROBIT
- Full rtl_433 flex spec builder with user-supplied pulse timings
- Bit-inversion fallback for transmitters with swapped short/long mapping
- Optional frame deduplication for repeated transmissions
- SSE streaming via /ook/stream

Frontend (static/js/modes/ook.js, templates/partials/modes/ook.html):
- Live MSB/LSB bit-order toggle — re-renders all stored frames instantly
  without restarting the decoder
- Full-detail frame display: timestamp, bit count, hex, dotted ASCII
- Modulation selector buttons with encoding hint text
- Full timing grid: short, long, gap/reset, tolerance, min bits
- CSV export of captured frames
- Global SDR device panel injection (device, SDR type, rtl_tcp, bias-T)

Integration (app.py, routes/__init__.py, templates/):
- Globals: ook_process, ook_queue, ook_lock
- Registered blueprint, nav entries (desktop + mobile), welcome card
- ookOutputPanel in visuals area with bit-order toolbar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thatsatechnique
2026-03-04 11:51:38 -08:00
parent 4741124d94
commit 4c282bb055
8 changed files with 1019 additions and 2 deletions

View File

@@ -287,6 +287,10 @@
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="2" y1="12" x2="5" y2="12"/><line x1="7" y1="12" x2="13" y2="12"/><line x1="15" y1="12" x2="18" y2="12"/><line x1="20" y1="12" x2="22" y2="12"/></svg></span>
<span class="mode-name">Morse</span>
</button>
<button class="mode-card mode-card-sm" onclick="selectMode('ook')">
<span class="mode-icon icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12h3"/><path d="M19 12h3"/><rect x="5" y="8" width="4" height="8" rx="1"/><rect x="10" y="9" width="4" height="6" rx="1"/><rect x="15" y="7" width="4" height="10" rx="1"/></svg></span>
<span class="mode-name">OOK Decoder</span>
</button>
</div>
</div>
@@ -697,6 +701,8 @@
{% include 'partials/modes/morse.html' %}
{% include 'partials/modes/ook.html' %}
{% include 'partials/modes/space-weather.html' %}
{% include 'partials/modes/tscm.html' %}
@@ -3285,6 +3291,29 @@
</div>
</div>
<!-- OOK Decoder Output Panel -->
<div id="ookOutputPanel" style="display: none; margin-bottom: 12px;">
<div style="background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 6px; padding: 8px 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">
<span>Decoded Frames</span>
<div style="display: flex; gap: 6px; align-items: center;">
<span style="color: var(--text-dim); font-size: 10px;">Bit order:</span>
<button class="btn btn-sm btn-ghost" id="ookBitMSB"
onclick="OokMode.setBitOrder('msb')"
style="background: var(--accent); color: #000;">MSB</button>
<button class="btn btn-sm btn-ghost" id="ookBitLSB"
onclick="OokMode.setBitOrder('lsb')">LSB</button>
<button class="btn btn-sm btn-ghost" onclick="OokMode.clearOutput()">Clear</button>
<button class="btn btn-sm btn-ghost" onclick="OokMode.exportLog()">CSV</button>
</div>
</div>
<div id="ookOutput" style="max-height: 400px; overflow-y: auto; font-family: var(--font-mono); font-size: 10px; color: var(--text-dim);"></div>
</div>
<div style="margin-top: 4px; font-size: 10px; color: #555; text-align: right;">
<span id="ookStatusBarFrames">0 frames</span>
</div>
</div>
<div class="output-content signal-feed" id="output">
<div class="placeholder signal-empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
@@ -3356,6 +3385,7 @@
<script src="{{ url_for('static', filename='js/modes/bt_locate.js') }}?v={{ version }}&r=btlocate4"></script>
<script src="{{ url_for('static', filename='js/modes/wefax.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/morse.js') }}?v={{ version }}&r=morse_iq12"></script>
<script src="{{ url_for('static', filename='js/modes/ook.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/space-weather.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/system.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/meteor.js') }}"></script>
@@ -3515,6 +3545,7 @@
waterfall: { label: 'Waterfall', indicator: 'WATERFALL', outputTitle: 'Spectrum Waterfall', group: 'signals' },
morse: { label: 'Morse', indicator: 'MORSE', outputTitle: 'CW/Morse Decoder', group: 'signals' },
system: { label: 'System', indicator: 'SYSTEM', outputTitle: 'System Health Monitor', group: 'system' },
ook: { label: 'OOK Decoder', indicator: 'OOK', outputTitle: 'OOK Signal Decoder', group: 'signals' },
};
const validModes = new Set(Object.keys(modeCatalog));
window.interceptModeCatalog = Object.assign({}, modeCatalog);
@@ -4363,6 +4394,7 @@
document.getElementById('morseMode')?.classList.toggle('active', mode === 'morse');
document.getElementById('meteorMode')?.classList.toggle('active', mode === 'meteor');
document.getElementById('systemMode')?.classList.toggle('active', mode === 'system');
document.getElementById('ookMode')?.classList.toggle('active', mode === 'ook');
const pagerStats = document.getElementById('pagerStats');
@@ -4462,6 +4494,8 @@
if (morseOutputPanel && mode !== 'morse') morseOutputPanel.style.display = 'none';
const morseDiagLog = document.getElementById('morseDiagLog');
if (morseDiagLog && mode !== 'morse') morseDiagLog.style.display = 'none';
const ookOutputPanel = document.getElementById('ookOutputPanel');
if (ookOutputPanel && mode !== 'ook') ookOutputPanel.style.display = 'none';
// Update output panel title based on mode
const outputTitle = document.getElementById('outputTitle');
@@ -4503,16 +4537,17 @@
// Show RTL-SDR device section for modes that use it
const rtlDeviceSection = document.getElementById('rtlDeviceSection');
if (rtlDeviceSection) {
rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'morse' || mode === 'radiosonde' || mode === 'meteor') ? 'block' : 'none';
rtlDeviceSection.style.display = (mode === 'pager' || mode === 'sensor' || mode === 'rtlamr' || mode === 'aprs' || mode === 'sstv' || mode === 'weathersat' || mode === 'sstv_general' || mode === 'wefax' || mode === 'morse' || mode === 'radiosonde' || mode === 'meteor' || mode === 'ook') ? 'block' : 'none';
// Save original sidebar position of SDR device section (once)
if (!rtlDeviceSection._origParent) {
rtlDeviceSection._origParent = rtlDeviceSection.parentNode;
rtlDeviceSection._origNext = rtlDeviceSection.nextElementSibling;
}
// For morse/radiosonde/meteor modes, move SDR device section inside the panel after the title
// For morse/radiosonde/meteor/ook modes, move SDR device section inside the panel after the title
const morsePanel = document.getElementById('morseMode');
const radiosondePanel = document.getElementById('radiosondeMode');
const meteorPanel = document.getElementById('meteorMode');
const ookPanel = document.getElementById('ookMode');
if (mode === 'morse' && morsePanel) {
const firstSection = morsePanel.querySelector('.section');
if (firstSection) firstSection.after(rtlDeviceSection);
@@ -4522,6 +4557,9 @@
} else if (mode === 'meteor' && meteorPanel) {
const firstSection = meteorPanel.querySelector('.section');
if (firstSection) firstSection.after(rtlDeviceSection);
} else if (mode === 'ook' && ookPanel) {
const firstSection = ookPanel.querySelector('.section');
if (firstSection) firstSection.after(rtlDeviceSection);
} else if (rtlDeviceSection._origParent && rtlDeviceSection.parentNode !== rtlDeviceSection._origParent) {
// Restore to original sidebar position when leaving morse mode
if (rtlDeviceSection._origNext) {
@@ -4626,6 +4664,8 @@
MeteorScatter.init();
} else if (mode === 'system') {
SystemHealth.init();
} else if (mode === 'ook') {
OokMode.init();
}
// Waterfall destroy is now handled by moduleDestroyMap above.