Tier 2: WiFi + Bluetooth ELM327 transports
- obdcore/transport.py: pluggable byte transports -- SerialTransport,
TcpTransport (WiFi ELM327, stdlib socket), BleTransport (experimental, via
optional 'bleak'; background asyncio loop buffering notifications). ble_scan().
- ElmLink refactored onto a transport with .serial()/.tcp()/.ble() factories
(close/cmd now go through self.io); no behavior change for serial.
- Controller.connect(conn={kind:serial|wifi|ble,...}); GUI connection bar gains
a transport selector (Serial/USB/BT-SPP | WiFi host:port | Bluetooth LE + Scan).
- Classic-Bluetooth needs no new code (pairs as a serial port); WiFi needs no
extra deps; BLE is opt-in (bleak not bundled, so CI binaries keep building).
- tests/test_transport.py: drives ElmLink over a fake ELM TCP server end-to-end
(connect, RPM, readiness, VIN). All 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:
+11
-2
@@ -55,12 +55,21 @@ class Controller:
|
||||
self.reg = PidRegistry(self.profile)
|
||||
self.dtcdb = DtcDatabase(self.profile)
|
||||
|
||||
def connect(self, port=None, baud=38400, mock=False):
|
||||
def connect(self, port=None, baud=38400, mock=False, conn=None):
|
||||
"""conn: optional {'kind': 'serial'|'wifi'|'ble', ...}. Falls back to
|
||||
serial(port, baud) for backward compatibility."""
|
||||
if mock:
|
||||
self.link = MockLink(clock=time.time)
|
||||
else:
|
||||
from obdcore.link import ElmLink # imported lazily (needs pyserial)
|
||||
self.link = ElmLink(port, baud)
|
||||
c = conn or {"kind": "serial", "port": port, "baud": baud}
|
||||
kind = c.get("kind", "serial")
|
||||
if kind == "wifi":
|
||||
self.link = ElmLink.tcp(c["host"], c.get("port", 35000))
|
||||
elif kind == "ble":
|
||||
self.link = ElmLink.ble(c["address"])
|
||||
else:
|
||||
self.link = ElmLink.serial(c.get("port", port), c.get("baud", baud))
|
||||
self.link.init()
|
||||
ok = self.link.connect()
|
||||
try:
|
||||
|
||||
+82
-13
@@ -156,16 +156,44 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
tb = QtWidgets.QToolBar("Connection")
|
||||
tb.setMovable(False)
|
||||
self.addToolBar(tb)
|
||||
tb.addWidget(QtWidgets.QLabel(" Port "))
|
||||
self.port_combo = QtWidgets.QComboBox()
|
||||
self.port_combo.setMinimumWidth(180)
|
||||
|
||||
self.conn_kind = QtWidgets.QComboBox()
|
||||
self.conn_kind.addItems(["Serial / USB / BT-SPP", "WiFi", "Bluetooth LE"])
|
||||
self.conn_kind.currentIndexChanged.connect(self._conn_kind_changed)
|
||||
tb.addWidget(self.conn_kind)
|
||||
|
||||
# serial inputs
|
||||
self._serial_w = []
|
||||
self._serial_w.append(tb.addWidget(QtWidgets.QLabel(" Port ")))
|
||||
self.port_combo = QtWidgets.QComboBox(); self.port_combo.setMinimumWidth(180)
|
||||
self._refresh_ports()
|
||||
tb.addWidget(self.port_combo)
|
||||
self._serial_w.append(tb.addWidget(self.port_combo))
|
||||
b = QtWidgets.QToolButton(); b.setText("↻"); b.clicked.connect(self._refresh_ports)
|
||||
tb.addWidget(b)
|
||||
tb.addWidget(QtWidgets.QLabel(" Baud "))
|
||||
self.baud_edit = QtWidgets.QLineEdit("38400"); self.baud_edit.setFixedWidth(70)
|
||||
tb.addWidget(self.baud_edit)
|
||||
self._serial_w.append(tb.addWidget(b))
|
||||
self._serial_w.append(tb.addWidget(QtWidgets.QLabel(" Baud ")))
|
||||
self.baud_edit = QtWidgets.QLineEdit("38400"); self.baud_edit.setFixedWidth(64)
|
||||
self._serial_w.append(tb.addWidget(self.baud_edit))
|
||||
|
||||
# wifi inputs
|
||||
self._wifi_w = []
|
||||
self._wifi_w.append(tb.addWidget(QtWidgets.QLabel(" Host ")))
|
||||
self.host_edit = QtWidgets.QLineEdit("192.168.0.10"); self.host_edit.setFixedWidth(120)
|
||||
self._wifi_w.append(tb.addWidget(self.host_edit))
|
||||
self._wifi_w.append(tb.addWidget(QtWidgets.QLabel(" : ")))
|
||||
self.tcpport_edit = QtWidgets.QLineEdit("35000"); self.tcpport_edit.setFixedWidth(56)
|
||||
self._wifi_w.append(tb.addWidget(self.tcpport_edit))
|
||||
|
||||
# ble inputs
|
||||
self._ble_w = []
|
||||
self._ble_w.append(tb.addWidget(QtWidgets.QLabel(" Device ")))
|
||||
self.ble_combo = QtWidgets.QComboBox(); self.ble_combo.setMinimumWidth(200)
|
||||
self.ble_combo.setEditable(True)
|
||||
self._ble_w.append(tb.addWidget(self.ble_combo))
|
||||
self.ble_scan_btn = QtWidgets.QToolButton(); self.ble_scan_btn.setText("Scan")
|
||||
self.ble_scan_btn.clicked.connect(self._ble_scan)
|
||||
self._ble_w.append(tb.addWidget(self.ble_scan_btn))
|
||||
|
||||
tb.addSeparator()
|
||||
self.mock_chk = QtWidgets.QCheckBox("Mock"); tb.addWidget(self.mock_chk)
|
||||
self.connect_btn = QtWidgets.QPushButton("Connect")
|
||||
self.connect_btn.clicked.connect(self._toggle_connect)
|
||||
@@ -174,6 +202,35 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
self._preset_tb = tb
|
||||
self._preset_sep = tb.addSeparator()
|
||||
self._preset_buttons = []
|
||||
self._conn_kind_changed()
|
||||
|
||||
def _conn_kind_changed(self):
|
||||
k = self.conn_kind.currentIndex()
|
||||
for a in self._serial_w:
|
||||
a.setVisible(k == 0)
|
||||
for a in self._wifi_w:
|
||||
a.setVisible(k == 1)
|
||||
for a in self._ble_w:
|
||||
a.setVisible(k == 2)
|
||||
|
||||
def _ble_scan(self):
|
||||
try:
|
||||
from obdcore.transport import ble_scan
|
||||
except Exception:
|
||||
QtWidgets.QMessageBox.information(self, "Bluetooth LE",
|
||||
"BLE needs the 'bleak' package (pip install bleak). Classic-Bluetooth "
|
||||
"ELM327 pair as a serial port — use Serial instead.")
|
||||
return
|
||||
self.status.showMessage("Scanning for BLE devices…")
|
||||
QtWidgets.QApplication.processEvents()
|
||||
try:
|
||||
devs = ble_scan(timeout=6.0)
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(self, "BLE scan failed", str(e)); return
|
||||
self.ble_combo.clear()
|
||||
for addr, name in devs:
|
||||
self.ble_combo.addItem(f"{name} [{addr}]", addr)
|
||||
self.status.showMessage(f"Found {len(devs)} BLE device(s).")
|
||||
|
||||
def _rebuild_preset_buttons(self):
|
||||
for b in self._preset_buttons:
|
||||
@@ -593,16 +650,28 @@ class MainWindow(QtWidgets.QMainWindow):
|
||||
if not ports:
|
||||
self.port_combo.addItem("(no ports found)", None)
|
||||
|
||||
def _toggle_connect(self):
|
||||
if self.ctl.connected:
|
||||
self._disconnect(); return
|
||||
port = self.port_combo.currentData()
|
||||
def _conn_spec(self):
|
||||
k = self.conn_kind.currentIndex()
|
||||
if k == 1:
|
||||
try:
|
||||
p = int(self.tcpport_edit.text())
|
||||
except ValueError:
|
||||
p = 35000
|
||||
return {"kind": "wifi", "host": self.host_edit.text().strip(), "port": p}
|
||||
if k == 2:
|
||||
addr = self.ble_combo.currentData() or self.ble_combo.currentText().strip()
|
||||
return {"kind": "ble", "address": addr}
|
||||
try:
|
||||
baud = int(self.baud_edit.text())
|
||||
except ValueError:
|
||||
baud = 38400
|
||||
return {"kind": "serial", "port": self.port_combo.currentData(), "baud": baud}
|
||||
|
||||
def _toggle_connect(self):
|
||||
if self.ctl.connected:
|
||||
self._disconnect(); return
|
||||
try:
|
||||
ok = self.ctl.connect(port=port, baud=baud, mock=self.mock_chk.isChecked())
|
||||
ok = self.ctl.connect(mock=self.mock_chk.isChecked(), conn=self._conn_spec())
|
||||
except Exception as e:
|
||||
QtWidgets.QMessageBox.critical(self, "Connect failed", str(e)); return
|
||||
self.ctl.start(); self.timer.start()
|
||||
|
||||
Reference in New Issue
Block a user