C# wielowątkowe pobieranie i przetwarzanie danych z SerialPort i ich wyświetlanie na wykresie

0

Witam,

Moim zadaniem jest napisanie aplikacji, która pobierze dane z SerialPort, przetworzy i wyświetli na wykresie. Do tego celu mam specjalną płytkę - moją na której siedzi STM32 i wysyła dane strumieniowo na żądanie aplikacji. Danych jest ograniczona ilość.

Aplikacja jest i działa, ale niestety podczas rejestracji danych wszystko jest zablokowane (jeden wątek). Postanowiłem ulepszyć to cudo i napisać wielowątkowo, tak aby dane były odświeżane na wykresie w "czasie rzeczywistym" oraz abym mógł przerwać rejestrację. Niestety czytanie o Multithreading w Google nie wiele mi na razie daje dlatego postanowiłem poprosić o wskazówki, przykładowe kody i ew. wyjaśnienia.

W mojej Aplikacji stworzyłem klasę board.cs która reprezentuje moją płytkę. Po wywołaniu metody RegisterData dzieje się to:

public void RegisterData(UInt16 TimeMeas)
    {
        intTimeMeas = (int)(TimeMeas) + 2;
        RxData.Clear();
        ActiveSignal = 1;
        Usart.DiscardInBuffer();
        string line = "Data=" + TimeMeas.ToString();
        DataTransferIsComplete = false;
        Console.WriteLine(line);
        UsartTimer.Enabled = true;
        Usart.WriteLine(line);
        TimerTick = 0;
        while (!DataTransferIsComplete)
        {
            Application.DoEvents();
            System.Threading.Thread.Sleep(10);         
        }
        
        RxData.Clear();
        ActiveSignal = 2;
        Usart.DiscardInBuffer();
        line = "Data2?";
        DataTransferIsComplete = false;
        Console.WriteLine(line);
        UsartTimer.Enabled = true;
        Usart.WriteLine(line);
        while (!DataTransferIsComplete)
        {
            System.Threading.Thread.Sleep(10);
            Application.DoEvents();
        }
    }

Metoda jako argument przyjmuje czas rejestracji danych w [s]. Po wysłaniu do urządzenia polecenia: "Data=5\n" płytka odsyła mi przez 5 sekund dane. Po zakończeniu transferu, proszę o drugi zestaw danych poleceniem "Data2?\n" i odbieram.
Zmienna DataTransferIsComplete jest ustawiana w moim Timerze po przekroczeniu Timeoutu....

Poniżej odbiór danych i obsługa tick timera:

private void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        try
        {
            TimerTick = 0;
            byte[] data = new byte[Usart.BytesToRead];
            Usart.Read(data, 0, data.Length);
            RxData.AddRange(data);
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    private void UsartTimerTick(object sender, EventArgs e)
    {
        TimerTick++;
        clk++;
        UpdateProgress(100 * clk / (intTimeMeas * 10));
        if (TimerTick > 20)
        {
            Console.WriteLine("Odebrano danych: " + RxData.Count);
            UsartTimer.Enabled = false;
            TimerTick = 0;
            if (ActiveSignal == 1)
            {
                Data1 = ConvertRxDataToData(RxData);
            }
            else
            {
                Data2 = ConvertRxDataToDataTwo(RxData);
                clk = 0;
                UpdateProgress(0);
            }
            DataTransferIsComplete = true;
        }
    }

W każdym wywołaniu port_DataReceived jest zerowana zmienna TimerTick . Jeżeli dane przestają przychodzić to TimerTick przekracza 20 i rozpoczynam obliczenia na danych a dopiero na koniec po za klasą board wywołuje:

Chart.Series["Data1"].Points.DataBindY(board.GetData1());
Chart.Series["Data2"].Points.DataBindY(board.GetData2());

Jak widać odbieram dwa strumienie. Najpierw jeden, potem drugi. Zależy mi na tym aby ten pierwszy był w czasie rzeczywistym rysowany na charcie, ten drugi jest krótki i nie musi być.

Próbowałem dokonywać obliczeń i wyświetlać na wykresie metodzie port_DataReceived ale rzucało wyjątkiem, że dane są modyfikowane przez inny wątek. No bo przecież ta metoda działa już winnym wątku. Nie wiem za bardzo jak to połączyć więc proszę o jakieś wskazówki...

0

Kod nie do przyjęcia. Zobacz, ile Ci się go powtarza. Trzeba z tego wyekstrahować dodatkowe metody. A więc w pierwszej kolejności:

void PrepareForData(int activeSignal, string output)
{
        RxData.Clear();
        ActiveSignal = activeSignal;
        Usart.DiscardInBuffer();
        string line = output;
        DataTransferIsComplete = false;
        Console.WriteLine(line);
        UsartTimer.Enabled = true;
        Usart.WriteLine(line);
}

public void RegisterData(UInt16 timeMeasure)
{
  intTimeMeas = (int)(timeMeasure) + 2;
  PrepareForData(1,  "Data=" + timeMeasure.ToString());

  TimerTick = 0;
  while (!DataTransferIsComplete)
  {
       Application.DoEvents();
       System.Threading.Thread.Sleep(10);         
  }

  PrepareForData(2, "Data2?");

  while (!DataTransferIsComplete)
  {
       System.Threading.Thread.Sleep(10);
       Application.DoEvents();
  }
}

Jak widzisz - nadal za dużo kodu się powtarza - pętla. A więc ją też należy wywalić do metody:

void ProcessMessages()
{
  while (!DataTransferIsComplete)
  {
       System.Threading.Thread.Sleep(10);
       Application.DoEvents();
  }
}

I generalnie cała ta część powinna iść do wątku. A więc:

poublic void DoJob(UInt16 timeMeasure)
{
  Thread th = new Thread(new ParameterizedThreadStart(RegisterData)); //tu podajesz nazwę metody, która ma działać w wątku, a z tego wzlędu, że metoda ma parametr, to ParametrizedThreadStart zamiast ThreadStart
  th.Start(timeMeasure);
}

To Ci się pewnie nie skompiluje, bo teraz metoda RegisterData jest metodą dla wątku. Z parametrem. A taka metoda jako parametr oczekuje object. Więc musisz jeszcze zmienić tak:

public void RegisterData(object objTimeMeasure)
{
   UInt16 timeMeasure = Convert.ToUint16(objTimeMeasure); //konwertujesz sobie to
   //dalej reszta kodu
}

Teraz RegisterData pracuje Ci w wątku.

Problemem jeszcze jest wywalanie tekstu na konsolę. Twój kod za dużo robi. Zamiast bezpośredniego wywalania tekstu na konsolę, powinien wywoływać jakieś zdarzenie. Rozumiesz?

0

Dokonałem modyfikacji i już jest lepiej. To znaczy, faktycznie teraz odbieranie danych działa w drugim wątku.
Teraz moim problemem jest wczytanie danych na wykres. Na początek zróbmy tak, abym dostał informację o zakończeniu odbierania danych, żebym mógł wywołać metodę w tym czasie która wrzuci mi dane na wykres. Jak to zrobić?

Problemem jeszcze jest wywalanie tekstu na konsolę. Twój kod za dużo robi. Zamiast bezpośredniego wywalania tekstu na konsolę, powinien wywoływać jakieś zdarzenie. Rozumiesz?>

Nie rozumiem. Wcześniej pisałem w C i to głownie systemy embedded. Przez zdarzenie rozumiem przerwanie z zewnątrz. W mikrokontrolerach wiedziałem jak je obsłużyć. Tu... nie wiem jak rozumieć przywołane przez Ciebie zdarzenie...

Dodano:
Trochę poczytałem o tym, już mniej więcej wiem jak to działa. Dopisałem event zakończenia pobierania do klasy board:

    public delegate void ZakonczonoPobieranie();
    public event ZakonczonoPobieranie PodczasZakonczenia;

Oraz wywołanie tego eventu w obsłudze timera wtedy kiedy dane są już odebrane:

private void UsartTimerTick(object sender, EventArgs e)
    {
        TimerTick++;
        clk++;
        //UpdateProgress(100 * clk / (intTimeMeas * 10));
        if (TimerTick > 20)
        {
            Console.WriteLine("Odebrano danych: " + RxData.Count);
            UsartTimer.Enabled = false;
            TimerTick = 0;
            
            if (ActiveSignal == 1)
            {
                Data1= ConvertRxDataToData(RxData);
            }
            else
            {
                Data2 = ConvertRxDataToDataTwo(RxData);
                clk = 0;
                UpdateProgress(0);
                if (PodczasZakonczenia != null)
                {
                    PodczasZakonczenia();
                }
            }
            DataTransferIsComplete = true;
        }
    }

W programie głównym napisałem dodawanie danych do wykresu w tym evencie oraz dodanie delegata:

        board.PodczasZakonczenia += board_PodczasZakonczenia;
        
       void board_PodczasZakonczenia()
        {
            ErbaHorizontalChart.Series["Data1"].Points.DataBindXY(board.GetData1_xvalues(), board.GetData1());
            ErbaHorizontalChart.Series["Data2"].Points.DataBindXY(board.GetData2_xvalues(), board.GetData2());

            Cursor.Current = Cursors.Default;
            System.Media.SystemSounds.Asterisk.Play();
        }

Wyrzuca mi błąd w metodzie void board_PodczasZakonczenia() w głównej klasie. Próbuje pobrać dane z innego wątku i sie sypie...:
Cross-thread operation not valid: Control 'ErbaHorizontalChart' accessed from a thread other than the thread it was created on.

0

Poczytaj jeszcze o Invoke. "C# thread invoke" <- te hasła Cię interesują. To jest swego rodzaju synchronizacja wątków.

0

Dzięki,

Zmodyfikowałem nieco metodę:

void board_PodczasZakonczenia()
        {
            ErbaHorizontalChart.Invoke(new Action(delegate()
            {
                ErbaHorizontalChart.Series["Data1"].Points.DataBindXY(board.GetData1_xvalues(), board.GetData1());
                ErbaHorizontalChart.Series["Data2"].Points.DataBindXY(board.GetData2_xvalues(), board.GetData2());
            }));
            

            Cursor.Current = Cursors.Default;
            System.Media.SystemSounds.Asterisk.Play();
        }

Teraz działa. Fajnie, bo pobieranie danych nie blokuje mi aplikacji. Chciałbym teraz aby dane na wykresie były uzupełniane na bieżąco, w czasie rzeczywistym... Jak to zrobić żeby to miało ręce i nogi?

0

Najlepiej to będzie jak w wątku na bieżąco będziesz wywoływał jakieś zdarzenie za każdym razem jak odbierzesz dane. Coś w stylu DataReceived z odpowiednimi parametrami. A w GUI obsługujesz to analogicznie do tego, co zrobiłeś. Ale nadal to robisz źle, bo posługujesz się jakimś timerem w wątku zamiast wątek usypiać na jakiś czas. Przy okazji, ja teraz patrzę na swój kod, to prawdopodobnie walnąłem babola i nie powinno tam być Application.DoEvents(). Przynajmniej w wersji, gdy metoda pracuje w wątku.

[Trochę nudnej teorii, która może Ci bardziej namieszać]
Tak naprawdę to zadziała trochę inaczej niż podejrzewasz. Wykres nie narysuje się w momencie odebrania danych, tylko w jakimś innym momencie - za to odpowiada metoda Invoke. Jak to po co i dlaczego nie wtedy kiedy chcesz? Istnieje coś takiego jak kolejka komunikatów. Wszystko, co robisz (przesunięcie myszy, kliknięcie, wciśnięcie klawisza, cokolwiek) powoduje tak naprawdę wysłanie jakiegoś komunikatu do Windows. Windows sobie te komunikaty w odpowiedni sposób kolejkuje. Następnie wysyła je do aplikacji, a aplikacja sama już sobie je obsługuje. W systemie nic nie dzieje się jednocześnie (zakładamy jednordzeniowy procesor). Mimo, że jest wielowątkowy to tak naprawdę system w taki sposób steruje tymi wątkami, że najpierw działa sobie jeden wątek, za chwilę działa drugi, potem trzeci, czwarty itd. (to nieco bardziej skomplikowane). Dlatego też, jeśli wątek odbierze Ci dane i wywoła zdarzenie, że dane zostały odebrane, metoda Invoke w obsłudze tego zdarzenia poczeka z wykonaniem tego kodu, do momentu, kiedy system przekaże kontrolę do wątku GUI. Tego użytkownik nie zobaczy, ale Ty możesz zaobserwować takie "skoki" w kodzie, jeśli będziesz to debugował.

0

Ideę pracy wielowątkowej rozumiem, dlatego w pierwszym poście napisałem w "czasie rzeczywistym" celowo w cudzysłowie. Po prostu nie znam mechanizmów pracy na wątkach.

Wiem, że ten wykres będzie się rysował np co 50 ms a nie w real time.

Jak sobie poradzić z timeoutem? Mówisz że źle że korzystam z Timera. Na pewno masz rację, robię tak dlatego że nie wiem jak można inaczej.
Ja wiem ile mam danych odebrać. Np pobieram dane przez 30s. Baudrate to 1Mb/s (sporo...). Dane próbkuje z częstotliwością 10 kHz, wysyłam po 2 bajty. Co daje 2B * 10 kHz * 30 s = 600000B danych. Mógłbym ustawić sobię, że wątek ma czekać na dane aż pozyska 600000B, ale co będzie jak przyjdzie 599999 B bo gdzieś jeden Bajt (powiedzmy zostanie nadpisany w buforze, albo np, stracę zasilanie mojej płytki wysyłającej dane. Program się zawiesi. Dlatego wykorzystuje Timer który wywołuje zdarzenie co 100 ms i inkrementuje zmienną. W zdarzeniu odebrania danych SerialPort tą zmienną zeruje. Dzięki temu podczas odbierania danych zmienna jest ciągle zerowana i nie osiąga dużej wartości. Gdy dane przestają przychodzić, i zmienna osiągnie np wartość 20, to wiem, że nastąpił koniec i już nic nie przyjdzie. Wtedy obsługuje zdarzenie zakończenia pobierania danych i rysuje zebrane dane na wykresie. I tu wkracza moje pytanie, jak w takim razie zrealizować obsługę timeoutu bez użycia Timera. Powiedziałeś że powinienem usypiać wątek. Ok, to zamiast DoEvents. A co zamiast Timera?

1 użytkowników online, w tym zalogowanych: 0, gości: 1