none
C# GPS-Protokoll in separaten Thread empfangen RRS feed

  • Frage

  • Hallo zusammen,

    für ein aktuelles Projekt suche ich nach einer eleganten Möglichkeit GPS-Daten von einer GPS-Maus zu empfangen. Bisher hatte ich dafür einen Timer verwendet, der einmal pro Sekunde die serielle Schnittstelle ausliest und die Daten verarbeitet um so das GPS-Protokoll zu zerlegen. 
    Das funktioniert auch wunderbar, weswegen ich mich jetzt an den Schritt wagen möchte das Empfangen der GPS-Daten in einem separaten Thread zu verschieben.

    Ich habe auch schon ein Beispiel gefunden:

    http://www.codeproject.com/Questions/228124/serial-port-using-threading

    Allerdings verstehe ich dieses Beispiel noch nicht 100%ig. In dem Beispiel wird das komplette Lesen der seriellen Schnittstelle in einer neuen Klasse erledigt. Im Konstruktor wird ein neuer Thread erzeugt, der dann einmalig die serielle Schnittstelle initialisiert und dann so lange ausließt bis die While-Schleife beendet wird.
    Der Teil ist kein Problem. Nur was mir noch nicht so ganz einleuchtet ist der Teil:

       if(DataReceived != null) DataReceived(this, new DataEventArgs(line));
    
     
    public class DataEventArgs : EventArgs {
     public string Data { get; private set; }
     
     public DataEventArgs(string data) { Data = data; }
    }

    Wenn ich mir das richtig zusammen reime, wird ja jedes mal ein neues Event definiert, wenn die serielle Schnittstelle Daten empfängt und diese Daten werden dann in einer separaten Klasse gespeichert und es wird zudem ein Event ausgelöst (dieses Event muss später im Code noch definiert werden).

    Soviel zu der Theorie...
    Jetzt habe ich das mal in meinem Code getestet. Meine Klasse sieht wie folgt aus

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.IO.Ports;
    
    namespace GUI
    {
        public class GPSReader
        {
            private Thread GPS_Thread;
    
            public event EventHandler<DataEventArgs> Event_DataReceived;
    
            private bool IsClose = false;
    
            public GPSReader()
            {
                GPS_Thread = new Thread(GPSData);
            }
    
            public void Start() 
            { 
                GPS_Thread.Start(); 
            }
    
            public void Stop() 
            { 
            }
    
            public void Close() 
            { 
                IsClose = true; 
            }
    
            private void GPSData()
            {
                SerialPort SerialPort = new SerialPort();
                SerialPort.BaudRate = DataContainer.Baudrate;
                SerialPort.Parity = Parity.None;
                SerialPort.StopBits = StopBits.One;
                SerialPort.DataBits = 8;
                SerialPort.Handshake = Handshake.None;
                SerialPort.Open();
     
                while(!IsClose)
                {
                    string ReadLine = SerialPort.ReadLine();
                    if (Event_DataReceived != null)
                    {
                        Event_DataReceived(this, new DataEventArgs(ReadLine));
                    }
                }
            }
        }
    }
     
    public class DataEventArgs : EventArgs
    {
        public DataEventArgs(string EventData)
        {
            Data = EventData;
        }
    
        public string Data { get; private set; }
    }

    In meinem MainWindow habe ich nun einen "Connect"-Button, der alles initialisiert:

     private void MenuItem_Click_Connect_GPS(object sender, RoutedEventArgs e)
            {
                try
                {
                    if(DataContainer.Baudrate <= 0)
                    {
                        AddStatus("Keine Baudrate ausgewählt!");
                        return;
                    }
                    else if(DataContainer.COM_Port == string.Empty)
                    {
                        AddStatus("Kein COM-Port ausgewählt!");
                        return;
                    }
                    else
                    {
                        ComPort.PortName = DataContainer.COM_Port;
                        ComPort.BaudRate = (int)DataContainer.Baudrate;
                        ComPort.ReadTimeout = 50;
                        ComPort.WriteTimeout = 50;
    
                        ComPort.Open();
    
                        if(ComPort.IsOpen)
                        {
                            AddStatus("Verbinde mit COM-Port...");
                            if(Reader == null)
                            {
                                AddStatus("GPS-Thread gestartet");
                                Reader = new GPSReader();
                                Reader.Event_DataReceived += GetGPS;
                                Reader.Start();
                            }
                        }
                    }
                }
                catch (Exception Error)
                {
                    if(Error is UnauthorizedAccessException)
                    {
                        AddStatus("COM-Port bereits geöffnet!");
                    }
                    else
                    {
                        AddStatus("Schwerer Programmfehler! Bitte im Log nachschauen!");
                        WriteErrorLog(Convert.ToString(Error));
                    }
                }

    Als Event habe ich mal test weise dieses hier definiert:

    public void GetGPS(object s, EventArgs e)
            {
                AddStatus("GPS-Event");
            }

    Theoretisch sollte ja nun bei jeder GPS-Nachricht ein Text "GPS-Event" in meiner Textbox erscheinen.

    Das passiert aber leider nicht. Dafür bekomme ich die ganze Zeit solche Ausgaben:

    Der Thread 0x2b24 hat mit Code 259 (0x103) geendet.
    Der Thread 0x214c hat mit Code 259 (0x103) geendet.
    Der Thread 0x1cfc hat mit Code 259 (0x103) geendet.
    Der Thread 0x2468 hat mit Code 259 (0x103) geendet.

    Wo steckt der Fehler in dem Code? Ich hoffe es kann mir einer helfen :)

    Ich sage schon einmal Danke :)
    Gruß
    Daniel



    Montag, 27. Juli 2015 11:24

Antworten

Alle Antworten

  • Hallo,

    ich vermute, dass der Thread nach einer Exception beendet wird.

    Sind SerialPort und ComPort unterschiedliche Objekte? Wenn ja, dann sollst Du eine Exception InvalidOperationException kriegen, weil Com-Port schon geöffnet ist. Warum wird Com Port zwei mal geöffnet?  

    Die Methode Open muss immer in try catch Blöcke einpackt werden.

    Beim SerialPort Objekt wird kein Port (PortName) gesetzt.    

    Grüße


    Montag, 27. Juli 2015 11:45
  • Hallo Iso,

    danke für das Feedback! Da habe ich den Wald vor lauter Bäumen nicht gesehen (der eigentliche Code im MainWindow ist schon etwas länger).
    Der COM-Port sollte natürlich nicht zwei mal geöffnet werden. Ich habe nun erst einmal meine Connect-Routine eingekürzt:

    private void MenuItem_Click_Connect_GPS(object sender, RoutedEventArgs e)
    	{
                if (Reader == null)
                {
                    AddStatus("GPS-Thread gestartet");
                    Reader = new GPSReader();
                    Reader.Event_DataReceived += GetGPS;
                    Reader.Start();
                }
    
                Update.Interval = 100;
                Update.Tick += new EventHandler(Update_GPS);
    
                Update.Start();
                AddStatus("Starte Update-Timer");
            }

    Und meine Klasse um den Namen und einen try-catch Block erweitert.
    Jetzt bekomme ich kurz nach dem Connecten eine "InvalidOperationException", da ich über "AddStatus" in eine Textbox schreiben will, die einem anderen Thread angehört.

    Das ist schon mal schön, weil dann tut sich wenigstens was :)
    Die Lösung des Problems ist jetzt wahrscheinlich so etwas hier:

            public void AppendTextBox(string Text)
            {
                if (!Dispatcher.CheckAccess())
                {
                    this.Dispatcher.Invoke(new Action<string>(AppendTextBox), new object[] { value });
                    return;
                }
                TextBox_Status.Text += Text;
            }

    Was ich jetzt nur noch nicht verstehe....
    Wie komme ich an den Text aus dem Eventhandler der neu erstellten Klasse dran?
    Irgendwie funktioniert das nicht wenn ich schreibe

    public void GetGPS(object s, EventArgs e)
    {
    	TextBox_GPS_Log.AppendText = e.Data;
    }
    Da ist angeblich kein Verweis vorhanden.



    • Bearbeitet Kampino89 Montag, 27. Juli 2015 12:00
    Montag, 27. Juli 2015 11:55
  • Hallo Daniel,

    die Exception „InvalidOperationException“ bei der Methode Open zeigt an, dass der Com Port schon geöffnet ist. (Wahrschenlich läuft noch eine zweite Instanz des Programms.)

    Grüße
    Montag, 27. Juli 2015 12:07
  • Hallo,

    eine zweite Instanz läuft nicht. Ich habe mal einen Screenshot der Meldung gemacht:

    Montag, 27. Juli 2015 12:23
  • Noch zwei Bemerkungen:

    1)Die Controls (z.B. TextBox_GPS_Log) dürfen nicht in einem Thread geändert werden, sondern in UI Thread.

    public void Aktualisieren(String text)
            {
                if (textBox_GPS_Log.InvokeRequired == false)
                {
                    textBox_GPS_Log.AppendText(text);
                }
                else
                {
                    textBox_GPS_Log.BeginInvoke(new DelTextAppend(Aktualisieren), new Object[] { text });
                }
            }

    2) Event abonnieren :

    Reader.Event_DataReceived += new DelegateVonEvent(GetGPS);




    • Bearbeitet Iso7 Montag, 27. Juli 2015 12:35
    Montag, 27. Juli 2015 12:24
  • Hallo,

    mmh warum zeigt mir Visual Studio an, dass die Controls keine Methode "InvokeRequired" besitzen? Brauche ich da, abgesehen von System.Windows.Forms noch eine andere Using-Direktive?

    Montag, 27. Juli 2015 13:32
  • Verwendest Du WinForm oder WPF? Oben ist ein Beispiel für WinForms. Für WPF gibt es Dispatcher.
    Montag, 27. Juli 2015 13:42
  • Hallo,

    ich verwende WPF. Ich probiere es morgen mal mit Dispatcher (hatte eben schon was in der Richtung im Internet gefunden).
    Danke schon mal für die Hilfe :)

    Montag, 27. Juli 2015 13:45
  • Hallo,

    ich habe gleich noch einmal eine Frage.
    Ich habe meine Klasse für den GPS-Reader nun angepasst:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.IO.Ports;
    
    namespace GUI
    {
        public class GPSReader
        {
            private Thread GPS_Thread;
            private Log_Writer Log = new Log_Writer();
    
            public event EventHandler<DataEventArgs> Event_DataReceived;
    
            private bool IsClose = false;
    
            public GPSReader()
            {
                GPS_Thread = new Thread(GPSData);
            }
    
            public void Start() 
            { 
                GPS_Thread.Start(); 
            }
    
            public void Stop() 
            { 
            }
    
            public void Close() 
            { 
                IsClose = true; 
            }
    
            private void GPSData()
            {
                SerialPort SerialPort = new SerialPort();
                SerialPort.PortName = DataContainer.COM_Port;
                SerialPort.BaudRate = DataContainer.Baudrate;
                SerialPort.Parity = Parity.None;
                SerialPort.StopBits = StopBits.One;
                SerialPort.DataBits = 8;
                SerialPort.Handshake = Handshake.None;
    
                try
                {
                    SerialPort.Open();
                }
                catch(Exception Error)
                {
                    Log.WriteErrorLog(Convert.ToString(Error));
                }
     
                while(!IsClose)
                {
                    if(SerialPort.IsOpen)
                    {
                        string ReadLine = SerialPort.ReadLine();
                        if (Event_DataReceived != null)
                        {
                            Event_DataReceived(this, new DataEventArgs(ReadLine));
                        }
                    }
                }
            }
        }
    
        public class DataEventArgs : EventArgs
        {
            public string GPS_Data { get; private set; }
    
            public DataEventArgs(string EventData)
            {
                GPS_Data = EventData;
            }
        }
    }

    Das Abarbeiten des GPS-Events in dem MainWindow klappt nun auch. Nun möchte ich aber auf die Daten aus dem Event, sprich die eingelesene Zeile, zugreifen.
    Deswegen habe ich in meinem GPS-Event diese Zeile

    TextBox_Status.Dispatcher.Invoke(new Action(() => { TextBox_Status.AppendText(Convert.ToString(e.GPS_Data) + "\n"); }));

    Allerdings zeigt mir Visual Studio an, dass es kein "GPS_Data" in den EventArgs gibt.

    Warum?

    Dienstag, 28. Juli 2015 06:12
  • Hallo Daniel,

    kannst Du die Methode komplett  zeigen, wo Invoke aufgerufen wird?

    Es gibt schon die Klasse DataEventArgs in Microsoft Bibliothek. Bist Du sicher, das Du die Objekten von der richtigen Klasse erzeugst?

    Grüße

    Dienstag, 28. Juli 2015 07:43
  • Hallo,

    die komplette Methode schaut so aus:

            void GetGPS(object s, EventArgs e)
            {
    		TextBox_Status.Dispatcher.Invoke(new Action(() => { TextBox_Status.AppendText(Convert.ToString(e.GPS_Data) + "\n"); }));
    	}

    Ansonsten tausche ich einfach mal den Namen von der Klasse aus....vielleicht löst das ja schon das Problem.

    Edit:

    Dein Hinweis war super!
    Habe die Klasse nun geändert:

        public class GPSDataEvents : EventArgs
        {
            public string GPS_Data { get; private set; }
    
            public GPSDataEvents(string EventData)
            {
                GPS_Data = EventData;
            }
        }

    Und entsprechend das Event ebenfalls:

    void GetGPS(object s, GPSDataEvents e)
    {
       TextBox_Status.Dispatcher.Invoke(new Action(() => { TextBox_Status.AppendText(Time + " - " + e.GPS_Data + "\n"); }));
    }
    Nun klappt es :)

    • Bearbeitet Kampino89 Dienstag, 28. Juli 2015 07:51
    Dienstag, 28. Juli 2015 07:48
  • Das nächste Verständnisproblem was ich habe ist wie ich den Thread für die GPS-Maus ordnungsgemäß beende.
    Soweit ich gelesen habe brauche ich die Join-Methode dafür.
    Ich habe es nun so gelöst

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.IO.Ports;
    
    namespace GUI
    {
        public class GPSReader
        {
            private Thread GPS_Thread;
            private SerialPort SerialPort;
            private Log_Writer Log;
    
            public event EventHandler<GPSDataEvents> Event_DataReceived;
    
            private bool Running = false;
            private bool IsClose = false;
    
            public GPSReader()
            {
                GPS_Thread = new Thread(GPSData);
            }
    
            public void Start() 
            { 
                GPS_Thread.Start();
                Running = true;
            }
    
            public void Stop() 
            {
                Running = false;
                GPS_Thread.Join();
            }
    
            public void Close() 
            { 
                IsClose = true; 
            }
    
            private void GPSData()
            {
                while (Running)
                {
                    SerialPort = new SerialPort();
                    SerialPort.PortName = DataContainer.COM_Port;
                    SerialPort.BaudRate = DataContainer.Baudrate;
                    SerialPort.DataBits = DataContainer.Databits;
                    SerialPort.Parity = DataContainer.Parität;
                    SerialPort.StopBits = DataContainer.Stop;
                    SerialPort.Handshake = DataContainer.HandShake;
    
                    try
                    {
                        SerialPort.Open();
                    }
                    catch (Exception Error)
                    {
                        Log = new Log_Writer();
                        Log.WriteErrorLog(Convert.ToString(Error));
                        Log = null;
                    }
    
                    while (!IsClose)
                    {
                        if (SerialPort.IsOpen)
                        {
                            string ReadLine = SerialPort.ReadLine();
                            if (Event_DataReceived != null)
                            {
                                Event_DataReceived(this, new GPSDataEvents(ReadLine));
                            }
                        }
                    }
                }
            }
        }
    
        public class GPSDataEvents : EventArgs
        {
            public string GPS_Data { get; private set; }
    
            public GPSDataEvents(string EventData)
            {
                GPS_Data = EventData;
            }
        }
    }

    Öffnen kann ich den Thread ganz normal. Nur wenn ich dann die Stop() Methode aufrufe bleibt mein Programm hängen.
    Ich habe mich versucht hieran zu halten:

    https://msdn.microsoft.com/de-de/library/7a2f3ay4%28v=VS.90%29.aspx?f=255&MSPPError=-2147217396

    Da wird ja auch mit einem Flag gearbeitet und dieses Flag wird vor der Join()-Methode auf "true" gesetzt, weswegen die Schleife im Thread nicht weiter abgearbeitet wird.
    Nur wie gesagt....bei mir hängt sich das Programm dann komplett auf, sprich der Thread wird nicht beendet.

    Dienstag, 28. Juli 2015 08:04
  • Hallo Daniel,

    dein Thread läuft immer in while Schleife und wird nicht beendet.

    Die Variable IsClose muss auch gesetzt werden:

    IsClose = true;
    Grüße

    Dienstag, 28. Juli 2015 08:15
  • Hallo,

    es funktioniert nun alles einwandfrei :) 
    Hatte noch ein paar Probleme mit dem erneuten Öffnen der Schnittstelle, aber das ist nun auch behoben.
    Dank dir für deine Hilfe. :)

    Dienstag, 28. Juli 2015 08:50