Laden...

Serielle Schnittstelle -> mehrere Befehle hintereinander

Erstellt von justcp vor 7 Jahren Letzter Beitrag vor 7 Jahren 4.536 Views
J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren
Serielle Schnittstelle -> mehrere Befehle hintereinander

Guten Tag allerseits,

erstmal muss ich sagen, das ist ein tolles Forum. Da nun die Suchfunktion nichts ergeben hat, habe ich mich hier angemeldet um mein Problem öffentlich darzustellen.

Folgendes Szenario:
Ich möchte ein kleines Tool schreiben, welches in bestimmten Intervallen, mehrere Befehle an ein Gerät welches über die serielle Schnittstelle mit dem Rechner verbunden ist, senden.
Dazu habe ich zwei richTextBox's. Die eine um mir die Resultate anzeigen zu lassen ( DataReceivedHandler) und die zweite um die Befehle reinzuschreiben.

Im moment steuere ich ein Netzteil, jedoch tritt folgendes Problem auf. Wenn mehrere Befehle eingegeben werden, splitte ich diesen String in dein string-array. Über eine foreach loop schicke ich dann jeden Befehl einzeln. Soweit so gut.

Jedoch wird immer nur der erste Befehl abgearbeitet wenn ich kein Delay dazwischen schalte. Meine Frage ist nun, wie realisiere ich das ganze ohne einen Delay? Was mache ich falsch? Was kann ich besser machen?

Das Programm ist bis aufs mindeste Reduziert. Den Serialport habe ich über die Toolbox hinzugefügt.

Danke im voraus.


namespace SerReadOut
{
    public partial class Form1 : Form
    {
        char[] bufferarray = new char[4096];     
        public string[] sendebefehle = new string[1024];

        public Form1()
        {
            InitializeComponent();

        }




        private void button1_Click(object sender, EventArgs e)
        {
            try { 
            serialPort1.Open();
           
            }
            catch { MessageBox.Show("NO"); }
            }


        private void button2_Click(object sender, EventArgs e)
        {
            serialPort1.Close();
           
        }




 

        private void WRITE_BOX(string data)
        {
         
            richTextBox1.Invoke(new MethodInvoker(delegate () { richTextBox1.AppendText("\n" + data); }));
            Array.Clear(BLOCKBUFFER, 0, BLOCKBUFFER.Length);
        }



   
        private void button3_Click(object sender, EventArgs e)
        {
                        
            
            string befehle =   (richTextBox2.Text);
            sendebefehle = befehle.Split(new string[] { "\r\n", "\n" }, StringSplitOptions.None);
          


            
            foreach(string value in sendebefehle) { 


                serialPort1.Write(value + "\r");

          

            }
            
            
        }


        private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {

            int count = serialPort1.BytesToRead;
            byte[] data = new byte[count];
            string resu =  serialPort1.ReadTo("K");        
            WRITE_BOX(resu);


        }
    }
}

16.806 Beiträge seit 2008
vor 7 Jahren
  1. Du solltest Programmlogik nicht mit UI mischen
    [Artikel] Drei-Schichten-Architektur

  2. Du solltest die SerialPort-Kommunikation in einen extra Task/Thread auslagern
    Sofern Du das gemacht hast beachte [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

Zum Problem:
Der SerialPort braucht per default kein Delay.
Kann das Gerät überhaupt mehrere Befehle puffern? Stimmen die SerialPort Settings wie Baudrate, HandShake und Co?

888 Beiträge seit 2007
vor 7 Jahren

Hier auch noch was zu SerialPort:
Template SerialPort

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

hallo,

die Settings stimmen soweit. Ob das Gerät mehrere Befehle Puffern kann, wage ich zu bezweifeln da dieses immer nur einzelne Befehle annimmt. Habe schon alles in einem char-Array versendet aber da hat sich gar nichts mehr getan.

Das Template habe ich mal umgeschrieben um mehrere Befehle abzuschicken und mir die Antworten anzeigen zu lassen. Auch hier taucht wieder das selbe Problem auf. Wenn ich eine kleine Pause einlege (sei es durch den Debugger etc.) tauchen die Ergebnisse wunderbar nach der Reihe auf. Ohne Delay wird nur das Ergebnis des ersten Befehls angezeigt.

Beim Threading wiederum kann ich mir das Konstrukt nicht vorstellen, wie ich es machen sollte. Wie sollte ich dann vorgehen? eine Sende-Routine welche z.B. bei 10 Befehlen, 10 Threads erstellt und nacheinander abarbeitet? Aber auch hier kommt es dann zu den selben Problemen nehme ich an?

16.806 Beiträge seit 2008
vor 7 Jahren

Nein.
Du sollst einen Task (lassen sich einfacher programmieren als Threads) für die komplette Kommunikation verwenden - einfach, dass das Konstrukt in sich gekapselt ist.
Niemand hat was gesagt von pro Command ein Thread - davon abgesehen, dass der SerialPort gar nicht Threadsafe ist.

Wie man eine Command-Queue für den SerialPort baut, wenn man es benötigt:
Semaphore ist das Stichwort.
Waiting for Serial Port response between command calls

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

Hallo,

habe nun das Problem gelöst indem ich eine Routine geschrieben habe, welche manuell den Puffer ausliest bzw. ohne auf das DatareceivedEvent zu warten, da dieses manchmal Probleme mit sich brachte. Zum Timeout: Den habe ich über die Systemzeit in dieser Routine erstellt.

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

hallo.
Nun hat sich ein neues "Problemchen" aufgetan.

In meinem Programm schicke ich einen Befehl und lese dann den Puffer aus.

Nun habe ich eine Option hinzugefügt, welche es mir ermöglicht dieselben Befehle immer und immer wieder in einem einstellbaren Zeitabstand abzuarbeiten. Dies tue ich, indem ich eine Routine über einen Thread starte.

Das Problem hierbei ist nun das sich bei einem Intervall von 1000ms ein kleiner Fehler fortpflanzt welcher mit dem Serialport zu tun hat.

Über eine StopWatch habe ich mal beobachtet das die Abarbeitungszeit ständigt wächst. Beginnend bei 520ms bin ich nach ca. 10 min. bei 1000ms und spätestens dann, schmeisst er mir eine exception wegen dem Thread.Sleep(interval - timer.elapsedMilliseconds), da dieser kleiner Null ist.

Der Code ist rein provisorisch!

 public void READOUT_THREAD_INTERVAL()

        {
            try
            {
               // while (true)
             //   {
                    timer.Reset();
                    timer.Start();
                     z = 0;
                     turn = 0;
                    isReady = true;

                    if ((isReady) && (z < Sender_Text_in_Zeilen.Length))
                    {

                        for (int i = 0; i < Item.Item2.Length; i++)
                        {
                            if ((Item.Item2[i] != null) && (Item.Item2[i] != ""))
                            {
                                //BEFEHLE WERDEN EINZELN GESCHICKT UND....
                                device.serport.Write("\u0001" + Item.Item2[i] + "\u0003");

                                isReady = false;


                              //....ANSCHLIESSEND AUS DEM PUFFER AUSGELESEN
                                READ_BUFFER_DEVICE(Item.Item1[i], erstesdatum);
                                erstesdatum++;
                            }
                        }
                        z++;
                    }

                    z = 0;
                    erstesdatum = 0;

                    richTextBox2.Invoke(new MethodInvoker(delegate () { richTextBox2.AppendText("\n"); }));
                    richTextBox3.Invoke(new MethodInvoker(delegate () { richTextBox3.AppendText("\n"); richTextBox3.ScrollToCaret(); }));

                    if (trimkonstante < 1)
                    {
                        funktionen.TRIM_ONE_TIME_COMMANDS(Item);
                        trimkonstante += 1;
                    }
                    isReady = true;
                    turn = 1;

                    if (timeout_val > 5)
                    {
                        richTextBox2.Invoke(new MethodInvoker(delegate () { richTextBox2.AppendText("Connection closed due Timeout"); }));
                        device.serport.Close();
                        timeout_val = 0;
                    }
                    timer.Stop();
                    Console.WriteLine(timer.ElapsedMilliseconds);
                    
                    System.Threading.Thread.Sleep((Convert.ToInt32(numericUpDown_Sender_Interval.Value)) - (int)timer.ElapsedMilliseconds);
                }
         //   }
            catch (Exception ex)
            {

                if (ex.GetType().IsAssignableFrom(typeof(InvalidOperationException)))
                {

                    Thread.CurrentThread.Abort();

                }
            }
        }

Ich kann mir leider nicht ausmalen woher diese ständig wachsende Abarbeitungszeit kommt. Daher meine Frage an euch... Wie würdet ihr das mit dem Interval lösen?
Auf das DataReceivedEvent möchte ich ungerne zurückgreifen, da dies beim Auslesen des Geräts Probleme und Tücken mit sich bringt.

Die Routine zum Auslesen sieht folgender Maßen aus:

 public void READ_BUFFER_DEVICE(string Notiz,int erstesdatum)
        {
           
            string str = "";
            int count = 0; 
            double second = DateTime.Now.Second;
            double TimeoutLimit = second + 2;                
            while( (!str.Contains("\u0001") && (!str.Contains("\u0003")) && (second <= TimeoutLimit)) ){

                count = this.device.serport.BytesToRead;
                str += this.device.serport.ReadExisting();
                second = DateTime.Now.Second;

            }
            if(second >= TimeoutLimit || str == "")
            {
                WRITE_INTOBOX(Notiz, "Fehler! - Timeout",1);
                timeout_val += 1;
            }
            else { 
            WRITE_INTOBOX(Notiz, str.Trim('\u0002', '\u0003'),erstesdatum);
            }
            isReady = true;

            
        }      
T
708 Beiträge seit 2008
vor 7 Jahren

woher diese ständig wachsende Abarbeitungszeit kommt

Du startest den Timer wieder neu, bevor der Code durchlaufen ist. Das bedeute, dass das Interval nach jedem Durchlauf um die Abarbeitungszeit Deiner Funktionalität kürzer wird.

Das vom Timer aufgerufene Event sollte erst ganz am Ende den Timer wieder neu Starten!

Auf das DataReceivedEvent möchte ich ungerne zurückgreifen, da dies beim Auslesen des Geräts Probleme und Tücken mit sich bringt.

Nein, das tut es gewiss nicht. Die Tücke liegt bei der Konfiguration des COM-Ports.
Und der Verwendung von mehreren Threads (Timer + Event).

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

Ach sorry, das war der falsche Code.

Ich starte den Thread t1 welcher die Funktion READOUT_THREAD_INTERVAL() ausführt.
diese läuft in einer while Schleife bzw. so:

public void READOUT_THREAD_INTERVAL()

        {
            try
            {
                while (true)
                {
                    timer.Reset();
                    timer.Start();
                     z = 0;
                     turn = 0;
                    isReady = true;

                    if ((isReady) && (z < Sender_Text_in_Zeilen.Length))
                    {

                        for (int i = 0; i < Item.Item2.Length; i++)
                        {
                            if ((Item.Item2[i] != null) && (Item.Item2[i] != ""))
                            {

                                device.serport.Write("\u0001" + Item.Item2[i] + "\u0003");

                                isReady = false;
                                READ_BUFFER_DEVICE(Item.Item1[i], erstesdatum);
                                erstesdatum++;
                            }
                        }
                        z++;
                    }

                    z = 0;
                    erstesdatum = 0;

                    richTextBox2.Invoke(new MethodInvoker(delegate () { richTextBox2.AppendText("\n"); }));
                    richTextBox3.Invoke(new MethodInvoker(delegate () { richTextBox3.AppendText("\n"); richTextBox3.ScrollToCaret(); }));

                    if (trimkonstante < 1)
                    {
                        funktionen.TRIM_ONE_TIME_COMMANDS(Item);
                        trimkonstante += 1;
                    }
                    isReady = true;
                    turn = 1;

                    if (timeout_val > 5)
                    {
                        richTextBox2.Invoke(new MethodInvoker(delegate () { richTextBox2.AppendText("Connection closed due Timeout"); }));
                        device.serport.Close();
                        timeout_val = 0;
                    }
                    timer.Stop();
                    Console.WriteLine(timer.ElapsedMilliseconds);
                    
                    System.Threading.Thread.Sleep((Convert.ToInt32(numericUpDown_Sender_Interval.Value)) - (int)timer.ElapsedMilliseconds);
               }
            }
            catch (Exception ex)
            {

                if (ex.GetType().IsAssignableFrom(typeof(InvalidOperationException)))
                {

                    Thread.CurrentThread.Abort();

                }
            }
        }

"timer" ist eine StopWatch mit der ich mir die benötigte Zeit zum Abarbeiten des Durchgangs anzeigen lasse.

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

Nach ellenlangem Debugging habe ich nun den Fehler ausfindig machen können.

Die Abarbeitungszeit steigt durch das ständige Append der Resultate in die Richtextbox.
Durch Auskommentieren dieses Abschnitts bleibt nun auch die Abarbeitungszeit konstant.
Das Problem ist nur, ich brauche trotzdem etwas, wo die Resultate für den Anwender ablesbar sind.

Daher meine Frage:
Sind Listboxen, Textboxen etc. genauso anfällig ?

16.806 Beiträge seit 2008
vor 7 Jahren

Trenn Deinen Code ordentlich.
[Artikel] Drei-Schichten-Architektur Hab ich die aber oben schon gesagt und Du leider ignoriert.
Dein Invoke kostet Zeit und der beeinflusst die Abarbeitung.

Leg dann Abarbeitung in einen Task aus und informier die GUI via Events über neue Elemente.
Dann ist das Problem mit sehr hoher Wahrscheinlichkeit weg und Du hast gleichzeitig besseren Code, den andere dann auch besser verstehen.
(Edit)Der Einwand von LaTino mit dem StringBuilder ist dennoch sehr ratsam (/Edit)

Provisorischer Code wird in 99% der Fälle produktiver Code.
Mein Tipp: gewöhn Dir provisorischen Code gar nicht erst an.

3.003 Beiträge seit 2006
vor 7 Jahren

Gutes Beispiel, wieso die UI getrennt sein sollte vom Rest.

Du hast das gebaut, was man einen "Schlemiel" nennt *. strings sind immutable, das heisst, jedesmal, wenn du was dranhängst, wird der ale String Zeichen für Zeichen in einen neuen kopiert, und dann die zusätzlichen Zeichen hinten angehängt. Wenn das oft vorkommt, wie in deinem Fall, nimmt die Performance exponentiell ab, weil das kopieren jedesmal länger dauert (die Zeichenkette wird ja auch länger).

Benutze einen StringBuilder, den du befüllst EDIT: und zwar eventgesteuert, so wie Abt das schrieb, und verknüpfe den Text deines Controls damit. StringBuilder leidet nicht unter dem o.g. Problem von Strings.

LaTino

Edit 2: lol @Abt, zwei sich gegenseitig per gleichzeitigem edit referenzierende Antworten...^^

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

Schande über mein Haupt 😃
Danke für die Aufklärung über die Append Funktion.
Werde das schnellstens mit dem Stringbuilder ausprobieren.

Jedoch ist mir dort auch etwas unklar. Nehmen wir an das Programm läuft eine etwas längere Zeit. Irgendwann bin ich doch sicherlich an der max.Capacity des Stringbuilders angelangt.

Wäre es dann nicht sinnvoller, nach jedem durchlauf einen Task zu starten, welcher seinen derzeitigen Inhalt in die RTB anhängt und anschließend diesen clearen? Aber irgendwann ist doch die Zeit zum anhängen des Textes größer als die Abarbeitungszeit des Durchlaufs... Überschneidet sich dann nicht das ganze?

Eventgesteuert hört sich sehr gut an. Da ich noch recht neu auf dem Gebiet bin, frage ich mich wie ich ein Event für einen Stringbuilder erstellen kann. Gibt es da etwas wie onValuechanged?

16.806 Beiträge seit 2008
vor 7 Jahren

Das Limit des StringBuilders wird nur durch .NET und dessen maximale Größe eines Objekt innerhalb eines Prozesses limitiert.
Das sind theoretisch 4GB, praktisch durch Objektoverhead weniger.

Die Logik, diesen Aufzuräumen; die kannste Dir ja überlegen.
Das mit dem Task macht jedenfalls so wenig sinn.

3.003 Beiträge seit 2006
vor 7 Jahren

...aber Gedanken darüber wieviel Text deine RichTextBox eigentlich fassen kann, bis der Speicher vollgelaufen ist, machst du dir nicht? 😄

Und nein, du hast das mit dem Ereignis nicht ganz richtig verstanden. Nicht der StringBuilder hat ein Ereignis, sondern als Ergebnis eines Ereignisses (letzten Endes: wenn Daten ankommen) wird der vorhandene StringBuilder benutzt und weiter gefüllt, sowie die Textbox mit dem Ergebnis ToString() des StringBuilders gefüllt. Im Prinzip leidet das immer noch daran, dass die ToString()-Ausführung immer länger dauernd wird, aber jetzt nicht mehr exponentiell, sondern linear (was ein Riesenunterschied ist).

Um auf den ersten Punkt zurückzukommen: Möglicherweise ist es für extrem lange Zeiträume sinnvoll, die Daten nicht direkt in eine RichTextBox zu schreiben, sondern irgendwohin, wo man auch locker mehrere Gigabyte Daten versenken kann. Datenbank zum Beispiel - aber diese Frage ist eine grundsätzlichere.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)

J
justcp Themenstarter:in
8 Beiträge seit 2016
vor 7 Jahren

Also habe es mit dem StringBuilder und dem Event ausprobiert.
Die Abarbeitungszeit bleibt jetzt gleich, dennoch habe ich nach allen 10 Durchgängen eine Aufsummierung von 1ms.... Das heisst nach 1000 Durchgängen geht schon eine Sekunde verloren.
Das mit der Datenbank ist ein interessanter Ansatz. Jedoch fehlen mir dazu noch die Skills.
Gibt es kein anderes Control, welches nicht ständig neu Zeichnet bzw. alles komplett kopiert?

Ich habe das Gefühl das ich daran jetzt scheitere 😕

3.003 Beiträge seit 2006
vor 7 Jahren

Das einfachste, was mir dazu einfällt, wäre, nur noch die letzten x Einträge zu speichern. Wie gesagt, dein Problem hat sich jetzt verlagert. Es tickt schön linear hoch - was schon einmal eine Verbesserung ist, allerdings wird die anzuzeigende Zeichenkette immer länger.

Lass dir mal im Notepad eine 1-GB-Datei öffnen. Das dauert halt 'ne Weile. Die vorgeschlagene Lösung wäre also, eben nicht 1 GB und mehr anzuzeigen, sondern nur die letzten paar. Wird sowieso bei 8 Millionen Einträgen keiner nach oben durchscrollen 😉.

Also, langer Rede kurzer Sinn:

  • jede neue Zeile Text an eine Logdatei hängen (super simpel, hier erklärt)
  • und du brauchst einen Container, der x Elemente fasst und das älteste rausschiebt, wenn etwas neues kommt und die Kapazität erreicht ist (so hier, EDIT: zweite Antwort, für deinen Anwendungsfall.)

Dann muss dein StringBuilder nur noch maximal x Elemente zusammenkleben und der RichTextBox geben, dh. die Zeit pro Ereignis steigt nicht mehr an.

LaTino

"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)