Pièges des applications de communication série — de la reconnexion à la conception des journaux
· Mis à jour le: · Go Komura · Communication série, RS-232, C#, .NET, Développement Windows, Intégration d'équipements
Intégration d’équipements, instruments de mesure, automates (PLC), lecteurs de codes-barres, convertisseurs USB-série. La communication série a l’air d’une technologie ancienne, mais elle reste encore tout à fait courante sur le terrain des applications Windows.
Ce qui est un peu dangereux, c’est que la communication série peut démarrer avec rien de plus qu’un seul port COM et un seul couple Read / Write. Le test de connectivité passe immédiatement, mais une fois en production, les symptômes suivants ont tendance à apparaître.
- Les commandes et les réponses se désynchronisent de temps en temps
- Ça se fige exactement une fois par jour
- La reprise ne se fait que si l’on débranche/rebranche l’USB
- L’UI se bloque de temps en temps
- Dans les journaux, il ne reste que “Timeout”
Ce qui est vraiment difficile dans une application de communication série, ce n’est pas l’API d’envoi/réception en elle-même, mais plutôt les limites, les délais d’attente, les transitions d’état, la reconnexion et l’observabilité.
1. Les conclusions d’abord
Pour commencer, voici un résumé formulé de façon pratique.
- La communication série est un byte stream ordonné ; les limites de message ne s’imposent jamais d’elles-mêmes
- Appeler
Read(100)ne garantit pas de récupérer exactement 100 octets - Le
DataReceivedde.NETn’est pas garanti de se déclencher à chaque octet reçu, et de plus il ne s’exécute pas sur le thread UI ReadLine()/WriteLine()ne sont vraiment pratiques que lorsque l’interlocuteur utilise réellement un protocole texte basé sur les lignes- Un seul timeout ne suffit pas. Séparer les significations
open,inter-byte,response,reconnectapporte plus de stabilité - Plutôt que de permettre un
Writedepuis n’importe où, se rapprocher d’un single writer casse moins facilement - Avec l’USB-série, il est plus tranquille de présumer dès le départ le débranchement/rebranchement, la ré-énumération, les changements de numéro COM et les échecs de reconnexion
En somme, la difficulté d’une application de communication série n’est pas « peut-on ouvrir le port », mais comment transformer la séquence d’octets en messages porteurs de sens, et comment gérer le temps et l’état qui l’entourent.
2. La communication série est un « byte stream ordonné », pas des « messages »
Du point de vue de l’application, la communication série ressemble à « envoyer une commande, recevoir une réponse ». Mais à la couche inférieure, ce qui circule réellement n’est qu’une séquence ordonnée d’octets.
Ce que vous envoyez en un seul Write peut très bien apparaître ainsi côté récepteur.
- Arriver en un seul
Read - Arriver en deux fois
- Arriver concaténé avec d’autres données
Si l’on oublie cette prémisse, l’application commence à supposer que « ce Read doit correspondre à cette réponse ». Cette supposition devient souvent la première mine terrestre d’une application de communication série.
| Idée reçue courante | Réalité |
|---|---|
Read(16) renvoie exactement 16 octets |
Selon l’arrivée des données et les timeouts, on peut n’en récupérer qu’une partie |
DataReceived = arrivée d’un message |
L’événement n’est pas garanti octet par octet, et ne s’exécute pas sur le thread UI |
Write est revenu = l’interlocuteur a terminé le traitement |
Dans la plupart des cas, cela signifie plutôt que l’émetteur a réussi à empiler les données dans un buffer |
| La liste des COM = la vérité de ce qui est connecté actuellement | L’ordre d’énumération n’est pas garanti, et le résultat peut être obsolète (stale) |
C’est pourquoi, en communication série, il faut définir soi-même les limites de message, sous forme de protocole. Trame de longueur fixe, basée sur un délimiteur, longueur + payload + checksum : la forme importe peu, mais entrer dans l’implémentation en laissant ce point flou conduit presque à coup sûr à des souffrances plus tard.
3. Ce qu’il faut décider en premier
Avant de construire une application de communication série, décidez au moins des points listés ici.
3.1 Les limites de trame
Décidez quelle séquence d’octets constitue un message. Longueur fixe ? Délimitée par un retour à la ligne ? Précédée d’une longueur ? Y a-t-il un checksum / CRC ? Si ce point reste flou, le récepteur ne peut pas distinguer « pas encore assez de données » de « données corrompues ».
3.2 Texte, binaire, ou un mélange des deux
Décidez d’abord s’il s’agit d’un protocole en lignes ASCII / UTF-8, de binaire pur, ou d’un mélange des deux. En particulier, un mélange du type « la partie commande est une chaîne de caractères, le payload est binaire, seule la fin comporte un retour à la ligne » fait s’effondrer la limite très vite si l’on ne précise pas explicitement jusqu’où décoder et à partir d’où traiter les octets bruts.
3.3 La signification de chaque timeout
Il est plus sûr de considérer les timeouts non pas comme un seul, mais séparés par signification.
- open timeout : jusqu’à l’ouverture du port
- inter-byte timeout : le temps sans octet en cours de trame
- response timeout : de l’émission de la commande jusqu’à la réponse complète
- reconnect backoff : l’intervalle d’attente entre les tentatives de reconnexion
Les timeouts sont plus stables lorsqu’on les considère non pas comme une « assurance contre la lenteur », mais comme des règles qui font avancer la transition d’état.
3.4 Le contrôle de flux et l’état des lignes
Voici les réglages qu’il convient d’expliciter.
BaudRateDataBitsParityStopBitsHandshakeDTR/RTS
Se contenter d’un « 8N1, ça devrait à peu près marcher » pour ce point conduit tout simplement à un blocage selon l’appareil interlocuteur.
3.5 La séparation des responsabilités
Répartissez qui est responsable de quoi.
- Qui lit
- Qui écrit
- Qui parse
- Qui répercute les résultats sur l’état métier
La communication série devient d’autant plus fragile qu’on mélange l’UI et la communication.
3.6 Les transitions d’état de démarrage, d’arrêt et de reconnexion
Au minimum, des états comme Closed, Opening, Ready, WaitingResponse, Fault et Reconnecting devraient figurer dans la conception. Juste après un débranchement/rebranchement, l’interlocuteur est peut-être encore en train de démarrer, et il arrive qu’il ne faille pas traîner la requête en attente précédente.
3.7 Les journaux et la capacité d’investigation
C’est presque toujours ce point qui pose le plus de problèmes plus tard. Au minimum, on veut conserver : les horodatages d’open / close / reopen, les réglages de port utilisés, le hex dump des trames envoyées et reçues, les erreurs de checksum / CRC, les frame timeout / response timeout, et la raison de chaque reconnexion.
4. Les pièges courants
4.1 Croire que « un Read = un message »
C’est le cas le plus fréquent. Supposons par exemple que l’interlocuteur renvoie une trame composée d’un en-tête, d’une longueur, d’un payload et d’un CRC. Si l’on appelle Read(buffer, 0, expectedLength) une seule fois en supposant que sa valeur de retour constitue directement une trame complète, une réception partielle casse facilement tout.
Voici les trois modes de rupture les plus courants.
- Seule la longueur est lue, le payload n’est pas encore arrivé
- Une trame et demie arrive, la seconde moitié n’arrive qu’au
Readsuivant - Deux trames arrivent regroupées, seule la première est traitée et le reste est jeté
La parade est simple : séparer en accumulant d’abord la réception, puis en laissant un parser en extraire les trames.
4.2 Transformer directement DataReceived en événement métier
Le SerialPort.DataReceived de .NET paraît pratique, mais le considérer comme « la notification qu’un message est arrivé » est dangereux. En pratique, traitez DataReceived simplement comme « quelque chose semble être arrivé », ne faites aucun traitement lourd dans le handler, et renvoyez systématiquement les mises à jour de l’UI vers le thread UI.
4.3 Croire que l’on peut Write depuis n’importe où
Une architecture où le bouton de l’UI, le minuteur de surveillance, le traitement de reconnexion et le keepalive appellent chacun directement Write est fragile. Le port série étant un byte stream, selon la conception, des interruptions de commande ou des envois supplémentaires pendant l’attente d’une réponse peuvent se produire. En particulier pour les protocoles de type requête-réponse ou les bus de type RS-485, se rapprocher d’un single writer apporte une stabilité nettement supérieure.
4.4 Tout faire passer par ReadLine() / WriteLine()
Pour un protocole texte basé sur les lignes, ReadLine() / WriteLine() sont pratiques. Mais ils ne le sont que lorsqu’il s’agit réellement d’un protocole en lignes. Une incohérence de NewLine, un retour à la ligne dans le payload, une différence d’encodage de caractères, ou du binaire mélangé, et la limite s’effondre immédiatement.
4.5 Ne pas concevoir les timeouts et les laisser à leur valeur par défaut
Placer une lecture synchrone sans y réfléchir conduit tout simplement à une attente infinie. Ce qui est encore plus gênant, c’est que le timeout configuré ne s’applique pas forcément à toutes les façons de lire. Les implémentations qui font une lecture synchrone sur le thread UI, qui essaient de tout exprimer avec un seul timeout, ou qui se contentent d’augmenter les retry, ont tendance à se bloquer.
4.6 Sous-estimer RTS/CTS, XON/XOFF, DTR/RTS
Le handshake et les lignes de contrôle ont un impact réel face à du matériel physique. En cas d’incohérence de réglage, les symptômes tendent à être : la transmission s’arrête de temps en temps, des données sont perdues au-delà d’un certain volume, ou le comportement diffère uniquement juste après l’ouverture. Selon l’appareil, les transitions de DTR/RTS sont même parfois interprétées comme un signal de démarrage ou de changement de mode.
4.7 Croire qu’un nouvel appel à Open() suffit à reconnecter
Surtout avec l’USB-série, il est tout à fait normal que le port disparaisse temporairement, que l’ancien handle devienne invalide, ou que la requête en attente précédente perde son sens. Il est plus sûr de traiter la reconnexion comme un ensemble regroupant au minimum : l’invalidation de la session, l’échec (fail) de la requête en attente, l’arrêt du reader / writer, la réouverture après un backoff, et la ré-exécution de l’initialisation de l’appareil.
4.8 Croire que l’énumération des ports COM est la vérité
GetPortNames() est pratique, mais apparaître dans la liste et pouvoir être ouvert (opening) ne sont pas la même chose. Faire aveuglément confiance au COM7 de la dernière fois, sélectionner automatiquement le premier résultat de l’énumération, ou considérer qu’apparaître dans la liste vaut validité : ces implémentations posent souvent problème en exploitation.
4.9 Des journaux d’envoi/réception trop pauvres
TimeoutException, IOException et Port closed seuls ne renseignent presque sur rien. Enregistrer les horodatages d’envoi/réception, le port profile, le hex dump des échanges, les erreurs de parser, la requête à laquelle correspond chaque réponse, et le déclencheur de chaque reconnect fait considérablement avancer le diagnostic.
5. Bonnes pratiques
Ce qui paie le plus, c’est de séparer les responsabilités.
reader: se contente de lire la séquence d’octets depuis le portwriter: se contente d’écrire dans l’ordre depuis la outbound queueparser: se contente d’extraire les frames à partir de la séquence d’octetsprotocol: gère la correspondance request/response et les checksumsapp state: se contente de mettre à jour l’état métier
Pour le traitement de réception, plutôt que de considérer directement l’unité de retour de Read comme l’unité métier, une architecture qui accumule d’abord dans un buffer avant de laisser le parser en extraire les frames est plus stable. Regrouper l’envoi dans un seul worker, en rapprochant le Write réel d’un single writer, permet de réduire les décalages d’ordre.
De même pour les timeouts, plutôt que de se contenter d’un seul chiffre, les séparer par signification — open, inter-byte, response, reconnect — facilite le diagnostic des causes. Conserver les réglages de port sous forme de profile plutôt que comme des valeurs codées en dur, et les journaliser au démarrage, rend l’investigation sur site bien plus facile.
Il est plus stable de considérer la reconnexion non pas comme un simple reopen, mais comme une régénération de session. Reconstruire jusqu’au buffer de réception, à l’état du parser, aux pending request, à la séquence d’initialisation et au jugement de readiness permet de réduire les bugs de reconnexion qui « ne se manifestent qu’occasionnellement ».
Enfin, nous recommandons de conserver à la fois des journaux bruts et des journaux résumés. Le raw hex dump et l’historique d’open / close sont efficaces pour l’investigation, tandis que le résumé des request id et du nombre de retry est efficace pour l’exploitation.
6. La checklist à consulter en premier
- Les limites de message sont-elles écrites noir sur blanc ?
- La réception est-elle structurée en accumulation d’octets → extraction de frame ?
- Traite-t-on
DataReceivedcomme l’arrivée d’un message ? - Y a-t-il des E/S synchrones sur le thread UI ?
- L’envoi est-il un single writer ?
- Les timeouts sont-ils séparés par signification plutôt qu’un seul ?
Handshake/ DTR / RTS sont-ils explicites ?- Le reconnect reconstruit-il la session ?
- Le raw hex dump est-il conservé ?
- Avez-vous testé le débranchement/rebranchement physique et la coupure en cours de transmission ?
Si plusieurs de ces points vous semblent douteux, cela vaut la peine de marquer une pause avant la mise en production.
7. Résumé
Pour finir, récapitulons une dernière fois les points essentiels.
- La communication série est un byte stream, pas des messages
- L’unité
Readet l’unité message ne coïncident pas - Les limites doivent être définies comme un protocole
- Transformer directement
DataReceiveden événement métier est fragile - Séparer les responsabilités d’envoi/réception, et rapprocher l’envoi d’un single writer
- Diviser les timeouts par signification, et concevoir la reconnexion à l’échelle de la session
- Des journaux incluant le raw hex dump rendent l’investigation ultérieure bien plus facile
Autrement dit, dans une application de communication série, la manière dont on interprète la séquence d’octets et dont on contrôle le temps et l’état compte bien plus que le simple fait d’ouvrir le port. Il suffit de séparer ces préoccupations dès la conception initiale pour réduire considérablement les défauts de communication du type « ça casse seulement de temps en temps ».
8. Références
- Microsoft Learn,
SerialPort.DataReceivedEvent - Microsoft Learn,
SerialPort.ReadMethod - Microsoft Learn,
SerialPort.ReadTimeoutProperty - Microsoft Learn,
SerialPort.BaseStreamProperty - Microsoft Learn,
SerialPort.NewLineProperty - Microsoft Learn,
HandshakeEnum - Microsoft Learn,
SerialPort.DtrEnableProperty - Microsoft Learn,
SerialPort.RtsEnableProperty - Microsoft Learn,
SerialPort.GetPortNamesMethod - Microsoft Learn,
SerialPortClass - Microsoft Learn,
COMMTIMEOUTSstructure - Microsoft Learn,
DCBstructure - Microsoft Learn,
CreateFilefunction - pySerial API, Serial API Reference
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Versionner le schéma de base de données d'une application métier — pratiques de migration pour éviter que « chaque client ait une base différente »
Guide pratique pour versionner le schéma de base de données d'applications métier dont les bases sont dispersées chez chaque client. Impl...
CI/CD pratique pour les applications WinForms / WPF — Automatiser du build à la signature et à la distribution avec GitHub Actions
Guide pratique pour mettre en place le CI/CD des applications WinForms / WPF avec GitHub Actions. Couvre un YAML minimal de build+tests s...
Quand votre application Windows maison est signalée comme un virus — gérer les faux positifs de Microsoft Defender et composer avec l'impact sur les performances
Nous détaillons la marche à suivre officielle lorsque Microsoft Defender signale à tort une application Windows développée en interne com...
Veille, mise en veille prolongée, Modern Standby et applications longue durée — concevoir pour éviter « ça s'était arrêté pendant la nuit »
Pourquoi une application Windows censée tourner en continu se retrouve « arrêtée quand on la consulte le matin » : différences entre la v...
Les applications métier fonctionnent-elles sous Windows on Arm ? — La réalité de l'émulation x64 (Prism) et des DLL/COM natifs
Une réponse, destinée aux développeurs et aux services informatiques, à la question « notre application métier fonctionnera-t-elle sous W...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Pour les applications Windows incluant de la communication série, il est plus stable de concevoir dès le départ le traitement de réception, les transitions d'état, la reconnexion et la séparation de l'UI.
Analyse des bugs et des causes
Ce sujet se prête bien au diagnostic des pannes de communication : blocages occasionnels, absence de reprise après un débranchement/rebranchement USB, ou causalité impossible à retracer dans les journaux.
Conseil technique et revue de conception
Clarifier les limites du protocole, le contrôle de flux, les délais d'attente et la conception en single writer avant l'implémentation permet de réduire les défauts coûteux à corriger a posteriori.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- En communication série, un appel à Read(16) permet-il de recevoir exactement 16 octets ?
- Pas nécessairement. La communication série est un byte stream ordonné, et les limites de message ne s'imposent jamais d'elles-mêmes. Un contenu envoyé en un seul Write peut très bien arriver côté récepteur en deux fois, ou concaténé avec d'autres données. Les cas de rupture typiques sont : seule la longueur est lue et le payload n'est pas encore arrivé, une trame et demie arrive et la seconde moitié n'arrive qu'au Read suivant, ou deux trames arrivent regroupées. La parade consiste à accumuler d'abord la réception dans un buffer, puis à laisser un parser en extraire les trames.
- Quelles précautions prendre avec l'événement DataReceived de SerialPort en .NET ?
- DataReceived ne se déclenche pas forcément à chaque octet reçu, et ne s'exécute pas sur le thread UI. Le considérer comme « la notification qu'un message est arrivé » est dangereux. En pratique, il vaut mieux le traiter simplement comme « quelque chose semble être arrivé », ne pas faire de traitement lourd dans le handler, et toujours renvoyer les mises à jour de l'UI vers le thread UI. Une architecture où la séquence d'octets reçus est d'abord accumulée, puis découpée en trames par un parser, est plus stable.
- Comment concevoir les délais d'attente (timeouts) pour la communication série ?
- Un seul timeout ne suffit pas ; il est plus stable de les séparer par signification : l'open timeout jusqu'à l'ouverture du port, l'inter-byte timeout pour l'absence d'octet en cours de trame, le response timeout de l'émission de la commande jusqu'à la réponse complète, et le reconnect backoff, l'intervalle d'attente entre les tentatives de reconnexion. Il est plus stable de considérer les timeouts non pas comme une simple assurance contre la lenteur, mais comme des règles qui font avancer la machine à états. Attention également : placer une lecture synchrone avec ses valeurs par défaut, sans y réfléchir, conduit tout simplement à une attente infinie.
- Pourquoi la reprise ne fonctionne-t-elle pas après un débranchement/rebranchement du câble avec un convertisseur USB-série ?
- Parce qu'avec l'USB-série, il est tout à fait normal que le port disparaisse temporairement, que l'ancien handle devienne invalide, que le numéro de COM change, ou que la requête en attente précédente perde son sens. Un simple nouvel appel à Open() ne suffit pas comme reconnexion. En concevant la reconnexion comme une « régénération de session » regroupant l'invalidation de la session, l'échec des requêtes en attente, l'arrêt du reader et du writer, la réouverture après un backoff, et la ré-exécution de la séquence d'initialisation de l'appareil, on réduit les bugs de reconnexion qui ne se manifestent qu'occasionnellement.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics