Fix slow OBD value load on dashboard (BUG-19)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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>')
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user