"""Controller -- owns the obdcore link/registry/store/scheduler for the GUI. Keeps all acquisition concerns out of the widgets. The GUI subscribes/ unsubscribes PIDs (== what's polled == what's plotted) and reads the store on a timer; the scheduler thread does the serial work. """ import time from obdcore import (PidRegistry, DtcDatabase, TimeSeriesStore, PollScheduler, CsvRecorder, load_default, load_profile) from obdcore.mock import MockLink # default poll rates (Hz) -- fast for the no-start metrics, slower for the rest 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): self.profile = load_default() self.reg = PidRegistry(self.profile) self.dtcdb = DtcDatabase(self.profile) self.store = TimeSeriesStore() self.link = None self.sched = None self.t0 = None self.connected = False def load_profile(self, path): """Switch the active vehicle profile (only allowed while disconnected).""" self.profile = load_profile(path) self.reg = PidRegistry(self.profile) self.dtcdb = DtcDatabase(self.profile) def connect(self, port=None, baud=38400, mock=False): if mock: self.link = MockLink(clock=time.time) else: from obdcore.link import ElmLink # imported lazily (needs pyserial) self.link = ElmLink(port, baud) self.link.init() ok = self.link.connect() try: self.link.fast_timing(True) except Exception: pass self.sched = PollScheduler(self.link, self.reg, self.store, clock=time.time) self.t0 = time.time() self.connected = True return ok def hz_for(self, key): return FAST_HZ if key in FAST else DEFAULT_HZ def subscribe(self, key): if self.sched: self.sched.subscribe(key, self.hz_for(key)) def unsubscribe(self, key): if self.sched: self.sched.unsubscribe(key) def subscribed(self): return set(self.sched.subscriptions()) if self.sched else set() def start(self): if self.sched: self.sched.start() def record(self, path): self.store.recorder = CsvRecorder(path) def stop_record(self): if self.store.recorder: self.store.recorder.close() self.store.recorder = None 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() self.sched = None self.stop_record() if self.link: try: self.link.fast_timing(False) except Exception: pass self.link.close() self.link = None self.connected = False