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]>
137 lines
4.5 KiB
Python
137 lines
4.5 KiB
Python
import sqlite3
|
|
import os
|
|
import time
|
|
from datetime import datetime
|
|
from sqlite3 import Error
|
|
from threading import Thread, Lock
|
|
|
|
from modules import app_car, app_account, app_gps
|
|
|
|
# Resolve db/sql/ relative to the repo root regardless of the process's cwd
|
|
# (previously a relative "../db/sql/" path, which broke depending on how/from
|
|
# where app.py was launched).
|
|
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
_SQL_DIR = os.path.join(_BASE_DIR, "db", "sql")
|
|
|
|
account = app_account.Account()
|
|
car = app_car.Car()
|
|
sCarModel = ''.join(letter for letter in car.model if letter.isalnum())
|
|
sql_file = account.name + "_" + sCarModel + ".db"
|
|
sql_path = os.path.join(_SQL_DIR, sql_file)
|
|
|
|
LOG_INTERVAL = 5 # seconds between trip-tracking snapshots
|
|
|
|
_conn = None
|
|
_conn_lock = Lock()
|
|
_log_thread = None
|
|
|
|
SQL_CREATE_TRACKING_TABLE = """CREATE TABLE IF NOT EXISTS tracking (
|
|
id integer PRIMARY KEY,
|
|
VN text NOT NULL,
|
|
NAME text NOT NULL,
|
|
DATETIME datetime,
|
|
GPSX float,
|
|
GPSY float,
|
|
SPEED float,
|
|
ODOMETER float
|
|
);"""
|
|
|
|
SQL_CREATE_DTC_TABLE = """CREATE TABLE IF NOT EXISTS dtc (
|
|
id integer PRIMARY KEY,
|
|
NAME text NOT NULL,
|
|
STATUS text,
|
|
DTC_ID text
|
|
);"""
|
|
|
|
|
|
def create_conn(db_file):
|
|
"""Open a SQLite connection to db_file, creating its parent folder if needed.
|
|
|
|
Returns the open connection (or None on failure) - previously this closed
|
|
the connection immediately in a `finally` block and returned nothing,
|
|
so every caller always got a dead `None` connection.
|
|
"""
|
|
os.makedirs(os.path.dirname(db_file), exist_ok=True)
|
|
conn = None
|
|
try:
|
|
conn = sqlite3.connect(db_file, check_same_thread=False)
|
|
print("SQLite library version:", sqlite3.sqlite_version)
|
|
except Error as e:
|
|
print(e)
|
|
return conn
|
|
|
|
|
|
def create_table(conn, create_table_sql):
|
|
""" create a table from the create_table_sql statement
|
|
:param conn: Connection object
|
|
:param create_table_sql: a CREATE TABLE statement
|
|
:return:
|
|
"""
|
|
try:
|
|
c = conn.cursor()
|
|
c.execute(create_table_sql)
|
|
except Error as e:
|
|
print(e)
|
|
|
|
|
|
def get_conn():
|
|
"""Return the shared SQLite connection, opening + initializing tables on first use."""
|
|
global _conn
|
|
if _conn is None:
|
|
_conn = create_conn(sql_path)
|
|
if _conn is not None:
|
|
create_table(_conn, SQL_CREATE_TRACKING_TABLE)
|
|
create_table(_conn, SQL_CREATE_DTC_TABLE)
|
|
else:
|
|
print("Error! cannot create the database connection.")
|
|
return _conn
|
|
|
|
|
|
def insert_tracking(gps_x, gps_y, speed, odometer=None):
|
|
"""Insert a single trip-tracking snapshot row (GPS position + speed)."""
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return
|
|
|
|
sql = """INSERT INTO tracking (VN, NAME, DATETIME, GPSX, GPSY, SPEED, ODOMETER)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)"""
|
|
with _conn_lock:
|
|
try:
|
|
conn.execute(sql, (car.model, account.name, datetime.now(), gps_x, gps_y, speed, odometer))
|
|
conn.commit()
|
|
except Error as e:
|
|
print("Failed to insert tracking row:", e)
|
|
|
|
|
|
def _log_loop():
|
|
# Deferred import to avoid a circular import at module load time
|
|
# (app_obd.py imports app_sql, and app_sql needs app_obd.speed here).
|
|
from modules import app_obd
|
|
|
|
while True:
|
|
gps_x = app_gps.LocationX
|
|
gps_y = app_gps.LocationY
|
|
speed = app_obd.speed
|
|
|
|
if gps_x is not None and gps_y is not None:
|
|
insert_tracking(gps_x, gps_y, speed)
|
|
|
|
time.sleep(LOG_INTERVAL)
|
|
|
|
|
|
def start_logging():
|
|
"""Start the background thread that periodically snapshots GPS+speed into SQLite."""
|
|
global _log_thread
|
|
if _log_thread is None or not _log_thread.is_alive():
|
|
get_conn() # make sure the db file/tables exist before the first insert
|
|
_log_thread = Thread(target=_log_loop)
|
|
_log_thread.daemon = True
|
|
_log_thread.start()
|
|
print(f"SQL tracking-logger thread started, alive: {_log_thread.is_alive()}")
|
|
else:
|
|
print(f"SQL tracking-logger thread already running, alive: {_log_thread.is_alive()}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
get_conn()
|