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):
|
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:
|
with get_cursor() as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
@@ -293,59 +305,97 @@ def compute_trips_for_device(device_id):
|
|||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|
||||||
if not rows:
|
if len(rows) < 2:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
def haversine(lat1, lon1, lat2, lon2):
|
def haversine_km(lat1, lon1, lat2, lon2):
|
||||||
R = 6371.0
|
R = 6371.0
|
||||||
dlat = math.radians(lat2 - lat1)
|
dlat = math.radians(lat2 - lat1)
|
||||||
dlon = math.radians(lon2 - lon1)
|
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))
|
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
||||||
|
|
||||||
# Split into segments by gap
|
# Detect motion events per step
|
||||||
segments = []
|
obd_motion = [] # indices where OBD speed > 1 km/h
|
||||||
current = [rows[0]]
|
gps_motion = [] # indices where GPS displacement >= 8 m
|
||||||
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)
|
|
||||||
|
|
||||||
# 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 = []
|
trips = []
|
||||||
for seg in segments:
|
for group in groups:
|
||||||
if len(seg) < 2:
|
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
|
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({
|
trips.append({
|
||||||
"start_time": start_time,
|
"start_time": seg_rows[0][0],
|
||||||
"end_time": end_time,
|
"end_time": seg_rows[-1][0],
|
||||||
"distance_km": round(dist, 3),
|
"distance_km": round(distance, 3),
|
||||||
"duration_s": duration_s,
|
"duration_s": duration_s,
|
||||||
"avg_speed_kmh": round(sum(speeds) / len(speeds), 1) if speeds else None,
|
"avg_speed_kmh": round(sum(speeds) / len(speeds), 1) if speeds else None,
|
||||||
"max_speed_kmh": round(max(speeds), 1) if speeds else None,
|
"max_speed_kmh": round(max(speeds), 1) if speeds else None,
|
||||||
"start_lat": seg[0][1],
|
"start_lat": seg_rows[0][1],
|
||||||
"start_lon": seg[0][2],
|
"start_lon": seg_rows[0][2],
|
||||||
"end_lat": seg[-1][1],
|
"end_lat": seg_rows[-1][1],
|
||||||
"end_lon": seg[-1][2],
|
"end_lon": seg_rows[-1][2],
|
||||||
"point_count": len(seg),
|
"point_count": len(seg_rows),
|
||||||
})
|
})
|
||||||
|
|
||||||
with get_cursor(commit=True) as cur:
|
with get_cursor(commit=True) as cur:
|
||||||
|
|||||||
Reference in New Issue
Block a user