#08: LoRa(WAN) node
Voor deze FabSkill heb ik bekeken hoe we LoRa(WAN) kunnen gebruiken voor toekomstige projecten.
Inleiding
LoRa is een draadloos protocol waarmee korte berichten kunnen worden verstuurd. Het belangrijkste doel van LoRa is dat dit op lange afstanden werkt (tot kilometers ver) en zuinig genoeg is dat je een sensor maanden tot jaren op een paar batterijen kan laten werken. Ook wordt LoRa gebruikt om er tekstberichten mee te versturen, bijvoorbeeld met MeshCore.
Een LoRa verbinding kan je opzetten tussen een zender en ontvanger node. De ontvanger stel je in om continu te luisteren naar berichten. Wanneer de zender dan een bericht verstuurd, kan de ontvanger deze uitlezen en verder verwerken.
LoRaWAN is een uitbreiding op LoRa. Het gebruikt dezelfde radio techniek, maar in plaats van dat je zelf een zender/ontvanger opzet kan je aanmelden op een regionaal netwerk. KPN heeft hiervoor landelijke dekking met KPN Things. Dit is gratis tot een klein aantal apparaten. Er zijn ook gratis community alternatieven zoals The Things Network.
Benodigdheden
Een korte shopping list wat we gaan gebruiken:
- Raspberry Pi Pico of ESP32
- LoRa shield met LoRa RF chipset
- 868MHz Antenne
- Arduino/PlatformIO met RadioLib
De Pico kan je elders op deze wiki info vinden hoe we die gebruiken. Bij voorkeur werk ik met PlatformIO.
Hier verder lees je hoe ik aan de slag ben geweest met diverse LoRa shields, antennes en de software.
LoRa shields/PCBs
Waveshare Pico-LoRa-SX126X
Dit shield kan op een Raspberry Pi Pico geklikt worden. Hier zit een SX1262 RF chipset op, die o.a. LoRa kan zenden/ontvangen. Een antenne kan met een u.FL connector worden aangesloten. Tevens kan er een Lithium batterij worden aangesloten.
Deze opstart code heb ik hiervoor gebruikt, waarin de SPI bus en I/O pinnen worden ingesteld:
#include <RadioLib.h>
arduino::MbedSPI SPI1 = {12, 11, 10}; // SX126X LoRa modem
SX1262 radio = new Module(3, 20, 15, 2, SPI1);
void setup() {
Serial.begin(9600);
SPI1.begin();
/* Start Radiolib (zie verder) */
}
Meer info: https://www.waveshare.com/pico-lora-sx1262-868m.htm
Draguino LoRa Shield v1.4
Dit is een iets ouder bord bedoelt voor de Arduino UNO. Echter met de Raspberry Pi Pico adapter kan dit bord ook worden gebruikt.
Dit bord bevat de RFM95 module. Dit is een modulenaam die de SX1276 chipset bevat. Daardoor gebruiken we dus deze driver met RadioLib.
#include <RadioLib.h>
arduino::MbedSPI SPI1 = {12, 11, 10}; // SX126X LoRa modem
SX1276 radio = new Module(17, 4, 3, 2, SPI1); //RFM95 = SX1276
void setup() {
Serial.begin(9600);
SPI1.begin();
/* Start Radiolib (zie verder) */
}
HelTec LoRa 32(V3), ESP32S3 + SX1262
Dit bordje heeft een ESP32S3 en SX1262 aan boord. De ESP32 beschikt over WiFi/Bluetooth, en verder heeft het bordje een klein schermpje.
Deze opstart code heb ik hiervoor gebruikt, waarin de SPI bus en I/O pinnen worden ingesteld:
#include <RadioLib.h>
#include <SPI.h>
SX1262 radio = new Module(8, 14, 12, 13, SPI);
void setup() {
Serial.begin(9600);
SPI.begin(9, 11, 10, 8);
/* Start Radiolib (zie verder) */
}
Meer info: https://heltec.org/project/wifi-lora-32-v3/
LoRa tussen 2 nodes
Ik ben begonnen met LoRa berichten lokaal te communiceren tussen 2 borden. Hiervoor heb ik de Heltec en Waveshare bordjes gebruikt. Hierboven zie je welke I/O configuratie ik heb gebruikt.
Om dan RadioLib op te zetten voor LoRa communicatie moet de chipset worden opgestart. De opstartcode van hieronder is afgeleid van de Receiving Blocking en Transmitting Blocking voorbeelden.
/** Voeg I/O code van jouw shield (zie hierboven) samen met setup() **/
void setup() {
/** Start hier de SPI bus **/
Serial.begin(9600);
// initialize SX1262 with default settings
Serial.print(F("[SX1262] Initializing ... "));
int state = radio.begin();
if (state == RADIOLIB_ERR_NONE) {
Serial.println(F("success!"));
} else {
Serial.print(F("failed, code "));
Serial.println(state);
while (true) { delay(10); }
}
}
Hieronder is een voorbeeld om een bericht te verzenden in combinatie met een MFRC522 RFID kaart lezer.
void loop() {
char pkt[128];
pkt[0] = 0;
if (mfrc522.PICC_IsNewCardPresent() && mfrc522.PICC_ReadCardSerial()) {
Serial.print(F("[SX1262] Transmitting packet ... "));
MFRC522::PICC_Type piccType = mfrc522.PICC_GetType(mfrc522.uid.sak);
snprintf(pkt, sizeof(pkt), "CARD: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %s",
mfrc522.uid.uidByte[0],
mfrc522.uid.uidByte[1],
mfrc522.uid.uidByte[2],
mfrc522.uid.uidByte[3],
mfrc522.uid.uidByte[4],
mfrc522.uid.uidByte[5],
mfrc522.uid.uidByte[6],
mfrc522.uid.uidByte[7],
mfrc522.uid.uidByte[8],
mfrc522.uid.uidByte[9],
reinterpret_cast<const char*>(mfrc522.PICC_GetTypeName(piccType))
);
Serial.println(pkt);
}
if (strlen(pkt)) {
int state = radio.transmit(pkt);
// // you can transmit C-string or Arduino string up to
// // 256 characters long
// String str = "Hello World! #" + String(count++);
// int state = radio.transmit(str);
// // you can also transmit byte array up to 256 bytes long
// /*
// byte byteArr[] = {0x01, 0x23, 0x45, 0x56, 0x78, 0xAB, 0xCD, 0xEF};
// int state = radio.transmit(byteArr, 8);
// */
if (state == RADIOLIB_ERR_NONE) {
// the packet was successfully transmitted
Serial.println(F("success!"));
} else if (state == RADIOLIB_ERR_PACKET_TOO_LONG) {
// the supplied packet was longer than 256 bytes
Serial.println(F("too long!"));
} else if (state == RADIOLIB_ERR_TX_TIMEOUT) {
// timeout occured while transmitting packet
Serial.println(F("timeout!"));
} else {
// some other error occurred
Serial.print(F("failed, code "));
Serial.println(state);
}
}
}
En de ontvangst kant luistert naar berichten, en print deze dan op de seriele bus:
void loop() {
Serial.print(F("[SX1262] Waiting for incoming transmission ... "));
// you can receive data as an Arduino String
String str;
int state = radio.receive(str);
if (state == RADIOLIB_ERR_NONE) {
// packet was successfully received
Serial.println(F("success!"));
// print the data of the packet
Serial.print(F("[SX1262] Data:\t\t\t"));
Serial.println(str);
// print the RSSI (Received Signal Strength Indicator)
// of the last received packet
Serial.print(F("[SX1262] RSSI:\t\t\t"));
Serial.print(radio.getRSSI());
Serial.println(F(" dBm"));
// print the SNR (Signal-to-Noise Ratio)
// of the last received packet
Serial.print(F("[SX1262] SNR:\t\t\t"));
Serial.print(radio.getSNR());
Serial.println(F(" dB"));
// print frequency error
// of the last received packet
Serial.print(F("[SX1262] Frequency error:\t"));
Serial.print(radio.getFrequencyError());
Serial.println(F(" Hz"));
} else if (state == RADIOLIB_ERR_RX_TIMEOUT) {
// timeout occurred while waiting for a packet
Serial.println(F("timeout!"));
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
// packet was received, but is malformed
Serial.println(F("CRC error!"));
} else {
// some other error occurred
Serial.print(F("failed, code "));
Serial.println(state);
}
}
LoRaWAN
Met LoRaWAN is het niet meer nodig om zelf een base station op te zetten om pakketten te ontvangen en door te zetten. Hiervoor kan KPN Things of The Things Network gebruikt worden.
In deze opstelling heb ik LoRaWAN 1.0.4 gebruikt, omdat alleen deze met KPN Things wordt ondersteunt. Er is ook een LoRaWAN 1.1, maar in tegenstelling tot de versienummering is deze ouder. Deze versie heeft ook meer encryptie sleutels nodig, en is dus niet backwards/forwards compatible.
The Things Network heeft een mooie beschrijving over hoe apparaten op een netwerk kunnen joinen: https://www.thethingsnetwork.org/docs/lorawan/end-device-activation/
In het kort: Over The Air Activation (OTAA) vereist dat een apparaat een Join-Request bericht verstuurd. Hierin staat het apparaat zijn DevEUI, de AppEUI voor de applicatie, en een zogenaamde DevNonce. Dit is een willekeurig getal die maar 1x gebruikt kan worden. De server reageert dan met een Join-Accept bericht met diverse parameters over het netwerk. Daarna kan al berichten worden uitgewisseld.
Voor KPN Things is het noodzakelijk om je apparaat eerst te registreren. Hier worden ook deze sleutels afgeleid. Binnen KPN Things heb ik een (freemium) project aangemaakt. Binnen dit project heb ik dan een nieuw apparaat toegevoegd:
Door te kiezen voor “Programmable” apparaat, genereert KPN Things de diverse keys voor mij. Je kan dit ook zelf doen.
Je kan elk apparaat een naam/serie nr geven.
De keys van dit voorbeeld hoef je niet over te typen, want die zijn voor illustratie redenen niet geldig
Vervolgens worden deze sleutels in config.h gezet:
#define RADIOLIB_LORAWAN_JOIN_EUI 0x0059AC0000123456 // << AppEUI // the Device EUI & two keys can be generated on the TTN console #ifndef RADIOLIB_LORAWAN_DEV_EUI // v DevEUI #define RADIOLIB_LORAWAN_DEV_EUI 0x0059AC0000123456 #endif #ifndef RADIOLIB_LORAWAN_APP_KEY // v AppKey #define RADIOLIB_LORAWAN_APP_KEY 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0 #endif #ifndef RADIOLIB_LORAWAN_NWK_KEY // Put your Nwk Key here #define RADIOLIB_LORAWAN_NWK_KEY // << dit veld is opzettelijk leeg #endif
En vervolgens initaliseren we de RadioLib met:
void setup() {
SPI1.begin();
Serial.begin(115200);
while(!Serial);
delay(5000); // Give time to switch to the serial monitor
Serial.println(F("\nSetup ... "));
Serial.println(F("Initialise the radio"));
int16_t state = radio.begin();
debug(state != RADIOLIB_ERR_NONE, F("Initialise radio failed"), state, true);
// Setup the OTAA session information
state = node.beginOTAA(joinEUI, devEUI, nullptr, appKey);
debug(state != RADIOLIB_ERR_NONE, F("Initialise node failed"), state, true);
Serial.println(F("Join ('login') the LoRaWAN Network"));
state = node.activateOTAA();
debug(state != RADIOLIB_LORAWAN_NEW_SESSION, F("Join failed"), state, true);
Serial.println(F("Ready!\n"));
}
Berichten kunnen nu uitgewisseld worden met sendReceive:
// Build payload byte array
uint8_t uplinkPayload[3];
uplinkPayload[0] = 0xCA;
uplinkPayload[1] = 0xFE;
uplinkPayload[2] = 0;
// Perform an uplink
int16_t state = node.sendReceive(uplinkPayload, sizeof(uplinkPayload));
debug(state < RADIOLIB_ERR_NONE, F("Error in sendReceive"), state, false);
// Check if a downlink was received
// (state 0 = no downlink, state 1/2 = downlink in window Rx1/Rx2)
if(state > 0) {
Serial.println(F("Received a downlink"));
} else {
Serial.println(F("No downlink received"));
}
Serial.print(F("Next uplink in "));
Serial.print(uplinkIntervalSeconds);
Serial.println(F(" seconds\n"));
// Wait until next uplink - observing legal & TTN FUP constraints
delay(uplinkIntervalSeconds * 1000UL); // delay needs milli-seconds






