Example TTN Code for Heltec HTCC-AB02A with GPS in US915

Figured I would post my implementation and code here in case anyone could use it for reference. It is a Heltec HTCC-AB02A with 1/2AA 1425 rechargeable battery and a u-blox NEO-6 TTY serial GPS for testing the range of my gateway. It sleeps for a few seconds, reads GPS and transmits via ABP the current coordinates.

20210328_154125 (1)

Here is the Arduino sketch that utilizes the TinyGPS+ and Cayenne LPP libraries:

#include <TinyGPS++.h>
#include <softSerial.h>
#include "LoRaWan_APP.h"
#include "loramac/system/timeServer.h"
#include "Arduino.h"
#include <CayenneLPP.h>

/* NOTE: To change Data Rate for non-ADR mode and Spreading Factor edit the following in LoRaWAN_APP.cpp
 *  
 *  #ifdef REGION_US915
 *  int8_t defaultDrForNoAdr = 1;
 * 
 *  0 LoRa: SF10 / 125 kHz 980 0 30 dBm – 2*TXpower
 *  1 LoRa: SF9 / 125 kHz 1760 1 28 dBm
 *  2 LoRa: SF8 / 125 kHz 3125 2 26 dBm
 *  3 LoRa: SF7 / 125 kHz 5470 3 : 9 ….
 *  4 LoRa: SF8 / 500 kHz 12500 10 10 dBm
 *  
 */

/*LoraWan region, select in arduino IDE tools*/
LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;

/*LoraWan Class, Class A and Class C are supported*/
DeviceClass_t  loraWanClass = LORAWAN_CLASS;

/*the application data transmission duty cycle.  value in [ms].*/
uint32_t appTxDutyCycle = 15000;

/*OTAA or ABP*/
bool overTheAirActivation = LORAWAN_NETMODE;

/*ADR enable*/
bool loraWanAdr = LORAWAN_ADR;

/* set LORAWAN_Net_Reserve ON, the node could save the network info to flash, when node reset not need to join again */
bool keepNet = LORAWAN_NET_RESERVE;

/* Indicates if the node is sending confirmed or unconfirmed messages */
bool isTxConfirmed = LORAWAN_UPLINKMODE;

/* Application port */
uint8_t appPort = 2;

uint8_t confirmedNbTrials = 4;

static const uint32_t GPSBaud = 9600;

// The TinyGPS++ object
TinyGPSPlus gps;

// The serial connection to the GPS device
//softSerial ss(GPIO17 /*TX pin*/, GPIO18 /*RX pin*/);


//Set these OTAA parameters to match your app/node in TTN
uint8_t devEui[] = { 0x00, 0x22, 0x0....
uint8_t appEui[] = { 0x70, 0xB3, 0xD.....
uint8_t appKey[] = { 0xC3, 0xAC, 0xE....

uint8_t nwkSKey[] = { 0x12, 0x3A, 0xA....
uint8_t appSKey[] = { 0xB5, 0x47, 0x25....
uint32_t devAddr =  ( uint32_t ) 0x2602.....

uint16_t userChannelsMask[6]={ 0xFF00,0x0000,0x0000,0x0000,0x0000,0x0000 };

static uint8_t counter=0;

///////////////////////////////////////////////////
//Some utilities for going into low power mode
TimerEvent_t sleepTimer;
//Records whether our sleep/low power timer expired
bool sleepTimerExpired;

// This custom version of delay() ensures that the gps object
// is being "fed".
static void smartDelay(unsigned long ms)
{
  unsigned long start = millis();
  do 
  {
    while (Serial1.available())
      gps.encode(Serial1.read());
  } while (millis() - start < ms);
}

static void printFloat(float val, bool valid, int len, int prec)
{
  if (!valid)
  {
    while (len-- > 1)
      Serial.print('*');
    Serial.print(' ');
  }
  else
  {
    Serial.print(val, prec);
    int vi = abs((int)val);
    int flen = prec + (val < 0.0 ? 2 : 1); // . and -
    flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1;
    for (int i=flen; i<len; ++i)
      Serial.print(' ');
  }
  smartDelay(0);
}

static void printInt(unsigned long val, bool valid, int len)
{
  char sz[32] = "*****************";
  if (valid)
    sprintf(sz, "%ld", val);
  sz[len] = 0;
  for (int i=strlen(sz); i<len; ++i)
    sz[i] = ' ';
  if (len > 0) 
    sz[len-1] = ' ';
  Serial.print(sz);
  smartDelay(0);
}

static void printDateTime(TinyGPSDate &d, TinyGPSTime &t)
{
  if (!d.isValid())
  {
    Serial.print(F("********** "));
  }
  else
  {
    char sz[32];
    sprintf(sz, "%02d/%02d/%02d ", d.month(), d.day(), d.year());
    Serial.print(sz);
  }
  
  if (!t.isValid())
  {
    Serial.print(F("******** "));
  }
  else
  {
    char sz[32];
    sprintf(sz, "%02d:%02d:%02d ", t.hour(), t.minute(), t.second());
    Serial.print(sz);
  }

  printInt(d.age(), d.isValid(), 5);
  smartDelay(0);
}

static void printStr(const char *str, int len)
{
  int slen = strlen(str);
  for (int i=0; i<len; ++i)
    Serial.print(i<slen ? str[i] : ' ');
  smartDelay(0);
}


static void wakeUp()
{
  sleepTimerExpired=true;
}

static void lowPowerSleep(uint32_t sleeptime)
{
  sleepTimerExpired=false;
  TimerInit( &sleepTimer, &wakeUp );
  TimerSetValue( &sleepTimer, sleeptime );
  TimerStart( &sleepTimer );
  //Low power handler also gets interrupted by other timers
  //So wait until our timer had expired
  while (!sleepTimerExpired) lowPowerHandler();
  TimerStop( &sleepTimer );
}


void setup()
{
  // Setup Lorawan

  boardInitMcu();
  Serial.begin(115200);
  
  #if(AT_SUPPORT)
    enableAt();
  #endif
    deviceState = DEVICE_STATE_INIT;
    LoRaWAN.ifskipjoin();


  // Setup GPS
  pinMode(Vext, OUTPUT);
  digitalWrite(Vext, LOW);
  delay(500);
  
  Serial.begin(115200);
  Serial1.begin(GPSBaud);

  Serial.println(F("by Mikal Hart"));
  Serial.println();
  Serial.println(F("Sats HDOP  Latitude   Longitude   Fix  Date       Time     Date Alt    Course Speed Card  Distance Course Card  Chars Sentences Checksum"));
  Serial.println(F("           (deg)      (deg)       Age                      Age  (m)    --- from GPS ----  ---- to London  ----  RX    RX        Fail"));
  Serial.println(F("----------------------------------------------------------------------------------------------------------------------------------------"));
}

void loop()
{
  static const double LONDON_LAT = 51.508131, LONDON_LON = -0.128002;

  printInt(gps.satellites.value(), gps.satellites.isValid(), 5);
  printFloat(gps.hdop.hdop(), gps.hdop.isValid(), 6, 1);
  printFloat(gps.location.lat(), gps.location.isValid(), 11, 6);
  printFloat(gps.location.lng(), gps.location.isValid(), 12, 6);
  printInt(gps.location.age(), gps.location.isValid(), 5);
  printDateTime(gps.date, gps.time);
  printFloat(gps.altitude.meters(), gps.altitude.isValid(), 7, 2);
  printFloat(gps.course.deg(), gps.course.isValid(), 7, 2);
  printFloat(gps.speed.kmph(), gps.speed.isValid(), 6, 2);
  printStr(gps.course.isValid() ? TinyGPSPlus::cardinal(gps.course.deg()) : "*** ", 6);

  unsigned long distanceKmToLondon =
    (unsigned long)TinyGPSPlus::distanceBetween(
      gps.location.lat(),
      gps.location.lng(),
      LONDON_LAT, 
      LONDON_LON) / 1000;
  printInt(distanceKmToLondon, gps.location.isValid(), 9);

  double courseToLondon =
    TinyGPSPlus::courseTo(
      gps.location.lat(),
      gps.location.lng(),
      LONDON_LAT, 
      LONDON_LON);

  printFloat(courseToLondon, gps.location.isValid(), 7, 2);

  const char *cardinalToLondon = TinyGPSPlus::cardinal(courseToLondon);

  printStr(gps.location.isValid() ? cardinalToLondon : "*** ", 6);

  printInt(gps.charsProcessed(), true, 6);
  printInt(gps.sentencesWithFix(), true, 10);
  printInt(gps.failedChecksum(), true, 9);
  Serial.println();
  
  smartDelay(1000);

  //Counter is just some dummy data we send for the example
  counter++; 
  
  //In this demo we use a timer to go into low power mode to kill some time.
  //You might be collecting data or doing something more interesting instead.
  lowPowerSleep(15000);  

  switch( deviceState )
  {
    case DEVICE_STATE_INIT:
    {
    #if(AT_SUPPORT)
      getDevParam();
    #endif
      printDevParam();
      LoRaWAN.init(loraWanClass,loraWanRegion);
      
      deviceState = DEVICE_STATE_JOIN;
      break;
    }
    case DEVICE_STATE_JOIN:
    {
      LoRaWAN.join();
      break;
    }
    case DEVICE_STATE_SEND:
    {

    CayenneLPP lpp(LORAWAN_APP_DATA_MAX_SIZE);
    lpp.addGPS(1, gps.location.lat(), gps.location.lng(), gps.altitude.meters());
    //lpp.addGPS(1, -12.34f, 45.56f, 9.01f);
    lpp.addGyrometer(1, -12.34f, 45.56f, 89.01f);
    lpp.getBuffer(), 
    appDataSize = lpp.getSize();
    memcpy(appData,lpp.getBuffer(),appDataSize);
      
      //prepareTxFrame( appPort );
      LoRaWAN.send();
      deviceState = DEVICE_STATE_CYCLE;
      break;
    }
    case DEVICE_STATE_CYCLE:
    {
      // Schedule next packet transmission
      txDutyCycleTime = appTxDutyCycle + randr( 0, APP_TX_DUTYCYCLE_RND );
      LoRaWAN.cycle(txDutyCycleTime);
      deviceState = DEVICE_STATE_SLEEP;
      break;
    }
    case DEVICE_STATE_SLEEP:
    {
      LoRaWAN.sleep();
      break;
    }
    default:
    {
      deviceState = DEVICE_STATE_INIT;
      break;
    }
  }

  if (millis() > 5000 && gps.charsProcessed() < 10)
    Serial.println(F("No GPS data received: check wiring"));
}

2 Likes

Cool stuff - not seen much with the Heltec modules.

To make this even more useful, can you share the names of the Arduino libraries you are using and the connections between the GPS & Heltec modules.

I’m sure it goes without saying that running the module on a 15 second cycle for too long would rapidly exceed the fair use policy.

Hi Nick,

Thanks, for sure. It would quickly exceed fair use if left running, depending on the spreading factor and BW. I only run it shortly while on our walks or on the bike for an hour or so, which looks like it would still be below the 30-second max airtime via the calculator, but close.

20210328_160711

pinout

Arduino libraries required are:

  • Heltec CubeCell Libraries
  • TinyGPS++
  • CayenneLPP

More info is here:

https://heltec-automation-docs.readthedocs.io/en/latest/cubecell/quick_start.html#use-arduino-board-manager

-Rob

1 Like

Here is where the battery is, as it wasn’t clear in my previous posts. The Heltec HTCC-AB02A module has the holder attached to the bottom of the PCB, and it can power both the GPS and module for more than 12 hours without sleep. Plan to test the low power and sleep modes, including integrated GPIO to power off external devices.

battery

1 Like

In a project such as that the main consumer of power will likely be the GPS. The UbloxNeo6M is quite an old GPS now, the more modern 8 series Ublox would use around 1/5 the power of the older 6 series.

Ublox8s are not inexpensive however, a much cheaper option, but with around the same power consumption would be a Quectel L80.

3 Likes

Thanks for the tip on the Quectel L80! Looks like a great system and with similar functionality, but love that it is a single PCB mount package. I just ordered a few to play with. The backup mode is also cool and only needs 7uA:

In backup mode, L80 module stops to acquire and track satellites. UART is not accessible. But the
backed-up memory in RTC domain which contains all the necessary GPS information for quick start-up
and a small amount of user configuration variables is alive. Due to the backed-up memory, EASY
technology is available. The typical consumption in backup mode can be as low as 7uA.

gps

1 Like

Here is some updated code that is tweaked a bit. Turns out you can set the data rate and spreading factor via LoRaWAN.setDataRateForNoADR() as opposed to editing the includes.

#include <TinyGPS++.h>
#include <softSerial.h>
#include "LoRaWan_APP.h"
#include "loramac/system/timeServer.h"
#include "Arduino.h"
#include <CayenneLPP.h>

#define INT_GPIO USER_KEY

const int led = 7;   //GPIO #13

/*LoraWan region, select in arduino IDE tools*/
LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;

/*LoraWan Class, Class A and Class C are supported*/
DeviceClass_t  loraWanClass = LORAWAN_CLASS;

/*the application data transmission duty cycle.  value in [ms].*/
uint32_t appTxDutyCycle = 15000;

/*OTAA or ABP*/
bool overTheAirActivation = LORAWAN_NETMODE;

/*ADR enable*/
bool loraWanAdr = LORAWAN_ADR;

/* set LORAWAN_Net_Reserve ON, the node could save the network info to flash, when node reset not need to join again */
bool keepNet = LORAWAN_NET_RESERVE;

/* Indicates if the node is sending confirmed or unconfirmed messages */
bool isTxConfirmed = LORAWAN_UPLINKMODE;

/* Application port */
uint8_t appPort = 2;

uint8_t confirmedNbTrials = 4;

static const uint32_t GPSBaud = 9600;

// The TinyGPS++ object
TinyGPSPlus gps;

//Set these OTAA parameters to match your app/node in TTN
uint8_t devEui[] = { 0x00, 0x22, .....
uint8_t appEui[] = { 0x70, 0xB3, ......
uint8_t appKey[] = { 0xC3, 0xAC, ....

uint8_t nwkSKey[] = { 0x12, 0x3A, .....
uint8_t appSKey[] = { 0xB5, 0x47, ......
uint32_t devAddr =  ( uint32_t )0x2602.....

uint16_t userChannelsMask[6]={ 0xFF00,0x0000,0x0000,0x0000,0x0000,0x0000 };

static uint8_t counter=0;

///////////////////////////////////////////////////
//Some utilities for going into low power mode
TimerEvent_t sleepTimer;
//Records whether our sleep/low power timer expired
bool sleepTimerExpired;

// This custom version of delay() ensures that the gps object
// is being "fed".
static void smartDelay(unsigned long ms)
{
  unsigned long start = millis();
  do 
  {
    while (Serial1.available())
      gps.encode(Serial1.read());
  } while (millis() - start < ms);
}

static void printFloat(float val, bool valid, int len, int prec)
{
  if (!valid)
  {
    while (len-- > 1)
      Serial.print('*');
    Serial.print(' ');
  }
  else
  {
    Serial.print(val, prec);
    int vi = abs((int)val);
    int flen = prec + (val < 0.0 ? 2 : 1); // . and -
    flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1;
    for (int i=flen; i<len; ++i)
      Serial.print(' ');
  }
  smartDelay(0);
}

static void printInt(unsigned long val, bool valid, int len)
{
  char sz[32] = "*****************";
  if (valid)
    sprintf(sz, "%ld", val);
  sz[len] = 0;
  for (int i=strlen(sz); i<len; ++i)
    sz[i] = ' ';
  if (len > 0) 
    sz[len-1] = ' ';
  Serial.print(sz);
  smartDelay(0);
}

static void printDateTime(TinyGPSDate &d, TinyGPSTime &t)
{
  if (!d.isValid())
  {
    Serial.print(F("********** "));
  }
  else
  {
    char sz[32];
    sprintf(sz, "%02d/%02d/%02d ", d.month(), d.day(), d.year());
    Serial.print(sz);
  }
  
  if (!t.isValid())
  {
    Serial.print(F("******** "));
  }
  else
  {
    char sz[32];
    sprintf(sz, "%02d:%02d:%02d ", t.hour(), t.minute(), t.second());
    Serial.print(sz);
  }

  printInt(d.age(), d.isValid(), 5);
  smartDelay(0);
}

static void printStr(const char *str, int len)
{
  int slen = strlen(str);
  for (int i=0; i<len; ++i)
    Serial.print(i<slen ? str[i] : ' ');
  smartDelay(0);
}


static void wakeUp()
{
  sleepTimerExpired=true;
}

static void lowPowerSleep(uint32_t sleeptime)
{
  sleepTimerExpired=false;
  TimerInit( &sleepTimer, &wakeUp );
  TimerSetValue( &sleepTimer, sleeptime );
  TimerStart( &sleepTimer );
  //Low power handler also gets interrupted by other timers
  //So wait until our timer had expired
  while (!sleepTimerExpired) lowPowerHandler();
  TimerStop( &sleepTimer );
}



void setDelay()
{
  Serial.println("User Button Pressed");
}

void setup()
{
  // Setup interrupt for user button
  pinMode(INT_GPIO,INPUT);
  attachInterrupt(INT_GPIO,setDelay,FALLING);

  // Setup LED
  pinMode(led, OUTPUT);
  digitalWrite(led, HIGH); // sets the digital pin 13 on
  //delay(10000);            // waits for a second
  //digitalWrite(led, LOW);  // sets the digital pin 13 off
  //delay(10000);  
  
  // Setup Lorawan

  boardInitMcu();
  Serial.begin(115200);
  
  #if(AT_SUPPORT)
    enableAt();
  #endif
    deviceState = DEVICE_STATE_INIT;
    LoRaWAN.ifskipjoin();


  // Setup GPS
  pinMode(Vext, OUTPUT);
  digitalWrite(Vext, LOW);
  delay(500);
  
  Serial.begin(115200);
  Serial1.begin(GPSBaud);

  Serial.println(F("by Mikal Hart"));
  Serial.println();
  Serial.println(F("Sats HDOP  Latitude   Longitude   Fix  Date       Time     Date Alt    Course Speed Card  Distance Course Card  Chars Sentences Checksum"));
  Serial.println(F("           (deg)      (deg)       Age                      Age  (m)    --- from GPS ----  ---- to London  ----  RX    RX        Fail"));
  Serial.println(F("----------------------------------------------------------------------------------------------------------------------------------------"));
}

void loop()
{
  static const double LONDON_LAT = 51.508131, LONDON_LON = -0.128002;

  printInt(gps.satellites.value(), gps.satellites.isValid(), 5);
  printFloat(gps.hdop.hdop(), gps.hdop.isValid(), 6, 1);
  printFloat(gps.location.lat(), gps.location.isValid(), 11, 6);
  printFloat(gps.location.lng(), gps.location.isValid(), 12, 6);
  printInt(gps.location.age(), gps.location.isValid(), 5);
  printDateTime(gps.date, gps.time);
  printFloat(gps.altitude.meters(), gps.altitude.isValid(), 7, 2);
  printFloat(gps.course.deg(), gps.course.isValid(), 7, 2);
  printFloat(gps.speed.kmph(), gps.speed.isValid(), 6, 2);
  printStr(gps.course.isValid() ? TinyGPSPlus::cardinal(gps.course.deg()) : "*** ", 6);
 
  printInt(gps.charsProcessed(), true, 6);
  printInt(gps.sentencesWithFix(), true, 10);
  printInt(gps.failedChecksum(), true, 9);
  Serial.println();
  
  smartDelay(1000);

  //Counter is just some dummy data we send for the example
  counter++; 
  
  switch( deviceState )
  {
    case DEVICE_STATE_INIT:
    {
    #if(AT_SUPPORT)
      getDevParam();
    #endif
      printDevParam();
      LoRaWAN.init(loraWanClass,loraWanRegion);

      /* NOTE: setDataRateForNoADR
       *  
       *  #ifdef REGION_US915
       *  int8_t defaultDrForNoAdr = 1;
       * 
       *  0 LoRa: SF9 / 125 kHz
       *  1 LoRa: SF9 / 125 kHz 1760 1 28 dBm
       *  2 LoRa: SF8 / 125 kHz 3125 2 26 dBm
       *  3 LoRa: SF7 / 125 kHz 5470 3 : 9 ….
       *  4 LoRa: SF7 / 125 kHz
       *  
       */
      LoRaWAN.setDataRateForNoADR(1);
      
      deviceState = DEVICE_STATE_JOIN;
      break;
    }
    case DEVICE_STATE_JOIN:
    {
      LoRaWAN.join();
      break;
    }
    case DEVICE_STATE_SEND:
    {

    CayenneLPP lpp(LORAWAN_APP_DATA_MAX_SIZE);
    lpp.addGPS(1, gps.location.lat(), gps.location.lng(), gps.altitude.meters());
    //lpp.addGPS(1, -12.34f, 45.56f, 9.01f);
    lpp.addGyrometer(1, -12.34f, 45.56f, 89.01f);
    lpp.getBuffer(), 
    appDataSize = lpp.getSize();
    memcpy(appData,lpp.getBuffer(),appDataSize);
      
      //prepareTxFrame( appPort );
      LoRaWAN.send();
      deviceState = DEVICE_STATE_CYCLE;
      break;
    }
    case DEVICE_STATE_CYCLE:
    {
      // Schedule next packet transmission
      txDutyCycleTime = appTxDutyCycle + randr( 0, APP_TX_DUTYCYCLE_RND );
      LoRaWAN.cycle(txDutyCycleTime);
      deviceState = DEVICE_STATE_SLEEP;
      break;
    }
    case DEVICE_STATE_SLEEP:
    {
      LoRaWAN.sleep();
      break;
    }
    default:
    {
      deviceState = DEVICE_STATE_INIT;
      break;
    }
  }

  if (millis() > 5000 && gps.charsProcessed() < 10)
    Serial.println(F("No GPS data received: check wiring"));

  // Go to sleep for a while
  lowPowerSleep(10000);  
    
}
1 Like

Did you need to make any changes to make this to work with TTN V3? And if it is not too much trouble, what are your Arduino IDE tools selection for the cubecell?
I would like to have thing working as good as possible before testing because TTN gateways are far from my place and I cannot connect to TTN from here :O(

Thanks for your example.