ESP32 over LTE-M, NB-IoT and Ethernet with Arduino

Connect an ESP32 over Ethernet, LTE-M or NB-IoT with the HydroNode Arduino library, fall back from WiFi to LTE, buffer readings offline, send once an hour.

HydroNode 8 min read
The HydroNode device list with a weather station that reports over LTE-MThe HydroNode device list with a weather station that reports over LTE-M

Not every sensor sits within reach of a router. The field, the allotment, the weather mast on the barn roof. From version 1.9.0 the HydroNode Arduino library therefore connects an ESP32 not only over WiFi but also over Ethernet, LTE-M, NB-IoT and LTE Cat-1, if you like with WiFi first and cellular as the fallback. On top comes a buffer: readings are no longer lost when the network is gone, and a battery device can collect for an hour and then send everything at once.

The idea: the network is a part of its own

The signature of every request does not depend on WiFi. It covers the values and the time, however the bytes travel. In 1.9.0 the network is therefore an object of its own, the transport. You pick the one for your hardware and keep the rest of the sketch: values, commands, battery thresholds, updates.

Transport.inoC++
// Before: WiFi built in
HydroNode hydro(SENSOR_ID, SECRET_KEY);

// Since 1.9.0: the network is its own object, the rest stays the same
HydroNodeWiFi wifi;                                  // HOW the board reaches the internet
HydroNodeClient hydro(wifi, SENSOR_ID, SECRET_KEY);  // WHAT it sends
NetworkHardwareExample in the library
WiFiEvery ESP32 and ESP8266QuickStart, Transport_WiFi
EthernetESP32 with a port (WT32-ETH01, Olimex ESP32-POE)Ethernet_ESP32
EthernetW5500 module on SPI, any boardEthernet_W5500
LTE-M, NB-IoTSIM7080G, SIM7000, BG95, SARA-R4Cellular_SendNow, Cellular_CollectHourly
LTE Cat-1A7670, SIM7600, TLS in the modemCellular_ModemTLS
WiFi, else LTE-MESP32 with a modemWiFi_Fallback_LTE
Any TLS clientUNO R4 WiFi, MKR, Nano 33 IoT, Pico WBoard_MKR_WiFi1010, Board_AnyClient_Template
Old sketches keep workingHydroNode hydro(id, secret) is now the client with WiFi built in. Every sketch written for 1.x compiles and behaves as before. Only what you include is compiled: a WiFi-only sketch grows by about 2 KB, modem code only ends up in sketches that include HydroNodeCellular.h.

LTE-M and NB-IoT

An ESP32 and a modemFor example a SIM7080G breakout or the LilyGO T-SIM7080G-S3.
A SIM with dataIoT SIMs like 1NCE or EMnify, or any SIM with LTE-M. Ask your provider whether LTE-M or NB-IoT reaches you.
Two more librariesTinyGSM for the modem, ESP_SSLClient for TLS on the ESP32. Both in the Library Manager.
A strong supplyThe modem draws peaks around 2 A while it sends.

TinyGSM drives the modem, ESP_SSLClient encrypts on the ESP32 and checks HydroNode's certificate against the bundled root certificates. The transport takes the time from the modem and reports the signal in dBm. LTE-M is faster and works on the move, NB-IoT reaches deeper into cellars and shafts.

Cellular_SendNow.inoC++
#define TINY_GSM_MODEM_SIM7080
#include <TinyGsmClient.h>
#include <ESP_SSLClient.h>
#include <HydroNodeClient.h>
#include <HydroNodeCellular.h>
#include <HydroNodeCerts.h>

TinyGsm modem(Serial1);
TinyGsmClient tcp(modem);
ESP_SSLClient tls;                                   // TLS on the ESP32, any modem
HydroNodeCellular<TinyGsm> lte(modem, tls, HydroNodeNet::LTE_M);
HydroNodeClient hydro(lte, SENSOR_ID, SECRET_KEY);

void setup() {
  Serial1.begin(115200, SERIAL_8N1, 4, 5);          // modem RX, TX
  tls.setClient(&tcp);
  tls.setCACert(HYDRONODE_CA_BUNDLE);
  lte.onBeforeConnect([](uint32_t now) { tls.setX509Time(now); });
  hydro.setHttpTimeout(30000);                       // radio round trips take longer

  modem.init();
  modem.setPreferredMode(1);                         // 1 LTE-M, 2 NB-IoT
  modem.waitForNetwork(180000L);
  modem.gprsConnect("iot.1nce.net");                 // the APN of your SIM
}

void loop() {
  HydroNodeValue reading[] = {{"TEMPERATURE", 14.2}, {"HUMIDITY", 81}};
  hydro.sendValues(reading, 2);
  delay(5UL * 60UL * 1000UL);
}

On the Dashboard the device then shows LTE-M, -97 dBm under Signal instead of a WiFi strength.

Collect and send once an hour

On a battery the modem is the biggest consumer. Registering and TLS cost the same for one value as for ten. So the example Cellular_CollectHourly measures every six minutes, puts the round into RTC memory and sleeps again. Only on the tenth time does the modem come on and send every round in one request.

  1. Every 6 minMeasure, record(), sleep
  2. 10 roundsOr the oldest is 1 h old
  3. flush()Modem on, one request
  4. 202Each reading at its own time
Every reading keeps its time. In the chart it looks as if each one had been sent right away.
Cellular_CollectHourly.inoC++
RTC_DATA_ATTR uint8_t mem[4096];                     // survives deep sleep
HydroNodeBuffer buffer(mem, sizeof(mem));

void setup() {
  // modem, TLS and hydro as above
  buffer.begin();                                    // keeps the rounds of the last wake-ups
  hydro.attachBuffer(buffer);

  HydroNodeValue reading[] = {{"TEMPERATURE", readTemp()}, {"SOIL_MOISTURE", readSoil()}};
  hydro.record(reading, 2);                          // store, radio stays off

  if (hydro.flushDue(10, 3600) && modemUp()) {       // 10 stored, or the oldest is 1 h old
    hydro.flush();                                   // all of them, one request
    modemDown();
  }
  esp_deep_sleep(6ULL * 60 * 1000000);               // next reading in 6 minutes
}
  • Roughly 80 % less radio energy and a fraction of the data, about 1.5 KB instead of 10 KB an hour.
  • HydroNode learns the rhythm from the deliveries. A device that sends hourly counts as delayed after three hours, not after 15 minutes.
  • Commands from the app arrive with the hourly answer.
  • If a send fails, the rounds stay in the buffer for up to seven days. When it is full, the oldest go first.

Never lose a reading again

The buffer helps plain WiFi devices too. Attached to the client, sendValues() stores a round that did not go out and returns -7. With the next request that works, the stored rounds follow. The router restarts, the internet is gone for an hour: nothing is missing in the chart.

Buffer_OfflineSafe.inoC++
static uint8_t mem[8192];                            // RAM, or save it to LittleFS
HydroNodeBuffer buffer(mem, sizeof(mem));

void setup() {
  buffer.begin();
  hydro.attachBuffer(buffer);                        // that is all
}

void loop() {
  int status = hydro.sendValues(reading, 2);         // -7 = stored, sent later
  delay(60000);
}

You decide where the buffer lives: in RAM, in the RTC memory of the ESP32 (survives deep sleep) or in flash (survives a power cut, see Buffer_OfflineSafe). At start, buffer.begin() checks whether the content is intact and only then keeps it.

Sent twice, stored once Stored rounds go to an endpoint of their own. Every round carries its own signature, and a round that arrives twice because the answer got lost on the way is still in the chart only once. In the console such lines show when they were measured and when they were sent.

WiFi first, LTE as the fallback

A device at the edge of WiFi range, or one that must report when the router fails: HydroNodeFailover takes the first network that is up, per request. Once WiFi is back, requests go over it again and the modem can switch off.

WiFi_Fallback_LTE.inoC++
HydroNodeWiFi wifi;
HydroNodeCellular<TinyGsm> lte(modem, tls, HydroNodeNet::LTE_M);
HydroNodeFailover net(wifi, lte);                    // WiFi while it is up, else LTE-M
HydroNodeClient hydro(net, SENSOR_ID, SECRET_KEY);

Where to go next