web-dev-qa-db-fra.com

Comment faire de la programmation SerialPort robuste avec .NET/C #?

J'écris un service Windows pour la communication avec un lecteur Serial Mag-stripe et une carte de relais (système de contrôle d'accès).

Je rencontre des problèmes où le code cesse de fonctionner (je reçois des exceptions IO) après qu'un autre programme a "interrompu" le processus en ouvrant le même port série que mon service.

Une partie du code est la suivante:

public partial class Service : ServiceBase
{
    Thread threadDoorOpener;
    public Service()
    {
        threadDoorOpener = new Thread(DoorOpener);
    }
    public void DoorOpener()
    {
        while (true)
        {
            SerialPort serialPort = new SerialPort();
            Thread.Sleep(1000);
            string[] ports = SerialPort.GetPortNames();
            serialPort.PortName = "COM1";
            serialPort.BaudRate = 9600;
            serialPort.DataBits = 8;
            serialPort.StopBits = StopBits.One;
            serialPort.Parity = Parity.None;
            if (serialPort.IsOpen) serialPort.Close();
            serialPort.Open();
            serialPort.DtrEnable = true;
            Thread.Sleep(1000);
            serialPort.Close();
        }
    }
    public void DoStart()
    {
        threadDoorOpener.Start();
    }
    public void DoStop()
    {
        threadDoorOpener.Abort();
    }
    protected override void OnStart(string[] args)
    {
        DoStart();
    }
    protected override void OnStop()
    {
        DoStop();
    }
}

Mon exemple de programme démarre avec succès le fil de travail et l'ouverture/la fermeture et le relèvement de DTR font que mon lecteur de bandes magnétiques s'allume (attend 1 seconde), s'arrête (attend une seconde), etc.

Si je lance HyperTerminal et que je me connecte au même port COM, HyperTerminal me dit que le port est actuellement utilisé. Si j’appuie de manière répétée sur ENTER dans HyperTerminal pour essayer de rouvrir le port, il réussira après quelques tentatives.

Cela a pour effet de provoquer des exceptions IO dans mon thread de travail, ce qui est attendu. Cependant, même si je ferme HyperTerminal, j'obtiens toujours la même exception IOException dans mon work-thread. Le seul remède consiste en réalité à redémarrer l'ordinateur.

D'autres programmes (qui n'utilisent pas les bibliothèques .NET pour l'accès au port) semblent fonctionner normalement à ce stade.

Des idées quant à ce qui cause ceci?

25
Thomas Kjørnes

@thomask

Oui, Hyperterminal active effectivement fAbortOnError dans le DCB de SetCommState, ce qui explique la plupart des IOExceptions levées par l'objet SerialPort. Certains PC/ordinateurs de poche ont également des UART dont l'indicateur d'abandon en cas d'erreur est activé par défaut. Il est donc impératif que la routine d'init d'un port série la supprime (ce que Microsoft a négligé de faire). J'ai récemment écrit un long article pour l'expliquer plus en détail (voir ceci si cela vous intéresse).

24
Zach Saw

Vous ne pouvez pas fermer une autre connexion à un port, le code suivant ne fonctionnera jamais:

if (serialPort.IsOpen) serialPort.Close();

Parce que votre objet n'a pas ouvert le port, vous ne pouvez pas le fermer.

De plus, vous devez fermer et éliminer le port série même après des exceptions.

try
{
   //do serial port stuff
}
finally
{
   if(serialPort != null)
   {
      if(serialPort.IsOpen)
      {
         serialPort.Close();
      }
      serialPort.Dispose();
   }
}

Si vous souhaitez que le processus soit interruptible, vous devez vérifier si le port est ouvert, puis reculer pendant un certain temps, puis réessayer.

while(serialPort.IsOpen)
{
   Thread.Sleep(200);
}
4
trampster

Avez-vous essayé de laisser le port ouvert dans votre application et d'activer/désactiver DtrEnable, puis de fermer le port à la fermeture de votre application? c'est à dire:

using (SerialPort serialPort = new SerialPort("COM1", 9600))
{
    serialPort.Open();
    while (true)
    {
        Thread.Sleep(1000);
        serialPort.DtrEnable = true;
        Thread.Sleep(1000);
        serialPort.DtrEnable = false;
    }
    serialPort.Close();
}

Je ne suis pas familier avec la sémantique DTR, donc je ne sais pas si cela fonctionnerait.

2
FryGuy

Comment faire des communications asynchrones fiables

N'utilisez pas les méthodes de blocage, la classe d'assistance interne présente quelques bogues subtiles.

Utilisez APM avec une classe d'état de session, dont les instances gèrent un tampon et un curseur de tampon partagés entre des appels, et une implémentation de rappel qui enveloppe EndRead dans un try...catch. En fonctionnement normal, la dernière chose que le bloc try devrait faire est d’établir le prochain rappel d’E/S superposé avec un appel à BeginRead()

catch devrait appeler un délégué à une méthode de redémarrage de manière asynchrone. L'implémentation de rappel doit se terminer immédiatement après le bloc catch afin que la logique de redémarrage puisse détruire la session en cours (l'état de la session est presque certainement corrompu) et créer une nouvelle session. La méthode de redémarrage doit pas être implémentée sur la classe d'état de session, car cela l'empêcherait de détruire et de recréer la session.

Lorsque l'objet SerialPort est fermé (ce qui se produit lorsque l'application se ferme), une opération d'E/S est peut-être en attente. Dans ce cas, la fermeture de SerialPort déclenche le rappel et, dans ces conditions, EndRead lève une exception impossible à distinguer d'une communication générale. Vous devez définir un indicateur dans votre état de session pour empêcher le comportement de redémarrage dans le bloc catch. Cela empêchera votre méthode de redémarrage d'interférer avec un arrêt naturel.

On peut compter sur cette architecture pour ne pas conserver l'objet SerialPort de manière inattendue.

La méthode de redémarrage gère la fermeture et la réouverture de l'objet de port série. Après avoir appelé Close() sur l'objet SerialPort, appelez Thread.Sleep(5) pour lui laisser une chance de le laisser partir. Il est possible que quelque chose d'autre s'empare du port, alors soyez prêt à le gérer tout en le rouvrant.

2
Peter Wone

J'ai essayé de changer le fil de travail comme ça, avec exactement le même résultat. Une fois que HyperTerminal a réussi à "capturer le port" (pendant que mon thread est en veille), mon service ne pourra plus ouvrir le port.

public void DoorOpener()
{
    while (true)
    {
        SerialPort serialPort = new SerialPort();
        Thread.Sleep(1000);
        serialPort.PortName = "COM1";
        serialPort.BaudRate = 9600;
        serialPort.DataBits = 8;
        serialPort.StopBits = StopBits.One;
        serialPort.Parity = Parity.None;
        try
        {
            serialPort.Open();
        }
        catch
        {
        }
        if (serialPort.IsOpen)
        {
            serialPort.DtrEnable = true;
            Thread.Sleep(1000);
            serialPort.Close();
        }
        serialPort.Dispose();
    }
}
1
Thomas Kjørnes

Ce code semble fonctionner correctement. Je l'ai testé sur ma machine locale dans une application de console, en utilisant Procomm Plus pour ouvrir/fermer le port, et le programme continue de tourner.

    using (SerialPort port = new SerialPort("COM1", 9600))
    {
        while (true)
        {
            Thread.Sleep(1000);
            try
            {
                Console.Write("Open...");
                port.Open();
                port.DtrEnable = true;
                Thread.Sleep(1000);
                port.Close();
                Console.WriteLine("Close");
            }
            catch
            {
                Console.WriteLine("Error opening serial port");
            }
            finally
            {
                if (port.IsOpen)
                    port.Close();
            }
        }
    }
1
FryGuy

Je pense que je suis arrivé à la conclusion que HyperTerminal ne joue pas bien. J'ai effectué le test suivant:

  1. Démarrer mon service en "mode console", il commence à allumer/éteindre l'appareil (je peux dire par sa DEL).

  2. Démarrez HyperTerminal et connectez-vous au port. Le périphérique reste allumé (HyperTerminal déclenche DTR) Mon service écrit dans le journal des événements qu'il ne peut pas ouvrir le port.

  3. Arrêtez HyperTerminal, je vérifie qu'il est correctement fermé à l'aide du gestionnaire de tâches

  4. L'appareil reste éteint (HyperTerminal a abaissé le DTR), mon application continue d'écrire dans le journal des événements, indiquant qu'elle ne peut pas ouvrir le port.

  5. Je lance une troisième application (celle avec laquelle je dois coexister) et lui dis de se connecter au port. Je le fais Aucune erreur ici.

  6. J'arrête l'application mentionnée ci-dessus.

  7. VOILA, mon service redémarre, le port s'ouvre avec succès et le voyant s'allume et s'éteint.

1
Thomas Kjørnes

Y a-t-il une bonne raison de garder votre service de "posséder" le port? Regardez le service UPS intégré - une fois que vous lui avez dit qu'un onduleur est connecté à, disons, COM1, vous pouvez embrasser ce port au revoir. Je suggérerais que vous fassiez de même, à moins que le partage du port ne soit un impératif opérationnel.

0
Coderer

Cette réponse a tardé à être un commentaire ...

Je crois que lorsque votre programme est dans un Thread.Sleep (1000) et que vous ouvrez votre connexion HyperTerminal, HyperTerminal prend le contrôle du port série. Lorsque votre programme se réveille et tente d'ouvrir le port série, une exception IOException est émise.

Modifiez votre méthode et essayez de gérer l’ouverture du port d’une manière différente.

EDIT: À propos de cela, vous devez redémarrer votre ordinateur lorsque votre programme échoue ...

C’est probablement parce que votre programme n’est pas vraiment fermé, ouvrez votre gestionnaire de tâches et voyez si vous pouvez trouver le service de votre programme. Veillez à arrêter toutes vos discussions avant de quitter votre application.

0
Anders R