mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
fix(ook): harden for upstream review — tests, cleanup, CSS extraction
- 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>
This commit is contained in:
9
app.py
9
app.py
@@ -832,7 +832,7 @@ def health_check() -> Response:
|
|||||||
def kill_all() -> Response:
|
def kill_all() -> Response:
|
||||||
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
"""Kill all decoder, WiFi, and Bluetooth processes."""
|
||||||
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
global current_process, sensor_process, wifi_process, adsb_process, ais_process, acars_process
|
||||||
global vdl2_process, morse_process, radiosonde_process
|
global vdl2_process, morse_process, radiosonde_process, ook_process
|
||||||
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
global aprs_process, aprs_rtl_process, dsc_process, dsc_rtl_process, bt_process
|
||||||
|
|
||||||
# Import modules to reset their state
|
# Import modules to reset their state
|
||||||
@@ -896,6 +896,13 @@ def kill_all() -> Response:
|
|||||||
with morse_lock:
|
with morse_lock:
|
||||||
morse_process = None
|
morse_process = None
|
||||||
|
|
||||||
|
# Reset OOK state
|
||||||
|
with ook_lock:
|
||||||
|
if ook_process:
|
||||||
|
safe_terminate(ook_process)
|
||||||
|
unregister_process(ook_process)
|
||||||
|
ook_process = None
|
||||||
|
|
||||||
# Reset APRS state
|
# Reset APRS state
|
||||||
with aprs_lock:
|
with aprs_lock:
|
||||||
aprs_process = None
|
aprs_process = None
|
||||||
|
|||||||
@@ -232,20 +232,39 @@ def start_ook() -> Response:
|
|||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
def _close_pipe(pipe_obj) -> None:
|
||||||
|
"""Close a subprocess pipe, suppressing errors."""
|
||||||
|
if pipe_obj is not None:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
pipe_obj.close()
|
||||||
|
|
||||||
|
|
||||||
@ook_bp.route('/ook/stop', methods=['POST'])
|
@ook_bp.route('/ook/stop', methods=['POST'])
|
||||||
def stop_ook() -> Response:
|
def stop_ook() -> Response:
|
||||||
global ook_active_device
|
global ook_active_device
|
||||||
|
|
||||||
with app_module.ook_lock:
|
with app_module.ook_lock:
|
||||||
if app_module.ook_process:
|
if app_module.ook_process:
|
||||||
stop_event = getattr(app_module.ook_process, '_stop_parser', None)
|
proc = app_module.ook_process
|
||||||
|
stop_event = getattr(proc, '_stop_parser', None)
|
||||||
|
parser_thread = getattr(proc, '_parser_thread', None)
|
||||||
|
|
||||||
|
# Signal parser thread to stop
|
||||||
if stop_event:
|
if stop_event:
|
||||||
stop_event.set()
|
stop_event.set()
|
||||||
|
|
||||||
safe_terminate(app_module.ook_process)
|
# Close pipes so parser thread unblocks from readline()
|
||||||
unregister_process(app_module.ook_process)
|
_close_pipe(getattr(proc, 'stdout', None))
|
||||||
|
_close_pipe(getattr(proc, 'stderr', None))
|
||||||
|
|
||||||
|
safe_terminate(proc)
|
||||||
|
unregister_process(proc)
|
||||||
app_module.ook_process = None
|
app_module.ook_process = None
|
||||||
|
|
||||||
|
# Join parser thread with timeout
|
||||||
|
if parser_thread:
|
||||||
|
parser_thread.join(timeout=0.5)
|
||||||
|
|
||||||
if ook_active_device is not None:
|
if ook_active_device is not None:
|
||||||
app_module.release_sdr_device(ook_active_device)
|
app_module.release_sdr_device(ook_active_device)
|
||||||
ook_active_device = None
|
ook_active_device = None
|
||||||
|
|||||||
110
static/css/modes/ook.css
Normal file
110
static/css/modes/ook.css
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
/* OOK Signal Decoder Styles */
|
||||||
|
|
||||||
|
.ook-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-preset-add {
|
||||||
|
margin-top: 6px;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-preset-add input {
|
||||||
|
width: 80px;
|
||||||
|
background: var(--bg-tertiary, #111);
|
||||||
|
border: 1px solid var(--border-color, #222);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-preset-hint {
|
||||||
|
font-size: 9px;
|
||||||
|
color: var(--text-muted, #555);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-encoding-btns {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-encoding-btns .preset-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-timing-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-timing-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-status-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--text-dim);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-warning {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #ffaa00;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-command-display {
|
||||||
|
display: none;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-command-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted, #555);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-command-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-deep, #0a0a0a);
|
||||||
|
border: 1px solid var(--border-color, #1a2e1a);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-dim);
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-dedup-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ook-dedup-label input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
@@ -86,7 +86,8 @@
|
|||||||
morse: "{{ url_for('static', filename='css/modes/morse.css') }}",
|
morse: "{{ url_for('static', filename='css/modes/morse.css') }}",
|
||||||
radiosonde: "{{ url_for('static', filename='css/modes/radiosonde.css') }}",
|
radiosonde: "{{ url_for('static', filename='css/modes/radiosonde.css') }}",
|
||||||
meteor: "{{ url_for('static', filename='css/modes/meteor.css') }}",
|
meteor: "{{ url_for('static', filename='css/modes/meteor.css') }}",
|
||||||
system: "{{ url_for('static', filename='css/modes/system.css') }}"
|
system: "{{ url_for('static', filename='css/modes/system.css') }}",
|
||||||
|
ook: "{{ url_for('static', filename='css/modes/ook.css') }}"
|
||||||
};
|
};
|
||||||
window.INTERCEPT_MODE_STYLE_LOADED = {};
|
window.INTERCEPT_MODE_STYLE_LOADED = {};
|
||||||
window.INTERCEPT_MODE_STYLE_PROMISES = {};
|
window.INTERCEPT_MODE_STYLE_PROMISES = {};
|
||||||
|
|||||||
@@ -13,16 +13,15 @@
|
|||||||
<h3>Frequency</h3>
|
<h3>Frequency</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Frequency (MHz)</label>
|
<label>Frequency (MHz)</label>
|
||||||
<input type="number" id="ookFrequency" value="433.920" step="0.001" min="1" max="1766">
|
<input type="number" id="ookFrequency" value="433.920" step="0.001" min="24" max="1766">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Presets <span style="font-size:9px; color:#555;">(right-click to remove)</span></label>
|
<label>Presets <span class="ook-preset-hint">(right-click to remove)</span></label>
|
||||||
<div id="ookPresetButtons" style="display: flex; flex-wrap: wrap; gap: 4px;">
|
<div id="ookPresetButtons" class="ook-presets">
|
||||||
<!-- Populated by OokMode.renderPresets() -->
|
<!-- Populated by OokMode.renderPresets() -->
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top: 6px; display: flex; gap: 4px;">
|
<div class="ook-preset-add">
|
||||||
<input type="text" id="ookNewPresetFreq" placeholder="MHz"
|
<input type="text" id="ookNewPresetFreq" placeholder="MHz">
|
||||||
style="width: 80px; background: #111; border: 1px solid #222; border-radius: 3px; color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; padding: 3px 6px;">
|
|
||||||
<button class="preset-btn" onclick="OokMode.addPreset()" style="background: #2ecc71; color: #000;">Add</button>
|
<button class="preset-btn" onclick="OokMode.addPreset()" style="background: #2ecc71; color: #000;">Add</button>
|
||||||
<button class="preset-btn" onclick="OokMode.resetPresets()" style="font-size: 10px;">Reset</button>
|
<button class="preset-btn" onclick="OokMode.resetPresets()" style="font-size: 10px;">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,16 +43,14 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Modulation</h3>
|
<h3>Modulation</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div style="display: flex; gap: 4px;">
|
<div class="ook-encoding-btns">
|
||||||
<button class="preset-btn" id="ookEnc_pwm"
|
<button class="preset-btn" id="ookEnc_pwm"
|
||||||
onclick="OokMode.setEncoding('pwm')"
|
onclick="OokMode.setEncoding('pwm')"
|
||||||
style="flex: 1; background: var(--accent); color: #000;">PWM</button>
|
style="background: var(--accent); color: #000;">PWM</button>
|
||||||
<button class="preset-btn" id="ookEnc_ppm"
|
<button class="preset-btn" id="ookEnc_ppm"
|
||||||
onclick="OokMode.setEncoding('ppm')"
|
onclick="OokMode.setEncoding('ppm')">PPM</button>
|
||||||
style="flex: 1;">PPM</button>
|
|
||||||
<button class="preset-btn" id="ookEnc_manchester"
|
<button class="preset-btn" id="ookEnc_manchester"
|
||||||
onclick="OokMode.setEncoding('manchester')"
|
onclick="OokMode.setEncoding('manchester')">Manchester</button>
|
||||||
style="flex: 1;">Manchester</button>
|
|
||||||
</div>
|
</div>
|
||||||
<input type="hidden" id="ookEncoding" value="pwm">
|
<input type="hidden" id="ookEncoding" value="pwm">
|
||||||
<p id="ookEncodingHint" class="info-text" style="font-size: 10px; color: var(--text-dim); margin-top: 4px;">
|
<p id="ookEncodingHint" class="info-text" style="font-size: 10px; color: var(--text-dim); margin-top: 4px;">
|
||||||
@@ -67,7 +64,7 @@
|
|||||||
<p class="info-text" style="font-size: 10px; color: var(--text-dim); margin-bottom: 8px;">
|
<p class="info-text" style="font-size: 10px; color: var(--text-dim); margin-bottom: 8px;">
|
||||||
Pulse widths in microseconds for the flex decoder.
|
Pulse widths in microseconds for the flex decoder.
|
||||||
</p>
|
</p>
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
<div class="ook-timing-grid">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Short (μs)</label>
|
<label>Short (μs)</label>
|
||||||
<input type="number" id="ookShortPulse" value="300" step="10" min="50" max="5000">
|
<input type="number" id="ookShortPulse" value="300" step="10" min="50" max="5000">
|
||||||
@@ -95,7 +92,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group" style="margin-top: 8px;">
|
<div class="form-group" style="margin-top: 8px;">
|
||||||
<label style="font-size: 10px; color: var(--text-dim);">Quick presets (short/long μs)</label>
|
<label style="font-size: 10px; color: var(--text-dim);">Quick presets (short/long μs)</label>
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px;">
|
<div class="ook-timing-presets">
|
||||||
<button class="preset-btn" onclick="OokMode.setTiming(300,600,8000,5000,150,8)"
|
<button class="preset-btn" onclick="OokMode.setTiming(300,600,8000,5000,150,8)"
|
||||||
title="Generic ISM default">300/600</button>
|
title="Generic ISM default">300/600</button>
|
||||||
<button class="preset-btn" onclick="OokMode.setTiming(300,900,8000,5000,150,16)"
|
<button class="preset-btn" onclick="OokMode.setTiming(300,900,8000,5000,150,16)"
|
||||||
@@ -113,8 +110,8 @@
|
|||||||
<div class="section">
|
<div class="section">
|
||||||
<h3>Options</h3>
|
<h3>Options</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
<label class="ook-dedup-label">
|
||||||
<input type="checkbox" id="ookDeduplicate" style="width: auto; margin: 0;">
|
<input type="checkbox" id="ookDeduplicate">
|
||||||
<span>Deduplicate frames</span>
|
<span>Deduplicate frames</span>
|
||||||
</label>
|
</label>
|
||||||
<p class="info-text" style="font-size: 10px; color: var(--text-dim); margin-top: 4px;">
|
<p class="info-text" style="font-size: 10px; color: var(--text-dim); margin-top: 4px;">
|
||||||
@@ -126,25 +123,25 @@
|
|||||||
|
|
||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div style="display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text-dim);">
|
<div class="ook-status-row">
|
||||||
<span id="ookStatusIndicator" class="status-dot" style="width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim);"></span>
|
<span id="ookStatusIndicator" class="ook-status-dot"></span>
|
||||||
<span id="ookStatusText">Standby</span>
|
<span id="ookStatusText">Standby</span>
|
||||||
<span style="margin-left: auto;" id="ookFrameCount">0 frames</span>
|
<span style="margin-left: auto;" id="ookFrameCount">0 frames</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<p class="info-text" style="font-size: 11px; color: #ffaa00; line-height: 1.5;">
|
<p class="info-text ook-warning">
|
||||||
Uses rtl_433 with a custom flex decoder. Requires rtl_433 installed.
|
Uses rtl_433 with a custom flex decoder. Requires rtl_433 installed.
|
||||||
Works on any OOK/ASK signal in the SDR's frequency range.
|
Works on any OOK/ASK signal in the SDR's frequency range.
|
||||||
</p>
|
</p>
|
||||||
<div id="ookCommandDisplay" style="display: none; margin-top: 8px;">
|
<div id="ookCommandDisplay" class="ook-command-display">
|
||||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
|
||||||
<span style="font-size: 10px; color: #555; text-transform: uppercase; letter-spacing: 1px;">Active Command</span>
|
<span class="ook-command-label">Active Command</span>
|
||||||
<button class="btn btn-sm btn-ghost" onclick="OokMode.copyCommand()" title="Copy to clipboard"
|
<button class="btn btn-sm btn-ghost" onclick="OokMode.copyCommand()" title="Copy to clipboard"
|
||||||
style="font-size: 9px; padding: 1px 6px;">Copy</button>
|
style="font-size: 9px; padding: 1px 6px;">Copy</button>
|
||||||
</div>
|
</div>
|
||||||
<pre id="ookCommandText" style="margin: 0; padding: 6px 8px; background: #0a0a0a; border: 1px solid #1a2e1a; border-radius: 4px; font-family: var(--font-mono); font-size: 10px; color: var(--text-dim); white-space: pre-wrap; word-break: break-all; line-height: 1.5;"></pre>
|
<pre id="ookCommandText" class="ook-command-text"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
252
tests/test_ook.py
Normal file
252
tests/test_ook.py
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
"""Tests for OOK signal decoder utilities and route handlers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
|
import threading
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from utils.ook import decode_ook_frame, ook_parser_thread
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _login_session(client) -> None:
|
||||||
|
"""Mark the Flask test session as authenticated."""
|
||||||
|
with client.session_transaction() as sess:
|
||||||
|
sess['logged_in'] = True
|
||||||
|
sess['username'] = 'test'
|
||||||
|
sess['role'] = 'admin'
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# decode_ook_frame
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestDecodeOokFrame:
|
||||||
|
def test_valid_hex_returns_bits_and_hex(self):
|
||||||
|
result = decode_ook_frame('aa55')
|
||||||
|
assert result is not None
|
||||||
|
assert result['hex'] == 'aa55'
|
||||||
|
assert result['bits'] == '1010101001010101'
|
||||||
|
assert result['byte_count'] == 2
|
||||||
|
assert result['bit_count'] == 16
|
||||||
|
|
||||||
|
def test_strips_0x_prefix(self):
|
||||||
|
result = decode_ook_frame('0xaa55')
|
||||||
|
assert result is not None
|
||||||
|
assert result['hex'] == 'aa55'
|
||||||
|
|
||||||
|
def test_strips_0X_uppercase_prefix(self):
|
||||||
|
result = decode_ook_frame('0Xff')
|
||||||
|
assert result is not None
|
||||||
|
assert result['hex'] == 'ff'
|
||||||
|
assert result['bits'] == '11111111'
|
||||||
|
|
||||||
|
def test_strips_spaces(self):
|
||||||
|
result = decode_ook_frame('aa 55')
|
||||||
|
assert result is not None
|
||||||
|
assert result['hex'] == 'aa55'
|
||||||
|
|
||||||
|
def test_invalid_hex_returns_none(self):
|
||||||
|
assert decode_ook_frame('zzzz') is None
|
||||||
|
|
||||||
|
def test_empty_string_returns_none(self):
|
||||||
|
assert decode_ook_frame('') is None
|
||||||
|
|
||||||
|
def test_just_0x_prefix_returns_none(self):
|
||||||
|
assert decode_ook_frame('0x') is None
|
||||||
|
|
||||||
|
def test_single_byte(self):
|
||||||
|
result = decode_ook_frame('48')
|
||||||
|
assert result is not None
|
||||||
|
assert result['bits'] == '01001000'
|
||||||
|
assert result['byte_count'] == 1
|
||||||
|
|
||||||
|
def test_hello_ascii(self):
|
||||||
|
"""'Hello' in hex is 48656c6c6f."""
|
||||||
|
result = decode_ook_frame('48656c6c6f')
|
||||||
|
assert result is not None
|
||||||
|
assert result['hex'] == '48656c6c6f'
|
||||||
|
assert result['byte_count'] == 5
|
||||||
|
assert result['bit_count'] == 40
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ook_parser_thread
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestOokParserThread:
|
||||||
|
def _run_parser(self, json_lines, encoding='pwm', deduplicate=False):
|
||||||
|
"""Feed JSON lines to parser thread and collect output events."""
|
||||||
|
raw = '\n'.join(json.dumps(line) for line in json_lines) + '\n'
|
||||||
|
stdout = io.BytesIO(raw.encode('utf-8'))
|
||||||
|
output_queue = queue.Queue()
|
||||||
|
stop_event = threading.Event()
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
target=ook_parser_thread,
|
||||||
|
args=(stdout, output_queue, stop_event, encoding, deduplicate),
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
t.join(timeout=2)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
while not output_queue.empty():
|
||||||
|
events.append(output_queue.get_nowait())
|
||||||
|
return events
|
||||||
|
|
||||||
|
def test_parses_codes_field_list(self):
|
||||||
|
events = self._run_parser([{'codes': ['aa55']}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
assert frames[0]['hex'] == 'aa55'
|
||||||
|
assert frames[0]['bits'] == '1010101001010101'
|
||||||
|
assert frames[0]['inverted'] is False
|
||||||
|
|
||||||
|
def test_parses_codes_field_string(self):
|
||||||
|
events = self._run_parser([{'codes': 'ff00'}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
assert frames[0]['hex'] == 'ff00'
|
||||||
|
|
||||||
|
def test_parses_code_field(self):
|
||||||
|
events = self._run_parser([{'code': 'abcd'}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
assert frames[0]['hex'] == 'abcd'
|
||||||
|
|
||||||
|
def test_parses_data_field(self):
|
||||||
|
events = self._run_parser([{'data': '1234'}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
assert frames[0]['hex'] == '1234'
|
||||||
|
|
||||||
|
def test_strips_brace_bit_count_prefix(self):
|
||||||
|
"""rtl_433 sometimes prefixes with {N} bit count."""
|
||||||
|
events = self._run_parser([{'codes': ['{16}aa55']}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
assert frames[0]['hex'] == 'aa55'
|
||||||
|
|
||||||
|
def test_deduplication_suppresses_consecutive_identical(self):
|
||||||
|
events = self._run_parser(
|
||||||
|
[{'codes': ['aa55']}, {'codes': ['aa55']}, {'codes': ['aa55']}],
|
||||||
|
deduplicate=True,
|
||||||
|
)
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
|
||||||
|
def test_deduplication_allows_different_frames(self):
|
||||||
|
events = self._run_parser(
|
||||||
|
[{'codes': ['aa55']}, {'codes': ['ff00']}, {'codes': ['aa55']}],
|
||||||
|
deduplicate=True,
|
||||||
|
)
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 3
|
||||||
|
|
||||||
|
def test_no_code_field_emits_ook_raw(self):
|
||||||
|
events = self._run_parser([{'model': 'unknown', 'id': 42}])
|
||||||
|
raw_events = [e for e in events if e.get('type') == 'ook_raw']
|
||||||
|
assert len(raw_events) == 1
|
||||||
|
|
||||||
|
def test_rssi_extracted_from_snr(self):
|
||||||
|
events = self._run_parser([{'codes': ['aa55'], 'snr': 12.3}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
assert frames[0]['rssi'] == 12.3
|
||||||
|
|
||||||
|
def test_encoding_passed_through(self):
|
||||||
|
events = self._run_parser([{'codes': ['aa55']}], encoding='manchester')
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert frames[0]['encoding'] == 'manchester'
|
||||||
|
|
||||||
|
def test_timestamp_present(self):
|
||||||
|
events = self._run_parser([{'codes': ['aa55']}])
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert 'timestamp' in frames[0]
|
||||||
|
assert len(frames[0]['timestamp']) > 0
|
||||||
|
|
||||||
|
def test_invalid_json_skipped(self):
|
||||||
|
"""Non-JSON lines should be silently skipped."""
|
||||||
|
raw = b'not json\n{"codes": ["aa55"]}\n'
|
||||||
|
stdout = io.BytesIO(raw)
|
||||||
|
output_queue = queue.Queue()
|
||||||
|
stop_event = threading.Event()
|
||||||
|
|
||||||
|
t = threading.Thread(
|
||||||
|
target=ook_parser_thread,
|
||||||
|
args=(stdout, output_queue, stop_event),
|
||||||
|
)
|
||||||
|
t.start()
|
||||||
|
t.join(timeout=2)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
while not output_queue.empty():
|
||||||
|
events.append(output_queue.get_nowait())
|
||||||
|
frames = [e for e in events if e.get('type') == 'ook_frame']
|
||||||
|
assert len(frames) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Route handlers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class TestOokRoutes:
|
||||||
|
@pytest.fixture
|
||||||
|
def client(self):
|
||||||
|
import app as app_module
|
||||||
|
from routes import register_blueprints
|
||||||
|
|
||||||
|
app_module.app.config['TESTING'] = True
|
||||||
|
if 'ook' not in app_module.app.blueprints:
|
||||||
|
register_blueprints(app_module.app)
|
||||||
|
with app_module.app.test_client() as c:
|
||||||
|
yield c
|
||||||
|
|
||||||
|
def test_status_returns_not_running(self, client):
|
||||||
|
_login_session(client)
|
||||||
|
resp = client.get('/ook/status')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.get_json()
|
||||||
|
assert data['running'] is False
|
||||||
|
|
||||||
|
def test_stop_when_not_running(self, client):
|
||||||
|
_login_session(client)
|
||||||
|
resp = client.post('/ook/stop')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
data = resp.get_json()
|
||||||
|
assert data['status'] == 'not_running'
|
||||||
|
|
||||||
|
def test_start_validates_frequency(self, client):
|
||||||
|
_login_session(client)
|
||||||
|
resp = client.post('/ook/start',
|
||||||
|
json={'frequency': 'invalid'},
|
||||||
|
content_type='application/json')
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
def test_start_validates_encoding(self, client):
|
||||||
|
_login_session(client)
|
||||||
|
resp = client.post('/ook/start',
|
||||||
|
json={'encoding': 'invalid_enc'},
|
||||||
|
content_type='application/json')
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
def test_start_validates_timing_params(self, client):
|
||||||
|
_login_session(client)
|
||||||
|
resp = client.post('/ook/start',
|
||||||
|
json={'short_pulse': 'not_a_number'},
|
||||||
|
content_type='application/json')
|
||||||
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
def test_start_rejects_negative_frequency(self, client):
|
||||||
|
_login_session(client)
|
||||||
|
resp = client.post('/ook/start',
|
||||||
|
json={'frequency': '-5'},
|
||||||
|
content_type='application/json')
|
||||||
|
assert resp.status_code == 400
|
||||||
Reference in New Issue
Block a user