Arduino Clean Code – Code Refactoring für’s Horn

  • Lesedauer:7 min Lesezeit

Es ist an der Zeit für ein Refactoring, Arduino Clean Code ist das Ziel! Der Arduino Code für die meisten Shot Clock Komponenten ist vor einigen Jahren entstanden. Zu der Zeit war mir Clean Code nicht geläufig. Ich habe einfach so programmiert, dass es funktioniert. Dadurch ist der gesamte Code in einem .ino File. Der Code ist zwar in Funktionen aufgeteilt, Clean Code sieht aber definitiv anders aus!

Tools für’s Refactoring zum Arduino Clean Code

Die Arduino IDE ist zwar eine nette Spielerei und funktioniert auch sehr gut, bietet uns Programmierern aber wenig Hilfsmittel. Warum sollte ich bei der Arduino Programmierung z. B. auf IntelliSense mit Autovervollständigung verzichten? Warum sollte ich auf eine intelligente Umbenennung der Variablen verzichten? Warum sollte ich auf alle sonstigen Vorzüge, die eine moderne IDE bietet verzichten? Sollte ich nicht!

Zum Glück gibt es mittlerweile verschiedene Möglichkeiten Arduinos in anderen IDE’s zu integrieren. Da ich für die meisten Tätigkeiten VS Code nutze, habe ich mich auch hier wieder für VS Code entschieden.

Für VS Code gibt es die Extension platformio. Diese bietet alles, was wir benötigen, um Code auf’s Board zu bekommen. Wie ihr damit durchstarten könnt habe im Beitrag Arduino in VS Code erklärt.

Bestandteile und Verantwortlichkeiten

Jetzt kommen wir zum eigentlichen Refactoring! Zuerst überlegen wir mal, was das Horn eigentlich alles macht. Wenn wir das wissen, können wir anfangen den Code in sinnvolle Teile zu zerlegen. Aus der Vogelperspektive muss das Horn zwei Dinge können:

  • Hupen auf Kommando
  • WLAN für OTA bereitstellen

Das sieht erst mal nach wenig Aufgaben aus. Um diese Aufgaben zu erledigen benötigen wir einige Schnittstellen:

  • Kommunikation mit anderen Komponenten
  • Kommunikation mit einem PC für OTA-Updates
  • Steuern von GPIOs

Daraus können wir im nächsten Schritt ein paar Abstraktions Layer ableiten, durch die der Code schön übersichtlich werden soll.

Arduino Clean Code Abstraktions Layer

Durch das Refactoring einiger anderer Bauteile der Shot Clock kann ich mich schon an ein paar Layern bedienen. Die grobe Funktion von diesen Layern zeige ich euch hier. Diese Layer haben sich schon mal als sehr hilfreich erwiesen, da ich sie in allen Komponenten der Shot Clock verwenden kann und die Integration sehr leicht von der Hand geht.

WiFiHandler

Es gibt bereits einen WiFiHandler. Dieser ist in der Lage einen WLAN Access Point zu öffnen und er kümmert sich um das Updaten des Gerätes via OTA (Over-The-Air). Vielleicht sollte ich den demnächst zu “Updater” o. ä. refactorn, da das ja seine Aufgabe ist. In seinem Header File können wir gut erkennen, was damit möglich ist:

class WiFiHandler
{
public:
    WiFiHandler(const char *ssid, const char *password, byte lastIpByte, bool useSerial = false);
    void init();
    void startAP();
    void startUpdateMode();
    void handleOTA();

private:
    bool useSerial;
    const char *ssid;
    const char *password;
    byte lastIpByte;
};

Dieser Layer bietet mit also die Möglichkeit einen Access Point zu starten, den Update Modus zu starten und das OTA Update durchzuführen. Mit diesen Methoden kann der ganze Code, der für die Updates zuständig ist, aus meinem Sketch verschwinden. Die Implementierungsdetails sind durch die Klasse abstrahiert, der Code wird leserlich.

RadioHandler

Ein weiterer Layer, der in allen Komponenten der Shot Clock verwendet wird, ist der RadioHandler. Seine Aufgabe ist es die Funkkommunikation zu ermöglichen. Dafür stellt er Methoden für die Kommunikation bereit. Der RadioHandler ist der Layer, der das RFM69 Funk Modul anspricht. Außerdem hält er einige Konstanten, die für alle Komponenten gleich sind. Die Konstanten definieren das Netzwerk und die Kommunikation. Auch die Definition des Payloads, der für die Kommunikation verwendet wird, ist hier hinterlegt. Zusätzlich gibt er noch ein Paar Informationen, wie z. B. die Node ID des Funkmoduls nach außen. Insgesamt kümmert er sich also um alles, was nah am Funkmodul ist.

//CONSTANTS
#define RADIO_FREQUENCY_IN_HZ     868000000
#define NETWORKID     200
#define FREQUENCY     RF69_868MHZ
#define ENCRYPTKEY    "sampleEncryptKey" 
#define IS_RFM69HCW   true 

//actions
#define START_STOP_CLOCK    1
#define RESET_CLOCK         2
#define HONK                3
#define REGISTER            4
#define START_UPDATE_MODE   6
#define CHANGE_NODE_ID      8
#define DO_NOTHING          9

typedef struct {
        byte    action;
        byte    value;
    } Payload;

class RadioHandler {
    public:
    RadioHandler(uint8_t slaveSelectPin, uint8_t interruptPin, bool isRFM69HW, uint8_t resetPin, uint8_t nodeId, bool useSerial);

    bool send(Payload payload, byte receiver);
    bool sendAction(byte action, byte receiver);
    byte getSenderId();
    Payload read();
    bool messageAvailable();
    void setAddress(uint16_t address);

    byte deviceId;

    private:
    void reset();
    void init(uint8_t frequency, uint8_t nodeId, uint8_t networkId);

    MyRFM69 radio;
    bool useSerial;
    uint8_t resetPin;
    uint8_t slaveSelectPin;
    uint8_t interruptPin;
    bool isRFM69HW;
};

Die wichtigsten Schnittstellen nach außen sind das Auslesen und das Schreiben von Nachrichten. Wie genau das gemacht wird braucht im main Code keiner sehen, wichtig ist nur, dass diese Dinge passieren.

DeviceManager

Ich habe noch einen zusätzlichen Layer spendiert, der DeviceManager heißt. Seine Zuständigkeit ist das Management der Devices. Er kümmert sich zum einen darum, dass sich alle Komponenten gegenseitig kennen. Zum anderen managed er, wer der Master ist, falls mehrere Clocks verwendet werden, oder die Base aktiv ist.

// ids
#define MASTER_ID 1
#define FIRST_CLOCK_ID 2
#define SECOND_CLOCK_ID 3
#define HORN_ID 4
#define INITIAL_CLOCK_ID 5
#define REMOTE_ID 9

// devices
#define DEVICE_BASE 1
#define DEVICE_CLOCK 2
#define DEVICE_HORN 3

typedef struct
{
    bool isAvailable;
    byte id;
} Device;

typedef struct
{
    Device clockOne;
    Device clockTwo;
    Device horn;
} Devices;

class DeviceManager
{
public:
    DeviceManager(RadioHandler *radioHandler, bool useSerial);
    void init(byte deviceType);
    void initMaster(byte deviceType);
    void handleNewDevice(byte deviceType);
    bool deviceAvailable(byte id);
    void registerAtMaster(byte deviceType);

    Devices devices;
    bool isMaster = false;

private:
    RadioHandler *radioHandler;
    bool useSerial;

    void updateAvailableDevices();
    bool setDeviceAdress(byte fromId, byte toId);
};

Dieser Layer abstrahiert damit die gesamte Logik für die Anmeldeprozedur der Geräte. Durch wenige Methoden bekommen alle Geräte automatisch eine Aufgabe, abhängig davon, welche anderen Geräte vorhanden sind.

Honker

Bleibt nur noch ein Layer, der dafür sorgt, dass das Gerät hupt. Dieser Layer ist spezifisch für diese Komponente und kann nicht von den anderen Komponenten übernommen werden. Dieser Layer hat nicht viele Aufgaben und fällt dementsprechend klein aus. Seine einzige Aufgabe ist es, das Horn zum Hupen zu bringen.

class Honker {
    public:
    Honker(uint8_t honkPin, bool useSerial=false);
    void honk();

    private:
    void init();
    
    uint8_t honkPin;
    bool useSerial;
};

Zusammenspiel der Layer im Arduino Clean Code

Alle diese Layer treffen nun im main Code aufeinander. Was dann noch fehlt ist ein Stück Horn spezifische Logik, die vorerst im main File wohnen darf.

#include "constants.h"
#include <WiFiHandler.h>
#include <RadioHandler.h>
#include <DeviceManager.h>
#include <Honker.h>

WiFiHandler wifi = WiFiHandler(WIFI_SSID, WIFI_PW, LAST_IP_BYTE, USESERIAL);
RadioHandler radioHandler = RadioHandler(RFM69_CS, RFM69_IRQ, IS_RFM69HCW, RFM69_RST, NODEID, USESERIAL);
DeviceManager deviceManager = DeviceManager(&radioHandler, USESERIAL);
Honker honker = Honker(HONKPIN, USESERIAL);

void handleIncomingMessage()
{
  if (radioHandler.messageAvailable())
  {
    Payload payload = radioHandler.read();
    switch (payload.action)
    {
    case HONK:
      honker.honk();
      break;
    case START_UPDATE_MODE:
      wifi.startAP();
      wifi.startUpdateMode();
      break;
    default:
      break;
    }
  }
}

void setup()
{
  if (USESERIAL)
    Serial.begin(SERIAL_BAUD);

  deviceManager.init(DEVICE_HORN);
}

void loop()
{
  if (wifi.isUpdateMode)
  {
    wifi.handleOTA();
  }
  else
  {
    handleIncomingMessage();
  }
}

Nach diesen Abstraktionen sieht der Code schon wesentlich übersichtlicher aus. Es ist schnell auf einer groben Ebene ersichtlich, was passiert ist. Die Implementierungsdetails sind eine Ebene nach hinten gerutscht. Der Code ist Cleaner!

Natürlich ist das noch nicht der perfekte Arduino Clean Code. Es gibt immer Dinge die verbessert werden können und sollten. Aber das erste Refactoring ist ein Schritt in die richtige Richtung.

Arduino Clean Code – Refactoring

Zum Abschluss noch ein kleines Zeitraffer eines Großteils des Refactorings. Hier seht ihr die Ausgangssituation, wie ich vorgegangen bin und einen weit fortgeschrittenen Stand (Ein paar Kleinigkeiten habe ich danach noch erledigt).

Dieser Beitrag hat 2 Kommentare

Schreibe einen Kommentar