Revive yaris-obd: USB OBD adapter, real GPS/GNSS, offline map, hotspot, SQLite logging, dashcam retrieval

Squashed rewrite of the project revival work (replaces prior history on this
branch to remove personal data that had been accidentally committed - real
LAN IP, GPS coordinates, home WiFi SSID, and phone model). See PLAN.md for
full phase-by-phase details. Summary:

- Phase 2: adapt app_obd.py for a USB OBD-II adapter (was Bluetooth-only),
  auto-detect /dev/ttyUSB*, fix several pre-existing crash bugs.
- Phase 3/4: make the web UI work fully offline (vendored Socket.IO/Leaflet/
  Chart.js/FontAwesome, offline map tile downloader script, live GPS marker
  tracking fix).
- Phase 5: systemd deployment/autostart on the Pi.
- Phase 6: WiFi hotspot mode via NetworkManager (auto-switch between home
  WiFi and a local AP for in-car web UI access), mDNS hostname + port 80
  redirect so the UI is reachable at a plain hostname with no port number.
- Phase 7: fix app_db.py/app_sql.py bugs, wire up local SQLite trip logging.
- Phase 8: brand-agnostic USB dashcam file retrieval (lsblk transport-based
  detection, works with any dashcam brand/folder layout).
- Rewritten readme.md with a proper project overview.

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-08 18:01:35 -04:00
co-authored by Copilot
parent f91a3d479d
commit 261ef3f4db
46 changed files with 2150 additions and 255 deletions
+119 -22
View File
@@ -1,22 +1,119 @@
from modules.app_events import socketio
from threading import Thread
class GPS:
def __init__(self):
self.LocationX = None
self.LocationY= None
@socketio.on('gps')
def get_gps():
global LocationX
global LocationY
LocationX = "45.5202844"
LocationY = "-73.5493208"
Location = [LocationX, LocationY]
socketio.emit('gps_location', Location)
print("websocket should be sent now")
return print("socket.io emitted")
from modules.app_events import socketio
from threading import Thread, Lock
import serial
import time
# SIM868 GNSS/GSM/GPRS HAT is wired to the Pi's GPIO UART (jumper set to "B: Pi-SIM868"),
# exposed as /dev/serial0 once the Pi's onboard Bluetooth has been disabled/freed.
SERIAL_PORT = "/dev/serial0"
BAUDRATE = 115200
POLL_INTERVAL = 2 # seconds between AT+CGNSINF polls
LocationX = None
LocationY = None
_serial_lock = Lock()
_serial = None
_gps_thread = None
class GPS:
def __init__(self):
self.LocationX = None
self.LocationY = None
def _get_serial():
global _serial
if _serial is None or not _serial.is_open:
_serial = serial.Serial(SERIAL_PORT, BAUDRATE, timeout=1)
time.sleep(0.3)
_serial.reset_input_buffer()
return _serial
def _send_at(cmd, wait=0.5):
"""Send an AT command to the SIM868 module and return the raw response."""
ser = _get_serial()
ser.write((cmd + "\r\n").encode())
time.sleep(wait)
return ser.read(1000).decode(errors="replace")
def _ensure_gnss_powered():
resp = _send_at("AT+CGNSPWR?")
if "+CGNSPWR: 0" in resp:
print("GNSS was off, powering it on...")
_send_at("AT+CGNSPWR=1", wait=1)
def _parse_cgnsinf(resp):
"""Parse a +CGNSINF response line into a dict, or None if there's no fix yet.
Format: +CGNSINF: run,fix,utc,lat,lon,alt,speed,course,fixmode,reserved1,
hdop,pdop,vdop,reserved2,gps_sat_view,gnss_sat_used,gnss_sat_view,cn0,hpa,vpa
"""
for line in resp.splitlines():
line = line.strip()
if line.startswith("+CGNSINF:"):
fields = line.split(":", 1)[1].strip().split(",")
if len(fields) < 8 or fields[1] != "1":
return None
lat, lon = fields[3], fields[4]
if not lat or not lon:
return None
return {
"lat": lat,
"lon": lon,
"alt": fields[5],
"speed": fields[6],
"course": fields[7],
}
return None
def _poll_loop():
global LocationX, LocationY
try:
with _serial_lock:
_ensure_gnss_powered()
except Exception as e:
print("Failed to initialize GNSS:", str(e))
while True:
try:
with _serial_lock:
resp = _send_at("AT+CGNSINF")
fix = _parse_cgnsinf(resp)
except Exception as e:
print("GPS read error:", str(e))
fix = None
if fix:
LocationX = fix["lat"]
LocationY = fix["lon"]
socketio.emit("gps_location", [LocationX, LocationY])
time.sleep(POLL_INTERVAL)
def start_gps():
"""Start the background thread that continuously polls the SIM868 for a GNSS fix."""
global _gps_thread
if _gps_thread is None or not _gps_thread.is_alive():
_gps_thread = Thread(target=_poll_loop)
_gps_thread.daemon = True
_gps_thread.start()
print(f"GPS thread started, alive: {_gps_thread.is_alive()}")
else:
print(f"GPS thread already running, alive: {_gps_thread.is_alive()}")
return GPS()
@socketio.on("gps")
def get_gps():
"""Return the last known GPS location and emit it over the 'gps_location' event."""
Location = [LocationX, LocationY]
socketio.emit("gps_location", Location)
print("gps_location emitted:", Location)
return Location