From bfc8ddabd97cfd13f1570246c91f97a93377b69e Mon Sep 17 00:00:00 2001 From: Mathieu Moran Date: Sat, 15 Aug 2026 17:38:45 -0400 Subject: [PATCH] fix: smooth GPS-derived replay speed to kill jitter spikes route_points (Pi) and get_trip_route (server) derived GPS speed from a single 5s segment, so GPS position jitter could read 150+ km/h when the car wasn't going that fast. Both now average over a trailing ~30s window (same as the max-speed calc), so spikes are damped. --- modules/app_trip.py | 28 ++++++++++++++++++++++------ server/modules/db.py | 23 +++++++++++++++-------- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/modules/app_trip.py b/modules/app_trip.py index 9b01d40..77248b8 100644 --- a/modules/app_trip.py +++ b/modules/app_trip.py @@ -285,12 +285,28 @@ def route_points(raw_rows, max_points=2000, odometer_baseline=0.0, prior_km=0.0) for index, row in enumerate(rows): row["speed_source"] = "obd" if row["speed"] > 0 else "gps" if row["speed"] <= 0 and index: - distance_km, segment_speed = _segment(row, rows[index - 1]) - row["speed"] = ( - segment_speed - if distance_km * 1000 >= GPS_MOTION_METERS - else 0 - ) + # Derive GPS speed over a trailing ~30s window, not the raw 5s + # segment — a single segment can spike 100+ km/h on GPS jitter + # (same smoothing build_trip_summaries uses for max speed). + window_start = max(0, index - GPS_SPEED_WINDOW) + start_row = rows[window_start] + seconds = ( + row["datetime"] - start_row["datetime"] + ).total_seconds() + if seconds > 0: + window_km = haversine_km( + (start_row["latitude"], start_row["longitude"]), + (row["latitude"], row["longitude"]), + ) + window_speed = window_km / seconds * 3600 + row["speed"] = ( + window_speed + if window_km * 1000 >= GPS_MOTION_METERS + and window_speed <= MAX_PLAUSIBLE_SPEED_KMH + else 0 + ) + else: + row["speed"] = 0 if index: segment_km, _ = _segment(row, rows[index - 1]) cumulative_km += segment_km diff --git a/server/modules/db.py b/server/modules/db.py index 66226d7..d2cfb78 100644 --- a/server/modules/db.py +++ b/server/modules/db.py @@ -625,6 +625,7 @@ def get_trip_route(device_id, trip_id): result_points = [] cumulative = 0.0 prev = None + gps_window = [] # trailing (lat, lon, datetime) points for GPS speed smoothing for p in points: lat = float(p[1]) lon = float(p[2]) @@ -632,18 +633,24 @@ def get_trip_route(device_id, trip_id): raw_odo = p[4] speed_source = "obd" if speed > 0 else "gps" # Mirror the Pi's route_points GPS-speed fallback: when OBD speed is - # missing (adapter dropout), derive speed from the previous point's - # movement so the replay doesn't flatline at 0 km/h while the car moves. - if speed <= 0 and prev is not None: - seconds = (p[0] - prev[2]).total_seconds() + # missing (adapter dropout), derive speed from a trailing ~30s window + # of movement (not the previous point) so the replay doesn't flatline + # at 0 km/h while the car moves — and a single jittery 5s segment can't + # spike the readout. + if speed <= 0 and gps_window: + start = gps_window[0] + seconds = (p[0] - start[2]).total_seconds() if seconds > 0: - dist_km = hav(prev[0], prev[1], lat, lon) - seg_speed = dist_km / seconds * 3600 - if seg_speed <= 220 and dist_km * 1000 >= 8: - speed = seg_speed + dist_km = hav(start[0], start[1], lat, lon) + w_speed = dist_km / seconds * 3600 + if w_speed <= 220 and dist_km * 1000 >= 8: + speed = w_speed if prev is not None: cumulative += hav(prev[0], prev[1], lat, lon) prev = (lat, lon, p[0]) + gps_window.append((lat, lon, p[0])) + if len(gps_window) > 6: + gps_window.pop(0) if raw_odo is not None and float(raw_odo) > 0: odometer = float(raw_odo) else: