Add Tools/Diagnostics: thread-safe DTC read/clear + Diagnostics panel
The polling thread owns the ELM327, so reading/clearing trouble codes from
the GUI thread would race PID reads and corrupt the stream. Add a one-off
command path that serializes ad-hoc link work onto the polling thread.
obdcore/scheduler.py:
- PollScheduler.run_oneoff(fn, timeout) enqueues a callable (queue.Queue +
threading.Event) and blocks for its result, re-raising the callable's
exception. tick() drains queued one-offs at its very top, so they run on
the same thread that does PID reads -- never concurrently. When the
scheduler thread isn't running, the job is drained inline on the caller
(still serialized vs tick(), safe because nothing else touches the link).
gui/controller.py:
- Controller.read_dtcs() -> {"stored","pending","permanent"} (modes 03/07/0A,
svc 0x43/0x47/0x4A) and clear_dtcs() -> bool. Both route through the
scheduler one-off when a scheduler exists, else call the link directly.
gui/main.py:
- Diagnostics menu (Read Codes / Clear Codes...) and a right-side QDockWidget
listing codes grouped Stored/Pending/Permanent. Each row is code +
description + system from DtcDatabase; no_start codes are flagged bold red.
- Clear is guarded by a confirmation warning (erases codes + freeze frame;
honest "the code comes right back" / permanent-codes-won't-clear tone from
run_clear in obd_reader.py). On confirm: clear, then re-read immediately and
show whatever returned, reporting active faults that came straight back.
tests/test_diagnostics.py:
- one-off returns its value, re-raises exceptions, is drained before a tick's
PID reads, and runs on a live background thread while polling continues.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
This commit is contained in:
@@ -15,6 +15,14 @@ FAST = {"ICP", "FICM_M", "RPM"}
|
||||
DEFAULT_HZ = 2
|
||||
FAST_HZ = 5
|
||||
|
||||
# DTC services: result-bucket label, request mode string, response service byte
|
||||
# (mode "03"->0x43 stored, "07"->0x47 pending, "0A"->0x4A permanent)
|
||||
DTC_SERVICES = (
|
||||
("stored", "03", 0x43),
|
||||
("pending", "07", 0x47),
|
||||
("permanent", "0A", 0x4A),
|
||||
)
|
||||
|
||||
|
||||
class Controller:
|
||||
def __init__(self):
|
||||
@@ -79,6 +87,31 @@ class Controller:
|
||||
def now(self):
|
||||
return (time.time() - self.t0) if self.t0 else 0.0
|
||||
|
||||
# -- diagnostics (DTCs) --------------------------------------------------
|
||||
# All link access goes through the scheduler's one-off path when a
|
||||
# scheduler exists, so a DTC read/clear never races the polling thread for
|
||||
# the serial link. When disconnected (no scheduler), call the link direct.
|
||||
def _oneoff(self, fn, timeout=8.0):
|
||||
if self.sched is not None:
|
||||
return self.sched.run_oneoff(fn, timeout=timeout)
|
||||
if self.link is not None:
|
||||
return fn()
|
||||
raise RuntimeError("not connected")
|
||||
|
||||
def read_dtcs(self):
|
||||
"""Read stored (03), pending (07) and permanent (0A) DTCs.
|
||||
Returns {"stored": [...], "pending": [...], "permanent": [...]}."""
|
||||
out = {}
|
||||
for label, mode, svc in DTC_SERVICES:
|
||||
out[label] = self._oneoff(
|
||||
lambda m=mode, s=svc: self.link.read_dtcs(m, s)) or []
|
||||
return out
|
||||
|
||||
def clear_dtcs(self):
|
||||
"""Mode 04: clear stored+pending codes and freeze frame.
|
||||
Returns True if the ECU acknowledged."""
|
||||
return bool(self._oneoff(lambda: self.link.clear_dtcs()))
|
||||
|
||||
def stop(self):
|
||||
if self.sched:
|
||||
self.sched.stop()
|
||||
|
||||
Reference in New Issue
Block a user