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
+136 -208
View File
@@ -47,9 +47,9 @@ from utils.validation import (
validate_rtl_tcp_port,
)
logger = logging.getLogger('intercept.dsc')
logger = logging.getLogger("intercept.dsc")
dsc_bp = Blueprint('dsc', __name__, url_prefix='/dsc')
dsc_bp = Blueprint("dsc", __name__, url_prefix="/dsc")
# Module state (track if running independent of process state)
dsc_running = False
@@ -62,12 +62,12 @@ dsc_active_sdr_type: str | None = None
def _get_dsc_decoder_path() -> str | None:
"""Get path to DSC decoder."""
# Check for our custom decoder
project_bin = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'bin', 'dsc-decoder')
project_bin = os.path.join(os.path.dirname(os.path.dirname(__file__)), "bin", "dsc-decoder")
if os.path.isfile(project_bin) and os.access(project_bin, os.X_OK):
return project_bin
# Check system PATH
system_decoder = shutil.which('dsc-decoder')
system_decoder = shutil.which("dsc-decoder")
if system_decoder:
return system_decoder
@@ -76,7 +76,7 @@ def _get_dsc_decoder_path() -> str | None:
def _check_dsc_tools() -> dict:
"""Check availability of DSC decoding tools."""
rtl_fm_path = get_tool_path('rtl_fm')
rtl_fm_path = get_tool_path("rtl_fm")
decoder_path = _get_dsc_decoder_path()
# Check for scipy/numpy (needed for decoder)
@@ -84,24 +84,16 @@ def _check_dsc_tools() -> dict:
try:
import numpy
import scipy
scipy_available = True
except ImportError:
pass
return {
'rtl_fm': {
'available': rtl_fm_path is not None,
'path': rtl_fm_path
},
'dsc_decoder': {
'available': decoder_path is not None,
'path': decoder_path
},
'scipy': {
'available': scipy_available,
'note': 'Required for DSC signal processing'
},
'ready': rtl_fm_path is not None and decoder_path is not None and scipy_available
"rtl_fm": {"available": rtl_fm_path is not None, "path": rtl_fm_path},
"dsc_decoder": {"available": decoder_path is not None, "path": decoder_path},
"scipy": {"available": scipy_available, "note": "Required for DSC signal processing"},
"ready": rtl_fm_path is not None and decoder_path is not None and scipy_available,
}
@@ -116,7 +108,7 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
global dsc_running
try:
app_module.dsc_queue.put({'type': 'status', 'status': 'started'})
app_module.dsc_queue.put({"type": "status", "status": "started"})
buffer = ""
while dsc_running:
@@ -130,10 +122,10 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
data = os.read(master_fd, 1024)
if not data:
break
buffer += data.decode('utf-8', errors='replace')
buffer += data.decode("utf-8", errors="replace")
while '\n' in buffer:
line, buffer = buffer.split('\n', 1)
while "\n" in buffer:
line, buffer = buffer.split("\n", 1)
line = line.strip()
if not line:
continue
@@ -143,7 +135,7 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
if parsed:
# Generate unique message ID
msg_id = f"{parsed['source_mmsi']}_{int(time.time() * 1000)}"
parsed['id'] = msg_id
parsed["id"] = msg_id
# Store in transient DataStore
app_module.dsc_messages.set(msg_id, parsed)
@@ -155,14 +147,11 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
logger.warning("DSC queue full, dropping message")
# Store critical alerts permanently
if parsed.get('is_critical'):
if parsed.get("is_critical"):
_store_critical_alert(parsed)
else:
# Raw output for debugging
app_module.dsc_queue.put({
'type': 'raw',
'text': line
})
app_module.dsc_queue.put({"type": "raw", "text": line})
except OSError:
break
@@ -172,10 +161,7 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
except Exception as e:
logger.error(f"DSC decoder error: {e}")
app_module.dsc_queue.put({
'type': 'error',
'error': str(e)
})
app_module.dsc_queue.put({"type": "error", "error": str(e)})
finally:
global dsc_active_device, dsc_active_sdr_type
with contextlib.suppress(OSError):
@@ -193,13 +179,13 @@ def stream_dsc_decoder(master_fd: int, decoder_process: subprocess.Popen) -> Non
with contextlib.suppress(Exception):
proc.kill()
unregister_process(proc)
app_module.dsc_queue.put({'type': 'status', 'status': 'stopped'})
app_module.dsc_queue.put({"type": "status", "status": "stopped"})
with app_module.dsc_lock:
app_module.dsc_process = None
app_module.dsc_rtl_process = None
# Release SDR device
if dsc_active_device is not None:
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr')
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or "rtlsdr")
dsc_active_device = None
dsc_active_sdr_type = None
@@ -208,15 +194,15 @@ def _store_critical_alert(msg: dict) -> None:
"""Store critical DSC alert (DISTRESS/URGENCY) to database."""
try:
store_dsc_alert(
source_mmsi=msg.get('source_mmsi', ''),
format_code=str(msg.get('format_code', '')),
category=msg.get('category', 'UNKNOWN'),
source_name=msg.get('source_name'),
dest_mmsi=msg.get('dest_mmsi'),
nature_of_distress=msg.get('nature_of_distress'),
latitude=msg.get('latitude'),
longitude=msg.get('longitude'),
raw_message=msg.get('raw_message')
source_mmsi=msg.get("source_mmsi", ""),
format_code=str(msg.get("format_code", "")),
category=msg.get("category", "UNKNOWN"),
source_name=msg.get("source_name"),
dest_mmsi=msg.get("dest_mmsi"),
nature_of_distress=msg.get("nature_of_distress"),
latitude=msg.get("latitude"),
longitude=msg.get("longitude"),
raw_message=msg.get("raw_message"),
)
logger.info(f"Stored {msg.get('category')} alert from {msg.get('source_mmsi')}")
except Exception as e:
@@ -231,115 +217,101 @@ def monitor_rtl_stderr(process: subprocess.Popen) -> None:
for line in process.stderr:
if not dsc_running:
break
err_text = line.decode('utf-8', errors='replace').strip()
err_text = line.decode("utf-8", errors="replace").strip()
if err_text:
logger.debug(f"[RTL_FM] {err_text}")
# Check for device busy error
if 'usb_claim_interface' in err_text.lower():
app_module.dsc_queue.put({
'type': 'error',
'error': 'SDR device busy',
'error_type': 'DEVICE_BUSY',
'suggestion': 'Use a different SDR device or stop other SDR processes'
})
if "usb_claim_interface" in err_text.lower():
app_module.dsc_queue.put(
{
"type": "error",
"error": "SDR device busy",
"error_type": "DEVICE_BUSY",
"suggestion": "Use a different SDR device or stop other SDR processes",
}
)
# Check for other common errors
if 'no supported devices' in err_text.lower():
app_module.dsc_queue.put({
'type': 'error',
'error': 'No SDR device found',
'error_type': 'NO_DEVICE'
})
if "no supported devices" in err_text.lower():
app_module.dsc_queue.put(
{"type": "error", "error": "No SDR device found", "error_type": "NO_DEVICE"}
)
except Exception:
pass
@dsc_bp.route('/status')
@dsc_bp.route("/status")
def get_status() -> Response:
"""Get DSC decoder status."""
global dsc_running
with app_module.dsc_lock:
running = (
dsc_running and
app_module.dsc_process is not None and
app_module.dsc_process.poll() is None
)
running = dsc_running and app_module.dsc_process is not None and app_module.dsc_process.poll() is None
# Get message counts
message_count = len(app_module.dsc_messages)
alert_summary = get_dsc_alert_summary()
return jsonify({
'running': running,
'frequency': DSC_VHF_FREQUENCY_MHZ,
'message_count': message_count,
'alerts': alert_summary
})
return jsonify(
{
"running": running,
"frequency": DSC_VHF_FREQUENCY_MHZ,
"message_count": message_count,
"alerts": alert_summary,
}
)
@dsc_bp.route('/tools')
@dsc_bp.route("/tools")
def check_tools() -> Response:
"""Check DSC decoder tool availability."""
tools = _check_dsc_tools()
return jsonify(tools)
@dsc_bp.route('/start', methods=['POST'])
@dsc_bp.route("/start", methods=["POST"])
def start_decoding() -> Response:
"""Start DSC decoder."""
global dsc_running
with app_module.dsc_lock:
if app_module.dsc_process and app_module.dsc_process.poll() is None:
return jsonify({
'status': 'error',
'message': 'DSC decoder already running'
}), 409
return jsonify({"status": "error", "message": "DSC decoder already running"}), 409
# Check tools
tools = _check_dsc_tools()
if not tools['ready']:
if not tools["ready"]:
missing = []
if not tools['rtl_fm']['available']:
missing.append('rtl_fm')
if not tools['dsc_decoder']['available']:
missing.append('dsc-decoder')
if not tools['scipy']['available']:
missing.append('scipy/numpy')
if not tools["rtl_fm"]["available"]:
missing.append("rtl_fm")
if not tools["dsc_decoder"]["available"]:
missing.append("dsc-decoder")
if not tools["scipy"]["available"]:
missing.append("scipy/numpy")
return jsonify({
'status': 'error',
'message': f'Missing required tools: {", ".join(missing)}'
}), 400
return jsonify({"status": "error", "message": f"Missing required tools: {', '.join(missing)}"}), 400
data = request.json or {}
# Validate device
try:
device = validate_device_index(data.get('device', '0'))
device = validate_device_index(data.get("device", "0"))
except ValueError as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 400
return jsonify({"status": "error", "message": str(e)}), 400
# Validate gain
try:
gain = validate_gain(data.get('gain', '40'))
gain = validate_gain(data.get("gain", "40"))
except ValueError as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 400
return jsonify({"status": "error", "message": str(e)}), 400
# Get SDR type from request
sdr_type_str = data.get('sdr_type', 'rtlsdr')
sdr_type_str = data.get("sdr_type", "rtlsdr")
# Check for rtl_tcp (remote SDR) connection
rtl_tcp_host = data.get('rtl_tcp_host')
rtl_tcp_port = data.get('rtl_tcp_port', 1234)
rtl_tcp_host = data.get("rtl_tcp_host")
rtl_tcp_port = data.get("rtl_tcp_port", 1234)
try:
sdr_type = SDRType(sdr_type_str)
@@ -350,13 +322,9 @@ def start_decoding() -> Response:
global dsc_active_device, dsc_active_sdr_type
if not rtl_tcp_host:
device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'dsc', sdr_type_str)
error = app_module.claim_sdr_device(device_int, "dsc", sdr_type_str)
if error:
return jsonify({
'status': 'error',
'error_type': 'DEVICE_BUSY',
'message': error
}), 409
return jsonify({"status": "error", "error_type": "DEVICE_BUSY", "message": error}), 409
dsc_active_device = device_int
dsc_active_sdr_type = sdr_type_str
@@ -369,7 +337,7 @@ def start_decoding() -> Response:
break
# Build rtl_fm command via SDR abstraction layer
decoder_path = tools['dsc_decoder']['path']
decoder_path = tools["dsc_decoder"]["path"]
if rtl_tcp_host:
try:
@@ -383,39 +351,33 @@ def start_decoding() -> Response:
sdr_device = SDRFactory.create_default_device(sdr_type, index=int(device))
builder = SDRFactory.get_builder(sdr_device.sdr_type)
rtl_cmd = list(builder.build_fm_demod_command(
device=sdr_device,
frequency_mhz=DSC_VHF_FREQUENCY_MHZ,
sample_rate=DSC_SAMPLE_RATE,
gain=float(gain) if gain and str(gain) != '0' else None,
modulation='fm',
squelch=0,
))
rtl_cmd = list(
builder.build_fm_demod_command(
device=sdr_device,
frequency_mhz=DSC_VHF_FREQUENCY_MHZ,
sample_rate=DSC_SAMPLE_RATE,
gain=float(gain) if gain and str(gain) != "0" else None,
modulation="fm",
squelch=0,
)
)
# Ensure trailing '-' for stdin piping and add DC blocking filter
if rtl_cmd and rtl_cmd[-1] == '-':
rtl_cmd = rtl_cmd[:-1] + ['-E', 'dc', '-']
if rtl_cmd and rtl_cmd[-1] == "-":
rtl_cmd = rtl_cmd[:-1] + ["-E", "dc", "-"]
# Decoder command
decoder_cmd = [decoder_path]
full_cmd = ' '.join(rtl_cmd) + ' | ' + ' '.join(decoder_cmd)
full_cmd = " ".join(rtl_cmd) + " | " + " ".join(decoder_cmd)
logger.info(f"Starting DSC decoder: {full_cmd}")
try:
# Start rtl_fm subprocess
rtl_process = subprocess.Popen(
rtl_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
rtl_process = subprocess.Popen(rtl_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
register_process(rtl_process)
# Start stderr monitor thread
stderr_thread = threading.Thread(
target=monitor_rtl_stderr,
args=(rtl_process,),
daemon=True
)
stderr_thread = threading.Thread(target=monitor_rtl_stderr, args=(rtl_process,), daemon=True)
stderr_thread.start()
# Create PTY for decoder output
@@ -423,11 +385,7 @@ def start_decoding() -> Response:
# Start decoder subprocess
decoder_process = subprocess.Popen(
decoder_cmd,
stdin=rtl_process.stdout,
stdout=slave_fd,
stderr=slave_fd,
close_fds=True
decoder_cmd, stdin=rtl_process.stdout, stdout=slave_fd, stderr=slave_fd, close_fds=True
)
register_process(decoder_process)
@@ -440,20 +398,18 @@ def start_decoding() -> Response:
dsc_running = True
# Start output streaming thread
output_thread = threading.Thread(
target=stream_dsc_decoder,
args=(master_fd, decoder_process),
daemon=True
)
output_thread = threading.Thread(target=stream_dsc_decoder, args=(master_fd, decoder_process), daemon=True)
output_thread.start()
return jsonify({
'status': 'started',
'frequency': DSC_VHF_FREQUENCY_MHZ,
'device': device,
'gain': gain,
'command': full_cmd
})
return jsonify(
{
"status": "started",
"frequency": DSC_VHF_FREQUENCY_MHZ,
"device": device,
"gain": gain,
"command": full_cmd,
}
)
except FileNotFoundError as e:
# Kill orphaned rtl_fm process
@@ -465,13 +421,10 @@ def start_decoding() -> Response:
rtl_process.kill()
# Release device on failure
if dsc_active_device is not None:
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr')
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or "rtlsdr")
dsc_active_device = None
dsc_active_sdr_type = None
return jsonify({
'status': 'error',
'message': f'Tool not found: {e.filename}'
}), 400
return jsonify({"status": "error", "message": f"Tool not found: {e.filename}"}), 400
except Exception as e:
# Kill orphaned rtl_fm process if it was started
try:
@@ -482,24 +435,21 @@ def start_decoding() -> Response:
rtl_process.kill()
# Release device on failure
if dsc_active_device is not None:
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr')
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or "rtlsdr")
dsc_active_device = None
dsc_active_sdr_type = None
logger.error(f"Failed to start DSC decoder: {e}")
return jsonify({
'status': 'error',
'message': str(e)
}), 500
return jsonify({"status": "error", "message": str(e)}), 500
@dsc_bp.route('/stop', methods=['POST'])
@dsc_bp.route("/stop", methods=["POST"])
def stop_decoding() -> Response:
"""Stop DSC decoder."""
global dsc_running, dsc_active_device, dsc_active_sdr_type
with app_module.dsc_lock:
if not app_module.dsc_process:
return jsonify({'status': 'not_running'})
return jsonify({"status": "not_running"})
dsc_running = False
@@ -530,116 +480,94 @@ def stop_decoding() -> Response:
# Release device from registry
if dsc_active_device is not None:
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or 'rtlsdr')
app_module.release_sdr_device(dsc_active_device, dsc_active_sdr_type or "rtlsdr")
dsc_active_device = None
dsc_active_sdr_type = None
return jsonify({'status': 'stopped'})
return jsonify({"status": "stopped"})
@dsc_bp.route('/stream')
@dsc_bp.route("/stream")
def stream() -> Response:
"""SSE stream for real-time DSC messages."""
def _on_msg(msg: dict[str, Any]) -> None:
process_event('dsc', msg, msg.get('type'))
process_event("dsc", msg, msg.get("type"))
response = Response(
sse_stream_fanout(
source_queue=app_module.dsc_queue,
channel_key='dsc',
channel_key="dsc",
timeout=1.0,
keepalive_interval=30.0,
on_message=_on_msg,
),
mimetype='text/event-stream',
mimetype="text/event-stream",
)
response.headers['Cache-Control'] = 'no-cache'
response.headers['X-Accel-Buffering'] = 'no'
response.headers['Connection'] = 'keep-alive'
response.headers["Cache-Control"] = "no-cache"
response.headers["X-Accel-Buffering"] = "no"
response.headers["Connection"] = "keep-alive"
return response
@dsc_bp.route('/messages')
@dsc_bp.route("/messages")
def get_messages() -> Response:
"""Get current DSC messages from transient store."""
messages = list(app_module.dsc_messages.values())
# Sort by timestamp (newest first)
messages.sort(key=lambda m: m.get('timestamp', ''), reverse=True)
messages.sort(key=lambda m: m.get("timestamp", ""), reverse=True)
return jsonify({
'count': len(messages),
'messages': messages
})
return jsonify({"count": len(messages), "messages": messages})
@dsc_bp.route('/alerts')
@dsc_bp.route("/alerts")
def get_alerts_endpoint() -> Response:
"""Get stored DSC alerts (paginated)."""
# Parse query params
category = request.args.get('category')
acknowledged = request.args.get('acknowledged')
limit = min(int(request.args.get('limit', 50)), 200)
offset = int(request.args.get('offset', 0))
category = request.args.get("category")
acknowledged = request.args.get("acknowledged")
limit = min(int(request.args.get("limit", 50)), 200)
offset = int(request.args.get("offset", 0))
# Convert acknowledged param
ack_filter = None
if acknowledged is not None:
ack_filter = acknowledged.lower() in ('true', '1', 'yes')
ack_filter = acknowledged.lower() in ("true", "1", "yes")
alerts = get_dsc_alerts(
category=category,
acknowledged=ack_filter,
limit=limit,
offset=offset
)
alerts = get_dsc_alerts(category=category, acknowledged=ack_filter, limit=limit, offset=offset)
summary = get_dsc_alert_summary()
return jsonify({
'alerts': alerts,
'count': len(alerts),
'summary': summary,
'pagination': {
'limit': limit,
'offset': offset
}
})
return jsonify(
{"alerts": alerts, "count": len(alerts), "summary": summary, "pagination": {"limit": limit, "offset": offset}}
)
@dsc_bp.route('/alerts/<int:alert_id>')
@dsc_bp.route("/alerts/<int:alert_id>")
def get_alert(alert_id: int) -> Response:
"""Get a specific DSC alert by ID."""
alert = get_dsc_alert(alert_id)
if not alert:
return jsonify({
'status': 'error',
'message': 'Alert not found'
}), 404
return jsonify({"status": "error", "message": "Alert not found"}), 404
return jsonify(alert)
@dsc_bp.route('/alerts/<int:alert_id>/acknowledge', methods=['POST'])
@dsc_bp.route("/alerts/<int:alert_id>/acknowledge", methods=["POST"])
def acknowledge_alert(alert_id: int) -> Response:
"""Acknowledge a DSC alert."""
data = request.json or {}
notes = data.get('notes')
notes = data.get("notes")
success = acknowledge_dsc_alert(alert_id, notes)
if not success:
return jsonify({
'status': 'error',
'message': 'Alert not found'
}), 404
return jsonify({"status": "error", "message": "Alert not found"}), 404
return jsonify({
'status': 'acknowledged',
'alert_id': alert_id
})
return jsonify({"status": "acknowledged", "alert_id": alert_id})
@dsc_bp.route('/alerts/summary')
@dsc_bp.route("/alerts/summary")
def get_alerts_summary() -> Response:
"""Get summary of unacknowledged DSC alerts."""
summary = get_dsc_alert_summary()