Contexte
Les campagnes de rappel de sécurité supposent d’atteindre chaque véhicule concerné, mais courriers et appels téléphoniques laissent une population résiduelle de propriétaires qui ne répondent jamais. Dans les territoires insulaires et ultramarins, le problème est plus aigu : la couverture réseau est inégale, la logistique de terrain s’étend sur plusieurs territoires et le registre de qui conduit quoi est incomplet. La question pratique devient alors une question de gestion de flotte : quels véhicules du parc en circulation sont encore concernés, où sont-ils aperçus, et où en est chacun d’eux sur le chemin de la réparation ?
Approche
Méthodes
Ingestion et versionnage des données. La liste de rappel est récupérée automatiquement selon un calendrier ou importée manuellement. Chaque import est comparé au précédent à l’aide d’identifiants stables, versionné par empreinte, et seul le delta est envoyé aux unités de terrain. Les codes de type constructeur sont traduits en noms de modèles lisibles lors de l’import.
Correspondance malgré le bruit de l’OCR. La détection des plaques et la reconnaissance des caractères s’exécutent sur l’ordinateur portable de terrain avec des modèles ONNX, sans dépendance au réseau. Deux garde-fous réduisent les fausses correspondances : (i) une étape de correction positionnelle exploite la structure connue des formats de plaques nationaux (actuel et ancien) pour lever les confusions classiques de l’OCR (O/0, I/1, S/5, B/8, Z/2, G/6), en rejetant toute lecture ambiguë ; (ii) une confirmation par double lecture ne comptabilise une plaque que si elle est lue deux fois dans une courte fenêtre de temps. Une page web pour smartphone offre une solution de secours manuelle (photo ou saisie de la plaque, avec validation de la clé de contrôle du VIN).
Synchronisation pensée pour le hors-ligne. Les correspondances sont mises en file d’attente localement et transmises au serveur central à la connexion suivante, de sorte que les détections survivent à des coupures de plusieurs jours. Les unités envoient un signal de vie (version du logiciel, version de la liste de rappel, compteurs de lectures).
Opérations de terrain. Les superviseurs planifient des tournées sous forme de points de contrôle ordonnés et géolocalisés sur une carte, les attribuent aux équipes et aux unités, réutilisent des modèles et suivent l’avancement. Les agents signalent les incidents avec un niveau de gravité. Un processus de contact consigne le résultat de chaque démarche auprès d’un propriétaire, et le tableau de bord en déduit un entonnoir de conversion (identifié → approché → engagé → réparé) avec les tendances hebdomadaires, le délai médian jusqu’à la réparation et une ventilation par territoire.
Protection des données dès la conception. Les plaques qui ne figurent pas sur la liste de rappel ne sont jamais stockées ni transmises ; les enregistrements conservés ont des durées de conservation fixes avec purge automatique ; les actions sont consignées dans un journal d’audit ; l’accès au tableau de bord repose sur des rôles, y compris des comptes en lecture seule ; les données au repos sont chiffrées. Un registre des traitements a été rédigé parallèlement au code.
Déploiement & exploitation (DevOps)
Le système tourne sur deux types de machines très différents : des ordinateurs portables Windows dans les véhicules des agents, qui peuvent rester hors ligne plusieurs jours, et un serveur par instance. Après les premières semaines de terrain, l’essentiel du travail d’ingénierie a consisté à rendre les deux côtés faciles à déployer, à superviser et à remettre en service, par une seule personne, sans interrompre le travail de terrain.
Portables de terrain : empaquetage, installation et mises à jour
Empaquetage. L’application de terrain (backend Python, interface React)
est figée avec PyInstaller en un exécutable Windows « un dossier » qui embarque
les modèles ONNX et la configuration de l’OCR : une unité n’a besoin ni d’une
installation Python ni du réseau pour lire les plaques. Des modes intégrés
--diag et --selftest vérifient l’environnement d’exécution embarqué et
lisent une image de test sans lancer l’interface, ce qui a permis de
diagnostiquer les échecs de démarrage directement sur la machine de terrain.
Installateur en un clic. Le build est livré sous forme d’archive avec un
INSTALLER.bat à lancer d’un double-clic. Il s’élève lui-même en
administrateur et exécute un script PowerShell qui retire des fichiers la
marque « téléchargé depuis Internet » (sans quoi l’environnement embarqué ne
se charge pas), désactive la mise en veille USB pour que le dongle GPS ne soit
pas suspendu, et installe le fichier de configuration de l’unité là où
l’exécutable le lit. Le script refuse de deviner : si plusieurs fichiers de
configuration différents sont présents, il les liste et demande lequel est
le nouveau. Une réinitialisation optionnelle efface la base locale lorsqu’une
unité change de serveur ; elle affiche le serveur cible avant de demander
confirmation et avertit que les détections non synchronisées seraient perdues.
Le jeton d’accès de l’unité n’est jamais affiché. Une mise à jour suit le même
chemin : nouveau build, nouvelle exécution de l’installateur.
Provisionnement. Un script Python qui n’utilise que la bibliothèque standard, et tourne donc sur un Windows nu, crée les unités à partir d’un fichier CSV via l’API d’administration du serveur et génère, pour chacune, son fichier de configuration (adresse du serveur, code de l’unité, nom affiché, jeton) et un LISEZMOI à remettre avec le portable. La clé d’administration est lue dans une variable d’environnement ou saisie de manière masquée, jamais passée en argument de ligne de commande.
Versionnage : logiciel et liste de rappel
Version du logiciel. Chaque unité indique la version de son application dans son signal de vie. Le tableau de bord la compare à la version attendue et signale les unités qui restent à mettre à jour.
Version de la liste de rappel. La liste de rappel est versionnée par empreinte. À chaque cycle, une unité compare son empreinte locale à celle du serveur : identique, rien à faire ; pas de copie locale, téléchargement complet ; sinon, le serveur ne renvoie que les lignes ajoutées et retirées depuis cette empreinte (et l’unité se rabat sur un téléchargement complet si le serveur ne connaît plus sa version). La modification est appliquée en une seule transaction, et l’empreinte locale n’avance que si tout a été appliqué.
Cette dernière règle vient d’un incident de terrain. Une contrainte de la base de données faisait échouer le diff sur une unité, l’erreur était absorbée par la boucle de synchronisation, et l’unité continuait d’envoyer ses signaux de vie alors que sa liste de rappel restait figée pendant plus de deux semaines : elle alertait donc sur des véhicules déjà réparés. La version 1.4.1 a corrigé la cause, ajouté un retour arrière explicite et livré une migration de schéma qui conserve les données existantes. Elle a été testée par une série de 62 tests, qui reproduisait le bug et migrait une base à l’ancien schéma. La supervision lève désormais une alerte « synchronisation des rappels figée » lorsqu’une unité n’a pas reçu une liste publiée au-delà d’un délai de grâce.
Synchronisation incrémentale et résiliente
Toutes les cinq minutes, ou à la demande, une unité exécute un cycle de synchronisation composé d’étapes indépendantes : signal de vie, envoi des détections (par lots), trace GPS, incidents, mises à jour des points de contrôle, puis récupération de la liste de rappel et des tournées. Les envois passent avant les récupérations, de sorte que les données de terrain partent en premier. Une étape en échec ne bloque plus les autres : dans une version antérieure, le cycle était monolithique, et un signal de vie en échec a un jour retardé les détections de 22 minutes. Les requêtes utilisent des délais d’attente courts, les téléchargements des délais plus longs. Le signal de vie transmet la version du logiciel, la version de la liste de rappel, le nombre de détections en attente et les compteurs de lectures, sur lesquels reposent le tableau de bord et la supervision.
Déploiement du serveur
Kit d’instance. Chaque déploiement est une instance indépendante, décrite par un court identifiant, sur un serveur virtuel Ubuntu LTS. Un script d’amorçage, interactif et relançable sans risque, installe les paquets système, Docker et Caddy depuis leurs dépôts officiels. Il active les mises à jour de sécurité automatiques sans redémarrage automatique, restreint SSH à l’authentification par clé, active un pare-feu en refus par défaut et une protection contre la force brute, et vérifie que l’enregistrement DNS pointe vers le serveur avant d’appliquer la configuration TLS. Les secrets résident dans des fichiers de configuration réservés à root sur le serveur, jamais dans le dépôt.
Conteneur et proxy inverse. Un Dockerfile multi-étapes construit le tableau de bord React avec Node, puis exécute le serveur FastAPI sur une image Python allégée, sous un utilisateur non privilégié. Son contrôle de santé prévoit une période de démarrage de trois minutes, car les modèles de plaques et d’OCR sont chargés avant que le serveur n’écoute. Le conteneur n’est publié que sur l’interface de boucle locale. Caddy est le seul point d’entrée public : certificat Let’s Encrypt automatique, HSTS explicite et aucun journal d’accès, car les URL de scan contiennent des plaques. Le journal d’accès de l’application elle-même est désactivé pour la même raison.
Déploiement et retour arrière. Le code ne quitte le poste
d’administration que sous la forme d’un git archive d’un arbre commité : les
modifications non commitées sont listées et ne partent pas, et les fins de
ligne sont forcées en LF. Sur le serveur, le nouvel arbre est extrait à côté
de l’actuel puis permuté, le précédent est conservé, et le commit déployé est
consigné. Le script de déploiement se détache ensuite de la session SSH : une
connexion coupée n’interrompt ni le déploiement ni un retour arrière. Un
verrou empêche les déploiements concurrents. Le script construit l’image
(étiquetée avec son commit), conserve l’image précédente comme rollback,
recrée le conteneur et attend qu’il se déclare en bonne santé. Il abandonne
au bout de cinq minutes, ou dès que le conteneur a redémarré deux fois. En
cas d’échec, il revient automatiquement à l’image et à l’arbre source
précédents, met de côté le code en échec et envoie une alerte. Revenir à
n’importe quelle version antérieure revient à redéployer ce commit. Un retour
arrière d’image ne défait pas une migration de schéma : la procédure
documentée consiste donc à faire une sauvegarde avant tout déploiement qui
modifie le schéma.
Volume chiffré et redémarrages sûrs
Les données du serveur (base, photos de contact, archives des listes de rappel) résident dans un conteneur de fichier LUKS2 monté avec des options restrictives. Sa phrase de passe n’est jamais stockée sur la machine : après un redémarrage, une personne déverrouille le volume. Le conteneur redémarre de lui-même, mais le serveur refuse de démarrer tant qu’un fichier sentinelle situé sur le volume chiffré est absent. Sans lui, un redémarrage aurait laissé un dossier vide à la place du volume, et l’application y aurait créé une base neuve, non chiffrée, et accepté les données de terrain. Cette protection a été vérifiée dans les deux sens sur un conteneur jetable. Un script distinct agrandit le volume pendant qu’il est monté.
Sauvegardes et reprise
Un timer systemd quotidien prend un instantané cohérent de la base SQLite via
son API de sauvegarde, l’archive avec les photos et les archives de rappel, et
chiffre le résultat avec age vers une clé publique. La clé privée n’est
affichée qu’une fois, à sa création, et conservée hors du serveur. Les
sauvegardes sont gardées localement et, en option, copiées vers un stockage
objet compatible S3 avec rclone, configuré uniquement par variables
d’environnement. Un mode de test de restauration déchiffre une sauvegarde
dans un dossier temporaire sur le volume chiffré, lance un contrôle
d’intégrité, compte les lignes par table, puis efface tout. Le mode
d’application déplace l’état courant de côté au lieu de le supprimer. La
documentation comprend une reconstruction pas à pas après perte totale du
serveur. Le plan de continuité vise un délai de reprise de 4 heures et une
perte de données maximale de 24 heures.
Supervision
Un chien de garde s’exécute toutes les 15 minutes et ne publie sur un webhook
de messagerie que lorsqu’un état change (problème, puis « rétabli »). Un bilan
plus complet est envoyé deux fois par jour, calé sur les horaires de terrain.
Il vérifie que le volume chiffré est monté, le point de santé de l’API (avec
plusieurs essais, car l’import quotidien peut le bloquer environ une minute),
l’état de santé du conteneur, la lisibilité de la base, l’occupation du disque
système et du volume chiffré, la taille du journal d’écriture anticipée de
SQLite, l’expiration du certificat TLS, l’âge et la taille de la dernière
sauvegarde, les unités silencieuses depuis plus de 48 heures (et non 24, car
les unités sont éteintes le week-end) et les unités dont la liste de rappel
est figée. Un guetteur distinct alerte lorsqu’aucun import de liste de rappel
n’a eu lieu depuis plus de 30 heures. Plusieurs de ces contrôles ont été
ajoutés après des incidents réels : le volume chiffré a un jour atteint 90 %
sans aucune alerte, car seul le disque système était surveillé. Un timer
hebdomadaire nettoie les caches de construction Docker et ne conserve que les
images courante et de rollback. Un script dédié tronque le journal SQLite
avec une attente de verrou courte, afin que les écritures du terrain ne
soient jamais bloquées longtemps.
Résilience hors ligne
Le côté terrain est conçu pour continuer à fonctionner pendant une panne du serveur : lecture des plaques, correspondance avec la copie locale de la liste de rappel, alertes, tournées déjà téléchargées et onglet de statistiques local fonctionnent sans réseau, et la file d’attente se vide à la connexion suivante. Seuls le tableau de bord (et la page pour smartphone hébergée sur le serveur) sont indisponibles tant que le serveur est arrêté.
Tests
Le dépôt ne comporte pas de chaîne d’intégration continue. Les versions sont testées avant leur livraison, et les commits consignent ces exécutions : 12/12 et 6/6 pour les étapes de correction des plaques et de confirmation, 62 tests pour la version 1.4.1. Les comportements qui ne se prêtent pas aux tests unitaires, comme la protection du volume ou un déploiement réel, ont été vérifiés sur des conteneurs jetables et sur le point de santé de la production.
Résultats
- Lors des premières opérations de terrain, les agents ont signalé 32 % des détections comme des erreurs de lecture sur environ une semaine. Cette mesure a motivé les étapes de correction positionnelle et de confirmation par double lecture, et les statistiques du tableau de bord excluent désormais les détections marquées comme faux positifs.
- Les nouvelles étapes ont été livrées avec des tests unitaires : 12/12 pour la correction des plaques et 6/6 pour la confirmation, les réglages et le journal des lectures.
- Après l’extension de la table de correspondance des noms de modèles, une seule ligne de la liste de rappel de production restait sans nom de modèle décodé.
Aucun taux de reconnaissance de bout en bout n’a encore été mesuré.
Limites & suite
- La précision de la reconnaissance n’a pas été mesurée sur un banc d’essai avec vérité terrain. Les objectifs du cahier des charges (taux de lecture, latence) sont des objectifs de conception, non des résultats.
- Aucune lecture de plaque à l’ancien format n’a été observée au départ. Un compteur dédié suit désormais ces lectures de bout en bout, mais le correctif doit encore être vérifié sur le terrain.
- Une fausse correspondance avec une liste volumineuse coûte toujours du temps aux agents. L’étape suivante consiste à calibrer le seuil de confiance et la fenêtre de confirmation sur des vidéos de terrain annotées.
Stack technique
Python · FastAPI · SQLite · ONNX Runtime · fast-alpr (détecteur de plaques YOLOv9 + OCR) · OpenCV · PaddleOCR · React · TypeScript · Vite · Leaflet · Docker · Caddy · LUKS · systemd · age · rclone · PyInstaller · PowerShell