diff --git a/BUGS.md b/BUGS.md index 5a2dd49..ffc62af 100644 --- a/BUGS.md +++ b/BUGS.md @@ -37,6 +37,22 @@ When CLOSED, move the entire entry to Resolved — don't duplicate. - Keep `systemd-timesyncd` for online NTP; fall back to the RTC (DS3231) and optionally GNSS time when offline. +### BUG-19 — Dashboard OBD values slow to appear while the car is running +- **Status**: `OPEN` 2026-08-13 +- **Phase**: 29 +- **Symptom**: on the Pi dashboard, the speed/RPM/coolant/fuel/throttle cards stay on + "—" for a long time after the page loads (or after ignition) before populating, even + though the car is running and OBD is connected. +- **Cause**: the value cards were populated **only** by socket.io push events from the + OBD polling threads — the dashboard never pulled a current snapshot on load, so a + freshly loaded page had to wait for the next poll tick (plus any reconnect/probe + latency). `/api/obd/status` only returned the connection state, not the actual + values, so there was no REST fallback either. +- **Fix**: emit a full current-value snapshot on socket connect, and extend + `/api/obd/status` to return live speed/RPM/coolant/fuel/throttle/MIL so the dashboard + fills the cards immediately on load (and every 15 s) via REST. Needs in-car + verification. + --- ## Resolved diff --git a/app.py b/app.py index ddf1a40..e38efef 100644 --- a/app.py +++ b/app.py @@ -203,11 +203,19 @@ def gps_status(): @app.route('/api/obd/status') def obd_status(): + port = app_obd._find_obd_port() + connected = app_obd._is_car_connected() return jsonify({ - "adapter_present": bool(app_obd._find_obd_port()), - "port": app_obd._find_obd_port(), - "car_connected": app_obd._is_car_connected(), - "status_label": "Connected" if app_obd._is_car_connected() else "Disconnected", + "adapter_present": bool(port), + "port": port, + "car_connected": connected, + "status_label": "Connected" if connected else "Disconnected", + "speed": app_obd.get_fresh_speed(), + "rpm": app_obd.rpm, + "coolant_temp": app_obd.coolant_temp, + "fuel_level": app_obd.fuel_level, + "throttle_position": app_obd.throttle_position, + "mil_on": app_obd.get_fresh_mil(), }) @app.route('/api/trip/') diff --git a/modules/app_obd.py b/modules/app_obd.py index e304543..181b147 100644 --- a/modules/app_obd.py +++ b/modules/app_obd.py @@ -271,9 +271,51 @@ def get_fresh_speed(max_age=5): return speed +def emit_current_state(): + """Push the current OBD values to a newly connected dashboard. + + The polling threads only emit on their next tick, so a freshly loaded + page would otherwise sit on "—" until then. Emitting a snapshot here makes + the cards fill immediately on connect/reconnect instead of waiting for the + next poll cycle (or a reconnect/probe round-trip). + """ + connected = _is_car_connected() + app_events.socketio.emit( + "obd_status", obd_status[1] if connected else obd_status[0] + ) + if connected: + app_events.socketio.emit("obd_speed", get_speedtime()) + app_events.socketio.emit("obd_rpm", rpm if rpm is not None else "-") + app_events.socketio.emit( + "obd_coolant_temp", + coolant_temp if coolant_temp is not None else "-", + ) + app_events.socketio.emit( + "obd_fuel_level", + fuel_level if fuel_level is not None else "-", + ) + app_events.socketio.emit( + "obd_throttle", + throttle_position if throttle_position is not None else "-", + ) + if mil_on is not None: + app_events.socketio.emit( + "obd_mil", {"on": mil_on, "dtc_count": len(dtc_codes)} + ) + if dtc_codes: + app_events.socketio.emit("obd_dtc", dtc_codes) + else: + app_events.socketio.emit("obd_speed", "-") + app_events.socketio.emit("obd_rpm", "-") + app_events.socketio.emit("obd_coolant_temp", "-") + app_events.socketio.emit("obd_fuel_level", "-") + app_events.socketio.emit("obd_throttle", "-") + + def obd_connection(): """Emit current state for a newly connected browser and ensure OBD is running.""" start_obd() + emit_current_state() def _persist_car_connected(): diff --git a/static/js/yaris.js b/static/js/yaris.js index 0475b25..4d2092d 100644 --- a/static/js/yaris.js +++ b/static/js/yaris.js @@ -314,9 +314,26 @@ async function refreshObdStatus() { if (el) el.textContent = status.adapter_present ? (status.car_connected ? 'Connected' : 'Adapter only') : 'Not detected'; + + // Fill the value cards from the REST snapshot so they appear immediately + // on load, even if the socket connection is slow to establish. Socket + // events keep updating them live afterward. + if (status.car_connected) { + fillObdValueCard('obd-speed', status.speed, 'km/h'); + fillObdValueCard('obd-rpm', status.rpm, 'rpm'); + fillObdValueCard('obd-coolant', status.coolant_temp, '°C'); + fillObdValueCard('obd-fuel', status.fuel_level, '%'); + fillObdValueCard('obd-throttle', status.throttle_position, '%'); + } } catch (e) { // Socket events will eventually update it } } + +function fillObdValueCard(id, value, unit) { + if (value == null) return; + const el = document.getElementById(id); + if (el) el.textContent = value + ' ' + unit; +} refreshObdStatus(); window.setInterval(refreshObdStatus, 15000); diff --git a/tests/test_app_obd.py b/tests/test_app_obd.py index a51099f..92fcdea 100644 --- a/tests/test_app_obd.py +++ b/tests/test_app_obd.py @@ -72,5 +72,59 @@ class ClearDtcCodesTest(unittest.TestCase): mock_mark.assert_called_once() +class EmitCurrentStateTest(unittest.TestCase): + def setUp(self): + self._saved = { + name: getattr(app_obd, name) + for name in ( + "speed", "rpm", "coolant_temp", "fuel_level", + "throttle_position", "mil_on", "dtc_codes", + ) + } + + def tearDown(self): + for name, value in self._saved.items(): + setattr(app_obd, name, value) + + def _emitted(self, mock_emit): + return {call.args[0]: call.args[1] for call in mock_emit.call_args_list} + + def test_connected_emits_current_values(self): + app_obd.speed = 42.5 + app_obd.rpm = 1200 + app_obd.coolant_temp = 88.0 + app_obd.fuel_level = 60.0 + app_obd.throttle_position = 12.0 + app_obd.mil_on = False + app_obd.dtc_codes = [] + with patch.object(app_obd, "_is_car_connected", return_value=True), \ + patch("modules.app_events.socketio.emit") as mock_emit: + app_obd.emit_current_state() + emitted = self._emitted(mock_emit) + self.assertEqual(emitted["obd_status"], "Connected") + self.assertEqual(emitted["obd_rpm"], 1200) + self.assertEqual(emitted["obd_coolant_temp"], 88.0) + self.assertEqual(emitted["obd_fuel_level"], 60.0) + self.assertEqual(emitted["obd_throttle"], 12.0) + # speed is emitted as [time, speed] + self.assertEqual(emitted["obd_speed"][1], 42.5) + + def test_disconnected_emits_dashes(self): + app_obd.rpm = None + app_obd.coolant_temp = None + app_obd.fuel_level = None + app_obd.throttle_position = None + with patch.object(app_obd, "_is_car_connected", return_value=False), \ + patch("modules.app_events.socketio.emit") as mock_emit: + app_obd.emit_current_state() + emitted = self._emitted(mock_emit) + self.assertEqual(emitted["obd_status"], "Disconnected") + self.assertEqual(emitted["obd_speed"], "-") + self.assertEqual(emitted["obd_rpm"], "-") + self.assertEqual(emitted["obd_coolant_temp"], "-") + self.assertEqual(emitted["obd_fuel_level"], "-") + self.assertEqual(emitted["obd_throttle"], "-") + + if __name__ == "__main__": unittest.main()