Arthur TVBNicolas DAILLYfcaronAlexandre LétocartTom Martin
Published

SerVision - IoT room server monitoring

Monitor a server room with LoRa/LoRaWAN network. Portainer, Node-Red, InfluxDB, Grafana, Website, TTN, Downlink, email alert sender.

IntermediateShowcase (no instructions)182
SerVision - IoT room server monitoring

Things used in this project

Hardware components

Arduino Leonardo
Arduino Leonardo
×1
Arduino Ethernet Shield 2
Arduino Ethernet Shield 2
×1
Sodaq explorer
×1
Flame sensor
×1
Photo resistor
Photo resistor
×1
DHT11 Temperature & Humidity Sensor (4 pins)
DHT11 Temperature & Humidity Sensor (4 pins)
×1
RGB Backlight LCD - 16x2
Adafruit RGB Backlight LCD - 16x2
×1
Slide Switch
Slide Switch
×2

Software apps and online services

Arduino IDE
Arduino IDE
Fusion
Autodesk Fusion
KiCad
KiCad
VS Code
Microsoft VS Code

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
JLC PCB

Story

Read more

Custom parts and enclosures

Arduino Leonardo

Boitier

Couvercle

LCD module

PCB Board

Shield Ethernet

Schematics

Project of PCB board for KiCad

Scheme comunication

Scheme and PCB design

Code

Sodaq Code

C/C++
//#include <Sodaq_RN2483.h>
//#include <Sodaq_wdt.h>
//#include <StringLiterals.h>
//#include <Switchable_Device.h>
//#include <Utils.h>

/*
* Copyright (c) 2015 SODAQ. All rights reserved.
*
* This file is part of Sodaq_RN2483.
*
* Sodaq_RN2483 is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of
* the License, or(at your option) any later version.
*
* Sodaq_RN2483 is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with Sodaq_RN2483.  If not, see
* <http://www.gnu.org/licenses></http:>.
*/

#define DEBUG
#include <Sodaq_RN2483.h>

    //----------------------------------------------//
    // ---------------- DEFINITIONS ----------------// 
    //----------------------------------------------//

#define uplinkCnt 10

// LED color customization for all the application
// Should give color mix for Orange (0xFF6600) assuming color index is accurate
#define LED_RED_CUST    0x00
#define LED_GREEN_CUST  0x99
#define LED_BLUE_CUST   0xFF

#define debugSerial SerialUSB
#define loraSerial  Serial2 // Serial communcation with the lora module
#define TXserial    Serial  // Serial communication with the arduino leonardo

    //----------------------------------------------//
    // ---------------- DECLARATION ----------------// 
    //----------------------------------------------//

// ----- LoRa key initialisation (we use OTAA for the sodaq) ----- //

//#define ABP
#define OTAA

#ifdef ABP
  // ABP Keys - Use your own KEYS!
  const uint8_t devAddr[4] =  {} ;
  const uint8_t nwkSKey[16] = {} ;
  const uint8_t appSKey[16] = {} ;
#else
  // OTAA Keys - Use your own KEYS!
  const uint8_t devEUI[8]  = {} ;
  const uint8_t appEUI[8]  = {} ;
  const uint8_t appKey[16] = {} ;
#endif

// Payload is 'Microchip ExpLoRa' in HEX
const uint8_t testPayload[] = {0x4d, 0x69, 0x63, 0x72, 0x6f, 0x63, 0x68, 0x69, 0x70, 0x20, 0x45, 0x78, 0x70, 0x4c, 0x6f, 0x52, 0x61} ;

String message = "";


    //----------------------------------------------//
    // ------------- UTILITY FUNCTIONS -------------// 
    //----------------------------------------------//



/**
 * @brief Parses and prints three IPv4 addresses from a 12-byte downlink payload.
 *
 * This function interprets the given 12-byte buffer as three sequential IPv4 addresses:
 * - Bytes 03   : Gateway IP
 * - Bytes 47   : External IP
 * - Bytes 811  : Internal IP
 *
 * The function prints each IP in human-readable format (e.g., 192.168.1.1)
 * to the debug serial port.
 *
 * @param data Pointer to the received downlink payload buffer.
 * @param length Length of the buffer (must be at least 12).
 */
void printDownlinkAsIPs(const uint8_t* data, uint16_t length) {
  if (length < 12) {
    debugSerial.println("Payload downlink invalide (taille < 12).");
    return;
  }

  debugSerial.print("Gateway IP : ");
  debugSerial.print(data[0]); debugSerial.print(".");
  debugSerial.print(data[1]); debugSerial.print(".");
  debugSerial.print(data[2]); debugSerial.print(".");
  debugSerial.println(data[3]);

  debugSerial.print("External IP : ");
  debugSerial.print(data[4]); debugSerial.print(".");
  debugSerial.print(data[5]); debugSerial.print(".");
  debugSerial.print(data[6]); debugSerial.print(".");
  debugSerial.println(data[7]);

  debugSerial.print("Internal IP : ");
  debugSerial.print(data[8]); debugSerial.print(".");
  debugSerial.print(data[9]); debugSerial.print(".");
  debugSerial.print(data[10]); debugSerial.print(".");
  debugSerial.println(data[11]);
}


/**
 * @brief Just a function to print the payload properly on the serial console
 *
 * @return void
 */
void printPayload(const uint8_t* payload, size_t length) {
  debugSerial.print("Payload [");
  debugSerial.print(length);
  debugSerial.print("] : ");

  for (size_t i = 0; i < length; i++) {
    debugSerial.print(payload[i]);
    if (i < length - 1) {
      debugSerial.print(", ");
    }
  }

  debugSerial.println();
}

/**
 * @brief Parses a formatted string and converts it into a LoRa-compatible payload.
 *
 * Expected format:
 *   "temp : <val>, hum : <val>, rawLum : <val>, flame : <val>, localConn : <val>, gatewayConn : <val>, publicConn : <val>#"
 *
 * @param msg   The input string.
 * @param payload Output byte array to store encoded data.
 * @return true if parsing and conversion were successful; false otherwise.
 */
bool parsePayload(const String& msg, uint8_t* payload) {
  String data = msg;
  data.trim();

  // Extract indexes of each field
  int tIdx = data.indexOf("temp : ") + 7;
  int hIdx = data.indexOf("hum : ") + 6;
  int lIdx = data.indexOf("rawLum : ") + 9;
  int fIdx = data.indexOf("flame : ") + 8;
  int lcIdx = data.indexOf("localConn : ") + 12;
  int gwIdx = data.indexOf("gatewayConn : ") + 14;
  int pcIdx = data.indexOf("publicConn : ") + 13;

  // Parse each value from the message
  float temp = data.substring(tIdx, data.indexOf(',', tIdx)).toFloat(); 
  int hum = data.substring(hIdx, data.indexOf(',', hIdx)).toInt();    
  int lum = data.substring(lIdx, data.indexOf(',', lIdx)).toInt();  
  int flame = data.substring(fIdx, data.indexOf(',', fIdx)).toInt();  
  int local = data.substring(lcIdx, data.indexOf(',', lcIdx)).toInt();  
  int gateway = data.substring(gwIdx, data.indexOf(',', gwIdx)).toInt();  
  int internet = data.substring(pcIdx).toInt();  // last value

  // Convert temperature (C) to fixed point value (x10), stored on 2 bytes
  uint16_t temp10 = (uint16_t)(temp * 10);
  payload[0] = temp10 >> 8;       // MSB
  payload[1] = temp10 & 0xFF;     // LSB

  // Store remaining values as single bytes
  payload[2] = (uint8_t)hum;
  payload[3] = (uint8_t)(lum);
  payload[4] = (uint8_t)flame;
  payload[5] = (uint8_t)local;
  payload[6] = (uint8_t)gateway;
  payload[7] = (uint8_t)internet;

  return true;
}

    //----------------------------------------------//
    // --------------- MAIN FUNCTIONS --------------// 
    //----------------------------------------------//


void setup()
{  
  TXserial.begin(9600);
	debugSerial.begin(9600) ;
	loraSerial.begin(LoRaBee.getDefaultBaudRate()) ;
  String message = "";
  
  //Initialize the LEDs and turn them all off
  pinMode(LED_RED, OUTPUT) ;
  pinMode(LED_GREEN, OUTPUT) ;
  pinMode(LED_BLUE, OUTPUT) ;

  digitalWrite(LED_RED, HIGH) ;
  digitalWrite(LED_GREEN, HIGH) ;
  digitalWrite(LED_BLUE, HIGH) ;

  // Power Up LED
  analogWrite(LED_RED,   LED_RED_CUST) ;
  analogWrite(LED_GREEN, LED_GREEN_CUST) ;
  analogWrite(LED_BLUE,  LED_BLUE_CUST) ;
  
  delay(2000) ;

  // Turn off the LEDs
  digitalWrite(LED_RED, HIGH) ;
  digitalWrite(LED_GREEN, HIGH) ;
  digitalWrite(LED_BLUE, HIGH) ;
  
	LoRaBee.setDiag(debugSerial) ; // optional
 
	#ifdef ABP
    if (LoRaBee.initABP(loraSerial, devAddr, appSKey, nwkSKey, true))
	  {
		  debugSerial.println("ABP Keys Accepted.") ;
	  }
	  else
	  {
		  debugSerial.println("ABP Key Setup Failed!") ;
	  }
  #else
    if (LoRaBee.initOTA(loraSerial, devEUI, appEUI, appKey, true))
    {
      debugSerial.println("OTAA Keys Accepted.") ;
    }
    else
    {
      debugSerial.println("OTAA Keys Setup Failed!") ;
    }
  #endif
  
  debugSerial.println("Sleeping for 5 seconds before starting sending out test packets.");
  for (uint8_t i = 5; i > 0; i--)
  {
    debugSerial.println(i) ;
    delay(1000) ;
  }

    debugSerial.println("--- LOOP --- ");

}

void loop()
{
  delay(60000); // Send the message every 10 sec
  while (TXserial.available()) {
    char c = TXserial.read();
    if (c == '#') {
      // End caracter is # so it is the end of the message string
      debugSerial.print("Received msg : ");
      debugSerial.println(message);
      message.trim(); // Clean message

      uint8_t payload[8]; // Payload of 8 bytes that is going to be sent via LoRa
      if (parsePayload(message, payload)) {
        debugSerial.println("Frame parsed with success");
        printPayload(payload, sizeof(payload));

        switch (LoRaBee.send(1, payload, sizeof(payload))) {
          case NoError:
          {
            debugSerial.println("Transmission OK");


            uint8_t downlink[64]; // max 64 bytes of downlink
            uint16_t len = LoRaBee.receive(downlink, sizeof(downlink)); // received func provided from RN2483 librarie

        if (len > 0) {
          debugSerial.print("Downlink received [");
          debugSerial.print(len);
          debugSerial.print(" bytes]: ");

          for (uint16_t i = 0; i < len; i++) {
            debugSerial.print("0x");
            if (downlink[i] < 16) debugSerial.print("0");
            debugSerial.print(downlink[i], HEX);
            debugSerial.print(" ");
          }
          debugSerial.println();

          printDownlinkAsIPs(downlink, len);

          // ----- Send IPs via serial to arduino ----- //

          TXserial.print("IPG:"); // Gateway
          TXserial.print(downlink[0]); TXserial.print(".");
          TXserial.print(downlink[1]); TXserial.print(".");
          TXserial.print(downlink[2]); TXserial.print(".");
          TXserial.println(downlink[3]);

          TXserial.print("IPE:"); // External (synoServer)
          TXserial.print(downlink[4]); TXserial.print(".");
          TXserial.print(downlink[5]); TXserial.print(".");
          TXserial.print(downlink[6]); TXserial.print(".");
          TXserial.println(downlink[7]);

          TXserial.print("IPI:"); // Internal (localDevice)
          TXserial.print(downlink[8]); TXserial.print(".");
          TXserial.print(downlink[9]); TXserial.print(".");
          TXserial.print(downlink[10]); TXserial.print(".");
          TXserial.println(downlink[11]);
          
          // ------------------------------------------- //



        }
        else {
              debugSerial.println("No downlink received.");
            }

            break;
          }

          default:
            debugSerial.println("Error during transmission");
            break;
        }

      } 
      
      else {
        debugSerial.println("Parsing error");
      }

      message = "";  // Clean up for next frame
      
    } else {
      message += c;
    }   
  }
}

Arduino Code

C/C++
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetClient.h>
#include <DHT.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>

    //----------------------------------------------//
    // ---------------- DEFINITIONS ----------------// 
    //----------------------------------------------//

// #define EthernetCheck

#define TXserial Serial1 // Serial to sodaq

// DHT11 sensor
#define DHTPIN 9      // digital sensor pin
#define DHTTYPE DHT11 // Sensor definition 

// Flame sensor
#define FLAME_ANALOG A0  
#define FLAME_DIGITAL 10 

// Photores 
#define LDR_PIN A3

//----------------------------------------------//
// ---------------- DECLARATION ----------------// 
//----------------------------------------------//

DHT dht(DHTPIN, DHTTYPE); // Dht sensor instance
LiquidCrystal_I2C lcd(0x27, 16, 2);  // I2C LCD instance

//  W5100 Ethernet module 
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
EthernetClient client;

// Serial frame to communicate with Sodaq
String serialFrame;

// Ethernet ok ?
bool ethOK;

// IPs list to test
IPAddress localDevice(10, 100, 200, 200);  // Internal printer
IPAddress gateway(10, 100, 0, 254);        // Network gateway
IPAddress synoServer(213, 151, 173, 126);  // External IP (synology) // Maybe change this to 8.8.8.8 because even if there is a problem, we can ping the synology


//----------------------------------------------//
// ------------- UTILITY FUNCTIONS -------------// 
//----------------------------------------------//

/**
 * @brief Checks whether a TCP connection can be established with a given IP address.
 *
 * This function attempts to initiate a TCP connection to the specified IP address
 * on port 80 (HTTP). If the connection is successful, it immediately closes the 
 * connection and returns true. Otherwise, it returns false.
 *
 * @param ip The IP address to attempt to connect to.
 * @return true if the connection is successfully established; false otherwise.
 */
bool checkConnection(IPAddress ip) {
  if (client.connect(ip, 80)) {
    client.stop();
    return true;
  }
  return false;
}

//----------------------------------------------//
// --------------- MAIN FUNCTIONS --------------// 
//----------------------------------------------//

void setup() {
  Serial.begin(9600); // Debug serial
  TXserial.begin(9600); 
  delay(2000);

  Serial.println("SETUP()...");
  
  // LCD init
  lcd.init();
  lcd.backlight();
  lcd.clear();

  // Start dht module
  dht.begin();
  Serial.println("DHT Sensor waking up...");

  // Start flame module
  pinMode(FLAME_DIGITAL, INPUT);
  Serial.println("Flame sensor waking up...");


#ifdef EthernetCheck 
  if (Ethernet.begin(mac)==0) {
    Serial.println("chec du DHCP !");
    // while (true);  // Block execution if no ip
    ethOK = false;
  }
  else{
    ethOK = true;
  }
  Serial.print("Adresse IP obtenue: ");
  Serial.println(Ethernet.localIP());
#endif

#ifndef EthernetCheck 
  ethOK = false;
#endif

  serialFrame = "[SETUP] IP : "+ String(ethOK);
  Serial.println(serialFrame);
  delay(1000);
}


void loop() {
  int ldrValue = analogRead(LDR_PIN);  // read photores (0-1023)

  // Conversion en luminosit approximative (lux)
  int luminosity = map(ldrValue, 50, 1023, 100, 0);  // transform 0-1023 to -> 100-0

  Serial.print("Valeur brute LDR : ");
  Serial.print(ldrValue);
  Serial.print(" | Luminosit estime : ");
  Serial.print(luminosity);
  Serial.println(" lux");
  Serial.println("--------------------------");


  // dht values
  float temperature = dht.readTemperature(); //  C
  float humidity = dht.readHumidity();       //  %

  //Check read error
  if (isnan(temperature) || isnan(humidity)) {
    Serial.println("DH11 sensor error");
    return;
  }

  // Values display
  Serial.print("Temperature : ");
  Serial.print(temperature);
  Serial.println(" C");
  Serial.print("Humidity : ");
  Serial.print(humidity);
  Serial.println(" %");
  Serial.println("--------------------------");

  // flame sensor read /!\ For now this doesn't work anymore because the sensor sur pin 10 and it is also SS pin for W100 ethernet module 
  int analogValue = analogRead(FLAME_ANALOG); // Intensity
  int flameDigitalValue = 0; // bool value 

  Serial.print("Fire intensity (analog) : ");
  Serial.println(analogValue);

  if (flameDigitalValue == HIGH) {
    Serial.println("ALERT : Flame detected !");
  } else {
    Serial.println("No flame detected.");
  }

  Serial.println("--------------------------");

  // Networking check
  bool localOK = checkConnection(localDevice);
  bool gatewayOK = checkConnection(gateway);
  bool publicOK = checkConnection(synoServer);

  Serial.println("------ Networking state ------");

  Serial.print("Rseau interne (");
  Serial.print(localDevice);
  Serial.print(") : ");
  Serial.println(localOK ? "OK" : "KO !");

  Serial.print("Passerelle (");
  Serial.print(gateway);
  Serial.print(") : ");
  Serial.println(gatewayOK ? "OK" : "KO !");

  Serial.print("Connexion Internet (");
  Serial.print(synoServer);
  Serial.print(") : ");
  Serial.println(publicOK ? "OK" : "KO !");

  Serial.println("--------------------------\n");
  serialFrame = "[LOOP] temp : " + String(temperature) + ", hum : "+String(humidity) + ", rawLum : "+ String(luminosity) + ", flame : " + String(flameDigitalValue) + ", localConn : "+/*String(localOK)*/"1" + ", gatewayConn : "+/*String(gatewayOK)*/"1"+", publicConn : " + String(/*publicOK*/1) + "#";
  Serial.println(serialFrame);
  TXserial.print(serialFrame);
  Serial.println("--------------------------\n");


  lcd.setCursor(0, 0);
  lcd.print("Temp:"+String(temperature)+"Hum:"+String(humidity));
  lcd.setCursor(0, 1);
  lcd.print("Lcl:"+String(localOK)+"Gtw:"+String(gatewayOK)+"Pblc:"+String(publicOK));

  delay(2000);  // Checks every 2 sec

  while (TXserial.available()) {
    String downlinkStr = TXserial.readStringUntil('\n');
    downlinkStr.trim();

    if (downlinkStr.startsWith("IPG:")) {
      downlinkStr.remove(0, 4); // remove "IPG:"
      gateway.fromString(downlinkStr);
      Serial.print("Updated gateway IP to: ");
      Serial.println(gateway);
    } else if (downlinkStr.startsWith("IPE:")) {
      downlinkStr.remove(0, 4); // remove "IPE:"
      synoServer.fromString(downlinkStr);
      Serial.print("Updated external IP to: ");
      Serial.println(synoServer);
    } else if (downlinkStr.startsWith("IPI:")) {
      downlinkStr.remove(0, 4); // remove "IPI:"
      localDevice.fromString(downlinkStr);
      Serial.print("Updated local IP to: ");
      Serial.println(localDevice);
    }
  }
}

TTN Decoder

JavaScript
function decodeUplink(input) {
  const bytes = input.bytes;

  // Payload de handshake
  const handshakeSignature = [
    0x4d, 0x69, 0x63, 0x72, 0x6f, 0x63, 0x68,
    0x69, 0x70, 0x20, 0x45, 0x78, 0x70, 0x4c,
    0x6f, 0x52, 0x61
  ];

  // Vérifie le handshake
  const isHandshake = bytes.length === handshakeSignature.length &&
    bytes.every((b, i) => b === handshakeSignature[i]);

  if (isHandshake) {
    return {
      data: {
        status: "Carte connectée : Microchip ExpLoRa"
      }
    };
  }

  // Payload normal (8 octets attendus)
  if (bytes.length !== 8) {
    return {
      errors: ["Payload length invalid, expected 8 bytes or handshake"]
    };
  }

  // Décodage du payload
  const tempRaw = (bytes[0] << 8) | bytes[1]; // Big-endian
  const temperature = tempRaw / 10.0;

  return {
    data: {
      temperature: temperature,
      humidity: bytes[2],
      luminosity_raw: bytes[3],          // Inversion du /4 côté Arduino
      flame_detected: bytes[4],
      local_connection: bytes[5],
      gateway_connection: bytes[6],
      public_connection: bytes[7]
    }
  };
}

Decoder Node-red to InfluxDB

JavaScript
let d = msg.payload.uplink_message.decoded_payload;

return {
    payload: {
        temperature: parseFloat(d.temperature),
        humidity: parseInt(d.humidity),
        luminosity: parseInt(d.luminosity_raw),
        flame: parseInt(d.flame_detected),
        localConn: parseInt(d.local_connection),
        gatewayConn: parseInt(d.gateway_connection),
        publicConn: parseInt(d.public_connection)
    }
};

Function to send email alert

JavaScript
const temperature = msg.payload.temperature || 0;
const humidity = msg.payload.humidity || "N/A";
const luminosity = msg.payload.luminosity || "N/A";
const flame = msg.payload.flame || 0;
const localConn = msg.payload.localConn;
const gatewayConn = msg.payload.gatewayConn;
const publicConn = msg.payload.publicConn;

let alerts = [];
let reason = "";


if (flame !== 0) {
    alerts.push("Flamme détectée");
}
if (temperature > 30) {
    alerts.push(`Température élevée (${temperature}°C)`);
}
if (!gatewayConn) {
    alerts.push("Connexion Gateway perdue");
}


if (alerts.length > 0) {
    reason = alerts.join(" | ");

    msg.topic = `⚠️ Alerte : ${reason}`;

    msg.payload = `
        <h2 style="color: red;">⚠️ Alerte détectée dans la salle serveur</h2>
        <p>Raisons : <strong>${alerts.join(', ')}</strong></p>

        <h3>Résumé des données :</h3>
        <ul>
            <li>🌡️ Température : <strong>${temperature}°C</strong></li>
            <li>💧 Humidité : <strong>${humidity}%</strong></li>
            <li>💡 Luminosité : <strong>${luminosity} lx</strong></li>
            <li>🔥 Flamme : <strong>${flame !== 0 ? "OUI" : "NON"}</strong></li>
        </ul>

        <h3>Connectivités :</h3>
        <ul>
            <li>📶 Connexion locale : ${localConn ? "OUI" : "NON"}</li>
            <li>🛰️ Connexion passerelle (gateway) : ${gatewayConn ? "OUI" : "NON"}</li>
            <li>🌍 Connexion publique : ${publicConn ? "OUI" : "NON"}</li>
        </ul>

        <p>Accédez au site de monitoring : <a href="http://X.X.X.X/monitoring-site/" target="_blank">X.X.X.X/monitoring-site</a></p>
    `;

    msg.headers = {
        "Content-Type": "text/html"
    };

    return msg;
}

return null;

Credits

Arthur TVB
1 project • 0 followers
Nicolas DAILLY
41 projects • 26 followers
Associated Professor at UniLaSalle - Amiens / Head of the Computer Network Department / Teach Computer and Telecommunication Networks
fcaron
22 projects • 7 followers
Alexandre Létocart
11 projects • 7 followers
Tom Martin
1 project • 0 followers

Comments