Как сделать надежное программирование SerialPort с .NET/С#?
Я пишу службу Windows для связи с считывателем Serial Mag-stripe и релейной панелью (системой контроля доступа).
Я сталкиваюсь с проблемами, когда код перестает работать (я получаю 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 заставляет мой считыватель Mag-stripe включать (ждать 1сек), выключать (ждать 1 сек) и т.д.
Если я запустил HyperTerminal и подключился к тому же COM-порту, HyperTerminal сообщит, что порт используется. Если я повторно нажимаю ENTER в HyperTerminal, чтобы попытаться снова открыть
порт будет успешным после нескольких попыток.
Это приводит к вызову IOExceptions в моем рабочем потоке, который ожидается. Однако, даже если я закрываю HyperTerminal, я все равно получаю то же самое IOException в своей рабочей нити. Единственное средство - фактически перезагрузить компьютер.
Другие программы (которые не используют библиотеки .NET для доступа к портам), как представляется, работают нормально на этом этапе.
Любые идеи относительно того, что вызывает это?
Ответы
Ответ 1
@thomask
Да, Hyperterminal фактически разрешает fAbortOnError в DCC SetCommState, что объясняет большую часть IOExceptions, создаваемых объектом SerialPort. Некоторые ПК/карманные компьютеры также имеют UART, которые по умолчанию отключили флаг ошибки, поэтому необходимо, чтобы процедура инициализации последовательного порта очистила его (что Microsoft не выполняла). Недавно я написал длинную статью, чтобы объяснить это более подробно (см. это, если вам интересно).
Ответ 2
Вы не можете закрыть какое-либо соединение elses с портом, следующий код никогда не будет работать:
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);
}
Ответ 3
Вы пытались оставить порт открытым в своем приложении и просто включить/отключить 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, поэтому я не знаю, будет ли это работать.
Ответ 4
Как сделать надежные асинхронные сообщения
Не используйте методы блокировки, у внутреннего вспомогательного класса есть некоторые тонкие ошибки.
Используйте APM с классом состояния сеанса, экземпляры которого управляют буфером и буфером, разделяемым между вызовами, и реализацией обратного вызова, которая обертывает EndRead
в try...catch
. При нормальной работе последнее, что должен сделать блок try
, - это настроить следующий перекрываемый обратный вызов ввода-вывода с вызовом BeginRead()
.
Когда все пошло не так, catch
должен асинхронно вызывать делегат на метод перезапуска. Реализация обратного вызова должна выйти сразу после блока catch
, чтобы логика перезапуска могла уничтожить текущий сеанс (состояние сеанса почти наверняка повреждено) и создать новый сеанс. Метод перезапуска не должен быть реализован в классе состояния сеанса, поскольку это предотвратит его уничтожение и воссоздание сеанса.
Когда объект SerialPort закрыт (что произойдет при выходе приложения), может быть ожидающая операция ввода-вывода. Когда это произойдет, закрытие SerialPort вызовет обратный вызов, и в этих условиях EndRead
выдаст исключение, которое неотличимо от общего comms shitfit. Вы должны установить флаг в состоянии сеанса, чтобы запретить поведение перезапуска в блоке catch
. Это остановит ваш метод перезапуска от вмешательства в естественное завершение работы.
В этой архитектуре можно полагаться, чтобы не удерживать объект SerialPort неожиданно.
Метод перезапуска управляет закрытием и повторным открытием объекта последовательного порта. После вызова Close()
объекта SerialPort
вызовите Thread.Sleep(5)
, чтобы дать ему возможность отпустить. Возможно, что-то еще захватит порт, поэтому будьте готовы справиться с этим, повторно открывая его.
Ответ 5
Я попытался изменить рабочий поток, как это, с тем же результатом. Как только 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();
}
}
Ответ 6
Этот код работает нормально. Я тестировал его на своей локальной машине в консольном приложении, используя 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();
}
}
}
Ответ 7
Думаю, я пришел к выводу, что HyperTerminal не играет хорошо. Я выполнил следующий тест:
-
Запустите мой сервис в режиме консоли, он включит/выключит устройство (я могу сказать по нему светодиод).
-
Запустите HyperTerminal и подключитесь к порту.
Устройство остается включенным (HyperTerminal повышает DTR)
Моя служба записывает в журнал событий, что он не может открыть порт
-
Остановить HyperTerminal, я проверяю, что он правильно закрыт с помощью диспетчера задач
-
Устройство остается выключенным (HyperTerminal понизил DTR), мое приложение продолжает записывать в журнал событий, говоря, что он не может открыть порт.
-
Я запускаю третье приложение (с которым мне нужно сосуществовать) и сообщайте ему, чтобы он подключался к порту. Я делаю так. Здесь нет ошибок.
-
Я останавливаю вышеупомянутое приложение.
-
VOILA, мое обслуживание снова начинает работать, порт открывается успешно, и светодиод горит вкл/выкл.
Ответ 8
Этот ответ получил длинный комментарий...
Я считаю, что когда ваша программа находится в Thread.Sleep(1000), и вы открываете свое соединение HyperTerminal, HyperTerminal берет на себя управление последовательным портом. Когда ваша программа затем просыпается и пытается открыть последовательный порт, генерируется исключение IOException.
Перепроектируйте свой метод и попытайтесь обработать открытие порта по-другому.
EDIT:
Об этом вы должны перезагрузить компьютер, когда ваша программа не работает...
Вероятно, потому, что ваша программа не закрыта, откройте свой диспетчер задач и посмотрите, можете ли вы найти свой программный сервис. Обязательно остановите все свои потоки перед выходом из приложения.
Ответ 9
Есть ли веская причина, чтобы ваш сервис "владел" портом? Посмотрите на встроенный сервис ИБП - как только вы скажете, что там подключен ИБП, например, COM1, вы можете поцеловать этот порт до свидания. Я бы посоветовал вам сделать то же самое, если не будет достаточно оперативных требований для совместного использования порта.