Skip to content

feat: add Generic OOK Signal Decoder module#178

Merged
smittix merged 16 commits intosmittix:mainfrom
thatsatechnique:main
Mar 6, 2026
Merged

feat: add Generic OOK Signal Decoder module#178
smittix merged 16 commits intosmittix:mainfrom
thatsatechnique:main

Conversation

@thatsatechnique
Copy link
Contributor

Summary

Adds a new Generic OOK Signal Decoder mode for decoding raw OOK (On-Off Keying) signals using rtl_433's flex decoder. This provides a configurable interface for capturing and analyzing unknown ISM-band protocols with live bit/hex/ASCII display.

Key features

  • Configurable modulation: PWM, PPM, and Manchester encoding via rtl_433 flex decoder
  • Adjustable pulse timing: Short/long pulse widths, gap/reset limits, tolerance, min bits — with quick presets (300/600, 300/900, 400/800, 500/1500, 500 MC)
  • Live frame display: Real-time SSE streaming of decoded frames with hex, bit string, ASCII interpretation, and RSSI
  • Bit order toggle: MSB/LSB display for protocol analysis
  • Frequency presets: Persistent localStorage-backed presets with add/remove/reset (ISM defaults: 433.920, 315.000, 868.000, 915.000)
  • Deduplication: Optional consecutive frame suppression for repeated transmissions
  • Frame export: CSV and JSON export of captured frames
  • Command visibility: Shows the active rtl_433 command with copy-to-clipboard
  • Cheat sheet: Help documentation covering modulation identification, pulse timing discovery, common ISM timings, and troubleshooting

Architecture

  • Follows established patterns: Flask blueprint, SSE fanout, subprocess management, IIFE JS module
  • kill_all() properly handles OOK process cleanup
  • stop_ook() closes pipes and joins parser thread to prevent hangs
  • Input validation on all timing parameters and frequency (matches backend 24–1766 MHz range)
  • XSS prevention via escapeHtml() on user-derived ASCII content
  • Frame cap (5000) prevents unbounded memory growth
  • Queue operations use put_nowait() under lock to prevent deadlocks
  • CSS extracted to ook.css using project CSS variables, lazy-loaded via INTERCEPT_MODE_STYLE_MAP

Files added

  • routes/ook.py — Flask blueprint with start/stop/status/stream endpoints
  • utils/ook.py — Frame decoder and parser thread (handles codes/code/data fields, 0x prefix, brace prefix, inversion fallback)
  • static/js/modes/ook.js — Frontend IIFE module
  • static/css/modes/ook.css — Scoped styles using CSS variables
  • templates/partials/modes/ook.html — Sidebar controls
  • tests/test_ook.py — 22 unit tests covering decoder, parser thread, and routes

Files modified

  • app.py — OOK globals (process, queue, lock) and kill_all() cleanup
  • routes/__init__.py — Blueprint registration
  • templates/index.html — Mode integration (12 touch points), CSS lazy-load entry, output panel layout
  • templates/partials/nav.html — Navigation entry
  • static/js/core/cheat-sheets.js — OOK cheat sheet content
  • .gitignore — Local utility script exclusion

Test plan

  • Start OOK decoder on 433.920 MHz with default PWM timing → frames stream in real-time
  • Switch modulation to PPM/Manchester → encoding hint updates, flex decoder spec changes
  • Adjust pulse timing via quick presets → values populate correctly
  • Enable deduplication → repeated frames are suppressed
  • Toggle MSB/LSB bit order → ASCII interpretation updates
  • Export frames as CSV and JSON → files download with correct content
  • Click "Copy" on active command → rtl_433 command copies to clipboard
  • Stop decoder → output panel persists with captured frames
  • Clear output → frames clear, panel hides
  • Add/remove/reset frequency presets → persists across page reloads
  • kill_all() with OOK running → process terminates cleanly
  • Run pytest tests/test_ook.py → all 22 tests pass
  • Switch modes → OOK cleanup runs via getModuleDestroyFn()

🤖 Generated with Claude Code

thatsatechnique and others added 12 commits March 3, 2026 16:13
…cation

fix: improve pager message display and mute visibility
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>
…r, TSCM link

- Timing presets: five quick-fill buttons (300/600, 300/900, 400/800, 500/1500, 500 MC)
  that populate all six pulse-timing fields at once — maps to CTF flag timing profiles
- RSSI per frame: add -M level to rtl_433 command; parse snr/rssi/level from JSON;
  display dB SNR inline with each frame; include rssi_db column in CSV export
- Auto bit-order suggest: "Suggest" button counts printable chars across all stored
  frames for MSB vs LSB, selects the winner, shows count — no decoder restart needed
- Pattern filter: live hex/ASCII filter input above the frame log; hides non-matching
  frames and highlights matches in green; respects current bit order
- TSCM integration: "Decode (OOK)" button in RF signal device details panel switches
  to OOK mode and pre-fills frequency — frontend-only, no backend changes needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… bar

- Fix double-scroll by switching ookOutputPanel to flex layout
- Keep decoded frames visible after stopping (persist for review)
- Wire global Clear/CSV/JSON status bar buttons to OOK functions
- Hide default output pane in OOK mode (uses own panel)
- Add command display showing the active rtl_433 command
- Add JSON export and auto-scroll support
- Fix 0x prefix stripping in OOK hex decoder
- Fix PWM encoding hint text

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers identifying modulation type (PWM/PPM/Manchester), finding
pulse timing via rtl_433 -A, common ISM frequencies and timings,
and troubleshooting tips for tolerance and bit order.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded frequency buttons with localStorage-backed presets.
Default presets are standard ISM frequencies (433.920, 315, 868, 915 MHz).
Users can add custom frequencies, right-click to remove, and reset to
defaults — matching the pager module pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix XSS: escape ASCII output in innerHTML via escapeHtml()
- Fix deadlock: use put_nowait() for queue ops under ook_lock
- Fix SSE leak: add ook to moduleDestroyMap so switching modes
  closes the EventSource
- Fix RSSI: explicit null check preserves valid zero values in
  JSON export
- Add frame cap: trim oldest frames at 5000 to prevent unbounded
  memory growth on busy bands
- Validate timing params: wrap int() casts in try/except, return
  400 instead of 500 on invalid input
- Fix PWM hint: correct to short=0/long=1 matching rtl_433
  OOK_PWM convention (UI, JS hints, and cheat sheet)
- Fix inversion docstring: clarify fallback only applies when
  primary hex parse fails, not for valid decoded frames

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add kill_all() handler for OOK process cleanup on global reset
- Fix stop_ook() to close pipes and join parser thread (prevents hangs)
- Add ook.css with CSS classes, replace inline styles in ook.html
- Register ook.css in lazy-load style map (INTERCEPT_MODE_STYLE_MAP)
- Fix frontend frequency min=24 to match backend validation
- Add 22 unit tests for decode_ook_frame, ook_parser_thread, and routes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Owner

@smittix smittix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: Generic OOK Signal Decoder

Well-structured PR that follows existing patterns closely. A few issues to address before merge:


Critical

1. Missing sdr_type in claim_sdr_device() / release_sdr_device()
routes/ook.py doesn't pass sdr_type_str to device claim/release calls, unlike every other module (e.g. sensor.py). Non-RTL-SDR devices (HackRF, LimeSDR) won't be tracked or released correctly. Also needs an ook_active_sdr_type module-level variable.

2. No range validation on timing parameters
short_pulse, long_pulse, reset_limit, gap_limit, tolerance, min_bits are cast to int() but have no bounds checks. HTML min/max attributes are trivially bypassed. Should add server-side range validation.


Major

3. kill_all() doesn't fully clean up OOK state
app.py terminates the process but doesn't release the SDR device, signal the parser thread's stop_event, close pipes, or reset ook_active_device. The device stays "claimed" in the registry after a global reset.

4. Monkey-patching subprocess.Popen
Custom attrs (_stop_parser, _parser_thread) attached directly to the Popen object. Fragile and makes kill_all() unable to do proper cleanup since it doesn't know about these attributes. Consider a small wrapper class or storing these as separate module-level variables.

5. XSS fallback in ook.js
appendFrameEntry falls back to raw innerHTML if escapeHtml is undefined. Crafted OOK payloads could produce <script> in ASCII interpretation. Should always use escapeHtml or switch to textContent.

6. Inverted bit fallback is dead code
In utils/ook.py, the inversion path runs bytes.fromhex() on the same string that already failed decode_ook_frame(), so it can never produce a result. Either fix the logic or remove the dead code.


Minor

  • Status event uses msg.status key instead of msg.text — inconsistent with other modules (e.g. sensor.py)
  • Parser thread errors logged at debug level — should be warning or error
  • Parser thread doesn't emit a status: stopped event when rtl_433 crashes unexpectedly — frontend stays in "Listening" state with no notification
  • No cache-busting query param on JS/CSS includes in index.html (other modules use ?v={{ version }})
  • Gain comparison uses != '0' (string) instead of != 0 (number) — validate_gain returns a float

Tests

22 tests covering decoder, parser thread, and routes — solid coverage. Gaps worth filling:

  • stop_ook with a running process (mocked)
  • start_ook success path
  • SSE stream endpoint
  • Inversion logic (currently dead code, so untestable as-is)

…nup, XSS

Critical:
- Pass sdr_type_str to claim/release_sdr_device (was missing 3rd arg)
- Add ook_active_sdr_type module-level var for proper device registry tracking
- Add server-side range validation on all timing params via validate_positive_int

Major:
- Extract cleanup_ook() function for full teardown (stop_event, pipes, process,
  SDR release) — called from both stop_ook() and kill_all()
- Replace Popen monkey-patching with module-level _ook_stop_event/_ook_parser_thread
- Fix XSS: define local _esc() fallback in ook.js, never use raw innerHTML
- Remove dead inversion code path in utils/ook.py (bytes.fromhex on same
  string that already failed decode — could never produce a result)

Minor:
- Status event key 'status' → 'text' for consistency with other modules
- Parser thread logging: debug → warning for missing code field and errors
- Parser thread emits status:stopped on exit (normal EOF or crash)
- Add cache-busting ?v={{ version }}&r=ook1 to ook.js script include
- Fix gain/ppm comparison: != '0' (string) → != 0 (number)

Tests: 22 → 33 (added start success, stop with process, SSE stream,
timing range validation, stopped-on-exit event)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 6, 2026 00:32
@thatsatechnique
Copy link
Contributor Author

Hey — really appreciate the thorough review! Addressed everything in 7b4ad20.

Oh man, sorry about the sdr_type and the dead inversion code — had some extra stuff in there from troubleshooting during dev that I didn't clean up properly. My bad. Thanks for catching it all.

Here's the full breakdown:


Critical

1. Missing sdr_type in claim_sdr_device() / release_sdr_device() — Fixed.

  • Added ook_active_sdr_type module-level variable
  • Moved sdr_type_str parsing above the claim_sdr_device() call so it's available
  • All claim/release paths now pass sdr_type_str correctly (matching sensor.py pattern)

2. No range validation on timing parameters — Fixed.

  • Replaced raw int() casts with validate_positive_int() from utils/validation.py
  • Bounds: short_pulse/long_pulse ≤ 100K, reset_limit/gap_limit ≤ 1M, tolerance ≤ 50K, min_bits 1–4096
  • Additional floor checks for min_bits >= 1 and pulse widths >= 1

Major

3. kill_all() full OOK cleanup — Fixed.

  • Extracted a cleanup_ook(emit_status=True) function in routes/ook.py that handles: stop_event signaling, pipe closing, process termination, parser thread join, SDR device release, and state reset
  • kill_all() now lazy-imports and calls cleanup_ook(emit_status=False) with a fallback to the old inline cleanup if the import fails

4. Monkey-patching subprocess.Popen — Fixed.

  • Replaced proc._stop_parser / proc._parser_thread with module-level _ook_stop_event and _ook_parser_thread variables
  • stop_ook() now delegates entirely to cleanup_ook(), same cleanup path as kill_all()

5. XSS fallback in ook.js — Fixed.

  • Defined a local _esc() at IIFE scope with a safe DOM-based fallback (textContentinnerHTML) if escapeHtml isn't loaded yet
  • Replaced the inline typeof escapeHtml ternary with _esc() — no more raw innerHTML path

6. Inverted bit fallback is dead code — Removed.

  • You were right — the inversion path ran bytes.fromhex() on the same raw string (without 0x stripping) that already failed decode_ook_frame(), so it could never succeed
  • Removed the entire block; inverted field is now hardcoded to False
  • Client-side bit order toggle still handles MSB/LSB display independently

Minor

  • Status event key: 'status''text' in both backend (routes/ook.py) and frontend (ook.js) — matches sensor.py pattern
  • Parser thread logging: debugwarning for missing code field and parser errors
  • Crash notification: Parser thread now emits {'type': 'status', 'text': 'stopped'} on exit (normal EOF or crash) so the frontend doesn't get stuck in "Listening"
  • Cache-busting: Added ?v={{ version }}&r=ook1 to ook.js script include in index.html
  • Gain/PPM comparison: != '0' (string) → != 0 (number) — validate_gain returns a float so the string comparison was always truthy

Tests

22 → 33 tests. New coverage:

  • start_ook success path (mocked Popen)
  • stop_ook with a running process (verifies full cleanup)
  • SSE stream endpoint (content-type and headers)
  • Timing range validation (out-of-range and negative rejected)
  • Parser status: stopped event on normal exit

All 33 pass. Let me know if anything else needs attention!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Generic OOK Signal Decoder” mode that runs rtl_433 flex decoding on the backend and streams decoded frames to a new frontend UI for live bit/hex/ASCII analysis.

Changes:

  • Introduces OOK frame decoding/parsing utilities and a new Flask blueprint with start/stop/status/SSE stream endpoints.
  • Integrates the OOK mode into the UI (navigation, mode panel, output panel, cheat sheet, and lazy-loaded CSS/JS).
  • Adds unit tests covering frame decoding, parser behavior, and route handlers.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
utils/ook.py Implements hex→bits decoding and a parser thread for rtl_433 JSON output.
routes/ook.py Adds OOK blueprint endpoints and subprocess/thread lifecycle management.
static/js/modes/ook.js Frontend module for controls, SSE, frame display, filtering, presets, and export.
static/css/modes/ook.css Styling for the OOK mode sidebar controls and command panel.
templates/partials/modes/ook.html Sidebar UI for frequency/modulation/timing controls and status/command display.
templates/index.html Adds OOK mode integration points (style map, mode catalog, panels, init/destroy hooks, exports).
templates/partials/nav.html Adds OOK mode entries to desktop and mobile navigation.
static/js/core/cheat-sheets.js Adds an OOK-specific cheat sheet entry.
routes/__init__.py Registers the new OOK blueprint.
app.py Adds global OOK process/queue/lock and hooks OOK cleanup into kill_all().
tests/test_ook.py Adds tests for decoder utilities, parser thread, and key OOK routes.
.gitignore Ignores local reset-sdr.* utility scripts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…torage

- Detect crashed rtl_433 process via poll() and clean up stale state
  instead of permanently blocking restarts with 409
- Replace innerHTML+onclick preset rendering with createElement/addEventListener
  to prevent XSS via crafted localStorage frequency values
- Normalize preset frequencies to toFixed(3) on save and render
- Add try/catch + shape validation to loadPresets() for corrupted localStorage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thatsatechnique
Copy link
Contributor Author

Copilot Review — Addressed

Reviewed all 4 Copilot suggestions. Fixed 3, rejected 1:

Fixed:

  • Stale process check (routes/ook.py) — start_ook() now checks poll() before returning 409. If rtl_433 crashed, stale state is cleaned up via cleanup_ook() and restart is allowed.
  • XSS in renderPresets() (ook.js) — Replaced innerHTML + inline onclick/oncontextmenu with createElement + textContent + addEventListener. Frequencies are normalized to toFixed(3) on save and render.
  • loadPresets() resilience (ook.js) — Wrapped JSON.parse in try/catch with array shape validation. Falls back to defaults on corrupted localStorage.

Rejected:

  • Frame trimming DOM mismatch — False positive. Frames arrive one at a time via SSE, so state.frames.length can only exceed MAX_FRAMES by 1. The single splice(0, 1) + single removeChild(firstChild) is correct.

Commit: 91989a0

@smittix
Copy link
Owner

smittix commented Mar 6, 2026

Follow-up Review

Nice work addressing all the feedback — the cleanup is solid. A few final observations:

Minor

1. appendFrameEntry still interpolates backend values into innerHTML
msg.timestamp, msg.bit_count, and msg.rssi are inserted raw into the HTML string (ook.js ~L245-256). These come from rtl_433 JSON via the parser thread, so exploitation risk is low, but building with createElement/textContent per span (like you did for renderPresets) would close the door entirely.

2. Confirm validate_positive_int exists in utils/validation.py
The route uses validate_positive_int() for timing params but I don't see a change to utils/validation.py in this diff. Just want to make sure it's already exported there and not missing from the PR.

Otherwise this looks good to merge. Clean architecture, good test coverage (33 tests), and follows existing patterns well.

thatsatechnique and others added 2 commits March 6, 2026 12:47
…rameEntry

Addresses final upstream review — all backend-derived values (timestamp,
bit_count, rssi, hex, ascii) now use DOM methods instead of innerHTML
interpolation, closing the last XSS surface. Bumps cache-buster to ook2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@thatsatechnique
Copy link
Contributor Author

Thanks for the follow-up! Rebased on current main and addressed both items:

1. appendFrameEntry innerHTML → DOM methods
Replaced all innerHTML interpolation with createElement/textContentmsg.timestamp, msg.bit_count, msg.rssi, interp.hex, and interp.ascii all go through textContent now. Same pattern as renderPresets. No raw HTML path remains.

2. validate_positive_int in utils/validation.py
Confirmed — it's already there at line 163, exported and used by the route. It was added in a prior commit to the codebase so it didn't show up in this PR's diff, but it's solid.

Cache-buster bumped to ook2. Commit: 7d9a220

@smittix
Copy link
Owner

smittix commented Mar 6, 2026

Great work on this — clean implementation, thorough test coverage, and really responsive to feedback throughout. Thanks for the contribution! 🎉

@smittix smittix merged commit 4ea64bd into smittix:main Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants