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(); |
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)
{
/* ... */
} |
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); |
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);
} |
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
}
} |
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();
} |
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(); |
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
} |
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.