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.

5 Comments

  1. 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 :)

  2. Netrix:

    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 :).

  3. Arek:

    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.
    Nie mam jak wykorzystać metody Invoke() w klasie algorytmu nie mam żadnych kontrolek interfejsu użytkownika. Co powinienem zrobić w takiej sytuacji?

  4. 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

  5. Netrix:

    @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
    Masz 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ę.

Leave a comment