655 lines
24 KiB
Python
655 lines
24 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
|
|
from modules import app_trip
|
|
|
|
# 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
|
|
IDLE_LOG_INTERVAL = 60
|
|
GPS_LOG_DISTANCE_METERS = 8
|
|
TRIP_REFRESH_INTERVAL = 30
|
|
TRIP_REBUILD_OVERLAP_ROWS = 120
|
|
TRIP_BUILD_VERSION = 2
|
|
|
|
_conn = None
|
|
_conn_lock = Lock()
|
|
_log_thread = None
|
|
_last_trip_refresh = 0
|
|
|
|
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
|
|
);"""
|
|
|
|
SQL_CREATE_TRIPS_TABLE = """CREATE TABLE IF NOT EXISTS trips (
|
|
id integer PRIMARY KEY,
|
|
START_TRACKING_ID integer NOT NULL,
|
|
END_TRACKING_ID integer NOT NULL,
|
|
START_DATETIME datetime NOT NULL,
|
|
END_DATETIME datetime NOT NULL,
|
|
START_GPSX float NOT NULL,
|
|
START_GPSY float NOT NULL,
|
|
END_GPSX float NOT NULL,
|
|
END_GPSY float NOT NULL,
|
|
DISTANCE_KM float NOT NULL,
|
|
DURATION_SECONDS integer NOT NULL,
|
|
POINT_COUNT integer NOT NULL,
|
|
AVERAGE_SPEED_KMH float NOT NULL DEFAULT 0,
|
|
MAX_SPEED_KMH float NOT NULL DEFAULT 0
|
|
);"""
|
|
|
|
SQL_CREATE_TRIP_BUILD_STATE_TABLE = """CREATE TABLE IF NOT EXISTS trip_build_state (
|
|
id integer PRIMARY KEY CHECK (id = 1),
|
|
LAST_TRACKING_ID integer NOT NULL,
|
|
BUILD_VERSION integer NOT NULL DEFAULT 1
|
|
);"""
|
|
|
|
SQL_CREATE_ALERT_STATE_TABLE = """CREATE TABLE IF NOT EXISTS alert_state (
|
|
ALERT_KEY text PRIMARY KEY,
|
|
ACTIVE integer NOT NULL DEFAULT 0,
|
|
ACTIVE_SINCE real,
|
|
LAST_ATTEMPT_AT real,
|
|
LAST_SENT_AT real,
|
|
RECOVERY_PENDING integer NOT NULL DEFAULT 0,
|
|
LAST_RECOVERY_AT real,
|
|
LAST_ERROR text,
|
|
LAST_VALUE 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)
|
|
create_table(_conn, SQL_CREATE_SETTINGS_TABLE)
|
|
create_table(_conn, SQL_CREATE_DASHCAM_UPLOADS_TABLE)
|
|
create_table(_conn, SQL_CREATE_DASHCAM_HASH_CACHE_TABLE)
|
|
create_table(_conn, SQL_CREATE_TRIPS_TABLE)
|
|
create_table(_conn, SQL_CREATE_TRIP_BUILD_STATE_TABLE)
|
|
create_table(_conn, SQL_CREATE_ALERT_STATE_TABLE)
|
|
_migrate_tracking_table(_conn)
|
|
_migrate_trips_table(_conn)
|
|
_migrate_trip_build_state_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 _migrate_trips_table(conn):
|
|
existing_columns = {
|
|
row[1] for row in conn.execute("PRAGMA table_info(trips)").fetchall()
|
|
}
|
|
additions = {
|
|
"AVERAGE_SPEED_KMH": "float NOT NULL DEFAULT 0",
|
|
"MAX_SPEED_KMH": "float NOT NULL DEFAULT 0",
|
|
}
|
|
for name, definition in additions.items():
|
|
if name not in existing_columns:
|
|
conn.execute(f"ALTER TABLE trips ADD COLUMN {name} {definition}")
|
|
conn.commit()
|
|
|
|
|
|
def _migrate_trip_build_state_table(conn):
|
|
existing_columns = {
|
|
row[1] for row in conn.execute("PRAGMA table_info(trip_build_state)").fetchall()
|
|
}
|
|
if "BUILD_VERSION" not in existing_columns:
|
|
conn.execute(
|
|
"ALTER TABLE trip_build_state "
|
|
"ADD COLUMN BUILD_VERSION integer NOT NULL DEFAULT 1"
|
|
)
|
|
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 False
|
|
|
|
sql = """INSERT INTO tracking (VN, NAME, DATETIME, GPSX, GPSY, SPEED, ODOMETER)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)"""
|
|
with _conn_lock:
|
|
try:
|
|
latitude = float(str(gps_x).replace("\x00", "").strip())
|
|
longitude = float(str(gps_y).replace("\x00", "").strip())
|
|
conn.execute(
|
|
sql,
|
|
(car.model, account.name, datetime.now(), latitude, longitude, speed, odometer),
|
|
)
|
|
conn.commit()
|
|
return True
|
|
except (Error, TypeError, ValueError) as e:
|
|
print("Failed to insert tracking row:", e)
|
|
return False
|
|
|
|
|
|
def should_log_tracking(position, speed, last_position, seconds_since_log):
|
|
if last_position is None:
|
|
return True
|
|
if speed is not None and speed > 1:
|
|
return True
|
|
distance_meters = app_trip.haversine_km(last_position, position) * 1000
|
|
return (
|
|
distance_meters >= GPS_LOG_DISTANCE_METERS
|
|
or seconds_since_log >= IDLE_LOG_INTERVAL
|
|
)
|
|
|
|
|
|
def refresh_trips(force=False):
|
|
"""Rebuild the derived trip index when new raw tracking rows are available."""
|
|
global _last_trip_refresh
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return 0
|
|
with _conn_lock:
|
|
last_tracking_id = conn.execute(
|
|
"SELECT COALESCE(MAX(id), 0) FROM tracking"
|
|
).fetchone()[0]
|
|
state = conn.execute(
|
|
"SELECT LAST_TRACKING_ID, BUILD_VERSION "
|
|
"FROM trip_build_state WHERE id = 1"
|
|
).fetchone()
|
|
recently_refreshed = (
|
|
_last_trip_refresh
|
|
and time.monotonic() - _last_trip_refresh < TRIP_REFRESH_INTERVAL
|
|
)
|
|
if (
|
|
not force
|
|
and state is not None
|
|
and state[1] == TRIP_BUILD_VERSION
|
|
and (state[0] == last_tracking_id or recently_refreshed)
|
|
):
|
|
return conn.execute("SELECT COUNT(*) FROM trips").fetchone()[0]
|
|
|
|
rebuild_start_id = 1
|
|
if not force and state is not None and state[1] == TRIP_BUILD_VERSION:
|
|
rebuild_start_id = max(1, state[0] - TRIP_REBUILD_OVERLAP_ROWS)
|
|
intersecting_trip = conn.execute(
|
|
"""SELECT MIN(START_TRACKING_ID) FROM trips
|
|
WHERE END_TRACKING_ID >= ?""",
|
|
(rebuild_start_id,),
|
|
).fetchone()
|
|
if intersecting_trip[0] is not None:
|
|
rebuild_start_id = min(rebuild_start_id, intersecting_trip[0])
|
|
rows = conn.execute(
|
|
"""SELECT id, DATETIME, GPSX, GPSY, SPEED
|
|
FROM tracking WHERE id >= ? ORDER BY id ASC""",
|
|
(rebuild_start_id,),
|
|
).fetchall()
|
|
|
|
trips = app_trip.build_trip_summaries(rows)
|
|
with _conn_lock:
|
|
try:
|
|
conn.execute(
|
|
"DELETE FROM trips WHERE END_TRACKING_ID >= ?",
|
|
(rebuild_start_id,),
|
|
)
|
|
conn.executemany(
|
|
"""INSERT INTO trips (
|
|
id, START_TRACKING_ID, END_TRACKING_ID,
|
|
START_DATETIME, END_DATETIME,
|
|
START_GPSX, START_GPSY, END_GPSX, END_GPSY,
|
|
DISTANCE_KM, DURATION_SECONDS, POINT_COUNT,
|
|
AVERAGE_SPEED_KMH, MAX_SPEED_KMH
|
|
) VALUES (
|
|
:id, :start_tracking_id, :end_tracking_id,
|
|
:start_datetime, :end_datetime,
|
|
:start_gpsx, :start_gpsy, :end_gpsx, :end_gpsy,
|
|
:distance_km, :duration_seconds, :point_count,
|
|
:average_speed_kmh, :max_speed_kmh
|
|
)""",
|
|
trips,
|
|
)
|
|
conn.execute(
|
|
"""INSERT INTO trip_build_state (
|
|
id, LAST_TRACKING_ID, BUILD_VERSION
|
|
) VALUES (1, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE
|
|
SET LAST_TRACKING_ID = excluded.LAST_TRACKING_ID,
|
|
BUILD_VERSION = excluded.BUILD_VERSION""",
|
|
(last_tracking_id, TRIP_BUILD_VERSION),
|
|
)
|
|
conn.commit()
|
|
_last_trip_refresh = time.monotonic()
|
|
except Error:
|
|
conn.rollback()
|
|
raise
|
|
with _conn_lock:
|
|
return conn.execute("SELECT COUNT(*) FROM trips").fetchone()[0]
|
|
|
|
|
|
def list_trips():
|
|
refresh_trips()
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return []
|
|
with _conn_lock:
|
|
rows = conn.execute(
|
|
"""SELECT id, START_DATETIME, END_DATETIME,
|
|
START_GPSX, START_GPSY, END_GPSX, END_GPSY,
|
|
DISTANCE_KM, DURATION_SECONDS, POINT_COUNT,
|
|
AVERAGE_SPEED_KMH, MAX_SPEED_KMH
|
|
FROM trips ORDER BY START_DATETIME DESC"""
|
|
).fetchall()
|
|
return [{
|
|
"id": row[0],
|
|
"start_datetime": row[1],
|
|
"end_datetime": row[2],
|
|
"start": {"latitude": row[3], "longitude": row[4]},
|
|
"end": {"latitude": row[5], "longitude": row[6]},
|
|
"distance_km": row[7],
|
|
"duration_seconds": row[8],
|
|
"point_count": row[9],
|
|
"average_speed_kmh": row[10],
|
|
"max_speed_kmh": row[11],
|
|
} for row in rows]
|
|
|
|
|
|
def get_trip_route(trip_id, max_points=2000):
|
|
refresh_trips()
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return None
|
|
with _conn_lock:
|
|
trip = conn.execute(
|
|
"""SELECT id, START_DATETIME, END_DATETIME, DISTANCE_KM,
|
|
DURATION_SECONDS, POINT_COUNT,
|
|
AVERAGE_SPEED_KMH, MAX_SPEED_KMH,
|
|
START_TRACKING_ID, END_TRACKING_ID
|
|
FROM trips WHERE id = ?""",
|
|
(trip_id,),
|
|
).fetchone()
|
|
if trip is None:
|
|
return None
|
|
rows = conn.execute(
|
|
"""SELECT id, DATETIME, GPSX, GPSY, SPEED
|
|
FROM tracking WHERE id BETWEEN ? AND ?
|
|
ORDER BY DATETIME ASC, id ASC""",
|
|
(trip[8], trip[9]),
|
|
).fetchall()
|
|
return {
|
|
"trip": {
|
|
"id": trip[0],
|
|
"start_datetime": trip[1],
|
|
"end_datetime": trip[2],
|
|
"distance_km": trip[3],
|
|
"duration_seconds": trip[4],
|
|
"point_count": trip[5],
|
|
"average_speed_kmh": trip[6],
|
|
"max_speed_kmh": trip[7],
|
|
},
|
|
"points": app_trip.route_points(rows, max_points=max_points),
|
|
}
|
|
|
|
|
|
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_alert_states():
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return {}
|
|
with _conn_lock:
|
|
rows = conn.execute(
|
|
"""SELECT ALERT_KEY, ACTIVE, ACTIVE_SINCE, LAST_ATTEMPT_AT,
|
|
LAST_SENT_AT, RECOVERY_PENDING, LAST_RECOVERY_AT,
|
|
LAST_ERROR, LAST_VALUE
|
|
FROM alert_state"""
|
|
).fetchall()
|
|
return {
|
|
row[0]: {
|
|
"active": bool(row[1]),
|
|
"active_since": row[2],
|
|
"last_attempt_at": row[3],
|
|
"last_sent_at": row[4],
|
|
"recovery_pending": bool(row[5]),
|
|
"last_recovery_at": row[6],
|
|
"last_error": row[7],
|
|
"last_value": row[8],
|
|
}
|
|
for row in rows
|
|
}
|
|
|
|
|
|
def save_alert_state(alert_key, state):
|
|
conn = get_conn()
|
|
if conn is None:
|
|
raise RuntimeError("SQLite connection unavailable")
|
|
with _conn_lock:
|
|
conn.execute(
|
|
"""INSERT INTO alert_state (
|
|
ALERT_KEY, ACTIVE, ACTIVE_SINCE, LAST_ATTEMPT_AT,
|
|
LAST_SENT_AT, RECOVERY_PENDING, LAST_RECOVERY_AT,
|
|
LAST_ERROR, LAST_VALUE
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(ALERT_KEY) DO UPDATE SET
|
|
ACTIVE = excluded.ACTIVE,
|
|
ACTIVE_SINCE = excluded.ACTIVE_SINCE,
|
|
LAST_ATTEMPT_AT = excluded.LAST_ATTEMPT_AT,
|
|
LAST_SENT_AT = excluded.LAST_SENT_AT,
|
|
RECOVERY_PENDING = excluded.RECOVERY_PENDING,
|
|
LAST_RECOVERY_AT = excluded.LAST_RECOVERY_AT,
|
|
LAST_ERROR = excluded.LAST_ERROR,
|
|
LAST_VALUE = excluded.LAST_VALUE""",
|
|
(
|
|
alert_key,
|
|
1 if state["active"] else 0,
|
|
state["active_since"],
|
|
state["last_attempt_at"],
|
|
state["last_sent_at"],
|
|
1 if state["recovery_pending"] else 0,
|
|
state["last_recovery_at"],
|
|
state["last_error"],
|
|
state["last_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 dashcam_file_uploaded_for_stat(relative_path, size, mtime):
|
|
"""Whether the current local file version is confirmed uploaded.
|
|
|
|
Uses the existing hash cache so list/retention scans never rehash large
|
|
video files merely to display their status.
|
|
"""
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return False
|
|
with _conn_lock:
|
|
cur = conn.execute(
|
|
"""SELECT 1
|
|
FROM dashcam_hash_cache AS cache
|
|
JOIN dashcam_uploads AS uploads
|
|
ON uploads.RELATIVE_PATH = cache.RELATIVE_PATH
|
|
AND uploads.SHA256 = cache.SHA256
|
|
WHERE cache.RELATIVE_PATH = ?
|
|
AND cache.SIZE = ?
|
|
AND cache.MTIME = ?""",
|
|
(relative_path, size, mtime),
|
|
)
|
|
return cur.fetchone() is not None
|
|
|
|
|
|
def uploaded_dashcam_file_stats():
|
|
"""Return exact uploaded file versions as (path, size, mtime) tuples."""
|
|
conn = get_conn()
|
|
if conn is None:
|
|
return set()
|
|
with _conn_lock:
|
|
cur = conn.execute(
|
|
"""SELECT cache.RELATIVE_PATH, cache.SIZE, cache.MTIME
|
|
FROM dashcam_hash_cache AS cache
|
|
JOIN dashcam_uploads AS uploads
|
|
ON uploads.RELATIVE_PATH = cache.RELATIVE_PATH
|
|
AND uploads.SHA256 = cache.SHA256"""
|
|
)
|
|
return set(cur.fetchall())
|
|
|
|
|
|
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
|
|
|
|
last_position = None
|
|
last_logged_at = None
|
|
while True:
|
|
gps_x = app_gps.LocationX
|
|
gps_y = app_gps.LocationY
|
|
speed = app_obd.get_fresh_speed()
|
|
|
|
if gps_x is not None and gps_y is not None:
|
|
try:
|
|
position = (
|
|
float(str(gps_x).replace("\x00", "").strip()),
|
|
float(str(gps_y).replace("\x00", "").strip()),
|
|
)
|
|
except (TypeError, ValueError) as error:
|
|
print("Skipping invalid GPS tracking snapshot:", error)
|
|
else:
|
|
now = time.monotonic()
|
|
seconds_since_log = (
|
|
now - last_logged_at
|
|
if last_logged_at is not None
|
|
else IDLE_LOG_INTERVAL
|
|
)
|
|
if should_log_tracking(
|
|
position, speed, last_position, seconds_since_log
|
|
) and insert_tracking(position[0], position[1], speed):
|
|
last_position = position
|
|
last_logged_at = now
|
|
|
|
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()
|