Arduino Uno als SignalK-Quelle

Nachdem wir im letzten Schritt den Raspberry PI mit einem SignalK-Server eingerichtet haben, werden wir in diesem Beitrag einen Sketch für einen Arduino UNO (bzw. Arduino AVR, funktioniert auch mit einen Arduino Mega) vorstellen, der Sensoren am Arduino ausliest und die Werte dann über UDP als JSON an den SignalK-Server schickt. Als Sensoren wählen wir hier einen HC-SR04 Ultraschall, der z.B. als Tankanzeige dienen kann, einen Bosch BMP280 als I2C-Druck-Temperatur-Sensor und wir lesen einen Analog-Eingang ein. Die Werte der Sensoren zeigen wir dann in der bereits auf dem Raspberry PI vorhandenen @Signalk/Instrumentpanel-Webapp an.

Der Arduino UNO (und auch der MEGA) hat sehr wenig Speicher (SRAM und Flash) und einen sehr langsamen Prozessor. Daher ist in diesem Schritt auch herauszufinden, ob er sich überhaupt für das Senden von SignalK-Daten eignet. Wenn sich die Basis erfolgreich darstellen lässt, können wir weitere Features hinzufügen. Falls nicht, müssten wir einen anderen Weg gehen.

Bevor wir anfangen, müssen wir uns die SignalK-Spezifikation etwas genauer anschauen. Es werden dort ein „Full Model“ und ein „Delta Model“ definiert. Das „Full Model“ beschreibt den kompletten Zustand des SignalK-Knotens und ist für den Arduino Uno vermutlich etwas überdimensioniert. Das „Delta Model“ enthält Aktualisierungen für ein „Full Model“. Deswegen ist ein „Delta Model“ für den Arduino Uno, der ein paar Sensoren einliest und an den Server sendet, das passende Model. Das „Full Model“ wird den von dem zentralen Server auf dem Raspberry Pi aufgebaut.

Für die Kommunikation der SignalK-Nachrichten zu dem Server gibt es wieder mehrer Optionen. Eine ist die Verwendung eines HTTP REST API’s. Diese Möglichkeit scheidet wegen der Implementierung eines HTTP-Stacks, die auf dem Knoten benötigt werden würde, wieder aus. Als Alternative können SignalK-Nachrichten auch über TCP oder UDP geschickt werden. Da UDP eine sehr ressourcenschonende Kommunikationsart ist, werden wir sie hier verwenden. Es ist auch möglich, SignalK über eine serielle Schnittstelle (z.b. den USB-zu-Seriell-Konverter auf einem Arduino Uno) zu schicken. Das werden wir hier auch implementieren, da es eine hilfreiche Option für Entwicklung und Debugging ist.

Verwendete Hardware und Software

Details des Sketches

Bevor ihr den Sketch anschauen oder compilieren könnt, müsst ihr ihn von github herunterladen. Ihr könnt ihn entweder als Archiv hier herunterladen: https://github.com/Vehicle-Hacks/Arduino-SignalK/releases/tag/v0.1.0. Oder ihr clont das Repsitory mit git:

git clone -b v0.2.0 https://github.com/Vehicle-Hacks/Arduino-SignalK.git

Ohne „-b v0.1.0“ erhaltet ihr den aktuellen Master, der sich in Details von dem hier vorgestellten Stand unterscheiden kann. Den hier besprochenen Sketch findet ihr dann unter: Arduino-SignalK/Arduino-AVR-SignalK/Arduino-AVR-SignalK.ino.

Grob gesehen müssten wir zyklisch Daten von Sensoren einlesen, diese in JSON-String konvertieren und dann an einen SignalK-Server senden. Üblicherweise wollen wir in einer verteilten Umgebung dazu noch einen Zeitstempel in einer gemeinsamen Zeitbasis senden. Dafür muss der Arduino eine globale Zeit beziehen. Ein dafür sehr verbreitetes Protokoll ist das Network Time Protokoll (NTP), das wir hier auch verwenden werden.

Im Sketch werden ein paar #define’s verwendet, um Funktionalitäten zu aktivieren oder zu deaktivieren. Das geschieht ganz am Anfang des Sketches. Per default sind alle Funktionalitäten aktiviert. Falls z.B: ein bestimmter Sensor nicht vorhanden ist, kann über auskommentieren die entsprechende Funktionalität deaktiviert werden:

#define SERIAL_OUTPUT   // Send SignalK messages on serial output.
#define USE_BMX280      // Read in temperature und pressure from BMx280.
#define USE_TANKLEVEL   // Measure tank level using an HC-SR04 Ultrasonic.

Die erste Zeile enthält des nächsten Abschnitts enthält die MAC-Adresse des Ethernet-Shields. Beim meinem Shield ist sie auf der Unterseite des Shields aufgeklebt – man muss das Shield also abnehmen, um sie zu finden. Die nächsten beiden relevanten Zeilen enthalten die IP-Adresse des SignalK-Servers (der Raspberry PI, der im letzten Artikel eingerichtet wurde) sowie den UDP-Port, an den der Arduino dort die Daten sendert.

byte mac[] = {0x90, 0xA2, 0xDA, 0x11, 0x05, 0xAA};
IPAddress signalkServer(192, 168, 178, 31); //SignalK Server Address
.
.
.
const unsigned int signalkPort = 8375;      // SignalK Port of the SignalK Server for UDP communication

Dann folgt eine Zeile, die die Adresse des NTP-Servers, den wir verwenden, enthält. Wenn eine Fritz!Box als Router vorhanden ist, kann diese als NTP-Server verwendet werden. Andere Router können die Funktion vermutlich auch übernehmen. Also am besten erst mal die Adresse des Routers ausprobieren. Falls das nicht funktioniert, kann auch ein öffentlicher Server wie time.nist.gov verwendet werden. Dies kann aber eine große Latenz erzeugen, die dann in der Loop-Funktion berücksichtigt werden muss:

const char timeServer[] = "fritz.box";

In dem nächsten Abschnitt wird der HC-SR04 konfigueriert. Die ersten beiden Zeilen anthalten die Pins für Trigger und Echoempfang. Die nächste Zeile gibt die maximale Entfernung an, die mit dem HC-SR04 ausgewertet wird. Mit den folgenden beiden Zeilen wird der Tank konfiguriert: „tankFull“ gibt den Abstand zwischen Ultraschall und Flüssigkeitsoberfläche bei vollem Tank an, „tankEmpty“ den Abstand bei leerem Tank.

const int echoSendPin = 2;
const int echoReceivePin = 3;
const int maxDistance = 320;
float tankFull  = 0.05;
float tankEmpty = 0.75;

Beim BMx280 gibt es nichts sinnvolles zu konfigurieren. Der letzte Parameter ist dann der Analog-Pin, der eingelesen wird:

const int analogPin = A0;

Nach den Includes und den Parametern befindet sich ein Abschnitt in dem viele Strings definiert werden:

const char string_0[] PROGMEM = "{"
                           "\"updates\": [{"
                           "\"source\": {"
                           "\"label\": \"Arduino SignalK\","
                           "\"type\": \"signalk\""
                           "},"
                           "\"timestamp\": \"";
.
.
.

Der Grund für die Definition in dieser Form ist, dass die JSON-Strukturen sehr viel Speicher benötigen, der Arduino aber nur wenig davon hat. Besonders die 2 KB SRAM eines Arduino Uno sind sehr begrenzt. Um Speicher zu sparen, können die Strings im deutlich größeren Flash abgelegt werden und werden nur für das Senden ausgelesen. Deswegen wird hier PROGMEM verwendet. Der Code, um die String auszulesen wird später in der Loop-Section beschrieben. Noch ein kurzer Hinweis zur Benamung der Strings: Diese wird uns in der Zukunft bei der Automatisierung helfen.

Nach der Definition der SignalK-Botschaften folgt der eigentliche Code. In der Setup-Funktion werden die Interfaces und Bibliotheken initialisiert – ansonsten befindet sich dort nichts besonderes.

In der loop()-Funktion wird als erstes ein NTP-Request an den Server gesendt (Mehr wird hier dazu nicht beschrieben, da es den Rahmen sprengen würde. Vielleicht in einem separaten Artikel). Da wir einen Moment auf die Antwort warten müssen, nutzen wir die Zeit, um den SignalK-Header mittels strcpy_P() aus dem Flash ins SRAM zu lesen. Anschließend schreiben wir den Header in ein UDP-Paket und – falls konfiguriert – auf die serielle Schnittstelle. Danach lesen wir den Zeitstempel aus der Antwort vom NTP-Server. Falls ein öffentlicher NTP-Server verwendet wird, ist hier evtl. ein delay() erforderlich, damit genug Zeit für die Antwort des NTP-Servers bleibt.

Nachdem der Header mit Zeitstempel geschrieben wurde, werden als nächstes – falls aktiviert – der Tankfüllstand, der BMx280 und der analoge Eingang eingelesen und in das Paket (bzw. auf die serielle Schnittstelle geschrieben). Ähnlich wie beim Header werden dabei die Strings immer mittels strcpy_P() aus dem Flash gelesen. Abschließend wird das UDP-Paket abgeschickt. Diese Funktion läuft permanent ab – auf dem Arduino Uno werden damit wenige HZ Sendefrequenz erreicht.

Hardware-Aufbau

Bevor wir mit den Tests anfangen können, müssen wir noch die Harware aufbauen. Ich habe dafür einen Arduino Uno, ein Ethernet-Shield (Wiznet W5100) und ein Breadboard genommen. Wie das ganze verkabelt wird, zeigt das folgende Fritzing-Bild:

Fritzing-Bild des Breadboard-Aufbaus
Breadboard-Aufbau

Das Ethernet-Shield ist auf den Arduino Uno gesteckt. Hier nochmal die benötigten Verbindungen als Tabelle:

Arduino-PinKomponenten-Pin
5VHC-SR04 VCC / BMP280 VCC
GNDHC-SR04 GND / BMP280 GND
D2HC-SR04 Trigger
D3HC-SR04 Echo
SDABMP280 SDA / SDI
SCLBMP280 SCL / SCK
Verbindungen Arduino mit dem HC-SR04 / BMP280

Testen des Sketches mit seriellem Output

Ab hier benötigen wir den SignalK-Server, so wie wir ihn im ersten Schritt eingerichtet haben. Als erstes testen wir, ob der SignalK-Server valide Daten über die serielle Schnittstelle empfangen kann. Eventuell auftretende Probleme mit der Netzwerk-Konfiguration können wir so zunächst ausschließen. Hier muss die serielle Ausgabe aktiv sein:

#define SERIAL_OUTPUT   // Send SignalK messages on serial output.
#define USE_BMX280      // Read in temperature und pressure from BMx280.
#define USE_TANKLEVEL   // Measure tank level using an HC-SR04 Ultrasonic.

Anschließend den Sketch mit der Arduino-IDE compilieren und auf den Arduino laden. Den SignalK-Server können wir mit der default-Konfiguration starten:

pi@raspberrypi:~/signalk-server-node $ ./bin/signalk-server

Die Oberfläche des SignalK-Servers erreichen wir, indem wir einen Webbrowser öffnen und dort den Port 3000 auf dem Raspberry PI öffnen. Dafür die URL „http://raspberrypi:3000“ in der Adresszeile des Browsers eingeben. Anschließend müssen wir unter „Server->Data Connections“ auf den Butten „Add“ klicken, um eine serielle Verbindung hinzufügen. Als „Input Type“ geben wir SignalK an. „Enabled“ is auf „yes“, „Logging“ auf „no“. Als ID können wir einen Namen vergeben, z.B: „Arduino Serial“. Als „SignalK Source“ wählen wir „Serial“, der Arduino verwendet üblicherweise das Device /dev/ttyACM0. Die „Baud Rate“ im Sketch ist auf 57600 gestellt. Die beiden letzten Felder können wir auf dem default-Wert lasen. Damit sollte die Konfiguration der seriellen Schnittstelle im SignalK-Server wie im folgenden Screenshot dargestellt aussehen:

Screenshot der SignalK-Konfiguration der seriellen Schnittstelle.
Konfiguration der seriellen Schnittstelle im SignalK-Server

Nachdem wir auf „Apply“ geklickt haben, wird die Verbindung übernommen. Es kann evtl. notwendig sein, den SignalK-Server neu zu starten, damit die Schnittstelle wirklich funktioniert. Falls sie funktioniert, wird sie im „Dashboard“ angezeigt und dort sollte die Datenrate > 0 sein. Bei mir sind es 4.6 Pakete/s:

Screenshot des SignalK-Dashboard mit aktiver serieller Schnittstelle.
SignalK-Dashboard mit aktiver und funktionierender serieller Verbindung

Konfigurieren des Instrumentpanel für den Arduino

Wenn wir jetzt unter den „Webapps“ die App „@Signalk/instrumentpanel“ öffnet, könnt ihr euch die vom Arduino gesendeten Signale visualisieren lassen. Nach dem Öffnen ist das Panel zunächst leer. Ihr klickt dann oben links auf den Schraubenschlüssel, um in die Konfiguration zu kommen. Dort nun für alle Signale „Show on grid“ aktivieren. Unter „Edit“ könnt ihr noch von digitaler auf analoge Anzeige umstellen:

Konfiguration des @Signalk/instrumentpanel

Wenn ihr jetzt auf dem Auge-Symbol wieder auf den Ansichtsmodus schaltet, werden euch die entsprechenden Signale angezeigt:

Anzeige der Arduino-Signale im @Signalk/instrumentpanel

Damit sind die ersten Signale von den Sensoren über den SignalK-Server an den Webbrowser geschickt worden. Spielt ein bisschen mit den Sensoren und beobachtet, wie sich die Werte ändern!

Verwenden des Sketches mit UDP-Output

Für die Verwendung als UDP-Sender müssen die IP-Adresse und der Port des SignalK-Servers in dem Sketch konfiguriert werden. Falls ihr die IP-Adresse des Raspberry Pi nicht kennt, könnt ihr euch mit „ip addr“ die Konfiguration aller Netzwerk-Devices anzeigen lassen. Das Ethernet heißt üblicherweise „eth0“. Die im vorhergehenden Schritt aktivierte serielle Ausgabe kann jetzt wieder deaktiviert werden, um die Last zu verringern:

//#define SERIAL_OUTPUT   // Send SignalK messages on serial output.
.
.
.
IPAddress signalkServer(192, 168, 178, 31); //SignalK Server Address
.
.
.
const unsigned int signalkPort = 8375;      // SignalK Port of the SignalK Server for UDP communication

Anschließend den Sketch mit der Arduino-IDE kompilieren und auf den Arduino laden. Als erstes testen wir mit Hilfe des Netzwerk-Tools „netcat“, ob der Arduino korrekte Daten an den Raspberry PI sendet. Dazu starten wir netcat so, dass es auf der IP des Raspberry PI und dem definierten Port auf eine UDP-Verbindung wartet:

nc -l -u 192.168.178.31 8375

Wenn der Sketch auf den Arduino geladen wurde und korrekt funktioniert, sollte jetzt eine Ausgabe ähnlich der folgenden erhalten werden:

pi@raspberrypi:~/signalk-server-node $ nc -l -u 192.168.178.31 8375
{"updates": [{"source": {"label": "Arduino SignalK","type": "signalk"},"timestamp": "2021-01-24T16:8:23","values": [{"path": "tanks.rainwater.currentLevel","value":0.87},{"path": "environment.inside.galley.temperature","value":23.34, "units": "K"},{"path": "environment.inside.galley.pressure","value":95871.00, "units": "Pa"},{"path": "environment.inside.engine.temperature","value":269.30}] }] }{"updates": [{"source": {"label": "Arduino SignalK","type": "signalk"},"timestamp": "2021-01-24T16:8:23","values": [{"path": "tanks.rainwater.currentLevel","value":0.87},{"path": "environment.inside.galley.temperature","value":23.34, "units": "K"},{"path": "environment.inside.galley.pressure","value":95871.00, "units": "Pa"},{"path": "environment.inside.engine.temperature","value":269.30}] }] }{"updates": [{"source": {"label": "Arduino SignalK","type": "signalk"},"timestamp": "2021-01-24T16:8:23","values": [{"path": "tanks.rainwater.currentLevel","value":0.87},{"path": "environment.inside.galley.temperature","value":23.34, "units": "K"},{"path": "environment.inside.galley.pressure","value":95869.00, "units": "Pa"},{"path": "environment.inside.engine.temperature","value":268.95}] }] }{"updates": [{"source": {"label": "Arduino SignalK","type": "signalk"},"timestamp": "2021-01-24T16:8:23","values": [{"path": "tanks.rainwater.currentLevel","value":0.87},{"path": "environment.inside.galley.temperature","value":23.34, "units": "K"},{"path": "environment.inside.galley.pressure","value":95868.00, "units": "Pa"},{"path": "environment.inside.engine.temperature","value":269.30}] }] }
.
.
.

Jetzt kann der SignalK-Server mit der default-Konfiguration gestartet werden:

pi@raspberrypi:~/signalk-server-node $ ./bin/signalk-server

Die Oberfläche wieder mit dem Webbrowser unter „http://raspberrypi:3000“ öffnen. Jetzt müssen wir die UDP-Verbindung des SignalK-Servers konfigurieren. Als „Input Type“ wählen wir wieder SignalK und stellen „Enabled“ aus „yes“ und „Logging“ auf „no“. Die ID können wir frei vergeben, z.B: „Arduino UDP“. Als „Source“ wird „UDP“ ausgewählt, als Port „8375“ (der gleiche wie im Arduino-Sketch). Die Konfiguration der UDP-Schnittstelle sollte damit wie im folgenden Screenshot aussehen:

Konfiguration der UDP-Schnittstelle im SignalK-Server

Wenn ihr auf „Apply“ klickt, wird die Verbindung angelegt. Eventuell kann es notwendig sein, den SignalK-Server neu zu starten, damit die Schnittstelle wirklich funktioniert. Wie im vorhergehenden Abschnitt beschrieben, könnt ihr jetzt über „Webapps“ wieder die App „@SignalK/Instrumentpanel öffnen und konfigurieren, in der ihr dann die vom Arduino gesendeten Sensorwerte ablesen könnt.

Ergebnisse und nächste Schritte

Ein erster Sketch, der alles wichtige enthält, um Sensordaten mit dem Arduino an einen SignalK-Server zu wenden ist damit vorhanden. Die Updaterate ist mit wenigen Hz nicht sehr hoch, sollte für einige Anwendungen aber ausreichen – und wahrscheinlich gibt es hier noch Optimierungspotentiale. Verbessert werden kann außerdem die Genauigkeit der Zeitstempel und eine Fehler beim Verbindungsaufbau lässt sich nur über einen harten Reset des Arduino beheben. Aber der erreichte Stand spricht dafür, den eingeschlagenen Weg weiter zu verfolgen. Wir werden euch auf dem laufenden halten und die Seite immer wieder aktuell halten!