#2 (framework): bi-directional / service-function engine
Profile-defined UDS action sequences, run safely -- the framework for #2 (real per-vehicle actuator tests/resets are follow-on, added as verified profile data). - obdcore/actions.py: Action model + run_action() executing session (Mode 10) -> security (Mode 27 seed->key) -> command steps (2F/31/11/3E/... any hex) with positive/negative response checks. Security KEY algorithms are per-vehicle secrets and NOT bundled -- only trivial transforms (xor-ff/invert/add-ff) known; an action naming an unknown algorithm is BLOCKED (fails safe). Never synthesizes bytes -- runs only what the profile defines. validate_action() rejects malformed hex at load. - profile.py: load/save an actions[] block; ElmLink/MockLink read_raw(hex). - GUI: Diagnostics -> Service & Bi-directional dialog -- lists the profile's actions with risk badges; caution/danger gated behind a warning confirmation. - generic-obd2: two safe STANDARD actions (Tester-Present ping; ECU-Reset, caution + engine-off warning). PROFILE_SPEC.md documents the actions schema + safety rules. - tests/test_actions.py: runner, session+reset, security handshake, unknown-algo block, hex validation, profile load. All 5 suites pass. 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:
@@ -153,6 +153,14 @@ class Controller:
|
||||
def read_freeze_frame(self):
|
||||
return self._oneoff(lambda: self.link.read_freeze_frame())
|
||||
|
||||
# -- bi-directional / service actions --
|
||||
def actions(self):
|
||||
return self.profile.actions or []
|
||||
|
||||
def run_action(self, action):
|
||||
from obdcore.actions import run_action
|
||||
return self._oneoff(lambda: run_action(action, self.link), timeout=20.0)
|
||||
|
||||
# -- trip / performance (fed from the live store each GUI tick) --
|
||||
def update_trip(self):
|
||||
spd = self.store.latest(self.speed_key) if self.speed_key else None
|
||||
|
||||
+65
@@ -87,6 +87,9 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
"I/M readiness monitors + MIL (will it pass inspection?)")
|
||||
self._act(diagm, "Vehicle Info (VIN)", self._vehicle_info,
|
||||
"VIN, calibration IDs, ECU name (mode 09)")
|
||||
diagm.addSeparator()
|
||||
self._act(diagm, "Service & Bi-directional…", self._service_actions,
|
||||
"Actuator tests, service resets, and other profile-defined functions")
|
||||
|
||||
viewm = mb.addMenu("&View")
|
||||
self.view_graph = self._act(viewm, "Graph View", lambda: self._set_view(0),
|
||||
@@ -483,6 +486,68 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
bb.rejected.connect(dlg.reject); lay.addWidget(bb)
|
||||
dlg.exec()
|
||||
|
||||
_RISK_COLOR = {"safe": "#3cb44b", "caution": "#e6a23c", "danger": "#e6194B"}
|
||||
|
||||
def _service_actions(self):
|
||||
if not self._need_connection():
|
||||
return
|
||||
acts = self.ctl.actions()
|
||||
dlg = QtWidgets.QDialog(self); dlg.setWindowTitle("Service & Bi-directional Functions")
|
||||
dlg.resize(560, 420)
|
||||
lay = QtWidgets.QVBoxLayout(dlg)
|
||||
if not acts:
|
||||
lay.addWidget(QtWidgets.QLabel(
|
||||
"No service functions are defined for this vehicle profile yet.\n\n"
|
||||
"These are manufacturer-specific — add them to the profile's \"actions\"\n"
|
||||
"block (see profiles/PROFILE_SPEC.md). OBDash never synthesizes command\n"
|
||||
"bytes; it only runs what a verified profile defines."))
|
||||
else:
|
||||
lay.addWidget(QtWidgets.QLabel(
|
||||
"<b>Caution:</b> these send commands to the vehicle. Read each warning."))
|
||||
scroll = QtWidgets.QScrollArea(); scroll.setWidgetResizable(True)
|
||||
inner = QtWidgets.QWidget(); il = QtWidgets.QVBoxLayout(inner)
|
||||
for a in acts:
|
||||
row = QtWidgets.QFrame()
|
||||
row.setStyleSheet("QFrame{border:1px solid #333;border-radius:6px;}")
|
||||
rl = QtWidgets.QHBoxLayout(row)
|
||||
txt = QtWidgets.QLabel(
|
||||
f"<b>{a.name}</b> "
|
||||
f"<span style='color:{self._RISK_COLOR.get(a.risk,'#999')}'>[{a.risk}]</span>"
|
||||
f"<br><span style='color:#999'>{a.description}</span>")
|
||||
txt.setWordWrap(True)
|
||||
rl.addWidget(txt, 1)
|
||||
btn = QtWidgets.QPushButton("Run")
|
||||
btn.clicked.connect(lambda _=False, act=a: self._run_action(act))
|
||||
rl.addWidget(btn)
|
||||
il.addWidget(row)
|
||||
il.addStretch(1)
|
||||
scroll.setWidget(inner); lay.addWidget(scroll)
|
||||
bb = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Close)
|
||||
bb.rejected.connect(dlg.reject); lay.addWidget(bb)
|
||||
dlg.exec()
|
||||
|
||||
def _run_action(self, action):
|
||||
if action.risk in ("caution", "danger"):
|
||||
msg = (action.warning or "This sends a command to the vehicle.") + \
|
||||
"\n\nProceed?"
|
||||
btn = QtWidgets.QMessageBox.warning(
|
||||
self, f"Run: {action.name}", msg,
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
|
||||
QtWidgets.QMessageBox.No)
|
||||
if btn != QtWidgets.QMessageBox.Yes:
|
||||
return
|
||||
try:
|
||||
res = self.ctl.run_action(action) or {}
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(self, action.name, str(e)); return
|
||||
if res.get("ok"):
|
||||
QtWidgets.QMessageBox.information(self, action.name, res.get("message", "Done."))
|
||||
self.status.showMessage(f"{action.name}: {res.get('message','done')}")
|
||||
else:
|
||||
QtWidgets.QMessageBox.warning(self, action.name,
|
||||
"Failed: " + res.get("message", "no response"))
|
||||
self.status.showMessage(f"{action.name} failed: {res.get('message','')}")
|
||||
|
||||
# ---------- center (graph + table stack) ----------
|
||||
def _build_center(self):
|
||||
self.stack = QtWidgets.QStackedWidget()
|
||||
|
||||
Reference in New Issue
Block a user