Industruino LoRaWAN client

with a LoRa radio module on the IDC port

Tom

LoRa is a radio technology for low power long range communication, typically kilometers. It is designed for small data packets and uses certain license-free frequency bands: 433, 868, 915 MHz depending on your region. It can be used peer-to-peer to exchange packets.

LoRaWAN is a protocol on top of the LoRa technology which defines rules for data transmission and formats. Several companies have been building LoRaWAN networks by installing LoRaWAN gateways, so LoRaWAN end nodes can send/receive data to/from a network server, and build applications on that. For example battery-operated location tracking devices.

The Things Network is a pioneer in this field, and its network covers 150 countries, with more than 21k gateways installed. Helium has added the blockchain/crypto hype to the concept, which has enabled it to grow very fast, with over 400k gateways installed globally. Loriot focuses on installing a global network of servers.

In this demo, we will use a LoRa radio module to send data from an Industruino to The Things Network (TTN), more precisely The Things Stack Community edition. TTN is a well established community of makers, with great documentation.

HARDWARE

We use a Dragino LoRa BEE radio module, which is built around the common SX1276 transceiver, with SPI communication. We will use the expansion port of the Industruino (IND.I/O or PROTO) which has all necessary pins available.

The module works on 3.3V so we had to add a voltage regulator on the 5V pin. The Industruino pins operate at 3.3V so they can be connected directly to the module.

From the Industruino:

  • 5V to voltage regulator, 3.3V to module
  • MOSI to MOSI
  • MISO to MISO
  • SCLK to SCLK
  • D10 to NSS (chip select)
  • D5 to DIO0
  • D7 to DIO1
We can leave RST and DIO2 disconnected on the module; DIO2 is only used for FSK, not for LoRa. We could use D4 or D6 for these, but i preferred not to, as we might use them later as chip selects for FRAM and SD if we ever design an Industruino LORA module. D0/D1 are used for the RS485, and D2/D3 for I2C (several baseboard functions).

LIBRARY

We use the Arduino LMIC library, derived from LMIC, a LoRaWAN-MAC-in-C implementation by IBM. It is well documented and works with the LoRa BEE module.
We need to edit one configuration file: project_config/lmic_project_config.h
#define CFG_as923 1
#define CFG_sx1276_radio 1
#define LMIC_PRINTF_TO SerialUSB
#define LMIC_DEBUG_LEVEL 2 /* 0, 1, or 2 */
#define LMIC_FAILURE_TO SerialUSB

The first line is the Frequency Plan specific to your location, for me it is AS923, the standard for Hong Kong (and several other Asian countries). Change this to EU868 or US915 or ... as needed.

The debug level of 2 is the highest, probably good to start with.

For the next step you will need to decide which LoRaWAN network you want to join. We will use The Things Network.

THE THINGS NETWORK

You will need to create an account for the console, to be able to create an application and a device, our LoRaWAN end node (Industruino). Just follow the TTN documentation. Choose OTAA, class A, and generate a DevEUI. AppEUI can be all zeros. Generate AppKey and register the device.

You can check if there are any TTN LoRaWAN gateways in your neighbourhood with this tool. If none, then you probably need to install your own gateway, as i did. I bought a Dragino LPS8 indoor gateway, updated its firmware, and configured it as a TTN Basics Station as recommended by TTN.

Let's now look at the sketch for the Industruino. We can start with the library example ttn-otaa.ino 

We just need to insert our APPEUI, DEVEUI, APPKEY that we got in the TTN console for our device.

And we also need to define which pins we are using, in the lmic_pinmap.

I have modified this example sketch to include feedback on the Industruino LCD, and instead of the 'hello world' string, send the uptime (millis) as 4 bytes of a uint32_t.

You can check in your TTN console if the data are arriving, and you can create data formatters there to decode your incoming data (uplink). You can also send data to your device (downlink) in the console, and below sketch will print the received bytes in the Serial Monitor.

TTN provides an MQTT integration, which means that you can subscribe to the TTN MQTT broker to receive LoRaWAN data, and also use MQTT for downlink.   

/*******************************************************************************
   Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
   Copyright (c) 2018 Terry Moore, MCCI
   modified by Tom Tobback for Industruino, Dec 2021

   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.

 *******************************************************************************/

// Industruino LCD
#include <UC1701.h>
static UC1701 lcd;
#define LCD_PIN 26

// LoRaWAN from https://github.com/mcci-catena/arduino-lmic
#include <lmic.h>
#include <hal/hal.h>
#include <SPI.h>
/*
  TT: for this library to work, we need to add a few parameters in project_config/lmic_project_config.h
  #define CFG_as923 1
  #define CFG_sx1276_radio 1
  #define LMIC_PRINTF_TO SerialUSB
  #define LMIC_DEBUG_LEVEL 2
  #define LMIC_FAILURE_TO SerialUSB
*/

// 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] = { 0, 0, 0, 0, 0, 0, 0, 0 }; // TTN suggests all zeros
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] = { 0x4E, 0x8F, 0x04, 0xD0, 0x7E, 0xD5, 0xB3, 0x70 };  // TTN DevEUI reverse - REPLACE
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] = { xxxxxxxxxx }; // TTN AppKey - REPLACE
void os_getDevKey (u1_t* buf) {
  memcpy_P(buf, APPKEY, 16);
}

static osjob_t sendjob;

// Schedule TX every this many seconds (might become longer due to duty
// cycle limitations).
const unsigned TX_INTERVAL = 180;

// INDUSTRUINO Pin mapping
const lmic_pinmap lmic_pins = {
  .nss = 10,                // industruino IDC: CS lora
  .rxtx = LMIC_UNUSED_PIN,
  .rst = LMIC_UNUSED_PIN,   // industruino IDC: no pins left, leave disconnected
  .dio = {5, 7, LMIC_UNUSED_PIN}, // industruino IDC: no pins left, we don't need FSK
};

// helper function for lorawan
void printHex2(unsigned v) {
  v &= 0xff;
  if (v < 16)
    SerialUSB.print('0');
  SerialUSB.print(v, HEX);
}

// event handler for lorawan
void onEvent (ev_t ev) {
  SerialUSB.print(os_getTime());
  SerialUSB.print(": ");
  lcd.setCursor(0, 3);
  switch (ev) {
    case EV_SCAN_TIMEOUT:
      SerialUSB.println(F("EV_SCAN_TIMEOUT"));
      lcd.print("EV_SCAN_TIMEOUT       ");
      break;
    case EV_BEACON_FOUND:
      SerialUSB.println(F("EV_BEACON_FOUND"));
      lcd.print("EV_BEACON_FOUND       ");
      break;
    case EV_BEACON_MISSED:
      SerialUSB.println(F("EV_BEACON_MISSED"));
      lcd.print("EV_BEACON_MISSED       ");
      break;
    case EV_BEACON_TRACKED:
      SerialUSB.println(F("EV_BEACON_TRACKED"));
      lcd.print("EV_BEACON_TRACKED       ");
      break;
    case EV_JOINING:
      SerialUSB.println(F("EV_JOINING"));
      lcd.print("EV_JOINING       ");
      break;
    case EV_JOINED:
      SerialUSB.println(F("EV_JOINED"));
      lcd.print("EV_JOINED       ");
      {
        u4_t netid = 0;
        devaddr_t devaddr = 0;
        u1_t nwkKey[16];
        u1_t artKey[16];
        LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey);
        SerialUSB.print("netid: ");
        SerialUSB.println(netid, DEC);
        SerialUSB.print("devaddr: ");
        SerialUSB.println(devaddr, HEX);
        SerialUSB.print("AppSKey: ");
        for (size_t i = 0; i < sizeof(artKey); ++i) {
          if (i != 0)
            SerialUSB.print("-");
          printHex2(artKey[i]);
        }
        SerialUSB.println("");
        SerialUSB.print("NwkSKey: ");
        for (size_t i = 0; i < sizeof(nwkKey); ++i) {
          if (i != 0)
            SerialUSB.print("-");
          printHex2(nwkKey[i]);
        }
        SerialUSB.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:
      ||     SerialUSB.println(F("EV_RFU1"));
      ||     break;
    */
    case EV_JOIN_FAILED:
      SerialUSB.println(F("EV_JOIN_FAILED"));
      lcd.print("EV_JOIN_FAILED       ");
      break;
    case EV_REJOIN_FAILED:
      SerialUSB.println(F("EV_REJOIN_FAILED"));
      lcd.print("EV_REJOIN_FAILED       ");
      break;
    case EV_TXCOMPLETE:
      SerialUSB.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
      lcd.print("EV_TXCOMPLETE       ");
      if (LMIC.txrxFlags & TXRX_ACK)
        SerialUSB.println(F("Received ack"));
      if (LMIC.dataLen) {
        SerialUSB.print(F("Received "));
        SerialUSB.print(LMIC.dataLen);
        SerialUSB.println(F(" bytes of payload"));
        for (int i = 0; i < LMIC.dataLen; i++) {
          SerialUSB.print("0x");
          SerialUSB.print(LMIC.frame[LMIC.dataBeg + i], HEX);
          SerialUSB.print(" ");
        }
        SerialUSB.println();
      }
      // Schedule next transmission
      os_setTimedCallback(&sendjob, os_getTime() + sec2osticks(TX_INTERVAL), do_send);
      break;
    case EV_LOST_TSYNC:
      SerialUSB.println(F("EV_LOST_TSYNC"));
      lcd.print("EV_LOST_TSYNC       ");
      break;
    case EV_RESET:
      SerialUSB.println(F("EV_RESET"));
      lcd.print("EV_RESET       ");
      break;
    case EV_RXCOMPLETE:
      // data received in ping slot
      SerialUSB.println(F("EV_RXCOMPLETE"));
      lcd.print("EV_RXCOMPLETE       ");
      break;
    case EV_LINK_DEAD:
      SerialUSB.println(F("EV_LINK_DEAD"));
      lcd.print("EV_LINK_DEAD       ");
      break;
    case EV_LINK_ALIVE:
      SerialUSB.println(F("EV_LINK_ALIVE"));
      lcd.print("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:
      ||    SerialUSB.println(F("EV_SCAN_FOUND"));
      ||    break;
    */
    case EV_TXSTART:
      SerialUSB.println(F("EV_TXSTART"));
      lcd.print("EV_TXSTART       ");
      break;
    case EV_TXCANCELED:
      SerialUSB.println(F("EV_TXCANCELED"));
      lcd.print("EV_TXCANCELED       ");
      break;
    case EV_RXSTART:
      /* do not print anything -- it wrecks timing */
      break;
    case EV_JOIN_TXCOMPLETE:
      SerialUSB.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept"));
      lcd.print("EV_JOIN_TXCOMPLETE       ");
      break;

    default:
      SerialUSB.print(F("Unknown event: "));
      SerialUSB.println((unsigned) ev);
      break;
  }
  SerialUSB.println();
}

// regular sending job
void do_send(osjob_t* j) {
  // Check if there is not a current TX/RX job running
  if (LMIC.opmode & OP_TXRXPEND) {
    SerialUSB.println(F("OP_TXRXPEND, not sending"));
  } else {
    // Prepare upstream data transmission at the next possible time.
    union long2bytes {
      uint32_t u;
      uint8_t b[4];
    };
    union long2bytes uptime;
    uptime.u = millis();
    LMIC_setTxData2(1, uptime.b, sizeof(uptime.b), 0);
    SerialUSB.print(F("Packet queued: "));
    SerialUSB.println(uptime.u);
    lcd.setCursor(0, 3);
    lcd.print("packet queued:       ");
    lcd.setCursor(0, 4);
    lcd.print(uptime.u);
  }
  // Next TX is scheduled after TX_COMPLETE event.
}

////////////////////////////////////////////////////////////////////////////////

void setup() {
  SerialUSB.begin(115200);
  pinMode(LCD_PIN, OUTPUT);
  digitalWrite(LCD_PIN, HIGH);
  lcd.begin();
  lcd.print("LoRaWAN demo");
  delay(2000); // for Industruino
  SerialUSB.println();
  SerialUSB.println(F(">>> LoRaWAN demo"));

  SerialUSB.print(F(">>> init radio.."));
  lcd.setCursor(0, 1);
  lcd.print("init..");
  // LMIC init
  os_init();
  // Reset the MAC state. Session and pending data transfers will be discarded.
  LMIC_reset();
  SerialUSB.println(F("OK"));
  lcd.print("OK");

  // Start job (sending automatically starts OTAA too)
  SerialUSB.println(F(">>> start sendjob task"));
  do_send(&sendjob);
  SerialUSB.println(F(">>> end of setup"));

}

////////////////////////////////////////////////////////////////////////////////

void loop() {
  os_runloop_once();
  lcd.setCursor(0, 6);
  lcd.print(millis());
}

////////////////////////////////////////////////////////////////////////////////