6bee9c0d7f
Foundation for the PySide6 + pyqtgraph Windows GUI, shared with the terminal
tool. Pure data/IO -- no Qt, no curses.
obdcore/
link.py ElmLink -- ELM327 serial (Mode-01/22, ATRV, DTC read/clear)
mock.py MockLink -- synthetic crank for tests + GUI dev (no truck)
registry.py PidRegistry (verified Ford 6.0 PIDs + confidence) + DtcDatabase
scheduler.py PollScheduler -- prioritized round-robin polling, dead-PID park,
derived channels; tick() is fake-clock test-drivable
store.py TimeSeriesStore (ring buffers + min/max) + CsvRecorder/replay
Design centers on the ELM327 bandwidth limit (~7-15 reads/sec): the active
view subscribes PIDs at chosen rates; acquisition runs off the UI thread;
the GUI only reads the store. FICM_M (09D0) promoted to verified after the
2026-06-30 on-truck crank read (48.0V, intermittent).
tests/test_obdcore.py: decoders vs real truck bytes, crank ramp + peak,
derived BOOST, dead-PID park/revive, record/replay roundtrip -- all pass.
ARCHITECTURE.md: layers, data model, GUI plan, 6.0 stock-PID limits
(no EGT/oil-PSI), feature backlog, P0-P5 roadmap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yT89n4zR4qbrySoSiEyZs
63 lines
2.0 KiB
Python
63 lines
2.0 KiB
Python
"""MockLink -- a synthetic ElmLink for tests and GUI development without a
|
|
truck. Simulates a cranking 6.0: ICP ramps toward ~540 psi, FICM holds ~48V,
|
|
battery sags, MAP/BARO sit at atmospheric. Same read interface as ElmLink.
|
|
"""
|
|
|
|
|
|
class MockLink:
|
|
def __init__(self, clock):
|
|
self.clock = clock # callable -> float seconds
|
|
self.t0 = clock()
|
|
self.protocol = "A6"
|
|
|
|
def init(self):
|
|
pass
|
|
|
|
def fast_timing(self, on=True):
|
|
pass
|
|
|
|
def connect(self):
|
|
return True
|
|
|
|
def is_can(self):
|
|
return True
|
|
|
|
def _u16le(self, raw16):
|
|
return [(raw16 >> 8) & 0xFF, raw16 & 0xFF]
|
|
|
|
def read_m22(self, pid, timeout=0.5):
|
|
el = self.clock() - self.t0
|
|
if pid == "1446": # ICP: ramps 0 -> 540 over ~2.7s
|
|
return self._u16le(int(min(540, el * 200) / 0.57))
|
|
if pid == "09D0": # FICM main ~48V (0x3000)
|
|
return self._u16le(0x3000)
|
|
if pid == "1440": # MAP atmospheric
|
|
return [0x01, 0x89]
|
|
if pid == "1442": # BARO atmospheric
|
|
return [0x01, 0x88]
|
|
if pid == "1445": # EBP atmospheric
|
|
return [0x01, 0x8F]
|
|
if pid == "1310": # EOT ~33C
|
|
return [0x1C, 0x92]
|
|
return None # everything else: no response
|
|
|
|
def read_m01(self, pid, nbytes, timeout=0.6):
|
|
if pid == "0C": # RPM 0 at rest
|
|
return [0x00, 0x00]
|
|
if pid == "05": # ECT 82C
|
|
return [122]
|
|
return None
|
|
|
|
def read_atrv(self, timeout=0.8):
|
|
el = self.clock() - self.t0
|
|
return 10.6 if el < 2.5 else 12.5 # crank sag then recover
|
|
|
|
def read_dtcs(self, mode, svc, timeout=5.0):
|
|
return ["P0148"] if mode == "03" else []
|
|
|
|
def clear_dtcs(self):
|
|
return True
|
|
|
|
def close(self):
|
|
pass
|