mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 09:08:12 -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:
+145
-129
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user