Fixed premission-issues with additional script
This commit is contained in:
154
app/obd2.py
Normal file
154
app/obd2.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# app/obd2.py — OBD-II Responder (SocketCAN) + Payload-Helpers
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, Dict, Optional
|
||||
|
||||
import can
|
||||
|
||||
from .can import link_state # nur für "IF ist UP?"-Check
|
||||
|
||||
# 11-bit OBD-II IDs (ISO 15765-4)
|
||||
OBD_REQ_ID = 0x7DF
|
||||
PID_SPEED = 0x0D
|
||||
PID_RPM = 0x0C
|
||||
|
||||
class ObdResponder:
|
||||
"""
|
||||
OBD-II Mode 01 PID-Responder (11-bit) über SocketCAN.
|
||||
- eigener Polling-RX-Loop (kein Notifier -> keine Stacktraces beim Link DOWN)
|
||||
- rebind(interface/resp_id) schließt Bus; Loop öffnet neu, sobald IF=UP
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
interface: str,
|
||||
resp_id: int,
|
||||
timeout_ms: int = 200,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
):
|
||||
self.interface = interface
|
||||
self.resp_id = resp_id
|
||||
self.timeout_ms = timeout_ms
|
||||
self.log = logger or logging.getLogger("obdcan")
|
||||
|
||||
self.providers: Dict[int, Callable[[], bytes]] = {}
|
||||
self._run = threading.Event(); self._run.set()
|
||||
|
||||
self.bus: Optional[can.BusABC] = None
|
||||
self._thread = threading.Thread(target=self._service_loop, name="OBD-SVC", daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
# ---------- Lifecycle ----------
|
||||
def _open_bus(self) -> None:
|
||||
self._close_bus()
|
||||
self.bus = can.interface.Bus(channel=self.interface, interface="socketcan")
|
||||
self.log.info("OBD responder started on %s (resp_id=0x%03X)", self.interface, self.resp_id)
|
||||
|
||||
def _close_bus(self) -> None:
|
||||
try:
|
||||
if self.bus:
|
||||
self.bus.shutdown()
|
||||
except Exception:
|
||||
pass
|
||||
self.bus = None
|
||||
|
||||
def stop(self) -> None:
|
||||
self._run.clear()
|
||||
try:
|
||||
self._thread.join(timeout=1.0)
|
||||
except RuntimeError:
|
||||
pass
|
||||
self._close_bus()
|
||||
|
||||
def rebind(self, interface: Optional[str] = None, resp_id: Optional[int] = None) -> None:
|
||||
if interface is not None:
|
||||
self.interface = interface
|
||||
if resp_id is not None:
|
||||
self.resp_id = resp_id
|
||||
self._close_bus()
|
||||
self.log.info("Rebind requested: %s, resp=0x%03X", self.interface, self.resp_id)
|
||||
|
||||
# ---------- API ----------
|
||||
def register_pid(self, pid: int, provider: Callable[[], bytes]) -> None:
|
||||
self.providers[pid] = provider
|
||||
|
||||
# ---------- RX/Service ----------
|
||||
def _service_loop(self) -> None:
|
||||
backoff = 0.5
|
||||
while self._run.is_set():
|
||||
if self.bus is None:
|
||||
# Öffnen nur, wenn Interface UP ist
|
||||
if link_state(self.interface) == "UP":
|
||||
try:
|
||||
self._open_bus()
|
||||
backoff = 0.5
|
||||
except Exception as e:
|
||||
self.log.warning("Bus open failed: %s", e)
|
||||
time.sleep(backoff); backoff = min(5.0, backoff * 1.7)
|
||||
continue
|
||||
else:
|
||||
time.sleep(0.5); continue
|
||||
|
||||
try:
|
||||
msg = self.bus.recv(0.05)
|
||||
if msg is not None:
|
||||
self._handle(msg)
|
||||
except (can.CanOperationError, OSError):
|
||||
# IF ging down -> Bus schließen und später neu öffnen
|
||||
self.log.info("CAN went DOWN — closing bus, will retry…")
|
||||
self._close_bus()
|
||||
time.sleep(0.5)
|
||||
except Exception as e:
|
||||
self.log.warning("CAN recv error: %s", e)
|
||||
time.sleep(0.1)
|
||||
|
||||
def _handle(self, msg: can.Message) -> None:
|
||||
if msg.is_extended_id or msg.arbitration_id != OBD_REQ_ID:
|
||||
return
|
||||
data = bytes(msg.data)
|
||||
if len(data) < 2:
|
||||
return
|
||||
|
||||
# tolerant: 02 01 <pid> ... oder 01 <pid> ...
|
||||
if data[0] == 0x02 and len(data) >= 3:
|
||||
mode, pid = data[1], data[2]
|
||||
else:
|
||||
mode, pid = data[0], (data[1] if len(data) >= 2 else None)
|
||||
|
||||
if mode != 0x01 or pid is None:
|
||||
return
|
||||
|
||||
provider = self.providers.get(pid)
|
||||
if not provider:
|
||||
return
|
||||
|
||||
try:
|
||||
payload = provider()
|
||||
if not isinstance(payload, (bytes, bytearray)) or len(payload) != 8:
|
||||
return
|
||||
out = can.Message(
|
||||
arbitration_id=self.resp_id,
|
||||
is_extended_id=False,
|
||||
data=payload,
|
||||
dlc=8
|
||||
)
|
||||
if self.bus:
|
||||
self.bus.send(out)
|
||||
except can.CanError:
|
||||
self.log.warning("CAN send failed (bus off?)")
|
||||
except Exception as e:
|
||||
self.log.exception("Provider error: %s", e)
|
||||
|
||||
# ------------------------- Payload-Helpers ----------------------------------
|
||||
|
||||
def make_speed_response(speed_kmh: int) -> bytes:
|
||||
A = max(0, min(255, int(speed_kmh)))
|
||||
return bytes([0x03, 0x41, PID_SPEED, A, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
def make_rpm_response(rpm: int) -> bytes:
|
||||
raw = max(0, int(rpm)) * 4
|
||||
A = (raw >> 8) & 0xFF
|
||||
B = raw & 0xFF
|
||||
return bytes([0x04, 0x41, PID_RPM, A, B, 0x00, 0x00, 0x00])
|
Reference in New Issue
Block a user