Как выполнить надежное программирование SerialPort с помощью .NET / C #?

StackOverflow https://stackoverflow.com/questions/441559

  •  22-07-2019
  •  | 
  •  

Вопрос

Я пишу службу Windows для связи с последовательным считывателем магнитных полос и релейной платой (системой контроля доступа).

Я сталкиваюсь с проблемами, когда код перестает работать (я получаю IOExceptions) после того, как другая программа "прервала" процесс, открыв тот же последовательный порт, что и моя служба.

Часть кода выглядит следующим образом:

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();
    }
}

Мой пример программы успешно запускает рабочий поток, а открытие / закрытие и повышение DTR приводит к включению моего считывателя с магнитной полосой (подождите 1 секунду), завершению работы (подождите 1 секунду) и так далее.

Если я запускаю HyperTerminal и подключаюсь к тому же COM-порту, HyperTerminal сообщает мне, что порт в данный момент используется.Если я повторно нажму ENTER в HyperTerminal, чтобы попытаться повторно открыть порт, это удастся после нескольких попыток.

Это приводит к возникновению исключений IOExceptions в моем рабочем потоке, что и ожидалось.Однако, даже если я закрою HyperTerminal, я все равно получу то же самое исключение IOException в моем рабочем потоке.Единственное лекарство на самом деле - перезагрузить компьютер.

Другие программы (которые не используют .СЕТЕВЫЕ библиотеки для доступа к портам), похоже, на данный момент работают нормально.

Есть какие-нибудь идеи относительно того, что является причиной этого?

Это было полезно?

Решение

@thomask

Да, Hyperterminal действительно включает fAbortOnError в DCB SetCommState, что объясняет большинство исключений IOExceptions, генерируемых объектом SerialPort .На некоторых ПК / карманных компьютерах также есть UART, у которых по умолчанию включен флаг прерывания при ошибке, поэтому крайне важно, чтобы процедура инициализации последовательного порта очищала его (чего Microsoft не сделала).Недавно я написал длинную статью, чтобы объяснить это более подробно (см. это если вам интересно).

Другие советы

Вы не можете закрыть чье-либо соединение с портом, следующий код никогда не будет работать:

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

Поскольку ваш объект не открыл порт, вы не можете его закрыть.

Также вы должны закрыть и утилизировать последовательный порт даже после возникновения исключений

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

Если вы хотите, чтобы процесс был прерываемым, вам следует проверить, открыт ли порт, затем отключиться на некоторое время, а затем повторить попытку, что-то вроде.

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

Вы пытались оставить порт открытым в своем приложении и просто включить / выключить DtrEnable, а затем закрыть порт при закрытии приложения? то есть:

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();
}

Я не знаком с семантикой DTR, поэтому не знаю, сработает ли это.

Как сделать надежную асинхронную связь

Не используйте методы блокировки, у внутреннего вспомогательного класса есть некоторые тонкие ошибки.

Используйте APM с классом состояния сеанса, экземпляры которого управляют буфером и буферным курсором, совместно используемыми между вызовами, и реализацией обратного вызова, которая оборачивает EndRead в try ... catch . В обычной работе последнее, что должен сделать блок try , - это установить следующий перекрывающийся обратный вызов ввода / вывода с вызовом BeginRead () .

Когда дела идут плохо, catch должен асинхронно вызывать делегата для метода перезапуска. Реализация обратного вызова должна завершиться сразу после блока catch , чтобы логика перезапуска могла уничтожить текущий сеанс (состояние сеанса почти наверняка повреждено) и создать новый сеанс. Метод restart должен not быть реализован в классе состояния сеанса, потому что это предотвратит его разрушение и воссоздание сеанса.

Когда объект SerialPort закрыт (что произойдет при выходе из приложения), вполне может быть ожидающая операция ввода-вывода. Когда это так, закрытие SerialPort вызовет обратный вызов, и при этих условиях EndRead сгенерирует исключение, которое неотличимо от обычного дерьма в области связи. Вы должны установить флаг в своем состоянии сеанса, чтобы запретить поведение перезапуска в блоке catch . Это предотвратит вмешательство вашего метода перезапуска в естественное отключение.

На эту архитектуру можно положиться, чтобы неожиданно не удерживать объект SerialPort.

Метод restart управляет закрытием и повторным открытием объекта последовательного порта. После вызова Close () для объекта SerialPort вызовите Thread.Sleep (5) , чтобы дать ему возможность отпустить. Возможно, что-то еще захватит порт, поэтому будьте готовы с этим справиться, открывая его снова.

Я думаю, что пришел к выводу, что HyperTerminal не очень хорошо играет. Я выполнил следующий тест:

<Ол>
  • Запустите мой сервис в «режиме консоли», он начнет включать / выключать устройство (это можно узнать по его индикатору).

  • Запустите HyperTerminal и подключитесь к порту. Устройство остается включенным (HyperTerminal поднимает DTR) Мой сервис пишет в журнал событий, что не может открыть порт

  • Остановите HyperTerminal, убедитесь, что он правильно закрыт с помощью диспетчера задач

  • Устройство остается выключенным (HyperTerminal снизил DTR), мое приложение продолжает запись в журнал событий, говоря, что не может открыть порт.

  • Я запускаю третье приложение (с которым мне нужно сосуществовать) и говорю ему подключиться к порту. Я так и делаю. Здесь нет ошибок.

  • Я остановил вышеупомянутое приложение.

  • VOILA, мой сервис снова включается, порт успешно открывается, и светодиод горит / выключается.

  • Я попытался изменить рабочий поток, как это, с тем же результатом. Однажды HyperTerminal удастся «захватить порт». (пока мой поток спит), моя служба не сможет снова открыть порт.

    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();
        }
    }
    

    Этот код работает правильно. Я протестировал его на своем локальном компьютере в консольном приложении, используя Procomm Plus, чтобы открыть / закрыть порт, и программа продолжает тикать.

        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();
                }
            }
        }
    

    Этот ответ должен быть длинным, чтобы стать комментарием...

    Я полагаю, что когда ваша программа находится в потоке.Режим ожидания (1000) и вы открываете свое гипертерминальное соединение, гипертерминал получает контроль над последовательным портом.Затем, когда ваша программа просыпается и пытается открыть последовательный порт, выдается исключение IOException.

    Измените свой метод и попробуйте обработать открытие порта по-другому.

    Редактировать:О том, что вам придется перезагрузить компьютер, когда ваша программа выйдет из строя...

    Вероятно, это связано с тем, что ваша программа на самом деле не закрыта, откройте свой taskmanager и посмотрите, сможете ли вы найти свой программный сервис.Обязательно остановите все ваши потоки перед выходом из приложения.

    Есть ли веская причина для того, чтобы ваш сервис не имел " владения " порт? Посмотрите на встроенный сервис ИБП - как только вы скажете, что к СОМ1 подключен ИБП, вы можете поцеловать этот порт на прощание. Я бы посоветовал вам сделать то же самое, если нет строгих эксплуатационных требований для совместного использования порта.

    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top