1. Einführung
TTN: Das globale Netzwerk für IoT
- The Things Network (TTN) ist ein globales, offenes LoRaWAN-Netzwerk, das es dir ermöglicht, IoT-Geräte wie Sensoren oder Aktoren drahtlos zu verbinden und Daten an zentrale Systeme zu übermitteln. LoRaWAN zeichnet sich durch eine sehr geringe Energieaufnahme und hohe Reichweite aus, weshalb es ideal für IoT-Projekte ist, bei denen Geräte oft in abgelegenen Bereichen betrieben werden.
- Mit TTN kannst du Daten von deinen Nodes über LoRa-Gateways an einen Cloud-Server senden. Dort können die Daten weiterverarbeitet, gespeichert oder an andere Systeme weitergeleitet werden. TTN stellt dafür eine einfache und kostenfreie Plattform bereit, die sowohl für Hobbyisten als auch professionelle IoT-Anwendungen geeignet ist.
Warum Sensornodes für TTN?
TTN bietet mit seiner Kombination aus Reichweite, Energieeffizienz, Kostenfreiheit und Community-Support die perfekte Grundlage für deine Projekte. Egal, ob du deine Umgebung überwachen, smarte Anwendungen entwickeln oder einfach nur erste Erfahrungen mit IoT sammeln möchtest – TTN eröffnet dir eine Welt voller Möglichkeiten.
Ziel dieses Beitrages
- Sensornode für TTN erstellen und integrieren
- Überblick über notwendige Schritte
2. Vorbereitung
Für die Realisierung unseres Vorhabens benötigen wir eine kleine Anzahl von Komponenten, die in hier kurz beschreiben will.
2.1 Benötigte Hardware
- Lora-fähiges Board
Es wird ein Board benötigt, dass einen Lora-Chip verbaut hat. Dazu gehören Heltec Lora Board, oder die TTGo Boards. - Eine zur Region geeignete angepasste Antenne. (868 Mhz für Europa)
- Temperatur/Luftfeuchte Sensor
Ich selbst habe einen SHT31 für dieses Projekt gewählt. Es kann aber auch ein BME-280 o.ä. sein.
Dabei wird der hier vorgestellte Code entsprechend deiner Wahl angepasst werden müssen.
2.2 Software und Accounts
- Arduino-IDE oder VS Code (ich in diesem Projekt VS Code)
- Library
Für mein Board TTGO Lora ESP32 habe ich die lmic-Library gewählt. Um ganz genau zu sein, diese hier:
mcci-catena/MCCI LoRaWAN LMIC library@^5.0.1 - TTN-Account erstellen und vorbereiten
3. Sensornodes für TTN aufbauen
3.1 Hardware-Aufbau
Für die Erstellung der Sensornode habe ich ein TTGO Lora Board mit OLED Display ausgewählt.
Dieses wurde per PIN Header auf eine Lochrasterplatte gesteckt. Damit kann ich mich frei entscheiden, was ich letztlich an das Board stecke, bzw. welche Sensoren ich betreiben möchte. Hier habe ich einen Sensor SHT31, der per I2C – Schnittstelle angeschaltet ist, verwendet. Information zu diesem Sensor habe ich im Beitrag Temperaturmessung mit dem SH3x bereits gegeben.
Um mit Sensor und Stromversorgung flexibel zu sein, habe ich mir 2 Connectoren aufgelötet.
Hier besteht die Möglichkeit über den USB Anschluss mit 5 V zu versorgen oder direkt 5V auf das Board zu legen.
Zur Beschaltung: siehe auch hier im Beispiel bei randomnerdtutorials.com
SHT31
SDA
SCL
VCC
GND
TTGO Board
SDA – PIN 21
SCL – PIN 22
3.3V
GND
Stromversorgung
5 V
GND
TTGO Board
5V
GND
3.2 Software-Konfiguration
Zur Vorbereitung des Programms benötigen wir in VS Code noch die Angabe von Board und Library.
Lege in VS Code ein neues Projekt an. Über die Ameise oder aus der Symbolleiste das Haus.
Mit Klick auf Boards bitte das Board auswählen, welches du in Verwendung hast. Danach mit ok bestätigen.
Mit Klick auf Librarys die LMIC library (aktuell in der Version 5.0.1 auswählen und auch bestätigen.
Die Platform.ini sollte dann so aussehen:
[env:ttgo-lora32-v2]
platform = espressif32
board = ttgo-lora32-v2
framework = arduino
lib_deps =
adafruit/Adafruit SHT31 Library@^2.2.2
mcci-catena/MCCI LoRaWAN LMIC library@^5.0.1
monitor_speed = 115200
Als nächstes muss in der hier gezeigten Datei (lmic_project_config.h) die projektspezifische Definition erfolgen. Kommentiere bitte die Zeilen, so wie hier dargestellt. Der Frequenzplan für Europa ist CFG_eu868 und der Lora_Cip ist der SX1276.
Account für TTN erstellen
Falls noch kein Account bei TheThingsNetwork (TTN) vorhanden ist, bitte unter https://www.thethingsnetwork.org/ registrieren. Es wird nicht zwingend ein eigenes Gateway benötigt. Hier reicht es aus, wenn in der Nähe ein Gateway existiert was die vom SensorNode gesendeten Daten empfängt und an das TTN weiterleitet.
Sensornode in TTN erstellen
In TTN gibt es Applicationen und End devices. Eine Application kann ein oder mehrere End devices enthalten.
Du kannst eine neue Application erstellen, um dein End device darin zu erstellen oder du hast bereits eine Application und nutzt diese für ein End device.
Erstellen einer Application:
Mit Klick auf das „Plus“ öffnet sich nachfolgende Auswahl. Klicke hier auf Add application.
Hier ist nicht allzu viel auszufüllen. Vergib unter Application ID einen aussagekräftigen Namen, bestehend aus Kleinbuchstaben und Bindestrich. Dieser kann später nicht mehr geändert werden. Vergib dann einen sprechenden Namen für deine Application nach freier Wahl und klicke dann auf Create application. Fertig!
Wenn du vorher noch kein End device hattest, ist die Application erst einmal leer.
Mit Klick auf „+ Register end device“ oder mit dem Plus oben rechts, kannst du ein neues End device registrieren.
Selektiere wie unten stehend „Enter end device specifics manually“.
Wähle den Frequenzplan, LoraWan Spezification und Regional Parameters wie angezeigt aus.
Scrolle weiter runter und vergib die Schlüssel, wie hier angezeigt. Die JoinEUI musst du selbst eintippen.
In der Hilfe steht, entweder du hast vom Hersteller eine eindeutige Nummer oder du vergibst selbst eine hexadezimal, also Zahlen von 0 – 9 und A – F. Die DEVEUI und AppKey generierst du mit Klick auf „Generate“.
Jetzt vergibst du noch eine End device ID und mit Klick auf Register end device ist der Sensornode eingerichtet.
Das Ergebnis sieht dann so aus. Im Screenshot habe ich einen anderen End devices Namen vergeben. „test-device“ aus dem oberen Bild und „test-blogpost“ unten sollten gleich heißen.
Damit ist das End device oder der SensorNode für TTN registriert. Was jetzt noch zu tun ist – die markierten Schlüssel sind in den Code zu übernehmen. Achte dabei darauf, dass die lmic-Bibliothek den AppEUI und die DevEUI LSB formatiert und den APPKey MSB formatiert haben möchte. Das erreichst du, indem du auf die Pfeile klickst und dann kopierst.
// OTAA-Schlüssel aus dem angelegten End device in LSB/MSB-Format einfügen
static const u1_t PROGMEM DEVEUI[8] = { 0xE1, 0xCD, 0x06, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 }; //LSB
static const u1_t PROGMEM APPEUI[8] = { 0x05, 0xFB, 0xBF, 0x34, 0x12, 0x03, 0x02, 0x01 }; //LSB
static const u1_t PROGMEM APPKEY[16] = { 0xA2, 0x96, 0xFC, 0x20, 0x62, 0xE4, 0xF3, 0xC2, 0x58, 0x63, 0xEA, 0x00, 0x78, 0xBC, 0xB1, 0xBE }; //MSB
Payload-Decoder in TTN einrichten
Da wir die Daten im Code codieren werden, müssen wir in TTN auch einen Decoder einrichten, damit wir die übertragene Temperatur und Luftfeuchte wieder im Klartext vorliegen haben. Informiere dich gerne auch in meinem Blogbeitrag zur Payload für TTN
function decodeUplink(input) { // Dekodierung der 4 Bytes aus dem Payload var temperature = (input.bytes[0] << 8) | input.bytes[1]; // 16-Bit Temperatur var humidity = (input.bytes[2] << 8) | input.bytes[3]; // 16-Bit Luftfeuchtigkeit // Rückgabe der dekodierten Daten als JSON return { data: { temperature: (temperature/ 100.0) -30, // Falls die Temperatur um Faktor 100 skaliert wurde humidity: humidity / 10.0 // Falls die Luftfeuchtigkeit um Faktor 10 skaliert wurde } }; }
Oben gezeigter Decoder muss, wie unten gezeigt im Uplink eingetragen werden. Dabei hast du 2 Stellen. Links trägst du den Decoder für die Application ein. Heißt, wenn du 2 Nodes in einer Application hast, wird der Decoder für beide Nodes gleichermaßen genutzt. Willst du für jeden Node einen Decoder verwenden, trage ihn bitte rechts mit Klick auf Payload formatters ein.
Das Programm
// VS Code Programm zur Übermittlung von Sensormesswerten an das TheTingsNetwork // Programm wurde mit Unterstützung ChatGPT optimiert // dieser Code steht jedem zur freien Verfügung //Librarys einbinden #include <Arduino.h> #include <lmic.h> #include <hal/hal.h> #include <SPI.h> #include <Wire.h> #include <Adafruit_SHT31.h> //globales Ein-/Ausschalten der Ausgaben über die serielle Schnittstelle #define DEBUG 1 #if DEBUG == 1 #define debug(x) Serial.print(x) #define debugln(x) Serial.println(x) #else #define debug(x) #define debugln(x) #endif //Variablen für die Zeitmesessung (Heater) unsigned long heaterStartMillis = millis(); const unsigned long heaterDuration = 10000; // Heizerdauer: 10 Sekunden bool heater_status = false; // Status ob Heater im SHT31 ein- oder ausgeschaltet ist //Sendeintervall der Sensorwerte an das TTN uint16_t sendInterval = 900; // wie oft werden Sensorwerte gemessen und gesendet in Sekunden // Variablen für Temperatur und Luftfeuchtigkeit float t; int t1; float h; int h1; //Sensor- und Sendejob anlegen Adafruit_SHT31 sht31 = Adafruit_SHT31(); // Sensorobjekt anlegen osjob_t sendjob; // Lora Sendobjekt anlegen // OTAA-Schlüssel aus dem angelegten End device in LSB/MSB-Format einfügen static const u1_t PROGMEM DEVEUI[8] = { 0xE1, 0xCD, 0x06, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 }; //LSB static const u1_t PROGMEM APPEUI[8] = { 0x05, 0xFB, 0xBF, 0x34, 0x12, 0x03, 0x02, 0x01 }; //LSB static const u1_t PROGMEM APPKEY[16] = { 0xA2, 0x96, 0xFC, 0x20, 0x62, 0xE4, 0xF3, 0xC2, 0x58, 0x63, 0xEA, 0x00, 0x78, 0xBC, 0xB1, 0xBE }; //MSB // Pinbelegung für TTGO LoRa32 Chip V2.0 const lmic_pinmap lmic_pins = { .nss = 18, .rxtx = LMIC_UNUSED_PIN, .rst = 14, .dio = {26, 33, 32}, //.spi = {SCK, MISO, MOSI}, }; // Deklaration für Sensor-Abfrageroutine void get_sht3x_data(); // die OTAA-Schlüssel in die Variablem kopieren void os_getArtEui(u1_t *buf) { memcpy_P(buf, APPEUI, 8); } void os_getDevEui(u1_t *buf) { memcpy_P(buf, DEVEUI, 8); } void os_getDevKey(u1_t *buf) { memcpy_P(buf, APPKEY, 16); } //Funktion zum Senden der Daten an das TTN void sendeDaten(osjob_t* j) { if (LMIC.opmode & OP_TXRXPEND) { debugln(F("Übertragung läuft, warte...")); } else { // Messung der Sensordaten debugln("Frage Sensor ab"); get_sht3x_data(); // Daten für die Übertragung vorbereiten debugln("Daten für Payload packen"); uint8_t daten[4]; daten[0] = t1 >> 8; daten[1] = t1 & 0xFF; daten[2] = h1 >> 8; daten[3] = h1 & 0xFF; // Daten senden LMIC_setTxData2(1, daten, sizeof(daten), 0); debugln(F("Paket in Warteschlange")); } } void setup() { Serial.begin(115200); while (!Serial); // Initialisiere SHT31 if (!sht31.begin(0x44)) { debugln(F("SHT31 nicht gefunden!")); while (true); } debugln(F("Starte OTAA...")); // LMIC-Initialisierung os_init(); LMIC_reset(); LMIC_setAdrMode(0); // Deaktiviert ADR LMIC_setDrTxpow(DR_SF12, 14); // Bsp: SF12 für maximale Reichweite, 14 dBm Sendeleistung LMIC_startJoining(); // OTAA-Join starten } void loop() { os_runloop_once(); // Prüfen, ob der Heizer aktiv ist und ob die Heizzeit abgelaufen ist if (heater_status && (millis() - heaterStartMillis >= heaterDuration)) { sht31.heater(false); // Heizer ausschalten heater_status = false; // Status des Heizers zurücksetzen debugln("Heater deaktiviert!"); } } // Routine zur Abfrage der Sensorwerte des SHT31 und Einschalten des Heaters void get_sht3x_data() { t = sht31.readTemperature(); h = sht31.readHumidity(); if (! isnan(t)) { // check if 'is not a number' debug("Temp *C = "); debug(t); debug("\t\t"); // Formatierte Ausgabe } else { debugln("Failed to read temperature"); } if (! isnan(h)) { // check if 'is not a number' debug("Hum. % = "); debugln(h); } else { debugln("Failed to read humidity"); } t1 = (t + 30) * 100; // negative Werte vermeiden und ganzzahlig machen h1 = h * 10; // Luftfeuchtigkeit ganzzahlig machen // Heizer aktivieren für genauere Messwerte Luftfeuchtigkeit if (!heater_status) { // Nur aktivieren, wenn er nicht schon läuft sht31.heater(true); // Heater einschalten heater_status = true; heaterStartMillis = millis(); // Startzeit des Heizers speichern debugln("Heater eingeschaltet!"); } } //Events prüfen und Aktionen starten void onEvent(ev_t ev) { debug(os_getTime()); debugln(": "); switch (ev) { case EV_JOINING: debugln(F("EV_JOINING: Versuche, dem Netzwerk beizutreten")); break; case EV_JOINED: debugln(F("EV_JOINED: Erfolgreich dem Netzwerk beigetreten")); LMIC_setLinkCheckMode(0); os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(1), sendeDaten); //erstes senden break; case EV_TXSTART: debugln(F("EV_TXSTART: Übertragung gestartet")); break; case EV_TXCOMPLETE: debugln(F("EV_TXCOMPLETE: Übertragung abgeschlossen")); if (LMIC.txrxFlags & TXRX_ACK) { debugln(F("ACK erhalten")); } if (LMIC.dataLen) { debug(F("Empfangene Daten: ")); debug(LMIC.dataLen); debugln(F(" Bytes")); } // hier wird die eigentliche Sendung abhängig von sendinterval gestartet os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(sendInterval), sendeDaten); break; case EV_JOIN_FAILED: debugln(F("EV_JOIN_FAILED: Join-Versuch fehlgeschlagen")); break; case EV_REJOIN_FAILED: debugln(F("EV_REJOIN_FAILED: Rejoin-Versuch fehlgeschlagen")); break; default: debug(F("Unbekanntes Ereignis: ")); debug(ev); debug(F(" (Hex: 0x")); Serial.print(ev, HEX); debugln(F(")")); break; } }
Kompiliere den vollständigen Code und lade ihn auf dein Board. Achte bitte darauf, dass wir einen Sender vor uns haben. Sender bitte immer mit angeschlossener Antenne einschalten. Wenn alles geklappt hat, solltest du im TTN einige Sekunden später den ersten Join Versuch sehen können.
So sieht beispielhaft ein Join ins TTN und die Übertragung der Daten aus. Dazu musst du in die serielle Schnittstelle von VSC wechseln.
Starte OTAA…
4056: EV_JOINING: Versuche, dem Netzwerk beizutreten
246809: EV_TXSTART: Übertragung gestartet
567875: EV_JOINED: Erfolgreich dem Netzwerk beigetreten
Frage Sensor ab
Temp *C = 20.30 Hum. % = 50.43
Heater eingeschaltet!
Daten für Payload packen
633242: EV_TXSTART: Übertragung gestartet
Paket in Warteschlange
1013251: EV_TXCOMPLETE: Übertragung abgeschlossen
Heater deaktiviert!
Im TTN siehst du mit Klick auf eine Zeile unter Live Data beispielhaft den vollständigen Request im JSON-Format.
"received_at": "2024-12-08T17:39:11.572418999Z", "uplink_message": { "session_key_id": "xxxxxxxxxx7t4bt2sZriMA==", "f_port": 1, "frm_payload": "E6gB+Q==", "decoded_payload": { "humidity": 50.5, "temperature": 20.32 }, "rx_metadata": [ { "gateway_ids": { "gateway_id": "gateway-xxxxxxxxx01", "eui": "41374E4B30xxxxxxx" }, "time": "2024-12-08T17:39:11.270569Z", "timestamp": 2628052980, "rssi": -89, "channel_rssi": -89, "snr": 8, "location": { "latitude": xx.6853717939413, "longitude": xx.3375034765693, "altitude": 54, "source": "SOURCE_REGISTRY" }, "uplink_token": "Ch4KHAoQZ2F0ZXdheS1wbmdoZ24wMRIIQTdOSzBGSjYQ9M+T5QkaDAi/s9e6BhDVzImuASCgoqihvkw=", "channel_index": 1, "received_at": "2024-12-08T17:39:11.342307728Z" }
Wie solche JSON Daten dekodiert werden und wie du die Nutzlast, also deine Temperatur und andere Sensorwerte weiterverarbeiten kannst, zeige ich dir im Beitrag – TTN Node Integration per Webhook.
Damit sind wir wieder am Ende eines DIYTechAdventures – Sensornode für TTN angekommen. Ich gebe zu, es war umfangreich. Aber nimm dir die Zeit alles in Ruhe zu testen. Falls Fragen sind, ich bin jederzeit per Kommentar erreichbar.