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, UPLOADED integer DEFAULT 0 );""" SQL_CREATE_DTC_TABLE = """CREATE TABLE IF NOT EXISTS dtc ( id integer PRIMARY KEY, NAME text NOT NULL, STATUS text, DTC_ID text );""" SQL_CREATE_SETTINGS_TABLE = """CREATE TABLE IF NOT EXISTS settings ( key text PRIMARY KEY, value text );""" SQL_CREATE_DASHCAM_UPLOADS_TABLE = """CREATE TABLE IF NOT EXISTS dashcam_uploads ( id integer PRIMARY KEY, RELATIVE_PATH text NOT NULL, SHA256 text NOT NULL, UPLOADED_AT datetime, UNIQUE(RELATIVE_PATH, SHA256) );""" # Cache of already-computed sha256 hashes, keyed by (path, size, mtime) so a # file's hash never needs recomputing once copied from the dashcam (its size # and mtime won't change afterward). Avoids re-hashing the entire, possibly # 20GB+, local backlog on every background-upload loop iteration and every # "pending files" list request from the phone-relay feature. SQL_CREATE_DASHCAM_HASH_CACHE_TABLE = """CREATE TABLE IF NOT EXISTS dashcam_hash_cache ( RELATIVE_PATH text PRIMARY KEY, SIZE integer NOT NULL, MTIME real NOT NULL, SHA256 text NOT NULL );""" 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) create_table(_conn, SQL_CREATE_SETTINGS_TABLE) create_table(_conn, SQL_CREATE_DASHCAM_UPLOADS_TABLE) create_table(_conn, SQL_CREATE_DASHCAM_HASH_CACHE_TABLE) _migrate_tracking_table(_conn) else: print("Error! cannot create the database connection.") return _conn def _migrate_tracking_table(conn): """Add the UPLOADED column to a pre-existing tracking table that predates the remote-upload feature (SQLite has no "ADD COLUMN IF NOT EXISTS").""" existing_columns = {row[1] for row in conn.execute("PRAGMA table_info(tracking)").fetchall()} if "UPLOADED" not in existing_columns: conn.execute("ALTER TABLE tracking ADD COLUMN UPLOADED integer DEFAULT 0") conn.commit() 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 get_setting(key, default=None): """Read a persisted UI/app setting (e.g. the dashcam-upload toggle).""" conn = get_conn() if conn is None: return default with _conn_lock: cur = conn.execute("SELECT value FROM settings WHERE key = ?", (key,)) row = cur.fetchone() return row[0] if row is not None else default def set_setting(key, value): conn = get_conn() if conn is None: return with _conn_lock: conn.execute( "INSERT INTO settings (key, value) VALUES (?, ?) " "ON CONFLICT(key) DO UPDATE SET value = excluded.value", (key, value), ) conn.commit() def get_unsent_tracking_rows(limit=100): """Return up to `limit` tracking rows not yet uploaded to the remote server, oldest first, as a list of dicts ready to be sent as JSON.""" conn = get_conn() if conn is None: return [] with _conn_lock: cur = conn.execute( """SELECT id, VN, NAME, DATETIME, GPSX, GPSY, SPEED, ODOMETER FROM tracking WHERE UPLOADED = 0 ORDER BY id ASC LIMIT ?""", (limit,), ) rows = cur.fetchall() return [ { "id": r[0], "vn": r[1], "name": r[2], "datetime": r[3], "gpsx": r[4], "gpsy": r[5], "speed": r[6], "odometer": r[7], } for r in rows ] def count_unsent_tracking_rows(): conn = get_conn() if conn is None: return 0 with _conn_lock: cur = conn.execute("SELECT COUNT(*) FROM tracking WHERE UPLOADED = 0") return cur.fetchone()[0] def mark_tracking_rows_uploaded(ids): """Flip UPLOADED=1 for the given row ids after a successful server ack.""" if not ids: return conn = get_conn() if conn is None: return with _conn_lock: conn.executemany("UPDATE tracking SET UPLOADED = 1 WHERE id = ?", [(i,) for i in ids]) conn.commit() def dashcam_file_marked_uploaded(relative_path, sha256): """Whether this exact (path, content) was already confirmed uploaded.""" conn = get_conn() if conn is None: return False with _conn_lock: cur = conn.execute( "SELECT 1 FROM dashcam_uploads WHERE RELATIVE_PATH = ? AND SHA256 = ?", (relative_path, sha256), ) return cur.fetchone() is not None def mark_dashcam_file_uploaded(relative_path, sha256): conn = get_conn() if conn is None: return with _conn_lock: conn.execute( "INSERT OR IGNORE INTO dashcam_uploads (RELATIVE_PATH, SHA256, UPLOADED_AT) VALUES (?, ?, ?)", (relative_path, sha256, datetime.now()), ) conn.commit() def get_cached_dashcam_hash(relative_path, size, mtime): """Return a previously-computed sha256 for this exact (path, size, mtime), or None if not cached / the file has changed since it was last hashed.""" conn = get_conn() if conn is None: return None with _conn_lock: cur = conn.execute( "SELECT SHA256 FROM dashcam_hash_cache WHERE RELATIVE_PATH = ? AND SIZE = ? AND MTIME = ?", (relative_path, size, mtime), ) row = cur.fetchone() return row[0] if row is not None else None def set_cached_dashcam_hash(relative_path, size, mtime, sha256): conn = get_conn() if conn is None: return with _conn_lock: conn.execute( "INSERT INTO dashcam_hash_cache (RELATIVE_PATH, SIZE, MTIME, SHA256) VALUES (?, ?, ?, ?) " "ON CONFLICT(RELATIVE_PATH) DO UPDATE SET SIZE = excluded.SIZE, MTIME = excluded.MTIME, SHA256 = excluded.SHA256", (relative_path, size, mtime, sha256), ) conn.commit() 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()