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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+217
-38
@@ -1,38 +1,217 @@
|
||||
import os
|
||||
import shutil
|
||||
from tqdm import tqdm
|
||||
|
||||
def mount_device(device, mount_point):
|
||||
os.system(f"sudo mount {device} {mount_point}")
|
||||
|
||||
def unmount_device(mount_point):
|
||||
os.system(f"sudo umount {mount_point}")
|
||||
|
||||
def copy_files(source_folder, destination_folder):
|
||||
if not os.path.exists(destination_folder):
|
||||
os.makedirs(destination_folder)
|
||||
|
||||
for root, dirs, files in os.walk(source_folder):
|
||||
for file in tqdm(files, desc="Copying files", unit="file"):
|
||||
source_path = os.path.join(root, file)
|
||||
destination_path = os.path.join(destination_folder, file)
|
||||
|
||||
# Check if the file already exists in the destination folder
|
||||
if not os.path.exists(destination_path):
|
||||
shutil.copy2(source_path, destination_folder)
|
||||
|
||||
if __name__ == "__main__":
|
||||
source_device = "/dev/sda"
|
||||
mount_point = "/mnt/dash"
|
||||
destination_folder = "~/yaris-obd/dash/videos"
|
||||
|
||||
try:
|
||||
# Mount the device
|
||||
mount_device(source_device, mount_point)
|
||||
|
||||
# Copy files from source to destination
|
||||
copy_files(os.path.join(mount_point, "DCIM"), os.path.expanduser(destination_folder))
|
||||
|
||||
finally:
|
||||
# Unmount the device (even if an exception occurs)
|
||||
unmount_device(mount_point)
|
||||
"""Dashcam USB retrieval.
|
||||
|
||||
Detects a dashcam plugged in as a USB mass-storage device (no hardcoded
|
||||
/dev/sda, no brand-specific assumptions), mounts it read-only, copies any
|
||||
video files it finds (any folder layout - DCIM/, MOVIE/, NORM/EVENT/PARKING/,
|
||||
etc. - all common dashcam brands are covered since we just walk the whole
|
||||
filesystem for video extensions instead of assuming a specific folder name),
|
||||
then unmounts it again.
|
||||
|
||||
Detection is done via `lsblk`, filtering for partitions whose parent disk is
|
||||
attached over USB (TRAN == "usb"). This is what actually distinguishes a
|
||||
plugged-in dashcam/USB stick from the Pi's own storage (the boot/root
|
||||
partitions are on the onboard SD card, transport "mmc", never "usb") - so
|
||||
it's safe to run against real hardware without risk of touching the Pi's own
|
||||
filesystem, regardless of which /dev/sdX letter the kernel happens to assign.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi", ".ts", ".mkv"}
|
||||
|
||||
# Filesystem types dashcams commonly ship formatted as (SD cards are almost
|
||||
# always FAT32/exFAT; a few older units use NTFS).
|
||||
DASHCAM_FS_TYPES = {"vfat", "exfat", "ntfs", "ntfs3"}
|
||||
|
||||
DEFAULT_MOUNT_POINT = "/mnt/dashcam"
|
||||
DEFAULT_DESTINATION = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dash", "videos"
|
||||
)
|
||||
|
||||
|
||||
def list_usb_storage_partitions():
|
||||
"""Return a list of dicts describing USB-attached storage partitions.
|
||||
|
||||
Each dict has: path, fstype, label, size, mountpoint. Only partitions
|
||||
whose parent disk reports TRAN "usb" are included - this is what makes
|
||||
the detection brand/device-agnostic (no hardcoded /dev/sda) while still
|
||||
being safe (never matches the Pi's own onboard mmc storage).
|
||||
"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["lsblk", "-J", "-o", "NAME,PATH,TRAN,FSTYPE,LABEL,SIZE,MOUNTPOINT,TYPE"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError) as e:
|
||||
print("Failed to run lsblk:", e)
|
||||
return []
|
||||
|
||||
try:
|
||||
data = json.loads(result.stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
print("Failed to parse lsblk output:", e)
|
||||
return []
|
||||
|
||||
partitions = []
|
||||
|
||||
def walk(devices, inherited_tran=None):
|
||||
for device in devices:
|
||||
tran = device.get("tran") or inherited_tran
|
||||
if device.get("type") == "part" and tran == "usb":
|
||||
partitions.append(
|
||||
{
|
||||
"path": device.get("path"),
|
||||
"fstype": device.get("fstype"),
|
||||
"label": device.get("label"),
|
||||
"size": device.get("size"),
|
||||
"mountpoint": device.get("mountpoint"),
|
||||
}
|
||||
)
|
||||
children = device.get("children") or []
|
||||
if children:
|
||||
walk(children, tran)
|
||||
|
||||
walk(data.get("blockdevices", []))
|
||||
return partitions
|
||||
|
||||
|
||||
def find_dashcam_partition():
|
||||
"""Pick the best candidate USB partition to import from, or None.
|
||||
|
||||
Prefers partitions with a recognized dashcam filesystem type; if several
|
||||
are found, the first one is returned (a single dashcam SD card should
|
||||
only expose one data partition).
|
||||
"""
|
||||
partitions = list_usb_storage_partitions()
|
||||
if not partitions:
|
||||
return None
|
||||
|
||||
recognized = [p for p in partitions if p["fstype"] in DASHCAM_FS_TYPES]
|
||||
return (recognized or partitions)[0]
|
||||
|
||||
|
||||
def mount_device(device_path, mount_point=DEFAULT_MOUNT_POINT):
|
||||
"""Mount device_path read-only at mount_point. Returns True on success."""
|
||||
os.makedirs(mount_point, exist_ok=True)
|
||||
try:
|
||||
subprocess.run(
|
||||
["sudo", "mount", "-o", "ro", device_path, mount_point],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Failed to mount {device_path}: {e.stderr.strip()}")
|
||||
return False
|
||||
|
||||
|
||||
def unmount_device(mount_point=DEFAULT_MOUNT_POINT):
|
||||
"""Unmount mount_point. Returns True on success (or if already unmounted)."""
|
||||
try:
|
||||
subprocess.run(
|
||||
["sudo", "umount", mount_point],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Not mounted is fine - nothing to clean up.
|
||||
if "not mounted" in e.stderr.lower():
|
||||
return True
|
||||
print(f"Failed to unmount {mount_point}: {e.stderr.strip()}")
|
||||
return False
|
||||
|
||||
|
||||
def find_video_files(source_folder):
|
||||
"""Walk source_folder, returning (absolute_path, relative_path) for every
|
||||
file whose extension looks like a video, regardless of folder layout."""
|
||||
found = []
|
||||
for root, _dirs, files in os.walk(source_folder):
|
||||
for file in files:
|
||||
if os.path.splitext(file)[1].lower() in VIDEO_EXTENSIONS:
|
||||
abs_path = os.path.join(root, file)
|
||||
rel_path = os.path.relpath(abs_path, source_folder)
|
||||
found.append((abs_path, rel_path))
|
||||
return found
|
||||
|
||||
|
||||
def copy_files(source_folder, destination_folder):
|
||||
"""Copy every video file found under source_folder into destination_folder,
|
||||
preserving its relative sub-folder path (dashcams often split files into
|
||||
date/event sub-folders - flattening them risks filename collisions).
|
||||
|
||||
Files already present at the destination (same relative path, same size)
|
||||
are skipped so re-running an import doesn't re-copy everything.
|
||||
|
||||
Returns a summary dict: {found, copied, skipped, errors}.
|
||||
"""
|
||||
os.makedirs(destination_folder, exist_ok=True)
|
||||
|
||||
videos = find_video_files(source_folder)
|
||||
copied = 0
|
||||
skipped = 0
|
||||
errors = []
|
||||
|
||||
for abs_path, rel_path in videos:
|
||||
dest_path = os.path.join(destination_folder, rel_path)
|
||||
|
||||
if os.path.exists(dest_path) and os.path.getsize(dest_path) == os.path.getsize(abs_path):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||
try:
|
||||
shutil.copy2(abs_path, dest_path)
|
||||
copied += 1
|
||||
except OSError as e:
|
||||
errors.append(f"{rel_path}: {e}")
|
||||
|
||||
return {"found": len(videos), "copied": copied, "skipped": skipped, "errors": errors}
|
||||
|
||||
|
||||
def import_dashcam(destination_folder=DEFAULT_DESTINATION, mount_point=DEFAULT_MOUNT_POINT):
|
||||
"""Full detect -> mount -> copy -> unmount pipeline.
|
||||
|
||||
Returns a summary dict describing what happened, always unmounting the
|
||||
device again afterwards (even if copying fails partway through).
|
||||
"""
|
||||
partition = find_dashcam_partition()
|
||||
if partition is None:
|
||||
return {"status": "no_device", "message": "No USB dashcam/storage device detected."}
|
||||
|
||||
device_path = partition["path"]
|
||||
|
||||
# Already mounted elsewhere (e.g. by the desktop environment) - read
|
||||
# straight from there instead of trying to mount it again ourselves.
|
||||
already_mounted_at = partition.get("mountpoint")
|
||||
|
||||
mounted_here = False
|
||||
try:
|
||||
source = already_mounted_at
|
||||
if not source:
|
||||
if not mount_device(device_path, mount_point):
|
||||
return {
|
||||
"status": "mount_failed",
|
||||
"device": device_path,
|
||||
"message": f"Failed to mount {device_path}.",
|
||||
}
|
||||
mounted_here = True
|
||||
source = mount_point
|
||||
|
||||
summary = copy_files(source, destination_folder)
|
||||
summary["status"] = "ok"
|
||||
summary["device"] = device_path
|
||||
return summary
|
||||
finally:
|
||||
if mounted_here:
|
||||
unmount_device(mount_point)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(import_dashcam())
|
||||
|
||||
+23
-15
@@ -2,22 +2,30 @@ from pymongo import MongoClient
|
||||
from dotenv import load_dotenv
|
||||
import os
|
||||
|
||||
load_dotenv("../db/creds.env")
|
||||
# Load creds.env from the repo's db/ folder regardless of the process's cwd.
|
||||
_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
load_dotenv(os.path.join(_BASE_DIR, "db", "creds.env"))
|
||||
|
||||
mUser = os.getenv()
|
||||
mPass = os.getenv()
|
||||
mDB = os.getenv()
|
||||
mUser = os.getenv("mangosrv_user")
|
||||
mPass = os.getenv("mangosrv_pass")
|
||||
mDB = os.getenv("mangosrv_db")
|
||||
|
||||
def get_database():
|
||||
|
||||
# Provide the mongodb atlas url to connect python to mongodb using pymongo
|
||||
CONNECTION_STRING = "mongodb+srv://"+ mUser + ":" + mPass + "@cluster.mongodb.net/" + mDB
|
||||
|
||||
# Create a connection using MongoClient. You can import MongoClient or use pymongo.MongoClient
|
||||
client = MongoClient(CONNECTION_STRING)
|
||||
|
||||
if not mUser or not mPass or not mDB:
|
||||
raise RuntimeError(
|
||||
"MongoDB credentials are missing. Set mangosrv_user, mangosrv_pass, "
|
||||
"and mangosrv_db in db/creds.env."
|
||||
)
|
||||
|
||||
# Provide the mongodb atlas url to connect python to mongodb using pymongo
|
||||
CONNECTION_STRING = "mongodb+srv://" + mUser + ":" + mPass + "@cluster.mongodb.net/" + mDB
|
||||
|
||||
# Create a connection using MongoClient. You can import MongoClient or use pymongo.MongoClient
|
||||
client = MongoClient(CONNECTION_STRING)
|
||||
return client[mDB]
|
||||
|
||||
# This is added so that many files can reuse the function get_database()
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Get the database
|
||||
dbname = get_database()
|
||||
if __name__ == "__main__":
|
||||
|
||||
# Get the database
|
||||
dbname = get_database()
|
||||
+10
-3
@@ -5,6 +5,7 @@ import time, threading
|
||||
|
||||
socketio = SocketIO()
|
||||
uptime_lock = threading.Lock()
|
||||
thread_uptime = None
|
||||
|
||||
@socketio.on('connect')
|
||||
def connect_event():
|
||||
@@ -14,9 +15,15 @@ def connect_event():
|
||||
uptime()
|
||||
|
||||
def uptime():
|
||||
thread = Thread(target=loop_uptime)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
global thread_uptime
|
||||
|
||||
if thread_uptime is None or not thread_uptime.is_alive():
|
||||
thread_uptime = Thread(target=loop_uptime)
|
||||
thread_uptime.daemon = True
|
||||
thread_uptime.start()
|
||||
print(f"Uptime thread started, alive: {thread_uptime.is_alive()}")
|
||||
else:
|
||||
print(f"Uptime thread already running, alive: {thread_uptime.is_alive()}")
|
||||
|
||||
def loop_uptime():
|
||||
uptime_lock.acquire()
|
||||
|
||||
+119
-22
@@ -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
|
||||
|
||||
+98
-60
@@ -1,7 +1,7 @@
|
||||
from modules import app_gps, app_sql, app_events
|
||||
import obd, time, threading
|
||||
import glob, obd, os, time, threading
|
||||
from datetime import datetime
|
||||
from threading import Thread
|
||||
from threading import Event, Thread
|
||||
|
||||
status = None
|
||||
speed = 0
|
||||
@@ -16,88 +16,126 @@ class OBD:
|
||||
obd_status = ["Disconnected", "Connected", "Connection...", "wtf"]
|
||||
obd_speed_lock = threading.Lock()
|
||||
obd_rpm_lock = threading.Lock()
|
||||
thread_speed = None
|
||||
thread_rpm = None
|
||||
stop_speed = Event()
|
||||
stop_rpm = Event()
|
||||
|
||||
try:
|
||||
print(obd_status[2])
|
||||
connection = obd.OBD() # put linux /dev/rfi... something in () for the rpi bluetooth adpater
|
||||
connection = None
|
||||
|
||||
|
||||
def _find_obd_port():
|
||||
"""Locate the USB-serial OBD-II adapter.
|
||||
|
||||
Honors an OBD_PORT env var override; otherwise picks the first
|
||||
/dev/ttyUSB* device found (the PL2303 USB adapter shows up here).
|
||||
Returns None if nothing is found, letting python-obd fall back to its
|
||||
own auto-scan (which also covers /dev/rfcomm* for the old Bluetooth setup).
|
||||
"""
|
||||
env_port = os.environ.get("OBD_PORT")
|
||||
if env_port:
|
||||
return env_port
|
||||
ports = sorted(glob.glob("/dev/ttyUSB*"))
|
||||
return ports[0] if ports else None
|
||||
|
||||
|
||||
def connect_obd():
|
||||
"""(Re)connect to the OBD-II adapter. Safe to call repeatedly."""
|
||||
global connection
|
||||
port = _find_obd_port()
|
||||
try:
|
||||
print(obd_status[2])
|
||||
connection = obd.OBD(portstr=port) if port else obd.OBD()
|
||||
except Exception as e:
|
||||
print("An error occurred:", str(e))
|
||||
connection = None
|
||||
return connection
|
||||
|
||||
|
||||
connect_obd()
|
||||
|
||||
except Exception as e:
|
||||
print("An error occurred:", str(e))
|
||||
|
||||
def get_speedtime():
|
||||
global speed
|
||||
date = datetime.now()
|
||||
today = str(date.today())
|
||||
time_now = str(date.strftime("%H:%M:%S"))
|
||||
return [str(time_now), speed]
|
||||
time_now = str(datetime.now().strftime("%H:%M:%S"))
|
||||
return [time_now, speed]
|
||||
|
||||
def obd_connection():
|
||||
if connection.is_connected():
|
||||
|
||||
def obd_connection():
|
||||
global thread_speed, thread_rpm
|
||||
|
||||
if connection is None or not connection.is_connected():
|
||||
connect_obd()
|
||||
|
||||
if connection is not None and connection.is_connected():
|
||||
print(obd_status[1])
|
||||
app_events.socketio.emit('obd_status', obd_status[1])
|
||||
print("OBD connection established.")
|
||||
|
||||
if thread_speed is None or not thread_speed.is_alive():
|
||||
stop_speed.clear()
|
||||
thread_speed = Thread(target=obd_speed)
|
||||
thread_speed.daemon = True
|
||||
thread_speed.start()
|
||||
print(f'thread_speed alive? {thread_speed.is_alive()}')
|
||||
|
||||
if thread_rpm is None or not thread_rpm.is_alive():
|
||||
stop_rpm.clear()
|
||||
thread_rpm = Thread(target=obd_rpm)
|
||||
thread_rpm.daemon = True
|
||||
thread_rpm.start()
|
||||
print(f'thread_rpm alive? {thread_rpm.is_alive()}')
|
||||
else:
|
||||
print("OBD connection failed.")
|
||||
print(obd_status[0])
|
||||
|
||||
app_events.socketio.emit('obd_status', obd_status[0])
|
||||
app_events.socketio.emit('obd_speed', "-")
|
||||
app_events.socketio.emit('obm_error', "-")
|
||||
app_events.socketio.emit('obd_error', "-")
|
||||
app_events.socketio.emit('obd_rpm', "-")
|
||||
|
||||
# debug speed for testing
|
||||
thread_speed = Thread(target=obd_speed)
|
||||
thread_speed.daemon = True
|
||||
thread_speed.start()
|
||||
|
||||
# debug rpm for testing
|
||||
thread_rpm = Thread(target=obd_rpm)
|
||||
thread_rpm.daemon = True
|
||||
thread_rpm.start()
|
||||
|
||||
def obd_speed():
|
||||
obd_speed_lock.acquire()
|
||||
cmd = obd.commands.SPEED # select an OBD command (sensor)
|
||||
response = connection.query(cmd) # send the command and parse the response
|
||||
loop = True
|
||||
try:
|
||||
while loop == True:
|
||||
global speed
|
||||
speed = speed + 1
|
||||
time.sleep(10)
|
||||
global speed
|
||||
with obd_speed_lock:
|
||||
cmd = obd.commands.SPEED # select an OBD command (sensor)
|
||||
while not stop_speed.is_set():
|
||||
if connection is None or not connection.is_connected():
|
||||
print("OBD disconnected, stopping speed polling.")
|
||||
break
|
||||
|
||||
response = connection.query(cmd) # send the command and parse the response
|
||||
if response.is_null():
|
||||
print("No data received. #"+ str(get_speedtime()))
|
||||
print(app_gps.get_gps())
|
||||
app_events.socketio.emit('obd_speed', get_speedtime())
|
||||
print("No data received. #" + str(get_speedtime()))
|
||||
print(app_gps.get_gps())
|
||||
else:
|
||||
print("Original value:", response.value)
|
||||
print("Value in kph:", response.value.to("kph"))
|
||||
app_events.socketio.emit('obd_speed', response.value.to("kph"))
|
||||
finally:
|
||||
obd_speed_lock.release()
|
||||
|
||||
speed = round(response.value.to("kph").magnitude, 1)
|
||||
print("Speed:", speed, "kph")
|
||||
|
||||
app_events.socketio.emit('obd_speed', get_speedtime())
|
||||
time.sleep(2)
|
||||
|
||||
def obd_rpm():
|
||||
obd_speed_lock.acquire()
|
||||
cmd = obd.commands.RPM # select an OBD command (sensor)
|
||||
response = connection.query(cmd) # send the command and parse the response
|
||||
loop = True
|
||||
try:
|
||||
while loop == True:
|
||||
global rpm
|
||||
speed = speed + 1
|
||||
time.sleep(1)
|
||||
global rpm
|
||||
with obd_rpm_lock:
|
||||
cmd = obd.commands.RPM # select an OBD command (sensor)
|
||||
while not stop_rpm.is_set():
|
||||
if connection is None or not connection.is_connected():
|
||||
print("OBD disconnected, stopping rpm polling.")
|
||||
break
|
||||
|
||||
response = connection.query(cmd) # send the command and parse the response
|
||||
if response.is_null():
|
||||
print("No data received. #"+ str(get_speedtime()))
|
||||
app_events.socketio.emit('obd_speed', get_speedtime())
|
||||
else:
|
||||
print("RPM:", response.value)
|
||||
app_events.socketio.emit('obd_speed', response.value.to("kph"))
|
||||
finally:
|
||||
obd_rpm_lock.release()
|
||||
print("No RPM data received. #" + str(get_speedtime()))
|
||||
app_events.socketio.emit('obd_rpm', "-")
|
||||
else:
|
||||
rpm = round(response.value.magnitude, 0)
|
||||
print("RPM:", rpm)
|
||||
app_events.socketio.emit('obd_rpm', rpm)
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
def kill_obd(obd_type):
|
||||
if obd_type == "speed":
|
||||
obd_speed.loop = False
|
||||
stop_speed.set()
|
||||
elif obd_type == "rpm":
|
||||
obd_rpm.loop = False
|
||||
stop_rpm.set()
|
||||
|
||||
+136
-78
@@ -1,78 +1,136 @@
|
||||
import sqlite3
|
||||
from modules import app_obd, app_car, app_account, app_gps
|
||||
from datetime import datetime
|
||||
from sqlite3 import Error
|
||||
|
||||
currentdatetime = datetime.now()
|
||||
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 = "../db/sql/" + sql_file
|
||||
|
||||
def create_conn(db_file):
|
||||
conn = None
|
||||
try:
|
||||
conn = sqlite3.connect(db_file)
|
||||
print(sqlite3.version)
|
||||
except Error as e:
|
||||
print(e)
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
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 main():
|
||||
db = sql_path
|
||||
|
||||
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
|
||||
);"""
|
||||
|
||||
# create a database connection
|
||||
conn = create_conn(db)
|
||||
|
||||
# create tables
|
||||
if conn is not None:
|
||||
# create diagnostics table
|
||||
create_table(conn, sql_create_tracking_table)
|
||||
|
||||
# create diagnostics table
|
||||
create_table(conn, sql_create_dtc_table)
|
||||
|
||||
else:
|
||||
print("Error! cannot create the database connection.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_conn(sql_path)
|
||||
|
||||
|
||||
|
||||
# ## VN, NAME, DATE, TIME, GPS_X, GPS_Y, SPEED, ODOMETER
|
||||
# (["VN", "NAME", "DATE", "TIME", "GPSX", "GPSY", "SPEED", "ODOMETER"])
|
||||
# ([car.model, account.name, currentday, currenttime, GPSX, GPSY, SPEED, ODOMETER])
|
||||
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()
|
||||
|
||||
Reference in New Issue
Block a user