Обеспечение асинхронной связи через последовательный порт - PullRequest
1 голос
/ 02 августа 2011

В настоящее время наше приложение подключается к Arduino через последовательный порт. Мы отправляем некоторые команды в формате ASCII и получаем то же самое в ответ. Для этого у нас есть очередь команд, поток, предназначенный для записи этих команд в порт, и поток, предназначенный для чтения и обработки всех входящих ответов. Сам класс отвечает за отправку ответов, что накладывает на него слишком большую ответственность (должен отвечать только за операции порта, а не за бизнес-логику).

Мы бы предпочли сделать это асинхронно. Все в системе может отправить команду с функцией обратного вызова и таймаутом. Если последовательный порт получает правильный ответ, он вызывает функцию обратного вызова. В противном случае он прерывается и, возможно, вызывает второй обратный вызов (или, возможно, один обратный вызов с флагом succeeded?).

Однако мы когда-либо использовали только асинхронные методы (особенно в веб-операциях), а не написали такую ​​систему. Кто-нибудь может дать нам несколько советов о том, как действовать?

Наш текущий план заключается в хранении очереди этих команд. При любом ответе, если найдена связанная команда (путем сравнения значений ASCII), она удаляется из очереди и выполняется обратный вызов. Таймер будет периодически проверять таймауты, отключать и выполнять соответствующий обратный вызов. Это кажется простым решением, но объем кода для его поддержки существенно возрастает, и мы хотели убедиться, что для этого не было лучших встроенных решений или передовых методов.

Редактировать : Чтобы уточнить, этот конкретный класс является одноэлементным (к лучшему или к худшему), и есть много других работающих потоков, которые могут получить к нему доступ. Например, один поток может захотеть запросить значение датчика, в то время как другой поток может управлять двигателем. Эти команды и связанные с ними ответы не происходят линейно; время может быть изменено. Таким образом, традиционной модели производитель-потребитель недостаточно; это скорее диспетчер.

Например, давайте назовем этот одноэлементный класс Arduino. Thread A запущен и хочет отправить команду "*03", поэтому он вызывает Arduino.Instance.SendCommand("*03"). Между тем, Thread B отправляет команду "*88", обе из которых отправляются почти в реальном времени. Через некоторое время поток Arduino SerialPort.Read() получает ответ для *88, а затем ответ для *03 (то есть в обратном порядке, в котором они были отправлены). Как мы можем позволить Thread A и Thread B правильно блокировать ожидание получения определенного ответа? Мы предполагаем, что будем использовать AutoResetEvent внутри каждого потока с асинхронным обратным вызовом, чтобы позволить нам .Set it.

Ответы [ 2 ]

3 голосов
/ 02 августа 2011

Если вам нужна производительность и асинхронность на самом высоком уровне, я рекомендую изучить порты завершения.Это то, что в конечном итоге скрыто в ядре Windows, и это потрясающе.Когда я использовал их, я использовал C ++, даже из-за этого обнаружил ошибку ядра, , но Я был ограничен языком.

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

Природа портов завершения заключается в работе с обратными вызовами.То есть, как правило, вы «помещаете» запрос в очередь, и когда что-то попадает туда, запрос читается, а указанный обратный вызов читается.На самом деле это очередь, но, как я уже сказал, на самом низком (управляемом) уровне (до того, как попасть почти на металл).

РЕДАКТИРОВАТЬ: Я написал что-то вродеУтилита тестирования FTP-сервера / клиента с портами завершения, поэтому базовый процесс такой же - чтение и запись команд в режиме в очереди .Надеюсь, это поможет.

РЕДАКТИРОВАТЬ # 2: Хорошо, вот что я хотел бы сделать, основываясь на ваших отзывах и комментариях.У меня была бы «исходящая очередь», ConcurrentQueue<Message>.Вы можете создать отдельную ветку для отправки сообщений, удалив каждое сообщение из очереди. Заметьте, если вы хотите, чтобы оно было немного более "безопасным", я предлагаю заглянуть в сообщение, отправить его, а затем снять его. В любом случае, класс Message может быть внутренним и выглядеть примерно так:

private class Message {
    public string Command { get; set; }
    ... additonal properties, like timeouts, etc. ...
}

В синглтон-классе (я назову это CommunicationService) у меня также будет ConcurrentBag<Action<Response>>.Теперь начинается самое интересное: о).Когда отдельное предприятие хочет что-то сделать, оно регистрируется, например, если у вас есть TemepratureMeter, я бы сделал так:

public class TemperatureMeter {
   private AutoResetEvent _signal = new AutoResetEvent(false);

   public TemperatureMeter {
     CommunicationService.AddHandler(HandlePotentialTemperatureResponse);
   }

   public bool HandlePotentialTemperatureResponse(Response response) {
     // if response is what I'm looking for
     _signal.Set();

     // store the result in a queue or something =)
   }

   public decimal ReadTemperature() {
     CommunicationService.SendCommand(Commands.ReadTemperature);
     _signal.WaitOne(Commands.ReadTemperature.TimeOut); // or smth like this

     return /* dequeued value from the handle potential temperature response */;
   }

}

А теперь, в вашем CommunicationService, когда выполучить ответ, вы просто к

foreach(var action in this._callbacks) {
   action(rcvResponse);
}

Вуаля, разделение проблем.Отвечает ли он на ваш вопрос лучше?

Другая возможная тактика - соединить сообщение и обратный вызов, но при этом обратный вызов будет Func<Response, bool>, и поток диспетчера проверяет, верен ли результат, возвращенный из Func,тогда этот обратный вызов расположен .

1 голос
/ 02 августа 2011

Лучшим выбором, если вы используете 4.0, является использование BlockingCollection для этого, для более старых версий используйте комбинацию из Queue<T> и AutoResetEvent. Таким образом, вы будете уведомлены , когда элемент добавлен и находится в потоке потребителя, а затем просто будет его использовать. здесь мы используем технологию push, когда в вашей текущей реализации вы используете технологию опроса «каждый раз, когда вы спрашиваете, есть ли какие-либо данные».

Пример: 4.0

//declare the buffer
private BlockingCollection<Data> _buffer = new BlockingCollection<Data>(new ConcurrentQueue<Data>());

//at the producer method "whenever you received an item":
_messageBuffer.Add(new Data());

//at the consumer thread "another thread(s) that is running without to consume the data when it arrived."
foreach (Data data in _buffer.GetConsumingEnumerable())// or "_buffer.Take" it will block here automatically waiting from new items to be added
{
    //handle the data here.
}

Пример: другие «младшие» версии:

private ConcurrentQueue<Data> _queue = new ConcurrentQueue<Data>();
private AutoResetEvent _queueNotifier = new AutoResetEvent(false);

//at the producer:
_queue.Enqueue(new Data());
_queueNotifier.Set();

//at the consumer:
while (true)//or some condition
{
    _queueNotifier.WaitOne();//here we will block until receive signal notification.
    Data data;
    if (_queue.TryDequeue(out data))
    {
        //handle the data
    }
}
...