#Mayke Day 4 – TTN and LoRaWAN – TiL

Tarim and I have been trying to get a LoRaWAN network up and running in Bristol using some of the old Bristol Wireless antenna locations. First step for me was in January when we got together and tried to get a Raspberry Pi Gateway working, with so much #fayle – a subtly broken Pi, a dodgy PSU connector, and I did not know that the Raspberry Pi imager process had changed for Bullseye (you have to set a user in settings, and enable ssh there – you can also put the wifi details in, so it’s handy if you know about it).

Aaanyway for #mayke (now on Mastodon) I’ve been trying for a couple of days to get a TTGO LoRa32 OLED v1.3(?) I bought ages ago to work with the Pi gateway. In summary: argh. there’s so many partial examples around and different naming things and allsorts. But are some notes on what works.

On the Raspberry Pi: 3B+ and a IC880A board that Tarim had – then install Bullseye (with ssh access and wifi and a pi user) and then install using The Things Network (TTN)’s example gateway instructions. All fine. My only daftness here was finding this command: /opt/ttn-station/bin/station -p and assuming (why?) that I was tailing the logs instead of running another instance on top of the systemctl one. Which led to all sorts of weird errors, including ones related to not resetting the device e.g.


 [HAL:INFO] [lgw_spi_close:159] Note: SPI port closed

[lgw_start:764] Failed to setup sx125x radio for RF chain 0



The TTGO was more tricky. There seem to be multiple libraries at multiple levels of abstraction and I wanted one that was Arduino-IDE compatible. It’s really hard to find out what pin mapping you need for these slightly obscure (and superceded) TTGO boards. Then there’s the difference between LoRaWAN Specification 1.0.3 and LoRaWAN Specification 1.1.1. After a while I realised that the MCCI_LoRaWAN_LMIC_library (0.9.2) I was using in the code I had found on the internet was made for 1.0.3 – and then configuring a TTN device was muuch easier with fewer baffling options.

One final self-own by my frenetic searching of forums looking for a bit of code with the right pin mapping for the TTGO

I somehow found some old code (I think it was this – don’t use it, 5 years’ old! – which I think is based on an old version of this, but adapted for the TTGO) which didn’t recognise all the event types from TTN. Updated below, basically adding this in setup()

in setup()
and LMIC_setLinkCheckMode(1) again in case EV_JOINED.
Thank you TTN forum users, and again.

A couple more things – though there are probably more I’ve forgotten.

  1. The gateway was ok to set up on the TTN console, but setting up devices was not – all the names for the different device ids were completely baffling and seem to have changed over time. You also need to set up an application before you can add a device. Two key learnings (a) you can get the little / big endian -ness and the right format for the ids by clicking on the ids themselves in the console, see image below and (b) the Gateway has the JoinEUI you need to set up a device (check the Gateway’s messages for this, see image below).
  2. You HAVE TO hand edit ./project_config/lmic_project_config.h in MCCI_LoRaWAN_LMIC_library on your machine to pick the right region (on a mac, mine was in /Users/[me]/Documents/Arduino/libraries/MCCI_LoRaWAN_LMIC_library/project_config/lmic_project_config.h)

Formatting endianness and chars

LSB is little- MSB is big- and <> switches between chars with the preceding 0x business and without. DEVEUI and APPEUI are little and APPKEY is big.

JoinEUI for devices is in the gateway messages like this:

I somewhat enjoyed the detective work and even read some of TFM. So a happy #mayke for me.

The final code I used:

// MIT License
// https://github.com/gonzalocasas/arduino-uno-dragino-lorawan/blob/master/LICENSE
// Based on examples from https://github.com/matthijskooijman/arduino-lmic
// Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman

#include <Arduino.h>
#include "lmic.h"
#include <hal/hal.h>
#include <SPI.h>

#define LEDPIN 2

unsigned int counter = 0;
char TTN_response[30];

// 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.

// Copy the value from Device EUI from the TTN console in LSB mode.
static const u1_t PROGMEM DEVEUI[8]= { 0x.., 0x.., .. };
void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}

// Copy the value from Application EUI from the TTN console in LSB mode
static const u1_t PROGMEM APPEUI[8]= { 0x.., 0x.., .. };
void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 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. Anyway its in MSB mode.
static const u1_t PROGMEM APPKEY[16] = { 0x.., .. };
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 = 120;

// Pin mapping
const lmic_pinmap lmic_pins = {
    .nss = 18,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 14,
    .dio = {26, 33, 32}  // Pins for the Heltec ESP32 Lora board/ TTGO Lora32 with 3D metal antenna

void do_send(osjob_t* j){
    // Payload to send (uplink)
    static uint8_t message[] = "Hello OTAA!";

    // 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, message, sizeof(message)-1, 0);
        Serial.println(F("Sending uplink packet..."));
        digitalWrite(LEDPIN, HIGH);
    // Next TX is scheduled after TX_COMPLETE event.

void onEvent (ev_t ev) {
    Serial.print(": ");
    Serial.print(": ");
    switch(ev) {
        case EV_SCAN_TIMEOUT:
        case EV_BEACON_FOUND:
        case EV_BEACON_MISSED:
        case EV_BEACON_TRACKED:
        case EV_JOIN_FAILED:
        case EV_REJOIN_FAILED:
        case EV_LOST_TSYNC:
        case EV_RESET:
        case EV_RXCOMPLETE:
            // data received in ping slot
        case EV_LINK_DEAD:
        case EV_LINK_ALIVE:

        case EV_SCAN_FOUND:
        case EV_TXSTART:
        case EV_TXCANCELED:
        case EV_RXSTART:
            // do not print anything -- it wrecks timing 

        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) {
              int i = 0;
              Serial.print(F("Data Received: "));
              Serial.write(LMIC.frame+LMIC.dataBeg, LMIC.dataLen);

              for ( i = 0 ; i < LMIC.dataLen ; i++ )
                TTN_response[i] = LMIC.frame[LMIC.dataBeg+i];
              TTN_response[i] = 0;


            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
            digitalWrite(LEDPIN, LOW);

            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
        case EV_JOINING:
            Serial.println(F("EV_JOINING: -> Joining..."));

        case EV_JOINED: {


            Serial.println(F("Unknown event"));


void setup() {
    delay(2500);                      // Give time to the serial monitor to pick up

    // Use the Blue pin to signal transmission.

    // LMIC init

    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);
    // Set up the channels used by the Things Network, which corresponds
    // to the defaults of most gateways. Without this, only three base
    // channels from the LoRaWAN specification are used, which certainly
    // works, so it is good for debugging, but can overload those
    // frequencies, so be sure to configure the full frequency range of
    // your network here (unless your network autoconfigures them).
    // Setting up channels should happen after LMIC_setSession, as that
    // configures the minimal channel set.

    LMIC_setupChannel(0, 868100000, DR_RANGE_MAP(DR_SF12, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(1, 868300000, DR_RANGE_MAP(DR_SF11, DR_SF7B), BAND_CENTI);      // g-band
    LMIC_setupChannel(2, 868500000, DR_RANGE_MAP(DR_SF10, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(3, 867100000, DR_RANGE_MAP(DR_SF9, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(4, 867300000, DR_RANGE_MAP(DR_SF8, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(5, 867500000, DR_RANGE_MAP(DR_SF7, DR_SF7),  BAND_CENTI);      // g-band
    LMIC_setupChannel(6, 867700000, DR_RANGE_MAP(DR_SF7, DR_SF7),  BAND_CENTI);      // g-band

    // TTN defines an additional channel at 869.525Mhz using SF9 for class B
    // devices' ping slots. LMIC does not have an easy way to define set this
    // frequency and support for class B is spotty and untested, so this
    // frequency is not configured here.

    // Disable link check validation
    //LMIC_setClockError(MAX_CLOCK_ERROR * 1 / 100);

    // TTN uses SF9 for its RX2 window.
    LMIC.dn2Dr = DR_SF9;

    // Set data rate and transmit power for uplink (note: txpow seems to be ignored by the library)

    // Start job
    do_send(&sendjob);     // Will fire up also the join

void loop() {