Aufbau kostengünstiger IoT mit NodeMCU und Vertx MQTT

By Gerald Mücke | July 30, 2017

Aufbau kostengünstiger IoT mit NodeMCU und Vertx MQTT

Auf der diesjährigen JCrete-Konferenz habe ich während des Hackdays das NodeMCU-Board kennengelernt, welches eine beeindruckende kostengünstige Alternative zu einem Arduino darstellt. Es verwendet den 80 MHz ESP8266-Chip, der standardmäßig WLAN-Unterstützung bietet. In diesem Artikel möchte ich beschreiben, wie man ein einfaches Temperatur- und Drucksensorgerät baut, das Daten über MQTT an einen Handler sendet, der mit dem reaktiven Vert.x-Framework implementiert wurde. Die Hardware für die Lösung kostet nicht mehr als 15 $.

Der ESP8266 kann mit LUA mit der Standard-Firmware oder wie ein Arduino mit C und der Arduino IDE programmiert werden.

Lösungsübersicht

Die in diesem Artikel beschriebenen Beispiele zeigen, wie man einen Temperatur- und Drucksensor baut, der Sensordaten an einen MQTT-Broker sendet. Das Board wird mit C++ unter Verwendung der Arduino IDE programmiert. Der Beispiel-Broker ist in Java implementiert unter Verwendung der MQTT-Unterstützung von Vert.x und gibt lediglich eingehende Nachrichten aus, was nur als Beispiel dienen soll.

Der NodeMCU wird mit einem BMP280 verbunden, einem Temperatur- und Drucksensor von Bosch. In diesem Beispiel verwende ich das Adafruit BMP280-Board.

Das Board verbindet sich über WLAN mit dem MQTT-Broker und veröffentlicht in regelmäßigen Abständen die Sensordaten über MQTT. Der Broker empfängt die MQTT-Nachrichten und gibt sie aus.

Lösungsübersicht

Preisgestaltung

Der ESP8266-Chip selbst kostet etwa 2 $, das NodeMCU-Entwicklungsboard kann für weniger als 5 $ gekauft werden node mcu Preis.

Das Adafruit BMP280 kostet etwa 10 $. Günstigere Alternativen mit dem gleichen Sensor sind ebenfalls erhältlich, was die Gesamtkosten der Sensorhardware auf 5-10 $ reduziert.

Verdrahtung

Der 3V3 des NodeMCU muss mit dem VIN des Sensors verbunden werden, ebenso die GND-Pins. Um die Standardeinstellungen des NodeMCU und des BMP280-Sensors zu verwenden, verbinde ich den D1-Pin mit dem SDI (was auf anderen Sensorboards oder Spezifikationen SDA genannt werden kann) und den D2-Pin mit dem Clock-Pin SCK (auch SCL genannt).

NodeMCUBMP280
3V3VIN
GNDGND
D1SDI
D2SCK

Steckbrett-Layout.

Sensorprogramm

Der Sensor – bestehend aus dem NodeMCU und dem BMP280 – benötigt ein Programm, um Messungen zu sammeln und über MQTT zu veröffentlichen. Der folgende Code fügt die erforderlichen Bibliotheken hinzu und setzt Parameter, die während der Einrichtung und Ausführung verwendet werden. Für Ihre lokale Umgebung müssen Sie die SSID Ihres WLANs und das WPA/WPA2-Passwort festlegen. Um Daten an den MQTT-Broker zu senden, müssen Sie auch den Hostnamen oder die IP-Adresse des Brokers festlegen. Falls Ihr Broker eine Authentifizierung erfordert, fügen Sie diese ebenfalls hinzu, obwohl der Beispielbroker, den ich später beschreibe, diese nicht erfordert. Schließlich definiert das MQTT-Topic, wo der NodeMCU die Sensordaten veröffentlicht und potenzielle Abonnenten diese empfangen können.

/**********************************************
* DevCon5 GmbH, info@devcon5.ch
* Temperatur und Druck von BMP280 lesen
* und über MQTT veröffentlichen
  **********************************************/

#include <Wire.h>
#include <SPI.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BMP280.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

#define BMP_SCK 13
#define BMP_MISO 12
#define BMP_MOSI 11
#define BMP_CS 10

/* Verwenden Sie nur die folgende Zeile, wenn I2C-Verdrahtung*/
Adafruit_BMP280 bme; // I2C

const char* ssid = "<WLAN-SSID hier>";
const char* pass = "<WLAN-Passwort hier>";

// MQTT-Serverparameter: Server, Port, Benutzer, Passwort und eindeutige Client-ID
const char* mqtt_server = "<Hostname oder IP des MQTT-Brokers hier>";
const int   mqtt_port = 1883;
const char* mqtt_user = "bmp280";
const char* mqtt_pass = "bmp280";
const char *mqtt_topic = "sensordaten";
String mqtt_clientid = "";

WiFiClient espClient;
PubSubClient client(mqtt_server, mqtt_port, espClient);

Während der Einrichtung initialisieren wir den Serial-Port, sodass wir Debug-Informationen über die serielle Verbindung schreiben können. Verwenden Sie den Serial Monitor der Arduino IDE, um die Nachrichten auszugeben.

Mit der Wire-Bibliothek konfigurieren wir, welche Pins des NodeMCU zur Kommunikation mit dem I2C-Bus des BMP280-Moduls verwendet werden. Der erste Parameter ist der Pin für die Daten (D1 von SDI). Der zweite Parameter ist der Pin für das Clock-Signal (D2 von SCK). Nachdem der BMP280 (als bme) initialisiert wurde, richten wir die WLAN-Verbindung ein. Der Beispielcode verwendet WPA/WPA2 mit SSID und Passwort, wie im Header festgelegt.

/**************************
*   S E T U P
    **************************/
    void setup() {

Serial.begin(9600);

//set the input pins for SDA and SCL (I2C)
Wire.begin(D1,D2);
if (!bme.begin()) {  
Serial.println("Could not find a valid BMP280 sensor, check wiring!");
end();
}
setup_wifi();

mqtt_clientid = String(ESP.getChipId());
}

// Mit WLAN-Netzwerk verbinden
void setup_wifi() {
delay(10);

//Überprüfen der Anwesenheit des WLAN-Shields
if(WiFi.status() == WL_NO_SHIELD){
Serial.println("WLAN-Shield nicht vorhanden");
end();
}

while(WiFi.status() != WL_CONNECTED ){
Serial.print("Versuch, eine Verbindung mit SSID herzustellen: ");
Serial.println(ssid);
//Verbindung über WPA/WPA2 herstellen
WiFi.begin(ssid,pass);
//10 Sekunden warten, dann erneut versuchen
delay(10000);
}

Serial.println();
Serial.println("WLAN verbunden");  
Serial.print("IP-Adresse: ");
Serial.print(WiFi.localIP());
Serial.print(" Signalstärke (RSSI):");
Serial.print(WiFi.RSSI());
Serial.println(" dBm");
}
void end(){
while(1);
}

Die Schleife ist ziemlich einfach. Zuerst wird der Verbindungsstatus des Clients überprüft und bei Bedarf neu verbunden. Anschließend wird die Nutzlast mit Hilfe der Sensordaten vom BMP280 erstellt. Die Höhenmessung, die ich hier verwende, basiert auf einem Druck auf Meereshöhe von 1013,25 hPa. Dies kann jedoch je nach tatsächlichen Wetterbedingungen Ihres Standortes variieren. Daher sind die Messungen möglicherweise nicht exakt. Nachdem die Nutzlast erstellt wurde, wird sie mit dem PubSubClient veröffentlicht.

/**************************
*  L O O P
   **************************/
   void loop() {

if (!client.connected()) {
reconnect();
}
client.loop();

String payload = "{";

payload += "\"temp\":";
payload += bme.readTemperature();
payload += ",\"tempUnit\":\"C\"";

payload += ",\"pres\":";
payload += bme.readPressure();
payload += ",\"presUnit\":\"Pa\"";

payload += ",\"alt\":";
payload += bme.readAltitude(1013.25);
payload += ",\"altUnit\":\"m\"";
payload += ",\"altRefPres\":";
payload += 1013.25;

payload += "}";

Serial.println(payload);
if (client.publish(mqtt_topic, (char*) payload.c_str())) {
Serial.println("Veröffentlichung ok");
} else {
Serial.println("Veröffentlichung fehlgeschlagen");
}

// 5 Sekunden warten
delay(5000);
}

// Verbindung mit MQTT-Broker herstellen
void reconnect() {
while (!client.connected()) {
Serial.print("MQTT-Verbindung wird versucht...");
Serial.println(mqtt_clientid);

    if (client.connect(mqtt_clientid.c_str(), mqtt_user, mqtt_pass)) {
      Serial.println("MQTT verbunden");
    } 
    else {
      Serial.print("fehlgeschlagen, rc=");
      Serial.print(client.state());
      Serial.println(" erneuter Versuch in 5 Sekunden");
      // 5 Sekunden warten, bevor erneut versucht wird
      delay(5000);
    }
}
delay(500);
}

MQTT-Broker

Für den Broker verwende ich das Vert.x-Framework, das eine ressourcenschonende, ereignisgesteuerte nicht blockierende I/O-Implementierung für MQTT-Server und -Clients bietet. Es kann daher für eine Reihe von Sensoren auf jeder benutzerdefinierten Hardware, einschließlich des Raspberry PI, angemessen laufen.

Fügen Sie die vertx-mqtt Maven-Abhängigkeit zu Ihrer pom.xml hinzu

<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mqtt-server</artifactId>
<version>3.4.2</version>
</dependency>

Die Initialisierung des Verticle besteht aus der Definition eines Endpoint-Handlers, der ausgeführt wird, wenn sich ein Client verbindet, und einem Aufruf der listen-Methode, die den MQTT-Server in Betrieb nimmt und Clientverbindungen akzeptiert.

Der Endpoint-Handler dieses Beispiels gibt lediglich eingehende Nachrichten aus. Ein vollständiger Broker würde die Abonnementverwaltung und die Zustellung veröffentlichter Nachrichten behandeln.

/**********************************************
* DevCon5 GmbH, info@devcon5.ch
* Vertx MQTT-Broker
  **********************************************/
  package ch.devcon5.mqtt;

import io.netty.handler.codec.mqtt.MqttQoS;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.mqtt.MqttServer;

public class VertxMQTTServer extends AbstractVerticle {

    public static void main(String... args) {

        final Vertx vertx = Vertx.vertx();
        vertx.deployVerticle(new VertxMQTTServer());
    }

    @Override
    public void start(final Future<Void> startFuture) throws Exception {

        final MqttServer mqttServer = MqttServer.create(vertx);
        mqttServer.endpointHandler(endpoint -> {
            System.out.printf("MQTT-Client [%s] Anfrage zur Verbindung, saubere Sitzung = %s %n",
                              endpoint.clientIdentifier(),
                              endpoint.isCleanSession());

            endpoint.publishHandler(message -> {
                final JsonObject msg = message.payload().toJsonObject();

                System.out.printf("Empfangene Nachricht [%s] mit QoS [%s]%n", msg, message.qosLevel());

                //TODO auf die Nachricht reagieren

                if (message.qosLevel() == MqttQoS.AT_LEAST_ONCE) {
                    endpoint.publishAcknowledge(message.messageId());
                } else if (message.qosLevel() == MqttQoS.EXACTLY_ONCE) {
                    endpoint.publishRelease(message.messageId());
                }
            }).publishReleaseHandler(endpoint::publishComplete);
            endpoint.accept(false);

        }).listen(ar -> {
            if (ar.succeeded()) {
                System.out.println("MQTT-Server hört auf Port " + ar.result().actualPort());
                startFuture.complete();
            } else {
                System.out.println("Fehler beim Starten des Servers");
                startFuture.fail(ar.cause());
            }
        });
    }
}

Das Verticle kann mit der Vert.x-Kommandozeile bereitgestellt oder direkt als Java-Anwendung ausgeführt werden.

comments powered by Disqus