Serielle Befehls Auslesung von Bluetooth Low Energy

Guten Abend Community,
ich bin gerade an einem Projekt, welches Benachrichtigungen von meinem Smartphone an den Arduino über BLE sendet. Die Übertragung funktioniert und wird so übertragen:

/noti;1;com.whatsapp;ABCD;Test

Schließe ich das BLE modul, welches über Serielle Anschlüsse verfügt, direkt an den UART adapter, empfange ich Daten.
Ich möchte nun dass der Arduino über Software Serial diese Zeile ausließt(am Ende ist ein \n) und den String bei dem ';' trennt. Hierzu habe ich folgendes, hoffentlich RAM freundliches geschrieben:

char bleBuffer[] = "";
char* split;

void loop() {
while(bleSerial.available()>0){
    
    String input = bleSerial.readStringUntil('\n');
    input.toCharArray(bleBuffer, sizeof(input));
    Serial.println(bleBuffer);
    split = strtok(bleBuffer, ";");
  while (split != NULL){
   Serial.println(split);
   split = strtok(NULL, ";");
  }
}
}

Funktionieren tut es jedoch kaum, gar nicht wenn ich oft etwas schicke. Ich möchte dann die einzelnen Daten zwischen dem Semikolon in einem multidimensionalen Array speichern.
Dazu hätte ich allgemein noch eine zwischen Frage. Kennt jemand eine Website die RAM optimiertes Arduino scripten erklärt? Weil die Arduino-Strings scheinen ja nicht sehr freundlich, da ich den gleichen Code mal mit Strings geschrieben hat und nach einer Zeit nicht mehr funktionierte.

Ich mein Code falsch? Oder hat jmd einen anderen Vorschlag wie ich Daten zwischen bestimmten Zeichen auslesen kann?

Folgende Schwierigkeit ist noch vorhanden: das BLE modul kann maximal 20Bytes übertragen, sodass diese im Abstand von 200ms ankommen. Deswegen habe ich das Serial.readStringUntil... gewählt. Richtig so?

LG Tim

Ich möchte dann die einzelnen Daten zwischen dem Semikolon in einem multidimensionalen Array speichern.

Zwei Fragen:

1.) musst du die Teil-Strings wirklich permanent Speichern, oder reicht es wenn du nur die Zeiger auf die Tokens kurz speicherst, die Daten verwendest und dann wieder verwirfst? Also z.B. um sie gleich auszugeben oder an eine andere Funktion übergeben

2.) Muss das allgemein sein oder geht es nur darum genau so einen String mit 5 Tokens zu splitten?

Folgende Schwierigkeit ist noch vorhanden: das BLE modul kann maximal 20Bytes übertragen, sodass diese im Abstand von 200ms ankommen. Deswegen habe ich das Serial.readStringUntil... gewählt. Richtig so?

Nein. Das liest man besser nicht-blockierend direkt in ein char Array:

Dann hast du auch nicht den unnötigen Umweg über die String Klasse.

zu 1.) Ich möchte die Daten eigentlich speichern, es kommen halt manchmal mehrere Benachrichtigungen, deswegen das Multidimensionale Array. Was bringt das verwerfen der Daten dann? Die Benachrichtigungen werden dann auf einem Display angezeigt

zu 2.) bei /noti soll der String gesplittet werden, bei /time 17:32:15 wird die Uhrzeit aktualisiert (muss ich noch programmieren)

d.h. ich speichere die ankommmenden Daten in einem charArray Puffer und stelle eine boolean auf true wenn ein \n ankommt, sprich der String fertig ist?

Man kann es so oder so machen. Wenn du sie speichern willst, ok. Ist halt etwas aufwendiger. Und man braucht es einfach nicht oft. Wenn du die Daten gleich auf ein Display schreibst, braucht man die Teil-Strings eigentlich nicht ins RAM zu kopieren. Token splitten → aufs Display → nächstes Token

Hier mal mit Parser für beides:

const int STRING_BUFFER_SIZE = 40;
char stringBuffer[STRING_BUFFER_SIZE];

const int MAX_TOKENS = 4;
const int MAX_TOKEN_LENGTH = 15;
char message[MAX_TOKENS][MAX_TOKEN_LENGTH];

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  if (readLine(Serial) == true)
  {
    Serial.print(F("Komplett: ")); Serial.println(stringBuffer);
    parseSerial();
  }
}

void parseSerial()
{
  if (strncmp_P(stringBuffer, PSTR("/noti"), 5) == 0)
  {
    parseText(stringBuffer + 6);
  }
  else  if (strncmp_P(stringBuffer, PSTR("/time"), 5) == 0)
  {
    parseTime(stringBuffer + 5);
  }
}

void parseText(char* buffer)
{
  char* token = strtok(buffer, ",;");
  int count = 0;

  while (token != NULL)
  {
    if (count < MAX_TOKENS)
      strlcpy(message[count], token, MAX_TOKEN_LENGTH);

    token = strtok(NULL, ",;");
    count++;
  }

  for (int i = 0; i < MAX_TOKENS; i++)
    Serial.println(message[i]);
  Serial.println(F("------"));
}

void parseTime(char* buffer)
{
  while (!isDigit(*buffer))   //nicht-Ziffern überspringen
    buffer++;

  int hours = atoi(strtok(buffer, ":"));
  int minutes = atoi(strtok(NULL, ":"));
  int seconds = atoi(strtok(NULL, ":"));

  Serial.println(hours);
  Serial.println(minutes);
  Serial.println(seconds);
  Serial.println(F("------"));
}

bool readLine(Stream& stream)
{
  static int index;

  while (stream.available())
  {
    char c = stream.read();

    if (c >= 32 && index < STRING_BUFFER_SIZE - 1)
    {
      stringBuffer[index++] = c;
    }
    else if (c == '\n' && index > 0)
    {
      stringBuffer[index] = '\0';
      index = 0;
      return true;
    }
  }
  return false;
}

Ich habe es mal so implementiert dass zwischen /time und der Zeit nicht unbedingt ein Zeichen kommen muss

Weiß nicht ob das genau ist was du willst. Auch nur grob getestet. Aber solche Parser habe schon öfters geschrieben

Vielen Dank,

Das PSTR bedeutet dass der String "/time" nicht in dem Ram bleibt?
Das strncmp_P bzw das _P bedeutet dass Strings die im Flash stehen auch abgefragt werden? Wann stehen denn Strings im Flash außer durch PROGMEM?
Was bedeutet strlcpy, also das 'l'? Was gibt es für einen Unterschied zwischen: strcpy(messages[count], token, Maxlengt) und messages[count] = token+'\0'; (hierbei ist messages ein char array pointer)

MasterTim17:
Wann stehen denn Strings im Flash außer durch PROGMEM?

String stehen immer im Flash. Aber wenn man nicht PROGMEM verwendet landen sie auch im RAM

Was bedeutet strlcpy, also das 'l'?

strcpy() hat keinerlei Kontrolle der Länge. strncpy() kopiert nur n Zeichen, aber wenn der Puffer nicht reicht, ist der String nicht korrekt terminiert. Deshalb gibt es bei manchen Bibliotheken die Erweiterung strlcpy() die nur maximal n-1 Zeichen kopiert und dann terminiert:
http://www.nongnu.org/avr-libc/user-manual/group__avr__string.html#ga64bc119cf084d1ecfd95098994597f12

stringBuffer + 5 "verkürzt" den string um 5 zeichen von vorne? denn folgendes funktioniert nicht: char* buffer = stringBuffer+5; obwohl es ja eigentlich genau so im code von dir steht

stringBuffer + 5 "verkürzt" den string um 5 zeichen von vorne?

Ja, weil man ja den Befehl selbst nicht verarbeiten will. Also übergibt man eine Adresse weiter hinten im String. Es ist nur einmal +6 wegen dem Strichpunkt danach, während man bei /time auch direkt danach die Zeit schreiben kann. Also z.B. /time45

Wenn folgendes funktioniert nicht: char* buffer = stringBuffer+5;

Sollte eigentlich gehen. Aber wie gesagt bei /noti habe ich +6 gemacht weil es ja "/noti;..." ist und man den Strichpunkt überspringen muss

Mein Code Soweit:

char bleBuffer[100];
char* split;
boolean read = false;
char bleNoti[][3][50] = {};

void loop() {

while(bleSerial.available()>0){
    static int index;
    char c = bleSerial.read();
    Serial.print(c);
    if(c=='\n'){
      bleBuffer[index]='\0';
      read = true;
      index=0;
    }else{
      bleBuffer[index] = c;
      index++;
    }
}
if(read){
    split = strtok(bleBuffer, ";");
    if(strstr(split,"noti")){
      char* buff = bleBuffer+strlen(split); //<-- funktioniert nicht
      int nmb = atoi(strtok(buff, ";"));
      int i=0;
      while (split != NULL){ // delimiter is the semicolon
       strlcpy(bleNoti[nmb][i],split,55);
       Serial.println(bleNoti[nmb][i]);
       split = strtok(NULL, ";");
      }
    }
    read = false;
}
}

Wenn ich es ohne die funktionierende Zeile mache, kommen alle tokens (/noti, usw) an AUßER die zahl O.o
und alles funktioniert nur beim ersten Mal. Je öfter ich einen solchen String schicke, desto mehr vermischen sich die ergebnisse:

/noti;1;com.mastertim.notificationlistener;My Notification;Notification Listener Service Example
/noti
com.mastertim.notificationlistener
My Notification
Notification Listener Service Example
/noti;2;com.mastertim.notificationlistener;My Notification;Notification Listener Service Example
/noti
com.mastertiNotification Listener Service Examplen
Notification Listener Service Example

Wieso hast du das alles komplett umgeändert? Vor allem wenn du dich nicht sicher auskennst.
Die Einlese-Routine selbst hat doch gepasst! Deine ist nicht sicher.

Das ist falsch:

char bleNoti[][3][50] = {};

Das ist ein leeres Array!! Kein Wunder dass da Unsinn raus kommt wenn du auf Speicher schreibst der nicht existiert. In Visual C++ kompiliert das schon gar nicht

Eine gute Alternative hier ist vielleicht ein struct mit char Arrays (und wirklich Arrays und nicht nur Zeiger!). Und dann ein Array aus structs. Dann hast du keine 3-dimensionalen Arrays. Die Teil-Strings können unterschiedlich lang sein. Und du hast ansprechende Namen.
strtok() kann man dann ein paar mal per Hand aufrufen statt in einer Schleife.

Speicher-schonend programmieren ist das aber nicht mehr wenn deine ganzen Teil-Strings maximal 50 Zeichen lang sind! Und wenn du ein Array der Länge 50 hast, darfst du bei strlcpy() nicht 55 angeben. Sowas macht man mit sizeof(). Dann passt sich das automatisch an.

strstr() überprüft ob der Such-String irgendwo vorkommt. Das geht zwar hier auch, aber man verwendet strcmp() um den kompletten String zu vergleichen oder strncmp() für die ersten Zeichen.

Dynamisch lässt sich das leider nicht auf dem Arduino machen. Inwiefern sind struct besser: die erste Dimension ist die anzahl der benachrichtigungen, die zweite die 3 komponenten der benachrichtigung
Hättest du ein beispiel wie sich die Char arrays mit den struct hierbei in eine verbindung bringen lassen?

Dynamisch lässt sich das leider nicht auf dem Arduino machen.

Geht schon. Ist aber meistens keine gute Idee.

Du musst bei statischen Arrays immer die Größe angeben. Und dann bei jedem Schreibzugriff penibel überprüfen ob du noch in den Array Grenzen bist! Also z.B. Speicher für maximal 5 messages anlegen. Das könnte man auch recht einfach so machen dass immer die älteste message überschrieben wird. Wie bei einem FIFO Puffer.

die erste Dimension ist die anzahl der benachrichtigungen, die zweite die 3 komponenten der benachrichtigung

Die Tokens sind aber nicht gleich lang. Das erste ist nur eine Zahl! Dafür wird in einem Multi-dimensionalen Array aber trotzdem 50 Byte belegt! In einem struct kann man das auch gleich als byte oder unsigned int abspeichern.

struct message_t
{
   int var;
   char text1[20];
   char text[10];
};

message_t messages[2];

EDIT: Strichpunkt nach der struct Deklaration vergessen

Vielen Dank für den Verweis auf struct. Werde mich damit mal beschäftigen. Ist es so dass das einfache deklarieren eines struct keinen speicherplatz verbraucht?

EDIT: strtok(bleBuffer, ";") kann ich bleBuffer damit um 6 zeichen verschieben?: (char*)bleBuffer+6

MasterTim17:
Ist es so dass das einfache deklarieren eines struct keinen speicherplatz verbraucht?

Die Deklaration verbraucht keinen Speicher. Da gibt man nur bekannt wie die Variable aussieht. Die Definition schon. Also oben wenn man ein Array (oder auch nur ein einzelenes struct) anlegt.

Wie bei anderen Variablen und Objekten von Klassen auch. In C++ ist ein struct nur eine Klasse in der alle Member public sind

kann ich bleBuffer damit um 6 zeichen verschieben?: (char*)bleBuffer+6

Du musst eine Zeiger Variable anlegen:

char* ptr = bleBuffer + 6;

Was anderes passiert auch nicht wenn man bleBuffer + x an eine Funktion übergibt.

Danke

split = strtok(bleBuffer, ";");
    if(strstr(split,"noti")){
      char* buff = bleBuffer+strlen(split); //<-- funktioniert nicht
      int nmb = atoi(strtok(buff, ";"));
      bleNoti[nmb].var = nmb;
      bleNoti[nmb].text1 = strtok(NULL, ";");
      bleNoti[nmb].text = strtok(NULL, ";");

mit der Zeiger Variable klappt jedoch nicht, der inhalt ist null, oder muss ich mit & den wert abfragen? Werde mich wohl mit zeigern beschätigen müssen…

auch funktioniert die Struct zuweisung wie oben nicht, strtok ist ja ein zeiger

Du kannst C Strings nicht mit = kopieren! Das sind keine Objekte. Array Variablen in C/C++ sind nicht viel mehr als Zeiger auf das erste Element

Wenn du ein Array hast dann musst du den Text da per strlcpy() reinkopieren!

char* ptr = strtok(NULL, ";");
strlcpy(bleNoti[nmb].text, ptr, sizeof(bleNoti[nmb].text));

Wenn du das machst:

char array[] = "test";
char* ptr = array;

Dann geht das. Aber ptr ist nur ein Zeiger auf den String. Das wird nur der Zeiger kopiert. Nicht der Inhalt.

char* buff = bleBuffer+strlen(split); //<-- funktioniert nicht

Wieso machst du das überhaupt? Die Länge von split ist hier bekannt!

folgendes funktioniert nicht:

char* buff = bleBuffer+6;
split = strtok(buff, ";"));

--> split hat keinen wert

Dein Vorgehen ist generell etwas unkoordiniert. Du probierst einfach nur aus, aber scheinst kein Konzept zu haben. Wenn du nicht wirklich weißt was du machst, rate ich dir dich stärker an meinen Code zu halten. Da kannst du immer noch Sachen anpassen, aber es sicherer als das was du machst. Bei Arrays und Zeigern muss man sehr aufpassen was man macht.

Ich nehme mal deine Verschlechterungen wieder raus. Also mit Grenz-Überprüfungen und Aufteilung in klar getrennte Funktionen (wodurch es einfacher wartbar ist).
Auch mit FIFO Funktionalität, so dass immer die letzten Nachricht überschrieben wird.

Wenn man “print” eingibt werden alle Nachrichten angezeigt.

const int STRING_BUFFER_SIZE = 100;
const int NUM_OF_MESSAGES = 3;

struct message_t
{
  byte num;
  char text1[10];
  char text2[15];
};

char stringBuffer[STRING_BUFFER_SIZE];
message_t messages[NUM_OF_MESSAGES];
int messageIndex;

void setup()
{
  Serial.begin(9600);
}

void loop()
{
  if (readLine(Serial) == true)
  {
    Serial.print(F("Empfangen: ")); Serial.println(stringBuffer);
    parseSerial();
  }
}

void parseSerial()
{
  if (strncmp_P(stringBuffer, PSTR("/noti"), 5) == 0)
  {
    parseMessage();
  }
  else if (strncmp_P(stringBuffer, PSTR("/time"), 5) == 0)
  {
    parseTime();
  }
  else if (strcmp_P(stringBuffer, PSTR("print")) == 0)
  {
    printMessages();
  }
}

void parseMessage()
{
  char* buffer = stringBuffer + 6;

  char* ptr = strtok(buffer, ",;");
  messages[messageIndex].num = atoi(ptr);

  ptr = strtok(NULL, ",;");
  strlcpy(messages[messageIndex].text1, ptr, sizeof(messages[messageIndex].text1));

  ptr = strtok(NULL, ",;");
  strlcpy(messages[messageIndex].text2, ptr, sizeof(messages[messageIndex].text2));

  Serial.println(messages[messageIndex].num);
  Serial.println(messages[messageIndex].text1);
  Serial.println(messages[messageIndex].text2);
  Serial.println(F("------"));

  messageIndex = (messageIndex + 1) % NUM_OF_MESSAGES;    //FIFO Prinzip
}

void parseTime()
{
  char* buffer = stringBuffer + 5;

  while (!isDigit(*buffer))   //nicht-Ziffern überspringen
    buffer++;

  int hours = atoi(strtok(buffer, ":"));
  int minutes = atoi(strtok(NULL, ":"));
  int seconds = atoi(strtok(NULL, ":"));

  Serial.println(hours);
  Serial.println(minutes);
  Serial.println(seconds);
  Serial.println(F("------"));
}

void printMessages()
{
  for (int i = 0; i < NUM_OF_MESSAGES; i++)
  {
    Serial.print(F("Message ")); Serial.println(i);
    Serial.print(' '); Serial.println(messages[i].num);
    Serial.print(' '); Serial.println(messages[i].text1);
    Serial.print(' '); Serial.println(messages[i].text2);
  }
  Serial.println(F("------"));
}

bool readLine(Stream& stream)
{
  static int index;

  while (stream.available())
  {
    char c = stream.read();

    if (c >= 32 && index < STRING_BUFFER_SIZE - 1)
    {
      stringBuffer[index++] = c;
    }
    else if (c == '\n' && index > 0)
    {
      stringBuffer[index] = '\0';
      index = 0;
      return true;
    }
  }
  return false;
}

Wenn du nicht ein festes + x machen willst um das Kommando zu überspringen (z.B. damit es allgemeiner und einfacher änderbar ist, dann ist übrigens ein Dummy strtok() die einfachste Lösung:

strtok(StringBuffer, ",;");
char* ptr = strtok(NULL, ",;");

Nach dem zweiten strtok() hast du dann einen Zeiger auf das Token nach dem Kommando