From a0be96dfaa1215d6c7109b5f144d75f1be5e4ed7 Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Fri, 14 Aug 2026 23:00:53 -0400 Subject: [PATCH] fix: BUG-22 smooth GPS-derived speeds in trip max (trailing window) --- modules/app_trip.py | 27 ++++++++++++++++++++++++++- server/modules/db.py | 17 ++++++++++------- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/modules/app_trip.py b/modules/app_trip.py index c295712..0244256 100644 --- a/modules/app_trip.py +++ b/modules/app_trip.py @@ -7,6 +7,7 @@ GPS_MOTION_METERS = 8 MIN_GPS_MOTION_SAMPLES = 3 MAX_GPS_SAMPLE_GAP_SECONDS = 30 MAX_PLAUSIBLE_SPEED_KMH = 220 +GPS_SPEED_WINDOW = 6 # ~30 s at 5 s logging — smooths GPS position jitter EARTH_RADIUS_KM = 6371.0088 @@ -180,6 +181,7 @@ def build_trip_summaries(raw_rows): distance_km = 0 segment_speeds = [] obd_speeds = [] + gps_speeds = [] has_obd_motion = any(event["obd"] for event in events) for previous, row in zip(trip_rows, trip_rows[1:]): segment_km, segment_speed = _segment(row, previous) @@ -189,6 +191,29 @@ def build_trip_summaries(raw_rows): segment_speeds.append(segment_speed) if row["speed"] > 0: obd_speeds.append(row["speed"]) + # Smoothed GPS-derived speeds for the max — a trailing ~30s window + # averages out GPS position jitter (raw 5s segments can spike). + for index in range(1, len(trip_rows)): + if trip_rows[index]["speed"] > 0: + continue # OBD present here — already counted in obd_speeds + window_start = max(0, index - GPS_SPEED_WINDOW) + start_row = trip_rows[window_start] + end_row = trip_rows[index] + if ( + start_row["latitude"] is not None + and end_row["latitude"] is not None + ): + seconds = ( + end_row["datetime"] - start_row["datetime"] + ).total_seconds() + window_km = haversine_km( + (start_row["latitude"], start_row["longitude"]), + (end_row["latitude"], end_row["longitude"]), + ) + if seconds > 0 and window_km * 1000 >= GPS_MOTION_METERS: + window_speed = window_km / seconds * 3600 + if window_speed <= MAX_PLAUSIBLE_SPEED_KMH: + gps_speeds.append(window_speed) start = trip_rows[0] end = trip_rows[-1] @@ -227,7 +252,7 @@ def build_trip_summaries(raw_rows): distance_km / duration_seconds * 3600, 1 ) if duration_seconds else 0, "max_speed_kmh": round( - max((obd_speeds + segment_speeds) or [0]), 1 + max((obd_speeds + gps_speeds) or [0]), 1 ), "start_odometer": next((r["odometer"] for r in trip_rows if r.get("odometer")), None), "end_odometer": next((r["odometer"] for r in reversed(trip_rows) if r.get("odometer")), None), diff --git a/server/modules/db.py b/server/modules/db.py index c2e4ea8..66226d7 100644 --- a/server/modules/db.py +++ b/server/modules/db.py @@ -461,13 +461,16 @@ def compute_trips_for_device(device_id): speeds.append(s) max_speeds.append(s) else: - # GPS-derived fallback for max (mirror the Pi): when OBD speed - # is missing, use the segment's GPS speed (>= 8 m, <= 220 km/h). - seconds = (seg_rows[i][0] - seg_rows[i - 1][0]).total_seconds() - if seconds > 0 and d * 1000 >= 8: - seg_speed = d / seconds * 3600 - if seg_speed <= 220: - max_speeds.append(seg_speed) + # GPS-derived fallback for max (mirror the Pi, but smoothed over + # a trailing ~30s window so GPS position jitter can't spike the max). + j = max(0, i - 6) + w_dt = (seg_rows[i][0] - seg_rows[j][0]).total_seconds() + w_d = haversine_km(seg_rows[j][1], seg_rows[j][2], + seg_rows[i][1], seg_rows[i][2]) + if w_dt > 0 and w_d * 1000 >= 8: + w_speed = w_d / w_dt * 3600 + if w_speed <= 220: + max_speeds.append(w_speed) duration_s = int((seg_rows[-1][0] - seg_rows[0][0]).total_seconds()) if duration_s < 30 or distance < 0.05: