← Retour à l'index

2. Coupure de signal (GAP)

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.

14cas d'usage 5classifications 12champs télémétrie 60sbuffer anti-race

Diagramme 1 — Cycle de vie complet d'un gap

Diagramme 1 Dernière pos. T=0 SILENCE — aucune donnée reçue seuil gap T+15min +60s buffer CRON ouvre gap T+16min open alerte 1h T+1h escalade 4h T+4h Signal revient T+Xh Moteur ferme le gap closed Classification + auto-resolve alerte Position GPS Action cron Action moteur Silence Seuil d'alerte

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

Diagramme 2 classifyGap(before, after) after.vibration_alarm === true OU defense_active a changé ? OUI tampering_suspect Priorité 1 (la plus haute) NON after.battery_level < before.battery_level - 20 ? OUI power_off Priorité 2 NON before.gsm_signal_level === 0 OU before.satellites < 3 ? OUI signal_loss Priorité 3 NON before.ignition === false ET speed ≤ 0.5 ET distance < 500m ? OUI planned_stop Priorité 4 NON unknown Priorité 5 (défaut)

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

Diagramme 3 Message LIVE age < 15min, non bufférisé Message BUFFERISE message_buffered=true OU age > 900s Step 10 : updateLastMessageAt Step 1 : detectGap Step 0 : preFilter Step 2 : classifyPosition Step 3 : detectStop Step 4 : manageTrip Step 5 : accumulateDistance Steps 6-9 : waypoint/corridor/fuel Step 10 : updateLastMessageAt Step 1 : detectGap Step 0 : preFilter Step 2 : classifyPosition Step 3 : detectStop SKIP Step 4 : manageTrip SKIP Step 5 : accumulateDistance Steps 6-9 : waypoint/corridor/fuel SKIP Légende Vivacité (toujours) Classification/distance Arrêt/trajet Waypoint/corridor/fuel

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

Diagramme 4 Forêt de Lom-Pangar Zone morte GSM — aucune couverture 14:22 GSM=0, SAT=0 1h22 de silence 15:44 GSM=65, SAT=8 Classification : signal_loss Bertoua Garoua-Boulai

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

Diagramme 5 (aucun) open closed cron ouvre moteur ferme moteur créé déjà-fermé (pas de gap cron)

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.


KORIDO Engine V2 — Document 2/8 — Index | ← 1. Trajet et Arrêt | 3. Lieux, Corridor et Déviation →