car-diagnostic-agent / fault_simulation.py
aldsouza's picture
Fault simulation
5b29a5b
Raw
History Blame Contribute Delete
30 kB
"""Apply ELM327-emulator PID overrides to simulate automotive faults."""
from __future__ import annotations
import logging
import re
import threading
from dataclasses import dataclass
from typing import Any
from elm_server import _emulator_lock, ensure_elm_emulator, get_emulator
from obd_connection import obd_session_lock
logger = logging.getLogger("car-diagnostic-agent.faults")
_active_fault_id: str | None = None
_state_lock = threading.Lock()
_DTC_PATTERN = re.compile(r"[PCBU]\d{4}")
@dataclass(frozen=True)
class FaultProfile:
id: str
name: str
category: str
description: str
signature: str
typical_dtc: str
llm_hint: str
overrides: dict[str, str]
# --- OBD encoding helpers (SAE J1979 / python-OBD) ---
def trim_b(pct: float) -> int:
return int(128 + pct * 128 / 100)
def maf_hex(gps: float) -> str:
return f"{int(gps * 100):04X}"
def clt_hex(deg_c: float) -> str:
return f"{int(deg_c + 40):02X}"
def rpm_hex(rpm: float) -> str:
return f"{int(4 * rpm):04X}"
def volt_hex(volts: float) -> str:
return f"{int(volts * 1000):04X}"
def pct_byte(pct: float) -> str:
return f"{int(pct * 255 / 100):02X}"
def ax(expr: str) -> str:
"""Build an emulator answer XML string with one <exec> block."""
return f"<exec>{expr}</exec><writeln />"
def _randint_expr(low: int, high: int) -> str:
return f'__import__("random").randint({low}, {high})'
def parse_dtc_codes(typical_dtc: str) -> list[str]:
"""Extract OBD-II codes like P0171 from a fault's typical_dtc field."""
return _DTC_PATTERN.findall(typical_dtc or "")
def dtc_to_bytes(code: str) -> tuple[int, int]:
kind = {"P": 0, "C": 1, "B": 2, "U": 3}[code[0]]
b1 = (kind << 6) | (int(code[1]) << 4) | int(code[2], 16)
b2 = int(code[3:5], 16)
return b1, b2
def build_get_dtc_answer(typical_dtc: str) -> str | None:
"""Build emulator GET_DTC override XML for Mode 03 stored codes."""
codes = parse_dtc_codes(typical_dtc)
if not codes:
return None
body = ["43", f"{len(codes):02X}"]
for code in codes:
b1, b2 = dtc_to_bytes(code)
body.extend([f"{b1:02X}", f"{b2:02X}"])
payload = " ".join(body)
return ax(f'ECU_R_ADDR_E + " {len(body):02X} {payload}"')
def get_active_dtc_codes() -> list[str]:
"""Return DTC codes for the active fault profile, if any."""
fault = get_active_fault()
if fault is None:
return []
return parse_dtc_codes(fault.typical_dtc)
def _effective_overrides(fault: FaultProfile) -> dict[str, str]:
overrides = dict(fault.overrides)
dtc_answer = build_get_dtc_answer(fault.typical_dtc)
if dtc_answer is not None:
overrides["GET_DTC"] = dtc_answer
return overrides
# --- Fault profiles ---
def _build_faults() -> dict[str, FaultProfile]:
faults: dict[str, FaultProfile] = {}
def add(profile: FaultProfile) -> None:
faults[profile.id] = profile
add(
FaultProfile(
id="vacuum_leak",
name="Vacuum leak (idle)",
category="Air system",
description="Unmetered air — cracked hose, PCV leak, intake gasket.",
signature="STFT ↑↑, LTFT ↑↑, MAF low, idle RPM slightly high",
typical_dtc="P0171",
llm_hint="Likely vacuum leak; positive trims and low MAF at idle.",
overrides={
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(20):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(15):02X}"'
),
"MAF": ax(f'ECU_R_ADDR_E + " 04 41 10 {maf_hex(12)}"'),
"INTAKE_PRESSURE": ax('ECU_R_ADDR_E + " 03 41 0B 37"'),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(950)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 00"'),
},
)
)
add(
FaultProfile(
id="dirty_maf",
name="Dirty MAF sensor (cruise)",
category="Air system",
description="MAF underestimates airflow at cruise.",
signature="MAF low vs RPM/load, LTFT +, STFT +",
typical_dtc="P0101",
llm_hint="MAF reading low for RPM; dirty or failed MAF suspected.",
overrides={
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(2500)}"'),
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 80"'),
"MAF": ax(f'ECU_R_ADDR_E + " 04 41 10 {maf_hex(16)}"'),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(12):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(10):02X}"'
),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 50"'),
},
)
)
add(
FaultProfile(
id="air_filter_restriction",
name="Air filter restriction",
category="Air system",
description="Blocked airflow — needs more throttle for same power.",
signature="Throttle ↑, MAF ↓, engine load ↓ at same RPM",
typical_dtc="—",
llm_hint="High throttle with low MAF and load suggests intake restriction.",
overrides={
"THROTTLE_POS": ax('ECU_R_ADDR_E + " 03 41 11 A0"'),
"MAF": ax(f'ECU_R_ADDR_E + " 04 41 10 {maf_hex(35)}"'),
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 45"'),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(2200)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 35"'),
},
)
)
add(
FaultProfile(
id="egr_stuck_open",
name="EGR stuck open (unstable idle)",
category="Air system",
description="Exhaust gas enters intake continuously.",
signature="Rough idle, RPM unstable, possible STFT +",
typical_dtc="P0401 / misfire codes",
llm_hint="Unstable idle with commanded EGR fully open.",
overrides={
"COMMANDED_EGR": ax('ECU_R_ADDR_E + " 03 41 2C FF"'),
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(600, 1100)})'
),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(8):02X}"'
),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 00"'),
},
)
)
add(
FaultProfile(
id="fuel_pump_weak_load",
name="Fuel pump weakening (under load)",
category="Fuel system",
description="Low fuel pressure under heavy acceleration.",
signature="Normal idle trims; under load STFT +20%, LTFT +15%",
typical_dtc="P0171",
llm_hint="Lean under load with high engine load and RPM.",
overrides={
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 FF"'),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(4500)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 90"'),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(20):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(15):02X}"'
),
},
)
)
add(
FaultProfile(
id="dirty_injectors",
name="Dirty fuel injectors",
category="Fuel system",
description="Reduced spray — uneven combustion.",
signature="Positive trims, mild RPM instability",
typical_dtc="P0300",
llm_hint="Positive trims with unstable RPM at idle.",
overrides={
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(14):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(11):02X}"'
),
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(1100, 1350)})'
),
},
)
)
add(
FaultProfile(
id="injector_leak",
name="Leaking injector (rich)",
category="Fuel system",
description="Continuous fuel drip — rich condition.",
signature="STFT −, LTFT − (negative trims)",
typical_dtc="P0172",
llm_hint="Negative fuel trims indicate rich running.",
overrides={
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(-18):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(-12):02X}"'
),
},
)
)
add(
FaultProfile(
id="injector_stuck_closed",
name="Injector stuck closed (lean misfire)",
category="Fuel system",
description="One cylinder lean → misfire tendency.",
signature="Positive trims, RPM instability at idle",
typical_dtc="P0301–P0304",
llm_hint="Lean trims with unstable idle — possible cylinder misfire.",
overrides={
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(16):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(8):02X}"'
),
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(750, 1250)})'
),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 00"'),
},
)
)
add(
FaultProfile(
id="ignition_coil_failure",
name="Ignition coil failure",
category="Ignition",
description="Weak spark on one cylinder.",
signature="RPM instability, O2 swings, trims may go positive",
typical_dtc="P0301",
llm_hint="High RPM std at idle with erratic O2 — misfire pattern.",
overrides={
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(500, 1500)})'
),
"O2_B1S2": ax(
f'ECU_R_ADDR_E + " 03 41 14 %.2X" % {_randint_expr(10, 200)}'
),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(6):02X}"'
),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 00"'),
},
)
)
add(
FaultProfile(
id="spark_plug_wear",
name="Spark plug wear (mid stage)",
category="Ignition",
description="Gradual ignition efficiency loss.",
signature="Slowly increasing fuel trims, reduced performance",
typical_dtc="—",
llm_hint="Elevated LTFT/STFT without obvious air or fuel mechanical fault.",
overrides={
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(9):02X}"'
),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(6):02X}"'
),
},
)
)
add(
FaultProfile(
id="thermostat_stuck_open",
name="Thermostat stuck open",
category="Cooling",
description="Engine never reaches operating temperature.",
signature="Coolant temp low after long run (~65°C vs ~90°C expected)",
typical_dtc="P0128",
llm_hint="Engine not reaching operating temperature; stuck-open thermostat.",
overrides={
"COOLANT_TEMP": ax(f'ECU_R_ADDR_E + " 03 41 05 {clt_hex(65)}"'),
"RUN_TIME": ax('ECU_R_ADDR_E + " 04 41 1F 0E 10"'),
},
)
)
add(
FaultProfile(
id="thermostat_stuck_closed",
name="Thermostat stuck closed (overheating)",
category="Cooling",
description="Coolant cannot circulate — overheating.",
signature="Coolant temp rapidly rising → 105–112°C",
typical_dtc="P0217",
llm_hint="High coolant temperature under moderate load.",
overrides={
"COOLANT_TEMP": ax(f'ECU_R_ADDR_E + " 03 41 05 {clt_hex(112)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 25"'),
},
)
)
add(
FaultProfile(
id="cooling_fan_failure_idle",
name="Cooling fan failure (idle/traffic)",
category="Cooling",
description="Overheats at idle but normal while moving.",
signature="High coolant only when speed ≈ 0",
typical_dtc="P0217",
llm_hint="Overheating at idle with normal cruise temps in comparison sessions.",
overrides={
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 00"'),
"COOLANT_TEMP": ax(f'ECU_R_ADDR_E + " 03 41 05 {clt_hex(108)}"'),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(750)}"'),
},
)
)
add(
FaultProfile(
id="water_pump_degradation",
name="Water pump degradation (high load)",
category="Cooling",
description="Reduced circulation — gradual overheating under load.",
signature="Temp rising under high RPM/load",
typical_dtc="—",
llm_hint="Elevated coolant at high load vs healthy baseline at same RPM.",
overrides={
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(3800)}"'),
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 C0"'),
"COOLANT_TEMP": ax(f'ECU_R_ADDR_E + " 03 41 05 {clt_hex(102)}"'),
},
)
)
add(
FaultProfile(
id="low_coolant",
name="Low coolant (unstable temp)",
category="Cooling",
description="Unstable temperature, overheating spikes.",
signature="High std on coolant temperature",
typical_dtc="—",
llm_hint="Erratic coolant readings — possible low coolant level.",
overrides={
"COOLANT_TEMP": ax(
f'ECU_R_ADDR_E + " 03 41 05 %.2X" % int(85 + {_randint_expr(-8, 25)})'
),
},
)
)
add(
FaultProfile(
id="catalyst_degradation",
name="Catalytic converter degradation",
category="Emissions",
description="Downstream O2 oscillates like upstream (unhealthy).",
signature="Upstream and downstream O2 both oscillating",
typical_dtc="P0420",
llm_hint="Downstream O2 oscillation suggests catalyst efficiency loss.",
overrides={
"O2_B1S2": ax(
f'ECU_R_ADDR_E + " 03 41 14 %.2X" % {_randint_expr(30, 170)}'
),
"O2_S1_WR_VOLTAGE": ax(
f'ECU_R_ADDR_E + " 04 41 24 %.4X" % {_randint_expr(200, 3500)}'
),
},
)
)
add(
FaultProfile(
id="catalyst_restriction",
name="Catalytic converter restriction",
category="Emissions",
description="Clogged catalyst — low power, high exhaust backpressure.",
signature="High load, low MAF, poor acceleration, cat temp ↑",
typical_dtc="P0420",
llm_hint="High load with low MAF and elevated catalyst temperature.",
overrides={
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 F0"'),
"MAF": ax(f'ECU_R_ADDR_E + " 04 41 10 {maf_hex(28)}"'),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(2800)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 40"'),
"CATALYST_TEMP_B1S1": ax('ECU_R_ADDR_E + " 04 41 3C 23 28"'),
},
)
)
add(
FaultProfile(
id="evap_purge_stuck_open",
name="EVAP purge valve stuck open",
category="Emissions",
description="Fuel vapors enter intake at wrong time.",
signature="At idle: fuel trims positive, worse after refueling",
typical_dtc="P0441",
llm_hint="Positive trims at idle with purge valve fully open.",
overrides={
"EVAPORATIVE_PURGE": ax('ECU_R_ADDR_E + " 03 41 2E FF"'),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(14):02X}"'
),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(6):02X}"'
),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 00"'),
},
)
)
add(
FaultProfile(
id="o2_sensor_drift",
name="O2 sensor drift",
category="Sensors",
description="Sensor aging — trims drift without obvious mechanical fault.",
signature="LTFT slowly drifting, O2 less responsive",
typical_dtc="P0136",
llm_hint="Elevated LTFT with sluggish downstream O2.",
overrides={
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(10):02X}"'
),
"O2_B1S2": ax(
f'ECU_R_ADDR_E + " 03 41 14 %.2X" % (128 + {_randint_expr(-5, 5)})'
),
},
)
)
add(
FaultProfile(
id="o2_sensor_failure_low",
name="O2 sensor failure (stuck low)",
category="Sensors",
description="Sensor dead — flatlined low voltage.",
signature="O2 stuck at 0V continuously",
typical_dtc="P0136",
llm_hint="Flatlined O2 voltage — failed downstream sensor.",
overrides={"O2_B1S2": ax('ECU_R_ADDR_E + " 03 41 14 00"')},
)
)
add(
FaultProfile(
id="o2_sensor_failure_high",
name="O2 sensor failure (stuck high)",
category="Sensors",
description="Sensor dead — flatlined high voltage.",
signature="O2 stuck high continuously",
typical_dtc="P0136",
llm_hint="O2 pinned high — failed downstream sensor.",
overrides={"O2_B1S2": ax('ECU_R_ADDR_E + " 03 41 14 C8"')},
)
)
add(
FaultProfile(
id="iat_sensor_failure_cold",
name="IAT sensor failure (stuck cold)",
category="Sensors",
description="Implausible intake air temperature (-40°C).",
signature="IAT = -40°C",
typical_dtc="P0112",
llm_hint="Implausible IAT reading — sensor circuit low.",
overrides={"INTAKE_TEMP": ax('ECU_R_ADDR_E + " 03 41 0F 00"')},
)
)
add(
FaultProfile(
id="ect_sensor_failure",
name="Coolant temp sensor failure",
category="Sensors",
description="Coolant reads -40°C when engine is warm → ECU runs rich.",
signature="COOLANT_TEMP implausible, rich trims",
typical_dtc="P0117",
llm_hint="Impossible ECT with rich compensation trims.",
overrides={
"COOLANT_TEMP": ax('ECU_R_ADDR_E + " 03 41 05 00"'),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(-10):02X}"'
),
},
)
)
add(
FaultProfile(
id="map_sensor_failure",
name="MAP sensor failure",
category="Sensors",
description="Intake pressure inconsistent with RPM/throttle/MAF.",
signature="High RPM but vacuum reads like idle",
typical_dtc="P0106",
llm_hint="MAP does not match RPM and throttle — sensor performance fault.",
overrides={
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(3000)}"'),
"THROTTLE_POS": ax('ECU_R_ADDR_E + " 03 41 11 70"'),
"INTAKE_PRESSURE": ax('ECU_R_ADDR_E + " 03 41 0B 95"'),
"MAF": ax(f'ECU_R_ADDR_E + " 04 41 10 {maf_hex(45)}"'),
},
)
)
add(
FaultProfile(
id="tps_failure",
name="Throttle position sensor failure",
category="Sensors",
description="RPM changes but throttle reading stuck.",
signature="TPS stuck while RPM varies",
typical_dtc="P0122 / P0123",
llm_hint="Throttle position does not track RPM changes.",
overrides={
"THROTTLE_POS": ax('ECU_R_ADDR_E + " 03 41 11 20"'),
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(900, 2500)})'
),
},
)
)
add(
FaultProfile(
id="alternator_failure",
name="Alternator failure",
category="Electrical",
description="Charging system cannot maintain voltage.",
signature="Module voltage low while running (~12.7 V)",
typical_dtc="P0562",
llm_hint="Low control-module voltage while engine running.",
overrides={
"CONTROL_MODULE_VOLTAGE": ax(
f'ECU_R_ADDR_E + " 04 41 42 {volt_hex(12.7)}"'
),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(1100)}"'),
},
)
)
add(
FaultProfile(
id="voltage_regulator_failure",
name="Voltage regulator failure",
category="Electrical",
description="Overcharging while running.",
signature="15 V+ while running",
typical_dtc="P0563",
llm_hint="System voltage dangerously high — regulator fault.",
overrides={
"CONTROL_MODULE_VOLTAGE": ax(
f'ECU_R_ADDR_E + " 04 41 42 {volt_hex(15.8)}"'
),
},
)
)
add(
FaultProfile(
id="compression_loss",
name="Engine compression loss",
category="Mechanical",
description="Ring/valve wear — indirect OBD signs only.",
signature="Abnormal trims, misfires, poor power",
typical_dtc="P0300",
llm_hint="High throttle with low MAF/load and unstable RPM.",
overrides={
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(700, 1300)})'
),
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 30"'),
"THROTTLE_POS": ax('ECU_R_ADDR_E + " 03 41 11 C0"'),
"MAF": ax(f'ECU_R_ADDR_E + " 04 41 10 {maf_hex(22)}"'),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(10):02X}"'
),
},
)
)
add(
FaultProfile(
id="head_gasket_leak",
name="Head gasket leak",
category="Mechanical",
description="Coolant unstable, misfires, overheating tendency.",
signature="Unstable coolant, misfires, overheating",
typical_dtc="P0300",
llm_hint="Combined overheating and misfire pattern.",
overrides={
"COOLANT_TEMP": ax(
f'ECU_R_ADDR_E + " 03 41 05 %.2X" % int(95 + {_randint_expr(0, 20)})'
),
"RPM": ax(
f'ECU_R_ADDR_E + " 04 41 0C %.4X" % int(4 * {_randint_expr(650, 1200)})'
),
"SHORT_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 06 {trim_b(8):02X}"'
),
},
)
)
add(
FaultProfile(
id="timing_chain_stretch",
name="Timing chain stretch",
category="Mechanical",
description="Subtle performance and timing anomalies.",
signature="Reduced performance, timing advance low, LTFT +",
typical_dtc="—",
llm_hint="Subtle timing and trim anomalies at cruise — compare to baseline.",
overrides={
"TIMING_ADVANCE": ax('ECU_R_ADDR_E + " 03 41 0E 18"'),
"LONG_FUEL_TRIM_1": ax(
f'ECU_R_ADDR_E + " 03 41 07 {trim_b(7):02X}"'
),
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 55"'),
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(2400)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 55"'),
},
)
)
add(
FaultProfile(
id="transmission_slip",
name="Transmission slip (proxy)",
category="Drivetrain",
description="High RPM with low speed increase — limited OBD-II proxy.",
signature="High RPM, low speed, moderate load",
typical_dtc="—",
llm_hint="RPM high relative to speed — possible slip (proxy signal only).",
overrides={
"RPM": ax(f'ECU_R_ADDR_E + " 04 41 0C {rpm_hex(4200)}"'),
"SPEED": ax('ECU_R_ADDR_E + " 03 41 0D 25"'),
"ENGINE_LOAD": ax('ECU_R_ADDR_E + " 03 41 04 90"'),
},
)
)
return faults
FAULTS: dict[str, FaultProfile] = _build_faults()
def list_fault_choices() -> list[tuple[str, str]]:
"""Return Gradio dropdown choices grouped by category."""
by_category: dict[str, list[tuple[str, str]]] = {}
for fault in FAULTS.values():
label = f"{fault.category}: {fault.name}"
by_category.setdefault(fault.category, []).append((label, fault.id))
choices: list[tuple[str, str]] = [("Healthy (no fault)", "none")]
for category in sorted(by_category):
for label, fault_id in sorted(by_category[category], key=lambda x: x[0]):
choices.append((label, fault_id))
return choices
def get_fault(fault_id: str | None) -> FaultProfile | None:
if not fault_id or fault_id == "none":
return None
return FAULTS.get(fault_id)
def get_active_fault_id() -> str | None:
with _state_lock:
return _active_fault_id
def get_active_fault() -> FaultProfile | None:
return get_fault(get_active_fault_id())
def describe_fault(fault_id: str | None) -> str:
fault = get_fault(fault_id)
if fault is None:
return (
"**Healthy baseline** — no PID overrides. "
"The emulator uses the default `car` scenario values."
)
return (
f"### {fault.name}\n\n"
f"{fault.description}\n\n"
f"**OBD signature:** {fault.signature}\n\n"
f"**Typical DTC:** `{fault.typical_dtc}`\n\n"
f"**Hint:** {fault.llm_hint}\n\n"
f"**Overrides:** {len(fault.overrides)} PID(s)"
+ (f" + GET_DTC" if parse_dtc_codes(fault.typical_dtc) else "")
+ " — "
+ ", ".join(f"`{k}`" for k in fault.overrides)
+ (", `GET_DTC`" if parse_dtc_codes(fault.typical_dtc) else "")
)
FAULT_INVESTIGATION_PROMPT = (
"Something seems wrong with my vehicle. "
"Please check live OBD vitals, recent stored snapshots, and any stored trouble codes "
"to figure out what's going on and recommend what I should do next."
)
def fault_investigation_prompt() -> str:
"""Generic driver message — agent must discover the fault via OBD tools."""
return FAULT_INVESTIGATION_PROMPT
def _clear_emulator_overrides(emulator: Any) -> None:
"""Drop PID/DTC overrides; scenario defaults apply without a full reset."""
emulator.answer.clear()
def _apply_overrides(emulator: Any, overrides: dict[str, str]) -> None:
_clear_emulator_overrides(emulator)
for pid, answer_xml in overrides.items():
emulator.answer[pid] = answer_xml
def _require_emulator() -> Any:
"""Return the in-process emulator, starting it only if missing."""
emulator = get_emulator()
if emulator is not None:
return emulator
ensure_elm_emulator()
emulator = get_emulator()
if emulator is None:
raise RuntimeError("ELM327 emulator is not ready")
return emulator
def apply_fault(fault_id: str) -> str:
"""Apply a fault profile to the running emulator."""
global _active_fault_id
if fault_id == "none":
return clear_faults()
fault = FAULTS.get(fault_id)
if fault is None:
raise ValueError(f"Unknown fault profile: {fault_id}")
emulator = _require_emulator()
with obd_session_lock:
with _emulator_lock:
_apply_overrides(emulator, _effective_overrides(fault))
with _state_lock:
_active_fault_id = fault_id
logger.info("Applied fault simulation: %s (%s)", fault.id, fault.name)
return (
f"**Active fault:** {fault.name} (`{fault.id}`)\n\n"
f"Typical DTC: `{fault.typical_dtc}`. Vitals refreshed and stored."
)
def clear_faults() -> str:
"""Reset emulator overrides and return to healthy car scenario."""
global _active_fault_id
emulator = _require_emulator()
with obd_session_lock:
with _emulator_lock:
_clear_emulator_overrides(emulator)
with _state_lock:
_active_fault_id = None
logger.info("Cleared fault simulation overrides")
return "**Healthy baseline** — all PID overrides cleared."