fix(phase26): server trip detection mirrors Pi motion-aware logic (BUG-04)
This commit is contained in:
+88
-38
@@ -280,7 +280,19 @@ _TRIP_GAP_S = 300 # 5 minutes
|
||||
|
||||
|
||||
def compute_trips_for_device(device_id):
|
||||
"""Recompute trip boundaries from tracking_points and upsert into trips table."""
|
||||
"""Recompute trip boundaries from tracking_points and upsert into trips table.
|
||||
|
||||
Mirrors the Pi's motion-aware trip-detection logic (modules/app_trip.py):
|
||||
OBD wheel speed > 1 km/h is the primary motion signal. GPS displacement
|
||||
>= 8 m per step (3 consecutive) is the fallback. A 5-minute gap between
|
||||
motion events starts a new trip. This prevents parked idle rows from
|
||||
merging two separate drives into one.
|
||||
"""
|
||||
INACTIVITY_SECONDS = 5 * 60
|
||||
GPS_MOTION_METERS = 8
|
||||
MIN_GPS_MOTION_SAMPLES = 3
|
||||
MAX_GPS_SAMPLE_GAP_SECONDS = 30
|
||||
|
||||
with get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
@@ -293,59 +305,97 @@ def compute_trips_for_device(device_id):
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
if not rows:
|
||||
if len(rows) < 2:
|
||||
return 0
|
||||
|
||||
import math
|
||||
|
||||
def haversine(lat1, lon1, lat2, lon2):
|
||||
def haversine_km(lat1, lon1, lat2, lon2):
|
||||
R = 6371.0
|
||||
dlat = math.radians(lat2 - lat1)
|
||||
dlon = math.radians(lon2 - lon1)
|
||||
a = math.sin(dlat/2)**2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dlon/2)**2
|
||||
a = (math.sin(dlat / 2) ** 2
|
||||
+ math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
|
||||
* math.sin(dlon / 2) ** 2)
|
||||
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||
|
||||
# Split into segments by gap
|
||||
segments = []
|
||||
current = [rows[0]]
|
||||
for prev, curr in zip(rows, rows[1:]):
|
||||
gap = (curr[0] - prev[0]).total_seconds()
|
||||
if gap > _TRIP_GAP_S:
|
||||
segments.append(current)
|
||||
current = [curr]
|
||||
else:
|
||||
current.append(curr)
|
||||
segments.append(current)
|
||||
# Detect motion events per step
|
||||
obd_motion = [] # indices where OBD speed > 1 km/h
|
||||
gps_motion = [] # indices where GPS displacement >= 8 m
|
||||
|
||||
# Build trip records
|
||||
for i in range(1, len(rows)):
|
||||
prev = rows[i - 1]
|
||||
curr = rows[i]
|
||||
gap = (curr[0] - prev[0]).total_seconds()
|
||||
speed = curr[3] or 0
|
||||
has_obd = speed > 1
|
||||
has_gps = False
|
||||
if 0 < gap <= MAX_GPS_SAMPLE_GAP_SECONDS:
|
||||
d = haversine_km(prev[1], prev[2], curr[1], curr[2])
|
||||
if d * 1000 >= GPS_MOTION_METERS:
|
||||
has_gps = True
|
||||
if has_obd:
|
||||
obd_motion.append(i)
|
||||
if has_gps:
|
||||
gps_motion.append(i)
|
||||
|
||||
# Qualify GPS motion: need 3+ consecutive within 30s gap
|
||||
qualified_gps = set()
|
||||
seq = []
|
||||
for idx in gps_motion:
|
||||
if not seq or (rows[idx][0] - rows[seq[-1]][0]).total_seconds() > MAX_GPS_SAMPLE_GAP_SECONDS:
|
||||
seq = [idx]
|
||||
else:
|
||||
seq.append(idx)
|
||||
if len(seq) >= MIN_GPS_MOTION_SAMPLES:
|
||||
qualified_gps.update(seq)
|
||||
|
||||
# All motion indices, sorted
|
||||
motion_indices = sorted(set(obd_motion) | qualified_gps)
|
||||
if not motion_indices:
|
||||
return 0
|
||||
|
||||
# Group motion indices into trip boundaries by inactivity gap
|
||||
groups = []
|
||||
for idx in motion_indices:
|
||||
if not groups or (rows[idx][0] - rows[groups[-1][-1]][0]).total_seconds() > INACTIVITY_SECONDS:
|
||||
groups.append([idx])
|
||||
else:
|
||||
groups[-1].append(idx)
|
||||
|
||||
# Build trip records from each group
|
||||
trips = []
|
||||
for seg in segments:
|
||||
if len(seg) < 2:
|
||||
for group in groups:
|
||||
start_idx = max(0, group[0] - 1)
|
||||
end_idx = group[-1]
|
||||
seg_rows = rows[start_idx:end_idx + 1]
|
||||
|
||||
distance = 0
|
||||
speeds = []
|
||||
for i in range(1, len(seg_rows)):
|
||||
d = haversine_km(seg_rows[i - 1][1], seg_rows[i - 1][2],
|
||||
seg_rows[i][1], seg_rows[i][2])
|
||||
distance += d
|
||||
s = seg_rows[i][3]
|
||||
if s is not None and s > 0:
|
||||
speeds.append(s)
|
||||
|
||||
duration_s = int((seg_rows[-1][0] - seg_rows[0][0]).total_seconds())
|
||||
if duration_s < 30 or distance < 0.05:
|
||||
continue
|
||||
start_time = seg[0][0]
|
||||
end_time = seg[-1][0]
|
||||
duration_s = int((end_time - start_time).total_seconds())
|
||||
if duration_s < 30:
|
||||
continue
|
||||
dist = sum(
|
||||
haversine(seg[i][1], seg[i][2], seg[i+1][1], seg[i+1][2])
|
||||
for i in range(len(seg) - 1)
|
||||
)
|
||||
if dist < 0.05: # skip <50m blips
|
||||
continue
|
||||
speeds = [r[3] for r in seg if r[3] is not None]
|
||||
|
||||
trips.append({
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"distance_km": round(dist, 3),
|
||||
"start_time": seg_rows[0][0],
|
||||
"end_time": seg_rows[-1][0],
|
||||
"distance_km": round(distance, 3),
|
||||
"duration_s": duration_s,
|
||||
"avg_speed_kmh": round(sum(speeds) / len(speeds), 1) if speeds else None,
|
||||
"max_speed_kmh": round(max(speeds), 1) if speeds else None,
|
||||
"start_lat": seg[0][1],
|
||||
"start_lon": seg[0][2],
|
||||
"end_lat": seg[-1][1],
|
||||
"end_lon": seg[-1][2],
|
||||
"point_count": len(seg),
|
||||
"start_lat": seg_rows[0][1],
|
||||
"start_lon": seg_rows[0][2],
|
||||
"end_lat": seg_rows[-1][1],
|
||||
"end_lon": seg_rows[-1][2],
|
||||
"point_count": len(seg_rows),
|
||||
})
|
||||
|
||||
with get_cursor(commit=True) as cur:
|
||||
|
||||
Reference in New Issue
Block a user