Ultra low power long range LoRa sensor

Hello. If you want to have truly low power sensor, forget WiFi and go LoRa. I would like to share my experience and examples here.

I built LoRa sender sensor, that sends incremental counter and battery status every hour. Hardware is LoRa Radio Node v1.0, compatible with Arduino Mini Pro 3.3V. The cost is about 15$. In order to measure battery as chip power, I bypassed LDO and I connected LiIon battery directly to 3.3V. I know it is not recommended but in years I never had single problem with powering Atmel or ESP8266 or ESP32 by LiIon battery directly connected to 3.3V, even if it has max 4.2V. The sensor takes less than 80µA in deep sleep mode and duty cycle is very short because it does not need to connect to WiFi, it just sends out LoRa packet. I estimate that duty cycle takes about 1s. In the sensor, you have to manually put wire jumper between DIO0 and GPIO2 terminals. Range of sensor is about 300m in heavy built street, with receiver behind several walls.

Then I built LoRa to MQTT gateway based on LilyGo LoRa32. That gateway takes string sent by LoRa sensor, strips out first byte and uses that byte as MQTT topic.

Code for LoRa sensor. You have to tweak it - check frequency of LoRa, frequency how often you want to send measurements and battery data. You also may need to change calibration contstant in Vcc read routine.


// Compile for Arduino Pro or Pro Mini

#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>  
#include "LowPower.h"

//############################## User definitions ###################
#define sensor_loop_min 60  // How often to check sensor
#define battery_loop_min 60  // How often to check and report battery

char BN = 255;   // Battery number, 255 is test
char SN = 254;   // Sensor number, 254 is test

int counter = 1;
unsigned long stopwatch_counter = 0;

boolean debug = false;

//###################################################################

unsigned long prev_millis_b = millis();  //period to send battery readings
unsigned long prev_millis_s = millis();  //period to send sensor readings
unsigned long rt_millis;

boolean just_booted = true;

#define SS      10   // SX1278's CS
#define RST     9   // SX1278's RESET
#define DI0     2   // SX1278's IRQ(Interrupt Request)
#define BAND  868E6 // Frequency for Europe

#define INTERNAL_LED 13

String rssi = "RSSI --";
String packSize = "--";
String packet ;


void setup() {

  Serial.begin(115200);
  while (!Serial);
  Serial.println();
  Serial.println("LoRa Sender Test");
  
  SPI.begin(); //SCK,MISO,MOSI,SS);
  LoRa.setPins(SS,RST,DI0);
  if (!LoRa.begin(BAND)) {
    Serial.println("Starting LoRa failed!");
    while (1);
  }
  Serial.println("init ok");
   
  delay(1500);

  rt_millis = stopWatch();

}


//###############################  START  ##############################

void loop() {

  if (debug) Serial.println("Millis: "+String((rt_millis/1000.0),3));
  
  if (just_booted || ((rt_millis - prev_millis_b) > battery_loop_min*6E4)) {
    Serial.println("      Battery run!");
    String SVcc = String(readVcc()/1000.0);
    dispatchPacket(BN+SVcc);
    prev_millis_b = rt_millis; 
    }
  

  // Sensor loop can only do check and if values are outside range then send packet
  if (just_booted || ((rt_millis - prev_millis_s) > sensor_loop_min*6E4)) {
    Serial.println("      Sensor run!");
    float sensor_value = 0;

    // ############ Insert sensor reading and logic here #############

    // Here we only send counter, replace it with real sensor value
    String Ssensor = String(counter); //sensor_value);
    dispatchPacket(SN+Ssensor);
    counter++;
    prev_millis_s = rt_millis; 
    }

  if (debug) Serial.flush();
  
  just_booted = false;  

  LowPower.powerDown(SLEEP_8S,ADC_OFF, BOD_OFF);
  rt_millis = rt_millis + stopWatch() + 8000;

}


// #############################  END  ###############################

long readVcc() {
   long result;
   // Read 1.1V reference against AVcc
   ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); // orig for Pro Mini
   //ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1); // modified for 1284P
   delay(2); // Wait for Vref to settle
   ADCSRA |= _BV(ADSC); // Convert
   while (bit_is_set(ADCSRA,ADSC));
   result = ADCL;
   result |= ADCH<<8;
   result = 1105000L / result; // Back-calculate AVcc in mV
   return result;
   }

void shutdownLoRa() {
  LoRa.end();
  //digitalWrite(RST, LOW);  // I tried to lower current by messing up with pins but it did not work
  //pinMode(RST, INPUT); 
  //pinMode(SS, INPUT); 
  }

void activateLoRa() {
  //pinMode(RST, OUTPUT); 
  //pinMode(SS, OUTPUT); 
  //digitalWrite(RST, HIGH); 
  LoRa.begin(BAND);
  }

void dispatchPacket( String pkt) {
  activateLoRa();
  //delay(500);
  //rt_millis = rt_millis + 500;
  Serial.print("Begin packet...");
  LoRa.beginPacket();
  Serial.println("...done");
  Serial.print("LoRa print...");
  LoRa.print(pkt);
  Serial.println("...done");
  Serial.print("battery...");
  //LoRa.print(SVcc);
  Serial.println("...done");
  Serial.print("endPacket...");
  LoRa.endPacket();
  Serial.println("...done");
  Serial.flush();
  shutdownLoRa();
}

unsigned long stopWatch() {
  unsigned long difference = millis() - stopwatch_counter;
  stopwatch_counter = millis();
  return difference;
  }

Code for LoRa to MQTT gateway is here, working on LilyGo LORA32 with OLED display. Again, tweak it as you need, change WiFi, gateway IP address, accesses etc.

#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
//#include "SSD1306.h"
#include "SSD1306Wire.h"
#include "images.h"  // get this file from https://github.com/imelekhin/LoRatoMQTT
#include <WiFi.h>
#include <PubSubClient.h>


// Pin definetion of WIFI LoRa 32
// HelTec AutoMation 2017 [email protected]
#define SCK     5    // GPIO5  -- SX127x's SCK
#define MISO    19   // GPIO19 -- SX127x's MISO
#define MOSI    27   // GPIO27 -- SX127x's MOSI
#define SS      18   // GPIO18 -- SX127x's CS
#define RST     14   // GPIO14 -- SX127x's RESET
#define DI00    26   // GPIO26 -- SX127x's IRQ(Interrupt Request)

#define BAND    868E6  //you can set band here directly,e.g. 868E6,915E6
#define PABOOST true

typedef struct {
  byte src_addr;
  char msg[255];
  unsigned long count;
} _l_packet;

_l_packet pkt;
boolean gotpacket;


SSD1306Wire display(0x3c, 21, 22); //4, 15);

String displaymessages[5] = {"str1", "str2", "str3", "str4", "str5"};



void logo() {
  display.clear();
  display.drawXbm(0, 5, logo_width, logo_height, logo_bits);
  display.display();
}



const char* ssid = "yourwifi";
const char* password =  "yourwifipass";
const char* mqttServer = "10.0.0.54";
const int mqttPort = 1883;
const char* mqttUser = "mqttgwuser";
const char* mqttPassword = "mqttgwpass";

WiFiClient espClient;
PubSubClient client(espClient);

void setup() {

  Serial.begin(115200);


  gotpacket = false;
  pkt.src_addr = 0;
  //pkt.msg;
  pkt.count = 0;
  initdisplay();
  initwifi();
  initlora();
  initmqtt();
  //LoRa.setSyncWord(0x12);  //priv network. WAN: 0x34);  //0x3444 for SX126x
  LoRa.receive();


}

void loop() {
  String topic;
  client.loop();


  if (WiFi.status() == WL_CONNECTED) {
    displaymessages[0] = "WiFi OK, RSSI :" + String(WiFi.RSSI());
  } else {
    displaymessages[0] = "WiFi disconnected, restart in 15sec";
    delay(15000);
    ESP.restart();
  }


  if (client.connected()) {
    displaymessages[1] = "MQTT connected";
  } else {
    displaymessages[1] = "MQTT disconnected, restart in 15sec";
    delay(15000);
    ESP.restart();
  }


  if (gotpacket) {
    gotpacket = false;
    topic = "lora_mqtt_gw/" + String(pkt.src_addr);
    Serial.print("###");
    Serial.print(topic.c_str());
    Serial.println("###");
    Serial.print("###");
    Serial.print((const char *)&pkt.msg[0]);
    Serial.println("###");
    Serial.println();
    Serial.flush();
    client.publish(topic.c_str(), (const char *)&pkt.msg[0], true);
    //client.publish("", (const char *)&pkt.msg[0]);
    displaymessages[4] = pkt.msg;
    displaymessages[3] = "Got pkt#" + String(pkt.count) + " RSSI:" + String(LoRa.packetRssi());
    displaymessages[2] = "LoRa SNR" + String(LoRa.packetSnr());
  }

  statusDisplay();

}

void messageLog(const char *msg) {
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 0, msg);
  display.display();
}

void statusDisplay() {
  display.clear();
  display.setTextAlignment(TEXT_ALIGN_LEFT);
  display.setFont(ArialMT_Plain_10);
  for (int i = 0; i <= 3; i++) {
    display.drawString(1 , 12 * i , displaymessages[i]);
  }
  display.drawStringMaxWidth(0 , 48 , 128, displaymessages[4]);
  display.display();
}

void initdisplay() {
  pinMode(16, OUTPUT);
  pinMode(25, OUTPUT);
  digitalWrite(16, LOW);    // set GPIO16 low to reset OLED
  delay(50);
  digitalWrite(16, HIGH); // while OLED is running, must set GPIO16 in high
  display.init();
  display.flipScreenVertically();
  display.setFont(ArialMT_Plain_10);
  logo();
  delay(1500);
  display.clear();
}


void initwifi() {

  //WiFi.setTimeout(30*1000);
  WiFi.begin(ssid, password);
  long timeout = millis()+30000;
  while ( WiFi.status() !=  WL_CONNECTED) {
    messageLog("Connecting to WiFi..");
    if (millis() > timeout) {
      messageLog("No WiFi, restarting...");
      delay(5000);
      ESP.restart(); 
    }
  }

  messageLog("Connected to the WiFi network");
  delay(1000);
}


void initmqtt() {

  client.setServer(mqttServer, mqttPort);

  while (!client.connect("ESP32Client", mqttUser, mqttPassword )) {
    messageLog("Connecting to MQTT...");

  }
  messageLog("Connected to MQTT... ");
  delay(1000);
}


void initlora() {
  SPI.begin(SCK, MISO, MOSI, SS);
  LoRa.setPins(SS, RST, DI00);

  while (!LoRa.begin(BAND)); //, PABOOST))
  {
    messageLog("Initialising LoRa module....");
  }

  messageLog("LoRa Init success!");
  delay(1000);

  LoRa.onReceive(onReceive);

}


void onReceive(int packetSize)
{

  LoRa.readBytes((uint8_t *)&pkt, packetSize);
  gotpacket = true;
  pkt.msg[packetSize - 1] = '\0';
  pkt.count++;

}
3 Likes

Nice. Do you have any data about range in open space ? What antenna do you use for node and gateway ? How much is the tx power of the node ?

I had pretty good results with SI4463 based modules on the 433MHz band with line of sight distances up to 1km using a tx power of +20dBm. But LoRa can probably go even farther if using a good antenna and with much lower power.

I’m not too familiar with the LoRa libs you used. Do they offer reliable data links with ACK and resend requests ?

1 Like

Hi, sorry I am even less technical with LoRa than you are. For technical details about the library, setting output power and using ACK you can look at github here: https://github.com/sandeepmistry/arduino-LoRa . I also think that ACK needs to be supported by the receiver, which uses same library, but not on Atmel/SX, but on ESP32/SX chips.

I tested on hundreds or probably thousands of transmissions and it never missed one.

Thanks, I’ll look into the library you mentioned. ACK can be either hardware based or be done entirely in software. So even if the radios don’t support it natively, it can be added to the firmware.

Sadly that doesn’t mean much. The RF medium is very volatile by nature. Sometimes you can have perfect message delivery and then all of the sudden you will start missing messages, because of unknown reasons (human made interferences, a thunderstorm far away, rain or fog, etc). A protocol without some form of ACK can never be reliable, so especially for long range that reduces use cases significantly (unless you just want to send repeated data like temperature or humidity where missing a measure isn’t a big deal).

Anyway, I am glad there is someone with same interest, who can probably improve what I have done :slight_smile:

LoRa is a great platform for a certain category of IoT devices. I’m glad that people like you experiment around with it, common DIY IoT stuff is always the same boring ESP Wifi stuff :slight_smile:

I know extremely little about LoRa, but I’ve become intrigued since learning about it via Pine64’s open, riscV chip the BL602, which sounds like it’s capable of transmitting/receiving LoRa and is only a few bucks.

I’d be really interested to see what could be done with this!

1 Like

I have that classic LoRa problem - a semi rural letterbox roughly 100m from the house. I am pleased to see this thread, thank you.

1 Like

I also had to solve mailbox notifications. For that I built simple circuit from PIR sensor, which triggers a relay and relay powers simple ESP32-LORA board. PIR sensor is inside mailbox so it detects when it is open and mail is dropped in. I set PIR sensor output to about 20 seconds. During that time I power ESP32 up, it sends LORA messages and then it is turned off by relay. This has by far the lowest battery consumption of all I tested, battery drops by milivolts per month and no fiddling with interrupts, deep sleep etc.

4 Likes

My mailbox has a slot which you insert the letters in, and usually you only open the door when collecting mail, or putting a package in (ie bigger than the slot).

Would a PIR trigger just on an envelope being pushed through the slot?

I have the same. I moved PIR sensor to inside right behind covered slot. This way when postman inserts letter and opens the slot cover, lightning conditions change on PIR and it turns on. Sometimes letter turns the box with the PIR over and it triggers it as well.

PIR sounds like it can be triggered/compromised by a lot of stuff, as you rely on a change in the IR heat readings between the different zones. I can imagine that under certain conditions it may not trigger, like when the sun shines on the mailbox and the high ambient temperature makes the PIR “blind” to changes. Or some bug sits on the lens while reading your mail. Or it may give false positives if e.g. someone left the door open?

Did you test or consider other types of sensors? Thinking along the lines of old-fashioned limit switches on the flap/back door, or Hall effect transistors and little magnets if you don’t want moving parts. Has the added value that you can see if someone left the back door open and your mail will escape (or get wet when the rain starts). No power consumption, they can give a nice wake interrupt if debounced, are immune to interference from insects, and perhaps the most reliable overall.

If you have a solar panel then perhaps even ultrasonic sensors. Low power, relative low cost, not depending on/affected by heat, and even waterproof if you use a car parking sensor. But it’s indeed not passive and draws current all the time, albeit very little. And it may just be triggered by that reading bug again.

1 Like

Be cool and measure inside space of mailbox with laser: VL53L0X :slight_smile: I use it to measure when my flower is starving for water and leafs are going down. Works well, my flower then spams me on Telegram

Even though your Arduino (or ESP8266/ESP32) survives getting fed by a LiIon you should consider a LiFePo4 battery instead. Keeps voltage withing spec. when bypasing the LDO

2 Likes

I’ve been running LoRaWAN devices for quite a while - yes transmission is dropped every now and again. IMO you need to tolerate this for your application - in my case a temperature reading is dropped and is only transmitted every 20-30 minutes. So you can see that is fine for some applications (long term monitoring) but not for others (short-term air-conditioning control). You can use the protocol for critical alerts however (eg. leak detection) where it will repeatedly transmit; the main thing is you want it to not be ‘normally’ transmitting too often, and therefore not drain the battery too quickly.

hello I would like to do the same project could you post the code and the material to buy thanks this way I can’t go wrong thanks