Sure. Here’s my LoRa_GNSS.ino code.
/*
* LoRaWAN GNSS tracker
*
*/
#include "LoRaWan_APP.h"
#include "CayenneLPP.h"
#include <Wire.h>
#include "HT_SSD1306Wire.h"
/* OTAA para*/
uint8_t devEui[] = { 0x22, 0x32, 0x33, 0x00, 0x00, 0x88, 0x88, 0x02 };
uint8_t appEui[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
uint8_t appKey[] = { 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x88, 0x66, 0x01 };
/* we're using OTAA, but these still need to be defined to compile successfully
/* ABP para*/
uint8_t nwkSKey[] = { 0x15, 0xb1, 0xd0, 0xef, 0xa4, 0x63, 0xdf, 0xbe, 0x3d, 0x11, 0x18, 0x1e, 0x1e, 0xc7, 0xda,0x85 };
uint8_t appSKey[] = { 0xd7, 0x2c, 0x78, 0x75, 0x8c, 0xdc, 0xca, 0xbf, 0x55, 0xee, 0x4a, 0x77, 0x8d, 0x16, 0xef,0x67 };
uint32_t devAddr = ( uint32_t )0x007e6ae1;
/*LoraWan channelsmask*/
/* using channels 8-15 */
uint16_t userChannelsMask[6]={ 0xFF00,0x0000,0x0000,0x0000,0x0000,0x0000 };
/*LoraWan region, select in arduino IDE tools*/
LoRaMacRegion_t loraWanRegion = ACTIVE_REGION;
/*LoraWan Class, Class A and Class C are supported*/
DeviceClass_t loraWanClass = CLASS_A;
/*the application data transmission duty cycle. value in [ms].*/
uint32_t appTxDutyCycle = 30000;
/*OTAA or ABP*/
bool overTheAirActivation = true;
/*ADR enable*/
bool loraWanAdr = true;
/* Indicates if the node is sending confirmed or unconfirmed messages */
bool isTxConfirmed = true;
/* Application port */
uint8_t appPort = 2;
/*!
* Number of trials to transmit the frame, if the LoRaMAC layer did not
* receive an acknowledgment. The MAC performs a datarate adaptation,
* according to the LoRaWAN Specification V1.0.2, chapter 18.4, according
* to the following table:
*
* Transmission nb | Data Rate
* ----------------|-----------
* 1 (first) | DR
* 2 | DR
* 3 | max(DR-1,0)
* 4 | max(DR-1,0)
* 5 | max(DR-2,0)
* 6 | max(DR-2,0)
* 7 | max(DR-3,0)
* 8 | max(DR-3,0)
*
* Note, that if NbTrials is set to 1 or 2, the MAC will not decrease
* the datarate, in case the LoRaMAC layer did not receive an acknowledgment
*/
uint8_t confirmedNbTrials = 4;
// GNSS settings
// U-BLOX firmware version
#define UBLOX_FW_VERSION 7
// Enable Serial debbug on Serial UART to see registers written
#define GNSS_DEBUG Serial
#define TX_PIN 46
#define RX_PIN 45
#define LED_PIN 35
#include "ublox_GNSS.h"
// Use hardware serial support
#define Serial_GNSS Serial1
float lat, lon, alt, acc, dist;
fixType_t fix = NO_FIX;
GNSS gnss( Serial_GNSS, UBLOX_FW_VERSION );
extern SSD1306Wire display;
typedef struct coords_s {
float lat;
float lon;
} COORDS;
COORDS home = { 35.90477, -78.83601 };
/* This is the first call to the display after sleep so we have to duplicate
* LoRaWanClass::displaySending()
*/
void displayAcquiring()
{
digitalWrite(Vext,LOW);
display.init();
display.setFont(ArialMT_Plain_16);
display.setTextAlignment(TEXT_ALIGN_CENTER);
display.clear();
display.drawString(display.getWidth()/2, display.getHeight()/2, "ACQUIRING LOC");
display.display();
}
void displayLocation()
{
char temp[30];
sprintf(temp,"dist: %3.1f km, hAcc %3.0f m",dist, acc);
display.setFont(ArialMT_Plain_10);
display.setTextAlignment(TEXT_ALIGN_RIGHT);
display.drawString(128, 0, temp);
display.display();
delay( 2000 );
}
/* Prepares the payload of the frame */
static void prepareTxFrame( uint8_t port )
{
/*appData size is LORAWAN_APP_DATA_MAX_SIZE which is defined in "commissioning.h".
*appDataSize max value is LORAWAN_APP_DATA_MAX_SIZE.
*if enabled AT, don't modify LORAWAN_APP_DATA_MAX_SIZE, it may cause system hanging or failure.
*if disabled AT, LORAWAN_APP_DATA_MAX_SIZE can be modified, the max value is reference to lorawan region and SF.
*for example, if use REGION_CN470,
*the max value for different DR can be found in MaxPayloadOfDatarateCN470 refer to DataratesCN470 and BandwidthsCN470 in "RegionCN470.h".
*/
gnss_acquire();
CayenneLPP *lpp = new CayenneLPP( LORAWAN_APP_DATA_MAX_SIZE );
lpp->reset();
lpp->addGPS( 1, lat, lon, alt/1000 );
lpp->addDistance( 2, acc );
lpp->addDistance( 3, dist * 1000 );
lpp->addDigitalOutput( 4, ( digitalRead( LED_PIN ) == HIGH ? 1 : 0 ));
appDataSize = lpp->copy( appData );
delete lpp;
}
void decodeDownlinkMsg( uint8_t *buf, uint8_t bufsize, uint8_t port )
{
StaticJsonDocument<256> jsonBuffer;
CayenneLPP lpp(0);
JsonArray array = jsonBuffer.to<JsonArray>();
lpp.decode( buf, bufsize, array );
//serializeJsonPretty(array, Serial);
//Serial.println();
for ( JsonObject item : array ) {
int lpp_type = item["type"];
switch ( lpp_type )
{
case LPP_DIGITAL_OUTPUT:
if ( item["channel"] == 4 )
{
int setval = LOW;
if ( item["value"] == 1 )
{
setval = HIGH;
}
gpio_hold_dis( (gpio_num_t )LED_PIN );
gpio_deep_sleep_hold_dis();
pinMode(LED_PIN, OUTPUT);
digitalWrite( LED_PIN, setval );
gpio_deep_sleep_hold_en();
gpio_hold_en( (gpio_num_t )LED_PIN );
}
break;
default:
break;
}
}
}
void downLinkDataHandle(McpsIndication_t *mcpsIndication)
{
printf("+REV DATA:%s,RXSIZE %d,PORT %d\r\n",mcpsIndication->RxSlot?"RXWIN2":"RXWIN1",mcpsIndication->BufferSize,mcpsIndication->Port);
printf("+REV DATA:");
for(uint8_t i=0;i<mcpsIndication->BufferSize;i++)
{
printf("%02X",mcpsIndication->Buffer[i]);
}
printf("\r\n");
if ( mcpsIndication->RxData )
{
decodeDownlinkMsg( mcpsIndication->Buffer, mcpsIndication->BufferSize, mcpsIndication->Port );
}
}
RTC_DATA_ATTR bool firstrun = true;
void gnss_setup( void )
{
Serial_GNSS.begin( 9600, SERIAL_8N1, RX_PIN, TX_PIN );
#define ACQ_TIMEOUT 40
#define SEARCH_PERIOD 0
#define UPDATE_PERIOD 0
#define ON_TIME 15
if ( gnss.config( ON_OFF ) )
{
Serial.println("\nGNSS configured.");
}
else
{
Serial.println("\nGNSS configuration failed.");
}
/* wait for first fix */
while ( gnss.getCoodinates(lon, lat, alt, fix, acc, 50) == 0 )
{
Serial.println("\nInitial fix failed.");
}
float distance = gnss.HaverSine( home.lat, home.lon, lat, lon );
Serial.println("Distance to home: " + String( distance, 2 ) + " km" );
if ( gnss.init( ON_OFF, ACQ_TIMEOUT, SEARCH_PERIOD, UPDATE_PERIOD, ON_TIME ) )
{
Serial.println("\nGNSS initialized.");
}
else
{
Serial.println("\nFailed to initialize GNSS module.");
}
gnss.conditionalOff();
}
void gnss_acquire( void )
{
Serial.println("Get location");
gnss.reinst( Serial_GNSS, UBLOX_FW_VERSION );
Serial_GNSS.begin( 9600, SERIAL_8N1, RX_PIN, TX_PIN );
gnss.wake();
delay( 1000 );
if ( gnss.getCoodinates(lon, lat, alt, fix, acc, 50) == 0)
{
Serial.println("Failed to get coordinates, check signal, accuracy required or wiring");
}
Serial.println("Location lat:" + String(lat, 7) +" lon:" + String(lon, 7) +
+ " alt: " + String( alt ) + "m horizontal accuracy: " + String(acc) + "m");
//Serial.println("\nOr try the following link to see on google maps:");
//Serial.println(String("https://www.google.com/maps/search/?api=1&query=") + String(lat,7) + "," + String(lon,7));
dist = gnss.HaverSine( home.lat, home.lon, lat, lon );
Serial.println("Distance to home: " + String( dist, 2 ) + " km" );
gnss.conditionalOff();
}
void setup() {
Serial.begin(115200);
Mcu.begin();
if(firstrun)
{
LoRaWAN.displayMcuInit();
firstrun = false;
gnss_setup();
}
deviceState = DEVICE_STATE_INIT;
}
void loop()
{
switch( deviceState )
{
case DEVICE_STATE_INIT:
{
#if(LORAWAN_DEVEUI_AUTO)
LoRaWAN.generateDeveuiByChipID();
#endif
LoRaWAN.init(loraWanClass,loraWanRegion);
break;
}
case DEVICE_STATE_JOIN:
{
LoRaWAN.displayJoining();
LoRaWAN.join();
break;
}
case DEVICE_STATE_SEND:
{
displayAcquiring();
prepareTxFrame( appPort );
displayLocation();
LoRaWAN.displaySending();
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.displayAck();
LoRaWAN.sleep(loraWanClass);
break;
}
default:
{
deviceState = DEVICE_STATE_INIT;
break;
}
}
}
The contents of the Arduino sketch.yaml file.
default_fqbn: heltec:esp32:WIFI_LoRa_32_V3:UploadSpeed=921600,CPUFreq=240,DebugLevel=none,LORAWAN_REGION=3,LoRaWanDebugLevel=0,LoopCore=1,EventsCore=1,LORAWAN_PREAMBLE_LENGTH=0,LORAWAN_DEVEUI=1
default_port: COM5
And the decoder.js file.
/**
* @reference https://github.com/myDevicesIoT/cayenne-docs/blob/ff6da3d400d406a3cbc6b380abe3eeaa7009bbb9/docs/LORA.md
* @reference http://openmobilealliance.org/wp/OMNA/LwM2M/LwM2MRegistry.html#extlabel
*
* Adapted for lora-app-server from https://gist.github.com/iPAS/e24970a91463a4a8177f9806d1ef14b8
*
* Type IPSO LPP Hex Data Size Data Resolution per bit
* Digital Input 3200 0 0 1 1
* Digital Output 3201 1 1 1 1
* Analog Input 3202 2 2 2 0.01 Signed
* Analog Output 3203 3 3 2 0.01 Signed
* Illuminance Sensor 3301 101 65 2 1 Lux Unsigned MSB
* Presence Sensor 3302 102 66 1 1
* Temperature Sensor 3303 103 67 2 0.1 °C Signed MSB
* Humidity Sensor 3304 104 68 1 0.5 % Unsigned
* Accelerometer 3313 113 71 6 0.001 G Signed MSB per axis
* Barometer 3315 115 73 2 0.1 hPa Unsigned MSB
* Time 3333 133 85 4 Unix time MSB
* Gyrometer 3334 134 86 6 0.01 °/s Signed MSB per axis
* GPS Location 3336 136 88 9 Latitude : 0.0001 ° Signed MSB
* Longitude : 0.0001 ° Signed MSB
* Altitude : 0.01 meter Signed MSB
*
* Additional types
* Generic Sensor 3300 100 64 4 Unsigned integer MSB
* Voltage 3316 116 74 2 0.01 V Unsigned MSB
* Current 3317 117 75 2 0.001 A Unsigned MSB
* Frequency 3318 118 76 4 1 Hz Unsigned MSB
* Percentage 3320 120 78 1 1% Unsigned
* Altitude 3321 121 79 2 1m Signed MSB
* Concentration 3325 125 7D 2 1 PPM unsigned : 1pmm = 1 * 10 ^-6 = 0.000 001
* Power 3328 128 80 2 1 W Unsigned MSB
* Distance 3330 130 82 4 0.001m Unsigned MSB
* Energy 3331 131 83 4 0.001kWh Unsigned MSB
* Colour 3335 135 87 3 R: 255 G: 255 B: 255
* Direction 3332 132 84 2 1º Unsigned MSB
* Switch 3342 142 8E 1 0/1
*
*/
// lppDecode decodes an array of bytes into an array of ojects,
// each one with the channel, the data type and the value.
function lppDecode(bytes) {
var sensor_types = {
0 : {'size': 1, 'name': 'digital_in', 'signed': false, 'divisor': 1},
1 : {'size': 1, 'name': 'digital_out', 'signed': false, 'divisor': 1},
2 : {'size': 2, 'name': 'analog_in', 'signed': true , 'divisor': 100},
3 : {'size': 2, 'name': 'analog_out', 'signed': true , 'divisor': 100},
100: {'size': 4, 'name': 'generic', 'signed': false, 'divisor': 1},
101: {'size': 2, 'name': 'illuminance', 'signed': false, 'divisor': 1},
102: {'size': 1, 'name': 'presence', 'signed': false, 'divisor': 1},
103: {'size': 2, 'name': 'temperature', 'signed': true , 'divisor': 10},
104: {'size': 1, 'name': 'humidity', 'signed': false, 'divisor': 2},
113: {'size': 6, 'name': 'accelerometer', 'signed': true , 'divisor': 1000},
115: {'size': 2, 'name': 'barometer', 'signed': false, 'divisor': 10},
116: {'size': 2, 'name': 'voltage', 'signed': false, 'divisor': 100},
117: {'size': 2, 'name': 'current', 'signed': false, 'divisor': 1000},
118: {'size': 4, 'name': 'frequency', 'signed': false, 'divisor': 1},
120: {'size': 1, 'name': 'percentage', 'signed': false, 'divisor': 1},
121: {'size': 2, 'name': 'altitude', 'signed': true, 'divisor': 1},
125: {'size': 2, 'name': 'concentration', 'signed': false, 'divisor': 1},
128: {'size': 2, 'name': 'power', 'signed': false, 'divisor': 1},
130: {'size': 4, 'name': 'distance', 'signed': false, 'divisor': 1000},
131: {'size': 4, 'name': 'energy', 'signed': false, 'divisor': 1000},
132: {'size': 2, 'name': 'direction', 'signed': false, 'divisor': 1},
133: {'size': 4, 'name': 'time', 'signed': false, 'divisor': 1},
134: {'size': 6, 'name': 'gyrometer', 'signed': true , 'divisor': 100},
135: {'size': 3, 'name': 'colour', 'signed': false, 'divisor': 1},
136: {'size': 9, 'name': 'gps', 'signed': true, 'divisor': [10000,10000,100]},
142: {'size': 1, 'name': 'switch', 'signed': false, 'divisor': 1},
};
function arrayToDecimal(stream, is_signed, divisor) {
var value = 0;
for (var i = 0; i < stream.length; i++) {
if (stream[i] > 0xFF)
throw 'Byte value overflow!';
value = (value << 8) | stream[i];
}
if (is_signed) {
var edge = 1 << (stream.length) * 8; // 0x1000..
var max = (edge - 1) >> 1; // 0x0FFF.. >> 1
value = (value > max) ? value - edge : value;
}
value /= divisor;
return value;
}
var sensors = [];
var i = 0;
while (i < bytes.length) {
var s_no = bytes[i++];
var s_type = bytes[i++];
if (typeof sensor_types[s_type] == 'undefined') {
throw 'Sensor type error!: ' + s_type;
}
var s_value = 0;
var type = sensor_types[s_type];
switch (s_type) {
case 113: // Accelerometer
case 134: // Gyrometer
s_value = {
'x': arrayToDecimal(bytes.slice(i+0, i+2), type.signed, type.divisor),
'y': arrayToDecimal(bytes.slice(i+2, i+4), type.signed, type.divisor),
'z': arrayToDecimal(bytes.slice(i+4, i+6), type.signed, type.divisor)
};
break;
case 136: // GPS Location
s_value = {
'latitude': arrayToDecimal(bytes.slice(i+0, i+3), type.signed, type.divisor[0]),
'longitude': arrayToDecimal(bytes.slice(i+3, i+6), type.signed, type.divisor[1]),
'altitude': arrayToDecimal(bytes.slice(i+6, i+9), type.signed, type.divisor[2])
};
break;
case 135: // Colour
s_value = {
'r': arrayToDecimal(bytes.slice(i+0, i+1), type.signed, type.divisor),
'g': arrayToDecimal(bytes.slice(i+1, i+2), type.signed, type.divisor),
'b': arrayToDecimal(bytes.slice(i+2, i+3), type.signed, type.divisor)
};
break;
default: // All the rest
s_value = arrayToDecimal(bytes.slice(i, i + type.size), type.signed, type.divisor);
break;
}
sensors.push({
'channel': s_no,
'type': s_type,
'name': type.name,
'value': s_value
});
i += type.size;
}
return sensors;
}
// To use with TTN
function decodeUplink(input) {
bytes = input.bytes;
fPort = input.fPort;
// flat output (like original decoder):
var response = {};
lppDecode(bytes, 1).forEach(function(field) {
response[field['name'] + '_' + field['channel']] = field['value'];
});
return {
data: response
};
// field output
//return {'fields': lppDecode(bytes, fPort)};
}
// To use with NodeRED
// Assuming msg.payload contains the LPP-encoded byte array
/*
msg.fields = lppDecode(msg.payload);
return msg;
*/
// Encode downlink function.
//
// Input is an object with the following fields:
// - data = Object representing the payload that must be encoded.
// - variables = Object containing the configured device variables.
//
// Output must be an object with the following fields:
// - bytes = Byte array containing the downlink payload.
//
// our data looks like: {"array":[{"channel":4,"name":"digital_out","value":1}]}
function encodeDownlink(input) {
var arr = parseExtJSON( input.data );
var i = 0;
var output = [];
arr.array.forEach(( item ) => {
switch ( item.name ) {
case "digital_out":
item.type = 1;
break;
default:
item.type = 255;
break;
}
output[i++] = item.channel;
output[i++] = item.type;
output[i++] = item.value;
});
return {
bytes: output
};
}
I recommend getting the LoRaWan_OLED example running on the Heltec before tackling this.