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.