Optimizing Heltec V3 with LoRa and Sensor Suite

Hello,

I want to optimize my current RadioLib persistence code that I’m running on a Heltec WiFi Lora v3. Logic wise, I know what I want to do but I’m stuck on the technical how.

Some of the sensors I’m using have a response time of almost 2 minutes. So I would like to gather data for “time <= 2 minutes” and then only send the data for “time > 2 minutes”, perhaps even get ten values or so when 2 minutes are over and send the mean via LoRaWAN.

I fear that delay() will not work for this because it technically doesn’t do anything during that time but the sensor has to be powered. Or maybe it’s sufficient? Using millis() did not work which is perhaps because the RadioLib code runs in the setup() part, as loop() is never reached. My attempt last weekend certainly failed, connection could not be established at all with a millis() in the setup() (expected that but wanted to test and rule it out).

I hope I’m making sense, my brain feels fried these days.

Thanks in advance for your suggestions, ideas, and help!

What basis do you believe that delay wouldn’t work or that millis did not work because of code run-in in the setup part. Have you tried creating the minimum code to test out your hypothesis which, once, spoiler alert, find that they do work just fine, you can then look at how to either slot the RadioLib code in to it, or copy elements in to the existing code.

It would also be useful for you to carry on sharing your code so we can pick up on any extras that have sneaked in.

TL;DR: in the setup do all the sensor initialising and then command the sensor to start the reading and then delay(2 * 60 * 1000) (ie 2 minutes) and then read the sensors then build the payload then send then go back to sleep.

The really short version is that a nicely structured sequence would require you to add just that one extra line of the delay.

Optimising after that to go in to light sleep as 2 minutes is a waste of battery can come after that.

You should also start getting a friend to make cups of tea & jam sandwiches which is who teachers teach procedural coding. You sit that issuing commands, they do exactly what you say, even if it creates a mess. You can then move on to telling the ESP board that same thing and figure out if the sequence makes sense.

With the delay(), I had read some conflicting information which made me conclude that it might not serve my purpose. My cheap excuse here is that this was after working 8h on day 1 and then 4h on day 2 just on sensor recalibration, so my brain capacity and patience were used up. I tried millis() with a standard script [code execution in loop() not in setup()], that did work. Adding the millis() into my existing code stopped the reconstruction of the LoRaWAN connection.

So my very initial idea of delay, before doubt and tiredness crept in, was correct, good to know. Well, with a 2 minute delay I would increase the sleep to 15 minutes as I really do not need data that often. Maybe even go down to measurements every 30 minutes but with some nice starting phase of 2 minutes and a mean transmitted value.

I will test out things on the weekend when I’m back with my sensors. Should things fail I’ll return here with the code; should everything work, I’ll just add a “closed” to the description at the top. Thanks!

This doesn’t make much sense - how did you implement a function call that returns ‘the time’? We can’t help you progress if you don’t share some code!

Right now it appears there’s a bigger problem than any delays or millis() on my side.

Using RadioLib persistence, I have the following codes:

The main code:
/*
CHANGE LOG
17.10.2025: Offsets auskommentiert um Sensoren zu testen.
30.10.2025: Neue Kalibrierwerte eingegeben.
*/


/*

ORIGINAL COMMENT FROM RADIOLIB PERSISTENCE DEEP SLEEP EXAMPLE

This demonstrates how to save the join information in to permanent memory
so that if the power fails, batteries run out or are changed, the rejoin
is more efficient & happens sooner due to the way that LoRaWAN secures
the join process - see the wiki for more details.

This is typically useful for devices that need more power than a battery
driven sensor - something like a air quality monitor or GPS based device that
is likely to use up it's power source resulting in loss of the session.

The relevant code is flagged with a ##### comment

Saving the entire session is possible but not demonstrated here - it has
implications for flash wearing and complications with which parts of the 
session may have changed after an uplink. So it is assumed that the device
is going in to deep-sleep, as below, between normal uplinks.

Once you understand what happens, feel free to delete the comments and 
Serial.prints - we promise the final result isn't that many lines.

*/

#if !defined(ESP32)
  #pragma error ("This is not the example your device is looking for - ESP32 only")
#endif

// ##### load the ESP32 preferences facilites
#include <Preferences.h>
Preferences store;

// LoRaWAN config, credentials & pinmap
#include "config.h" 
#include <RadioLib.h>

// andere Bibliotheken
#include <Arduino.h>
#include <OneWire.h>

// ––––––––––––––––––– BEGINN SENSORIK –––––––––––––––––––

// PINBELEGUNG
#define TE_PIN 7 // Temperatur
#define PH_PIN 6 // pH-Werte
#define TDS_PIN 5 // Feststoffe
#define DO_PIN 3 // Sauerstoff
#define VReg 47 // Enable Pin

// -- Temperatur
//Temperature chip i/o
OneWire ds(TE_PIN);
#define VREF    3300//VREF(mv)
#define ADC_RES 4096//ADC Resolution

// -- pH
float voltage, phValue;
// letzte Aktualisierung der pH-Werte am 30.10.2025 (JBL-Sensor)
float acidVoltage = 2158.88;     // Voltage at pH 4, war 2119.95 (Amazon)
float neutralVoltage = 1674.52;  // Voltage at pH 7, war 1632.55 (Amazon)
const float calibTemp = 16.44;    // Kalibriertemperatur in °C

// -- Feststoffe (TDS)
#define adcRange 4096.0 // 1024.0 für Arduino
#define aref 3.3 // 5.0 für Arduino
#define kValue 0.96 // war 1.38, konnte ich nicht kalibrieren
#define tdsFactor 0.5 // abhängig von Wassergüte, zw. 0.5 und 0.7
float analogValue, tdsVoltage, tempoVal, Conductivity, tdsValue;

// -- Sauerstoff
// Single-point calibration Mode=0
// Two-point calibration Mode=1
#define TWO_POINT_CALIBRATION 1 // wir nutzen zwei Punkte

// Single point calibration needs to be filled CAL1_V and CAL1_T
#define CAL1_V (735.03) //mv, hohe Temperatur DO gesättigt, war 837.47
#define CAL1_T (32.75)   //℃, hohe Temperatur DO gesättigt, war 30.90
// Two-point calibration needs to be filled CAL2_V and CAL2_T
// CAL1 High temperature point, CAL2 Low temperature point
#define CAL2_V (423.77) //mv, niedrige Temperatur DO gesättigt, bei Nullsauerstoff wäre es 28.09, war 584.08
#define CAL2_T (14.29)   //℃, niedrige Temperatur DO gesättigt, bei Nullsauerstoff wäre es 19.47, war 16.17

const uint16_t DO_Table[41] = {
    14460, 14220, 13820, 13440, 13090, 12740, 12420, 12110, 11810, 11530,
    11260, 11010, 10770, 10530, 10300, 10080, 9860, 9660, 9460, 9270,
    9080, 8900, 8730, 8570, 8410, 8250, 8110, 7960, 7820, 7690,
    7560, 7430, 7300, 7180, 7070, 6950, 6840, 6730, 6630, 6530, 6410};

//uint8_t Temperaturet;
uint16_t ADC_Raw;
uint16_t ADC_Voltage;
uint16_t DO;

int16_t readDO(uint32_t voltage_mv, uint8_t Temp)
{
#if TWO_POINT_CALIBRATION == 0
  uint16_t V_saturation = (uint32_t)CAL1_V + (uint32_t)35 * Temp - (uint32_t)CAL1_T * 35;
  return (voltage_mv * DO_Table[Temp] / V_saturation);
#else
  uint16_t V_saturation = (int16_t)((int8_t)Temp - CAL2_T) * ((uint16_t)CAL1_V - CAL2_V) / ((uint8_t)CAL1_T - CAL2_T) + CAL2_V;
  return (voltage_mv * DO_Table[Temp] / V_saturation);
#endif
}

// ––––––––––––––––––– ENDE SENSORIK –––––––––––––––––––

// utilities & vars to support ESP32 deep-sleep. The RTC_DATA_ATTR attribute
// puts these in to the RTC memory which is preserved during deep-sleep
RTC_DATA_ATTR uint16_t bootCount = 0;
RTC_DATA_ATTR uint16_t bootCountSinceUnsuccessfulJoin = 0;
RTC_DATA_ATTR uint8_t LWsession[RADIOLIB_LORAWAN_SESSION_BUF_SIZE];

// abbreviated version from the Arduino-ESP32 package, see
// https://espressif-docs.readthedocs-hosted.com/projects/arduino-esp32/en/latest/api/deepsleep.html
// for the complete set of options
void print_wakeup_reason() {
  esp_sleep_wakeup_cause_t wakeup_reason = esp_sleep_get_wakeup_cause();
  if (wakeup_reason == ESP_SLEEP_WAKEUP_TIMER) {
    Serial.println(F("Wake from sleep"));
  } else {
    Serial.print(F("Wake not caused by deep sleep: "));
    Serial.println(wakeup_reason);
  }

  Serial.print(F("Boot count: "));
  Serial.println(++bootCount);      // increment before printing
}

// put device in to lowest power deep-sleep mode
void gotoSleep(uint32_t seconds) {
  esp_sleep_enable_timer_wakeup(seconds * 1000UL * 1000UL); // function uses uS
  Serial.println(F("Sleeping\n"));
  Serial.flush();

  esp_deep_sleep_start();

  // if this appears in the serial debug, we didn't go to sleep!
  // so take defensive action so we don't continually uplink
  Serial.println(F("\n\n### Sleep failed, delay of 5 minutes & then restart ###\n"));
  delay(5UL * 60UL * 1000UL);
  ESP.restart();
}

int16_t lwActivate() {
  int16_t state = RADIOLIB_ERR_UNKNOWN;

  // setup the OTAA session information
  node.beginOTAA(joinEUI, devEUI, NULL, appKey);

  Serial.println(F("Recalling LoRaWAN nonces & session"));
  // ##### setup the flash storage
  store.begin("radiolib");
  // ##### if we have previously saved nonces, restore them and try to restore session as well
  if (store.isKey("nonces")) {
    uint8_t buffer[RADIOLIB_LORAWAN_NONCES_BUF_SIZE];										// create somewhere to store nonces
    store.getBytes("nonces", buffer, RADIOLIB_LORAWAN_NONCES_BUF_SIZE);	// get them from the store
    state = node.setBufferNonces(buffer); 															// send them to LoRaWAN
    debug(state != RADIOLIB_ERR_NONE, F("Restoring nonces buffer failed"), state, false);

    // recall session from RTC deep-sleep preserved variable
    state = node.setBufferSession(LWsession); // send them to LoRaWAN stack

    // if we have booted more than once we should have a session to restore, so report any failure
    // otherwise no point saying there's been a failure when it was bound to fail with an empty LWsession var.
    debug((state != RADIOLIB_ERR_NONE) && (bootCount > 1), F("Restoring session buffer failed"), state, false);

    // if Nonces and Session restored successfully, activation is just a formality
    // moreover, Nonces didn't change so no need to re-save them
    if (state == RADIOLIB_ERR_NONE) {
      Serial.println(F("Succesfully restored session - now activating"));
      state = node.activateOTAA();
      debug((state != RADIOLIB_LORAWAN_SESSION_RESTORED), F("Failed to activate restored session"), state, true);

      // ##### close the store before returning
      store.end();
      return(state);
    }

  } else {  // store has no key "nonces"
    Serial.println(F("No Nonces saved - starting fresh."));
  }

  // if we got here, there was no session to restore, so start trying to join
  state = RADIOLIB_ERR_NETWORK_NOT_JOINED;
  while (state != RADIOLIB_LORAWAN_NEW_SESSION) {
    Serial.println(F("Join ('login') to the LoRaWAN Network"));
    state = node.activateOTAA();

    // ##### save the join counters (nonces) to permanent store
    Serial.println(F("Saving nonces to flash"));
    uint8_t buffer[RADIOLIB_LORAWAN_NONCES_BUF_SIZE];           // create somewhere to store nonces
    uint8_t *persist = node.getBufferNonces();                  // get pointer to nonces
    memcpy(buffer, persist, RADIOLIB_LORAWAN_NONCES_BUF_SIZE);  // copy in to buffer
    store.putBytes("nonces", buffer, RADIOLIB_LORAWAN_NONCES_BUF_SIZE); // send them to the store
    
    // we'll save the session after an uplink

    if (state != RADIOLIB_LORAWAN_NEW_SESSION) {
      Serial.print(F("Join failed: "));
      Serial.println(state);

      // how long to wait before join attempts. This is an interim solution pending 
      // implementation of TS001 LoRaWAN Specification section #7 - this doc applies to v1.0.4 & v1.1
      // it sleeps for longer & longer durations to give time for any gateway issues to resolve
      // or whatever is interfering with the device <-> gateway airwaves.
      uint32_t sleepForSeconds = min((bootCountSinceUnsuccessfulJoin++ + 1UL) * 60UL, 3UL * 60UL);
      Serial.print(F("Boots since unsuccessful join: "));
      Serial.println(bootCountSinceUnsuccessfulJoin);
      Serial.print(F("Retrying join in "));
      Serial.print(sleepForSeconds);
      Serial.println(F(" seconds"));

      gotoSleep(sleepForSeconds);

    } // if activateOTAA state
  } // while join

  Serial.println(F("Joined"));

  // reset the failed join count
  bootCountSinceUnsuccessfulJoin = 0;

  delay(1000);  // hold off off hitting the airwaves again too soon - an issue in the US
  
  // ##### close the store
  store.end();  
  return(state);
}

// setup & execute all device functions ...
void setup() {
  Serial.begin(115200);

  // Enable, aktiviert Stromverbindung zu Sensoren 
  pinMode(VReg, OUTPUT);
  digitalWrite(VReg, HIGH);
  delay(3000);  // Sensoren aktivieren und warten, bis Werte stabil

  // while (!Serial);  							// wait for serial to be initalised // commented out because no serial
  delay(2000);  									// give time to switch to the serial monitor
  Serial.println(F("\nSetup"));
  print_wakeup_reason();

  int16_t state = 0;  						// return value for calls to RadioLib

  // setup the radio based on the pinmap (connections) in config.h
  Serial.println(F("Initalise the radio"));
  state = radio.begin();
  debug(state != RADIOLIB_ERR_NONE, F("Initalise radio failed"), state, true);

  // activate node by restoring session or otherwise joining the network
  state = lwActivate();
  // state is one of RADIOLIB_LORAWAN_NEW_SESSION or RADIOLIB_LORAWAN_SESSION_RESTORED

  // ----- and now for the main event -----
  Serial.println(F("Sending uplink"));

  // ––––––––––––––––––– BEGINN SENSORIK –––––––––––––––––––
    
  // -- TEMPERATUR
  byte data[12];
  byte addr[8];
  float temperature = -1000;

  if ( !ds.search(addr)) {
    //no more sensors on chain, reset search
    ds.reset_search();
  }

  if ( OneWire::crc8( addr, 7) != addr[7]) {
    Serial.println("CRC is not valid!");
  }

  if ( addr[0] != 0x10 && addr[0] != 0x28) {
    Serial.print("Device is not recognized");
  }

  ds.reset();
  ds.select(addr);
  ds.write(0x44,1); // start conversion, with parasite power on at the end

  delay(750);         // Wartezeit für 12-bit Auflösung

  byte present = ds.reset();
  ds.select(addr);    
  ds.write(0xBE); // Read Scratchpad

  
  for (int i = 0; i < 9; i++) { // we need 9 bytes
    data[i] = ds.read();
  }
  
  ds.reset_search();
  
  byte MSB = data[1];
  byte LSB = data[0];

  float tempRead = ((MSB << 8) | LSB); //using two's compliment
  temperature = tempRead / 16;

  // -- PH (JBL-Sensor mit Kalibrierung vom 30.10.2025)
  voltage = analogRead(PH_PIN) / 4096.0 * 3300.0;  // voltage in mV

  // ---- Temperaturkompensierte Berechnung ----
  float nernstSlopeCalib = 59.16 * (calibTemp + 273.15) / 298.15;
  float nernstSlopeNow   = 59.16 * (temperature + 273.15) / 298.15;
  float tempCompFactor   = nernstSlopeNow / nernstSlopeCalib;

  // Berechne Steigung und Offset
  float rawSlope = (7.0 - 4.0) / ((neutralVoltage - 1500.0) / 3.0 - (acidVoltage - 1500.0) / 3.0);
  float slope = rawSlope * tempCompFactor;
  float intercept = 7.0 - slope * (neutralVoltage - 1500.0) / 3.0;

  // pH-Wert berechnen
  phValue = slope * (voltage - 1500.0) / 3.0 + intercept;

  // -- TDS UND LEITFÄHIGKEIT
  analogValue = analogRead(TDS_PIN);
  tdsVoltage = analogValue / adcRange * aref;
  tempoVal = (133.42*tdsVoltage*tdsVoltage*tdsVoltage - 255.86*tdsVoltage*tdsVoltage + 857.39*tdsVoltage)*kValue;
  Conductivity = tempoVal / (1.0+0.02*(temperature-25.0));  //temperature compensation
  tdsValue = Conductivity * tdsFactor;
  
  // -- SAUERSTOFF
  ADC_Raw = analogRead(DO_PIN);
  ADC_Voltage = uint32_t(VREF) * ADC_Raw / ADC_RES;
  DO = readDO(ADC_Voltage, temperature);
  float DO_val = DO / 1000.0; // convert DO unit to mg/L

  delay(1000);

  // –– AUSGABE (nur zum Debuggen)   
  Serial.print("Temp.: ");
  Serial.print(temperature);
  Serial.print(" °C | pH: ");
  Serial.print(phValue);
  Serial.print(" | Feststoffe (TDS): ");
  Serial.print(tdsValue);
  Serial.print(" ppm | Leitf.: ");
  Serial.print(Conductivity);
  Serial.print(" µS/cm | Sauerstoff: ");
  Serial.print(DO_val);
  Serial.println(" mg/L");

  // Spannungen ausgeben
  Serial.print("pH Volt: ");
  Serial.print(voltage);
  Serial.print(" | TDS Volt: ");
  Serial.print(tdsVoltage);
  Serial.print(" | DO Volt: ");
  Serial.println(ADC_Voltage);

  // ––––––––––––––––––– ENDE SENSORIK –––––––––––––––––––

  // 17.10.2025: Wir nehmen einmal die Offsets raus um den Code zu testen.
  /*
  temperature = temperature + 1.0; // Offset ca 1°C
  phValue = phValue + 2.0; // Offset ca 2
  tdsValue = tdsValue + 550.0; // irgendwann mal TDS Sensor kalibrieren
  Conductivity = tdsValue * 2;
  DO_val = DO_val + 4.0; // etwa Offset von 4mg/L
  */

  // OFFSETS
  phValue = phValue + 4.0;

  // INTEGER CONVERSIONS
  int int_temp = temperature * 100; // EC-Sensor kann in 0-50°C operieren
  int int_ph = phValue * 100; // Wertebereich: 0 — 14
  int int_tds = tdsValue * 100; // Wertebereich: 0 — 1000 ppm
  int int_ec = Conductivity * 100; // Wertebereich: 100 - 2000 μS/cm (empfohlen)
  int int_do = DO_val * 100; // Wertebereich: 0 — 20 mg/L

  // PAYLOAD AUFBAU
  uint8_t uplinkPayload[12];
  // -- Temperatur
  uplinkPayload[0] = int_temp >> 8;
  uplinkPayload[1] = int_temp;
  // -- pH-Wert
  uplinkPayload[2] = int_ph >> 8;
  uplinkPayload[3] = int_ph;
  // -- Feststoffe (TDS) 
  uplinkPayload[4] = int_tds >> 8;
  uplinkPayload[5] = int_tds;
  // -- Leitfähigkeit (EC)
  //uplinkPayload[6] = int_ec >> 8;
  //uplinkPayload[7] = int_ec;
  // -- gelöster Sauerstoff (DO)
  uplinkPayload[6] = int_do >> 8;
  uplinkPayload[7] = int_do;
  
  // perform an uplink
  state = node.sendReceive(uplinkPayload, sizeof(uplinkPayload));    
  debug((state < RADIOLIB_ERR_NONE) && (state != RADIOLIB_ERR_NONE), F("Error in sendReceive"), state, false);

  Serial.print(F("FCntUp: "));
  Serial.println(node.getFCntUp());

  // now save session to RTC memory
  uint8_t *persist = node.getBufferSession();
  memcpy(LWsession, persist, RADIOLIB_LORAWAN_SESSION_BUF_SIZE);
  
  // wait until next uplink - observing legal & TTN FUP constraints
  gotoSleep(uplinkIntervalSeconds);

}


// The ESP32 wakes from deep-sleep and starts from the very beginning.
// It then goes back to sleep, so loop() is never called and which is
// why it is empty.

void loop() {}
Code for config.h (not my actual DevUI and AppKey):
#ifndef _RADIOLIB_EX_LORAWAN_CONFIG_H
#define _RADIOLIB_EX_LORAWAN_CONFIG_H

#include <RadioLib.h>

// first you have to set your radio model and pin configuration
// this is provided just as a default example
SX1262 radio = new Module(8, 14, 12, 13);

// 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 = 5UL * 60UL;    // minutes x seconds, sends every 15 minutes

// 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   0x0000000000000000
#endif
#ifndef RADIOLIB_LORAWAN_APP_KEY   // Replace with your App Key 
#define RADIOLIB_LORAWAN_APP_KEY   {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
#endif
//#ifndef RADIOLIB_LORAWAN_NWK_KEY   // Put your Nwk Key here
//#define RADIOLIB_LORAWAN_NWK_KEY   {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
//#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 = EU868;
const uint8_t subBand = 0;  // 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

In the Serial Monitor, I receive the output Join failed: -1116 and the TTN console states DevNonce is too small. This is the code I’ve been using for almost a year without issues (only changed calibration values some times [newest calibration from last week is not yet included]).

The device is using LoRaWAN Specification 1.0.4 and RP002 Regional Parameters 1.0.4 ever since I created it last year in August. Up to last week, it worked flawlessly. I never encountered the DevNonce error before. My gateway is up and receiving uplinks.

@CaptainAwesome is feeling very sad - :sob: I will buy him some chocolate for when I see him next.

Read his hand crafted notes.md that is in the LoRaWAN Starter folder - which also says to use LW 1.1.0 because that was the primary version at the time but the more recent version does now support v1.0.4 so please don’t make our lives worse by changing that in retrospect.

Be aware that the LW version is not the Regional Parameters version, which is a subset of a LW version.

I can only guess that you were using one board for development and you’ve now transferred to a new board but with the same credentials. The persistence on the first board would have kept the Dev Nonces but the new board doesn’t know about the first board or where it got to in the count, so it’s started at 0, quite rightly but the server has a much bigger join count.

Which is why we turn off the Join Nonces check when developing. For deployment of new devices in bulk we’d leave it on. But not for making stuff, as you have discovered.

This is entirely a LW thing and the situation you are in is like driving a car and you have no idea what is under the bonnet - is it petrol, diesel, hybrid, electric only, hydrogen or hamsters. Which means when you have a breakdown, you have no idea what’s what. So please read the basics of LW so you have context to when you Google an error message and find out that it is a fundamental.

DO NOT WATCH THE VIDEO until you have read each section. And do not watch the video on the web page, watch it on YouTube and ONLY watch one section at a time.

You do not have to know or understand everything in what is the bare minimum to successfully implement LW (as noted by @CaptainAwesome in the second paragraph of notes.md) - but it will seed your brain so when a hiccup occurs, your reticular activation system (next to the basal ganglia), starts niggling away that you’ve seen something about this and will be better placed to do simple natural language text queries in Google and have a feel for what the results are. Or not even a normal sentence - just Join failed DevNonce is too small yields a result in the “AI” Google results box. I tend to start out with all the text but then eliminate anything specific - in this case the error code of -1116 which you will most assuredly know from the RadioLib docs is in the TypeDef.h file because you’ve look around the library you are relying on - so that Google doesn’t get distracted with domain specific details.

If you want to be the go to person for environmental monitoring in your field, this stuff is essential. And puts you in the upper 90th quartile of developers.

Thank you for the in-depth reply and resources.

I hate to tell you this but I’m still using the exact same board, no changes there. Which is why the error confuses me so much, as just last week it was working fine, see the image below from my ThingSpeak as proof.

No bother to me, just another fact to add to the mix.

You may have done a full chip erase, either consciously or not, which would clear the info on the Nonces held in the device. Or the store command faltered. Or the code was disabled. Or the device got hit by a neutrino (cosmic ray) or an alpha particle or a gamma ray and flipped a bit in the flash memory. Or a sudden increase in UV output from the Petrova line. Which could be real - take a picture of a RPi2 with the camera flash on - it reboots!

But mostly it doesn’t matter as this sort of glitch is just another day in the life of a developer and comes up as part of the LW developing landscape, along with a whole host of other issues. And then there randoms in embedded dev 101: compiling, flashing, the serial output going awol, not resetting etc etc etc.

Anything can happen on the bench, which is why testing somewhere else but close to hand with a new(ish) bit of hardware is super important as only that really confirms if it’s working or if it’s being affected by the locality of someone watching.

The fundamental solution remains the same, it doesn’t matter HOW you arrived at this place, just turn off Join Nonce checks on the console.

Somewhere half a year to a year ago or so I broke the Nonces layout. So if you updated RadioLib from say 7.1 to 7.6 and uploaded new code, that’s likely the reason. But the easy solution while developing is indeed as per Nick - check the button.

You will HAVE to reset the nonces to get out of this - or wait a long time for the join to reach the previous number.

Thanks, turning that on did work. Web accessibility wise I’m concerned about them having “enabled” permanently next to a check box because that’s confusing but that’s just me complaining on the side. One more question: If I reset the used DevNonces, could I then disable the “resets join nonces” option again or is that like trying to extinguish an oil fire with water?

Back to the original problem. A delay of 2 minutes with delay(120000); just stops code execution for two minutes. If I do this after sensor initialization, the issue is that I got the value from two minutes ago. I need new values for two minutes, then after those two minutes get and send the newest value. Feels like a classic while-loop or if-else condition checking passed time since starting time. I can do both in a completely normal code but RadioLib still confuses me in the aspect that everything is happening in the setup() part and I fear that some things I would normally do in the loop() part will not work there. I’ll have to leave for a while now but I’ll keep thinking about how to solve this.

Up to you, either you tattoo the information somewhere so you don’t trip over it again, or you leave it enabled so you can get on with developing.

The attack vector, effort required to decrypt & inclination by people to try to clone your development device over the airwaves makes for a study in the hugely not likely. I’m more likely to be able to prove that your use of LW for a publicly funded / academic project doing environmental monitoring is in breach of EU laws about data sharing.

As for the latter, please could you arrange some chemical lobotomy for your next seriously hard thinking session and chill-ax. Find a friend, give them some bread, butter & jam and instruct them on how to make a sandwich which will start to embed how simple procedural programming is or should be. You choose the sequence, you transcribe the sequence in to code. Done.

The setup() and loop() thing is just Arduino simplification, you can do what the heck you like, where you want, when you want. But Arduino sort of spoon feeds people with a setup section and a loop section. It calls the setup section when it starts doing Arduino things and then when that finishes, there is an internal loop that calls loop() perpetually.

There is nothing wrong with you leaving loop() empty at all. And if you want a pile of loops in setup, that’s up to you - Arduino can’t tell nor does it care. And it’s not RadioLib doing everything in the setup(), it’s ESP32 deep sleep restarting that forces RadioLib or indeed anything using ESP32 deep sleep to run in the setup because Arduino, at which point we form a möbius strip out of the whole thing and disappear up our own …

Other, far more sensible and less power hungry platforms better suited for battery powered LW IoT sensors, don’t do any of this Arduino setup / loop stuff and definitely don’t flat-line the whole processor to deep sleep - they can just deep sleep and then carry on. But I digress.

And RadioLib only has blocking calls - if you want it to do something, you call it’s function, it does it and then it comes back. So if you want to do a loop that runs for a few days before asking RadioLib to do something, that’s all good. So loops aren’t bad for RadioLib.

Before we get to a sort of solution/hint, let’s reiterate what was recently shared - there is nothing special about setup() and loop(). You can code what you like in either. There are no particular restrictions on where your code goes and the only agency that may break down your door to mercilessly interrogate you for where you put your code is going to be ICE because they do what they like.

However, you are safe, because you can put it in the loop - hurray - if you do periodic calls, one for reading the sensor and one that finally triggers the payload build, send and sleep. Or periodic calls with a counter, where the counter triggers the end phase.

What would be simpler is in setup you can have a for loop that counts a number of readings with a delay after each one. And after build the payload, send and then sleep.

Try to break this down in to small chunks - if you aren’t up on for loops, learn about them - don’t apply them using CopynPasta until you understand them.

You can do it with a while loop which is a slightly more verbose version of a for loop. I’m not seeing how if else helps without a loop.

But that’s trying to apply the clever voodoo strange words that is code - as in, what magic incantation should I invoke to do this. Rather you should ask yourself what it should do in natural language and then apply the code constructs that matches.