Was bei allen Nodes, egal ob auf Arduino, ESP, Raspi oder anderen Controllern basierend, gleich ist, ist die Notwendigkeit, die erfassten Daten in das LoRaWAN Netzwerk zu senden. Dazu bedarf es etwas Software, die die Chips (z.B. SX1276 bzw. RFM95W) ansteuern kann.
Hat man nun die entspechende Hardware zusammengebaut und die entsprechende SW aufgesetzt erhält man einen „Rohling“. Eine Node, die sich im The Things Network anmelden und Daten absenden kann. Wir wollen hier immer nur mit TTN zusammenarbeiten, daher nachfolgend keine Betrachtung anderer Dienste.
Als Hardware benötigen wir einen Arduino (z.B. Uno oder kompatibel), das Dragino LoRa-Shield für Europa (868 MHz), eine Antenne und eine Batterie (hier zu Testzwecken erstmal ein Akku) - fertig ist die Hardware.
Das Dragino-Shield beinhaltet auch schon den LoRoa-Chip, Pegelwandler von 5 V auf 3,3 V bzw. 3,3 V Versorgung für den LoRo-Chip sowie einen SMA Antennenanschluß. Bei einigen Lieferanten wird noch eine Antenne beigelegt (siehe Bild). Diese können wir meist getrost wegwerfen, außer wir haben nur wenige zig Meter zum nächsten Gateway. Diese beigelegten Antennen sind oft nicht auf die Frequenz angepasst und müssen evtl. gegen hochwertige Antennen ersetzt werden.
Wie kommen wir nun zu unserem Sketch?
Glücklicherweise hat sich vor längerer Zeit die Firma IBM (IBM Zurich) mit LoRa beschäftigt und ein dazugehöriges Programm geschrieben (The IBM Zurich LMiC codebase). Allerdings ist es recht groß und passt nicht zu unserer Arduino IDE. Aber einige Leute (z.B. Matthis Kooijman, Terry Moore, ChaeHee Won, Frank Rose) haben sich die Mühe gemacht das Programm zu entschlacken und für unsere Arduino-IDE anzupassen.
Mit dieser Bibliothek und dem Example „ttn-abp.ino“ können wir einen Rohling aufsetzen.
Zuerst die Bibliothek installieren
Geht in die IDE-Bibliotheksverwaltung und sucht mit dem Stichwort „LMIC“. Wird euch die Biblitohek „MCCI LoRaWAN LMIC library by IBM, Matthis Kooijman, Terry Moore, ChaeHee Won, Frank Rose ……..“, „Arduino port of the LMIC (LoraWAN-MAC-in-C) framework provided by IBM…..“ angeboten, seid ihr richtig. Installiert die neueste Version.
Solte die Bibliothek nicht gefunden werden, laden wir sie direkt von GitHub herunter.
Die Bibliothek finden wir bei GitHub.
Nach dem Import in die Arduino-IDE können wir schon fast loslegen.
(Stand 07.11.2020: Bibliothek Verson 3.2.0)
Fast?
Ja fast, denn da es weltweit diverse Sendefrequenzen gibt und der LoRa-Chip an unterschiedliche Arduino-Pins angeschlossen werden kann, muß man das noch entsprechend in der Bibliothek ändern bzw. im Sketch eintragen.
Zuerst in der Bibliothek.
In der Bibliothek, nachdem sie installiert ist, müssen wir eintragen, welchen Standard wir benutzen - d.h. EU 868 MHz.
Die Bibliothek ist vermutlich zu finden unter ../Arduino/Sketche/libraries. Das variiert je nach Installation der Arduino-IDE.
Nebenbei, ist die IDE schon aktuell? Wenn nicht erst ein Update durchführen. Auch solltet ihr nicht die IDE aus dem Microsoft Store installieren, sondern den Download von der Arduino Homepage.
Sucht dann im Ordner libraries nach dem Ordner MCCI_LoRaWAN_LMIC_library und darin den Ordner project_config. Öffnet die Datei „lmic_project_config.h“ mit einem Dooppelklick und ändert sie folgendermaßen ab.
// project-specific definitions #define CFG_eu868 1 //#define CFG_us915 1 //#define CFG_au921 1 //#define CFG_as923 1 // #define LMIC_COUNTRY_CODE LMIC_COUNTRY_CODE_JP /* for as923-JP */ //#define CFG_kr920 1 //#define CFG_in866 1 #define CFG_sx1276_radio 1 //#define LMIC_USE_INTERRUPTS
Entfernt die beiden Schrägstriche vor der Zeile #define CFG_eu868 1 und #define CFG_sx1276_radio 1. Alle anderen CFG-Zeilen werden mit zwei Schrägstrichen auskommentiert. Datei speichern - fertig.
Nach einem Update der Bibliothek prüft, ob die Datei „lmic_project_config.h“ nicht überschrieben wurde.
Das waren alle Vorbereitungen in der Bibliothek. Alle weitere Anpassungen werden im Sketch vorgenommen.
Nun laden wir ein Beispiel aus der Bibliothek. Sucht dazu die Datei „ttn-otaa.ino“ und öffnet sie durch Doppelklick in der Arduino-IDE. Da wir diese Datei noch verändern müssen, damit sie für unsere Arduino-Node passt, speichern wir sie erst einmal unter einem eigenen Namen ab, damit das Original erhalten bleibt. Ich nenn das einfach mal „LoRa_node_roh“.
Der Sketch schaut dann so aus:
/******************************************************************************* * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman * Copyright (c) 2018 Terry Moore, MCCI * * Permission is hereby granted, free of charge, to anyone * obtaining a copy of this document and accompanying files, * to do whatever they want with them without any restriction, * including, but not limited to, copying, modification and redistribution. * NO WARRANTY OF ANY KIND IS PROVIDED. * * This example sends a valid LoRaWAN packet with payload "Hello, * world!", using frequency and encryption settings matching those of * the The Things Network. * * This uses OTAA (Over-the-air activation), where where a DevEUI and * application key is configured, which are used in an over-the-air * activation procedure where a DevAddr and session keys are * assigned/generated for use with all further communication. * * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably * violated by this sketch when left running for longer)! * To use this sketch, first register your application and device with * the things network, to set or generate an AppEUI, DevEUI and AppKey. * Multiple devices can use the same AppEUI, but each device has its own * DevEUI and AppKey. * * Do not forget to define the radio type correctly in * arduino-lmic/project_config/lmic_project_config.h or from your BOARDS.txt. * *******************************************************************************/ #include <lmic.h> #include <hal/hal.h> #include <SPI.h> // // For normal use, we require that you edit the sketch to replace FILLMEIN // with values assigned by the TTN console. However, for regression tests, // we want to be able to compile these scripts. The regression tests define // COMPILE_REGRESSION_TEST, and in that case we define FILLMEIN to a non- // working but innocuous value. // #ifdef COMPILE_REGRESSION_TEST # define FILLMEIN 0 #else # warning "You must replace the values marked FILLMEIN with real values from the TTN control panel!" # define FILLMEIN (#dont edit this, edit the lines that use FILLMEIN) #endif // This EUI must be in little-endian format, so least-significant-byte // first. When copying an EUI from ttnctl output, this means to reverse // the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3, // 0x70. static const u1_t PROGMEM APPEUI[8]={ FILLMEIN }; void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);} // This should also be in little endian format, see above. static const u1_t PROGMEM DEVEUI[8]={ FILLMEIN }; void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);} // This key should be in big endian format (or, since it is not really a // number but a block of memory, endianness does not really apply). In // practice, a key taken from ttnctl can be copied as-is. static const u1_t PROGMEM APPKEY[16] = { FILLMEIN }; void os_getDevKey (u1_t* buf) { memcpy_P(buf, APPKEY, 16);} static uint8_t mydata[] = "Hello, world!"; static osjob_t sendjob; // Schedule TX every this many seconds (might become longer due to duty // cycle limitations). const unsigned TX_INTERVAL = 60; // Pin mapping const lmic_pinmap lmic_pins = { .nss = 6, .rxtx = LMIC_UNUSED_PIN, .rst = 5, .dio = {2, 3, 4}, }; void onEvent (ev_t ev) { Serial.print(os_getTime()); Serial.print(": "); switch(ev) { case EV_SCAN_TIMEOUT: Serial.println(F("EV_SCAN_TIMEOUT")); break; case EV_BEACON_FOUND: Serial.println(F("EV_BEACON_FOUND")); break; case EV_BEACON_MISSED: Serial.println(F("EV_BEACON_MISSED")); break; case EV_BEACON_TRACKED: Serial.println(F("EV_BEACON_TRACKED")); break; case EV_JOINING: Serial.println(F("EV_JOINING")); break; case EV_JOINED: Serial.println(F("EV_JOINED")); { u4_t netid = 0; devaddr_t devaddr = 0; u1_t nwkKey[16]; u1_t artKey[16]; LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey); Serial.print("netid: "); Serial.println(netid, DEC); Serial.print("devaddr: "); Serial.println(devaddr, HEX); Serial.print("artKey: "); for (size_t i=0; i<sizeof(artKey); ++i) { Serial.print(artKey[i], HEX); } Serial.println(""); Serial.print("nwkKey: "); for (size_t i=0; i<sizeof(nwkKey); ++i) { Serial.print(nwkKey[i], HEX); } Serial.println(""); } // Disable link check validation (automatically enabled // during join, but because slow data rates change max TX // size, we don't use it in this example. LMIC_setLinkCheckMode(0); break; /* || This event is defined but not used in the code. No || point in wasting codespace on it. || || case EV_RFU1: || Serial.println(F("EV_RFU1")); || break; */ case EV_JOIN_FAILED: Serial.println(F("EV_JOIN_FAILED")); break; case EV_REJOIN_FAILED: Serial.println(F("EV_REJOIN_FAILED")); break; case EV_TXCOMPLETE: Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)")); if (LMIC.txrxFlags & TXRX_ACK) Serial.println(F("Received ack")); if (LMIC.dataLen) { Serial.print(F("Received ")); Serial.print(LMIC.dataLen); Serial.println(F(" bytes of payload")); } // Schedule next transmission os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send); break; case EV_LOST_TSYNC: Serial.println(F("EV_LOST_TSYNC")); break; case EV_RESET: Serial.println(F("EV_RESET")); break; case EV_RXCOMPLETE: // data received in ping slot Serial.println(F("EV_RXCOMPLETE")); break; case EV_LINK_DEAD: Serial.println(F("EV_LINK_DEAD")); break; case EV_LINK_ALIVE: Serial.println(F("EV_LINK_ALIVE")); break; /* || This event is defined but not used in the code. No || point in wasting codespace on it. || || case EV_SCAN_FOUND: || Serial.println(F("EV_SCAN_FOUND")); || break; */ case EV_TXSTART: Serial.println(F("EV_TXSTART")); break; default: Serial.print(F("Unknown event: ")); Serial.println((unsigned) ev); break; } } void do_send(osjob_t* j){ // Check if there is not a current TX/RX job running if (LMIC.opmode & OP_TXRXPEND) { Serial.println(F("OP_TXRXPEND, not sending")); } else { // Prepare upstream data transmission at the next possible time. LMIC_setTxData2(1, mydata, sizeof(mydata)-1, 0); Serial.println(F("Packet queued")); } // Next TX is scheduled after TX_COMPLETE event. } void setup() { Serial.begin(9600); Serial.println(F("Starting")); #ifdef VCC_ENABLE // For Pinoccio Scout boards pinMode(VCC_ENABLE, OUTPUT); digitalWrite(VCC_ENABLE, HIGH); delay(1000); #endif // LMIC init os_init(); // Reset the MAC state. Session and pending data transfers will be discarded. LMIC_reset(); LMIC_setLinkCheckMode(0); LMIC.dn2Dr = SF9; LMIC_setDrTxpow(DR_SF7, 14); // Start job (sending automatically starts OTAA too) do_send(&sendjob); } void loop() { os_runloop_once(); }
Wenn man sich den Sketch anschaut, fällt auf, dass die in der Arduino-IDE übliche Reihenfolge nicht eingehalten wurde. Das liegt daran, dass das Programm ursprünglich nicht dafür entwickelt wurde, und bei C-Compilern erst alle Definitionen / Funktionen etc. vorgenommen werden müssen, bevor eine Funktion verwendet werden kann. Bei der Arduino-IDE ist das nicht nötig.
Schauen wir einmal von oben nach unten durch. Ich habe einige wichtige Zeilen / Bereiche gelb hinterlegt.
In den Zeilen 56, 60 und 66 müssen wir irgendwelche Adressen eintragen. Dazu später mehr. Um dies vornehmen zu können, müssen wir vorher dies hier abgearbeitet haben.
In Zeile 69 wird es schon etwas interessanter: Hello World! Das ist der „Datensatz“ der später in den LoRaWAN-Netzwerk gesendet werden soll - natürlich als Test. Später sollen es ja Messwerte von Sensoren sein.
Zeile 74 erklärt sich auch fast von selbst. Wir erinnern uns, die Belegungszeit eines Nodes auf den Funkfrequenzen unterliegt gewissen Einschränkungen. Also wird man nur ab und an senden - hier momentan alle 60 Sekunden.
Zeile 77 bis 82. Etwas ganz Wichtiges. Hiermit definieren wir die Pins, an die der LoRa Chip angeschlossen ist. Da wir ein Dragino-Shield benutzen, ist hier unbedingt einen Anpassung nötig. Ersetzt diese Zeilen mit diesen Zeilen:
const lmic_pinmap lmic_pins = { .nss = 10, .rxtx = LMIC_UNUSED_PIN, .rst = 9, .dio = {2, 6, 7}, };
Die letzte, sehr wichtige Zeile ist die 155.
Hier wird nach erfolgreicher Übermittlung der Daten das nächste Datenpaket fertig gemacht und verschickt. Die entsprechende Funktion erhält die Daten und die Wartezeit bis zur nächsten Übertragung und führt sie dann aus.
Schon hier merken wir, dass für die zeitliche Einteilung einer Datenübertragung die Loop-Schleife nicht verwendet werden kann. Für die Bereitstellung der Daten müssen wir also an anderer Stelle entsprechenden Code einfügen. Dies wäre z.B. bei Zeile 154. Dazu später mehr.
Nehmen wir an, wir hätten das Thema Adressen geklärt und der Sketch würde compiliert werden, dann erhielten wir die Nachricht, dass auf dem Uno nur noch sehr wenig Speicherplatz übrig bliebe. Sollten wir also Sensoren mit etwas umfangreicher Software verwenden, müssten wir Speicherplatz sparen, oder auf einen Mega ausweichen. Etwas Speicher würde frei werden, wenn wir die Kommunikation zum seriellen Monitor entfernen würden. Im richtigen Leben hängt der Node auch nicht am Rechner und wir lesen auch nicht mit. Eine Idee wäre dieses über einen Compiler-Schalter zu bewerkstelligen. Dann könnte man auch gleich die „leeren“ Case Fälle entfernen. Ich habe das mal ganz brutal gelöscht und die wichtigen Passagen für die Erweiterung für Sensoren markiert. Dieser Sketch läßt genau 378 Byte für Variablen übrig.
Hier also der Sketch LoRa „LoRa_node_roh_ohne_COM“
/******************************************************************************* Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman Copyright (c) 2018 Terry Moore, MCCI Permission is hereby granted, free of charge, to anyone obtaining a copy of this document and accompanying files, to do whatever they want with them without any restriction, including, but not limited to, copying, modification and redistribution. NO WARRANTY OF ANY KIND IS PROVIDED. This example sends a valid LoRaWAN packet with payload "Hello, world!", using frequency and encryption settings matching those of the The Things Network. This uses OTAA (Over-the-air activation), where where a DevEUI and application key is configured, which are used in an over-the-air activation procedure where a DevAddr and session keys are assigned/generated for use with all further communication. Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in g1, 0.1% in g2), but not the TTN fair usage policy (which is probably violated by this sketch when left running for longer)! To use this sketch, first register your application and device with the things network, to set or generate an AppEUI, DevEUI and AppKey. Multiple devices can use the same AppEUI, but each device has its own DevEUI and AppKey. Do not forget to define the radio type correctly in arduino-lmic/project_config/lmic_project_config.h or from your BOARDS.txt. *******************************************************************************/ #include <lmic.h> #include <hal/hal.h> #include <SPI.h> // Applcation EUI Reihenfolge LSB MSB static const u1_t PROGMEM APPEUI[8] = { 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx }; void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8); } //Device EUI Reihenfolge LSB MSB static const u1_t PROGMEM DEVEUI[8] = { 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx }; void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8); } // Application Key Achtung: Reihenfolge MSB LSB static const u1_t PROGMEM APPKEY[16] = { 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx, 0xxx }; void os_getDevKey (u1_t* buf) { memcpy_P(buf, APPKEY, 16); } static uint8_t mydata[] = "Hello World!"; static osjob_t sendjob; // Schedule TX every this many seconds (might become longer due to duty // cycle limitations). const unsigned TX_INTERVAL = 60; // Pin Mapping für Dragino LoRa Shield for Arduino const lmic_pinmap lmic_pins = { .nss = 10, .rxtx = LMIC_UNUSED_PIN, .rst = 9, .dio = {2, 6, 7}, }; void onEvent (ev_t ev) { switch (ev) { case EV_JOINED: { u4_t netid = 0; devaddr_t devaddr = 0; u1_t nwkKey[16]; u1_t artKey[16]; LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey); } // Disable link check validation (automatically enabled // during join, but because slow data rates change max TX // size, we don't use it in this example. LMIC_setLinkCheckMode(0); break; case EV_TXCOMPLETE: // Schedule next transmission os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send); break; default: break; } } void do_send(osjob_t* j) { // Check if there is not a current TX/RX job running if (LMIC.opmode & OP_TXRXPEND) { } else { // Prepare upstream data transmission at the next possible time. LMIC_setTxData2(1, mydata, sizeof(mydata) - 1, 0); } // Next TX is scheduled after TX_COMPLETE event. } void setup() { #ifdef VCC_ENABLE // For Pinoccio Scout boards pinMode(VCC_ENABLE, OUTPUT); digitalWrite(VCC_ENABLE, HIGH); delay(1000); #endif // LMIC init os_init(); // Reset the MAC state. Session and pending data transfers will be discarded. LMIC_reset(); // Start job (sending automatically starts OTAA too) do_send(&sendjob); } void loop() { os_runloop_once(); }
Schon etwas kürzer. Dieser Sketch sendet alle 60 Sekunden ein „Hello World!“ ins TTN-Netzwerk bzw. in die TTN-Cloud. Sofern - sofern die nötigen Adressangaben vorgenommen wurde. Die echten Daten habe ich mit xx ersetzt. Denn diese Angaben sind individuell.
Spätestens jetzt müssen wir unbedingt über die Adressen reden. Von daher machen wir einen Sprung zu der Seite TTN Konto und kommen danach wieder zurück.
Wie wir am obigen Listing sehen, müssen wir in den Sketch drei Daten eintragen, die wir aus unserem TTN-Konto auslesen müssen. Die application EUI, den application key und die device address.
Weiter müssen die Daten in einem bestimmten Format eingetragen werden.
{ 0x34, 0x3C, 0x12, 0x27, 0x66, 0x23, 0x45, 0x13 };
(Diese Daten sind willkürlich, real wirds dann gleich.)
Dieses Format kennen wir und es wird oft verwendet. Allerdings ist zu beachten, dass die Bytes in folgender Reihenfolge eingetragen werden:
Application EUI Reihenfolge LSB MSB
Device EUI Reihenfolge LSB MSB
Application Key Achtung: Reihenfolge MSB LSB
Glücklicherweise ermöglicht es uns TTN diese Daten direkt mit copy zu übernehmen und mit paste einzusetzen - ergibt schon keine Tippfehler.
Holen wir uns also die Daten. Zur besseren Verständlichkeit nachfolgend Screenshots einer Anmeldung bei TTN und dem Verauf der Datenabfrage.
In TTN anmelden, die gewünschte Applikation wählen und danach das Device für das wir die Daten benötigen. Auf der Übersichtsseite für das Device finden wir nun die Daten.
Da die Daten zu einer echten Node gehören, sind sie unkenntlich gemacht.
Wir benötigen nun:
Application EUI Reihenfolge LSB MSB ← roter Pfeil
Device EUI Reihenfolge LSB MSB ← blauer Pfeil
Application Key Achtung: Reihenfolge MSB LSB ← grüner Pfeil
Die Daten liegen aber nicht im richtigen Format vor und müssen daher erst korrekt angezeigt werden. Beim Application Key müssen sie sogar erst einmal sichtbar gemacht werden.
Das geht folgendermaßen:
Es gibt vier Steuerbuttons, mit denen man dies alles bewerkstelligen kann.
Haben wir die Daten in den Sketch übertragen, können wir ihn kompilieren und auf unseren Node übertragen.
Sobald der Node anfängt zu senden, meldet er sich erstmalig am System an, erhält eine Systemantwort (OTAA - over the air activation, siehe auch Bild weiter oben: Activation Method: OTAA) und startet dann mit der zyklischen Übertragung der Daten.
Schalten wir in unserem TTN-Konto die Device-Ansicht von Overview auf Data um, können wir dieses Vorgehen beobachten.
Kurz nachdem die Node eingeschaltet ist, läuft der erste Datensatz ein. Mit jedem Datensatz wächst die Liste. Neue Datensätze sind oben, ältere weiter unten.
Der erste Datensatz ist links mit einem gelben Doppelblitz markiert - dem Zeichen für die Aktivierung.
Daneben stehen die bereits bekannten Daten, mit denen man den Node indentifiziert. Klickt man auf das Doppelblitzsymbol öffnet sich die Detailansicht dieses Datensatzes.
Zusätzlich werden die Metadaten der Übertragung angezeigt.
Der zweite Datensatz in diesem Beispiel trägt einen blauen Pfeil, der nach oben zeigt - dies bedeutet Uplink. D.h. vom Node zum Gateway. Diesmal ist die Anzeige anders. Man sieht nur Counter, Port und Payload. Klickt man auf den blauen Pfeil, öffnen sich ebenfalls die Detaildaten.
Metadaten
Betrachtet man die Metadaten, so fällt auf, dass sich die Sendefrequenz geändert hat. Wir erinnern uns, nach jedem Sendevorgang wird der Kanal gewechselt, um das Risiko zu minimieren Daten auf Grund eines belegten Kanals zu verlieren.
Man erkennt auch die ID des Gateways über das die Datenpakete empfangen wurden. Sollten mehrere Gateways im Sendebereich der Node gewesen sein und diese ebenfalls die Daten empfangen haben, so werden auch diese Gateways in den Metadatensätzen aufgelistet.
Weitere Informationen sind u.a. die Empfangszeit, Modulationsart, Spreizungsfaktor, Feldstärke etc.
Was ist nun die Payload?
Das sind nichts anderes als die Daten, die die Node gesendet hat.
In diesem Fall stand im Datensatz (im Gegensatz zu Hello World vom obigen Beispielcode) ein einfaches „Hi“. Die „48“ und „69“ sind nichts anderes als die Ascii Codes der beiden Buchstaben „H“ und „i“.
Natürlich bleiben noch einige Fragen bzw. Aufgaben offen.
Stand: 30.11.2020