mirror of
https://github.com/smittix/intercept.git
synced 2026-07-22 16:18:11 -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:
+248
-188
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user