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:
James Smith
2026-07-05 14:48:11 +01:00
parent 82e64104fe
commit 96172ca593
189 changed files with 19883 additions and 19552 deletions
+248 -188
View File
@@ -17,6 +17,7 @@ from flask import Flask
try:
from flask_sock import Sock
WEBSOCKET_AVAILABLE = True
except ImportError:
WEBSOCKET_AVAILABLE = False
@@ -33,21 +34,21 @@ from utils.waterfall_fft import (
quantize_to_uint8,
)
logger = get_logger('intercept.waterfall_ws')
logger = get_logger("intercept.waterfall_ws")
AUDIO_SAMPLE_RATE = 48000
_shared_state_lock = threading.Lock()
_shared_audio_queue: queue.Queue[bytes] = queue.Queue(maxsize=20)
_shared_state: dict[str, Any] = {
'running': False,
'device': None,
'center_mhz': 0.0,
'span_mhz': 0.0,
'sample_rate': 0,
'monitor_enabled': False,
'monitor_freq_mhz': 0.0,
'monitor_modulation': 'wfm',
'monitor_squelch': 0,
"running": False,
"device": None,
"center_mhz": 0.0,
"span_mhz": 0.0,
"sample_rate": 0,
"monitor_enabled": False,
"monitor_freq_mhz": 0.0,
"monitor_modulation": "wfm",
"monitor_squelch": 0,
}
# Generation counter to prevent stale WebSocket handlers from clobbering
# shared state set by a newer handler (e.g. old handler's finally block
@@ -96,16 +97,16 @@ def _set_shared_capture_state(
return _capture_generation
if running and generation is None:
_capture_generation += 1
_shared_state['running'] = bool(running)
_shared_state['device'] = device if running else None
_shared_state["running"] = bool(running)
_shared_state["device"] = device if running else None
if center_mhz is not None:
_shared_state['center_mhz'] = float(center_mhz)
_shared_state["center_mhz"] = float(center_mhz)
if span_mhz is not None:
_shared_state['span_mhz'] = float(span_mhz)
_shared_state["span_mhz"] = float(span_mhz)
if sample_rate is not None:
_shared_state['sample_rate'] = int(sample_rate)
_shared_state["sample_rate"] = int(sample_rate)
if not running:
_shared_state['monitor_enabled'] = False
_shared_state["monitor_enabled"] = False
gen = _capture_generation
if not running:
_clear_shared_audio_queue()
@@ -122,17 +123,17 @@ def _set_shared_monitor(
was_enabled = False
freq_changed = False
with _shared_state_lock:
was_enabled = bool(_shared_state.get('monitor_enabled'))
_shared_state['monitor_enabled'] = bool(enabled)
was_enabled = bool(_shared_state.get("monitor_enabled"))
_shared_state["monitor_enabled"] = bool(enabled)
if frequency_mhz is not None:
old_freq = float(_shared_state.get('monitor_freq_mhz', 0.0) or 0.0)
_shared_state['monitor_freq_mhz'] = float(frequency_mhz)
old_freq = float(_shared_state.get("monitor_freq_mhz", 0.0) or 0.0)
_shared_state["monitor_freq_mhz"] = float(frequency_mhz)
if abs(float(frequency_mhz) - old_freq) > 1e-6:
freq_changed = True
if modulation is not None:
_shared_state['monitor_modulation'] = str(modulation).lower().strip()
_shared_state["monitor_modulation"] = str(modulation).lower().strip()
if squelch is not None:
_shared_state['monitor_squelch'] = max(0, min(100, int(squelch)))
_shared_state["monitor_squelch"] = max(0, min(100, int(squelch)))
if (was_enabled and not enabled) or (enabled and freq_changed):
_clear_shared_audio_queue()
@@ -140,15 +141,15 @@ def _set_shared_monitor(
def get_shared_capture_status() -> dict[str, Any]:
with _shared_state_lock:
return {
'running': bool(_shared_state['running']),
'device': _shared_state['device'],
'center_mhz': float(_shared_state.get('center_mhz', 0.0) or 0.0),
'span_mhz': float(_shared_state.get('span_mhz', 0.0) or 0.0),
'sample_rate': int(_shared_state.get('sample_rate', 0) or 0),
'monitor_enabled': bool(_shared_state.get('monitor_enabled')),
'monitor_freq_mhz': float(_shared_state.get('monitor_freq_mhz', 0.0) or 0.0),
'monitor_modulation': str(_shared_state.get('monitor_modulation', 'wfm')),
'monitor_squelch': int(_shared_state.get('monitor_squelch', 0) or 0),
"running": bool(_shared_state["running"]),
"device": _shared_state["device"],
"center_mhz": float(_shared_state.get("center_mhz", 0.0) or 0.0),
"span_mhz": float(_shared_state.get("span_mhz", 0.0) or 0.0),
"sample_rate": int(_shared_state.get("sample_rate", 0) or 0),
"monitor_enabled": bool(_shared_state.get("monitor_enabled")),
"monitor_freq_mhz": float(_shared_state.get("monitor_freq_mhz", 0.0) or 0.0),
"monitor_modulation": str(_shared_state.get("monitor_modulation", "wfm")),
"monitor_squelch": int(_shared_state.get("monitor_squelch", 0) or 0),
}
@@ -160,16 +161,16 @@ def start_shared_monitor_from_capture(
squelch: int,
) -> tuple[bool, str]:
with _shared_state_lock:
if not _shared_state['running']:
return False, 'Waterfall IQ stream not active'
if _shared_state['device'] != device:
return False, 'Waterfall stream is using a different SDR device'
_shared_state['monitor_enabled'] = True
_shared_state['monitor_freq_mhz'] = float(frequency_mhz)
_shared_state['monitor_modulation'] = str(modulation).lower().strip()
_shared_state['monitor_squelch'] = max(0, min(100, int(squelch)))
if not _shared_state["running"]:
return False, "Waterfall IQ stream not active"
if _shared_state["device"] != device:
return False, "Waterfall stream is using a different SDR device"
_shared_state["monitor_enabled"] = True
_shared_state["monitor_freq_mhz"] = float(frequency_mhz)
_shared_state["monitor_modulation"] = str(modulation).lower().strip()
_shared_state["monitor_squelch"] = max(0, min(100, int(squelch)))
_clear_shared_audio_queue()
return True, 'started'
return True, "started"
def stop_shared_monitor_from_capture() -> None:
@@ -178,7 +179,7 @@ def stop_shared_monitor_from_capture() -> None:
def read_shared_monitor_audio_chunk(timeout: float = 1.0) -> bytes | None:
with _shared_state_lock:
if not _shared_state['running'] or not _shared_state['monitor_enabled']:
if not _shared_state["running"] or not _shared_state["monitor_enabled"]:
return None
try:
return _shared_audio_queue.get(timeout=max(0.0, float(timeout)))
@@ -188,13 +189,13 @@ def read_shared_monitor_audio_chunk(timeout: float = 1.0) -> bytes | None:
def _snapshot_monitor_config() -> dict[str, Any] | None:
with _shared_state_lock:
if not (_shared_state['running'] and _shared_state['monitor_enabled']):
if not (_shared_state["running"] and _shared_state["monitor_enabled"]):
return None
return {
'center_mhz': float(_shared_state['center_mhz']),
'monitor_freq_mhz': float(_shared_state['monitor_freq_mhz']),
'modulation': str(_shared_state['monitor_modulation']),
'squelch': int(_shared_state['monitor_squelch']),
"center_mhz": float(_shared_state["center_mhz"]),
"monitor_freq_mhz": float(_shared_state["monitor_freq_mhz"]),
"modulation": str(_shared_state["monitor_modulation"]),
"squelch": int(_shared_state["monitor_squelch"]),
}
@@ -232,8 +233,8 @@ def _demodulate_monitor_audio(
next_phase = float((float(rotator_phase) + phase_inc * samples.size) % (2.0 * np.pi))
shifted = samples * rotator
mod = str(modulation or 'wfm').lower().strip()
target_bb = 220000.0 if mod == 'wfm' else 48000.0
mod = str(modulation or "wfm").lower().strip()
target_bb = 220000.0 if mod == "wfm" else 48000.0
pre_decim = max(1, int(fs // target_bb))
if pre_decim > 1:
usable = (shifted.size // pre_decim) * pre_decim
@@ -244,14 +245,14 @@ def _demodulate_monitor_audio(
if shifted.size < 16:
return None, next_phase
if mod in ('wfm', 'fm'):
if mod in ("wfm", "fm"):
audio = np.angle(shifted[1:] * np.conj(shifted[:-1])).astype(np.float32)
elif mod == 'am':
elif mod == "am":
envelope = np.abs(shifted).astype(np.float32)
audio = envelope - float(np.mean(envelope))
elif mod == 'usb':
elif mod == "usb":
audio = np.real(shifted).astype(np.float32)
elif mod == 'lsb':
elif mod == "lsb":
audio = -np.real(shifted).astype(np.float32)
else:
audio = np.real(shifted).astype(np.float32)
@@ -261,11 +262,11 @@ def _demodulate_monitor_audio(
audio = audio - float(np.mean(audio))
if mod in ('fm', 'am', 'usb', 'lsb'):
if mod in ("fm", "am", "usb", "lsb"):
taps = int(max(1, min(31, fs1 / 12000.0)))
if taps > 1:
kernel = np.ones(taps, dtype=np.float32) / float(taps)
audio = np.convolve(audio, kernel, mode='same')
audio = np.convolve(audio, kernel, mode="same")
out_len = int(audio.size * AUDIO_SAMPLE_RATE / fs1)
if out_len < 32:
@@ -289,13 +290,13 @@ def _demodulate_monitor_audio(
def _parse_center_freq_mhz(payload: dict[str, Any]) -> float:
"""Parse center frequency from mixed legacy/new payload formats."""
if payload.get('center_freq_mhz') is not None:
return float(payload['center_freq_mhz'])
if payload.get("center_freq_mhz") is not None:
return float(payload["center_freq_mhz"])
if payload.get('center_freq_hz') is not None:
return float(payload['center_freq_hz']) / 1e6
if payload.get("center_freq_hz") is not None:
return float(payload["center_freq_hz"]) / 1e6
raw = float(payload.get('center_freq', 100.0))
raw = float(payload.get("center_freq", 100.0))
# Backward compatibility: some clients still send center_freq in Hz.
if raw > 100000:
return raw / 1e6
@@ -304,9 +305,9 @@ def _parse_center_freq_mhz(payload: dict[str, Any]) -> float:
def _parse_span_mhz(payload: dict[str, Any]) -> float:
"""Parse display span in MHz from mixed payload formats."""
if payload.get('span_hz') is not None:
return float(payload['span_hz']) / 1e6
return float(payload.get('span_mhz', 2.0))
if payload.get("span_hz") is not None:
return float(payload["span_hz"]) / 1e6
return float(payload.get("span_mhz", 2.0))
def _pick_sample_rate(span_hz: int, caps: SDRCapabilities, sdr_type: SDRType) -> int:
@@ -322,13 +323,13 @@ def _pick_sample_rate(span_hz: int, caps: SDRCapabilities, sdr_type: SDRType) ->
def _resolve_sdr_type(sdr_type_str: str) -> SDRType:
"""Convert client sdr_type string to SDRType enum."""
mapping = {
'rtlsdr': SDRType.RTL_SDR,
'rtl_sdr': SDRType.RTL_SDR,
'hackrf': SDRType.HACKRF,
'limesdr': SDRType.LIME_SDR,
'lime_sdr': SDRType.LIME_SDR,
'airspy': SDRType.AIRSPY,
'sdrplay': SDRType.SDRPLAY,
"rtlsdr": SDRType.RTL_SDR,
"rtl_sdr": SDRType.RTL_SDR,
"hackrf": SDRType.HACKRF,
"limesdr": SDRType.LIME_SDR,
"lime_sdr": SDRType.LIME_SDR,
"airspy": SDRType.AIRSPY,
"sdrplay": SDRType.SDRPLAY,
}
return mapping.get(sdr_type_str.lower(), SDRType.RTL_SDR)
@@ -340,8 +341,8 @@ def _build_dummy_device(device_index: int, sdr_type: SDRType) -> SDRDevice:
return SDRDevice(
sdr_type=sdr_type,
index=device_index,
name=f'{sdr_type.value}-{device_index}',
serial='N/A',
name=f"{sdr_type.value}-{device_index}",
serial="N/A",
driver=sdr_type.value,
capabilities=caps,
)
@@ -355,7 +356,7 @@ def init_waterfall_websocket(app: Flask):
sock = Sock(app)
@sock.route('/ws/waterfall')
@sock.route("/ws/waterfall")
def waterfall_stream(ws):
"""WebSocket endpoint for real-time waterfall streaming."""
logger.info("WebSocket waterfall client connected")
@@ -367,7 +368,7 @@ def init_waterfall_websocket(app: Flask):
reader_thread = None
stop_event = threading.Event()
claimed_device = None
claimed_sdr_type = 'rtlsdr'
claimed_sdr_type = "rtlsdr"
my_generation = None # tracks which capture generation this handler owns
capture_center_mhz = 0.0
capture_start_freq = 0.0
@@ -413,13 +414,13 @@ def init_waterfall_websocket(app: Flask):
except (json.JSONDecodeError, TypeError):
continue
cmd = data.get('cmd')
cmd = data.get("cmd")
if cmd == 'start':
if cmd == "start":
shared_before = get_shared_capture_status()
keep_monitor_enabled = bool(shared_before.get('monitor_enabled'))
keep_monitor_modulation = str(shared_before.get('monitor_modulation', 'wfm'))
keep_monitor_squelch = int(shared_before.get('monitor_squelch', 0) or 0)
keep_monitor_enabled = bool(shared_before.get("monitor_enabled"))
keep_monitor_modulation = str(shared_before.get("monitor_modulation", "wfm"))
keep_monitor_squelch = int(shared_before.get("monitor_squelch", 0) or 0)
# Stop any existing capture
was_restarting = iq_process is not None
stop_event.set()
@@ -432,7 +433,7 @@ def init_waterfall_websocket(app: Flask):
if claimed_device is not None:
app_module.release_sdr_device(claimed_device, claimed_sdr_type)
claimed_device = None
claimed_sdr_type = 'rtlsdr'
claimed_sdr_type = "rtlsdr"
_set_shared_capture_state(running=False, generation=my_generation)
my_generation = None
stop_event.clear()
@@ -451,36 +452,40 @@ def init_waterfall_websocket(app: Flask):
center_freq_mhz = _parse_center_freq_mhz(data)
requested_vfo_mhz = float(
data.get(
'vfo_freq_mhz',
data.get('frequency_mhz', center_freq_mhz),
"vfo_freq_mhz",
data.get("frequency_mhz", center_freq_mhz),
)
)
span_mhz = _parse_span_mhz(data)
gain_raw = data.get('gain')
if gain_raw is None or str(gain_raw).lower() == 'auto':
gain_raw = data.get("gain")
if gain_raw is None or str(gain_raw).lower() == "auto":
gain = None
else:
gain = float(gain_raw)
device_index = int(data.get('device', 0))
sdr_type_str = data.get('sdr_type', 'rtlsdr')
fft_size = int(data.get('fft_size', 1024))
fps = int(data.get('fps', 25))
avg_count = int(data.get('avg_count', 4))
ppm = data.get('ppm')
device_index = int(data.get("device", 0))
sdr_type_str = data.get("sdr_type", "rtlsdr")
fft_size = int(data.get("fft_size", 1024))
fps = int(data.get("fps", 25))
avg_count = int(data.get("avg_count", 4))
ppm = data.get("ppm")
if ppm is not None:
ppm = int(ppm)
bias_t = bool(data.get('bias_t', False))
db_min = data.get('db_min')
db_max = data.get('db_max')
bias_t = bool(data.get("bias_t", False))
db_min = data.get("db_min")
db_max = data.get("db_max")
if db_min is not None:
db_min = float(db_min)
if db_max is not None:
db_max = float(db_max)
except (TypeError, ValueError) as exc:
ws.send(json.dumps({
'status': 'error',
'message': f'Invalid waterfall configuration: {exc}',
}))
ws.send(
json.dumps(
{
"status": "error",
"message": f"Invalid waterfall configuration: {exc}",
}
)
)
continue
# Clamp and normalize runtime settings
@@ -488,10 +493,14 @@ def init_waterfall_websocket(app: Flask):
fps = max(2, min(60, fps))
avg_count = max(1, min(32, avg_count))
if center_freq_mhz <= 0 or span_mhz <= 0:
ws.send(json.dumps({
'status': 'error',
'message': 'center_freq_mhz and span_mhz must be > 0',
}))
ws.send(
json.dumps(
{
"status": "error",
"message": "center_freq_mhz and span_mhz must be > 0",
}
)
)
continue
# Resolve SDR type and choose a valid sample rate
@@ -514,17 +523,21 @@ def init_waterfall_websocket(app: Flask):
max_claim_attempts = 4 if was_restarting else 1
claim_err = None
for _claim_attempt in range(max_claim_attempts):
claim_err = app_module.claim_sdr_device(device_index, 'waterfall', sdr_type_str)
claim_err = app_module.claim_sdr_device(device_index, "waterfall", sdr_type_str)
if not claim_err:
break
if _claim_attempt < max_claim_attempts - 1:
time.sleep(0.4)
if claim_err:
ws.send(json.dumps({
'status': 'error',
'message': claim_err,
'error_type': 'DEVICE_BUSY',
}))
ws.send(
json.dumps(
{
"status": "error",
"message": claim_err,
"error_type": "DEVICE_BUSY",
}
)
)
continue
claimed_device = device_index
claimed_sdr_type = sdr_type_str
@@ -543,22 +556,30 @@ def init_waterfall_websocket(app: Flask):
except NotImplementedError as e:
app_module.release_sdr_device(device_index, sdr_type_str)
claimed_device = None
claimed_sdr_type = 'rtlsdr'
ws.send(json.dumps({
'status': 'error',
'message': str(e),
}))
claimed_sdr_type = "rtlsdr"
ws.send(
json.dumps(
{
"status": "error",
"message": str(e),
}
)
)
continue
# Pre-flight: check the capture binary exists
if not shutil.which(iq_cmd[0]):
app_module.release_sdr_device(device_index, sdr_type_str)
claimed_device = None
claimed_sdr_type = 'rtlsdr'
ws.send(json.dumps({
'status': 'error',
'message': f'Required tool "{iq_cmd[0]}" not found. Install SoapySDR tools (rx_sdr).',
}))
claimed_sdr_type = "rtlsdr"
ws.send(
json.dumps(
{
"status": "error",
"message": f'Required tool "{iq_cmd[0]}" not found. Install SoapySDR tools (rx_sdr).',
}
)
)
continue
# Spawn I/Q capture process (retry to handle USB release lag)
@@ -581,10 +602,10 @@ def init_waterfall_websocket(app: Flask):
# Brief check that process started
time.sleep(0.3)
if iq_process.poll() is not None:
stderr_out = ''
stderr_out = ""
if iq_process.stderr:
with suppress(Exception):
stderr_out = iq_process.stderr.read().decode('utf-8', errors='replace').strip()
stderr_out = iq_process.stderr.read().decode("utf-8", errors="replace").strip()
unregister_process(iq_process)
iq_process = None
if attempt < max_attempts - 1:
@@ -596,9 +617,7 @@ def init_waterfall_websocket(app: Flask):
time.sleep(0.5)
continue
detail = f": {stderr_out}" if stderr_out else ""
raise RuntimeError(
f"I/Q capture process exited immediately{detail}"
)
raise RuntimeError(f"I/Q capture process exited immediately{detail}")
break # Process started successfully
except Exception as e:
logger.error(f"Failed to start I/Q capture: {e}")
@@ -608,11 +627,15 @@ def init_waterfall_websocket(app: Flask):
iq_process = None
app_module.release_sdr_device(device_index, sdr_type_str)
claimed_device = None
claimed_sdr_type = 'rtlsdr'
ws.send(json.dumps({
'status': 'error',
'message': f'Failed to start I/Q capture: {e}',
}))
claimed_sdr_type = "rtlsdr"
ws.send(
json.dumps(
{
"status": "error",
"message": f"Failed to start I/Q capture: {e}",
}
)
)
continue
capture_center_mhz = center_freq_mhz
@@ -634,25 +657,37 @@ def init_waterfall_websocket(app: Flask):
)
# Send started confirmation
ws.send(json.dumps({
'status': 'started',
'center_mhz': center_freq_mhz,
'start_freq': start_freq,
'end_freq': end_freq,
'fft_size': fft_size,
'sample_rate': sample_rate,
'effective_span_mhz': effective_span_mhz,
'db_min': db_min,
'db_max': db_max,
'vfo_freq_mhz': target_vfo_mhz,
}))
ws.send(
json.dumps(
{
"status": "started",
"center_mhz": center_freq_mhz,
"start_freq": start_freq,
"end_freq": end_freq,
"fft_size": fft_size,
"sample_rate": sample_rate,
"effective_span_mhz": effective_span_mhz,
"db_min": db_min,
"db_max": db_max,
"vfo_freq_mhz": target_vfo_mhz,
}
)
)
# Start reader thread — puts frames on queue, never calls ws.send()
def fft_reader(
proc, _send_q, stop_evt,
_fft_size, _avg_count, _fps, _sample_rate,
_start_freq, _end_freq, _center_mhz,
_db_min=None, _db_max=None,
proc,
_send_q,
stop_evt,
_fft_size,
_avg_count,
_fps,
_sample_rate,
_start_freq,
_end_freq,
_center_mhz,
_db_min=None,
_db_max=None,
):
"""Read I/Q from subprocess, compute FFT, enqueue binary frames."""
required_fft_samples = _fft_size * _avg_count
@@ -670,7 +705,7 @@ def init_waterfall_websocket(app: Flask):
frame_start = time.monotonic()
# Read raw I/Q bytes
raw = b''
raw = b""
remaining = bytes_per_frame
while remaining > 0 and not stop_evt.is_set():
chunk = proc.stdout.read(min(remaining, 65536))
@@ -684,7 +719,9 @@ def init_waterfall_websocket(app: Flask):
# Process FFT pipeline
samples = cu8_to_complex(raw)
fft_samples = samples[-required_fft_samples:] if len(samples) > required_fft_samples else samples
fft_samples = (
samples[-required_fft_samples:] if len(samples) > required_fft_samples else samples
)
power_db = compute_power_spectrum(
fft_samples,
fft_size=_fft_size,
@@ -696,7 +733,9 @@ def init_waterfall_websocket(app: Flask):
db_max=_db_max,
)
frame = build_binary_frame(
_start_freq, _end_freq, quantized,
_start_freq,
_end_freq,
quantized,
)
# Drop frame if main loop cannot keep up.
@@ -705,13 +744,10 @@ def init_waterfall_websocket(app: Flask):
monitor_cfg = _snapshot_monitor_config()
if monitor_cfg:
center_mhz_cfg = float(monitor_cfg.get('center_mhz', _center_mhz))
monitor_mhz_cfg = float(monitor_cfg.get('monitor_freq_mhz', _center_mhz))
center_mhz_cfg = float(monitor_cfg.get("center_mhz", _center_mhz))
monitor_mhz_cfg = float(monitor_cfg.get("monitor_freq_mhz", _center_mhz))
offset_hz = (monitor_mhz_cfg - center_mhz_cfg) * 1e6
if (
last_monitor_offset_hz is None
or abs(offset_hz - last_monitor_offset_hz) > 1.0
):
if last_monitor_offset_hz is None or abs(offset_hz - last_monitor_offset_hz) > 1.0:
monitor_rotator_phase = 0.0
last_monitor_offset_hz = offset_hz
@@ -720,8 +756,8 @@ def init_waterfall_websocket(app: Flask):
sample_rate=_sample_rate,
center_mhz=center_mhz_cfg,
monitor_freq_mhz=monitor_mhz_cfg,
modulation=monitor_cfg.get('modulation', 'wfm'),
squelch=int(monitor_cfg.get('squelch', 0)),
modulation=monitor_cfg.get("modulation", "wfm"),
squelch=int(monitor_cfg.get("squelch", 0)),
rotator_phase=monitor_rotator_phase,
)
if audio_chunk:
@@ -742,65 +778,89 @@ def init_waterfall_websocket(app: Flask):
reader_thread = threading.Thread(
target=fft_reader,
args=(
iq_process, send_queue, stop_event,
fft_size, avg_count, fps, sample_rate,
start_freq, end_freq, center_freq_mhz,
db_min, db_max,
iq_process,
send_queue,
stop_event,
fft_size,
avg_count,
fps,
sample_rate,
start_freq,
end_freq,
center_freq_mhz,
db_min,
db_max,
),
daemon=True,
)
reader_thread.start()
elif cmd in ('tune', 'set_vfo'):
elif cmd in ("tune", "set_vfo"):
if not iq_process or claimed_device is None or iq_process.poll() is not None:
ws.send(json.dumps({
'status': 'error',
'message': 'Waterfall capture is not running',
}))
ws.send(
json.dumps(
{
"status": "error",
"message": "Waterfall capture is not running",
}
)
)
continue
try:
shared = get_shared_capture_status()
vfo_freq_mhz = float(
data.get(
'vfo_freq_mhz',
data.get('frequency_mhz', data.get('center_freq_mhz', capture_center_mhz)),
"vfo_freq_mhz",
data.get("frequency_mhz", data.get("center_freq_mhz", capture_center_mhz)),
)
)
squelch = int(data.get('squelch', shared.get('monitor_squelch', 0)))
modulation = str(data.get('modulation', shared.get('monitor_modulation', 'wfm')))
squelch = int(data.get("squelch", shared.get("monitor_squelch", 0)))
modulation = str(data.get("modulation", shared.get("monitor_modulation", "wfm")))
except (TypeError, ValueError) as exc:
ws.send(json.dumps({
'status': 'error',
'message': f'Invalid tune request: {exc}',
}))
ws.send(
json.dumps(
{
"status": "error",
"message": f"Invalid tune request: {exc}",
}
)
)
continue
if not (capture_start_freq <= vfo_freq_mhz <= capture_end_freq):
ws.send(json.dumps({
'status': 'retune_required',
'message': 'Frequency outside current capture span',
'capture_start_freq': capture_start_freq,
'capture_end_freq': capture_end_freq,
'vfo_freq_mhz': vfo_freq_mhz,
}))
ws.send(
json.dumps(
{
"status": "retune_required",
"message": "Frequency outside current capture span",
"capture_start_freq": capture_start_freq,
"capture_end_freq": capture_end_freq,
"vfo_freq_mhz": vfo_freq_mhz,
}
)
)
continue
monitor_enabled = bool(shared.get('monitor_enabled'))
monitor_enabled = bool(shared.get("monitor_enabled"))
_set_shared_monitor(
enabled=monitor_enabled,
frequency_mhz=vfo_freq_mhz,
modulation=modulation,
squelch=squelch,
)
ws.send(json.dumps({
'status': 'tuned',
'vfo_freq_mhz': vfo_freq_mhz,
'start_freq': capture_start_freq,
'end_freq': capture_end_freq,
'center_mhz': capture_center_mhz,
}))
ws.send(
json.dumps(
{
"status": "tuned",
"vfo_freq_mhz": vfo_freq_mhz,
"start_freq": capture_start_freq,
"end_freq": capture_end_freq,
"center_mhz": capture_center_mhz,
}
)
)
elif cmd == 'stop':
elif cmd == "stop":
stop_event.set()
if reader_thread and reader_thread.is_alive():
reader_thread.join(timeout=2)
@@ -812,11 +872,11 @@ def init_waterfall_websocket(app: Flask):
if claimed_device is not None:
app_module.release_sdr_device(claimed_device, claimed_sdr_type)
claimed_device = None
claimed_sdr_type = 'rtlsdr'
claimed_sdr_type = "rtlsdr"
_set_shared_capture_state(running=False, generation=my_generation)
my_generation = None
stop_event.clear()
ws.send(json.dumps({'status': 'stopped'}))
ws.send(json.dumps({"status": "stopped"}))
except Exception as e:
logger.info(f"WebSocket waterfall closed: {e}")