Harden OBD connection and telemetry
Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -676,23 +676,10 @@ returns HTTP 200 with no "relay" text over the real network) and the remote serv
|
||||
CORS-hook removal is baked into the image at build time - confirmed `/health` still returns
|
||||
`{"status":"ok"}` and the response no longer carries any `Access-Control-*` header).
|
||||
|
||||
**Handoff note for next session (2026-08-09, end of session)**: Phase 12 is fully done/deployed/
|
||||
verified as described above - nothing left to do there. Work had just started on **Phase 17 (OBD
|
||||
hardening)** next - picked because it's pure Python with zero risk to the Pi's live filesystem,
|
||||
in contrast to Phase 13's overlay-FS work which is riskier and deserves its own careful pass.
|
||||
**No code was written for Phase 17 yet** - only read `modules/app_events.py` (confirms
|
||||
`app_obd.obd_connection()` is only called from the Socket.IO `connect` handler, exactly as
|
||||
previously noted) and `modules/app_gps.py` (to use its `_poll_loop()`/`start_gps()` background-
|
||||
thread pattern as the template for OBD's new reconnect loop - lock+while-True+`time.sleep`,
|
||||
daemon thread, idempotent `start_*()` guard against double-starting). A `pip install obd` on the
|
||||
dev machine to inspect `python-obd`'s actual API (for DTC codes, ELM327 version/protocol
|
||||
attributes, exception types) was attempted but interrupted by a tool permission prompt before
|
||||
producing any output - **this still needs to be done** (or just read the `python-obd` source
|
||||
directly, e.g. via `pip download`/GitHub) before writing the DTC/diagnostics/protocol-override
|
||||
parts of Phase 17, to get exact attribute/method names right (e.g. `connection.protocol_name()`,
|
||||
`obd.commands.GET_DTC`, whatever `python-obd`'s actual exception class is called) rather than
|
||||
guessing. Phases 13 and 14 (UI foundation) are also fully ready to start (no blocking deps) if
|
||||
picking a different phase first makes more sense than continuing Phase 17.
|
||||
**Update 2026-08-09**: Phase 17 (OBD hardening) was completed next; see its section below.
|
||||
Phase 14 is the recommended next software phase because Phases 15/16 and the later list-based
|
||||
features depend on its shared UI foundation. Phase 13 remains independent but requires a careful
|
||||
hardware/filesystem deployment pass rather than ordinary application deployment.
|
||||
|
||||
### Phase 13: Power-loss resilience & watchdog — 🚧 PLANNED
|
||||
**New phase, not code-dependent on anything else — OS/hardware-level, can be done anytime,
|
||||
@@ -772,7 +759,7 @@ Same file-browser treatment for videos already imported to the Pi's own disk
|
||||
Settings page - e.g. "delete local copies older than N days once uploaded", off by default)
|
||||
so the Pi's limited storage doesn't need to be babysat manually as the dashcam backlog grows.
|
||||
|
||||
### Phase 17: OBD real-hardware readiness (code hardening) — 🚧 PLANNED
|
||||
### Phase 17: OBD real-hardware readiness (code hardening) — ✅ DONE
|
||||
**Independent of everything else in this plan — only touches `app_obd.py`/`app_events.py`, no UI
|
||||
dependency for the backend part, so this can be done anytime/in parallel with the phases above.**
|
||||
Nothing has been installed or tested in the actual 2013 Yaris yet - `modules/app_obd.py` has
|
||||
@@ -814,6 +801,25 @@ all in `modules/app_obd.py` / `modules/app_events.py`:
|
||||
confirmed once tested against the real car (falls under "code hardening only" for now - add
|
||||
the query with graceful "unsupported" handling rather than assuming it will work).
|
||||
|
||||
**Implemented 2026-08-09**:
|
||||
- Added an idempotent, browser-independent reconnect daemon started from `app.py`; it retries
|
||||
every 10 seconds and restarts any dead polling workers after reconnect.
|
||||
- Serialized all python-obd queries so speed, RPM, and the expanded sensor worker cannot issue
|
||||
overlapping serial exchanges. Query exceptions and silent connection-state changes now close
|
||||
and discard the stale handle, emit disconnected state, and hand recovery back to the reconnect
|
||||
loop instead of permanently killing a worker.
|
||||
- Added `OBD_PROTOCOL` support and distinct logs for no serial adapter, failed ELM327 handshake,
|
||||
and ELM327 connected but no ECU response. Successful connections log port, ELM327 firmware,
|
||||
negotiated protocol ID, and protocol name once.
|
||||
- Added backend polling/emits for coolant temperature, fuel level, throttle position, MIL/DTC
|
||||
status, stored DTCs, and SAE PID `0xA6` odometer. Odometer uses a custom python-obd command with
|
||||
a null/unsupported result left as `None`; support is not assumed for the 2013 Yaris.
|
||||
- Verified against the Pi's installed python-obd 0.7.3 API, plus focused mocked checks covering
|
||||
successful connection, forced protocol, ECU-off status, silent disconnect cleanup, query
|
||||
exceptions, and odometer decoding. The Pi currently has `/dev/ttyUSB0` present but is not
|
||||
connected to the car, so actual PID availability and negotiated Toyota protocol still require
|
||||
the user-planned first in-car test.
|
||||
|
||||
### Phase 18: Trip history & replay — 🚧 PLANNED
|
||||
**New phase.** Right now the `tracking` table (Phase 7) is just a raw stream of GPS/speed points
|
||||
with no concept of a discrete "trip" - there's no way to look back and see "trip #47, left home
|
||||
|
||||
@@ -8,7 +8,7 @@ app = Flask(__name__)
|
||||
app_events.socketio.init_app(app)
|
||||
|
||||
account = app_account.Account()
|
||||
obd = app_obd.OBD()
|
||||
obd = app_obd.start_obd()
|
||||
car = app_car.Car()
|
||||
gps = app_gps.start_gps()
|
||||
app_sql.start_logging()
|
||||
|
||||
@@ -10,7 +10,7 @@ thread_uptime = None
|
||||
@socketio.on('connect')
|
||||
def connect_event():
|
||||
print('Client connected')
|
||||
print('Getting OBD status...')
|
||||
print('Getting current OBD status...')
|
||||
app_obd.obd_connection()
|
||||
uptime()
|
||||
|
||||
@@ -42,4 +42,3 @@ def loop_uptime():
|
||||
uptime_lock.release()
|
||||
|
||||
|
||||
|
||||
|
||||
+394
-141
@@ -1,141 +1,394 @@
|
||||
from modules import app_gps, app_sql, app_events
|
||||
import glob, obd, os, time, threading
|
||||
from datetime import datetime
|
||||
from threading import Event, Thread
|
||||
|
||||
status = None
|
||||
speed = 0
|
||||
rpm = None
|
||||
|
||||
class OBD:
|
||||
def __init__(self):
|
||||
self.status = status
|
||||
self.rpm = rpm
|
||||
self.speed = speed
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
def get_speedtime():
|
||||
global speed
|
||||
time_now = str(datetime.now().strftime("%H:%M:%S"))
|
||||
return [time_now, speed]
|
||||
|
||||
|
||||
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('obd_error', "-")
|
||||
app_events.socketio.emit('obd_rpm', "-")
|
||||
|
||||
def obd_speed():
|
||||
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())
|
||||
else:
|
||||
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():
|
||||
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 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":
|
||||
stop_speed.set()
|
||||
elif obd_type == "rpm":
|
||||
stop_rpm.set()
|
||||
from datetime import datetime
|
||||
from threading import Event, Lock, Thread
|
||||
import glob
|
||||
import os
|
||||
import time
|
||||
|
||||
import obd
|
||||
|
||||
from modules import app_events, app_gps
|
||||
|
||||
|
||||
RECONNECT_INTERVAL = 10
|
||||
SENSOR_POLL_INTERVAL = 5
|
||||
|
||||
status = None
|
||||
speed = 0
|
||||
rpm = None
|
||||
coolant_temp = None
|
||||
fuel_level = None
|
||||
throttle_position = None
|
||||
mil_on = None
|
||||
dtc_codes = []
|
||||
odometer = None
|
||||
|
||||
|
||||
class OBD:
|
||||
def __init__(self):
|
||||
self.status = status
|
||||
self.rpm = rpm
|
||||
self.speed = speed
|
||||
|
||||
|
||||
obd_status = ["Disconnected", "Connected", "Connecting"]
|
||||
obd_speed_lock = Lock()
|
||||
obd_rpm_lock = Lock()
|
||||
obd_sensor_lock = Lock()
|
||||
_connection_lock = Lock()
|
||||
_connect_lock = Lock()
|
||||
_query_lock = Lock()
|
||||
thread_speed = None
|
||||
thread_rpm = None
|
||||
thread_sensors = None
|
||||
_reconnect_thread = None
|
||||
stop_speed = Event()
|
||||
stop_rpm = Event()
|
||||
stop_sensors = Event()
|
||||
|
||||
connection = None
|
||||
|
||||
|
||||
def _decode_odometer(messages):
|
||||
data = messages[0].data[2:]
|
||||
if len(data) != 4:
|
||||
raise ValueError(f"expected 4 odometer bytes, received {len(data)}")
|
||||
return int.from_bytes(bytes(data), byteorder="big") / 10
|
||||
|
||||
|
||||
ODOMETER_COMMAND = obd.OBDCommand(
|
||||
"ODOMETER",
|
||||
"Vehicle odometer",
|
||||
b"01A6",
|
||||
6,
|
||||
_decode_odometer,
|
||||
)
|
||||
|
||||
|
||||
def _find_obd_port():
|
||||
"""Locate the USB-serial OBD-II adapter."""
|
||||
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 _connection_status(candidate):
|
||||
try:
|
||||
return candidate.status()
|
||||
except Exception:
|
||||
return obd.OBDStatus.NOT_CONNECTED
|
||||
|
||||
|
||||
def _is_car_connected(candidate=None):
|
||||
if candidate is None:
|
||||
with _connection_lock:
|
||||
candidate = connection
|
||||
return (
|
||||
candidate is not None
|
||||
and _connection_status(candidate) == obd.OBDStatus.CAR_CONNECTED
|
||||
)
|
||||
|
||||
|
||||
def _emit_disconnected():
|
||||
app_events.socketio.emit("obd_status", obd_status[0])
|
||||
app_events.socketio.emit("obd_speed", "-")
|
||||
app_events.socketio.emit("obd_error", "-")
|
||||
app_events.socketio.emit("obd_rpm", "-")
|
||||
|
||||
|
||||
def _close_connection(candidate):
|
||||
try:
|
||||
candidate.close()
|
||||
except Exception as error:
|
||||
print("Failed to close OBD connection:", str(error))
|
||||
|
||||
|
||||
def _mark_connection_lost(candidate, operation, error):
|
||||
global connection
|
||||
with _connection_lock:
|
||||
if connection is not candidate:
|
||||
return
|
||||
connection = None
|
||||
print(f"OBD {operation} failed; reconnecting: {error}")
|
||||
_close_connection(candidate)
|
||||
_emit_disconnected()
|
||||
|
||||
|
||||
def _query(command, *, force=False):
|
||||
with _connection_lock:
|
||||
candidate = connection
|
||||
if candidate is None:
|
||||
return None
|
||||
if not _is_car_connected(candidate):
|
||||
_mark_connection_lost(
|
||||
candidate,
|
||||
"connection check",
|
||||
f"status changed to {_connection_status(candidate)}",
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
with _query_lock:
|
||||
return candidate.query(command, force=force)
|
||||
except Exception as error:
|
||||
_mark_connection_lost(candidate, command.name, error)
|
||||
return None
|
||||
|
||||
|
||||
def _log_connection_diagnostics(candidate):
|
||||
firmware = "unknown"
|
||||
try:
|
||||
with _query_lock:
|
||||
response = candidate.query(obd.commands.ELM_VERSION)
|
||||
if not response.is_null():
|
||||
firmware = str(response.value)
|
||||
except Exception as error:
|
||||
print("Unable to read ELM327 firmware version:", str(error))
|
||||
|
||||
print(
|
||||
"OBD connected:"
|
||||
f" port={candidate.port_name()},"
|
||||
f" ELM327={firmware},"
|
||||
f" protocol={candidate.protocol_id()} ({candidate.protocol_name()})"
|
||||
)
|
||||
|
||||
|
||||
def connect_obd():
|
||||
"""Connect to the adapter and ECU, with actionable failure diagnostics."""
|
||||
global connection
|
||||
with _connect_lock:
|
||||
if _is_car_connected():
|
||||
return connection
|
||||
|
||||
with _connection_lock:
|
||||
stale_connection = connection
|
||||
connection = None
|
||||
if stale_connection is not None:
|
||||
_close_connection(stale_connection)
|
||||
|
||||
port = _find_obd_port()
|
||||
detected_ports = [port] if port else obd.scan_serial()
|
||||
if not detected_ports:
|
||||
print(
|
||||
"No OBD-II adapter detected. Check USB power/cabling and the "
|
||||
"OBD_PORT setting."
|
||||
)
|
||||
with _connection_lock:
|
||||
connection = None
|
||||
_emit_disconnected()
|
||||
return None
|
||||
|
||||
protocol = os.environ.get("OBD_PROTOCOL") or None
|
||||
selected_port = port or detected_ports[0]
|
||||
print(
|
||||
f"{obd_status[2]} to {selected_port}"
|
||||
+ (f" using protocol {protocol}" if protocol else " using auto protocol")
|
||||
+ "..."
|
||||
)
|
||||
|
||||
try:
|
||||
candidate = obd.OBD(portstr=selected_port, protocol=protocol)
|
||||
except Exception as error:
|
||||
print(
|
||||
f"OBD adapter detected at {selected_port}, but the ELM327 "
|
||||
f"handshake failed: {error}"
|
||||
)
|
||||
with _connection_lock:
|
||||
connection = None
|
||||
_emit_disconnected()
|
||||
return None
|
||||
|
||||
candidate_status = _connection_status(candidate)
|
||||
if candidate_status == obd.OBDStatus.NOT_CONNECTED:
|
||||
print(
|
||||
f"OBD adapter detected at {selected_port}, but the ELM327 "
|
||||
"handshake failed."
|
||||
)
|
||||
_close_connection(candidate)
|
||||
candidate = None
|
||||
elif candidate_status != obd.OBDStatus.CAR_CONNECTED:
|
||||
print(
|
||||
f"ELM327 handshake succeeded at {selected_port}, but the car ECU "
|
||||
f"did not respond (status: {candidate_status}). Check ignition."
|
||||
)
|
||||
_close_connection(candidate)
|
||||
candidate = None
|
||||
|
||||
with _connection_lock:
|
||||
connection = candidate
|
||||
|
||||
if candidate is None:
|
||||
_emit_disconnected()
|
||||
return None
|
||||
|
||||
_log_connection_diagnostics(candidate)
|
||||
app_events.socketio.emit("obd_status", obd_status[1])
|
||||
_start_polling_threads()
|
||||
return candidate
|
||||
|
||||
|
||||
def get_speedtime():
|
||||
time_now = str(datetime.now().strftime("%H:%M:%S"))
|
||||
return [time_now, speed]
|
||||
|
||||
|
||||
def obd_connection():
|
||||
"""Emit current state for a newly connected browser and ensure OBD is running."""
|
||||
start_obd()
|
||||
if _is_car_connected():
|
||||
app_events.socketio.emit("obd_status", obd_status[1])
|
||||
app_events.socketio.emit("obd_speed", get_speedtime())
|
||||
app_events.socketio.emit("obd_rpm", rpm if rpm is not None else "-")
|
||||
else:
|
||||
_emit_disconnected()
|
||||
|
||||
|
||||
def obd_speed():
|
||||
global speed
|
||||
with obd_speed_lock:
|
||||
while not stop_speed.is_set() and _is_car_connected():
|
||||
response = _query(obd.commands.SPEED)
|
||||
if response is None:
|
||||
break
|
||||
if response.is_null():
|
||||
print("No speed data received. #" + str(get_speedtime()))
|
||||
print(app_gps.get_gps())
|
||||
else:
|
||||
speed = round(response.value.to("kph").magnitude, 1)
|
||||
print("Speed:", speed, "kph")
|
||||
|
||||
app_events.socketio.emit("obd_speed", get_speedtime())
|
||||
stop_speed.wait(2)
|
||||
|
||||
|
||||
def obd_rpm():
|
||||
global rpm
|
||||
with obd_rpm_lock:
|
||||
while not stop_rpm.is_set() and _is_car_connected():
|
||||
response = _query(obd.commands.RPM)
|
||||
if response is None:
|
||||
break
|
||||
if response.is_null():
|
||||
print("No RPM data received. #" + str(get_speedtime()))
|
||||
app_events.socketio.emit("obd_rpm", "-")
|
||||
else:
|
||||
rpm = round(response.value.magnitude)
|
||||
print("RPM:", rpm)
|
||||
app_events.socketio.emit("obd_rpm", rpm)
|
||||
|
||||
stop_rpm.wait(1)
|
||||
|
||||
|
||||
def _quantity_value(command, unit=None):
|
||||
response = _query(command)
|
||||
if response is None or response.is_null():
|
||||
return None
|
||||
value = response.value.to(unit) if unit else response.value
|
||||
return round(value.magnitude, 1)
|
||||
|
||||
|
||||
def _poll_diagnostics():
|
||||
global coolant_temp, fuel_level, throttle_position
|
||||
global mil_on, dtc_codes, odometer
|
||||
|
||||
coolant_temp = _quantity_value(obd.commands.COOLANT_TEMP, "degC")
|
||||
fuel_level = _quantity_value(obd.commands.FUEL_LEVEL, "percent")
|
||||
throttle_position = _quantity_value(obd.commands.THROTTLE_POS, "percent")
|
||||
|
||||
status_response = _query(obd.commands.STATUS)
|
||||
if status_response is not None and not status_response.is_null():
|
||||
mil_on = bool(status_response.value.MIL)
|
||||
app_events.socketio.emit(
|
||||
"obd_mil",
|
||||
{"on": mil_on, "dtc_count": status_response.value.DTC_count},
|
||||
)
|
||||
|
||||
dtc_response = _query(obd.commands.GET_DTC)
|
||||
if dtc_response is not None and not dtc_response.is_null():
|
||||
dtc_codes = [
|
||||
{"code": code, "description": description}
|
||||
for code, description in dtc_response.value
|
||||
]
|
||||
app_events.socketio.emit("obd_dtc", dtc_codes)
|
||||
|
||||
odometer_response = _query(ODOMETER_COMMAND, force=True)
|
||||
if odometer_response is not None and not odometer_response.is_null():
|
||||
odometer = round(odometer_response.value, 1)
|
||||
|
||||
app_events.socketio.emit("obd_coolant_temp", coolant_temp)
|
||||
app_events.socketio.emit("obd_fuel_level", fuel_level)
|
||||
app_events.socketio.emit("obd_throttle", throttle_position)
|
||||
app_events.socketio.emit("obd_odometer", odometer)
|
||||
|
||||
|
||||
def obd_sensors():
|
||||
with obd_sensor_lock:
|
||||
while not stop_sensors.is_set() and _is_car_connected():
|
||||
_poll_diagnostics()
|
||||
stop_sensors.wait(SENSOR_POLL_INTERVAL)
|
||||
|
||||
|
||||
def _start_polling_threads():
|
||||
global thread_speed, thread_rpm, thread_sensors
|
||||
|
||||
thread_specs = (
|
||||
("speed", obd_speed, stop_speed),
|
||||
("rpm", obd_rpm, stop_rpm),
|
||||
("sensors", obd_sensors, stop_sensors),
|
||||
)
|
||||
threads = {
|
||||
"speed": thread_speed,
|
||||
"rpm": thread_rpm,
|
||||
"sensors": thread_sensors,
|
||||
}
|
||||
|
||||
for name, target, stop_event in thread_specs:
|
||||
thread = threads[name]
|
||||
if thread is None or not thread.is_alive():
|
||||
stop_event.clear()
|
||||
thread = Thread(target=target, name=f"obd-{name}", daemon=True)
|
||||
thread.start()
|
||||
if name == "speed":
|
||||
thread_speed = thread
|
||||
elif name == "rpm":
|
||||
thread_rpm = thread
|
||||
else:
|
||||
thread_sensors = thread
|
||||
print(f"OBD {name} thread started, alive: {thread.is_alive()}")
|
||||
|
||||
|
||||
def _reconnect_loop():
|
||||
while True:
|
||||
if _is_car_connected():
|
||||
_start_polling_threads()
|
||||
else:
|
||||
connect_obd()
|
||||
time.sleep(RECONNECT_INTERVAL)
|
||||
|
||||
|
||||
def start_obd():
|
||||
"""Start the browser-independent OBD reconnect loop once."""
|
||||
global _reconnect_thread
|
||||
if _reconnect_thread is None or not _reconnect_thread.is_alive():
|
||||
_reconnect_thread = Thread(
|
||||
target=_reconnect_loop,
|
||||
name="obd-reconnect",
|
||||
daemon=True,
|
||||
)
|
||||
_reconnect_thread.start()
|
||||
print(f"OBD reconnect thread started, alive: {_reconnect_thread.is_alive()}")
|
||||
return OBD()
|
||||
|
||||
|
||||
def kill_obd(obd_type):
|
||||
if obd_type == "speed":
|
||||
stop_speed.set()
|
||||
elif obd_type == "rpm":
|
||||
stop_rpm.set()
|
||||
elif obd_type == "sensors":
|
||||
stop_sensors.set()
|
||||
elif obd_type == "all":
|
||||
stop_speed.set()
|
||||
stop_rpm.set()
|
||||
stop_sensors.set()
|
||||
|
||||
Reference in New Issue
Block a user