Back
Featured image of post Projekt Log 03 | Arduino Part I

Projekt Log 03 | Arduino Part I

Mit diesem Post beginnt nur die tatsächliche Umsetzung des Projekts. Ziel ist es, eine sinnvolle Kommunikation zwischen dem Raspberry/PC und Arduino herzustellen. Außerdem sollen die Basisfunktionen für LEDs und Knöpfe festgelegt werden.

Anforderungen

Auf dem Raspberry soll zu einem späteren Zeitpunkt die Website gehostet werden. Dort wird auch eine Datenbank existieren. Es macht also durchaus Sinn, dass nur der Raspberry über den tatsächlichen Spielstand informiert ist. Der Arduino ist dem Raspberry also untergeordnet und muss “nur” Folgendes leisten können:

  • Eingehende Befehle vom Raspberry verarbeiten, dazu zählen:
    • Setzen der Farbe und Helligkeit der LEDs
    • Manipulation des Status von weiterer Hardware (Falls noch was dazu kommt)
  • Befehle an den Raspberry versenden, dazu zählen:
    • Knopfdrücke, inkl Information über Art (also Tor, Eigentor oder Rückgängig und welcher Slot)
    • Befehle von weiterer Hardware (Falls noch was dazu kommt)
    • Fehlermeldungen (z.b. wenn ein Befehl nicht verstanden worden ist)

Eigenes Protokoll

Für die Kommunikation zwischen Arduino und Raspberry benötigt es ein Protokoll. Das muss nicht kompliziert sein, aber ohne wird der Code schnell unübersichtlich und nicht nachvollziehbar. Deshalb habe ich mir zunächst mein eigenes Protokoll ausgedacht.

Eine Nachricht zwischen Raspberry und Arduino und umgekehrt sollte immer gleich aufgebaut sein. Das macht das coden deutlich leichter. Außerdem muss verifiziert sein, dass die Nachricht vollständig angekommen ist. Die Kommunikation erfolgt über die serielle Schnittstelle, es recht also, wenn ich das Protokoll mit ACII Zeichen definiere.

Eine übertragene Nachricht hat dabei die Form !X!Y!, wobei die Zeichen folgende Bedeutung haben:

  • X Dieses Zeichen stellt den Modus dar.
  • Y Dieses Zeichen stellt eigentliche Nachricht dar.
  • ! Das Ausrufezeichen kennzeichnet den Start und das Ende sowie die Separation zwischen Modus und Nachricht.

Wenn der Arduino an der Seriellen Schnittstelle nun etwas empfängt, muss der sicher stellen, das ! drei mal vorkommt. Dann kann der Modus und die Nachricht extrahiert werden. Hier mal zwei Beispiele für mein Protokoll:

  • !e!transfer! Es handelt sich hier um eine Fehlermeldung (e für Error). Im Detail teilt uns der Arduino mit, das die Nachricht unvollständig ist.
  • !s!1234567812345678123456781234567812345678901234567890! Der Arduino soll die LEDs neu setzen. Die ersten 32 Zahlen stehen für die vier Inputdevices mit jeweils 8 LEDs, die letzten 20 Zahlen für die LEDs der Spielstandsanzeigen. Wie die Zahlen dann interpretiert werden, hängt von dem Code auf dem Arduino ab (0 steht für LED aus, 1 für Rot, 2 für Grün usw.)

In der Theorie habe ich gedacht, dass dieses Protokoll gut funktioniert. Es bringt aber einige Probleme mit sich:

  • Die Farbcodierung ist sehr unflexibel (da nur 10 Farben im Arduino hardgecodet sind)
  • Die Extraktion von Modus und Nachricht gestaltet sich als relativ schwer, da:
    • Nachricht hat keine Feste Länge (SEHR viel Liebesmühe verschwendet)
    • Umwandlung von ASCII Zeichen in Integern schwieriger als man denkt
  • Implementierung weiterer Modi erfordert recht viel Code

Am Ende war ich sehr unzufrieden mit der Unflexibilität dieses Protokolls. Wie so oft: Anstatt das Rad neu zu erfinden und Stunden in eine Lösung zu stecken, die quasi nichts kann, hätte ich mal lieber Google im Voraus gefragt. Dieses Problem hatten nämlich schon Tausende Leute vor mir und ein paar schlaue Leute haben das JSON Format für den Arduino zugänglich gemacht.

ArduinoJSON

ArduinoJSON ist eine C++ Bibliothek die es ermöglicht die seriell eingehenden Daten als JSON Dictionary zu interpretieren und umgekehrt Daten als JSON in die serielle Schnittstelle zu schreiben. Das macht es auf beiden Seiten sehr einfach mit eingehenden und ausgehenden Daten umzugehen. Ich muss mich also nicht mehr um die Umwandlung und Validierung kümmern, außerdem ist das Protokoll sehr flexibel.

Eine Nachricht zwischen Arduino und Raspberry bzw. umgekehrt hat nun die gestalt {"mode": X,"msg": Y}. Das ist deutlich flexibler, da nun Y ebenfalls ein Dictionary sein kann. So ist beispielsweise folgender Befehl möglich:

{"mode":"setled","msg":{"pos":"wd","R":[1,2,3,4,5,6,7,8,9,10],"G":[1,2,3,4,5,6,7,8,9,10],"B":[1,2,3,4,5,6,7,8,9,10]}}

Der Modus besagt, dass die LEDs neu gesetzt werden sollen. In der Nachricht ist nun eine genaue Spielerposition (wd für White Defense) angegeben. Außerdem ist für jede LED ein R,G und B Wert vorhanden. So habe ich volle Kontrolle über jede LED in Farbe und Helligkeit.

Hier noch ein paar Beispiele aus dem seriellen Monitor:

monitor1

monitor1

monitor1

Die LEDs selbst sind noch nicht angeschlossen, es passiert in Realität also erst mal noch nichts. ABER: Eine flexible Kommunikation steht, und das ist schon die halbe Miete!

Buttons

Das Ziel ist es, an jeder Spielerposition zwei Knöpfe zu haben. Bei einem Druck auf einen dieser soll ein Befehl an den Raspberry über die serielle Schnittstelle geschickt werden. Zur Implementierung vom Basic Code reicht es erst mal, einen Button zu testen. Dazu hab ich folgendes Setup:

setup

Ich habe das Breadboard, den Raspberry und den Arduino auf ein Holz geklebt, damit ich alles schnell auf und abbauen kann. Sehr praktisch!

Bei Knöpfen ist es sehr wichtig, sie zu Entprellen. Bei sehr einfachen Projekten reicht es, mit der Funktion delay() zu arbeiten. Allerdings ist das in meinem Fall nicht sinnvoll, da sonst eventuelle Events auf der seriellen Schnittstelle nicht bemerkt werden. Alternativ können Kondensatoren genutzt werden, aber die Software Lösung bietet mehr Flexibilität und es muss nichts gelötet werden. Eine deutlich elegante Lösung ist es, den Zeitpunkt des erstmaligen HIGH Signals durch einen Knopfdruck mit millis() zu messen und erst nach einer gewissen Entprellzeit das Signal als HIGH zu interpretieren. Die folgende Grafik sollte das ganz gut darstellen:

entprellen

Am Anfang habe ich wiedermal versucht, all das selber zu implementieren. Aber nach einigen Versuchen habe ich die Bibliothek Bounce2 gefunden, die all das übernimmt. Der Code wird damit deutlich aufgeräumter.

Damit später beim Kickern ein Spielzug auch zurückgenommen werden kann, sollte es einen UNDO Knopf geben. Anstatt einen extra Knopf dafür einzubauen, lässt sich so was auch über die verstrichene Zeit des Knopfdrucks abbilden. In der folgenden Grafik ist das dargestellt. Wird ein Knopfdruck erkannt, wird die Zeitmessung begonnen. Verstreichen mehr als eine festgesetzte Zeit (aktuell 1,5 Sekunden), wird dieser Knopfdruck als “Langdruck / Undo” interpretiert. Endet der Knopfdruck vor dieser Zeit, so wird er als “Kurzdruck / Tor” interpretiert. Bei der Roten und blauen Linie wird dann das jeweilige Signal an den Raspberry geschickt.

entprellen

Noch ein Beispiel aus dem seriellen Monitor, nachdem der Knopf einmal kurz und einmal lang gedrückt wurde (gwd steht für Goal White Defense):

entprellen

Code

Hier noch der aktuelle Code mit ein paar Kommentaren.

#include <ArduinoJson.h>
#include <Bounce2.h>


// Serial input
String serial_input_string = "";
bool serial_input_string_complete = false;

// LED General
const int ARRAYLEN = 10;

// Button General
const int DEBOUNCETIME = 30;
const int UNDOTIME = 1500;

// Button GoalWhiteDefense
const int GWD_PIN = 6;
Bounce2::Button gwd_button = Bounce2::Button();
int gwd_press_started = 0;
bool gwd_sended = true;

void setup() {
  Serial.begin(115200);
  
  // reserve 200 bytes for the serial_input_string
  serial_input_string.reserve(200);

  // Init button objects
  gwd_button.attach (GWD_PIN,INPUT_PULLUP);
  gwd_button.interval(DEBOUNCETIME);
  gwd_button.setPressedState(LOW); 
}


void loop() {
  // Check for incoming commands
  check_serial_input();

  // Check buttons
  check_button(gwd_button, gwd_press_started, gwd_sended, "gwd");

}


void check_button(Bounce2::Button & button, int & press_started, bool & sended, String button_indent){
  // Init variables
  int pressed_time;

  // Update button object
  button.update();

  // Detect rising edge and start timing
  if (button.pressed()){
    press_started = millis();
    sended = false;
  }

  // Calculate the time the button has already been pressed
  pressed_time = millis() - press_started;

  // if button is released before UNDOTIME has passed, it's a normal press
  if (button.released()){
    if (pressed_time < UNDOTIME){
      send("pressed", button_indent);
    }
    sended = true;
  }

  // If button is still pressed after UNDOTIME has passed, it's an UNDO press
  if (button.isPressed() && sended == false && pressed_time > UNDOTIME){
    send("pressed", "undo");
    sended = true;
  }
}


void decode_serial_input(){
  // Init JSON document
  StaticJsonDocument<512> json_dict;

  // Deserialize the JSON document
  DeserializationError err = deserializeJson(json_dict, serial_input_string);

  // Test if parsing succeeds
  if (err) {
    raise_error(err.f_str());
    return;
  }

  // Fetch mode
  String mode = json_dict["mode"];

  // Process mode setled
  // {"mode":"setled","msg":{"pos":"wd","R":[1,2,3,4,5,6,7,8,9,10],"G":[1,2,3,4,5,6,7,8,9,10],"B":[1,2,3,4,5,6,7,8,9,10]}}
  if(mode == "setled"){
    // Bool if error in nested json
    bool nested_exists = true;
    
    // Fetch non-nested json
    const char* pos = json_dict["pos"];

    // Fetch nested json
    int red[ARRAYLEN];
    for (int i=0; i<ARRAYLEN; i++){
      red[i] = json_dict["msg"]["R"][i];
      if (!red[i] && red[i] != 0){
        nested_exists = false;
      }
    }

    // Fetch nested json
    int green[ARRAYLEN];
    for (int i=0; i<ARRAYLEN; i++){
      green[i] = json_dict["msg"]["G"][i];
      if (!green[i] && green[i] != 0){
        nested_exists = false;
      }
    }

    // Fetch nested json
    int blue[ARRAYLEN];
    for (int i=0; i<ARRAYLEN; i++){
      blue[i] = json_dict["msg"]["B"][i];
      if (!blue[i] && blue[i] != 0){
        nested_exists = false;
      }
    }

    // Check if non-nested and nested are valid
    if (nested_exists){
      send("echo","setled");
      // Process pos, red, green, blue
    }
    else{
      raise_error("MissingKey or InvalidValues");
    }
  }
  else{
    raise_error("MissingMode or InvalidInput");
  }
}


void send(String mode, String msg){
  core_send(mode, msg);
}


void raise_error(String err){
  core_send("error",err);
}


void core_send(String mode, String msg){
  StaticJsonDocument<64> json;
  json["mode"] = mode;
  json["msg"] = msg;

  // Write to serial with additional linebreak
  serializeJson(json, Serial);
  Serial.print("\n");
}


void check_serial_input(){
  if (serial_input_string_complete) {
    // Execute encoded command
    decode_serial_input();
    
    // clear the string:
    serial_input_string = "";
    serial_input_string_complete = false;
  }
}


void serialEvent() {
  // This function is called automatically after each loop
  // Copied from Examples --> Communication --> SerialEvent
  
  while (Serial.available()) {
    
    // get the new byte:
    char inChar = (char)Serial.read();
    
    // add it to the serial_input_string:
    serial_input_string += inChar;
    
    // if newline, set a flag so the main loop can do stuff
    if (inChar == '\n') {
      serial_input_string_complete = true;
      
    }
  }
}
Built with Hugo
Theme Stack designed by Jimmy