port szeregowy, ThreadSleep() w UI

0

Potrzebuję obsłużyć sekwencję komend-odpowiedzi do pewnego urządzenia podłączonego po porcie szeregowym.
Z grubsza taka przykładowa sekwencja wygląda tak;
// ***********************************************************************************
// PC:: OPEN PORT,
// => X, <=0x10,
// => V [171B], <=0x06 0x1e 0x34 0x30 0x32 0x03,
// =>S, <=0x06 0x1e 0x34 0x30 0x32 0x03,
// <= 96xVALUE+HEADER [???B],
// =>X, <=0x10,
// CLOSE PORT ::READER
// ***********************************************************************************
Odpowiedzi symuluję wysyłając odpowiednie znaki, lub sekwencje z RealTerm'a. Co do zasady trochę działa, ale...

Problem, który napotkałem polega na tym, że w mojej zmiennej ReaderAnswer (public string ReaderAnswer zadeklarowane w ciele głównego formularza Form1 ) pojawia się odebrana dana, ale dopiero po kliknięciu testowego MessageBox'a (poniżej fragment kodu), a bez MessageBox'a wartość pojawia się dopiero po IF (oczywiście wszystko wysłane na czas, ale obsługa portu szeregowego to iny wątek, Delegate, Invoke, itd.). Wstrzymywanie UI jest akceptowalne, więc używam ThreadSleep() żeby dać sobie szansę na wysłanie symulowanej odpowiedzi, ale jeśli nie będzie tych MessageBox'ów na kolejnych etapach, to dane są jakby spóźnione w mojej sekwencji, a tym samym nigdy się to nie wykona poprawnie. Innymi słowy zatrzymywanie wątku UI przez ThreadSleep niczego nie wnosi (choćbym czekał bardzo długo), a dane odczytane z bufora portu pojawiają się w mojej zmiennej tylko po zaklikaniu MessageBox'a.

Może komuś z Was przyjdzie coś do głowy, może robię jakiś oczywisty błąd...

// ******** START/STOP reading ********
private void pictureBox_startReading_Click(object sender, EventArgs e)
{
bool _error = false;
bool _readingEnabled = true;

        while (_readingEnabled)
        {
            connect();          // Open port
            Thread.Sleep(200);

            //step 1
            ReaderAnswer = string.Empty;
            expectedReaderAnswer = BiotekReader.biotek_str_dle_hex;           // "\x10";
            expectedReaderAnswerLength = expectedReaderAnswer.Length;

            Send_Biotek_X();

            Thread.Sleep(2000);       // muszę mieć czas na odpowiedź

            MessageBox.Show("1");   // KLUCZOWY MessageBox - jeśli go nie będzie, poniższy IF ustawi _error
            //MessageBox.Show(ReaderAnswer + " : " + expectedReaderAnswer);
            if (String.Equals(ReaderAnswer, expectedReaderAnswer))
            {
                //MessageBox.Show(ReaderAnswer + "   ok1   :-)");                 //
            }
            else
            {
                MessageBox.Show(ReaderAnswer + "   !! ok 1" + "\n" + "   ;-(");
                _error = true;
                break;
            }
            //MessageBox.Show(ReaderAnswer + " : " + expectedReaderAnswer);   // poglądając wartość ReaderAnswer będzie ona OK na tym etapie mimo, 
                                                                                                                            // że przed IF'em powyżej była Empty

            Thread.Sleep(1000);

            //step 2
            ReaderAnswer = string.Empty;
            expectedReaderAnswer = BiotekReader.biotek_str_status_6B_hex_2;     //!!
            expectedReaderAnswerLength = expectedReaderAnswer.Length;

            Send_Biotek_V();
            Thread.Sleep(2000);

            MessageBox.Show("2");   // kolejny KLUCZOWY dla poniższego IF'a MessageBox
            rtbox_test_RX.AppendText(" <" + ReaderAnswer);
            if (String.Equals(ReaderAnswer, expectedReaderAnswer))
				...
				...
				...

    // delegate used for Invoke
    internal delegate void StringDelegate(string data);

    /// <summary>
    /// Handle data received event from serial port.
    /// </summary>
    /// <param name="data">incoming data</param>
    public void OnDataReceived(string dataIn)
    {
        //Handle multi-threading
        if (InvokeRequired)
        {
            Invoke(new StringDelegate(OnDataReceived), new object[] { dataIn });
            return;
        }
0

ten kod jak i samo opis jest dziwny. Co to jest Send_Biotek_X i Send_Biotek_V, gdzie się ustawia ReaderAnswer i jak w ogóle działa (ma jakieś timeouty, timmingi czy coś podobnego czy po prostu po wysłaniu polecenia trzeba zaczekać na odpowiedz) komunikacja z tym urządzeniem?

0

Tak, zdaję sobie sprawę, że to wszystko może być niezbyt zrozumiałe - samo zrozumienie protokołu komunikacyjnego tego konkretnego urzadzenia Biotek (nie publikuą dokumentacji, bo maja swoje komercyjne oprogramowanie) zajęło mi kilka tygodni (mogłem podsłuchać komunikację jego oryginalnego oprogramowania, ale mam też takie urządzenie starszego typu w domu). Dziś byłem już mocno sfrustrowany pisząc post niejako w desperacji... Ale miło mi, że ktoś to jednak przeczytał, więc postaram się to doprecyzować.

Na początek sama sekwencja komend i odpowiedzi pokazana na samym początku:

Send_Biotek_X to 1-bajtowa komenda X (wysyłam ASCII "X"), po której spodziewamy się 1-bajtowej odpowiedzi 0x10 (komunikujemy sie z urządzeniem). Nastepnie komenda V ((Send_Biotek_V) - to już trudniejsza sprawa, bo to jest 171 bajtów konfiguracji pomiaru, np. określona długość fali filtra optycznego (urządzenie ma ich kilka, do różnych celów wykorzystujemy różne gługości fali; mam statyczny string odpowiedniej konfiguracji dla mojego przypadku). Jeśli urządzenie zaakceptuje komendę V z parametrami, to odpowiada 6-bajtowym statusem. Status zawsze zaczyna się od dwóch bajtów 0x06, 0x1e (ACK, RS), nastepnie są trzy bajty faktycznego statusu (np. 0x34 0x30 0x32, albo 0x30 0x30 0x30) i kończy się 0x03 (EOT). Wtedy juz tylko podajemy jednobajtową komendę S (Send_Biotek_S), a urzadzenie powinno znowu odpowiedzieć 6-bajtowym statusem (dokładnie tym samym co poprzednio), a następnie zwraca 96 wartosci liczbowych, które są "kwintesencją tematu" (o to walczymy).
I to wszystko umiem wykonać ręcznie z dowolnego terminala - podaję komendy, lub wysyłam przykładowy ciąg dla komendy V, a urządzenie poprawnie odpowiada - sekwencja jest sprawdzona. Zatem wydaje się, że wystarczy "tylko" to zakodować i zabrać sie za interpretację wyników...

I tak dochodzimy do mojego kodu dotyczącego odbierania danych z urządzenia - założyłem, że jeśli dobrze dobiorę pauzy pomiędzy kolejnymi krokami, to w tym właśnie czasie obsługa portu szeregowego w innym wątku odbierze dane (które w warunkach testowych wysyłam z drugiego okienka z RealTerm'a, gdzie mam naszykowane sprawdzone odpowiedzi; porty szeregowe w kompie spięte ze sobą) i będa czekały do oceny ich poprawności zanim wyślę nastepną komendę. Dla przykładu spodziewam się w ReaderAnswer pojawienia się jednego bajtu o wartości 0x10:

        ReaderAnswer = string.Empty;
        expectedReaderAnswer = BiotekReader.biotek_str_dle_hex;           // "\x10";
        expectedReaderAnswerLength = expectedReaderAnswer.Length;

        Send_Biotek_X();

        Thread.Sleep(2000);       // muszę mieć czas na odpowiedź

Następujacy dalej IF ( czy ReaderAnswer = expectedReaderAnswer ? ) wykonuje sie zgodnie z przewidywaniem jedynie, gdy przed nim jest MessageBox.Show - jeśli go nie ma, to ReaderAnswer jest PUSTY (podglądam to zakładając Breakpoint'y). Jeśli nie ma MessageBox.Show, to ReaderAnswer pojawia się dopiero po wykonaiu sie IF...

Tego własnie nie mogę pojąć i nie znalezłem też żadnego pomysłu jak to oszukać :-(

0

Pokaż metodę connect

0

Obsługa portu szeregowego z wykorzystaniem kodu aplikacji Termie (David McClurg):


//Open Port
                CommPort com = CommPort.Instance;
                if (!com.IsOpen)
                {
                    com.Open();
                    this.progressBar_CommStatus.Value = 100;
                }
public void Open()
        {
			Close();

            try
            {
                _serialPort.PortName = Settings.Port.PortName;
                _serialPort.BaudRate = Settings.Port.BaudRate;
                _serialPort.Parity = Settings.Port.Parity;
                _serialPort.DataBits = Settings.Port.DataBits;
                _serialPort.StopBits = Settings.Port.StopBits;
                _serialPort.Handshake = Settings.Port.Handshake;

                // Set the read/write timeouts
                _serialPort.ReadTimeout = 50;
				_serialPort.WriteTimeout = 50;

				_serialPort.Open();
				StartReading();
			}
            catch (IOException)
            {
                StatusChanged(String.Format("{0} does not exist", Settings.Port.PortName));
            }
            catch (UnauthorizedAccessException)
            {
                StatusChanged(String.Format("{0} already in use", Settings.Port.PortName));
            }
            catch (Exception ex)
            {
                StatusChanged(String.Format("{0}", ex.ToString()));
            }

            // Update the status
            if (_serialPort.IsOpen)
            {
                string p = _serialPort.Parity.ToString().Substring(0, 1);   //First char
                string h = _serialPort.Handshake.ToString();
                if (_serialPort.Handshake == Handshake.None)
                    h = "no handshake"; // more descriptive than "None"

                StatusChanged(String.Format("{0}: {1} bps, {2}{3}{4}, {5}",
                    _serialPort.PortName, _serialPort.BaudRate,
                    _serialPort.DataBits, p, (int)_serialPort.StopBits, h));
            }
            else
            {
                StatusChanged(String.Format("{0} already in use", Settings.Port.PortName));
            }
        }		

private void StartReading()
		{
			if (!_keepReading)
			{
				_keepReading = true;
				_readThread = new Thread(ReadPort);
				_readThread.Start();
			}
		}
1

Send_Biotek_X to 1-bajtowa komenda X (wysyłam ASCII "X"), po której spodziewamy się 1-bajtowej odpowiedzi 0x10 (komunikujemy sie z urządzeniem). Nastepnie komenda V ((Send_Biotek_V) - to już trudniejsza sprawa, bo to jest 171 bajtów konfiguracji pomiaru, ...

Ale mi nie chodzi o opis co ta komenda robi bo ja się tego domyślam. Także nie potrzebuję opisu protokołu bo on jest na razie nieważny. Mi chodzi o kod w jaki sposób wysyłasz coś do urządzenia. Ja wiem jak się to powinno robić przy użyciu CommPort ale nie wiem czy Ty to wiesz i czy robisz to poprawnie.

I tak dochodzimy do mojego kodu dotyczącego odbierania danych z urządzenia - założyłem, że jeśli dobrze dobiorę pauzy pomiędzy kolejnymi krokami,

To jest wg mnie błędne podejście - nie powinieneś czekać x czasu tylko powinieneś czekać aż coś przyjdzie. Oczywiście należało by założyć, że jeśli po jakimś czasie (5 - 10 sekund) nie ma odpowiedzi to uznaje się, że odpowiedź już nie nadejdzie.

to w tym właśnie czasie obsługa portu szeregowego w innym wątku odbierze dane

no i o ten kod, który "odbiera te dane" przede wszystkim chodzi. Bez kodu, który jest odpowiedzialny za wysyłanie (zapis do portu) i odbieranie (odczyt portu) jest naprawdę ciężko cokolwiek powiedzieć

0

@abrakadaber:
> w jaki sposób wysyłasz coś do urządzenia
Wysyłam tak, jak napisałem. I działa to DOBRZE - urządzenie odpowiada tak, jak należy. Korzystam z cudzej biblioteki do obsługi portu szeregowego, nic nadzwyczajnego, w zasadzie dość książkowo, Załozyłem, że jesli zawodowy programista to napisał, zrobił obsługę wyjątkowych sytuacji, to wywaliłem mój kod obsługi portu szeregowego i użyłem cudzego.
Jedyna modyfikacja, to "sklejanie danych" odbieranych z bufora (metodą ReadExisting) w OnDataReceived - dla autora tej biblioteki nie miało to znaczenia, bo on doklejał pojawiające się dane do textbox'a, więc nie miało znaczenia w jakich porcjach sie pojawiają. Ja natomiast zauważyłem, że jesli moje urządzenie przykładowo poprawnie odpowiada na wysłaną komendę V, gdy spodziewam się 6 bajtów odpowiedzi, to OnDataReceived jest wołany 2, lub 3 razy, odbierając te 6 bajtów w kawałkach. Stąd składam ReaderAnswer do stanu, gdy jest on zgodny z expectedReaderAnswerLength. I dopiero wtedy badam, czy to jest prawidłowa odpowiedź na tym etapie sekwencji.
Ale to nie z tym mam problem...

> nie powinieneś czekać x czasu tylko powinieneś czekać aż coś przyjdzie
Stąd moje expectedReaderAnswer i expectedReaderAnswerLength zanim wyślę komendę do urządzenia. Niezależnie od tego ile urządzenie potrzebuje czasu na odpowiedź (na status to milisekundy, ale na pomiary to już 30-40 sekund) potrzebowałem w prosty sposób dawać sobie szansę na wysłanie odpowiedzi "z ręki", symulując urządzenie. Ale tak sobie teraz pomyślałem, że bedę czekał aż pojawi się tyle danych ile sie spodziewam sprawdzając ReaderAnswer.Length, a ewentualny timeout obsłużę timerem.

I jeszcze mały update z dnia dzisiejszego:
Zastąpiłem Thread.Sleep(2000) taką funkcją:

        public void Wait(int ms)
        {
            DateTime start = DateTime.Now;
            while ((DateTime.Now - start).TotalMilliseconds < ms)
                Application.DoEvents();
        }

No i działa bez MessageBox.Show!! Czyli jak ja to rozumiem, Thread.Sleep usypiało także asynchroniczną obsługę odbierania danych, lub na etapie kompilacji nieco odwraca się kolejność wykonywania tych fragmentów kodu.

screenshot-20201231014153.png

A poza wszystkim, dziś odebrałem 2 kolejne urządzenia robiące w zasadzie to samo, co to pierwsze, ale których protokoły komunikacyjne są supełnie inne (raczej brak dokumentacji, trzeba podsłuchiwać oryginalne programy do ich obsługi, tylko jest problem z ich znalezieniem, bo sa stare - "epoki Win95"). Z tym wszystkimm przyjdzie mi się zmierzyć w najbliższym czasie, ale obsługę różnych "normalnych" funkcji tej apki chyba oddam komuś do napisania, bo to dla mnie taki lekko hobbistyczny projekcik - z C# mam do czynienia od kilku miesięcy, od 20+ lat nie kodowałem w niczym obiektowym. Ciagle mam niespodziewane problemy i strasznie mi sie to w czasie rozłazi, a ktoś tam na to trochę czeka...

1
dstachur napisał(a):

No i działa bez MessageBox.Show!! Czyli jak ja to rozumiem, Thread.Sleep usypiało także asynchroniczną obsługę odbierania danych,

Jakbyś wiedział jak działa ta biblioteka i co robi (dlatego dwa razy prosiłem o kod ODBIERAJĄCY dane z portu COM i się nie doczekałem) to byś wiedział czy tam jest wątek czy nie. Pisanie na ślepo to nie programowanie a używanie tej funkcji to zabijanie wydajności - zobacz sobie ile Twój program zjada procesora podczas działania tej funkcji. Jak już to powinna ona wyglądać tak:

public void Wait(int ms)
        {
            DateTime start = DateTime.Now;
            while ((DateTime.Now - start).TotalMilliseconds < ms)
            {
                Thread.Sleep(100);
                Application.DoEvents();
            }
        }

A poza wszystkim, dziś odebrałem 2 kolejne urządzenia robiące w zasadzie to samo, co to pierwsze, ale których protokoły komunikacyjne są supełnie inne (raczej brak dokumentacji, trzeba podsłuchiwać oryginalne programy do ich obsługi, tylko jest problem z ich znalezieniem, bo sa stare - "epoki Win95").

Przyzwyczaj się :p - tak to zazwyczaj działa w starych (nowe w większości przypadków starają się trzymać jakiś standard) mocno specyficznych urządzeniach

0

@abrakadaber:
*> zobacz sobie ile Twój program zjada procesora
*Widzisz, ja się na codzień zajmuję nieco innym IT i mam do dyspozycji kilka tysięcy rdzeni CPU różnej klasy, więc w tym przypadku może to zabijać 100% CPU (1 rdzenia?) przez te pół sekundy, byle efekt był właściwy. A jeśli wiedziałbym dlaczego działa to inaczej, niż wydaje mi sie, że powinno, to nie pytałbym na forum... Fajnie, że podpowiadasz jak zrobić to lepiej, ale żeby mnie tak "łajać" to bez sensu raczej...

> Pisanie na ślepo to nie programowanie
Ja chyba wole "pisać na ślepo", próbując różnych sposobów, niż nie pisać w ogóle. A jak mi się znudzi, to oddam to komuś bardziej profesjonalnemu. W kazdym razie jedni maja z tego chleb, a inni mają fun.

> Przyzwyczaj się :p - tak to zazwyczaj działa w starych (nowe w większości przypadków starają się trzymać jakiś standard) mocno specyficznych urządzeniach
Ja jestem przyzwyczajony, bo studia skończyłem ponad 20 lat temu, lutownicy używam od ponad 30 lat i raczej lubie dłubać w "starociach" :-)
Dawniej w instrukcji do nowego samochodu pisali jak regilować zawory, a dziś jest napisane, żeby nie pić kwasu z akumulatora - podobnie jest z brakiem dokumentacji do współczesnych modeli urządzeń. W przypadku mojej komunikacji ze starym urządzeniem diagnostycznym miałem zagwozdkę związaną z ustawianiem linii DTR (ComPort.DtrEnable = true) - dziś już nikt normalny nie używa tych sygnałów (kto ostatnio widział z bliska RS232 na DB25?!), ale dawniej to komputer musiał wykazać gotowość do przyjecia danych (bo nie potrafił robić zbyt wiele jednoczesnie). A jak czytasz współczesną dokumentację do obsługi portu szeregowego w C#, albo oglądasz na YT tutorial jak to zakodować, to nikt o DTR nie wspomina i jak nie spróbujesz "na ślepo" paru opcji, to nie pójdziesz dalej.

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