Daten aus NMEA Datentelegramm extrahieren

Hallo zusammen!
Ich tüfftle immernoch an meinem Windgrafik-Projekt. Der Windsensor schickt jede Sekunde dieses Datentelegramm:

        $WIMWV,179,R,3.4,N,A*35

Die erste Zahlengruppe gibt die Windrichtung an, die 2. Zahlengruppe die Windstärke an. Die Zahlen nach dem * sind die Prüfsumme.
Da sich die Werte verändern schwankt die Länge der jeweiligen Zahlengruppe zwischen 1-3 Stellen.
Ich habe dafür folgenden Sketch gebaut:

/*
 * Testprogramm für die Extrahierung der Werte
 * für Windrichtung und Windstärke aus dem
 * Datentelegramm des Windmessers
 * 
 * Beispiel:
 * $WIMWV,179,R,3.4,N,A*35
 */


char Empf [25]  = "0";
int Richt;
float Wind;

void setup()
{
  Serial.begin(4800); // opens serial port, sets data rate to 4800 bps
}

void loop()
{
  while (Serial.available() == 0)
  {
    Serial.println("Warte auf Eingabe");
  }

  Serial.readBytes(Empf, 25);  //Datentelegramm einlesen

  int Zeich = 7;    //Springe zu Windrichtung
  Richt = atoi(Empf + Zeich);

  while (Empf[Zeich] != ',')    //Ermittle die Länge der Windstärke (1 bis 3 Stellen)
  {
    Zeich++;
  }
  
  Wind = atof(Empf + Zeich + 3); //Nicht gebrauchte Zeichen überspringen
  Serial.print("Windrichtung ");
  Serial.println(Richt);
  Serial.print("Windstärke ");
  Serial.println(Wind);
  Empf[0] = "0";    //Variable für Empfang wieder leeren
  delay(9000);     //Anzeige in Ruhe ablesen

}

Der funktioniert und packt mir die Daten in die entsprechenden Variablen für die Weiterverarbeitung.
Meine Fragen dazu:

  1. Ist es sinnvoll so vorzugehen oder gibt es eine elegantere Lösung?

  2. Wenn ich eine Funktion draus mache - gibt es eine Möglichkeit, dass eine Funktion 2 Werte zurückgibt? Dadurch könnte ich die globalen Variablen sparen.

  3. Kann man am Arduino über Rx die Daten empfangen und zur Kontrolle über die USB-Leitung am Seriellen Monitor ansehen, welche Daten da angekommen sind?
    Für Tipps wäre ich dankdar.

Schau Dir mal strtok an.

Gruß Tommy

  1. Sieh dir die Serial Input Basics an um zuverlässig Daten einzulesen:
    Serial Input Basics - updated
    Wenn du die Zeile hast, dann mit strtok zerteilen.

  2. du übergibst der Funktion zwei Werte mittels Referenz und änderst die Werte dann in der Funktion. Siehe call be reference

  3. Üblich wäre es, auf einem Uno z.B. SoftSerial für die einkommenden Daten zu verwenden und HW-Serial für die ganz normalen Debug-Ausgaben.

Nein, es ist nicht sinnvoll.
Du liest zwangsläufig schneller als das Datenpaket übertragen wird.
Damit verlässt Du die Schleife bevor alle Daten da sind.
Beim nächsten Umlauf sind Teile aus der vorherigen Übertragung noch im Puffer und Du hast keinen definierten Anfangszustand.

Trenne Dich von der Schleife.
Mit dem $ hast Du einen definierten Startpunkt.
Ab da lese alle Zeichen ein, bis Du das Zeilenendezeichen bekommst.
Dein Puffer könnte Dir evtl. zu klein werden. Diverse Sensoren geben die Direction z.B. mit einer Nachkommastelle aus. Bei zweistelligem Vorkommaeintrag für die Geschwindigkeit und 2 zusätzlichen Stellen für die Direction ist 25 ausgefüllt und Dir fehlt Platz für das \0-byte

Du könntest ein struct bauen und das struct befüllen, oder mehrere Einzelwerte via reference beschreiben.

Ja, das kann man machen, ist aber nicht sinnvoll.
Wie schon geschrieben, macht man das via SoftwareSerial.
Wenn Du dort den zweiten PIN sparen willst, kannst Du dort für den TX-Pin eine Zahl angeben, die größer ist als der letzte Pin auf dem verwendeten Board. (Ich nehm gerne 255 ;) )

Da bisher noch nix hier passiert ist, mal ein Nachtrag und eine Variantenauswahl, wie ich das machen würde...

// Forensketch NMEA Werte einlesen und ausgeben
// https://forum.arduino.cc/t/daten-aus-nmea-datentelegramm-extrahieren/1093586/
// basiert auf https://forum.arduino.cc/t/daten-aus-nmea-datentelegramm-extrahieren/1093586/1
// UNGETESTET - Kompiliert Fehler- und Warnungsfrei
/*
   Testprogramm für die Extrahierung der Werte
   für Windrichtung und Windstärke aus dem
   Datentelegramm des Windmessers

   Beispiel:
   $WIMWV,179,R,3.4,N,A*35
*/
#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 255); // RX, TX

#define DEBUG   // Wenn aktiv, wird über die USB-Serielle gelesen

#ifdef DEBUG
  #define INSERIAL Serial
#else
  #define INSERIAL mySerial
#endif

struct myStruct
{
  // char *nmeaHeader;  // Example: WIMWV
  uint16_t drctn = 0;   // Eigentlich ein Floatwert...!!!
  // char speedType;    // R oder T
  float spd = 0;        // Geschwindigkeit als unbestimmter Zahlenwert
  // char type;         // K: Km/h M: m/s N: Knoten S: Meilen/h
  // char valide;       // A: Daten i.O. V: nicht i.O.
  // byte crc;
  bool newData = false; // Um festzustellen, ob neue Daten da sind...
};

myStruct wimwv;

void setup()
{
  Serial.begin(115200);
  mySerial.begin(4800);
}
//
void loop()
{
  einlesen();
  // Variante 1:
  ausgeben(1000);                                     // übergibt das Intervall für die Ausgabe
  // Variante 2:
  // ausgeben();                                      // Auswertung in der Funktion
}
//
void einlesen()
{
  const byte bufLen = 30;
  static char buf [bufLen]  = {'\0'};
  static byte idx = 0;
  if (INSERIAL.available())
  {
    char myChar = INSERIAL.read();
    if (isControl(myChar))                            // Steuerzeichen?
    {
      if (idx != 0)                                   // Buffer gefüllt?
      {
        //                                               Example: $WIMWV,179,R,3.4,N,A*35
#ifdef DEBUG
        Serial.print(F("Buffer: "));
        Serial.println(buf);
#endif
        if (!strncmp(buf, "WIMWV", 5))                //
        {
          strtok(buf, ",");                           // Example: WIMWV
          wimwv.drctn = atoi(strtok(NULL, ","));      // Example: 179
          strtok(NULL, ",");                          // Example: R
          wimwv.spd = atof(strtok(NULL, ","));        // Example: 3.4
          // strtok(NULL, ",");                       // Example: N
          // strtok(NULL, ",");                       // Example: A
          // strtok(NULL, "*");                       // Example: >>CRC<<
          wimwv.newData = true;                       // ...
        }
      }
    }
    else if (myChar == '$' ||                         // Start der Sequenz ODER
             idx >= bufLen)                           // nix gefunden
    {
      memset(buf, '\0', bufLen);
      idx = 0;
    }
    else
    {
      buf[idx] = myChar;                              // buffer befüllen
      idx++;
    }
  }
}

void ausgabe()
{
  Serial.print(F("Direction: "));
  Serial.println(wimwv.drctn );
  Serial.print(F("Speed: "));
  Serial.println(wimwv.spd);
}

//
void ausgeben(const uint32_t &intervall)
{
  static uint32_t lastmillis = 0;
  if (millis() - intervall > lastmillis)
  {
    ausgabe();
    lastmillis = millis();
  }
}

void ausgeben()
{
  if (wimwv.newData)
  {
    ausgabe();
    wimwv.newData = false;
  }
}

Erstmal vielen Dank an alle!
Leider hatte ich gestern wenig Zeit mir Eure Vorschläge anzusehen.
Besonderen Dank an my_xy_projekt - Du hast Dir ja richtig Mühe gemacht. Ich werde mir Deinen Sketch in den nächsten Tagen in Ruhe ansehen und hoffentlich auch verstehen.
Das Problem, dass die Daten zu langsam reinkommen hatte ich bisher nicht im Blick, da ich sie immer von Hand über den Seriellen Monitor eingegeben hatte. Heute habe ich mal den "Livebetrieb" probiert und gemerkt, dass da wirklich ein Problem ist.

Mein Windsensor gibt die Richtung immer nur ohne Nachkommastellen aus. Daher bleibt es bei maximal drei Stellen. Die Geschwindigkeit wird auch nicht über zwei Vorkommastellen gehen. Falls doch wird der Windgeber nur kurz Daten liefern. :wink:

Mir ist gerade noch ein anderer Gedanke gekommen:
Ich könnte die Winddaten eventuell auch für die Steuerung des Pinnenpiloten brauchen. Der will aber NMEA2000. Gefunden habe ich dazu, dass da die Datentelegramme immer mit einer Kennnummer beginnen. Aber wie geht es dann weiter? Trennzeichen? Prüfsumme? Baudrate?
Kennt sich da jemand aus?

Nicht wirklich. Ich wollte nur mal was ganz anderes machen und abgelenkt sein.

Jupp; Ich habe hellseherische Fähigkeiten :slight_smile:

Schau nochmal in die Beschreibung des Sensors.
Einige geben bei unplausiblen Werten eine 999 oder auch 999.9 auf allen Werten raus.
Den Buffer mit 3 Bytes zu erweiteren ist wohl einfacher als Gründe für fehlerhafte Werte zu finden.
Ich habe irgendwo ein PDF zu den NEMA-Strukturen.
WEnn ich es finde, melde ich mcih nochmal.
Bis dahin: Testen und Bescheid geben, wenn es nicht funktioniert. Ich mache das blind ohne Hardware und kontrolliere nur obs kompiliert.

Hallo my_xy_projekt,

Deinen Sektch habe ich mir angesehen, es bleiben aber einige Fragen. Vielleicht findest Du ja Zeit, sie zu beantworten.

struct myStruct

Du erzeugst für die Daten ein struct - warum? Ging es nur darum, mir den Umgang damit am Praxisbeispiel zu zeigen oder bringt es wirklich einen Vorteil?
Da es "global" erzeugt wird bringt es doch keine Einsparung von globalen Variablen.

const byte bufLen = 30;

Wenn ich das richtig verstehe kann die Variable "bufLen" durch diese Definition im Wert nicht verändert werden. OK, der Sketch wird dadurch vielleicht lesbarer. Aber ginge dafür nicht auch "#define bufLen 30"?

Serial.print(F("Buffer: "));

Soweit ich gelesen habe, sorgt das "F" dafür, dass der Text "Buffer: " nicht im SRAM sondern im PROGMEM landet - aber bringt das wirklich so große Einsparungen?

Bei Dir stehen die Definitionen for die Funktionen hinter setup und loop, Bisher dachte ich, dass die Funktionen immer davor stehen müssen, damit sie schon existieren, bevor loop beginnt.

Ich hab nochmal einen eigenen Sketch für den Empfang der Daten gebastelt. Der Windmesser schickt wirklich immer nur das gleiche Datentelegramm ohne "Unplausible Werte".

Als Kontrollausgabe nutze ich das TFT-Display. So sieht der aus:

/*
   Testprogramm für die Extrahierung der Werte
   für Windrichtung und Windstärke aus dem
   Datentelegramm des Windmessers

   Beispiel:
   $WIMWV,179,R,3.4,N,A*35

   Die Serielle Schnittstelle wird an Windempfänger angeschlossen
   Ausgabe auf dem TFT-Display
*/
#include <Adafruit_TFTLCD.h>
#include <Adafruit_GFX.h>

#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4


#define BLACK   0x0000
#define BLAU    0x001F
#define ROT     0xF800
#define GRUEN   0x07E0
#define CYAN    0x07FF
#define MAGENTA 0xF81F
#define GELB    0xFFE0
#define WEISS   0xFFFF

Adafruit_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET); //Pins für Display festlegen und Namen "tft"



char Empf [25]  = "0";
int Richt;
float Wind;
char *name = NULL;
int Lauf = 0;

void setup()
{
  Serial.begin(4800); // opens serial port, sets data rate to 4800 bps

  tft.reset();
  tft.begin(0x9341); //Diese Nummer ist für mein Display richtig!!!!
  tft.setRotation(2);
  tft.fillScreen(BLACK);
  tft.setCursor(0, 0);
}

void loop()
{
  tft.setTextColor(WEISS);
  tft.setTextSize(1);

  while (Serial.available() == 0); //Sind überhaupt Zeichen da?

  if (Serial.find('$') == true)  //Startzeichen des Datensatz
  {
    Serial.readBytesUntil('\n', Empf, 25); //Datentelegramm einlesen bis "Neue Zeile"
  }

  tft.println(Empf);   // Nur zur Kontrolle

  name = strtok(Empf, ",");  //Mir ist nicht ganz klar, ob man das einsparen kann - die Kennzeichen WIMWV
  //sind immer gleich

  Richt = atoi(strtok(NULL, "V,"));

  Wind = atof(strtok(NULL, ",R,"));

  tft.print("Windrichtung ");
  tft.println(Richt);
  tft.print("Windst""\x84""rke ");   //mit echtem "ä"
  tft.println(Wind);
  memset(Empf, 0, 25);    //Variable für Empfang wieder leeren

  Lauf++;
  if (Lauf == 14)  //Soll nur verhindern, dass das Display überläuft
  {
    tft.fillScreen(BLACK);
    tft.setCursor(0, 0);
    Lauf = 0;
  }
}

Mir ist klar das "Serial.readBytesUntil" einiges an Zeit braucht, es erscheint mir aber so einfacher zu programmieren.

Bei meinem Projekt ist die Zeit auch nicht so entscheidend. Die Daten kommen im Sekundentakt herein. Der Durchschnittswert aus 270 Sekunden soll alle 4,5 Minuten zu einem Bildpunkt auf dem Dispaly werden. Die 320 möglichen Punkte bilden dann eine Kurve des Verlaufs über einen Tag. Wenn da mal ein Datentelegramm nicht gelesen wird, ist das nicht so schlimm. Text soll nicht ausgegeben werden.

Oder mache ich schon wieder einen Denkfehler?

Noch eine Zusatzfrage zu Deinem Sketch:

void ausgeben(const uint32_t &intervall)
{
  static uint32_t lastmillis = 0;
  if (millis() - intervall > lastmillis)
  {
    ausgabe();
    lastmillis = millis();
  }
}

Täte es für die Verzögerung nicht auch ein "delay(1000);" ?

Da hast du recht. Normalerweise gehört eine Funktionsdeklaration an den Anfang, falls die Definition erst später kommt. Das erledigt die Arduino-IDE für dich, die aus allen .ino Dateien eine gemeinsame .cpp erzeugt, und auch #include <Arduino.h> davor einfügt.
Deine anderen Fragen fallen in die Kategorie "Welche Verbesserungen bietet C++ gegenüber C, und warum sind sie besser?"

Mit delay hängt ausgeben() für eine Sekunde, so ist es sofort fertig, ohne ausgabe() aufzurufen. Wenn in der Sekunde sonst nichts passiert, täte es auch ein delay, das stimmt, aber das kann die Funktion ausgeben() nicht wissen.

Moins,
letzte Frage zuerst:

Nein.
In der Zeit passiert nix anderes.
Also auch keine Datenaufnahme.
Nu magst meinen, das is nich schlimm.
Aber irgendwann kommt noch was dazu, was in der Zeit ausgeführt werden muss. Dann stehst vor dem Problem, wie Du die delays alle auflöst...
Also gleich richtig machen.

Ja, ich hab ein struct gefertigt. Weils zusammengehört.
Man kann damit jetzt rumspielen. Muss aber nicht.

Für deine Drei Worte:
Mit F-Makro: Globale Variablen verwenden 371 Bytes
Ohne F-Makro: Globale Variablen verwenden 399 Bytes

Du willst zwei Werte - drctn und spd - nicht global anlegen, weil Du Speicher sparen willst.
Mach mal:

Serial.prinltn(sizeof(uint16_t));
Serial.println(sizeof(float));

Vergleiche mit der Differenz.
Ups. ;)

Achso.. Zum Thema const

Nein.
Du kannst den Wert bufLen auch bei der Verwendung von define nicht zur Laufzeit verändern.
Du entziehst aber dem Compiler die Kontrolle.