← Retour à l'index

1. Trajet et Arrêt

Le cycle fondamental du moteur : comment un déplacement GPS se transforme en données structurées. 25 cas d'usage couvrant l'ouverture, l'accumulation de métriques, la fermeture, la grace period, la classification, le suivi moteur et les labels chauffeur.

Automate d'état : TRAJET (Trip)

Diagramme 1 (aucun) open closed (stop | stale) 1re position acceptée (pas de trip ouvert) position acceptée +9 métriques accumulées StopOpened cause=stop cron >48h, cause=stale 9 métriques accumulées par position : distance_m, driving_seconds, max_speed_kmh, speed_range_1/2/3_seconds (0-60 / 60-90 / 90+), idle_seconds (moteur ON, vitesse=0), position_count, fuel (FLS + CAN snapshots) Pipeline steps 5 (accumulateDistance) + 6 (accumulateTripMetrics)
Invariant clé : ended_at = stop.started_at (le trajet se termine au moment où le véhicule s'est immobilisé, pas au moment où le moteur détecte l'arrêt). La table trips impose une seule trip ouverte par véhicule via UNIQUE INDEX WHERE state = 'open'.

Automate d'état : ARRET (Stop)

Diagramme 2 (aucun) open closing_grace en mémoire seulement closed immobile >= 180s mouvement dist <= 200m grace expirée (120s) OU dist > 200m retour immobile mouvement dist > 200m (fermeture directe) Paramètres (DEFAULT_STOP_CONFIG) : minDurationSeconds : 180 (3 minutes) graceRadiusMeters : 200m gracePeriodSeconds : 120 (2 minutes) La grace period est en mémoire seulement. En DB, stops.state passe directement de 'open' a 'closed'. started_at = stationarySince (PAS le timestamp courant).

Interaction Trajet / Arrêt

Diagramme 3 temps → Trip #1 (open) Stop (open) Trip #2 P1 1re pos. P2..Pn extension StopOpened immobile >= 180s trip.ended_at = stop.started_at trip.close_cause = 'stop' TripClosed StopClosed départ nouveau trip Invariant : StopOpened ferme le trip, StopClosed permet l'ouverture du suivant. Si StopOpened sans trip ouvert (cold start) : pas de trip zero-durée créé. L'arrêt existe seul.

Cas d'usage TRAJET (1-13)

1
Ouverture sur 1re position acceptée
Quand le moteur reçoit une position avec quality_flag = 'accepted' et qu'aucun trip n'est ouvert pour ce véhicule, un nouveau trip est créé.
Déclencheur : Step 4 (manageTrip) — position acceptée + pas de currentTrip
Enregistré : INSERT trips (state='open', started_at, start_position, started_in_waypoint_id)
Ex : K12 redémarre après un arrêt à l'entrepôt de Douala. 1re position à 08:12 → trip ouvert.
2
Extension par position acceptée
Chaque position acceptée dans un trip ouvert incremente 9 métriques : distance_m, driving_seconds, max_speed_kmh, speed_range_1/2/3_seconds, idle_seconds, position_count, et fuel snapshots.
Déclencheur : Step 6 (accumulateTripMetrics) — TripExtended
Enregistré : UPDATE trips SET distance_m+=, driving_seconds+=, ... WHERE state='open'
Ex : 47 positions sur 133 km entre Douala et Edea → métriques incrémentées a chaque batch.
3
Fermeture par détection d'arrêt
Chemin principal de fermeture. Quand StopOpened se déclenche (step 3), le step 4 ferme le trip avec close_cause='stop' et ended_at = stop.started_at.
Déclencheur : Step 4 (manageTrip) — StopOpened event du step 3
Enregistré : UPDATE trips SET state='closed', ended_at, end_position, ended_in_waypoint_id, close_cause='stop'
Ex : K12 s'arrête a la station Edea. Trip ferme, ended_at = moment d'immobilisation.
4
Fermeture par cron (stale >48h)
Filet de sécurité : le cron ferme les trips ouverts depuis plus de 48h sans position acceptée. Cause='stale'. Cascade : ferme aussi les arrêts ouverts et abandonne les déviations actives de cette mission.
Déclencheur : Cron (toutes les 2 min) — trip.state='open' AND started_at < now - 48h
Enregistré : UPDATE trips, UPDATE stops (ended_at IS NULL), UPDATE corridor_déviations (state='abandoned')
Ex : Tracker K12 HS depuis 50h. Cron ferme le trip + l'arrêt orphelin pour debloquer l'index unique.
5
Cold start — pas de trip sur arrêt seul
Si StopOpened se déclenche et qu'aucun trip n'est ouvert (véhicule immobile au démarrage), le moteur ne crée PAS de trip zero-durée. L'arrêt existe indépendamment, sans parent trip. Le trip s'ouvrira au prochain mouvement.
Déclencheur : StopOpened + currentTrip = null (Décision D-stop-before-trip)
Enregistré : Rien côté trip. L'arrêt est créé normalement.
Ex : K12 garé au dépôt depuis la veille. Le tracker envoie sa 1re donnée → arrêt détecté sans trip précédent.
6
Snapshot carburant a l'ouverture
À l'ouverture du trip, fuel_start_pct est capturé depuis le capteur FLS, et can_fuel_consumed baseline depuis le compteur CAN cumulatif (si disponible). Ces valeurs servent de référence pour le calcul de consommation à la fermeture.
Déclencheur : TripOpened — 1re position acceptée avec fuel_level_percent
Enregistré : trips.fuel_start_pct, baseline CAN en mémoire (tripAccumulator)
7
Finalisation carburant a la fermeture
À la fermeture, le moteur calcule : fuel_consumed_liters (CAN delta ou FLS delta x capacite reservoir), fuel_efficiency_l100km, fillings_count et fillings_volume_liters (ravitaillements pendant le trip), idle_fuel_liters (CAN idle delta si disponible).
Déclencheur : TripClosed — stop ou stale
Enregistré : UPDATE trips SET fuel_end_pct, fuel_consumed_liters, fuel_efficiency_l100km, fillings_count, fillings_volume_liters, idle_fuel_liters, can_fuel_consumed
Ex : 133 km Douala→Edea. FLS start 78%, end 62%. Reservoir 400L → 64L consommes. 48.1 L/100km.
8
Garde contre reset compteur CAN
Les compteurs CAN cumulatifs peuvent se remettre à zéro sur un cycle d'alimentation ECU. Si le delta CAN est négatif, can_fuel_consumed est mis a NULL et le calcul retombe sur FLS : (fuel_start_pct - fuel_end_pct) x tank_capacity_liters. Le delta négatif n'est JAMAIS traité comme zero.
Déclencheur : TripClosed — canDelta < 0
Enregistré : trips.can_fuel_consumed = NULL, fallback FLS
9
Contexte waypoint a l'ouverture
À l'ouverture, le moteur vérifie si le véhicule est dans un waypoint (via activeWaypointVisits). Si oui, started_in_waypoint_id est enregistré sur le trip. Permet d'afficher "Depart de Garoua" sans jointure waypoint_visits.
Déclencheur : TripOpened (Décision D-waypoint-visit-simple)
Enregistré : trips.started_in_waypoint_id (nullable — NULL si en pleine route)
Ex : K12 quitte l'entrepôt Douala (visite active) → started_in_waypoint_id = UUID de l'entrepôt.
10
Contexte waypoint a la fermeture
À la fermeture, le moteur vérifie si le véhicule est dans un waypoint. Si oui, ended_in_waypoint_id est enregistré. La carte de detail du trip affiche "arrive a Plaines" directement via FK.
Déclencheur : TripClosed (Décision D-waypoint-visit-simple)
Enregistré : trips.ended_in_waypoint_id (nullable)
Ex : K12 s'arrête a la Station Edea → ended_in_waypoint_id = UUID Station Edea.
11
Groupement en journées (query time)
Les trips consécutifs avec un écart inférieur a 4h sont groupés en "journées" (journeys) par une CTE SQL avec LAG(ended_at) et fenêtre de 14 400s. Ce n'est PAS matérialisé — c'est un paramètre de requête pour le Vehicle Diary et les rapports.
Déclencheur : Requête SQL (pas le moteur temps réel)
Enregistré : Rien — calcule a la volee. journey_group, journey_started_at, total_distance_m, trip_count
Ex : 3 trips Douala→Edea→Kribi→Edea dans la meme journee, separes par des arrêts <4h → 1 journey.
12
Événement trip_started
À l'ouverture du trip, un événement trip_started (severity: info, status: closed) est publié vers la queue d'evaluation. Visible sur le dashboard fleet et le portail de suivi. Pas de WhatsApp par défaut.
Déclencheur : TripOpened → publishTripEvent
Enregistré : INSERT vehicle_events (event_type='trip_started', severity='info', status='closed')
13
Événement trip_completed
À la fermeture du trip, un événement trip_completed (info, closed) est publié. Contient les métriques finales : distance, durée, consommation. Visible dashboard + portail.
Déclencheur : TripClosed → publishTripEvent
Enregistré : INSERT vehicle_events (event_type='trip_completed', severity='info', status='closed')
Ex : "Trip K12 terminé : 133 km, 2h14, 48.1 L/100km" affiche sur le portail de suivi du client.

Cas d'usage ARRET (14-25)

14
Ouverture après 180s d'immobilité
Un arrêt s'ouvre quand le véhicule est immobile depuis 180 secondes. started_at = stationarySince (le moment réel d'immobilisation), PAS le timestamp de la position courante. Un gap de données ne décale pas le started_at.
Déclencheur : Step 3 (detectStop) — position immobile + stationarySince >= 180s
Enregistré : INSERT stops (state='open', started_at=stationarySince, position, is_authorized, nearest_waypoint_id)
Ex : K12 s'arrête a T=0, gap de données T=10→T=70min. L'arrêt ouvre avec started_at = T=0.
15
Grace period sur mouvement bref
Si le véhicule bouge de moins de 200m depuis le point d'arrêt, la grace period demarre en mémoire (closing_grace, 120s). En DB, l'arrêt reste state='open'. Cela évite les faux départs (manoeuvre au parking, déplacement au poste de pesée).
Déclencheur : Step 3 — mouvement détecté + distance <= 200m + gracePeriod > 0
Enregistré : Rien en DB — transition en mémoire uniquement (VehicleTrackingState.currentStop)
Ex : K12 avance de 80m dans la file du poste de pesée → grace period, l'arrêt ne se ferme pas.
16
Fermeture : grace expirée ou distance >200m
La grace period expire après 120s OU la distance dépasse 200m → l'arrêt se ferme. ended_at, fuel_delta_liters, idle_at_stop_seconds, engine_off_at_stop_seconds et duration_seconds sont enregistrés.
Déclencheur : Step 3 — grace period expirée OU distance depuis arrêt > 200m
Enregistré : UPDATE stops SET state='closed', ended_at, fuel_delta_liters, idle_at_stop_seconds, engine_off_at_stop_seconds
Ex : K12 quitte la station Edea après 22 min. Grace period → distance >200m → fermeture.
17
Annulation de la grace period
Si pendant la grace period le véhicule redevient immobile (retour a la position de l'arrêt), la grace est annulee et l'arrêt reste ouvert. Pas de transition en DB.
Déclencheur : Step 3 — closing_grace + retour à l'immobilité
Enregistré : Rien — annulation en mémoire, stop reste 'open' en DB
Ex : K12 avance de 50m puis s'arrête a nouveau → grace annulee, arrêt toujours ouvert.
18
Classification : enregistré vs non-enregistré
À l'ouverture, le moteur calcule la distance au waypoint le plus proche (haversine, données deja en mémoire via BatchContext). Si le waypoint est de type "registrable", l'arrêt est is_authorized = true. Sinon, is_authorized = false. C'est un fait, pas un jugement.
Déclencheur : StopOpened → enrichissement waypoint proximity
Enregistré : stops.is_authorized (boolean), stops.nearest_waypoint_id (FK)
19
Autorisation par type de waypoint
Registre : dépôt, warehouse, fuel_station, rest_area, port, custom
Non-registre : checkpoint (passage seulement), city (trop large)
Special : border = registre, avec suivi de durée (>6h = alerte Phase 2/3)
Déclencheur : Type du nearest_waypoint_id → règle de classification
Enregistré : is_authorized dérivé du type du waypoint
Ex : K12 s'arrête a 300m d'un checkpoint gendarmerie → non-enregistré (passage seulement).
20
Accumulateur allumage pendant arrêt
Pendant un arrêt ouvert, chaque position avec données d'allumage met a jour un accumulateur en mémoire sur ActiveStop : ignitionOnSeconds / ignitionOffSeconds. Le delta depuis lastIgnitionChangeAt est ajoute au compteur correspondant. À la fermeture, les valeurs sont persistees.
Déclencheur : Step 3 — chaque position pendant un arrêt ouvert avec ignition data
Enregistré : À la fermeture : stops.idle_at_stop_seconds (moteur ON), stops.engine_off_at_stop_seconds (moteur OFF)
Ex : Arrêt 45 min a Edea : 12 min moteur ON (file d'attente), 33 min moteur OFF (repos) → "Arrêt moteur eteint".
21
Delta carburant a la fermeture
À la fermeture, fuel_delta_liters est calcule a partir de la télémétrie aux bornes de l'arrêt (FLS debut vs FLS fin). C'est un champ de commodité pour le journal de bord — la détection fine des drains/remplissages se fait en continu via le step 8b (fuel state machine).
Déclencheur : StopClosed — fuel_level_percent aux bornes
Enregistré : stops.fuel_delta_liters (positif = remplissage, négatif = consommation/drain)
Ex : Arrêt station Edea. FLS debut 62%, fin 85%. Reservoir 400L → +92L. fuel_delta = +92.
22
Alerte arrêt non-autorise (contextuelle)
L'alerte unauthorized_stop (warning) ne se déclenche PAS sur chaque arrêt non-enregistré. Elle est contextuelle :
- Arrêt pendant déviation corridor active → oui
- Nuit + non-enregistré + durée > seuil (10 min) → oui
- Zone restreinte explicite → oui
- Arrêt court de jour sur le corridor → journal seulement
Déclencheur : StopOpened + shouldAlertOnUnregisteredStop(stop, context, config)
Enregistré : INSERT vehicle_events (event_type='unauthorized_stop', severity='warning', status='open')
Ex : K12 s'arrête a 23h30 hors waypoint connu depuis 15 min → alerte unauthorized_stop.
23
Escalade arrêt non-autorise
Si l'événement unauthorized_stop n'est pas acquitté (status='open') après 30 minutes, le cron l'escalade de warning à critical. Une nouvelle notification WhatsApp immédiate est envoyée. Les événements en status 'investigating' ou 'action_taken' ne sont PAS escaladés.
Déclencheur : Cron escalation — status='open' + created_at + 30 min < now
Enregistré : UPDATE vehicle_events SET severity='critical', escalated_at=now()
Ex : Alerte a 23h30, personne ne réagit. A 00h00 → escalade critical + WhatsApp immédiat au propriétaire.
24
Auto-résolution au départ
Quand le véhicule repart (StopClosed), l'événement unauthorized_stop associe a cet arrêt est automatiquement résolu : status = 'auto_resolved'. L'auto-résolution cible les status 'open' et 'investigating'. Les 'action_taken' nécessitent une résolution humaine.
Déclencheur : StopClosed → auto-resolve par stop_id
Enregistré : UPDATE vehicle_events SET status='auto_resolved', resolved_at=now() WHERE stop_id=$stopId
Ex : K12 repart a 01h15 → alerte unauthorized_stop auto-résolue. L'historique conserve l'episode complet.
25
Labellisation par le chauffeur
À l'ouverture d'un arrêt, une notification push est envoyée au chauffeur : "Arrêt détecté. Quel est le motif ?" Le chauffeur choisit parmi 7 options : Ravitaillement / Panne / Contrôle / Repos / Chargement-Déchargement / Personnel / Autre. L'API matche par vehicle_id + timestamp à +/- 5 min.
Déclencheur : Push notification → POST /mobile/stop-label → event-evaluation queue
Enregistré : INSERT vehicle_events (event_type='stop_labelled', source='driver', label). Icône + texte dans le journal.
Ex : K12 s'arrête. Chauffeur sélectionne "Contrôle" → l'arrêt dans le journal affiche l'icône gendarmerie.

Classification des arrêts — arbre de décision

Diagramme 4 Position d'arrêt détectée Waypoint le plus proche (haversine, BatchContext) Aucun waypoint proche is_authorized = false Waypoint trouve ENREGISTRE dépôt, warehouse, fuel_station, rest_area, port, custom NON-ENREGISTRE checkpoint (passage), city (trop large) BORDER (special) Enregistré. Suivi de durée deferred Phase 2/3 (seuil 6h). NON-ENREGISTRE Journal seulement — pas d'alerte Alerte CONTEXTUELLE (pas systematique) : 1. Déviation corridor active → unauthorized_stop (warning) 2. Nuit + non-enregistré + durée > 10 min → warning 3. Zone restreinte explicite → warning 4. Court arrêt de jour sur corridor → journal seul, pas d'alerte

Trace chronologique : Entrepôt Douala → Station Edea (133 km)

Trace position par position — K12, mission Douala-N'Djamena
# Heure Position Vitesse Action du moteur
P1 08:00:00 Entrepôt Douala (4.0483, 9.7043) 0 km/h STOP Arrêt ouvert (stationarySince = 07:57:00, il y a 180s). is_authorized=true (dépôt).
SKIP Pas de trip ouvert → cold start, pas de trip zero-durée (UC #5).
P2 08:12:30 Sortie entrepôt (4.0490, 9.7055) 18 km/h STOP Mouvement détecté, distance = 140m → closing_grace (en mémoire, 120s).
DB : arrêt reste state='open'.
P3 08:13:15 Route Bonaberi (4.0512, 9.7080) 42 km/h STOP Distance depuis arrêt = 450m > 200m → StopClosed. Durée : 15m30s. Moteur ON : 15m30s.
TRIP TripOpened. started_in_waypoint_id = Entrepôt Douala. fuel_start_pct = 78%.
EVENT trip_started publié (info, dashboard + portail).
P4 08:15:00 Pont Wouri (4.0550, 9.7120) 55 km/h TRIP TripExtended. +580m. driving_seconds += 105s. speed_range_1 += 105s.
DIST accumulateDistance : +580m cumulatif.
P5-P25 08:15→09:45 Route nationale N3, Douala → Edea 60-85 km/h TRIP TripExtended x21. distance += ~120km. driving_seconds croissant.
speed_range_1 (0-60) += quelques secondes, speed_range_2 (60-90) += majorite.
DIST Accumulation continue.
P26 09:48:00 Entrée Edea (3.8000, 10.1330) 35 km/h TRIP TripExtended. distance totale = 131.2 km.
P27 09:50:00 Station Edea (3.7985, 10.1355) 5 km/h TRIP TripExtended. distance = 132.8 km. idle_seconds += (moteur ON, vitesse basse).
P28 09:50:30 Station Edea (3.7984, 10.1356) 0 km/h TRIP TripExtended. distance = 133.0 km. idle_seconds continue.
SKIP Immobile, mais stationarySince = maintenant. Pas encore 180s.
P29-P32 09:51→09:53 Station Edea (immobile) 0 km/h TRIP TripExtended (idle). Accumulateur allumage : ignitionOnSeconds croissant.
SKIP Immobile 150s. Pas encore 180s.
P33 09:53:30 Station Edea (immobile, 180s) 0 km/h STOP StopOpened. started_at = 09:50:30 (stationarySince). is_authorized=true (fuel_station). nearest_waypoint = Station Edea.
TRIP TripClosed. ended_at = 09:50:30 (= stop.started_at). close_cause='stop'. ended_in_waypoint_id = Station Edea.
distance = 133.0 km, driving_seconds = 5670s (1h34m30s), max_speed = 85 km/h.
fuel_end_pct = 62%. fuel_consumed = 64L. efficiency = 48.1 L/100km.
EVENT trip_completed publié. Accumulateur allumage demarre pour l'arrêt.
P34-P40 09:54→10:12 Station Edea (immobile, remplissage) 0 km/h STOP Arrêt ouvert. ignitionOnSeconds croissant (moteur ON pendant remplissage).
FLS monte progressivement de 62% a 85% → fuel state machine détecte FuelFillingStarted/Ended.
P41 10:14:00 Sortie station (3.7990, 10.1340) 22 km/h STOP Mouvement détecté. Distance = 180m → closing_grace (< 200m).
P42 10:14:45 Route N3 direction Kribi (3.7995, 10.1310) 38 km/h STOP Distance = 520m > 200m → StopClosed. Durée : 24m15s. idle_at_stop = 24m15s (moteur ON tout le long). fuel_delta = +92L.
TRIP TripOpened (Trip #2). started_in_waypoint_id = Station Edea. fuel_start_pct = 85%.
EVENT trip_started publié. Prochaine étape : Kribi.
Points clés de cette trace :
- L'arrêt au dépôt (P1) n'a pas de trip parent (cold start, UC #5).
- Le trip se ferme au moment où le véhicule est devenu immobile (09:50:30), pas quand le moteur détecte les 180s (09:53:30) (UC #3).
- La grace period (P41) empeche un faux départ pendant la manoeuvre de sortie (UC #15).
- Le trip suivant s'ouvre immédiatement quand l'arrêt se ferme et une position acceptée arrive (UC #1).
- Les métriques carburant capturent le remplissage complet : trip #1 finit à 62%, trip #2 démarre à 85% (UC #6, #7).