SerialPort i nasłuchiwanie w C#
W poprzedniej notce zaprezentowałem prosty emulator Modbus, który niedawno napisałem. W tej notce postaram się napisać w jaki sposób zaimplementowałem obsługę portu szeregowego COM oraz nasłuchiwanie nieblokujące aplikację.
Zaczynając od początku, SerialPort jest klasą z .NET Framework, która umożliwia obsługę portów szeregowych. Na początek przykład, w jaki sposób pobrać listę dostępnych w komputerze portów:
string[] ports = SerialPort.GetPortNames(); |
I tyle, tablica stringów zawiera nazwy dostępnych portów. Ważniejszym krokiem jest ustanowienie połączenia (tu przykładowe dane):
SerialPort sp = new SerialPort(); sp.ReadTimeout = 1000; sp.WriteTimeout = 1000; sp.PortName = "COM1" // Jeden z wylistowanych portów sp.BaudRate = 115200; sp.DataBits = 8 sp.Parity = Parity.None; sp.StopBits = StopBits.One sp.Encoding = Encoding.BigEndianUnicode; try { sp.Open(); } catch (Exception err) { /* ... */ } |
Skoro port został otwarty można już coś do niego wysłać:
byte[] data = /* jakieś dane */ sp.Write(data, 0, data.Length); |
Odbieranie danych wygląda analogicznie:
byte[] data = null; if (sp.BytesToRead > 0) { data = new byte[sp.BytesToRead]; sp.Read(data, 0, data.Length); } |
Troszkę większym problemem jest sprawienie, by aplikacja oczekiwała na nowe dane, a w razie czego je obsłużyła, nie powodując jednocześnie zastoju. Do tego celu należy użyć wątków, a dokładniej jednego, w którym należy wywołać taką funkcję:
private boolean bReceiving = false; public void Receive() { byte[] data; while (bReceiving) { if (sp.BytesToRead > 0) { data = new byte[sp.BytesToRead]; sp.Read(data, 0, data.Length); receiveHandler.DataReceivedEvent(data); } Thread.Sleep(50); // Żeby nie zużywać niepotrzebnie zasobów } } |
Funkcja ta, jak widać zawiera pętle, która sprawdza czy istnieją dane do odczytu, jeśli tak to je odczytuje i przekazuje dalej do metody, która z nich skorzysta. Kod wywołania wątku oraz interfejs receiveHandler jest następujący:
interface IDataReceiveListener { void DataReceivedEvent(byte[] data); } Thread transmissionThread = null; void Run() { bReceiving = true; transmissionThread = new Thread(() => { Receive(); }); transmissionThread.Start(); } |
Teraz najważniejsze, ponieważ korzystam w tym kodzie z dwóch wątków (głównego + dodatkowego), w tym miejscu należy zwrócić szczególną uwagę na dostęp do danych prze oba wątki. W mojej aplikacji założyłem, że drugi wątek obsługuje obiekt SerialPort (mimo że tworzę go w pierwszym wątku), zatem dostęp do tego obiektu z wątku głównego (tutaj w przypadku wysyłania) musi być opatrzony sekcją krytyczną:
Thread.BeginCriticalRegion(); sp.Write(data, 0, data.Length); Thread.EndCriticalRegion(); |
Natomiast jest jeszcze przypadek gdy wątek drugi odbiera dane i odwołuje się do obiektów z wątku pierwszego. W tym przypadku należy skorzystać funkcji Invoke() klasy Form, która przyjmuje obiekt typu delegate, a jego zadaniem jest wywołanie odpowiedniej funkcji z wątku pierwszego. Kod który to robi jest następujący:
private delegate void ProcessRequestDelegate(byte[] data); /* funkcja interfejsu IDataReceiveListener */ public void DataReceivedEvent(byte[] data) { this.Invoke(new ProcessRequestDelegate(this.ProcessRequest), new object[] { data }); } private void ProcessRequest(byte[] data) { // some stuff } |
I to wszystko na ten temat. Jak widać pisanie aplikacji okienkowych w C# jest banalnie proste.
michal:
Z tego co widzę, sama biblioteka wygląda przyjaźniej niż RXTX dla javy.
Jednak analogiczny program w javie dało się zrobić bez większych problemów jako wieloplatformowy.
Jak się będę nudził, to sprawdzę czy twój modbus działa z mono pod linuksem :)
1 February 2011, 1:07 amNetrix:
Z tego co czytałem to pod Javą jest problem przy zabawie bitami, z racji braku typu unsigned. Generalnie nie lubię babrać się w Javie jak nie muszę, zwłaszcza, że w C# pisze się dużo przyjemniej, w dużo wygodniejszym IMO środowisku. No i klasa SerialPort sama zachęcała do skorzystania z niej.
Szczerze mówiąc nie miałem okazji bawić się Mono, więc jakbyś się tym bawił to pokaż rezultaty :).
8 February 2011, 3:16 amArek:
Dobry artykuł, lepszy niż wiele tych po angielsku. Mam jednak pewien problem. Otóż potrzebuję portu RS-232 do komunikacji z urządzeniem zewnętrznym – jest ustanowiony algorytm komunikacyjny (nie ważne jaki, nie będę opisywał szczegółów). Dane które odczytałem z portu, a raczej z urządzenia zewnętrznego zostaną obrobione przez klasę algorytmu komunikacyjnego.
1 August 2012, 10:55 amNie mam jak wykorzystać metody Invoke() w klasie algorytmu nie mam żadnych kontrolek interfejsu użytkownika. Co powinienem zrobić w takiej sytuacji?
Konrad:
Witam, a czy nie łatwiej i lepiej by było zamiast tworzyć specjalnych wątków użyć gotowych Eventów ?
np. sp.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);
void sp_DataReceived(object sender, SerialDataReceivedEventArgs e){}
i wtedy w metodzie sp_DataReceived można wstawić:
if (sp.BytesToRead > 0)
{
data = new byte[sp.BytesToRead];
sp.Read(data, 0, data.Length);
receiveHandler.DataReceivedEvent(data);
}
Dzięki temu Event uruchamia się tylko wtedy, gdy przyjdą jakieś dane, zamiast tak jak wątek co 50ms.
Pzdr
29 August 2012, 11:38 amNetrix:
@Arek
Szczerze mówiąc dawno nie pisałem w C# i moja wiedza na dzień dzisiejszy jest raczej szczątkowa. Na twoim miejscu poszukałbym w jaki sposób działa Invoke() i spróbował to zrobić analogicznie, tylko bez kontrolek.
@Konrad
22 September 2012, 9:34 pmMasz rację, lepiej skorzystać z czegoś gotowego. W chwili pisania użycie wątków przyszło znacznie szybciej niż Eventy, dlatego ich użyłem. Dzięki za cenna informację.