Осигуряване на комуникация чрез асинхронен сериен порт

В момента нашето приложение се свързва с Arduino през сериен порт. Изпращаме някои ASCII-форматирани команди и получаваме същото в замяна. За да направим това, имаме опашка от команди, нишка, посветена на писането на тези команди в порта, и нишка, посветена на четенето и обработката на всички входящи отговори. Самият клас е отговорен за изпращането на отговорите, което му дава твърде голяма отговорност (трябва да отговаря само за пристанищните операции, а не за бизнес логиката).

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

Ние обаче сме използвали само асинхронни методи (особено в уеб операции), а не сме писали такава система. Може ли някой да ни даде насоки как да продължим?

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

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

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


person drharris    schedule 02.08.2011    source източник


Отговори (2)


Ако търсите производителност и асинхронизация на най-доброто ниво, предлагам да разгледате Completion Ports. Това е, което в крайна сметка е отдолу, скрито в ядрото на Windows, и е страхотно. Когато ги използвах, използвах C++, дори открих грешка в ядрото поради него, но бях ограничен до езика.

Видях тази статия в CodeProject, която може да си струва да проучите, за да видите къде може да развие идеята ви и/или да използва кода, който е там.

Естеството на портовете за завършване е да работят върху обратни извиквания. Тоест, като цяло вие "поставяте" заявка в опашката и когато нещо попадне там, заявката се чете и указаното обратно извикване се чете. Всъщност това е опашка, но както казах, на най-ниското (управляемо) ниво (преди да стане почти на метал).

РЕДАКТИРАНЕ: Написах нещо като помощна програма за тестване на FTP сървър/клиент с портове за завършване, така че основният процес е същият - четене и писане на команди в queuable мода. Дано помогне.

РЕДАКТИРАНЕ #2: Добре, ето какво бих направил въз основа на вашите отзиви и коментари. Щях да имам "изходяща опашка", ConcurrentQueue<Message>. Можете да имате отделна нишка за изпращане на съобщения, като премахнете всяко съобщение от опашката. Забележка, ако искате да е малко по-„безопасно“, предлагам да надникнете в съобщението, да го изпратите и след това да го извадите от опашката. Както и да е, класът Message може да бъде вътрешен и да изглежда по следния начин:

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

В класа singleton (ще го нарека CommunicationService), също бих имал ConcurrentBag<Action<Response>>. Тук започва забавлението :o). Когато отделен концерн иска да направи нещо, той се регистрира, например, ако имате 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, когато получите отговор, вие просто трябва да a

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

Воаля, разделяне на загрижеността. Отговаря ли по-добре на въпроса ви?

Друга възможна тактика би била да свържете съобщение и обратно извикване, но ако обратното извикване е Func<Response, bool> и нишката на диспечера проверява дали резултатът, върнат от Func, е верен, тогава това обратно извикване се изхвърля.

person Anže Vodovnik    schedule 02.08.2011
comment
Това е доста интересно и ще го разгледам. В този конкретен сценарий ние сме по-малко загрижени за производителността и повече за управлението на проблемите. Класът Arduino не трябва да се интересува какво се прави с данните (както прави сега), той трябва да остави тази отговорност на класовете, които отговарят за изпращането на първоначалните команди. Като се има предвид, че само ‹100 от тези команди се изпращат на минута, производителността не е критична, но това със сигурност ще бъде интересно четиво. - person drharris; 02.08.2011
comment
Могат ли вашите отговори да бъдат изпратени не по ред? Или винаги е така, че основната ви система ще ги обработва на опашка? - person Anže Vodovnik; 02.08.2011
comment
Те могат да бъдат изпратени по всяко време, във всякакъв ред. Различни компоненти в системата ще правят заявки в почти произволни моменти и микроконтролерът ще ги обработва донякъде асинхронно. Например, един компонент може да поиска температурата, което е бърза операция, докато друг компонент движи стъпков двигател, което отнема повече време. Заявката и отговорът на сензора може да се случи през цялото време на работа на двигателя. Заявките се обработват линейно, но отговорите, които получавам, не идват в определен ред. - person drharris; 02.08.2011
comment
Но можете да заключите кой отговор е за кое съобщение? Ако е така, тогава предлагам да преместите цялата грижа към система, базирана на конвейер. Затова използвайте опашка за изпращане и тръбопровод за отговор на достигане. Това означава, че всеки член получава известие за отговор. Ще редактирам #2 моя отговор по-горе, за да отразя това. - person Anže Vodovnik; 03.08.2011
comment
Това всъщност е точно по линията на това, което си мислехме, но ни помага да го абстрахираме още повече. Мисля, че отделянето на тази част в класа TemperatureMeter е липсващата част. Опитвахме се да направим това директно от друга нишка, поради което кодът нарастваше. Вероятно ще направим една крачка напред и ще капсулираме този вид логика в abstract class и ще позволим всеки базов клас да бъде създаден от конфигурация от фабрика. Благодарим ви, че ни помогнахте да го изясним! - person drharris; 03.08.2011

По-добър избор, ако използвате 4.0, е да използвате BlockingCollection за това, за по-стари версии използвайте комбинация от Queue<T> и AutoResetEvent. Така че ще бъдете уведомени, когато артикулът бъде добавен и в потребителската нишка и след това просто го консумирайте. тук използваме технология за натискане, където при текущото ви внедряване вие ​​използвате технология за анкета „всеки път, когато питате дали има някакви данни“.

Пример: 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
    }
}
person Jalal Said    schedule 02.08.2011
comment
По същество вече правим това, за да обработим входящите данни от серийния порт. Ние капсулираме всеки отговор и го поставяме в BlockingCollection. След това друга нишка просто чака да дойде нов отговор и след това го обработва, след като го получи. Въпросът тук е как след това да го обработя по начин, който ще позволи на втори потребител (специфичен за отговора, който получавам) да изчака този отговор. - person drharris; 02.08.2011