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
+143 -141
View File
@@ -34,9 +34,9 @@ from utils.sdr import SDRFactory, SDRType
from utils.sse import sse_stream_fanout
from utils.validation import validate_device_index, validate_gain
logger = get_logger('intercept.ais')
logger = get_logger("intercept.ais")
ais_bp = Blueprint('ais', __name__, url_prefix='/ais')
ais_bp = Blueprint("ais", __name__, url_prefix="/ais")
# Track AIS state
ais_running = False
@@ -49,17 +49,17 @@ _ais_error_logged = True
# Common installation paths for AIS-catcher
AIS_CATCHER_PATHS = [
'/usr/local/bin/AIS-catcher',
'/usr/bin/AIS-catcher',
'/opt/homebrew/bin/AIS-catcher',
'/opt/homebrew/bin/aiscatcher',
"/usr/local/bin/AIS-catcher",
"/usr/bin/AIS-catcher",
"/opt/homebrew/bin/AIS-catcher",
"/opt/homebrew/bin/aiscatcher",
]
def find_ais_catcher():
"""Find AIS-catcher binary, checking PATH and common locations."""
# First try PATH
for name in ['AIS-catcher', 'aiscatcher']:
for name in ["AIS-catcher", "aiscatcher"]:
path = shutil.which(name)
if path:
return path
@@ -84,7 +84,7 @@ def parse_ais_stream(port: int):
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(AIS_SOCKET_TIMEOUT)
sock.connect(('localhost', port))
sock.connect(("localhost", port))
ais_connected = True
_ais_error_logged = True
logger.info("Connected to AIS-catcher TCP server")
@@ -95,14 +95,14 @@ def parse_ais_stream(port: int):
while ais_running:
try:
data = sock.recv(SOCKET_BUFFER_SIZE).decode('utf-8', errors='ignore')
data = sock.recv(SOCKET_BUFFER_SIZE).decode("utf-8", errors="ignore")
if not data:
logger.warning("AIS connection closed (no data)")
break
buffer += data
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
@@ -111,7 +111,7 @@ def parse_ais_stream(port: int):
msg = json.loads(line)
vessel = process_ais_message(msg)
if vessel:
mmsi = vessel.get('mmsi')
mmsi = vessel.get("mmsi")
if mmsi:
app_module.ais_vessels.set(mmsi, vessel)
pending_updates.add(mmsi)
@@ -128,21 +128,25 @@ def parse_ais_stream(port: int):
if mmsi in app_module.ais_vessels:
_vessel_snap = app_module.ais_vessels[mmsi]
with contextlib.suppress(queue.Full):
app_module.ais_queue.put_nowait({
'type': 'vessel',
**_vessel_snap
})
app_module.ais_queue.put_nowait({"type": "vessel", **_vessel_snap})
# Geofence check
_v_lat = _vessel_snap.get('lat')
_v_lon = _vessel_snap.get('lon')
_v_lat = _vessel_snap.get("lat")
_v_lon = _vessel_snap.get("lon")
if _v_lat and _v_lon:
try:
from utils.geofence import get_geofence_manager
for _gf_evt in get_geofence_manager().check_position(
mmsi, 'vessel', _v_lat, _v_lon,
{'name': _vessel_snap.get('name'), 'ship_type': _vessel_snap.get('ship_type_text')}
mmsi,
"vessel",
_v_lat,
_v_lon,
{
"name": _vessel_snap.get("name"),
"ship_type": _vessel_snap.get("ship_type_text"),
},
):
process_event('ais', _gf_evt, 'geofence')
process_event("ais", _gf_evt, "geofence")
except Exception:
pass
pending_updates.clear()
@@ -172,139 +176,138 @@ def process_ais_message(msg: dict) -> dict | None:
# AIS-catcher outputs different message types
# We're interested in position reports and static data
mmsi = msg.get('mmsi')
mmsi = msg.get("mmsi")
if not mmsi:
return None
mmsi = str(mmsi)
# Get existing vessel data or create new
vessel = app_module.ais_vessels.get(mmsi) or {'mmsi': mmsi}
vessel = app_module.ais_vessels.get(mmsi) or {"mmsi": mmsi}
# Extract common fields
# AIS-catcher JSON_FULL uses 'longitude'/'latitude', but some versions use 'lon'/'lat'
lat_val = msg.get('latitude') or msg.get('lat')
lon_val = msg.get('longitude') or msg.get('lon')
lat_val = msg.get("latitude") or msg.get("lat")
lon_val = msg.get("longitude") or msg.get("lon")
if lat_val is not None and lon_val is not None:
try:
lat = float(lat_val)
lon = float(lon_val)
# Validate coordinates (AIS uses 181 for unavailable)
if -90 <= lat <= 90 and -180 <= lon <= 180:
vessel['lat'] = lat
vessel['lon'] = lon
vessel["lat"] = lat
vessel["lon"] = lon
except (ValueError, TypeError):
pass
# Speed over ground (knots)
if 'speed' in msg:
if "speed" in msg:
try:
speed = float(msg['speed'])
speed = float(msg["speed"])
if speed < 102.3: # 102.3 = not available
vessel['speed'] = round(speed, 1)
vessel["speed"] = round(speed, 1)
except (ValueError, TypeError):
pass
# Course over ground (degrees)
if 'course' in msg:
if "course" in msg:
try:
course = float(msg['course'])
course = float(msg["course"])
if course < 360: # 360 = not available
vessel['course'] = round(course, 1)
vessel["course"] = round(course, 1)
except (ValueError, TypeError):
pass
# True heading (degrees)
if 'heading' in msg:
if "heading" in msg:
try:
heading = int(msg['heading'])
heading = int(msg["heading"])
if heading < 511: # 511 = not available
vessel['heading'] = heading
vessel["heading"] = heading
except (ValueError, TypeError):
pass
# Navigation status
if 'status' in msg:
vessel['nav_status'] = msg['status']
if 'status_text' in msg:
vessel['nav_status_text'] = msg['status_text']
if "status" in msg:
vessel["nav_status"] = msg["status"]
if "status_text" in msg:
vessel["nav_status_text"] = msg["status_text"]
# Vessel name (from Type 5 or Type 24 messages)
if 'shipname' in msg:
name = msg['shipname'].strip().strip('@')
if "shipname" in msg:
name = msg["shipname"].strip().strip("@")
if name:
vessel['name'] = name
vessel["name"] = name
# Callsign
if 'callsign' in msg:
callsign = msg['callsign'].strip().strip('@')
if "callsign" in msg:
callsign = msg["callsign"].strip().strip("@")
if callsign:
vessel['callsign'] = callsign
vessel["callsign"] = callsign
# Ship type
if 'shiptype' in msg:
vessel['ship_type'] = msg['shiptype']
if 'shiptype_text' in msg:
vessel['ship_type_text'] = msg['shiptype_text']
if "shiptype" in msg:
vessel["ship_type"] = msg["shiptype"]
if "shiptype_text" in msg:
vessel["ship_type_text"] = msg["shiptype_text"]
# Destination
if 'destination' in msg:
dest = msg['destination'].strip().strip('@')
if "destination" in msg:
dest = msg["destination"].strip().strip("@")
if dest:
vessel['destination'] = dest
vessel["destination"] = dest
# ETA
if 'eta' in msg:
vessel['eta'] = msg['eta']
if "eta" in msg:
vessel["eta"] = msg["eta"]
# Dimensions
if 'to_bow' in msg and 'to_stern' in msg:
if "to_bow" in msg and "to_stern" in msg:
try:
length = int(msg['to_bow']) + int(msg['to_stern'])
length = int(msg["to_bow"]) + int(msg["to_stern"])
if length > 0:
vessel['length'] = length
vessel["length"] = length
except (ValueError, TypeError):
pass
if 'to_port' in msg and 'to_starboard' in msg:
if "to_port" in msg and "to_starboard" in msg:
try:
width = int(msg['to_port']) + int(msg['to_starboard'])
width = int(msg["to_port"]) + int(msg["to_starboard"])
if width > 0:
vessel['width'] = width
vessel["width"] = width
except (ValueError, TypeError):
pass
# Draught
if 'draught' in msg:
if "draught" in msg:
try:
draught = float(msg['draught'])
draught = float(msg["draught"])
if draught > 0:
vessel['draught'] = draught
vessel["draught"] = draught
except (ValueError, TypeError):
pass
# Rate of turn
if 'turn' in msg:
if "turn" in msg:
try:
turn = float(msg['turn'])
turn = float(msg["turn"])
if -127 <= turn <= 127: # Valid range
vessel['rate_of_turn'] = turn
vessel["rate_of_turn"] = turn
except (ValueError, TypeError):
pass
# Message type for debugging
if 'type' in msg:
vessel['last_msg_type'] = msg['type']
if "type" in msg:
vessel["last_msg_type"] = msg["type"]
# Timestamp
vessel['last_seen'] = time.time()
vessel["last_seen"] = time.time()
# Check for DSC DISTRESS matching this MMSI
try:
for _dsc_key, _dsc_msg in app_module.dsc_messages.items():
if (str(_dsc_msg.get('source_mmsi', '')) == mmsi
and _dsc_msg.get('category', '').upper() == 'DISTRESS'):
vessel['dsc_distress'] = True
if str(_dsc_msg.get("source_mmsi", "")) == mmsi and _dsc_msg.get("category", "").upper() == "DISTRESS":
vessel["dsc_distress"] = True
break
except Exception:
pass
@@ -312,7 +315,7 @@ def process_ais_message(msg: dict) -> dict | None:
return vessel
@ais_bp.route('/tools')
@ais_bp.route("/tools")
def check_ais_tools():
"""Check for AIS decoding tools and hardware."""
has_ais_catcher = find_ais_catcher() is not None
@@ -321,60 +324,64 @@ def check_ais_tools():
devices = SDRFactory.detect_devices()
has_rtlsdr = any(d.sdr_type == SDRType.RTL_SDR for d in devices)
return jsonify({
'ais_catcher': has_ais_catcher,
'ais_catcher_path': find_ais_catcher(),
'has_rtlsdr': has_rtlsdr,
'device_count': len(devices)
})
return jsonify(
{
"ais_catcher": has_ais_catcher,
"ais_catcher_path": find_ais_catcher(),
"has_rtlsdr": has_rtlsdr,
"device_count": len(devices),
}
)
@ais_bp.route('/status')
@ais_bp.route("/status")
def ais_status():
"""Get AIS tracking status for debugging."""
process_running = False
if app_module.ais_process:
process_running = app_module.ais_process.poll() is None
return jsonify({
'tracking_active': ais_running,
'active_device': ais_active_device,
'connected': ais_connected,
'messages_received': ais_messages_received,
'last_message_time': ais_last_message_time,
'vessel_count': len(app_module.ais_vessels),
'vessels': dict(app_module.ais_vessels),
'queue_size': app_module.ais_queue.qsize(),
'ais_catcher_path': find_ais_catcher(),
'process_running': process_running
})
return jsonify(
{
"tracking_active": ais_running,
"active_device": ais_active_device,
"connected": ais_connected,
"messages_received": ais_messages_received,
"last_message_time": ais_last_message_time,
"vessel_count": len(app_module.ais_vessels),
"vessels": dict(app_module.ais_vessels),
"queue_size": app_module.ais_queue.qsize(),
"ais_catcher_path": find_ais_catcher(),
"process_running": process_running,
}
)
@ais_bp.route('/start', methods=['POST'])
@ais_bp.route("/start", methods=["POST"])
def start_ais():
"""Start AIS tracking."""
global ais_running, ais_active_device, ais_active_sdr_type
with app_module.ais_lock:
if ais_running:
return api_error('AIS tracking already active', 409)
return api_error("AIS tracking already active", 409)
data = request.json or {}
# Validate inputs
try:
gain = int(validate_gain(data.get('gain', '40')))
device = validate_device_index(data.get('device', '0'))
gain = int(validate_gain(data.get("gain", "40")))
device = validate_device_index(data.get("device", "0"))
except ValueError as e:
return api_error(str(e), 400)
# Find AIS-catcher
ais_catcher_path = find_ais_catcher()
if not ais_catcher_path:
return api_error('AIS-catcher not found. Install from https://github.com/jvde-github/AIS-catcher/releases', 400)
return api_error("AIS-catcher not found. Install from https://github.com/jvde-github/AIS-catcher/releases", 400)
# Get SDR type from request
sdr_type_str = data.get('sdr_type', 'rtlsdr')
sdr_type_str = data.get("sdr_type", "rtlsdr")
try:
sdr_type = SDRType(sdr_type_str)
except ValueError:
@@ -397,27 +404,27 @@ def start_ais():
# Check if device is available
device_int = int(device)
error = app_module.claim_sdr_device(device_int, 'ais', sdr_type_str)
error = app_module.claim_sdr_device(device_int, "ais", sdr_type_str)
if error:
return api_error(error, 409, error_type='DEVICE_BUSY')
return api_error(error, 409, error_type="DEVICE_BUSY")
# Build command using SDR abstraction
sdr_device = SDRFactory.create_default_device(sdr_type, index=device)
builder = SDRFactory.get_builder(sdr_type)
bias_t = data.get('bias_t', False)
bias_t = data.get("bias_t", False)
tcp_port = AIS_TCP_PORT
# Optional UDP NMEA forwarding (e.g. for OpenCPN on port 10110)
udp_host = data.get('udp_host') or None
udp_host = data.get("udp_host") or None
udp_port = None
if udp_host:
try:
udp_port = int(data.get('udp_port', 10110))
udp_port = int(data.get("udp_port", 10110))
if not 1 <= udp_port <= 65535:
raise ValueError
except (TypeError, ValueError):
return api_error('Invalid udp_port (1-65535)', 400)
return api_error("Invalid udp_port (1-65535)", 400)
cmd = builder.build_ais_command(
device=sdr_device,
@@ -434,10 +441,7 @@ def start_ais():
try:
logger.info(f"Starting AIS-catcher with device {device}: {' '.join(cmd)}")
app_module.ais_process = subprocess.Popen(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
start_new_session=True
cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, start_new_session=True
)
# Wait for process to start
@@ -446,15 +450,15 @@ def start_ais():
if app_module.ais_process.poll() is not None:
# Release device on failure
app_module.release_sdr_device(device_int, sdr_type_str)
stderr_output = ''
stderr_output = ""
if app_module.ais_process.stderr:
with contextlib.suppress(Exception):
stderr_output = app_module.ais_process.stderr.read().decode('utf-8', errors='ignore').strip()
stderr_output = app_module.ais_process.stderr.read().decode("utf-8", errors="ignore").strip()
if stderr_output:
logger.error(f"AIS-catcher stderr:\n{stderr_output}")
error_msg = 'AIS-catcher failed to start. Check SDR device connection.'
error_msg = "AIS-catcher failed to start. Check SDR device connection."
if stderr_output:
error_msg += f' Error: {stderr_output[:500]}'
error_msg += f" Error: {stderr_output[:500]}"
return api_error(error_msg, 500)
ais_running = True
@@ -465,12 +469,7 @@ def start_ais():
thread = threading.Thread(target=parse_ais_stream, args=(tcp_port,), daemon=True)
thread.start()
return jsonify({
'status': 'started',
'message': 'AIS tracking started',
'device': device,
'port': tcp_port
})
return jsonify({"status": "started", "message": "AIS tracking started", "device": device, "port": tcp_port})
except Exception as e:
# Release device on failure
app_module.release_sdr_device(device_int, sdr_type_str)
@@ -478,7 +477,7 @@ def start_ais():
return api_error(str(e), 500)
@ais_bp.route('/stop', methods=['POST'])
@ais_bp.route("/stop", methods=["POST"])
def stop_ais():
"""Stop AIS tracking."""
global ais_running, ais_active_device, ais_active_sdr_type
@@ -500,55 +499,56 @@ def stop_ais():
# Release device from registry
if ais_active_device is not None:
app_module.release_sdr_device(ais_active_device, ais_active_sdr_type or 'rtlsdr')
app_module.release_sdr_device(ais_active_device, ais_active_sdr_type or "rtlsdr")
ais_running = False
ais_active_device = None
ais_active_sdr_type = None
app_module.ais_vessels.clear()
return jsonify({'status': 'stopped'})
return jsonify({"status": "stopped"})
@ais_bp.route('/stream')
@ais_bp.route("/stream")
def stream_ais():
"""SSE stream for AIS vessels."""
def _on_msg(msg: dict[str, Any]) -> None:
process_event('ais', msg, msg.get('type'))
process_event("ais", msg, msg.get("type"))
response = Response(
sse_stream_fanout(
source_queue=app_module.ais_queue,
channel_key='ais',
channel_key="ais",
timeout=SSE_QUEUE_TIMEOUT,
keepalive_interval=SSE_KEEPALIVE_INTERVAL,
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["Cache-Control"] = "no-cache"
response.headers["X-Accel-Buffering"] = "no"
return response
@ais_bp.route('/vessel/<mmsi>/dsc')
@ais_bp.route("/vessel/<mmsi>/dsc")
def get_vessel_dsc(mmsi: str):
"""Get DSC messages associated with a vessel MMSI."""
if not mmsi or not mmsi.isdigit():
return api_error('Invalid MMSI', 400)
return api_error("Invalid MMSI", 400)
matches = []
try:
for _key, msg in app_module.dsc_messages.items():
if str(msg.get('source_mmsi', '')) == mmsi:
if str(msg.get("source_mmsi", "")) == mmsi:
matches.append(dict(msg))
except Exception:
pass
return api_success(data={'mmsi': mmsi, 'dsc_messages': matches})
return api_success(data={"mmsi": mmsi, "dsc_messages": matches})
@ais_bp.route('/vessels')
@ais_bp.route("/vessels")
def ais_vessels():
"""Export current AIS vessel data as JSON.
@@ -563,22 +563,24 @@ def ais_vessels():
"""
vessels = dict(app_module.ais_vessels)
mmsi_filter = request.args.get('mmsi')
mmsi_filter = request.args.get("mmsi")
if mmsi_filter:
vessels = {k: v for k, v in vessels.items() if str(k) == str(mmsi_filter)}
return jsonify({
'count': len(vessels),
'vessels': list(vessels.values()),
})
return jsonify(
{
"count": len(vessels),
"vessels": list(vessels.values()),
}
)
@ais_bp.route('/dashboard')
@ais_bp.route("/dashboard")
def ais_dashboard():
"""Popout AIS dashboard."""
embedded = request.args.get('embedded', 'false') == 'true'
embedded = request.args.get("embedded", "false") == "true"
return render_template(
'ais_dashboard.html',
"ais_dashboard.html",
shared_observer_location=SHARED_OBSERVER_LOCATION_ENABLED,
default_latitude=DEFAULT_LATITUDE,
default_longitude=DEFAULT_LONGITUDE,