Hello,
I have a TTGO LoRa32 V1 connecting to ChirpStack via a Laird RG191 with the following code using the RadioLib library:
LoRaWAN_Starter.ino
/*
RadioLib LoRaWAN Starter Example
! Please refer to the included notes to get started !
This example joins a LoRaWAN network and will send
uplink packets. Before you start, you will have to
register your device at https://www.thethingsnetwork.org/
After your device is registered, you can run this example.
The device will join the network and start uploading data.
Running this examples REQUIRES you to check "Resets DevNonces"
on your LoRaWAN dashboard. Refer to the network's
documentation on how to do this.
For default module settings, see the wiki page
https://github.com/jgromes/RadioLib/wiki/Default-configuration
For full API reference, see the GitHub Pages
https://jgromes.github.io/RadioLib/
For LoRaWAN details, see the wiki page
https://github.com/jgromes/RadioLib/wiki/LoRaWAN
*/
#include "config.h"
void setup() {
Serial.begin(115200);
while(!Serial);
delay(5000); // Give time to switch to the serial monitor
Serial.println(F("\nSetup ... "));
Serial.println(F("Initialise the radio"));
int16_t state = radio.begin();
debug(state != RADIOLIB_ERR_NONE, F("Initialise radio failed"), state, true);
// Setup the OTAA session information
state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey);
debug(state != RADIOLIB_ERR_NONE, F("Initialise node failed"), state, true);
Serial.println(F("Join ('login') the LoRaWAN Network"));
state = node.activateOTAA();
debug(state != RADIOLIB_LORAWAN_NEW_SESSION, F("Join failed"), state, true);
Serial.println(F("Ready!\n"));
}
void loop() {
Serial.println(F("Sending uplink"));
// // This is the place to gather the sensor inputs
// // Instead of reading any real sensor, we just generate some random numbers as example
// uint8_t value1 = 50;
// uint16_t value2 = 1500;
// // Build payload byte array
// uint8_t uplinkPayload[3];
// uplinkPayload[0] = value1;
// uplinkPayload[1] = highByte(value2); // See notes for high/lowByte functions
// uplinkPayload[2] = lowByte(value2);
// Text string to send
const char* textValue = "Hello";
// Calculate the length of the string (including the null terminator)
uint8_t textLength = strlen(textValue);
// Create a byte array to hold the string (plus one byte for null terminator)
uint8_t uplinkPayload[textLength + 1];
// Copy the string into the payload byte array
for (uint8_t i = 0; i < textLength; i++) {
uplinkPayload[i] = textValue[i]; // Copy character by character
}
// Add a null terminator if needed (for C-style strings)
uplinkPayload[textLength] = '\0'; // Optional, if you need null-termination
// Perform an uplink
int16_t state = node.sendReceive(uplinkPayload, sizeof(uplinkPayload),true);
debug(state < RADIOLIB_ERR_NONE, F("Error in sendReceive"), state, false);
// Check if a downlink was received
// (state 0 = no downlink, state 1/2 = downlink in window Rx1/Rx2)
if(state > 0) {
Serial.println(F("Received a downlink"));
} else {
Serial.println(F("No downlink received"));
}
Serial.print(F("Next uplink in "));
Serial.print(uplinkIntervalSeconds);
Serial.println(F(" seconds\n"));
// Wait until next uplink - observing legal & TTN FUP constraints
delay(uplinkIntervalSeconds * 1000UL); // delay needs milli-seconds
}
config.h
#ifndef _RADIOLIB_EX_LORAWAN_CONFIG_H
#define _RADIOLIB_EX_LORAWAN_CONFIG_H
#define ARDUINO_TTGO_LORA32_V1
#include <RadioLib.h>
// first you have to set your radio model and pin configuration
// this is provided just as a default example
SX1276 radio = new Module(18, 26, 14, 33);
// if you have RadioBoards (https://github.com/radiolib-org/RadioBoards)
// and are using one of the supported boards, you can do the following:
/*
#define RADIO_BOARD_AUTO
#include <RadioBoards.h>
Radio radio = new RadioModule();
*/
// how often to send an uplink - consider legal & FUP constraints - see notes
const uint32_t uplinkIntervalSeconds = 1UL * 30UL; // minutes x seconds
// joinEUI - previous versions of LoRaWAN called this AppEUI
// for development purposes you can use all zeros - see wiki for details
#define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000
// the Device EUI & two keys can be generated on the TTN console
#ifndef RADIOLIB_LORAWAN_DEV_EUI // Replace with your Device EUI
#define RADIOLIB_LORAWAN_DEV_EUI 0x70B3D57ED006F85A
#endif
#ifndef RADIOLIB_LORAWAN_APP_KEY // Replace with your App Key
#define RADIOLIB_LORAWAN_APP_KEY 0x11, 0x25, 0x4E, 0x23, 0x83, 0xD5, 0x85, 0x39, 0x87, 0xC4, 0xDA, 0x5B, 0x68, 0xA6, 0x67, 0xA8
#endif
#ifndef RADIOLIB_LORAWAN_NWK_KEY // Put your Nwk Key here
#define RADIOLIB_LORAWAN_NWK_KEY 0xDC, 0xEC, 0x04, 0x77, 0xB0, 0x90, 0x2C, 0x60, 0xF2, 0xA3, 0x40, 0x66, 0x44, 0x4A, 0x91, 0xFB
#endif
// for the curious, the #ifndef blocks allow for automated testing &/or you can
// put your EUI & keys in to your platformio.ini - see wiki for more tips
// regional choices: EU868, US915, AU915, AS923, AS923_2, AS923_3, AS923_4, IN865, KR920, CN500
const LoRaWANBand_t Region = AU915;
const uint8_t subBand = 2; // For US915, change this to 2, otherwise leave on 0
// ============================================================================
// Below is to support the sketch - only make changes if the notes say so ...
// copy over the EUI's & keys in to the something that will not compile if incorrectly formatted
uint64_t joinEUI = RADIOLIB_LORAWAN_JOIN_EUI;
uint64_t devEUI = RADIOLIB_LORAWAN_DEV_EUI;
uint8_t appKey[] = { RADIOLIB_LORAWAN_APP_KEY };
uint8_t nwkKey[] = { RADIOLIB_LORAWAN_NWK_KEY };
// create the LoRaWAN node
LoRaWANNode node(&radio, &Region, subBand);
// result code to text - these are error codes that can be raised when using LoRaWAN
// however, RadioLib has many more - see https://jgromes.github.io/RadioLib/group__status__codes.html for a complete list
String stateDecode(const int16_t result) {
switch (result) {
case RADIOLIB_ERR_NONE:
return "ERR_NONE";
case RADIOLIB_ERR_CHIP_NOT_FOUND:
return "ERR_CHIP_NOT_FOUND";
case RADIOLIB_ERR_PACKET_TOO_LONG:
return "ERR_PACKET_TOO_LONG";
case RADIOLIB_ERR_RX_TIMEOUT:
return "ERR_RX_TIMEOUT";
case RADIOLIB_ERR_CRC_MISMATCH:
return "ERR_CRC_MISMATCH";
case RADIOLIB_ERR_INVALID_BANDWIDTH:
return "ERR_INVALID_BANDWIDTH";
case RADIOLIB_ERR_INVALID_SPREADING_FACTOR:
return "ERR_INVALID_SPREADING_FACTOR";
case RADIOLIB_ERR_INVALID_CODING_RATE:
return "ERR_INVALID_CODING_RATE";
case RADIOLIB_ERR_INVALID_FREQUENCY:
return "ERR_INVALID_FREQUENCY";
case RADIOLIB_ERR_INVALID_OUTPUT_POWER:
return "ERR_INVALID_OUTPUT_POWER";
case RADIOLIB_ERR_NETWORK_NOT_JOINED:
return "RADIOLIB_ERR_NETWORK_NOT_JOINED";
case RADIOLIB_ERR_DOWNLINK_MALFORMED:
return "RADIOLIB_ERR_DOWNLINK_MALFORMED";
case RADIOLIB_ERR_INVALID_REVISION:
return "RADIOLIB_ERR_INVALID_REVISION";
case RADIOLIB_ERR_INVALID_PORT:
return "RADIOLIB_ERR_INVALID_PORT";
case RADIOLIB_ERR_NO_RX_WINDOW:
return "RADIOLIB_ERR_NO_RX_WINDOW";
case RADIOLIB_ERR_INVALID_CID:
return "RADIOLIB_ERR_INVALID_CID";
case RADIOLIB_ERR_UPLINK_UNAVAILABLE:
return "RADIOLIB_ERR_UPLINK_UNAVAILABLE";
case RADIOLIB_ERR_COMMAND_QUEUE_FULL:
return "RADIOLIB_ERR_COMMAND_QUEUE_FULL";
case RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND:
return "RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND";
case RADIOLIB_ERR_JOIN_NONCE_INVALID:
return "RADIOLIB_ERR_JOIN_NONCE_INVALID";
case RADIOLIB_ERR_N_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_N_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_A_FCNT_DOWN_INVALID:
return "RADIOLIB_ERR_A_FCNT_DOWN_INVALID";
case RADIOLIB_ERR_DWELL_TIME_EXCEEDED:
return "RADIOLIB_ERR_DWELL_TIME_EXCEEDED";
case RADIOLIB_ERR_CHECKSUM_MISMATCH:
return "RADIOLIB_ERR_CHECKSUM_MISMATCH";
case RADIOLIB_ERR_NO_JOIN_ACCEPT:
return "RADIOLIB_ERR_NO_JOIN_ACCEPT";
case RADIOLIB_LORAWAN_SESSION_RESTORED:
return "RADIOLIB_LORAWAN_SESSION_RESTORED";
case RADIOLIB_LORAWAN_NEW_SESSION:
return "RADIOLIB_LORAWAN_NEW_SESSION";
case RADIOLIB_ERR_NONCES_DISCARDED:
return "RADIOLIB_ERR_NONCES_DISCARDED";
case RADIOLIB_ERR_SESSION_DISCARDED:
return "RADIOLIB_ERR_SESSION_DISCARDED";
}
return "See https://jgromes.github.io/RadioLib/group__status__codes.html";
}
// helper function to display any issues
void debug(bool failed, const __FlashStringHelper* message, int state, bool halt) {
if(failed) {
Serial.print(message);
Serial.print(" - ");
Serial.print(stateDecode(state));
Serial.print(" (");
Serial.print(state);
Serial.println(")");
while(halt) { delay(1); }
}
}
// helper function to display a byte array
void arrayDump(uint8_t *buffer, uint16_t len) {
for(uint16_t c = 0; c < len; c++) {
char b = buffer[c];
if(b < 0x10) { Serial.print('0'); }
Serial.print(b, HEX);
}
Serial.println();
}
#endif
I have successfully registered the device to my ChirpStack network server (with “Disable frame-counter validation” enabled:
The device connects and transmits successfully only after “Flush OTAA device nonces” is done.
If I reboot the board (regardless of how many times I try), I get the following error:
In the serial monitor of the board, I get:
Join failed - RADIOLIB_ERR_NO_JOIN_ACCEPT (-1116)
As soon as I select “Flush OTAA device nonces”, the device connects without issue.
I am using the latest version of ChirpStack via Docker (6th April 2025) which I downloaded and installed today.
Research and Findings
From LoRaWAN.cpp:
int16_t LoRaWANNode::activateOTAA(uint8_t joinDr, LoRaWANJoinEvent_t *joinEvent) {
// check if there is an active session
if(this->isActivated()) {
// already activated, don't do anything
return(RADIOLIB_ERR_NONE);
}
if(this->bufferNonces[RADIOLIB_LORAWAN_NONCES_ACTIVE]) {
// session restored but not yet activated - do so now
this->isActive = true;
return(RADIOLIB_LORAWAN_SESSION_RESTORED);
}
int16_t state = RADIOLIB_ERR_UNKNOWN;
Module* mod = this->phyLayer->getMod();
// starting a new session, so make sure to update event fields already
if(joinEvent) {
joinEvent->newSession = true;
joinEvent->devNonce = this->devNonce;
joinEvent->joinNonce = this->joinNonce;
}
// setup all MAC properties to default values
this->createSession(RADIOLIB_LORAWAN_MODE_OTAA, joinDr);
// build the JoinRequest message
uint8_t joinRequestMsg[RADIOLIB_LORAWAN_JOIN_REQUEST_LEN];
this->composeJoinRequest(joinRequestMsg);
// select a random pair of Tx/Rx channels
state = this->selectChannels();
RADIOLIB_ASSERT(state);
// set the physical layer configuration for uplink
state = this->setPhyProperties(&this->channels[RADIOLIB_LORAWAN_UPLINK],
RADIOLIB_LORAWAN_UPLINK,
this->txPowerMax - 2*this->txPowerSteps);
RADIOLIB_ASSERT(state);
// calculate JoinRequest time-on-air in milliseconds
if(this->dwellTimeUp) {
RadioLibTime_t toa = this->phyLayer->getTimeOnAir(RADIOLIB_LORAWAN_JOIN_REQUEST_LEN) / 1000;
if(toa > this->dwellTimeUp) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Dwell time exceeded: ToA = %lu, max = %d", (unsigned long)toa, this->dwellTimeUp);
return(RADIOLIB_ERR_DWELL_TIME_EXCEEDED);
}
}
// if requested, delay until transmitting JoinRequest
RadioLibTime_t tNow = mod->hal->millis();
if(this->tUplink > tNow) {
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("Delaying transmission by %lu ms", (unsigned long)(this->tUplink - tNow));
if(this->tUplink > mod->hal->millis()) {
mod->hal->delay(this->tUplink - mod->hal->millis());
}
}
// send it
state = this->phyLayer->transmit(joinRequestMsg, RADIOLIB_LORAWAN_JOIN_REQUEST_LEN);
this->rxDelayStart = mod->hal->millis();
RADIOLIB_ASSERT(state);
RADIOLIB_DEBUG_PROTOCOL_PRINTLN("JoinRequest sent (DevNonce = %d) <-- Rx Delay start", this->devNonce);
RADIOLIB_DEBUG_PROTOCOL_HEXDUMP(joinRequestMsg, RADIOLIB_LORAWAN_JOIN_REQUEST_LEN);
// JoinRequest successfully sent, so increase & save devNonce
this->devNonce += 1;
LoRaWANNode::hton<uint16_t>(&this->bufferNonces[RADIOLIB_LORAWAN_NONCES_DEV_NONCE], this->devNonce);
// set the Time on Air of the JoinRequest
this->lastToA = this->phyLayer->getTimeOnAir(RADIOLIB_LORAWAN_JOIN_REQUEST_LEN) / 1000;
// configure Rx1 and Rx2 delay for JoinAccept message - these are re-configured once a valid JoinAccept is received
this->rxDelays[1] = RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_1_MS;
this->rxDelays[2] = RADIOLIB_LORAWAN_JOIN_ACCEPT_DELAY_2_MS;
// handle Rx1 and Rx2 windows - returns window > 0 if a downlink is received
state = receiveCommon(RADIOLIB_LORAWAN_DOWNLINK, this->channels, this->rxDelays, 2, this->rxDelayStart);
if(state < RADIOLIB_ERR_NONE) {
return(state);
} else if (state == RADIOLIB_ERR_NONE) {
return(RADIOLIB_ERR_NO_JOIN_ACCEPT);
}
// process JoinAccept message
state = this->processJoinAccept(joinEvent);
RADIOLIB_ASSERT(state);
return(RADIOLIB_LORAWAN_NEW_SESSION);
}
From LoRaWAN.h :
virtual int16_t activateOTAA(uint8_t initialDr = RADIOLIB_LORAWAN_DATA_RATE_UNUSED, LoRaWANJoinEvent_t *joinEvent = NULL);
struct LoRaWANJoinEvent_t {
/*! \brief Whether a new session was started */
bool newSession = false;
/*! \brief The transmitted Join-Request DevNonce value */
uint16_t devNonce = 0;
/*! \brief The received Join-Request JoinNonce value */
uint32_t joinNonce = 0;
};
“devNonce” is always 0 on reboot of the device (I have verified this).
Given that there is no option to disable the “devNonce” check or reset the counter on each join in ChirpStack how can I resolve this automatically? Can I choose an older LoRaWAN version in ChripStack? The only issue with that is RadioLib states:
Choose LoRaWAN 1.1.0 - the last one in the list - the latest specification. RadioLib uses RP001 Regional Parameters 1.1 revision B.
Any help would be much apprecaited.