Quand le GPS se tait : ouverture proactive par cron, fermeture par le moteur, classification automatique en 5 catégories, contexte télémétrique aux frontières, traitement des messages bufférisés. 14 cas d'usage.
Cycle de vie complet : dernière position → silence → cron ouvre le gap → alerte 1h → escalade 4h → signal revient → moteur classifie et ferme
Diagramme 2 — Arbre de décision : classification du gap
Ordre de priorité : tampering_suspect → power_off → signal_loss → planned_stop → unknown. Première condition vraie gagne.
Pourquoi tampering_suspect est priorité 1 ? Un brouilleur GPS/GSM provoque simultanément une perte de signal ET une alarme de vibration/defense. Si on testait signal_loss en premier, le sabotage serait masqué en simple zone morte.
Diagramme 3 — Contexte télémétrique aux frontières
Le champ boundary_context (JSONB) sur data_gaps capture un instantané des 12 champs diagnostiques à chaque frontière du gap. Les champs en surbrillance sont ceux utilisés par l'algorithme de classification.
Champ
Type
Before (dernière pos.)
After (première pos.)
Utilisé par
timestamp
timestamptz
2026-03-12 14:22:00
2026-03-12 15:44:12
durée du gap
latitude
number
5.4827
5.4831
distance (planned_stop)
longitude
number
10.7834
10.7840
distance (planned_stop)
speed_kmh
number
0
12
planned_stop
heading_degrees
number
182
175
orientation
satellites
integer
0
8
signal_loss
hdop
number
12.5
1.2
précision GPS
gsm_signal_level
integer
0
65
signal_loss
battery_level
integer
85
52
power_off (-33%)
ignition_status
boolean
false
true
planned_stop
external_power
boolean
true
true
alimentation
mcc / mnc
integer
624 / 02
624 / 02
opérateur réseau
Exemple : gap de 1h22 dans la forêt de Lom-Pangar. gsm_signal=0 et satellites=0 au before → classification signal_loss
Diagramme 4 — Fast path : messages bufférisés
Fast path bufférisé : seuls classify + distance s'exécutent. Stop, waypoint, corridor, fuel et events sont ignorés. Optimisation I/O pour les dumps de zone morte.
Diagramme 5 — Exemple réel : forêt de Lom-Pangar
Corridor Douala-N'Djamena, segment Bertoua → Garoua-Boulai. La forêt de Lom-Pangar est une zone morte GSM connue : before.gsm_signal=0 → signal_loss.
Les 14 cas d'usage
Ouverture et fermeture du gap
1
Gap ouvert par le cron (proactif)
Quand last_message_at < now - gapThreshold ET aucun gap ouvert : le cron INSERT un gap avec state='open'. Le buffer de 60 secondes supplémentaires dans la requête cron empêche d'ouvrir un gap pendant qu'une transaction moteur est en cours de commit.
2
Gap fermé par le moteur (données reprennent)
Quand des données arrivent après un silence : le moteur exécute UPDATE data_gaps SET state='closed', ended_at=... WHERE vehicle_id=$vid AND state='open' RETURNING id. Si une ligne est retournée, le gap cron est fermé. Classification + boundary_context sont remplis.
3
Gap créé déjà-fermé (réactif)
Si l'UPDATE ci-dessus retourne 0 lignes (pas de gap cron existant), le moteur INSERT un gap state='closed' avec les deux frontières connues. Le cron n'avait pas encore détecté le silence, mais le moteur capture quand même la coupure.
Classification du gap (5 catégories, par priorité)
4
tampering_suspect
Priorité 1 (la plus haute).after.vibration_alarm === true OU defense_active a changé entre before et after. Detection de sabotage du traceur. Prioritaire car un brouilleur provoque aussi une perte de signal — sans cette priorité, le sabotage serait masqué en simple zone morte.
5
power_off
Priorité 2.after.battery_level < before.battery_level - 20 (uniquement quand before.battery_level n'est pas null). Chute de batterie > 20% pendant le silence = coupure d'alimentation externe. Plus spécifique que signal_loss car la perte de batterie est un signal actif.
6
signal_loss
Priorité 3.before.gsm_signal_level === 0 OU before.satellites < 3. Zone morte GSM ou perte de couverture satellite. Classification la plus fréquente sur le corridor Douala-N'Djamena (forêts, zones rurales).
7
planned_stop
Priorité 4.before.ignition === false ET speed ≤ 0.5 ET distance < 500m entre les frontières. Le véhicule ne bougéait pas avant le silence et n'a pas beaucoup bougé pendant — arrêt prévu (repos nocturne, attente douane).
8
unknown
Priorité 5 (défaut). Aucun critère ne correspond. Le gap est enregistré pour le journal de bord mais ne déclenche pas d'alerte spécifique au-delà de l'alerte de durée standard.
Contexte aux frontières
9
Capture du contexte télémétrique
12 champs capturés sur chaque frontière (before + after) : timestamp, lat, lng, speed, heading, satellites, hdop, gsm_signal, battery, ignition, external_power, mcc/mnc. Stockés en JSONB dans boundary_context sur data_gaps.
Protection anti-race
10
Gap creation race fix
Pattern UPDATE-then-INSERT : le moteur tente d'abord l'UPDATE du gap cron, puis INSERT seulement si 0 lignes retournées. Le cron ajoute un buffer de 60 secondes a sa requête (last_message_at < NOW() - threshold - 60s) pour que la transaction moteur ait le temps de committer last_message_at. Elimine la race sans verrouillage.
Alertes véhicule hors-ligne
11
Vehicle offline extended (alerte progressive)
Gap ouvert depuis > 1h → événement vehicle_offline_extended (warning). Gap ouvert depuis > 4h → escalade à critical. Le cron vérifie les gaps ouverts et crée/escalade les événements dans vehicle_events.
12
Auto-résolution à la reprise du signal
Quand le gap se ferme (GapClosed), le consumer d'événements exécute : UPDATE vehicle_events SET status='auto_resolved' WHERE gap_id=$gapId AND event_type='vehicle_offline_extended' AND status IN ('open', 'investigating'). L'alerte disparaît sans intervention humaine.
Vivacité et fast path
13
Vivacité à deux niveaux (two-tier liveness)
Les steps 1 (detectGap) et 10 (updateLastMessageAt) s'exécutent AVANT preFilter pour TOUTES les positions, y compris celles avec du GPS invalide. Un traceur envoyant des coordonnees 0,0 (GPS à froid) remet quand meme le timer de gap à zéro. La vivacite ne dépend pas de la qualité GPS.
14
Fast path messages bufférisés
Si message_buffered=true OU ageSeconds > 900 (15 min de retard) : seuls classify (step 2) et distance (step 5) s'exécutent. Stop, waypoint, corridor, fuel et events sont ignorés. Optimisation pour les dumps de positions bufférisées en sortie de zone morte — le traitement est I/O-bound (INSERT batch), pas CPU-bound.
Algorithme de classification (référence)
// classifyGap(before, after, durationMs, distanceM)// Retourne la première classification correspondante (priorité)1. after.vibration_alarm === true
OR after.defense_active !== before.defense_active
→'tampering_suspect'2. after.battery_level < before.battery_level - 20
(seulement si before.battery_level != null)→'power_off'3. before.gsm_signal_level === 0
OR (before.satellites !== null AND before.satellites < 3)
→'signal_loss'4. before.ignition === false
AND before.speed_kmh <= 0.5 (ou null)
AND distanceM < 500
→'planned_stop'5.→'unknown'
Requête d'ouverture de gap (cron)
-- Véhicules éligibles à l'ouverture de gap
SELECT v.id, v.last_message_at
FROM vehicles v
WHERE v.last_message_at < NOW()
- INTERVAL '$gapThresholdSeconds seconds'
- INTERVAL '60 seconds' -- buffer anti-race
AND NOT EXISTS (
SELECT 1 FROM data_gaps dg
WHERE dg.vehicle_id = v.id AND dg.state = 'open'
)
Pattern de fermeture (moteur)
-- Étape 1 : tenter de fermer un gap cron existant
UPDATE data_gaps
SET state = 'closed',
ended_at = $firstPositionTimestamp,
boundary_context = $jsonb,
classification = $classification
WHERE vehicle_id = $vid AND state = 'open'
RETURNING id;
-- Étape 2 : si 0 lignes, insérer un gap déjà-ferméIF rowCount === 0 THEN
INSERT INTO data_gaps (vehicle_id, state, started_at, ended_at,
boundary_context, classification)
VALUES ($vid, 'closed', $lastMessageAt, $firstPositionTimestamp,
$jsonb, $classification);
Automate d'état du gap
Deux chemins de création : cron (proactif, state='open') et moteur (réactif, state='closed'). Le moteur ferme toujours le gap cron s'il existe.