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
+145 -129
View File
@@ -14,33 +14,35 @@ from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable
logger = logging.getLogger('intercept.gps')
logger = logging.getLogger("intercept.gps")
@dataclass
class GPSSatellite:
"""Individual satellite data from gpsd SKY message."""
prn: int
elevation: float | None = None # degrees
azimuth: float | None = None # degrees
snr: float | None = None # dB-Hz
used: bool = False
constellation: str = 'GPS' # GPS, GLONASS, Galileo, BeiDou, SBAS, QZSS
constellation: str = "GPS" # GPS, GLONASS, Galileo, BeiDou, SBAS, QZSS
def to_dict(self) -> dict:
return {
'prn': self.prn,
'elevation': self.elevation,
'azimuth': self.azimuth,
'snr': self.snr,
'used': self.used,
'constellation': self.constellation,
"prn": self.prn,
"elevation": self.elevation,
"azimuth": self.azimuth,
"snr": self.snr,
"used": self.used,
"constellation": self.constellation,
}
@dataclass
class GPSSkyData:
"""Sky view data from gpsd SKY message."""
satellites: list[GPSSatellite] = field(default_factory=list)
hdop: float | None = None
vdop: float | None = None
@@ -54,22 +56,23 @@ class GPSSkyData:
def to_dict(self) -> dict:
return {
'satellites': [s.to_dict() for s in self.satellites],
'hdop': self.hdop,
'vdop': self.vdop,
'pdop': self.pdop,
'tdop': self.tdop,
'gdop': self.gdop,
'xdop': self.xdop,
'ydop': self.ydop,
'nsat': self.nsat,
'usat': self.usat,
"satellites": [s.to_dict() for s in self.satellites],
"hdop": self.hdop,
"vdop": self.vdop,
"pdop": self.pdop,
"tdop": self.tdop,
"gdop": self.gdop,
"xdop": self.xdop,
"ydop": self.ydop,
"nsat": self.nsat,
"usat": self.usat,
}
@dataclass
class GPSPosition:
"""GPS position data."""
latitude: float
longitude: float
altitude: float | None = None
@@ -90,21 +93,21 @@ class GPSPosition:
def to_dict(self) -> dict:
"""Convert to dictionary for JSON serialization."""
return {
'latitude': self.latitude,
'longitude': self.longitude,
'altitude': self.altitude,
'speed': self.speed,
'heading': self.heading,
'climb': self.climb,
'satellites': self.satellites,
'fix_quality': self.fix_quality,
'timestamp': self.timestamp.isoformat() if self.timestamp else None,
'device': self.device,
'epx': self.epx,
'epy': self.epy,
'epv': self.epv,
'eps': self.eps,
'ept': self.ept,
"latitude": self.latitude,
"longitude": self.longitude,
"altitude": self.altitude,
"speed": self.speed,
"heading": self.heading,
"climb": self.climb,
"satellites": self.satellites,
"fix_quality": self.fix_quality,
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
"device": self.device,
"epx": self.epx,
"epy": self.epy,
"epv": self.epv,
"eps": self.eps,
"ept": self.ept,
}
@@ -112,26 +115,32 @@ def _classify_constellation(prn: int, gnssid: int | None = None) -> str:
"""Classify satellite constellation from PRN or gnssid."""
if gnssid is not None:
mapping = {
0: 'GPS', 1: 'SBAS', 2: 'Galileo', 3: 'BeiDou',
4: 'IMES', 5: 'QZSS', 6: 'GLONASS', 7: 'NavIC',
0: "GPS",
1: "SBAS",
2: "Galileo",
3: "BeiDou",
4: "IMES",
5: "QZSS",
6: "GLONASS",
7: "NavIC",
}
return mapping.get(gnssid, 'GPS')
return mapping.get(gnssid, "GPS")
# Fall back to PRN range heuristic
if 1 <= prn <= 32:
return 'GPS'
return "GPS"
elif 33 <= prn <= 64:
return 'SBAS'
return "SBAS"
elif 65 <= prn <= 96:
return 'GLONASS'
return "GLONASS"
elif 120 <= prn <= 158:
return 'SBAS'
return "SBAS"
elif 201 <= prn <= 264:
return 'BeiDou'
return "BeiDou"
elif 301 <= prn <= 336:
return 'Galileo'
return "Galileo"
elif 193 <= prn <= 200:
return 'QZSS'
return 'GPS'
return "QZSS"
return "GPS"
class GPSDClient:
@@ -142,7 +151,7 @@ class GPSDClient:
device management, making it ideal when gpsd is already running.
"""
DEFAULT_HOST = 'localhost'
DEFAULT_HOST = "localhost"
DEFAULT_PORT = 2947
def __init__(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT):
@@ -194,20 +203,20 @@ class GPSDClient:
"""Return gpsd connection info."""
return f"gpsd://{self.host}:{self.port}"
def add_callback(self, callback: Callable[[GPSPosition], None]) -> None:
"""Add a callback to be called on position updates."""
if callback not in self._callbacks:
self._callbacks.append(callback)
def add_callback(self, callback: Callable[[GPSPosition], None]) -> None:
"""Add a callback to be called on position updates."""
if callback not in self._callbacks:
self._callbacks.append(callback)
def remove_callback(self, callback: Callable[[GPSPosition], None]) -> None:
"""Remove a position update callback."""
if callback in self._callbacks:
self._callbacks.remove(callback)
def add_sky_callback(self, callback: Callable[[GPSSkyData], None]) -> None:
"""Add a callback to be called on sky data updates."""
if callback not in self._sky_callbacks:
self._sky_callbacks.append(callback)
def add_sky_callback(self, callback: Callable[[GPSSkyData], None]) -> None:
"""Add a callback to be called on sky data updates."""
if callback not in self._sky_callbacks:
self._sky_callbacks.append(callback)
def remove_sky_callback(self, callback: Callable[[GPSSkyData], None]) -> None:
"""Remove a sky data update callback."""
@@ -228,7 +237,7 @@ class GPSDClient:
# Enable JSON watch mode
watch_cmd = '?WATCH={"enable":true,"json":true}\n'
self._socket.send(watch_cmd.encode('ascii'))
self._socket.send(watch_cmd.encode("ascii"))
self._running = True
self._error = None
@@ -289,11 +298,11 @@ class GPSDClient:
self._error = "Connection closed by gpsd"
break
buffer += data.decode('ascii', errors='ignore')
buffer += data.decode("ascii", errors="ignore")
# Process complete JSON lines
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:
@@ -301,21 +310,21 @@ class GPSDClient:
try:
msg = json.loads(line)
msg_class = msg.get('class', '')
msg_class = msg.get("class", "")
message_count += 1
if message_count <= 5 or message_count % 20 == 0:
print(f"[GPS] gpsd msg [{message_count}]: {msg_class}", flush=True)
if msg_class == 'TPV':
if msg_class == "TPV":
self._handle_tpv(msg)
elif msg_class == 'SKY':
elif msg_class == "SKY":
self._handle_sky(msg)
elif msg_class == 'DEVICES':
elif msg_class == "DEVICES":
# Track connected device
devices = msg.get('devices', [])
devices = msg.get("devices", [])
if devices:
self._device = devices[0].get('path', 'unknown')
self._device = devices[0].get("path", "unknown")
print(f"[GPS] gpsd device: {self._device}", flush=True)
except json.JSONDecodeError:
@@ -334,41 +343,41 @@ class GPSDClient:
def _handle_tpv(self, msg: dict) -> None:
"""Handle TPV (Time-Position-Velocity) message from gpsd."""
# mode: 0=unknown, 1=no fix, 2=2D fix, 3=3D fix
mode = msg.get('mode', 0)
mode = msg.get("mode", 0)
if mode < 2:
# No fix yet
return
lat = msg.get('lat')
lon = msg.get('lon')
lat = msg.get("lat")
lon = msg.get("lon")
if lat is None or lon is None:
return
# Parse timestamp
timestamp = None
time_str = msg.get('time')
time_str = msg.get("time")
if time_str:
with contextlib.suppress(ValueError, AttributeError):
# gpsd uses ISO format: 2024-01-01T12:00:00.000Z
timestamp = datetime.fromisoformat(time_str.replace('Z', '+00:00'))
timestamp = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
position = GPSPosition(
latitude=lat,
longitude=lon,
altitude=msg.get('alt'),
speed=msg.get('speed'), # m/s in gpsd
heading=msg.get('track'),
climb=msg.get('climb'),
altitude=msg.get("alt"),
speed=msg.get("speed"), # m/s in gpsd
heading=msg.get("track"),
climb=msg.get("climb"),
fix_quality=mode,
timestamp=timestamp,
device=self._device or f"gpsd://{self.host}:{self.port}",
epx=msg.get('epx'),
epy=msg.get('epy'),
epv=msg.get('epv'),
eps=msg.get('eps'),
ept=msg.get('ept'),
epx=msg.get("epx"),
epy=msg.get("epy"),
epv=msg.get("epv"),
eps=msg.get("eps"),
ept=msg.get("ept"),
)
print(f"[GPS] gpsd FIX: {lat:.6f}, {lon:.6f} (mode: {mode})", flush=True)
@@ -382,22 +391,24 @@ class GPSDClient:
DOP-only SKY arrives, preserve the most recent satellite list
instead of overwriting it with an empty one.
"""
raw_sats = msg.get('satellites', [])
raw_sats = msg.get("satellites", [])
has_satellites = len(raw_sats) > 0
if has_satellites:
sats = []
for sat in raw_sats:
prn = sat.get('PRN', 0)
gnssid = sat.get('gnssid')
sats.append(GPSSatellite(
prn=prn,
elevation=sat.get('el'),
azimuth=sat.get('az'),
snr=sat.get('ss'),
used=sat.get('used', False),
constellation=_classify_constellation(prn, gnssid),
))
prn = sat.get("PRN", 0)
gnssid = sat.get("gnssid")
sats.append(
GPSSatellite(
prn=prn,
elevation=sat.get("el"),
azimuth=sat.get("az"),
snr=sat.get("ss"),
used=sat.get("used", False),
constellation=_classify_constellation(prn, gnssid),
)
)
else:
# DOP-only SKY message — keep existing satellites
with self._lock:
@@ -405,13 +416,13 @@ class GPSDClient:
sky_data = GPSSkyData(
satellites=sats,
hdop=msg.get('hdop'),
vdop=msg.get('vdop'),
pdop=msg.get('pdop'),
tdop=msg.get('tdop'),
gdop=msg.get('gdop'),
xdop=msg.get('xdop'),
ydop=msg.get('ydop'),
hdop=msg.get("hdop"),
vdop=msg.get("vdop"),
pdop=msg.get("pdop"),
tdop=msg.get("tdop"),
gdop=msg.get("gdop"),
xdop=msg.get("xdop"),
ydop=msg.get("ydop"),
nsat=len(sats),
usat=sum(1 for s in sats if s.used),
)
@@ -452,9 +463,12 @@ def get_gps_reader() -> GPSDClient | None:
return _gps_client
def start_gpsd(host: str = 'localhost', port: int = 2947,
callback: Callable[[GPSPosition], None] | None = None,
sky_callback: Callable[[GPSSkyData], None] | None = None) -> bool:
def start_gpsd(
host: str = "localhost",
port: int = 2947,
callback: Callable[[GPSPosition], None] | None = None,
sky_callback: Callable[[GPSSkyData], None] | None = None,
) -> bool:
"""
Start the global GPS client connected to gpsd.
@@ -524,42 +538,40 @@ def detect_gps_devices() -> list[dict]:
devices: list[dict] = []
system = platform.system()
if system == 'Linux':
if system == "Linux":
# Common USB GPS device paths
patterns = ['/dev/ttyUSB*', '/dev/ttyACM*']
patterns = ["/dev/ttyUSB*", "/dev/ttyACM*"]
for pattern in patterns:
for path in sorted(glob.glob(pattern)):
desc = _describe_device_linux(path)
devices.append({'path': path, 'description': desc})
devices.append({"path": path, "description": desc})
# Also check /dev/serial/by-id for descriptive names
serial_dir = '/dev/serial/by-id'
serial_dir = "/dev/serial/by-id"
if os.path.isdir(serial_dir):
for name in sorted(os.listdir(serial_dir)):
full = os.path.join(serial_dir, name)
real = os.path.realpath(full)
# Skip if we already found this device
if any(d['path'] == real for d in devices):
if any(d["path"] == real for d in devices):
# Update description with the more descriptive name
for d in devices:
if d['path'] == real:
d['description'] = name
if d["path"] == real:
d["description"] = name
continue
devices.append({'path': real, 'description': name})
devices.append({"path": real, "description": name})
elif system == 'Darwin':
elif system == "Darwin":
# macOS: USB serial devices (prefer cu. over tty. for outgoing)
patterns = ['/dev/cu.usbmodem*', '/dev/cu.usbserial*']
patterns = ["/dev/cu.usbmodem*", "/dev/cu.usbserial*"]
for pattern in patterns:
for path in sorted(glob.glob(pattern)):
desc = _describe_device_macos(path)
devices.append({'path': path, 'description': desc})
devices.append({"path": path, "description": desc})
# Sort: devices with GPS-related descriptions first
gps_keywords = ('gps', 'gnss', 'u-blox', 'ublox', 'nmea', 'sirf', 'navigation')
devices.sort(key=lambda d: (
0 if any(k in d['description'].lower() for k in gps_keywords) else 1
))
gps_keywords = ("gps", "gnss", "u-blox", "ublox", "nmea", "sirf", "navigation")
devices.sort(key=lambda d: (0 if any(k in d["description"].lower() for k in gps_keywords) else 1))
return devices
@@ -567,11 +579,12 @@ def detect_gps_devices() -> list[dict]:
def _describe_device_linux(path: str) -> str:
"""Get a human-readable description of a Linux serial device."""
import os
basename = os.path.basename(path)
# Try to read from sysfs
try:
# /sys/class/tty/ttyUSB0/device/../product
sysfs = f'/sys/class/tty/{basename}/device/../product'
sysfs = f"/sys/class/tty/{basename}/device/../product"
if os.path.exists(sysfs):
with open(sysfs) as f:
return f.read().strip()
@@ -583,12 +596,14 @@ def _describe_device_linux(path: str) -> str:
def _describe_device_macos(path: str) -> str:
"""Get a description of a macOS serial device."""
import os
return os.path.basename(path)
def is_gpsd_running(host: str = 'localhost', port: int = 2947) -> bool:
def is_gpsd_running(host: str = "localhost", port: int = 2947) -> bool:
"""Check if gpsd is reachable."""
import socket
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1.0)
@@ -599,8 +614,7 @@ def is_gpsd_running(host: str = 'localhost', port: int = 2947) -> bool:
return False
def start_gpsd_daemon(device_path: str, host: str = 'localhost',
port: int = 2947) -> tuple[bool, str]:
def start_gpsd_daemon(device_path: str, host: str = "localhost", port: int = 2947) -> tuple[bool, str]:
"""
Start gpsd daemon pointing at the given device.
@@ -614,21 +628,22 @@ def start_gpsd_daemon(device_path: str, host: str = 'localhost',
with _gpsd_process_lock:
# Already running?
if is_gpsd_running(host, port):
return True, 'gpsd already running'
return True, "gpsd already running"
gpsd_bin = shutil.which('gpsd')
gpsd_bin = shutil.which("gpsd")
if not gpsd_bin:
return False, 'gpsd not installed'
return False, "gpsd not installed"
# Stop any existing managed process
stop_gpsd_daemon()
try:
import os
if not os.path.exists(device_path):
return False, f'Device {device_path} not found'
cmd = [gpsd_bin, '-N', '-n', '-S', str(port), device_path]
if not os.path.exists(device_path):
return False, f"Device {device_path} not found"
cmd = [gpsd_bin, "-N", "-n", "-S", str(port), device_path]
logger.info(f"Starting gpsd: {' '.join(cmd)}")
print(f"[GPS] Starting gpsd: {' '.join(cmd)}", flush=True)
@@ -640,22 +655,23 @@ def start_gpsd_daemon(device_path: str, host: str = 'localhost',
# Give gpsd a moment to start
import time
time.sleep(1.5)
if _gpsd_process.poll() is not None:
stderr = ''
stderr = ""
if _gpsd_process.stderr:
stderr = _gpsd_process.stderr.read().decode('utf-8', errors='ignore').strip()
msg = f'gpsd exited with code {_gpsd_process.returncode}'
stderr = _gpsd_process.stderr.read().decode("utf-8", errors="ignore").strip()
msg = f"gpsd exited with code {_gpsd_process.returncode}"
if stderr:
msg += f': {stderr}'
msg += f": {stderr}"
return False, msg
# Verify it's listening
if is_gpsd_running(host, port):
return True, f'gpsd started on {device_path}'
return True, f"gpsd started on {device_path}"
else:
return False, 'gpsd started but not accepting connections'
return False, "gpsd started but not accepting connections"
except Exception as e:
logger.error(f"Failed to start gpsd: {e}")