Fail-proof a LoRa WiFi POST project

Hi,

I started a new project and just started learning Arduino a few days ago.
The code I wrote seem to work smoothly, though I wonder whether it is fail-proof enough, because the microcontrollers are intended to be left unsupervised.

Here is the context:
We have several buildings that may get a power outage at any time in the common sections of a any of the buildings.
So the idea is to put a client microcontroller in each building, and a gateway microcontroller in a private apartment (private apartments are less prone to power outages).
The gateway pings each client sequencially via LoRa, the client answers the gateway via LoRa again, and then the gateway sends an HTTP POST request to a server via WiFi, and the server timestamps the request.
If a building has a power outage, the client does not answer, the HTTP POST request is skipped, and therefore we know that a building has a power outage.

I would like to get opinions about whether the code is fail-proof enough or not in that context, and also if it can be improved in general.
My background is that I started to learn Python since December, so I kind of have a beginner-intermediate level.

I am using TTGO ESP32-Paxcounter LoRa32 V2.1 1.6 Version 868 MHz LoRa ESP-32 OLED 0,96" MCUs.
They are plugged to power sockets via a USB cable and a power adaptor.

Here is the code for the gateway:

//Bibliothèques pour LoRa
#include <SPI.h>
#include <LoRa.h>

//Bibliothèques pour l'écran OLED
#include <Wire.h>
#include "SSD1306.h"

//Bibliothèques pour la requête HTTP
#include <WiFi.h>
#include <HTTPClient.h>

//Définition des pins utilisés par l'émetteur/receveur LoRa
#define SCK     5    // GPIO5  -- SCK
#define MISO    19   // GPIO19 -- MISO
#define MOSI    27   // GPIO27 -- MOSI
#define SS      18   // GPIO18 -- CS
#define RST     23   // GPIO14 -- RESET (If Lora does not work, replace it with GPIO14)
#define DI0     26   // GPIO26 -- IRQ(Interrupt Request)
#define BAND    redacted // 868E6
// Décision 2006/771/CE modifiée
// Par coefficient d’utilisation, on entend le rapport de temps, sur une heure, 
// durant lequel un dispositif particulier émet effectivement.
// Les conditions moins restrictives au sens de l’article 3, paragraphe 3 signifient que 
// les États membres peuvent autoriser une valeur supérieure pour le coefficient d’utilisation.

SSD1306 display(0x3c, 21, 22);
String rssi = "RSSI --";
String packSize = "--";
String packet ;

const int csPin = 18;          // LoRa radio chip select
const int resetPin = 23;       // LoRa radio reset
const int irqPin = 26;         // change for your board; must be a hardware interrupt pin

long unsigned lastSendTime = 0;

#define WIFI_NETWORK_1 "redacted"
#define WIFI_PASSWORD_1 "redacted"
#define WIFI_NETWORK_2 "redacted"
#define WIFI_PASSWORD_2 "redacted"
const char* host = "https://redacted.redacted"; // hostname of web server:
const char* path = "/redacted/";
const char* liveToken = "redacted";
#define WIFI_TIMEOUT_MS 20000

byte localAddress = 0xREDACTED0;
String destinationName[] = {"a", "b", "c", "d", "e", "f", "sg"} ;
byte destinationAddress[] = {0xREDACTED1, 0xREDACTED2, 0xREDACTED3, 0xREDACTED4, 0xREDACTED5, 0xREDACTED6, 0xREDACTED7};
int destinationAddressLength;
int isInDestinationAddress;
int destinationAddressCheck;
int interval = 5000;
String aucunMessage = "0";
int i = 0;
byte incomingSize;
byte outgoingSize;


void sendMessage(String outgoing, byte destinationAddress) {
  LoRa.beginPacket();
  LoRa.write(destinationAddress);
  LoRa.write(localAddress);
  LoRa.write(outgoing.length());
  LoRa.print(outgoing);
  LoRa.endPacket();
    
  display.clear();
  display.drawString(0, 0, "redacted_email");
  display.drawString(0, 10, "   .redacted@redacted.com");
  display.drawString(0, 30, "Envoi au bâtiment : " + String(outgoing));
  display.display();
  
  outgoingSize = sizeof(outgoing);
  Serial.print("Message envoyé : " + String(outgoing));
  Serial.print(" depuis 0x" + String(localAddress, HEX));
  Serial.println(" à 0x" + String(destinationAddress, HEX));
  Serial.println("Longueur du message envoyé : " + String(outgoingSize) + " bytes");
}

void receiveMessage(int packetSize, String destName) {
  if (packetSize == 0) return;

  int recipient = LoRa.read();
  byte sender = LoRa.read();
  byte incomingLength = LoRa.read();

  String incoming = "";

  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  if (incomingLength != incoming.length()) {
    Serial.println("Erreur : La longueur du message ne correspond pas");
    return;
  }

  if (recipient != localAddress) {
    Serial.println("Erreur: L'adresse du destinataire ne correspond pas");
    return;
  }

  isInDestinationAddress = 0;  // L'envoyeur n'est pas autorisé.
  for(int destinationAddressCheck = 0; destinationAddressCheck < destinationAddressLength; destinationAddressCheck++){
    if (sender == destinationAddress[destinationAddressCheck]){
      isInDestinationAddress = 1;  // L'envoyeur est autorisé.
      break;
    }  // Sinon isInDestinationAddress reste sur 0, l'envoyeur n'est pas autorisé.
  }
  if (isInDestinationAddress == 0) {  // Si l'envoyeur n'est pas autorisé.
    Serial.println("Expéditeur non autorisé : 0x" + String(sender, HEX));
    display.clear();
    display.drawString(0, 0, "Expéditeur non autorisé : \n0x" + String(sender, HEX));
    display.drawString(0, 30, "redacted_email");
    display.drawString(0, 40, "   .redacted@redacted.com");
    display.display();
    return;
  }
  
  int rssi = LoRa.packetRssi();
  float snr = LoRa.packetSnr();
  long freqErr = LoRa.packetFrequencyError();
  
  display.clear();
  display.drawString(0, 0, "redacted_email");
  display.drawString(0, 10, "   .redacted@redacted.com");
  display.drawString(0, 20, "Message reçu : " + String(incoming));
  display.drawString(0, 33, "RSSI : " + String(rssi));
  display.drawString(0, 43, "SNR : " + String(snr));
  display.drawString(0, 53, "Erreurs : " + String(freqErr));
  display.display();
  
  incomingSize = sizeof(incoming);
  Serial.println("Longueur du message reçu : " + String(incomingSize) + " bytes");
  Serial.println() * 2;
  Serial.print("Message reçu : " + String(incoming));
  Serial.print(" depuis 0x" + String(sender, HEX));
  Serial.println(" à 0x" + String(recipient, HEX));
  Serial.println("RSSI : " + String(rssi));
  Serial.println("SNR : " + String(snr));
  Serial.println("Erreurs : " + String(freqErr));

  post(destName, incoming);
}

void post(String batiment, String message){
  HTTPClient http;
  String url = String(host) + String(path);
  http.begin(url);
  http.setConnectTimeout(1500);  // Durée maximale de la tentative de connexion
  http.setTimeout(1500);  // Durée maximale lorsqu'on est déjà connecté au serveur

  http.addHeader("Content-Type", "application/x-www-form-urlencoded");
  
  String payload = String("token=") + String(liveToken) + String("batiment=") + String(batiment) + String("message=") + String(message);
  // + String("&") + String("int_temp_c=") +  String(temp_c);
  // + String("&") + String("int_temp_f=") +  String(temp_f);
  int httpResponseCode = http.POST(payload); //Make the request

  if (httpResponseCode > 0) { //Check for the returning code
    String response = http.getString();  //Get the response to the request
  
    Serial.println(httpResponseCode);   //Print return code
    Serial.println(response);           //Print request answer
    display.drawString(85, 33, "HTTP");
    display.drawString(85, 43, "POST");
    display.drawString(85, 53, String(httpResponseCode));
    display.display();
  
  }else{
    Serial.print("Erreur dans l'envoi HTTP POST : ");
    Serial.println(httpResponseCode);
  }
  http.end(); //Free the resources
}
  

bool connectToWiFi(){
  Serial.print("Connexion au WiFi en cours");

  unsigned long startAttemptTime = millis();

  while (WiFi.status() != WL_CONNECTED && millis() - startAttemptTime < WIFI_TIMEOUT_MS) {
        delay(100);
        Serial.print(".");
    }
  
  if(WiFi.status() != WL_CONNECTED){
    Serial.println("Connexion au WiFi échouée");
    // take action
    }else{
      Serial.print("Connecté au WiFi");
      Serial.println(WiFi.localIP());
    }
  }


void screenInit() {
  pinMode(16,OUTPUT);
  pinMode(2,OUTPUT);
  
  digitalWrite(16, LOW);    // set GPIO16 low to reset OLED
  delay(50); 
  digitalWrite(16, HIGH); // while OLED is running, must set GPIO16 in high
  }


void setup() {
  screenInit();
  
  Serial.begin(115200);
  Serial.println("Démarrage LoRa duplex");

  SPI.begin(SCK,MISO,MOSI,SS);
  LoRa.setPins(csPin, resetPin, irqPin);

  if (!LoRa.begin(BAND)) {
    Serial.println("L'initialisation du LoRa a échoué. Veuillez vérifier vos connexions.");
    while (true) {}
  }
  display.init();
  display.flipScreenVertically();  
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 0, "Démarrage");
  display.drawString(0, 10, "LoRa duplex avec retour");
  display.drawString(0, 20, "Connection au WiFi");
  display.drawString(0, 40, "redacted_email");
  display.drawString(0, 50, " .redacted@redacted.com");
  display.display();

  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_NETWORK_1, WIFI_PASSWORD_1);
  connectToWiFi();
  if(WiFi.status() != WL_CONNECTED){
     WiFi.mode(WIFI_STA);
    WiFi.begin(WIFI_NETWORK_2, WIFI_PASSWORD_2);
    connectToWiFi();
    }else{
      Serial.println("Connecté");}

  destinationAddressLength = sizeof(destinationAddress);
  
  delay(500);
  lastSendTime = millis();
}


void loop() {
  if (millis() - lastSendTime > interval) {
    lastSendTime = millis();
    sendMessage(destinationName[i], destinationAddress[i]);
    i++;
    if (i == destinationAddressLength) {i = 0;}
  }
  receiveMessage(LoRa.parsePacket(), destinationName[i]);
}

And here is a client:

//Bibliothèques pour LoRa
#include <SPI.h>
#include <LoRa.h>

//Bibliothèques pour l'écran OLED
#include <Wire.h>
#include "SSD1306.h"

//Définition des pins utilisés par l'émetteur/receveur LoRa
#define SCK     5    // GPIO5  -- SCK
#define MISO    19   // GPIO19 -- MISO
#define MOSI    27   // GPIO27 -- MOSI
#define SS      18   // GPIO18 -- CS
#define RST     23   // GPIO14 -- RESET (If Lora does not work, replace it with GPIO14)
#define DI0     26   // GPIO26 -- IRQ(Interrupt Request)
#define BAND    redacted // 868E6
// Décision 2006/771/CE modifiée
// Par coefficient d’utilisation, on entend le rapport de temps, sur une heure, 
// durant lequel un dispositif particulier émet effectivement.
// Les conditions moins restrictives au sens de l’article 3, paragraphe 3 signifient que 
// les États membres peuvent autoriser une valeur supérieure pour le coefficient d’utilisation.

SSD1306 display(0x3c, 21, 22);
String rssi = "RSSI --";
String packSize = "--";
String packet ;

const int csPin = 18;          // LoRa radio chip select
const int resetPin = 23;       // LoRa radio reset
const int irqPin = 26;         // change for your board; must be a hardware interrupt pin

byte localAddress = 0xREDACTED6;
String localAddressString = "f";
byte destinationAddress = 0xREDACTED0;
String aucunMessage = "0";
String incoming;
int yy = 0;
String erreur;

void setup() {
  Serial.begin(115200);
  Serial.println("Démarrage LoRa duplex");
  Serial.print("Adresse locale : ");
  Serial.println(String(localAddress, HEX));
  Serial.print("Adresse de destination : ");
  Serial.print(String(destinationAddress, HEX));

  display.init();
  display.flipScreenVertically();  
  display.setFont(ArialMT_Plain_10);
  display.drawString(0, 10, "LoRa duplex : client " + localAddressString);
  display.drawString(0, 40, "redacted_email");
  display.drawString(0, 50, " .redacted@redacted.com");
  display.display();

  LoRa.setPins(csPin, resetPin, irqPin);

  if (!LoRa.begin(BAND)) {
    Serial.println("L'initialisation du LoRa a échoué. Veuillez vérifier vos connexions.");
    while (true) {}
  }
}

void loop() {
  receiveMessage(LoRa.parsePacket());
}

void sendMessage(String outgoing) {
  LoRa.beginPacket();
  LoRa.write(destinationAddress);
  LoRa.write(localAddress);
  LoRa.write(outgoing.length());
  LoRa.print(outgoing);
  LoRa.endPacket();
  Serial.println("Sending " + outgoing);
}

void receiveMessage(int packetSize) {
  if (packetSize == 0) return;

  int recipient = LoRa.read();
  byte sender = LoRa.read();
  byte incomingLength = LoRa.read();

  String incoming = "";

  while (LoRa.available()) {
    incoming += (char)LoRa.read();
  }

  if (incomingLength != incoming.length()) {
    String erreur = "La longueur du message \nne correspond pas";
    displayErreur(erreur);
    return;
  }

  if (recipient != localAddress) {
    String erreur = "L'adresse du destinataire \nne correspond pas";
    displayErreur(erreur);
    return;
  }

  Serial.print("Données reçues : " + incoming);
  Serial.print(" depuis 0x" + String(sender, HEX));
  Serial.println(" vers 0x" + String(recipient, HEX));

  sendMessage(aucunMessage);
  displayMessage(incoming);
}

void displayErreur(String erreur) {
    Serial.println("Erreur : " + erreur);
    display.clear();
    display.drawString(0, yy, erreur);
    display.drawString(0, 30 + yy, "redacted_email");
    display.drawString(0, 40 + yy, " .redacted@redacted.com");
    display.display();
    yyCalc();
}

void yyCalc() {
  // On fait défiler le texte vers le bas
  // pour éviter de laisser une image statique sur l'écran OLED.
  yy += 5;
  if (yy >= 20) {
    yy = 0;
  }
}

void displayMessage(String incoming) {
  display.clear();
  display.drawString(0, yy, "Message reçu : " + incoming);
  display.drawString(0, 20 + yy, "redacted_email");
  display.drawString(0, 30 + yy, " .redacted@redacted.com");
  display.display();
  yyCalc();
}

Thanks!

The arduino has a thing called a watchdog timer, you need to implement that. Because that does not generate a reset to your hardware, only the micro and starts at: void setup(); you should reset your hardware if possible then initialize your hardware as it may have been trashed as well. Not knowing your system I do not know if this is possible, not all devices can be reset via software. If a failure of anytype can endanger life, limb or property a computer designed for this should be used, the Arduino is not.

You don't indicate which Arduino you are using, so we have no idea of how much memory is used and how much is free. Be very careful using the String class as this can easily fragment the heap memory and the first you will know about is apparently completely random errors usually resulting in a crash. This could happen both to your clients and also to your server. I would recommend dropping the use of the evil Strings class and use of heap memory and replace then by C character strings (character arrays) allocated off the stack, or if you can specify a maximum size use global or static ones.

Thanks for your answers, I will have a look into these.
I am using TTGO ESP32-Paxcounter LoRa32 V2.1 1.6 Version 868 MHz LoRa ESP-32 OLED 0,96" MCUs.

They are plugged to power sockets via a USB cable and a power adaptor.

But after the code is satisfactory enough, I plan to use these MCUs in the future instead as they are cheaper:
Wemos® TTGO LORA32 868/915 Mhz ESP32 LoRa OLED 0,96"

This topic was automatically closed 120 days after the last reply. New replies are no longer allowed.