Verschachtelte Switch Case (AT Kommandos interpretieren)

Hallo zusammen,

seit längerer Zeit suche ich nach einem einigermaßen elegantem Weg, AT-Kommandos zu interpretieren.

Diese starten, daher der Name, mit AT und werden dann mit weiteren (Steuer-)Zeichen aufgefüllt.
Der Abschluss erfolgt immer mit einem Carriage Return.
(Das prüfe ich beim füllen des Puffers und rufe dann die entsprechende Funktion auf)

So weit, so einfach:

uint8_t Buffer[10];
uint8_t Counter;

void ProcessInput(){
  if(Counter < 3)
    return;
  if(Buffer[0] != 'A' || Buffer[1] != 'T')
    return;
    
  switch(Buffer[2]){
    case 'I':
      Serial.print("ELM 327");
      break;
    case 'D':
      //Reset to default
      Serial.print("OK");
      break;
    default:
      Serial.print("?");
      break;
  }
  
  //Carriage Return 13
  Serial.write(0x0D);
  
  //Prompt: >
  Serial.write(0x3E);
}

Nun bestehen die Kommandos leider nicht alle aus 3 Zeichen, sondern allerhand möglichen Kombinationen:

ATD
ATD0
ATD1
ATDP
ATDPN
ATS0
ATS1
ATSI
ATSP
ATSH XX YY ZZ

(Das ist nur ein klitzekleiner Auszug der Kommandopalette)

Diese auch noch zum Überfluss Befehle und Werte freudig durchmischen.
So sieht der Fall mit dem "D" dann schnell so aus:

    case 'D':
      if(Counter == 3){
        //Reset to default
        Serial.print("OK");
      }
      if(Counter == 4){
        //Describe Protocol
        if(Buffer[3] == 'P')
          Serial.print("ISO4711");
        else
          bool displayDlc = Buffer[3] == '1';          
      }
      //Describe Protocol by number
      if(Counter == 5)
        Serial.print(3);
      break;

Denke Ihr versteht schon worauf ich hinaus möchte :roll_eyes:
Das switch-case wird extrem unübersichtlich und die Kombination aus Längen- und Zeichenprüfung wird in anderen Beispielen nur noch schlimmer!

Jetzt könnte ich natürlich für jeden Fall mit mehreren Optionen eine Funktion aufrufen, welche wiederum ein switch-case beinhaltet. Und dieses im Zweifel noch eine weitere Funktion mit weiteren cases :confused:

Wenn man alles per if abfrühstückt, ist alles in einer Ebene und ein bisschen übersichtlicher:

uint8_t Buffer[10];
uint8_t Counter;

void ProcessInput(){
  if(Counter < 3)
    return;
  if(Buffer[0] != 'A' || Buffer[1] != 'T')
    return;
    
  if(Compare('I')){
    Serial.print("ELM 327");
  }
  if(Compare('D')){
    //Reset to default
    Serial.print("OK");
  }
  if(Compare('D', '0')){
    bool displayDlc = false;
  }
  if(Compare('D', '1')){
    bool displayDlc = true;
  }
  if(Compare('D', 'P')){
    //Describe Protocol        
    Serial.print("ISO4711");
  }
  if(Compare('D', 'P', 'N')){
    //Describe Protocol by number      
    Serial.print(3);
  }
  //Carriage Return 13
  Serial.write(0x0D);
  
  //Prompt: >
  Serial.write(0x3E);
}

bool Compare(char c1){
  if(Counter != 3)
    return false;
  return Buffer[2] == c1;
}
bool Compare(char c1, char c2){
  if(Counter != 4)
    return false;
  return Buffer[2] == c1 && Buffer[3] == c2;
}
bool Compare(char c1, char c2, char c3){
  if(Counter != 5)
    return false;
  return Buffer[2] == c1 && Buffer[3] == c2 && Buffer[3] == c3;
}

Diese "Lösung" kommt dann spätestens bei der Übergabe von Hex-Werten für z.B. "ATSH" zum Ende:
-> ATSH0CFF34

Wie seht Ihr das?
Den einen Königsweg wird es nicht geben, aber möglicherweise habt Ihr eine Idee wie man das besser lösen kann!

Vielen Dank und schönes Wochenende :slight_smile:

Wofür brauchst Du einen AT-Parser? Evtl. gibt es bessere Kommunikationsprotokolle.

Gruß Tommy

Edit: Schau mal hier, ob Dir das was nutzt.

TriB:
...
Wie seht Ihr das?
...

Parser/Interpreter können eine Strafarbeit sein.

Ich würde nach jedem erkannten Teil der Sequenz (also z. B. schon nach „AT“) den Empfangspuffer leeren, zum passenden Codeteil springen und mit dem Empfang des nächsten Teils warten.

Gruß

Gregor

Hi,

vielen Dank für den Input!

@Tommy56 natürlich gibt es bessere Protokolle! Aber ich sende die Befehle nicht selbst, ich empfange sie nur.
Sowohl Dein Link, als auch der von @noiasca zeigen offenbar die "klassischen" AT-Befehle, welche wie angesprochen, in Klassen aufgeteilt werden.

Schade, das wäre eine gute Sache!
Bei mir geht es um AT Befehle für einen ELM327. Der verwendet weder Trenn-/Steuerzeichen, noch nutzt dieser Klassen.
(Die vollständige Doku dazu gibt es hier. Das Befehlsverzeichnis ab Seite 10).

Deshalb bleibt mir eigentlich nur die Interpretation Zeichen für Zeichen, zwischen "AT" und Carriage Return.

Die "Strafarbeit", wie @gregorss es treffend beschreibt ist auch schon erledigt.
Mein Programm läuft seit ein paar Jahren erfolgreich.
Was mich nur stört, ist ein riesen Ungetüm von switch-case mit zahlreichen Unterabfragen. Das muss doch irgendwie besser gehen, ohne dass man Angst haben muss, dort in einem Jahr nochmal rein zu schauen :wink:

Wie oben am Beispiel von "ATD" gezeigt, kann das schon der ganze Befehl sein.
Oder ein Boolean als 1 oder 0 folgen.
Oder ein "P" um wieder was ganz anderes zu machen.
Zu guter Letzt kann nach dem "P" noch ein "N" kommen.

Ich bräuchte ein Switch-Case, welches Zeichenketten vergleicht, aber in Kombination mit einer Länge, damit ein "ATD" nicht bei einem "ATDP" ausgelöst wird. Jedoch kann auf einen Befehl auch eine Reihe von Hex-Werten folgen ("ATSH0F0F0F"). Da wäre es wiederum hinderlich.
Da bin ich ja schon fast bei einem kleinen Regex :confused:

Hintergrund:
Von ELM327 kompatiblen Apps oder Geräten empfange ich Anfragen und muss diese beantworten.
Eine Vielzahl davon ist für mich irrelevant und ich quittiere sie stumpf mit einem "OK".
Nur rund 25 Befehle + ein paar Kombinationen sind wichtig. Dann werden Funktionen aufgerufen oder schlicht Variablen gesetzt.

TriB:
... ohne dass man Angst haben muss, dort in einem Jahr nochmal rein zu schauen :wink:

Mach' die Strafarbeit zur Strafarbeit, die ultimative Folter für den, der's machen muss: Schreibe verständliche Doku, kommentiere den Code so, dass Du es auch in einem Jahr noch verstehst.

Viele bevorzugen leider, das Ding in einem Jahr nochmal von Anfang an zu programmieren. Gute Doku ist nicht ohne Grund gut und teuer.

Gruß

Gregor

PS: Siehe Punkt 83 :slight_smile:

hm - OBD2 ist doch ein Protokoll für Kfz. Da gibt es doch OBD2-Adapter auf seriell, oder auch CAN-Bus-shields, bluetooth-fähige Adpater usw. Wäre denn ein anderes OBD2-Modul eine Lösungsmöglichkeit?

viele Grüße

Stefan

Also wenjn Du das analysieren musst, würde ich eine Zeile komplett bis zum '\n' in einen globalen Puffer einlesen.
Die ersten beiden Zeichen müssen "AT" sein, sonst verwerfen, außer wenn nur '\n', das hat die Wiederholungsfunktion.

Dann nach dem 1. Buchstaben danach in Funktionen abtauchen, die je nach Vielfalt den Buchstaben komplett abhandeln (bei bis 5? Varianten) oder in weitere Funktionen verzweigen. Zahlenwerte kopieren, mit '\0' abschließen und mit atoi umwandeln.

Das wird eine Sauarbeit, die Du (wie gregorss schon sagte) sehr gut kommentieren solltest.

Wie Du die ermittelten Werte weiter verarbeitest ist dann der 2. Teil.

Evtl. solltest Du jeder ermittelten Kombination einen Index zuweisen, an dem Du hinterher erkennst, welche Zahlenwerte Du noch verarbeiten musst. Das ist aber von Deiner Auswertungslogik abhängig.

Ich denke, hier könnte der 328P schon in Speicherprobleme laufen.

Ich beneide Dich nicht um diese Aufgabe.

Gruß Tommy

TriB:
Wie oben am Beispiel von "ATD" gezeigt, kann das schon der ganze Befehl sein.
Oder ein Boolean als 1 oder 0 folgen.
Oder ein "P" um wieder was ganz anderes zu machen.
Zu guter Letzt kann nach dem "P" noch ein "N" kommen.

Ich bräuchte ein Switch-Case, welches Zeichenketten vergleicht, aber in Kombination mit einer Länge, damit ein "ATD" nicht bei einem "ATDP" ausgelöst wird. Jedoch kann auf einen Befehl auch eine Reihe von Hex-Werten folgen ("ATSH0F0F0F"). Da wäre es wiederum hinderlich.
Da bin ich ja schon fast bei einem kleinen Regex :confused:

Hintergrund:
Von ELM327 kompatiblen Apps oder Geräten empfange ich Anfragen und muss diese beantworten.
Eine Vielzahl davon ist für mich irrelevant und ich quittiere sie stumpf mit einem "OK".
Nur rund 25 Befehle + ein paar Kombinationen sind wichtig. Dann werden Funktionen aufgerufen oder schlicht Variablen gesetzt.

Ich denke, Du zäumst das Pferd falsch auf.
Ich bin nicht ganz schlau aus Deinem Text geworden, was Du auswerten MUSST (nicht kannst!) - kann aber auch an mir liegen. Morgen siehts vielleicht besser aus.

Bei 25 ist es doch noch übersichtlich.

Jenachdem was Du tatsächlich verarbeitest, könnte switch/case das falsche Mittel sein.
Zumindest würde ich nicht fortwährend das Kommando von vorne nach hinten lesen.

Kannst Du eine Befehlsübersicht derer liefern, die ausgewertet werden MÜSSEN?

Wie oben am Beispiel von "ATD" gezeigt, kann das schon der ganze Befehl sein.
Oder ein Boolean als 1 oder 0 folgen.
Oder ein "P" um wieder was ganz anderes zu machen.
Zu guter Letzt kann nach dem "P" noch ein "N" kommen.

hab mal einen Blick in das PDF geworfen.

Es gibt 6 Befehle die mit ATD... Anfangen

ATD
ATD0
ATD1
ATDM1
ATDP
ATDPN

imho brauchst doch nur zunächst das AT wegschneiden und dann bis zum CR oder blank den Rest anschauen, welcher Befehl es wirklich is.
dazu halt den Rest vergleichen mit strcmp ob du nun eben noch

D
D0
D1
DM1
DP
DPN

hast.
eine wurst an

if else if else if..

so, dann hast noch ein paar Sachen wo dahinter ein wert kommt. Da würde ich einen helper schreiben, der den Datenwulst auswertet. Durch den Vorangegangen Befehl weist du ja wie die Datenwurst aufgebaut sein wird.

Was hat dir eigentlich die Suche nach Arduino ELM327 library so alles beschert? War da nichts dabei?

Vorab, damit wir uns nicht falsch verstehen :slight_smile:
Habe eine funktionierende Lösung, die seit ein paar Jahren erfolgreich im Einsatz ist.
Die schreibe ich gerade neu, da ich ein paar Kernfunktionen erweitern muss.

Mich stört es einfach, dass ich so viele Verschachtelungen von switch-case, Prüfungen auf die Nachrichtenlänge und if-else Kombinationen habe.

Will man eine Kleinigkeit ändern, sucht man ewig.

StefanL38:
hm - OBD2 ist doch ein Protokoll für Kfz. Da gibt es doch OBD2-Adapter auf seriell, oder auch CAN-Bus-shields, bluetooth-fähige Adpater usw. Wäre denn ein anderes OBD2-Modul eine Lösungsmöglichkeit?

Leider nein, denn das ist genau die Krux :smiley:
Es stecken nämlich keine Autos dahinter, sondern Motorräder. Und die kennen (noch) kein OBD2.

gregorss:
Mach' die Strafarbeit zur Strafarbeit, die ultimative Folter für den, der's machen muss: Schreibe verständliche Doku, kommentiere den Code so, dass Du es auch in einem Jahr noch verstehst.

An der Doku mangelt es nicht! Die gibt´s :smiley: Und es ist auch jeder Befehl kommentiert. Letzteres macht es aber nur noch unübersichtlicher! Die Kommentare sind alle unterschiedlich eingerückt oder ein Einzeiler-else-Zweig hat eigentlich gar keinen Platz für einen Kommentar.
Und eine Funktion sollte per Clean-Code Definition auch nicht länger als eine Seite sein.

gregorss:
PS: Siehe Punkt 83 :slight_smile:

Das wiederum hilft mir und meinem Gemüt definitiv weiter :stuck_out_tongue_closed_eyes:

Tommy56:
Dann nach dem 1. Buchstaben danach in Funktionen abtauchen, die je nach Vielfalt den Buchstaben komplett abhandeln (bei bis 5? Varianten) oder in weitere Funktionen verzweigen. Zahlenwerte kopieren, mit '\0' abschließen und mit atoi umwandeln.

Hmm, also ein switch-case für das erste Zeichen und dann abzweigen. Macht die monolithische Einstiegsfunktion definitiv übersichtlicher!
Fragmentiert es dann aber später.

Tommy56:
Evtl. solltest Du jeder ermittelten Kombination einen Index zuweisen, an dem Du hinterher erkennst, welche Zahlenwerte Du noch verarbeiten musst. Das ist aber von Deiner Auswertungslogik abhängig.

Die Idee hatte ich tatsächlich auch schon! Quasi ein "Tabelle" mit allen Kombinationen. Der Index vom Eintrag kann dann eine "flaches" switch-case aufrufen.
Für die Werte/Zahlen könnte man ggf. Platzhalter verwenden.
Sicherlich nicht das performanteste aber ich glaube ich muss das einfach mal programmieren und dann auf mich wirken lassen ::slight_smile:
Mit Kommentaren kann man den Index auch wieder sprechen lassen.

my_xy_projekt:
Jenachdem was Du tatsächlich verarbeitest, könnte switch/case das falsche Mittel sein.
Zumindest würde ich nicht fortwährend das Kommando von vorne nach hinten lesen.

Kannst Du eine Befehlsübersicht derer liefern, die ausgewertet werden MÜSSEN?

Nene, fortwährend ist Quatsch! Man muss immer bis zum CR warten. Sonst könnte man ja ein "ATD\r" nicht von einem "ATD1\r" unterscheiden.
IMHO reicht die Liste im Eingangspost für die Kombinationsvielfalt aus. Alle weiteren Befehle sind nach dem selben Schema aufgebaut, nur die Zeichen sind andere.
Es handelt sich aber um alle General, OBD und ISO Kommandos. Keine für CAN.

noiasca:
dazu halt den Rest vergleichen mit strcmp ob du nun eben noch
[...]
so, dann hast noch ein paar Sachen wo dahinter ein wert kommt. Da würde ich einen helper schreiben, der den Datenwulst auswertet.

strcmp würde zumindest schon mal alles erschlagen, was keine Werte mit sich führt.
Der Helper wäre dann quasi eine modifizierte strcmp Funktion, die nur bis zu einem bestimmten Zeichen vergleicht.
Oder um das mit Tommy´s Idee zu kombinieren, wäre es eine Funktion, die "hart" alle Zeichen vergleicht, Zahlen jedoch mit einem Platzhalter berücksichtigt.
Also doch ein Mini-Regex :smiley:

Glaube, das ist es!!!

[Eine Falsche Bier und ne Packung M&M´s später]
Habe kurz was gebastelt, was mir grundsätzlich gut gefällt!
Zu so später Stunde aber bitte meine Testfunktion noch nicht so ernst nehmen :kissing:

In kurz:

  if(Compare(Buffer, "ATD"))  
    ...    
  if(Compare(Buffer, "ATD[b]b[/b]"))
    ...
  if(Compare(Buffer, "ATDP"))  
    ...
  if(Compare(Buffer, "ATDPN"))  
    ...
  if(Compare(Buffer, "ATSH[b]hhhhhh[/b]"))
    ...

In komplett:

uint8_t Buffer[11];
uint8_t Counter;

void setup() {
  Serial.begin(115200);
  Test();
}

void loop() {
 
}

void Test(){
  uint8_t Buffer1[] = {'A', 'T', 'D', '\0'};
  Counter = 3;
  memcpy(Buffer, Buffer1, Counter +1);
  Serial.println("ATD:");
  ProcessInput();    
  uint8_t Buffer2[] = {'A', 'T', 'D', '0', '\0'};
  Counter = 4;
  memcpy(Buffer, Buffer2,  Counter +1);
  Serial.println("ATD0:");
  ProcessInput();
  uint8_t Buffer3[] = {'A', 'T', 'D', 'P', '\0'};
  Counter = 4;
  memcpy(Buffer, Buffer3,  Counter +1);
  Serial.println("ATDP:");
  ProcessInput();
  uint8_t Buffer4[] = {'A', 'T', 'D', 'P', 'N', '\0'};
  Counter = 5;
  memcpy(Buffer, Buffer4,  Counter +1);
  Serial.println("ATDPN:");
  ProcessInput();
  uint8_t Buffer5[] = {'A', 'T', 'S', 'H', '0', 'F', '0', '1', '1', '0', '\0'};
  Counter = 10;
  memcpy(Buffer, Buffer5,  Counter +1);
  Serial.println("ATSH0F0110:");
  ProcessInput();
}

void ProcessInput(){
  if(Compare(Buffer, "ATD"))  
    Serial.println("OK");    
  if(Compare(Buffer, "ATDb")){//Klein b Platzhalter für bool
    bool displayDlc = Buffer[3] == '1';
    Serial.print("Display: ");
    Serial.println(displayDlc);
  }
  if(Compare(Buffer, "ATDP"))  
    Serial.println("ISO4711");
  if(Compare(Buffer, "ATDPN"))  
    Serial.println(3);
  if(Compare(Buffer, "ATSHhhhhhh")){//Klein h Platzhalter für Hex
    uint8_t hex1 = GetByteFromHex(Buffer[4], Buffer[5]);
    uint8_t hex2 = GetByteFromHex(Buffer[6], Buffer[7]);
    uint8_t hex3 = GetByteFromHex(Buffer[8], Buffer[9]);
    Serial.print(getHex(hex1) + ' ');
    Serial.print(getHex(hex2) + ' ');
    Serial.println(getHex(hex3));
  }
}

bool Compare(uint8_t *atCmd, String str){    
  if(Counter != str.length())
    return false;
  for(uint8_t i = 0; i < str.length(); i++){
    if(atCmd[i] == '\0')
      return false;
    switch(str[i]){
      case 'b':
        if(!(atCmd[i] == '0' || atCmd[i] == '1'))
          return false;        
        break;
      case 'h':
        if(!((atCmd[i] > 65 || atCmd[i] < 90) || (atCmd[i] > 48 || atCmd[i] < 57)))
          return false;
        break;
      default:
        if(atCmd[i] != str[i])
          return false;
        break;
    }
  }
  return true;
}

byte GetByteFromHex(uint8_t hexValue0, uint8_t hexValue1) {
  return getVal(hexValue1) + (getVal(hexValue0) << 4);
}

byte getVal(char c) {
  if (c >= '0' && c <= '9')
    return (byte)(c - '0');
  else
    return (byte)(c - 'A' + 10);
}

String getHex(uint8_t data) {
  String str;
  if (data < 0x10)
    str = "0";
  str += String(data, HEX);
  str.toUpperCase();
  return str;
}

Output:

ATD:
OK
ATD0:
Display: 0
ATDP:
ISO4711
ATDPN:
3
ATSH0F0110:
0F 01 10

Gute Nacht :sleeping:

Guten Morgen,

TriB:
Vorab, damit wir uns nicht falsch verstehen :slight_smile:
Habe eine funktionierende Lösung, die seit ein paar Jahren erfolgreich im Einsatz ist.
Die schreibe ich gerade neu, da ich ein paar Kernfunktionen erweitern muss.

Das hab ich schon so verstanden.

Nene, fortwährend ist Quatsch! Man muss immer bis zum CR warten. Sonst könnte man ja ein "ATD\r" nicht von einem "ATD1\r" unterscheiden.
IMHO reicht die Liste im Eingangspost für die Kombinationsvielfalt aus. Alle weiteren Befehle sind nach dem selben Schema aufgebaut, nur die Zeichen sind andere.

Ich hab mich missverständlich ausgedrückt.
Du vergleichst und zerlegst Zeichen für Zeichen

Ich würde es nicht zerlegen.

Habe kurz was gebastelt, was mir grundsätzlich gut gefällt!

void ProcessInput(){

if(Compare(Buffer, "ATD"))  
   Serial.println("OK");

Genau da hatte ich auch meinen Ansatz gesehen, aber nicht mit Compare sondern mit strlen und strstr

strlen, um zum einen ATD und ATDP z.B. zu unterscheiden und zum anderen anhand der Länge des chararrays schon zu verzweigen und nicht alle (25?) Bedingungen prüfen zu müssen wenn die letzte dann erfüllt ist.

Damit musst Du nicht erst in einen Zwischenpuffer schreiben, sondern vergleichst das eingelesene direkt.

Und bevor Du damit anfängst möchte ich nochmal auf Deine Einrückungen und Übersichtlichkeitsfrage eingehen.
kopier den folgenden Code mal in Deine IDE und drücke stgr-t :wink:

// *INDENT-OFF*
void setup()
  {
                // Hier wird Bedingung abgefragt
  for (int i = 0; i < 1; i++) 
    {           // Hier wird code ausgefuehrt
    }
  }
// *INDENT-ON*

void loop()
  {
                // Hier wird Bedingung abgefragt
  for (int i = 0; i < 1; i++) 
    {           // Hier wird code ausgefuehrt
    }
  }

[edit] compilierbar gemacht, bevor einer meckert das ist kein funktionsfähiger Code (Soll er auch gar nicht sein) :slight_smile: [/edit]

Hi

Was ich zumindest deutlich lesbarer finde: Die öffnende Klammer steht auch in der den Block einleitenden Zeile.

// *INDENT-OFF*
void setup(){
  for (int i = 0; i < 1; i++){ // Hier wird Bedingung abgefragt
           // Hier wird code ausgefuehrt
  }
}
// *INDENT-ON*

void loop(){
  for (int i = 0; i < 1; i++){  // Hier wird Bedingung abgefragt
           // Hier wird code ausgefuehrt
  }
}

Mit den einzeln stehenden Klammern ist das Aussehen des Code, wie wenn ich Leerzeilen mit der Gießkanne verteile.
Kommentare gerne in der Zeile, Die Einen benötigt.
Bei Funktionen (zu Methoden bin ich noch nicht gekommen ...) mache ich mir gerne einen Kommentar-Block oberhalb der Funtkion, ähnlich, wie ich Das zu Assembler lernen durfte.
Da steht drin, was die Funktion macht, was Sie haben will und was Sie zurück gibt.
Bei Assembler noch, welche Register zerstört werden und so Kram - hier wohl nicht von Belag.

Wenn bei mir eine einzelne Klammer auftaucht, hat Die einen Sinn - z.B. einen eigenen Block innerhalb eines case, damit ich dort lokale Variablen benutzen kann, Die sonst angemeckert werden, da Diese im Rest des switch-case Konstrukt undefiniert sind.

MfG

postmaster-ino:
Hi

Was ich zumindest deutlich lesbarer finde: Die öffnende Klammer steht auch in der den Block einleitenden Zeile.

Darum geht es nicht. :wink:
Die Aufgabe war den Code so zu übernehmen und dann STRG-T zu drücken .

(Das macht keinen Unterschied - geht hier auch :wink: - und ist trotzdem schöner lesbar)
(man kann sich ja zwei Unsitten gleichzeitig abgewöhnen)

Parsen?

Lesetipp: "wikipedia Backus Naur Form"