Fix slow OBD value load on dashboard (BUG-19)

This commit is contained in:
Mathieu Moran
2026-08-13 16:57:16 -04:00
parent e873db918d
commit c2462356e1
5 changed files with 141 additions and 4 deletions
+16
View File
@@ -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 - Keep `systemd-timesyncd` for online NTP; fall back to the RTC (DS3231) and optionally
GNSS time when offline. 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 ## Resolved
+12 -4
View File
@@ -203,11 +203,19 @@ def gps_status():
@app.route('/api/obd/status') @app.route('/api/obd/status')
def obd_status(): def obd_status():
port = app_obd._find_obd_port()
connected = app_obd._is_car_connected()
return jsonify({ return jsonify({
"adapter_present": bool(app_obd._find_obd_port()), "adapter_present": bool(port),
"port": app_obd._find_obd_port(), "port": port,
"car_connected": app_obd._is_car_connected(), "car_connected": connected,
"status_label": "Connected" if app_obd._is_car_connected() else "Disconnected", "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/<int:trip_id>') @app.route('/api/trip/<int:trip_id>')
+42
View File
@@ -271,9 +271,51 @@ def get_fresh_speed(max_age=5):
return speed 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(): def obd_connection():
"""Emit current state for a newly connected browser and ensure OBD is running.""" """Emit current state for a newly connected browser and ensure OBD is running."""
start_obd() start_obd()
emit_current_state()
def _persist_car_connected(): def _persist_car_connected():
+17
View File
@@ -314,9 +314,26 @@ async function refreshObdStatus() {
if (el) el.textContent = status.adapter_present if (el) el.textContent = status.adapter_present
? (status.car_connected ? 'Connected' : 'Adapter only') ? (status.car_connected ? 'Connected' : 'Adapter only')
: 'Not detected'; : '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) { } catch (e) {
// Socket events will eventually update it // 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(); refreshObdStatus();
window.setInterval(refreshObdStatus, 15000); window.setInterval(refreshObdStatus, 15000);
+54
View File
@@ -72,5 +72,59 @@ class ClearDtcCodesTest(unittest.TestCase):
mock_mark.assert_called_once() 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__": if __name__ == "__main__":
unittest.main() unittest.main()