mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 09:08:12 -07:00
style: apply ruff-format to entire codebase
First-time run of ruff-format via pre-commit hook normalises quote style, trailing commas, and whitespace across 188 Python files. No logic changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+176
-157
@@ -28,12 +28,13 @@ from utils.morse import (
|
||||
# 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'
|
||||
sess["logged_in"] = True
|
||||
sess["username"] = "test"
|
||||
sess["role"] = "admin"
|
||||
|
||||
|
||||
def generate_tone(freq: float, duration: float, sample_rate: int = 8000, amplitude: float = 0.8) -> bytes:
|
||||
@@ -44,13 +45,13 @@ def generate_tone(freq: float, duration: float, sample_rate: int = 8000, amplitu
|
||||
t = i / sample_rate
|
||||
val = int(amplitude * 32767 * math.sin(2 * math.pi * freq * t))
|
||||
samples.append(max(-32768, min(32767, val)))
|
||||
return struct.pack(f'<{len(samples)}h', *samples)
|
||||
return struct.pack(f"<{len(samples)}h", *samples)
|
||||
|
||||
|
||||
def generate_silence(duration: float, sample_rate: int = 8000) -> bytes:
|
||||
"""Generate silence as 16-bit LE PCM bytes."""
|
||||
n_samples = int(sample_rate * duration)
|
||||
return b'\x00\x00' * n_samples
|
||||
return b"\x00\x00" * n_samples
|
||||
|
||||
|
||||
def generate_morse_audio(text: str, wpm: int = 15, tone_freq: float = 700.0, sample_rate: int = 8000) -> bytes:
|
||||
@@ -61,7 +62,7 @@ def generate_morse_audio(text: str, wpm: int = 15, tone_freq: float = 700.0, sam
|
||||
char_gap = 3 * dit_dur
|
||||
word_gap = 7 * dit_dur
|
||||
|
||||
audio = b''
|
||||
audio = b""
|
||||
words = text.upper().split()
|
||||
for wi, word in enumerate(words):
|
||||
for ci, char in enumerate(word):
|
||||
@@ -70,9 +71,9 @@ def generate_morse_audio(text: str, wpm: int = 15, tone_freq: float = 700.0, sam
|
||||
continue
|
||||
|
||||
for ei, element in enumerate(morse):
|
||||
if element == '.':
|
||||
if element == ".":
|
||||
audio += generate_tone(tone_freq, dit_dur, sample_rate)
|
||||
elif element == '-':
|
||||
elif element == "-":
|
||||
audio += generate_tone(tone_freq, dah_dur, sample_rate)
|
||||
|
||||
if ei < len(morse) - 1:
|
||||
@@ -90,7 +91,7 @@ def generate_morse_audio(text: str, wpm: int = 15, tone_freq: float = 700.0, sam
|
||||
|
||||
def write_wav(path, pcm_bytes: bytes, sample_rate: int = 8000) -> None:
|
||||
"""Write mono 16-bit PCM bytes to a WAV file."""
|
||||
with wave.open(str(path), 'wb') as wf:
|
||||
with wave.open(str(path), "wb") as wf:
|
||||
wf.setnchannels(1)
|
||||
wf.setsampwidth(2)
|
||||
wf.setframerate(sample_rate)
|
||||
@@ -100,21 +101,22 @@ def write_wav(path, pcm_bytes: bytes, sample_rate: int = 8000) -> None:
|
||||
def decode_text_from_events(events) -> str:
|
||||
out = []
|
||||
for ev in events:
|
||||
if ev.get('type') == 'morse_char':
|
||||
out.append(str(ev.get('char', '')))
|
||||
elif ev.get('type') == 'morse_space':
|
||||
out.append(' ')
|
||||
return ''.join(out)
|
||||
if ev.get("type") == "morse_char":
|
||||
out.append(str(ev.get("char", "")))
|
||||
elif ev.get("type") == "morse_space":
|
||||
out.append(" ")
|
||||
return "".join(out)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMorseTable:
|
||||
def test_morse_table_contains_letters_and_digits(self):
|
||||
chars = set(MORSE_TABLE.values())
|
||||
for ch in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789':
|
||||
for ch in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789":
|
||||
assert ch in chars
|
||||
|
||||
def test_round_trip_morse_lookup(self):
|
||||
@@ -152,6 +154,7 @@ class TestEnvelopeDetector:
|
||||
|
||||
def test_magnitude_with_numpy_array(self):
|
||||
import numpy as np
|
||||
|
||||
det = EnvelopeDetector(block_size=100)
|
||||
arr = np.ones(100, dtype=np.float64) * 0.6
|
||||
assert abs(det.magnitude(arr) - 0.6) < 0.01
|
||||
@@ -170,52 +173,46 @@ class TestEnvelopeMorseDecoder:
|
||||
|
||||
def ook_on(duration):
|
||||
n = int(sample_rate * duration)
|
||||
return struct.pack(f'<{n}h', *([int(0.7 * 32767)] * n))
|
||||
return struct.pack(f"<{n}h", *([int(0.7 * 32767)] * n))
|
||||
|
||||
def ook_off(duration):
|
||||
n = int(sample_rate * duration)
|
||||
return b'\x00\x00' * n
|
||||
return b"\x00\x00" * n
|
||||
|
||||
# Generate dit-dah (A = .-)
|
||||
audio = (
|
||||
ook_off(0.3)
|
||||
+ ook_on(dit_dur)
|
||||
+ ook_off(dit_dur)
|
||||
+ ook_on(3 * dit_dur)
|
||||
+ ook_off(0.5)
|
||||
)
|
||||
audio = ook_off(0.3) + ook_on(dit_dur) + ook_off(dit_dur) + ook_on(3 * dit_dur) + ook_off(0.5)
|
||||
|
||||
decoder = MorseDecoder(
|
||||
sample_rate=sample_rate,
|
||||
tone_freq=700.0,
|
||||
wpm=wpm,
|
||||
detect_mode='envelope',
|
||||
detect_mode="envelope",
|
||||
)
|
||||
events = decoder.process_block(audio)
|
||||
events.extend(decoder.flush())
|
||||
elements = [e['element'] for e in events if e.get('type') == 'morse_element']
|
||||
elements = [e["element"] for e in events if e.get("type") == "morse_element"]
|
||||
|
||||
assert '.' in elements
|
||||
assert '-' in elements
|
||||
assert "." in elements
|
||||
assert "-" in elements
|
||||
|
||||
def test_envelope_metrics_have_zero_snr(self):
|
||||
"""Envelope mode metrics should report zero SNR fields."""
|
||||
decoder = MorseDecoder(
|
||||
sample_rate=8000,
|
||||
detect_mode='envelope',
|
||||
detect_mode="envelope",
|
||||
)
|
||||
metrics = decoder.get_metrics()
|
||||
assert metrics['detect_mode'] == 'envelope'
|
||||
assert metrics['snr'] == 0.0
|
||||
assert metrics['noise_ref'] == 0.0
|
||||
assert metrics["detect_mode"] == "envelope"
|
||||
assert metrics["snr"] == 0.0
|
||||
assert metrics["noise_ref"] == 0.0
|
||||
|
||||
def test_goertzel_mode_unchanged(self):
|
||||
"""Default goertzel mode still works as before."""
|
||||
decoder = MorseDecoder(sample_rate=8000, wpm=15)
|
||||
assert decoder.detect_mode == 'goertzel'
|
||||
assert decoder.detect_mode == "goertzel"
|
||||
metrics = decoder.get_metrics()
|
||||
assert 'detect_mode' in metrics
|
||||
assert metrics['detect_mode'] == 'goertzel'
|
||||
assert "detect_mode" in metrics
|
||||
assert metrics["detect_mode"] == "goertzel"
|
||||
|
||||
|
||||
class TestDropoutTolerance:
|
||||
@@ -252,10 +249,10 @@ class TestDropoutTolerance:
|
||||
|
||||
events = decoder.process_block(audio)
|
||||
events.extend(decoder.flush())
|
||||
elements = [e['element'] for e in events if e.get('type') == 'morse_element']
|
||||
elements = [e["element"] for e in events if e.get("type") == "morse_element"]
|
||||
|
||||
# Should produce a single dah, not two dits.
|
||||
assert elements == ['-'], f'Expected single dah but got {elements}'
|
||||
assert elements == ["-"], f"Expected single dah but got {elements}"
|
||||
|
||||
|
||||
class TestTimingAndWpmEstimator:
|
||||
@@ -274,35 +271,36 @@ class TestTimingAndWpmEstimator:
|
||||
|
||||
events = decoder.process_block(audio)
|
||||
events.extend(decoder.flush())
|
||||
elements = [e['element'] for e in events if e.get('type') == 'morse_element']
|
||||
elements = [e["element"] for e in events if e.get("type") == "morse_element"]
|
||||
|
||||
assert '.' in elements
|
||||
assert '-' in elements
|
||||
assert "." in elements
|
||||
assert "-" in elements
|
||||
|
||||
def test_wpm_estimator_sanity(self):
|
||||
target_wpm = 18
|
||||
audio = generate_morse_audio('PARIS PARIS PARIS', wpm=target_wpm)
|
||||
decoder = MorseDecoder(sample_rate=8000, tone_freq=700.0, wpm=12, wpm_mode='auto')
|
||||
audio = generate_morse_audio("PARIS PARIS PARIS", wpm=target_wpm)
|
||||
decoder = MorseDecoder(sample_rate=8000, tone_freq=700.0, wpm=12, wpm_mode="auto")
|
||||
|
||||
events = decoder.process_block(audio)
|
||||
events.extend(decoder.flush())
|
||||
|
||||
metrics = decoder.get_metrics()
|
||||
assert metrics['wpm'] >= 10.0
|
||||
assert metrics['wpm'] <= 35.0
|
||||
assert metrics["wpm"] >= 10.0
|
||||
assert metrics["wpm"] <= 35.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Decoder thread tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMorseDecoderThread:
|
||||
def test_thread_emits_waiting_heartbeat_on_no_data(self):
|
||||
stop_event = threading.Event()
|
||||
output_queue = queue.Queue(maxsize=64)
|
||||
|
||||
read_fd, write_fd = os.pipe()
|
||||
read_file = os.fdopen(read_fd, 'rb', 0)
|
||||
read_file = os.fdopen(read_fd, "rb", 0)
|
||||
|
||||
worker = threading.Thread(
|
||||
target=morse_decoder_thread,
|
||||
@@ -318,7 +316,7 @@ class TestMorseDecoderThread:
|
||||
msg = output_queue.get(timeout=0.3)
|
||||
except queue.Empty:
|
||||
continue
|
||||
if msg.get('type') == 'scope' and msg.get('waiting'):
|
||||
if msg.get("type") == "scope" and msg.get("waiting"):
|
||||
got_waiting = True
|
||||
break
|
||||
|
||||
@@ -333,7 +331,7 @@ class TestMorseDecoderThread:
|
||||
def test_thread_produces_character_events(self):
|
||||
stop_event = threading.Event()
|
||||
output_queue = queue.Queue(maxsize=512)
|
||||
audio = generate_morse_audio('SOS', wpm=15)
|
||||
audio = generate_morse_audio("SOS", wpm=15)
|
||||
|
||||
worker = threading.Thread(
|
||||
target=morse_decoder_thread,
|
||||
@@ -347,7 +345,7 @@ class TestMorseDecoderThread:
|
||||
while not output_queue.empty():
|
||||
events.append(output_queue.get_nowait())
|
||||
|
||||
chars = [e for e in events if e.get('type') == 'morse_char']
|
||||
chars = [e for e in events if e.get("type") == "morse_char"]
|
||||
assert len(chars) >= 1
|
||||
|
||||
|
||||
@@ -355,6 +353,7 @@ class TestMorseDecoderThread:
|
||||
# Route lifecycle regression
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMorseLifecycleRoutes:
|
||||
def _reset_route_state(self):
|
||||
with app_module.morse_lock:
|
||||
@@ -372,9 +371,9 @@ class TestMorseLifecycleRoutes:
|
||||
morse_routes.morse_stop_event = None
|
||||
morse_routes.morse_control_queue = None
|
||||
morse_routes.morse_runtime_config = {}
|
||||
morse_routes.morse_last_error = ''
|
||||
morse_routes.morse_last_error = ""
|
||||
morse_routes.morse_state = morse_routes.MORSE_IDLE
|
||||
morse_routes.morse_state_message = 'Idle'
|
||||
morse_routes.morse_state_message = "Idle"
|
||||
|
||||
def test_start_stop_reaches_idle_and_releases_resources(self, client, monkeypatch):
|
||||
_login_session(client)
|
||||
@@ -382,26 +381,30 @@ class TestMorseLifecycleRoutes:
|
||||
|
||||
released_devices = []
|
||||
|
||||
monkeypatch.setattr(app_module, 'claim_sdr_device', lambda idx, mode, sdr_type='rtlsdr': None)
|
||||
monkeypatch.setattr(app_module, 'release_sdr_device', lambda idx, sdr_type='rtlsdr': released_devices.append(idx))
|
||||
monkeypatch.setattr(app_module, "claim_sdr_device", lambda idx, mode, sdr_type="rtlsdr": None)
|
||||
monkeypatch.setattr(
|
||||
app_module, "release_sdr_device", lambda idx, sdr_type="rtlsdr": released_devices.append(idx)
|
||||
)
|
||||
|
||||
class DummyDevice:
|
||||
sdr_type = morse_routes.SDRType.RTL_SDR
|
||||
|
||||
class DummyBuilder:
|
||||
def build_fm_demod_command(self, **kwargs):
|
||||
return ['rtl_fm', '-f', '14060000', '-']
|
||||
return ["rtl_fm", "-f", "14060000", "-"]
|
||||
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'create_default_device', staticmethod(lambda sdr_type, index: DummyDevice()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'get_builder', staticmethod(lambda sdr_type: DummyBuilder()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'detect_devices', staticmethod(lambda: []))
|
||||
monkeypatch.setattr(
|
||||
morse_routes.SDRFactory, "create_default_device", staticmethod(lambda sdr_type, index: DummyDevice())
|
||||
)
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, "get_builder", staticmethod(lambda sdr_type: DummyBuilder()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, "detect_devices", staticmethod(lambda: []))
|
||||
|
||||
pcm = generate_morse_audio('E', wpm=15, sample_rate=22050)
|
||||
pcm = generate_morse_audio("E", wpm=15, sample_rate=22050)
|
||||
|
||||
class FakeRtlProc:
|
||||
def __init__(self, payload: bytes):
|
||||
self.stdout = io.BytesIO(payload)
|
||||
self.stderr = io.BytesIO(b'')
|
||||
self.stderr = io.BytesIO(b"")
|
||||
self.returncode = None
|
||||
|
||||
def poll(self):
|
||||
@@ -420,40 +423,43 @@ class TestMorseLifecycleRoutes:
|
||||
def fake_popen(cmd, *args, **kwargs):
|
||||
return FakeRtlProc(pcm)
|
||||
|
||||
monkeypatch.setattr(morse_routes.subprocess, 'Popen', fake_popen)
|
||||
monkeypatch.setattr(morse_routes, 'register_process', lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes, 'unregister_process', lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(morse_routes, "register_process", lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes, "unregister_process", lambda _proc: None)
|
||||
monkeypatch.setattr(
|
||||
morse_routes,
|
||||
'safe_terminate',
|
||||
lambda proc, timeout=0.0: setattr(proc, 'returncode', 0),
|
||||
"safe_terminate",
|
||||
lambda proc, timeout=0.0: setattr(proc, "returncode", 0),
|
||||
)
|
||||
|
||||
start_resp = client.post('/morse/start', json={
|
||||
'frequency': '14.060',
|
||||
'gain': '20',
|
||||
'ppm': '0',
|
||||
'device': '0',
|
||||
'tone_freq': '700',
|
||||
'wpm': '15',
|
||||
})
|
||||
start_resp = client.post(
|
||||
"/morse/start",
|
||||
json={
|
||||
"frequency": "14.060",
|
||||
"gain": "20",
|
||||
"ppm": "0",
|
||||
"device": "0",
|
||||
"tone_freq": "700",
|
||||
"wpm": "15",
|
||||
},
|
||||
)
|
||||
assert start_resp.status_code == 200
|
||||
assert start_resp.get_json()['status'] == 'started'
|
||||
assert start_resp.get_json()["status"] == "started"
|
||||
|
||||
status_resp = client.get('/morse/status')
|
||||
status_resp = client.get("/morse/status")
|
||||
assert status_resp.status_code == 200
|
||||
assert status_resp.get_json()['state'] in {'running', 'starting', 'stopping', 'idle'}
|
||||
assert status_resp.get_json()["state"] in {"running", "starting", "stopping", "idle"}
|
||||
|
||||
stop_resp = client.post('/morse/stop')
|
||||
stop_resp = client.post("/morse/stop")
|
||||
assert stop_resp.status_code == 200
|
||||
stop_data = stop_resp.get_json()
|
||||
assert stop_data['status'] == 'stopped'
|
||||
assert stop_data['state'] == 'idle'
|
||||
assert stop_data['alive'] == []
|
||||
assert stop_data["status"] == "stopped"
|
||||
assert stop_data["state"] == "idle"
|
||||
assert stop_data["alive"] == []
|
||||
|
||||
final_status = client.get('/morse/status').get_json()
|
||||
assert final_status['running'] is False
|
||||
assert final_status['state'] == 'idle'
|
||||
final_status = client.get("/morse/status").get_json()
|
||||
assert final_status["running"] is False
|
||||
assert final_status["state"] == "idle"
|
||||
assert 0 in released_devices
|
||||
|
||||
def test_start_retries_after_early_process_exit(self, client, monkeypatch):
|
||||
@@ -462,31 +468,35 @@ class TestMorseLifecycleRoutes:
|
||||
|
||||
released_devices = []
|
||||
|
||||
monkeypatch.setattr(app_module, 'claim_sdr_device', lambda idx, mode, sdr_type='rtlsdr': None)
|
||||
monkeypatch.setattr(app_module, 'release_sdr_device', lambda idx, sdr_type='rtlsdr': released_devices.append(idx))
|
||||
monkeypatch.setattr(app_module, "claim_sdr_device", lambda idx, mode, sdr_type="rtlsdr": None)
|
||||
monkeypatch.setattr(
|
||||
app_module, "release_sdr_device", lambda idx, sdr_type="rtlsdr": released_devices.append(idx)
|
||||
)
|
||||
|
||||
class DummyDevice:
|
||||
sdr_type = morse_routes.SDRType.RTL_SDR
|
||||
|
||||
class DummyBuilder:
|
||||
def build_fm_demod_command(self, **kwargs):
|
||||
cmd = ['rtl_fm', '-f', '14.060M', '-M', 'usb', '-s', '22050']
|
||||
if kwargs.get('direct_sampling') is not None:
|
||||
cmd.extend(['--direct', str(kwargs['direct_sampling'])])
|
||||
cmd.append('-')
|
||||
cmd = ["rtl_fm", "-f", "14.060M", "-M", "usb", "-s", "22050"]
|
||||
if kwargs.get("direct_sampling") is not None:
|
||||
cmd.extend(["--direct", str(kwargs["direct_sampling"])])
|
||||
cmd.append("-")
|
||||
return cmd
|
||||
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'create_default_device', staticmethod(lambda sdr_type, index: DummyDevice()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'get_builder', staticmethod(lambda sdr_type: DummyBuilder()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'detect_devices', staticmethod(lambda: []))
|
||||
monkeypatch.setattr(
|
||||
morse_routes.SDRFactory, "create_default_device", staticmethod(lambda sdr_type, index: DummyDevice())
|
||||
)
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, "get_builder", staticmethod(lambda sdr_type: DummyBuilder()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, "detect_devices", staticmethod(lambda: []))
|
||||
|
||||
pcm = generate_morse_audio('E', wpm=15, sample_rate=22050)
|
||||
pcm = generate_morse_audio("E", wpm=15, sample_rate=22050)
|
||||
rtl_cmds = []
|
||||
|
||||
class FakeRtlProc:
|
||||
def __init__(self, stdout_bytes: bytes, returncode: int | None):
|
||||
self.stdout = io.BytesIO(stdout_bytes)
|
||||
self.stderr = io.BytesIO(b'')
|
||||
self.stderr = io.BytesIO(b"")
|
||||
self.returncode = returncode
|
||||
|
||||
def poll(self):
|
||||
@@ -505,39 +515,42 @@ class TestMorseLifecycleRoutes:
|
||||
def fake_popen(cmd, *args, **kwargs):
|
||||
rtl_cmds.append(cmd)
|
||||
if len(rtl_cmds) == 1:
|
||||
return FakeRtlProc(b'', 1)
|
||||
return FakeRtlProc(b"", 1)
|
||||
return FakeRtlProc(pcm, None)
|
||||
|
||||
monkeypatch.setattr(morse_routes.subprocess, 'Popen', fake_popen)
|
||||
monkeypatch.setattr(morse_routes, 'register_process', lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes, 'unregister_process', lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(morse_routes, "register_process", lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes, "unregister_process", lambda _proc: None)
|
||||
monkeypatch.setattr(
|
||||
morse_routes,
|
||||
'safe_terminate',
|
||||
lambda proc, timeout=0.0: setattr(proc, 'returncode', 0),
|
||||
"safe_terminate",
|
||||
lambda proc, timeout=0.0: setattr(proc, "returncode", 0),
|
||||
)
|
||||
|
||||
start_resp = client.post('/morse/start', json={
|
||||
'frequency': '14.060',
|
||||
'gain': '20',
|
||||
'ppm': '0',
|
||||
'device': '0',
|
||||
'tone_freq': '700',
|
||||
'wpm': '15',
|
||||
})
|
||||
start_resp = client.post(
|
||||
"/morse/start",
|
||||
json={
|
||||
"frequency": "14.060",
|
||||
"gain": "20",
|
||||
"ppm": "0",
|
||||
"device": "0",
|
||||
"tone_freq": "700",
|
||||
"wpm": "15",
|
||||
},
|
||||
)
|
||||
assert start_resp.status_code == 200
|
||||
assert start_resp.get_json()['status'] == 'started'
|
||||
assert start_resp.get_json()["status"] == "started"
|
||||
assert len(rtl_cmds) >= 2
|
||||
assert rtl_cmds[0][0] == 'rtl_fm'
|
||||
assert '--direct' in rtl_cmds[0]
|
||||
assert '2' in rtl_cmds[0]
|
||||
assert rtl_cmds[1][0] == 'rtl_fm'
|
||||
assert '--direct' in rtl_cmds[1]
|
||||
assert '1' in rtl_cmds[1]
|
||||
assert rtl_cmds[0][0] == "rtl_fm"
|
||||
assert "--direct" in rtl_cmds[0]
|
||||
assert "2" in rtl_cmds[0]
|
||||
assert rtl_cmds[1][0] == "rtl_fm"
|
||||
assert "--direct" in rtl_cmds[1]
|
||||
assert "1" in rtl_cmds[1]
|
||||
|
||||
stop_resp = client.post('/morse/stop')
|
||||
stop_resp = client.post("/morse/stop")
|
||||
assert stop_resp.status_code == 200
|
||||
assert stop_resp.get_json()['status'] == 'stopped'
|
||||
assert stop_resp.get_json()["status"] == "stopped"
|
||||
assert 0 in released_devices
|
||||
|
||||
def test_start_falls_back_to_next_device_when_selected_device_has_no_pcm(self, client, monkeypatch):
|
||||
@@ -546,8 +559,10 @@ class TestMorseLifecycleRoutes:
|
||||
|
||||
released_devices = []
|
||||
|
||||
monkeypatch.setattr(app_module, 'claim_sdr_device', lambda idx, mode, sdr_type='rtlsdr': None)
|
||||
monkeypatch.setattr(app_module, 'release_sdr_device', lambda idx, sdr_type='rtlsdr': released_devices.append(idx))
|
||||
monkeypatch.setattr(app_module, "claim_sdr_device", lambda idx, mode, sdr_type="rtlsdr": None)
|
||||
monkeypatch.setattr(
|
||||
app_module, "release_sdr_device", lambda idx, sdr_type="rtlsdr": released_devices.append(idx)
|
||||
)
|
||||
|
||||
class DummyDevice:
|
||||
def __init__(self, index: int):
|
||||
@@ -558,35 +573,35 @@ class TestMorseLifecycleRoutes:
|
||||
def __init__(self, index: int, serial: str):
|
||||
self.sdr_type = morse_routes.SDRType.RTL_SDR
|
||||
self.index = index
|
||||
self.name = f'RTL {index}'
|
||||
self.name = f"RTL {index}"
|
||||
self.serial = serial
|
||||
|
||||
class DummyBuilder:
|
||||
def build_fm_demod_command(self, **kwargs):
|
||||
cmd = ['rtl_fm', '-d', str(kwargs['device'].index), '-f', '14.060M', '-M', 'usb', '-s', '22050']
|
||||
if kwargs.get('direct_sampling') is not None:
|
||||
cmd.extend(['--direct', str(kwargs['direct_sampling'])])
|
||||
cmd.append('-')
|
||||
cmd = ["rtl_fm", "-d", str(kwargs["device"].index), "-f", "14.060M", "-M", "usb", "-s", "22050"]
|
||||
if kwargs.get("direct_sampling") is not None:
|
||||
cmd.extend(["--direct", str(kwargs["direct_sampling"])])
|
||||
cmd.append("-")
|
||||
return cmd
|
||||
|
||||
monkeypatch.setattr(
|
||||
morse_routes.SDRFactory,
|
||||
'create_default_device',
|
||||
"create_default_device",
|
||||
staticmethod(lambda sdr_type, index: DummyDevice(int(index))),
|
||||
)
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, 'get_builder', staticmethod(lambda sdr_type: DummyBuilder()))
|
||||
monkeypatch.setattr(morse_routes.SDRFactory, "get_builder", staticmethod(lambda sdr_type: DummyBuilder()))
|
||||
monkeypatch.setattr(
|
||||
morse_routes.SDRFactory,
|
||||
'detect_devices',
|
||||
staticmethod(lambda: [DummyDetected(0, 'AAA00000'), DummyDetected(1, 'BBB11111')]),
|
||||
"detect_devices",
|
||||
staticmethod(lambda: [DummyDetected(0, "AAA00000"), DummyDetected(1, "BBB11111")]),
|
||||
)
|
||||
|
||||
pcm = generate_morse_audio('E', wpm=15, sample_rate=22050)
|
||||
pcm = generate_morse_audio("E", wpm=15, sample_rate=22050)
|
||||
|
||||
class FakeRtlProc:
|
||||
def __init__(self, stdout_bytes: bytes, returncode: int | None):
|
||||
self.stdout = io.BytesIO(stdout_bytes)
|
||||
self.stderr = io.BytesIO(b'')
|
||||
self.stderr = io.BytesIO(b"")
|
||||
self.returncode = returncode
|
||||
|
||||
def poll(self):
|
||||
@@ -604,40 +619,43 @@ class TestMorseLifecycleRoutes:
|
||||
|
||||
def fake_popen(cmd, *args, **kwargs):
|
||||
try:
|
||||
dev = int(cmd[cmd.index('-d') + 1])
|
||||
dev = int(cmd[cmd.index("-d") + 1])
|
||||
except Exception:
|
||||
dev = 0
|
||||
if dev == 0:
|
||||
return FakeRtlProc(b'', 1)
|
||||
return FakeRtlProc(b"", 1)
|
||||
return FakeRtlProc(pcm, None)
|
||||
|
||||
monkeypatch.setattr(morse_routes.subprocess, 'Popen', fake_popen)
|
||||
monkeypatch.setattr(morse_routes, 'register_process', lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes, 'unregister_process', lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes.subprocess, "Popen", fake_popen)
|
||||
monkeypatch.setattr(morse_routes, "register_process", lambda _proc: None)
|
||||
monkeypatch.setattr(morse_routes, "unregister_process", lambda _proc: None)
|
||||
monkeypatch.setattr(
|
||||
morse_routes,
|
||||
'safe_terminate',
|
||||
lambda proc, timeout=0.0: setattr(proc, 'returncode', 0),
|
||||
"safe_terminate",
|
||||
lambda proc, timeout=0.0: setattr(proc, "returncode", 0),
|
||||
)
|
||||
|
||||
start_resp = client.post('/morse/start', json={
|
||||
'frequency': '14.060',
|
||||
'gain': '20',
|
||||
'ppm': '0',
|
||||
'device': '0',
|
||||
'tone_freq': '700',
|
||||
'wpm': '15',
|
||||
})
|
||||
start_resp = client.post(
|
||||
"/morse/start",
|
||||
json={
|
||||
"frequency": "14.060",
|
||||
"gain": "20",
|
||||
"ppm": "0",
|
||||
"device": "0",
|
||||
"tone_freq": "700",
|
||||
"wpm": "15",
|
||||
},
|
||||
)
|
||||
assert start_resp.status_code == 200
|
||||
start_data = start_resp.get_json()
|
||||
assert start_data['status'] == 'started'
|
||||
assert start_data['config']['active_device'] == 1
|
||||
assert start_data['config']['device_serial'] == 'BBB11111'
|
||||
assert start_data["status"] == "started"
|
||||
assert start_data["config"]["active_device"] == 1
|
||||
assert start_data["config"]["device_serial"] == "BBB11111"
|
||||
assert 0 in released_devices
|
||||
|
||||
stop_resp = client.post('/morse/stop')
|
||||
stop_resp = client.post("/morse/stop")
|
||||
assert stop_resp.status_code == 200
|
||||
assert stop_resp.get_json()['status'] == 'stopped'
|
||||
assert stop_resp.get_json()["status"] == "stopped"
|
||||
assert 1 in released_devices
|
||||
|
||||
|
||||
@@ -645,10 +663,11 @@ class TestMorseLifecycleRoutes:
|
||||
# Integration: synthetic CW -> WAV decode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMorseIntegration:
|
||||
def test_decode_morse_wav_contains_expected_phrase(self, tmp_path):
|
||||
wav_path = tmp_path / 'cq_test_123.wav'
|
||||
pcm = generate_morse_audio('CQ TEST 123', wpm=15, tone_freq=700.0)
|
||||
wav_path = tmp_path / "cq_test_123.wav"
|
||||
pcm = generate_morse_audio("CQ TEST 123", wpm=15, tone_freq=700.0)
|
||||
write_wav(wav_path, pcm, sample_rate=8000)
|
||||
|
||||
result = decode_morse_wav_file(
|
||||
@@ -658,14 +677,14 @@ class TestMorseIntegration:
|
||||
wpm=15,
|
||||
bandwidth_hz=200,
|
||||
auto_tone_track=True,
|
||||
threshold_mode='auto',
|
||||
wpm_mode='auto',
|
||||
threshold_mode="auto",
|
||||
wpm_mode="auto",
|
||||
min_signal_gate=0.0,
|
||||
)
|
||||
|
||||
decoded = ' '.join(str(result.get('text', '')).split())
|
||||
assert 'CQ TEST 123' in decoded
|
||||
decoded = " ".join(str(result.get("text", "")).split())
|
||||
assert "CQ TEST 123" in decoded
|
||||
|
||||
events = result.get('events', [])
|
||||
event_counts = Counter(e.get('type') for e in events)
|
||||
assert event_counts['morse_char'] >= len('CQTEST123')
|
||||
events = result.get("events", [])
|
||||
event_counts = Counter(e.get("type") for e in events)
|
||||
assert event_counts["morse_char"] >= len("CQTEST123")
|
||||
|
||||
Reference in New Issue
Block a user