Friday, April 15, 2022

Good times with LMIC and an AdaFruit LoRaWAN Feather M0

Until recently it has been a challenge to get anything like the current time on a Feather acting as a class A LoRaWAN node. The Feather has a built-in real time clock (RTC) but setting its value would require a downlink from your server-side application or an external module, in which case you didn’t need a RTC.

Recent updates to MCCIs version of LMIC and support in the community edition of the Things Stack make it easier to get close to the current time loaded into the Feathers RTC using the LMIC_requestNetworkTime function.

LMIC and the Things Stack now support v1.0.3 of the LoRaWAN specification, which added the ability to ask for the network time from a gateway using the DeviceTimeReq MAC command. If this command is included in an uplink, the gateway will schedule a downlink with the time in a DeviceTimeAns field of the MAC commands in the downlink. In my testing this downlink arrives in the RX windows of the uplink with the request - that is it arrives within 5 or 6 seconds. This might be different on networks with a shorter RX1 delay.

The DeviceTimeReq command is not supported in LoRaWAN versions less than 1.0.3, so your device must be registered on the Things Stack as a 1.0.3 device.

The time is set to the end of the transmission time of the uplink containing the DeviceTimeReq command, in GPS format which is seconds since 1980-01-06T00:00:00Z. To transform this to a UTC epoch-based value, you must:

  • Add the number of seconds since 1970-01-01T00:00:00Z to cover the basic difference between the two epochs.

  • There is something called the GPS-UTC offset, currently sitting at 18 seconds. I think you subtract this number from the value provided by the gateway.

  • The downlink is not received and processed until about RX1 delay + window seconds after the uplink was sent.

Given all the variables here, the time you end up with will be good, but not great. One advantage of that is that even if you have a site with many nodes, they won’t all be completely in sync so if you decide to uplink hourly, on the hour, the gateway still won’t get hit with a lot of uplinks at exactly the same time.

Based upon the usual LMIC demo sketches, here is an example.

 

#include <lmic.h>
#include <hal/hal.h>
#include <RTCZero.h>

RTCZero rtc;

static const uint8_t PROGMEM DEVEUI[8]={ ... };
static const uint8_t PROGMEM APPEUI[8]={ ... };
static const uint8_t PROGMEM APPKEY[16]={ ... };

void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}

// This should also be in little endian format, see above.
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 the TTN console can be copied as-is.
void os_getDevKey (u1_t* buf) {  memcpy_P(buf, APPKEY, 16);}

static uint16_t counter = 0;
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 for Adafruit Feather M0 LoRa, etc.
// /!\ By default Adafruit Feather M0's pin 6 and DIO1 are not connected.
// Please ensure they are connected.
const lmic_pinmap lmic_pins = {
    .nss = 8,
    .rxtx = LMIC_UNUSED_PIN,
    .rst = 4,
    .dio = {3, 6, LMIC_UNUSED_PIN},
    .rxtx_rx_active = 0,
    .rssi_cal = 8,              // LBT cal for the Adafruit Feather M0 LoRa, in dB
    .spi_freq = 8000000,
};

void printHex2(unsigned v) {
    v &= 0xff;
    if (v < 16)
        Serial.print('0');
    Serial.print(v, HEX);
}

void onEvent (ev_t ev) {
    switch(ev) {
        case EV_JOINING:
            Serial.println(F("EV_JOINING"));
            break;
        case EV_JOINED:
            Serial.println(F("EV_JOINED"));
            // 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_JOIN_FAILED:
            Serial.println(F("EV_JOIN_FAILED"));
            break;
        case EV_TXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);
            Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));

            // Schedule next transmission
            os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
            break;
        case EV_RXCOMPLETE:
            digitalWrite(LED_BUILTIN, LOW);
            // data received in ping slot
            Serial.println(F("EV_RXCOMPLETE"));
            break;
        case EV_TXSTART:
            digitalWrite(LED_BUILTIN, HIGH);
            Serial.println(F("EV_TXSTART"));
            break;
        case EV_JOIN_TXCOMPLETE:
            Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept"));
            break;
    }
}


static bool timeOk = false;

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 {
        if ( ! timeOk) {
            Serial.println("Adding DeviceTimeReq MAC command to uplink.");
            LMIC_requestNetworkTime(lmic_request_network_time_cb, 0);
        }

        // Prepare upstream data transmission at the next possible time.
        counter++;
        LMIC_setTxData2(1, (unsigned char *)&counter, sizeof(counter), 0);
        Serial.println("Packet queued");
    }
    // Next TX is scheduled after TX_COMPLETE event.
}

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    digitalWrite(LED_BUILTIN, LOW);

    delay(5000);
    while (! Serial)
        ;
    Serial.begin(115200);
    Serial.println("Starting");

    // LMIC init
    os_init();
    // Reset the MAC state. Session and pending data transfers will be discarded.
    LMIC_reset();

    LMIC_setLinkCheckMode(0);

    rtc.begin();

    // Start job (sending automatically starts OTAA too)
    do_send(&sendjob);
}


static lmic_time_reference_t lmicTimeRef;

void print2digits(int number)
{
    if (number < 10) {
        Serial.print("0");
    }
    Serial.print(number);
}

void printTime()
{
    // Print date...
    Serial.print(rtc.getDay());
    Serial.print("/");
    Serial.print(rtc.getMonth());
    Serial.print("/");
    Serial.print(rtc.getYear());
    Serial.print("   ");

    // ...and time
    print2digits(rtc.getHours());
    Serial.print(":");
    print2digits(rtc.getMinutes());
    Serial.print(":");
    print2digits(rtc.getSeconds());

    Serial.println();
}

void lmic_request_network_time_cb(void * pUserData, int flagSuccess)
{
    if (flagSuccess != 0 && LMIC_getNetworkTimeReference(&lmicTimeRef))
    {
        timeOk = true;

        // GPS time is reported in seconds, and RTCZero also works with seconds in epoch values.
        // 315964800 is the number of seconds between the UTC epoch and the GPS epoch, when GPS started.
        // 18 is the GPS-UTC offset.
        constexpr uint32_t adjustment = 315964800 - 18;
        uint32_t ts = lmicTimeRef.tNetwork + adjustment;

        // Adding rxDelay because the server sets the timestamp value to when it receives the uplink with the
        // time request in it, but doesn't send the response until rxDelay seconds later.
        ts = ts + LMIC.rxDelay;

        // Add a second to cover RX1.
        ts = ts + 1;

        rtc.setEpoch(ts);

        Serial.print("Network time request returned: tlocal = ");
        Serial.print(lmicTimeRef.tLocal);
        Serial.print(", tNetwork = ");
        Serial.println(lmicTimeRef.tNetwork);

        Serial.print("Adjusted to UTS epoch = ");
        Serial.println(ts);
        printTime();
    }
}


void loop() {
    os_runloop_once();
}

 

And the output:

 

Starting
Adding DeviceTimeReq MAC command to uplink.
Packet queued
EV_JOINING
EV_TXSTART
EV_JOINED
EV_TXSTART
Network time request returned: tlocal = 781908, tNetwork = 1334029344
Adjusted to UTS epoch = 1649994132
15/4/22   03:42:12
EV_TXCOMPLETE (includes waiting for RX windows)

No comments: