Verzweifle an Serial.read()

Hi,

ich habe noch nicht sonderlich viel mit meinem Arduino Uno gemacht - das scheint mein Hauptproblem zu sein.

Ich beiße mir aktuell an einer Sache die Zähne aus:

Ich würde gerne über die serielle Verbindung (COM-Port / USB Kabel) dem Arduino einen "Hex"-String schicken (z.B. ff000000) je zwei Hex-Werte soll er zu einem Integer (oder Long) zusammenführen:

  • Zeichen 1 und 2 = Wert von 0 bis 255
  • Zeichen 3 und 4 = Wert von 0 bis 255
  • Zeichen 5 und 6 = Wert von 0 bis 255
  • Zeichen 7 und 8 = Wert von 0 bis 255

Da irgendwie immer nur komisches Zeugs raus kam, habe ich ein paar Serial.print() Ausgaben zusammengeschrieben um herauszufinden, wo das Problem liegt.

Der folgende Code:

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


void loop() { 
  char fwd[2] = "";
  char bwd[2] = "";;
  char lft[2] = "";;
  char rgt[2] = "";;
  char command[8] = "";
  
  if (Serial.available() > 7) 
  {

    for (int i; i < 8; i++)
    {
      command[i] = (char)Serial.read();  
    }
    fwd[0] = command[0];
    fwd[1] = command[1];
    bwd[0] = command[2];
    bwd[1] = command[3];
    lft[0] = command[4];
    lft[1] = command[5];
    rgt[0] = command[6];
    rgt[1] = command[7];
    
    Serial.print("FWD:");
    Serial.print(fwd);
    Serial.print("#");
    Serial.println(strtol(fwd, NULL, 16));
    
    Serial.print("BWD:");
    Serial.print(bwd);
    Serial.print("#");
    Serial.println(strtol(bwd, NULL, 16));
    
    Serial.print("LFT:");
    Serial.print(lft);
    Serial.print("#");
    Serial.println(strtol(lft, NULL, 16));

    Serial.print("RGT:");
    Serial.print(rgt);
    Serial.print("#");
    Serial.println(strtol(rgt, NULL, 16));
  }
}

Erzeugt folgende Ausgabe (wenn ich ff000000) sende:

FWD:ff p#255
BWD:00ff p#255
LFT:0000ff p#255
RGT:000000ff p#255

Woher kommen denn die zusätzlichen Zeiten in bwd, lft, rgt ?

Vielen Dank für jede Hilfe im Voraus!

Was auffällt ist dass deine Strings nicht richtig terminiert sind. Dann wandelt strtol() mehr also du willst. Woher soll die Funktion wissen dass sie nach zwei Zeichen aufhören soll?

Eine Option um eine beliebige Anzahl von beliebig langen per Komma getrennten Hex-Strings einzulesen habe ich gerade hier gezeigt:

http://forum.arduino.cc/index.php?topic=380980.msg2626876#msg2626876

Das Senden muss mit einem Linefeed/newline abgeschlossen werden! (neben der Baudrate einstellen)

Also z.B. 12,AB,FF,34

Wenn es aber immer nur wirklich zwei Bytes sind, kannst du das auch viel einfacher machen und die Konvertierungs-Funktion per Hand schreiben. Dann geht es auch ohne Terminierung

Hier ist eine einfache Alternative wenn du immer nur 4 * 2 Ziffern in einem String einlesen willst. Auch das mit einem LF am Ende!

const int SERIAL_BUFFER_SIZE = 9;
char serialBuffer[SERIAL_BUFFER_SIZE];

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

void loop()
{
  if (readSerial(Serial))
  {
    if (strlen(serialBuffer) == 8)
      parseSerial();
    else
      Serial.println(F("8 digits expected!"));
  }

}

void parseSerial()
{

  byte fwd = hexToByte(&serialBuffer[0]);
  byte bwd = hexToByte(&serialBuffer[2]);
  byte lft = hexToByte(&serialBuffer[4]);
  byte right = hexToByte(&serialBuffer[6]);

  Serial.println(fwd, HEX);
  Serial.println(bwd, HEX);
  Serial.println(lft, HEX);
  Serial.println(right, HEX);
  Serial.println(F("----"));
}

byte hexToByte(char* str)
{
  byte value = 0;
  byte weight = 1;

  for (int i = 0; i < 2; i++)
  {
    value *= weight;

    if (str[i] >= '0' && str[i] <= '9')
      value += str[i] - '0';
    else if (str[i] >= 'A' && str[i] <= 'F')
      value += str[i] - ('A' - 10);
    else if (str[i] >= 'a' && str[i] <= 'f')
      value += str[i] - ('a' - 10);

    weight *= 16;
  }

  return value;
}

bool readSerial(Stream& stream)
{
  static byte index;

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

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

Das liest erst mal per readSerial() den ganzen String ein. Wenn man 8 Zeichen hat, übergibt man einen Zeiger auf den nächsten Teil-String an hexToByte() und die Funktion wandelt die nächsten zwei Zeichen von Hex in ein Byte (aber Vorsicht: der Algorithmus funktioniert so wirklich nur mit zwei Ziffern!)

Hier kannst du also sowas eintippen:
AA12ff56
12345678

Oder noch eine simple Möglichkeit:
Du machst einmal strtoul() (also die unsigned Version) und extrahierst dann die Bytes einfach aus dem Integer

Erst einmal vielen Dank für die schnellen und tollen Antworten!

Leider konnte ich mich erst jetzt wieder mit dem Thema auseinander setzen.

Der Hinweis mit der Terminierung war entscheidend. Es funktioniert jetzt auch - aber nur, wenn ich direkt nach dem einlesen der entsprechenden Bytes die Umwandlung durchführe.

Das verstehe ich zwar auch nicht - damit kann ich aber weiter arbeiten :slight_smile:

Dieser Code funktioniert:

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


void loop() { 
  char fwd[] = "";
  char bwd[] = "";
  char lft[] = "";
  char rgt[] = "";
  
  if (Serial.available() > 7) 
  {
    
    fwd[0] = Serial.read();
    fwd[1] = Serial.read();
    fwd[2] = '\0';

    Serial.print("FWD:");
    Serial.print(fwd);
    Serial.print("#");
    Serial.println(strtol(fwd, NULL, 16));

    bwd[0] = Serial.read();
    bwd[1] = Serial.read();
    bwd[2] = '\0';

    Serial.print("BWD:");
    Serial.print(bwd);
    Serial.print("#");
    Serial.println(strtol(bwd, NULL, 16));

    lft[0] = Serial.read();
    lft[1] = Serial.read();
    lft[2] = '\0';

    Serial.print("LFT:");
    Serial.print(lft);
    Serial.print("#");
    Serial.println(strtol(lft, NULL, 16));
    
    rgt[0] = Serial.read();
    rgt[1] = Serial.read();
    rgt[2] = '\0';
    
    Serial.print("RGT:");
    Serial.print(rgt);
    Serial.print("#");
    Serial.println(strtol(rgt, NULL, 16));
  }
}

Und bringt folgende Ausgabe: (bei ffffffff)

FWD:ff#255
BWD:ff#255
LFT:ff#255
RGT:ff#255

Bei diesem Code kommmt allerdings etwas anderes heraus (obwohl es eigentlich identisch sein sollte?)

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


void loop() { 
  char fwd[] = "";
  char bwd[] = "";
  char lft[] = "";
  char rgt[] = "";
  
  if (Serial.available() > 7) 
  {
    
    fwd[0] = Serial.read();
    fwd[1] = Serial.read();
    fwd[2] = '\0';

    bwd[0] = Serial.read();
    bwd[1] = Serial.read();
    bwd[2] = '\0';

    lft[0] = Serial.read();
    lft[1] = Serial.read();
    lft[2] = '\0';
   
    rgt[0] = Serial.read();
    rgt[1] = Serial.read();
    rgt[2] = '\0';

    Serial.print("FWD:");
    Serial.print(fwd);
    Serial.print("#");
    Serial.println(strtol(fwd, NULL, 16));

    Serial.print("BWD:");
    Serial.print(bwd);
    Serial.print("#");
    Serial.println(strtol(bwd, NULL, 16));

    Serial.print("LFT:");
    Serial.print(lft);
    Serial.print("#");
    Serial.println(strtol(lft, NULL, 16));

    Serial.print("RGT:");
    Serial.print(rgt);
    Serial.print("#");
    Serial.println(strtol(rgt, NULL, 16));
  }
}

Bei "ffffffff" kommt hier ein merkwürdiges Ergebnis raus:

FWD:#0
BWD:#0
LFT:f#15
RGT:ff#255

Schöner Puffer-Überlauf

Das ist Blödsinn:

 char fwd[] = "";
  char bwd[] = "";
  char lft[] = "";
  char rgt[] = "";

Wie groß denkst du dass diese Arrays sind? Spoiler: 2 Byte. Du schreibst aber 3 Byte rein

Gebe bei solchen Puffer Arrays immer die Größe direkt an!

Du brauchst doch gar nicht extra Arrays für jeden String. Sondern kannst das gleiche Array für alles verwenden

thedeadlink:
Bei "ffffffff" kommt hier ein merkwürdiges Ergebnis raus:

Du greifst auf nicht definierte Speicherbereiche zu, da Deine Felder zu klein sind. Versuche:

  char fwd[] = "  ";
  char bwd[] = "  ";
  char lft[] = "  ";
  char rgt[] = "  ";

oder

  char fwd[3];
  char bwd[3];
  char lft[3];
  char rgt[3];

Oha. Da rächen sich wohl die Jahre mit PHP?

Ich habe jetzt den Code von Serenifly beinahe vollständig übernommen - und glaube ihn aber auch (halbwegs) verstanden zu haben. (Danke!)

Allerdings habe ich die Konvertierung der beiden Chars in Integer (oder Long) doch mit strtol() realisiert.

Ich überlege, ob ich nicht noch einbaue, dass nur ein Kommando akzeptiert wird, welches mit einem "#" beginnt und einem "/n" endet und dazwischen genau 8 Byte lang ist. Alles andere soll er verwerfen.

Aber vielleicht bekomme ich das ja mal selbst gelöst (?)

Nocheinmal vielen Dank für Eure schnelle Hilfe!

//Das Kommando soll 8 Byte lang sein
const int CMD_LENGTH = 8;
//Das Array muss ein char länger sein; für die Null-Terminierung
char command[CMD_LENGTH+1];
//Vorwärts von 0 bis 255
int fwd = 0;
//Rückwärts von 0 bis 255
int bwd = 0;
//Lenken nach links von 0 bis 255 
int lft = 0;
//Lenken nach rechts von 0 bis 255 
int rgt = 0;

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


void loop() {
  if (newCommand(Serial)) {
    updateAction();
    Serial.print("Command: ");
    Serial.println(command);
    Serial.print("FWD:");
    Serial.print(fwd);
    Serial.print(" BWD:");
    Serial.print(bwd);
    Serial.print(" LFT:");
    Serial.print(lft);
    Serial.print(" RGT:");
    Serial.println(rgt);    
  } 
 
}

bool newCommand(Stream& stream)
{
   // Wir müssen alle Bytes aus dem Puffer holen.
   // Wenn weniger als 8 Bytes kommen, dann verwerfen wir das komplette Kommando.
  static byte index;

  while (stream.available())
  {
    char c = stream.read();
    if (c >= 32 && index < CMD_LENGTH)
    {
      command[index] = c;
      index++;
    }
    else if (c == '\n' && index == CMD_LENGTH)
    {
      command[index] = '\0';
      index = 0;
      return true;
    }
  }
  return false;  
}

void updateAction()
{
  // Jetzt gehe ich davon aus, dass ich 8 Bytes habe wo ich die einzelnen Kommandos extrahieren müsste.
  int index = 0;
  char temp[3];
  while (index < CMD_LENGTH)
  {
    temp[0] = command[index];
    index++;
    temp[1] = command[index];
    index++;
    temp[2] = '\0';

    // FWD
    if (index < 3) 
    {
      fwd = strtol(temp, NULL, 16);
    } else if (index < 5) 
    {
      bwd = strtol(temp, NULL, 16);
    } else if (index < 7)
    {
      lft = strtol(temp, NULL, 16);
    } else 
    {
      rgt = strtol(temp, NULL, 16);
    }
  }
}
if (c == '\n' && index == CMD_LENGTH)

Hier könnte man diskutieren ob das wirklich so eine gute Idee ist. Dadurch wird das nur abgeschlossen wenn das LF immer genau an der Stelle kommt. Das sollte normal der Fall sein, aber theoretisch kann da auch mal was schief laufen.

Sicherer ist es man schließt bei einem LF immer ab, egal wo und wann es kommt.

Was wie gesagt auch ganz, ganz simpel geht ist das (ohne Serial. Die Eingabe wird als Konstante vorgegeben):

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

  parseValue("AA12ff56");
  parseValue("12345678");
}

void loop()
{
}

void parseValue(const char* str)
{
  unsigned long value = strtoul(str, NULL, 16);
  Serial.println(value, HEX);

  byte b4 = value >> 24;
  byte b3 = value >> 16;
  byte b2 = value >> 8;
  byte b1 = value;

  Serial.println(b4, HEX);
  Serial.println(b3, HEX);
  Serial.println(b2, HEX);
  Serial.println(b1, HEX);

  Serial.println("---");
}

Man wandelt den String einmal in einen unsigned long und extrahiert dann die Bytes einfach durch Schieben nach rechts

So braucht man nichts umzukopieren oder zig Teil-Strings zu wandeln. Das wird die einfachste Lösung sein. Geht nur bis 8 Ziffern, aber das reicht hier.

Ich wollte Euch den funktionierenden Code nicht vorenthalten.

Der Arduino ist noch mit einem Adafruit Shield ausgestattet:
Link zur Webseite: https://www.adafruit.com/products/1438
Adafruit Motor/Stepper/Servo Shield for Arduino v2 Kit - v2.3

#include <Wire.h>
#include <Adafruit_MotorShield.h>
#include "utility/Adafruit_MS_PWMServoDriver.h"
#include <Servo.h>

Adafruit_MotorShield AFMS = Adafruit_MotorShield(); 

Adafruit_DCMotor *myMotor = AFMS.getMotor(1);
Servo steering;

uint8_t default_speed_forward = 200;
uint8_t default_speed_backward = 140;
int steering_min = 45;
int steering_center = 80;
int steering_max = 135;

//Das Kommando soll 8 Byte lang sein
const int CMD_LENGTH = 8;
//Das Array muss ein char länger sein; für die Null-Terminierung
char command[CMD_LENGTH+1];
//Vorwärts von 0 bis 255
uint8_t fwd = 0;
//Rückwärts von 0 bis 255
uint8_t bwd = 0;
//Lenken nach links von 0 bis 255 
int lft = 0;
//Lenken nach rechts von 0 bis 255 
int rgt = 0;

void setup() {
  Serial.begin(9600);
  steering.attach(10); 
  AFMS.begin(); 
}


void loop() {
  //Delay für die Stabilität?
  delay(1);
  
  if (newCommand(Serial)) {
    updateAction();
    if (fwd == 0 && bwd == 0) {
      myMotor->setSpeed(0);
      myMotor->run(RELEASE);
    }
    if (fwd>0) {
      myMotor->setSpeed(default_speed_forward);
      myMotor->run(FORWARD);     
    } 
    if (bwd>0) {
      myMotor->setSpeed(default_speed_backward);
      myMotor->run(BACKWARD);      
    }
    if (lft == 0 && rgt == 0) {
      steering.write(steering_center);       
    }
    if (lft>0) {
      steering.write(steering_min);       
    }
    if (rgt>0) {
      steering.write(steering_max);       
    }
    
  } 
 
}

bool newCommand(Stream& stream)
{
   // Wir müssen alle Bytes aus dem Puffer holen.
   // Wenn weniger als 8 Bytes kommen, dann verwerfen wir das komplette Kommando.
  static byte index;

  while (stream.available())
  {
    char c = stream.read();
    if (c >= 32 && index < CMD_LENGTH)
    {
      command[index] = c;
      index++;
    }
    else if (c == '\n' && index == CMD_LENGTH)
    {
      command[index] = '\0';
      index = 0;
      return true;
    }
  }
  return false;  
}

void updateAction()
{
  // Jetzt gehe ich davon aus, dass ich 8 Bytes habe wo ich die einzelnen Kommandos extrahieren müsste.
  int index = 0;
  char temp[3];
  while (index < CMD_LENGTH)
  {
    temp[0] = command[index];
    index++;
    temp[1] = command[index];
    index++;
    temp[2] = '\0';

    // FWD
    if (index < 3) 
    {
      fwd = strtol(temp, NULL, 16);
    } else if (index < 5) 
    {
      bwd = strtol(temp, NULL, 16);
    } else if (index < 7)
    {
      lft = strtol(temp, NULL, 16);
    } else 
    {
      rgt = strtol(temp, NULL, 16);
    }
  }
}