Files
Smartwave/orchestrateur/sensors/gps.py
T
Ninluc 3753f57041
Build, push image, and notify Watchtower / build-image (push) Successful in 3m22s
Build, push image, and notify Watchtower / notify (push) Successful in 1m24s
Small things because fuck sd cards
2026-08-25 19:16:50 +02:00

123 lines
4.0 KiB
Python

import serial
import time
import threading
from shared.logging import log
from sensors.lock import safe_serial_access
def calculate_nmea_checksum(line: str) -> bool:
"""Validates standard NMEA 0183 sentence checksum ($...*HH)."""
if not line.startswith('$') or '*' not in line:
return False
try:
content, checksum_str = line[1:].split('*', 1)
calculated_checksum = 0
for char in content:
calculated_checksum ^= ord(char)
return calculated_checksum == int(checksum_str[:2], 16)
except Exception:
return False
class GROVEGPS:
def __init__(self, port='/dev/ttyAMA0', baud=9600, timeout=1):
self.ser = serial.Serial(port, baud, timeout=timeout)
self.clean_data()
def clean_data(self):
self.timestamp = ""
self.quality = 0
self.satellites = 0
self.altitude = -1.0
self.latitude = -1.0
self.longitude = -1.0
def read(self):
"""Reads the freshest GGA sentence from serial, thread-safely."""
# Allow sufficient time for the serial reads (readline can block up to timeout=1s per line)
with safe_serial_access(timeout=2.0) as acquired:
if not acquired:
log("GPS: Serial lock acquisition timed out")
return False
# 1. Flush accumulated stale buffer data
self.ser.reset_input_buffer()
# 2. Read through the fresh incoming stream to find a complete GGA line
for _ in range(15):
raw_bytes = self.ser.readline()
try:
line = raw_bytes.decode('utf-8', errors='ignore').strip()
log(f"GPS: Read line: {line}")
except Exception:
continue
if (line.startswith('$GPGGA') or line.startswith('$GNGGA')) and calculate_nmea_checksum(line):
return self.parse_gga(line)
return False
def parse_gga(self, line):
self.clean_data()
gga = line.split(',')
if len(gga) < 10:
return False
try:
self.timestamp = gga[1]
self.quality = int(gga[6]) if gga[6] != "" else 0
self.satellites = int(gga[7]) if gga[7] != "" else 0
# If quality > 0 and coordinates exist, convert NMEA DDDMM.MMMM to decimal degrees
if self.quality > 0 and gga[2] != "" and gga[4] != "":
lat_raw = float(gga[2])
ns = gga[3]
lon_raw = float(gga[4])
ew = gga[5]
# Latitude calculation
lat_deg = lat_raw // 100
lat_min = lat_raw % 100
self.latitude = lat_deg + (lat_min / 60.0)
if ns == 'S':
self.latitude = -self.latitude
# Longitude calculation
lon_deg = lon_raw // 100
lon_min = lon_raw % 100
self.longitude = lon_deg + (lon_min / 60.0)
if ew == 'W':
self.longitude = -self.longitude
self.altitude = float(gga[9]) if gga[9] != "" else -1.0
return True
else:
# No lock on this line
return True
except (ValueError, IndexError):
return False
# Shared instance
gps = GROVEGPS()
def get_gps_data():
"""Returns GPS dictionary if fix is valid, otherwise returns None."""
has_data = gps.read()
# Strictly check that we have a valid GPS lock (quality > 0 and valid coordinates)
if has_data and gps.quality > 0 and gps.latitude != -1.0:
return {
"timestamp": gps.timestamp,
"latitude": round(gps.latitude, 6),
"longitude": round(gps.longitude, 6),
"altitude": gps.altitude,
"quality": gps.quality,
"satellites": gps.satellites
}
else:
log(f"GPS: No valid fix or data available. Satellites: {gps.satellites}, Quality: {gps.quality}")
return None