Now it’s about time to get the Challenger RP2040 LoRa board configured and flashed with some firmware so that we can start seeing some data coming through.
Step 3 – Installing the Arduino LoRa library
To be able to send data up to our newly created end device in the TTN network we need a library that can handle both the LoRa radio module (RFM95W) on the board as well as taking high level data and packaging it in a format that TTN will understand.
Arduino LMIC is such a library and this is what we’ve selected to use here at iLabs. Unfortunately although being a well functioning and supported library it does not work right out of the box together with our LoRa boards. It has a hard coded reference to the SPI bus used which makes it incompatible with any boards that use any spi bus other than the basic one.
To overcome this we have created a fork of the library and added functionality that supports using any available spi port on your board. This is good not only for our board but for any board that uses an spi port other than SPI. The fork is of course fully compatible with the upstream version so anyone can use it.
To use this fork you need to clone our github repo into the libraries folder, this can be done in the following way:
This example was done on a Ubuntu linux machine but should be similar on Windows and Mac machines.
If you already had the MCCI_LoRaWAN_LMIC_library installed you will get an error message that the destination path already exists. In that case you can either delete this directory or if you don’t want to do that rename it to something else.
We hope that our suggested changes can be included in the official version of the library soon but for now we have to keep these changes on our own fork.
Step 4 – Preparing the example applications for the Challenger RP2040 LoRa board.
With the latest commit to the library there is no longer any need to call a set function for what SPI channel to use when communicating with the LoRa module. This is now defined in the hw control structure (lmic_pinmap).
Step 5 – Filling in the authentication keys from TTN
As this is an ABP (Activation By Personalization) example we also need to fill in the authentication keys that we previously generated in the TTN console. In the examples provided by the library this is done in the following way:
// LoRaWAN NwkSKey, network session key
static const PROGMEM u1_t NWKSKEY[16] = { YOUR OWN KEYS };
// LoRaWAN AppSKey, application session key
static const u1_t PROGMEM APPSKEY[16] = { YOUR OWN KEYS };
// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed.
static const u4_t DEVADDR = 0x260BD125;
Step 6 – Setting the appropriate region
In order for the entire system to work together the appropriate region must be configured in the library.
Unfortunately due to how the Arduino build system is designed this configuration needs to be done in the library itself. Here’s a shortlist of the actions needed:
- Navigate to your MCCI_LoRaWAN_LMIC_library folder. It is found in your Arduino/libraries folder.
- Now enter the project_config folder and open the lmic_project_config.h file.
It is a very short file containing a few library global macros the specifies the region used. Make sure it looks like this:
// project-specific definitions
#define CFG_eu868 1
//#define CFG_us915 1
//#define CFG_au915 1
//#define CFG_as923 1
// #define LMIC_COUNTRY_CODE LMIC_COUNTRY_CODE_JP /* for as923-JP; also define CFG_as923 */
//#define CFG_kr920 1
//#define CFG_in866 1
#define CFG_sx1276_radio 1
//#define LMIC_USE_INTERRUPTS
Of course, if you are targeting another region you should select the appropriate region. But for EU it should look like this.
Now what !?
At this point you should be able to simply compile the example you have selected and it will connect and start sending data to TTN. Of course, it is rarely that easy and our example is based on our own internal LoRa infrastructure. But don’t give up and do get in touch with us if you start feeling disheartened.
If you find any errors or have suggestions to this guide please let us know and we will update it.
For online support please join our Discord server at https://discord.gg/6VftEwmtRe
Complete listing
For completeness of this article here’s the entire listing for the program we used to do these tests.
/*******************************************************************************
* The Things Network - ABP Feather
*
* Example of using an Adafruit Feather M0 and DHT22 with a
* single-channel TheThingsNetwork gateway.
*
* This uses ABP (Activation by Personalization), where session keys for
* communication would be assigned/generated by TTN and hard-coded on the device.
*
* Learn Guide: https://learn.adafruit.com/lora-pi
*
* Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
* Copyright (c) 2018 Terry Moore, MCCI
* Copyright (c) 2018 Brent Rubell, Adafruit Industries
*
* 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.
*******************************************************************************/
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
// include the DHT22 Sensor Library
#include "DHT.h"
// DHT digital pin and sensor type
#define DHTPIN 10
#define DHTTYPE DHT22
//
// 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
// LoRaWAN NwkSKey, network session key
static const PROGMEM u1_t NWKSKEY[16] = { FILLMEIN };
// LoRaWAN AppSKey, application session key
static const u1_t PROGMEM APPSKEY[16] = { FILLMEIN };
// LoRaWAN end-device address (DevAddr)
// See http://thethingsnetwork.org/wiki/AddressSpace
// The library converts the address to network byte order as needed.
#ifndef COMPILE_REGRESSION_TEST
static const u4_t DEVADDR = 0x260BD125;
#else
static const u4_t DEVADDR = 0;
#endif
// These callbacks are only used in over-the-air activation, so they are
// left empty here (we cannot leave them out completely unless
// DISABLE_JOIN is set in arduino-lmic/project_config/lmic_project_config.h,
// otherwise the linker will complain).
void os_getArtEui (u1_t* buf) { }
void os_getDevEui (u1_t* buf) { }
void os_getDevKey (u1_t* buf) { }
// payload to send to TTN gateway
static uint8_t payload[5];
static osjob_t sendjob;
// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL = 30;
// Pin mapping for Adafruit Feather M0 LoRa
// /!\ By default Adafruit Feather M0's pin 6 and DIO1 are not connected.
// Please ensure they are connected.
const lmic_pinmap lmic_pins = {
.nss = RFM95W_SS,
.rxtx = LMIC_UNUSED_PIN,
.rst = RFM95W_RST,
.dio = {RFM95W_DIO0, RFM95W_DIO1, RFM95W_DIO2},
.rxtx_rx_active = 0,
.rssi_cal = 8, // LBT cal for the Adafruit Feather M0 LoRa, in dB
.spi = &SPI1,
.spi_freq = 8000000,
};
// init. DHT
DHT dht(DHTPIN, DHTTYPE);
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"));
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.println(F("Received "));
Serial.println(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;
case EV_TXCANCELED:
Serial.println(F("EV_TXCANCELED"));
break;
case EV_RXSTART:
/* do not print anything -- it wrecks timing */
break;
case EV_JOIN_TXCOMPLETE:
Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept"));
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 {
// read the temperature from the DHT22
float temperature = 28.5; //dht.readTemperature();
Serial.print("Temperature: "); Serial.print(temperature);
Serial.println(" *C");
// adjust for the f2sflt16 range (-1 to 1)
temperature = temperature / 100;
// read the humidity from the DHT22
float rHumidity = 77.0; // dht.readHumidity();
Serial.print("%RH ");
Serial.println(rHumidity);
// adjust for the f2sflt16 range (-1 to 1)
rHumidity = rHumidity / 100;
// float -> int
// note: this uses the sflt16 datum (https://github.com/mcci-catena/arduino-lmic#sflt16)
uint16_t payloadTemp = LMIC_f2sflt16(temperature);
// int -> bytes
byte tempLow = lowByte(payloadTemp);
byte tempHigh = highByte(payloadTemp);
// place the bytes into the payload
payload[0] = tempLow;
payload[1] = tempHigh;
// float -> int
uint16_t payloadHumid = LMIC_f2sflt16(rHumidity);
// int -> bytes
byte humidLow = lowByte(payloadHumid);
byte humidHigh = highByte(payloadHumid);
payload[2] = humidLow;
payload[3] = humidHigh;
// prepare upstream data transmission at the next possible time.
// transmit on port 1 (the first parameter); you can use any value from 1 to 223 (others are reserved).
// don't request an ack (the last parameter, if not zero, requests an ack from the network).
// Remember, acks consume a lot of network resources; don't ask for an ack unless you really need it.
LMIC_setTxData2(1, payload, sizeof(payload)-1, 0);
}
// Next TX is scheduled after TX_COMPLETE event.
}
void setup() {
while (!Serial)
delay(10);
Serial.begin(115200);
delay(100);
Serial.println(F("Starting"));
// LMIC init
os_init();
// Reset the MAC state. Session and pending data transfers will be discarded.
LMIC_reset();
// Set static session parameters. Instead of dynamically establishing a session
// by joining the network, precomputed session parameters are be provided.
// On AVR, these values are stored in flash and only copied to RAM
// once. Copy them to a temporary buffer here, LMIC_setSession will
// copy them into a buffer of its own again.
uint8_t appskey[sizeof(APPSKEY)];
uint8_t nwkskey[sizeof(NWKSKEY)];
memcpy_P(appskey, APPSKEY, sizeof(APPSKEY));
memcpy_P(nwkskey, NWKSKEY, sizeof(NWKSKEY));
LMIC_setSession (0x13, DEVADDR, nwkskey, appskey);
// Use a frequency in the g3 which allows 10% duty cycling.
LMIC.freq = 869525000;
// Use a medium spread factor. This can be increased up to SF12 for
// better range, but then, the interval should be (significantly)
// raised to comply with duty cycle limits as well.
LMIC.datarate = DR_SF9;
// Maximum TX power
LMIC.txpow = 27;
// We'll only enable Channel 16 (905.5Mhz) since we're transmitting on a single-channel
// LMIC_enableChannel(16);
// Disable link check validation
// LMIC_setLinkCheckMode(0);
// TTN uses SF9 for its RX2 window.
LMIC.dn2Dr = DR_SF9;
// Set data rate and transmit power for uplink
// LMIC_setDrTxpow(DR_SF7,14);
// Start job
do_send(&sendjob);
}
void loop() {
os_runloop_once();
}
The forked repo is at https://github.com/PontusO/arduino-lmic
I don’t see a pull request upstream to change the library to support the board for the long term (e.g. making it configurable)?
Could you explain why you’d use arduino-lmic instead of the Pico library at https://github.com/ArmDeveloperEcosystem/lorawan-library-for-pico ?
Thanks,
Adrian.
Hey Adrian,
Thanks for checking out our tech articles.
The upstream PR was retracted when no response at all was given from the maintainer and we wanted to make many more changes to the library to make it more user friendly. For now the changes lives in our repo awaiting a few more updates that we want to do.
As far as I can see the lorawan for pico is not an Arduino library but rather a support library for the Raspberry Pico C/C++ SDK. We are planning to merge support for our boards into the official Pico SDK but our resources are limited and we simply do not have the time to do this rn.
The above mentioned repository of PontusO on Github does not exist? ..in the public area?
Are you sure you typed it in correctly? https://github.com/PontusO/arduino-lmic works for me.