Aymen B.Matteo ANDRIATSILAVOIoana CâcăuZimeng Jiang
Published

OpenRuche - Les4Nations

ConnectedHive: Empowering a smarter, sustainable future

IntermediateWork in progressOver 6 days77
OpenRuche - Les4Nations

Things used in this project

Hardware components

Seeed Studio HX711
×1
DHT22 temperature-humidity sensor
Adafruit DHT22 temperature-humidity sensor
×2
Bosche H40A 200 kg
×1
Adafruit Waterproof DS18B20 Digital temperature sensor
Adafruit Waterproof DS18B20 Digital temperature sensor
×2
Adafruit TSL2561
×1
STMicroelectronics STM32WL55JC1
×1
DS12AIP65
×1
OSEPP SC1007
×1
Seeed Studio LiPo Rider Pro 106990008
×1
Accu LiPo 3,7 Vcc 2000 mAh L903759
×1
Boîtier ABS étanche G201CMF
×1
Boîtier ABS étanche G218CMF
×1
3.3V Step-Up Voltage Regulator U1V11F3
×1
Resistor 100k ohm
Resistor 100k ohm
×1
Through Hole Resistor, 300 kohm
Through Hole Resistor, 300 kohm
×1

Software apps and online services

Arduino IDE
Arduino IDE
STM32CUBEPROG
STMicroelectronics STM32CUBEPROG
Otii 3
Node-RED
Node-RED
KiCad
KiCad
Ubidots
Ubidots
Beep

Hand tools and fabrication machines

Otii Arc Pro

Story

Read more

Schematics

KiCad Shield

Shield of STM32WL55JC1 on KiCad

NodeRED flow

the Workflow Nodered for data transmission

Pinout_STM32WL55JC1

A pinout map of the STM32WL55JC1 board with each pin assigned functionality(ies) and names.

Code

Measurements_withDownlink_andDeepSleep

Arduino
This code includes the necessary libraries, sets up all the sensors and sends the board into a loop of: measures acquisitions, sending via LoRaWAN protocol, going into sleep mode for a set amount of time and processing the downlink command.
#include <OneWire.h>
#include <DallasTemperature.h>
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_TSL2561_U.h>
#include <STM32LoRaWAN.h>
#include <DHT.h>
#include <DHT22.h>
#include <HX711.h>
#include "STM32LowPower.h"

#define GREEN_LED_PIN PB_9

// bon code
#define WEIGHT_MEASURE_TIMEOUT 5000 // Timeout for weight measurement in milliseconds

// DHT22: one on PB1 (A0), the other on PB2 (A0)
#define DHT22_1_PIN PB_1
#define DHT22_2_PIN PB_2
#define DHT22_TYPE DHT22
DHT dht22_1(DHT22_1_PIN, DHT22_TYPE);
DHT dht22_2(DHT22_2_PIN, DHT22_TYPE);

// DS18B20 on A4 - PB_14
#define DS18B20_PIN PB_14
OneWire oneWire(DS18B20_PIN);
DallasTemperature sensors(&oneWire);
DeviceAddress ds18b20_1 = { 0x28, 0x2A, 0xEF, 0x95, 0xF0, 0x01, 0x3C, 0xE6 }; // Replace with your sensor's address
DeviceAddress ds18b20_2 = { 0x28, 0x73, 0xAD, 0x09, 0x0C, 0x00, 0x00, 0x28 }; // Replace with your sensor's address

// Light Sensor TSL2561 on D14 (SDA) and D15 (SCL)
Adafruit_TSL2561_Unified tsl = Adafruit_TSL2561_Unified(TSL2561_ADDR_FLOAT, 12345);

// HX711 on PA12 (D15) and PA11 (D14)
#define LOADCELL_DOUT_PIN  PA_11
#define LOADCELL_SCK_PIN PA_12
HX711 scale;
float calibration_factor = 13350;

// LoRa
LoRaModem modem;
unsigned long DELAY_MS = 600000;

// Pololu 3V3 regulator - shutdown pin
#define SHDPN_PIN PB_13
// #define LED_PIN PB_15
// #include "arduino_secrets.h"
// String appEui = SECRET_APP_EUI;
// String appKey = SECRET_APP_KEY;
//  Zimeng's credentials
String appEui = "70B3D57ED003E7A7";
String appKey = "4E6A6840906E8B2B64EE6EFC082E864E";

void setPinsAnalog() {
  // Set some pins to analog mode with pull-up/pull-down resistors to avoid floating states
  //  Digital pins
  pinMode(PA0, INPUT_ANALOG); //0
  pinMode(PA1, INPUT_ANALOG); //1
  //pinMode(PA2, INPUT_ANALOG);//PA2 //2 -- left untouched
  //pinMode(PA3, INPUT_ANALOG);//PA3 //3 -- left untouched
  pinMode(PA4, INPUT_ANALOG); //4
  pinMode(PA5, INPUT_ANALOG); //5
  pinMode(PA6, INPUT_ANALOG); //6
  pinMode(PA7, INPUT_ANALOG); //7
  pinMode(PA8, INPUT_ANALOG); //8
  // pinMode(PA9, INPUT_ANALOG); //9 -- used
  pinMode(PB0, INPUT_ANALOG); //16
  pinMode(PB5, INPUT_ANALOG); //21
  pinMode(PB6, INPUT_ANALOG); //22
  pinMode(PB7, INPUT_ANALOG); //23
  pinMode(PB8, INPUT_ANALOG); //24
  // pinMode(PB9, INPUT_ANALOG); //25
  pinMode(PB10, INPUT_ANALOG); //26
  pinMode(PB11, INPUT_ANALOG); //27
  pinMode(PB12, INPUT_ANALOG); //28
  pinMode(PB15, INPUT_ANALOG); //31
  pinMode(PC0, INPUT_ANALOG); //32
  pinMode(PC1, INPUT_ANALOG); //33
  pinMode(PC2, INPUT_ANALOG); //34
  pinMode(PC3, INPUT_ANALOG); //35
  pinMode(PC4, INPUT_ANALOG); //36
  pinMode(PC5, INPUT_ANALOG); //37
  pinMode(PC6, INPUT_ANALOG); //38
  pinMode(PC13, INPUT_ANALOG); //39
  pinMode(PC14, INPUT_ANALOG); //40
  pinMode(PC15, INPUT_ANALOG); //41
  pinMode(PH3, INPUT_ANALOG); //42
  //  Analog pins
  // pinMode(PA10, INPUT_ANALOG); //PIN_A0 -- used
  // pinMode(PA11, INPUT_ANALOG); //PIN_A1 -- used
  // pinMode(PA12, INPUT_ANALOG); //PIN_A2 -- used
  pinMode(PA13, INPUT_ANALOG); //PIN_A3
  pinMode(PA14, INPUT_ANALOG); //PIN_A4
  pinMode(PA15, INPUT_ANALOG); //PIN_A5
  // pinMode(PB1, INPUT_ANALOG); //PIN_A6 -- used
  // pinMode(PB2, INPUT_ANALOG); //PIN_A7 -- used
  pinMode(PB3, INPUT_ANALOG); //PIN_A8
  pinMode(PB4, INPUT_ANALOG); //PIN_A9
  // pinMode(PB13, INPUT_ANALOG); //PIN_A10 -- used
  // pinMode(PB14, INPUT_ANALOG); //PIN_A11 -- used
}

void setup() {
  pinMode(GREEN_LED_PIN, OUTPUT);

  pinMode(SHDPN_PIN, OUTPUT);
  digitalWrite(SHDPN_PIN, LOW); // Start with sensors disabled

  Serial.begin(115200);
  while (!Serial);

  Serial.println("SHDN set to low");

  // Set SOME pins to analog mode in rder to consume less
  setPinsAnalog();
  Serial.println("Unused pins set to analog mode");

  // Initialize DHT22
  Serial.println("Initializing DHT22...");
  dht22_1.begin();
  dht22_2.begin();

  // Initialize DS18B20
  Serial.println("Initializing DS18B20...");
  sensors.begin();

  // Initialize Light Sensor
  setTSL2561();
  // Initialize HX711
  Serial.println("Initializing HX711...");
  scale.begin(LOADCELL_DOUT_PIN, LOADCELL_SCK_PIN);
  //scale.tare(); // Rinitialiser la balance  0
  scale.set_scale(calibration_factor); // Ajuster avec ce facteur de calibration // Ajuster au facteur de calibration

  // Initialize LoRa
  Serial.println("Initializing LoRa...");
  if (!modem.begin(EU868)) {
    Serial.println("chec de l'initialisation du module LoRa");
    while (1) {}
  }
  Serial.print("Device EUI : ");
  Serial.println(modem.deviceEUI());

  // int connected = modem.joinOTAA(appEui, appKey);
  int connected = modem.joinOTAA(appEui, appKey, "70B3D57ED006E70B");
  if (!connected) {
    Serial.println("chec de connexion OTAA, essayez prs d'une fentre");
    while (1) {}
  }
  Serial.println("Connect  LoRaWAN !");

  // pinMode(LED_PIN, OUTPUT);
  // digitalWrite(LED_PIN, LOW); // teindre la LED au dmarrage

  // Configure low power
  LowPower.begin();

  for (int i = 0; i < 3; i++){
    digitalWrite(GREEN_LED_PIN, HIGH);
    delay(300);
    digitalWrite(GREEN_LED_PIN, LOW);
    delay(300);
  }
}

void loop() {
  // Enable sensor power via SHDN
  digitalWrite(SHDPN_PIN, HIGH);
  delay(1000); // Wait for sensors to stabilize
  Serial.println("SHDN set to high");

  // Read DHT22
  Serial.println("Reading DHT22...");
  float t22_1 = dht22_1.readTemperature();
  float h22_1 = dht22_1.readHumidity();
  if (isnan(t22_1) || isnan(h22_1)) {
    Serial.println(" Erreur : donnes capteur non valides !");
  } else {
    Serial.print("DHT22_1 -> Temprature : "); Serial.print(t22_1, 1); Serial.println("C");
    Serial.print("DHT22_1 -> Humidit : "); Serial.print(h22_1, 1); Serial.println("%");
  }

  float t22_2 = dht22_2.readTemperature();
  float h22_2 = dht22_2.readHumidity();
  if (isnan(t22_2) || isnan(h22_2)) {
    Serial.println(" Erreur : donnes capteur non valides !");
  } else {
    Serial.print("DHT22_2 -> Temprature : "); Serial.print(t22_2, 1); Serial.println("C");
    Serial.print("DHT22_2 -> Humidit : "); Serial.print(h22_2, 1); Serial.println("%");
  }

  // Read DS18B20
  Serial.println("Reading DS18B20...");
  float tempC1 = readDS18B20(ds18b20_1, "DS18B20_1");
  float tempC2 = readDS18B20(ds18b20_2, "DS18B20_2");

  // Read Light Sensor
  Serial.println("Reading Light Sensor...");
  sensors_event_t event;
  tsl.getEvent(&event);
  float light = 0;
  if (event.light) {
    light = event.light;
    Serial.print(light); Serial.println(" lux");
  } else {
    Serial.println("Sensor overload");
  }

  // Read HX711
  Serial.print("Reading: ");
  float weight = scale.get_units() / 2.2046 - 4.62;

 // ----------------------------------A VERIFIER --------------------------------------
  if (weight < 0.00) {
    weight = 0.00;
  }
  Serial.println("Poids = " + String(weight) + " kg");
  
  // float weight = readHX711();
  // if (isnan(weight)) {
  //   Serial.println(" Erreur : mesure de poids non valide !");
  // } else {
  //   Serial.print("Weight: " + String(weight) + " kg\n");
  // }

  // Disable sensor power
  digitalWrite(SHDPN_PIN, LOW);
  Serial.println("SHDN set to low");

  // Send data via LoRa
  Serial.println("Sending data via LoRa...");
  sendLoRaData(t22_1, h22_1, t22_2, h22_2, tempC1, tempC2, light, weight);

  // Go to deep sleep for 10 minutes
  Serial.println("Going to deep sleep for " + String(DELAY_MS) +" ms...\n");
  LowPower.deepSleep(DELAY_MS); // 600000 ms = 10 minutes

  // Rinitialiser le TSL2561 aprs le rveil
  setTSL2561();
}

float readDS18B20(DeviceAddress deviceAddress, const char* sensorName) {
  sensors.requestTemperaturesByAddress(deviceAddress);
  float tempC = sensors.getTempC(deviceAddress);
  if (tempC == DEVICE_DISCONNECTED_C) {
    Serial.print(sensorName); Serial.println(" -> Erreur : capteur dconnect !");
    return NAN;
  } else {
    Serial.print(sensorName); Serial.print(" -> Temprature : "); Serial.print(tempC); Serial.println("C");
    return tempC;
  }
}

void setTSL2561() {
  Serial.println("Initalizing/Waking up Light Sensor...");
  if (!tsl.begin()) {
    Serial.println("Ooops, no TSL2561 detected ... Check your wiring or I2C ADDR!");
    //while (1);
  }
  tsl.enableAutoRange(true);
  /* Changing the integration time gives you better sensor resolution (402ms = 16-bit data) */
  //tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_13MS);      /* fast but low resolution */
  tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_101MS);  /* medium resolution and speed   */
  // tsl.setIntegrationTime(TSL2561_INTEGRATIONTIME_402MS);  /* 16-bit data but slowest conversions */
}

void sendLoRaData(float t22_1, float h22_1, float t22_2, float h22_2, float tempC1, float tempC2, float light, float weight) {
  // Incrmentation de a taille (+ de prcisions)
  uint8_t txData[20]; // txData[16]  txData[20]

  int16_t t22_1_int = t22_1 * 10;
  int16_t h22_1_int = h22_1 * 10;
  int16_t t22_2_int = t22_2 * 10;
  int16_t h22_2_int = h22_2 * 10;
  int32_t tempC1_int = tempC1 * 100; // prcision au centime
  int32_t tempC2_int = tempC2 * 100; // prcision au centime
  int16_t light_int = light;  
  int32_t weight_int = weight * 100; // prcision au centime

  // Packing data
  txData[0] = t22_1_int >> 8;
  txData[1] = t22_1_int & 0xFF;
  txData[2] = h22_1_int >> 8;
  txData[3] = h22_1_int & 0xFF;
  txData[4] = t22_2_int >> 8;
  txData[5] = t22_2_int & 0xFF;
  txData[6] = h22_2_int >> 8;
  txData[7] = h22_2_int & 0xFF;
  txData[8] = tempC1_int >> 24;  
  txData[9] = tempC1_int >> 16;
  txData[10] = tempC1_int >> 8;
  txData[11] = tempC1_int & 0xFF;
  txData[12] = tempC2_int >> 24;
  txData[13] = tempC2_int >> 16;
  txData[14] = tempC2_int >> 8;
  txData[15] = tempC2_int & 0xFF;
  txData[16] = light_int >> 8;  
  txData[17] = light_int & 0xFF;
  txData[18] = weight_int >> 8; 
  txData[19] = weight_int & 0xFF;

  modem.beginPacket();
  modem.write(txData, sizeof(txData));
  int err = modem.endPacket(true);

  if (err > 0) {
    Serial.println(" Donnes envoyes avec succs !");
  } else {
    Serial.println(" chec de l'envoi des donnes.");
  }

  if (modem.available()) {
    Serial.print("Received packet on port ");
    int downlinkPort = modem.getDownlinkPort();
    Serial.print(downlinkPort);
    Serial.print(": ");

    while (modem.available()) {
      uint8_t b = modem.read();
      Serial.print(b, HEX); // Afficher le payload en hexadcimal
      Serial.print(" ");

      if (b == 0x01) { // Anomalie dtecte (WARNING)
        DELAY_MS = 120000; // 2 minutes
        Serial.println("\nDELAY_2MIN");
        // digitalWrite(LED_PIN, HIGH);
        // Serial.println("\nLED ON");
      } else if (b == 0x00) { // Retour  la normale (OK)
        DELAY_MS = 600000; // 10 minutes
        Serial.println("\nDELAY_10MIN");
        // digitalWrite(LED_PIN, LOW);
        // Serial.println("\nLED OFF");
      }
    }
    Serial.println();
  }
}

float readHX711() {
  Serial.println("Reading HX711...");
  scale.power_up();
  unsigned long startTime = millis();
  float weight = NAN;
  while (millis() - startTime < WEIGHT_MEASURE_TIMEOUT) {
    // Wait for HX711 to be ready with a timeout
    unsigned long dataWaitStart = millis();
    bool dataReady = false;
    while (millis() - dataWaitStart < 1000) { // 1-second data ready timeout
      if (scale.is_ready()) {
        dataReady = true;
        break;
      }
      delay(1);
    }
    if (!dataReady) {
      Serial.println("HX711 not ready. Timeout!");
      break;
    }
    // Read weight if data is ready
    weight = scale.get_units() / 2.2046 - 4.62;
    if (!isnan(weight)) {
      break;
    }
    delay(10); // Small delay to prevent tight loop
  }
  scale.power_down();
  return weight;
}

NodeRED flow

JSON
the workflow NodeRED for data transmission
[
    {
        "id": "c526047e539d0589",
        "type": "tab",
        "label": "Open Ruche",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "137523090d1e0314",
        "type": "http request",
        "z": "c526047e539d0589",
        "name": "Serveur_Python",
        "method": "POST",
        "ret": "obj",
        "paytoqs": "ignore",
        "url": "http://localhost:14000/data",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [
            {
                "keyType": "Content-Type",
                "keyValue": "",
                "valueType": "application/json",
                "valueValue": ""
            }
        ],
        "x": 560,
        "y": 280,
        "wires": [
            [
                "28782ac2abb8bde5",
                "ee3637cc38654b92"
            ]
        ]
    },
    {
        "id": "759ca868c14b792c",
        "type": "function",
        "z": "c526047e539d0589",
        "name": "formateurServeur",
        "func": "// Extraire les donnes des capteurs du payload TTN\nlet ttnPayload = msg.payload.uplink_message.decoded_payload;\n\n// Formater les donnes pour le serveur Flask\nlet formattedData = {\n    dht22_1_temperature: {\n        value: ttnPayload.dht22_1_temperature.value,\n        unit: ttnPayload.dht22_1_temperature.unit\n    },\n    dht22_1_humidity: {\n        value: ttnPayload.dht22_1_humidity.value,\n        unit: ttnPayload.dht22_1_humidity.unit\n    },\n    dht22_2_temperature: {\n        value: ttnPayload.dht22_2_temperature.value,\n        unit: ttnPayload.dht22_2_temperature.unit\n    },\n    dht22_2_humidity: {\n        value: ttnPayload.dht22_2_humidity.value,\n        unit: ttnPayload.dht22_2_humidity.unit\n    },\n    ds18b20_1_temperature: {\n        value: ttnPayload.ds18b20_1_temperature.value,\n        unit: ttnPayload.ds18b20_1_temperature.unit\n    },\n    ds18b20_2_temperature: {\n        value: ttnPayload.ds18b20_2_temperature.value,\n        unit: ttnPayload.ds18b20_2_temperature.unit\n    },\n    light_intensity: {\n        value: ttnPayload.light_intensity.value,\n        unit: ttnPayload.light_intensity.unit\n    },\n    weight: {\n        value: ttnPayload.weight.value,\n        unit: ttnPayload.weight.unit\n    }\n};\n\n// Assigner les donnes formates au payload\nmsg.payload = formattedData;\n\n// Retourner le message\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 350,
        "y": 280,
        "wires": [
            [
                "137523090d1e0314",
                "a1b72d0e24769d8b"
            ]
        ]
    },
    {
        "id": "28782ac2abb8bde5",
        "type": "switch",
        "z": "c526047e539d0589",
        "name": "resultat",
        "property": "payload.status",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "OK",
                "vt": "str"
            },
            {
                "t": "eq",
                "v": "WARNING",
                "vt": "str"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 740,
        "y": 280,
        "wires": [
            [
                "24ef70963ac20ffd"
            ],
            [
                "e38a7f3486aed343",
                "127aa1ec29e97b9b"
            ]
        ]
    },
    {
        "id": "e38a7f3486aed343",
        "type": "function",
        "z": "c526047e539d0589",
        "name": "message",
        "func": "// Formate le message d'alerte\nlet subject = \" Alerte Capteur \";\nlet message = `Alerte : Les capteurs suivants ont dtect des anomalies :\\n\\n`;\n\n// Extrait les anomalies du champ \"details\"\nlet anomalies = msg.payload.details.split(\"Alerte : \").filter(Boolean);\n\n// Ajoute chaque anomalie au message avec un emoji et un saut de ligne\nanomalies.forEach((anomalie) => {\n    message += `${anomalie.trim()}\\n`;\n});\n\n// Ajoute une conclusion au message\nmessage += \"\\nVeuillez vrifier ces capteurs ds que possible.\";\n\n// Ajoute le sujet et le message au payload\nmsg.topic = subject;\nmsg.payload = message;\n\n// Retourne le message format\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 900,
        "y": 360,
        "wires": [
            [
                "a64c66b965dfd087",
                "a26bad76d5799d0b"
            ]
        ]
    },
    {
        "id": "88ebf2978f665a1b",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 320,
        "y": 340,
        "wires": []
    },
    {
        "id": "a1b72d0e24769d8b",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 2",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 540,
        "y": 240,
        "wires": []
    },
    {
        "id": "ee3637cc38654b92",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 3",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "payload",
        "targetType": "msg",
        "statusVal": "",
        "statusType": "auto",
        "x": 740,
        "y": 240,
        "wires": []
    },
    {
        "id": "a64c66b965dfd087",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 4",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1080,
        "y": 380,
        "wires": []
    },
    {
        "id": "a26bad76d5799d0b",
        "type": "e-mail",
        "z": "c526047e539d0589",
        "server": "smtp.gmail.com",
        "port": "465",
        "authtype": "BASIC",
        "saslformat": true,
        "token": "oauth2Response.access_token",
        "secure": true,
        "tls": true,
        "name": "jiangmengmeng1211@gmail.com",
        "dname": "Alerte Email",
        "x": 1090,
        "y": 340,
        "wires": []
    },
    {
        "id": "1c99efa19711e07b",
        "type": "mqtt in",
        "z": "c526047e539d0589",
        "name": "LoRa STM32",
        "topic": "v3/projet-lorawan-open-ruche@ttn/devices/stm32-device/up",
        "qos": "0",
        "datatype": "auto-detect",
        "broker": "34fc5ca9084ec789",
        "nl": false,
        "rap": true,
        "rh": 0,
        "inputs": 0,
        "x": 150,
        "y": 340,
        "wires": [
            [
                "759ca868c14b792c",
                "88ebf2978f665a1b",
                "8bf5950652f43c91"
            ]
        ]
    },
    {
        "id": "8bf5950652f43c91",
        "type": "function",
        "z": "c526047e539d0589",
        "name": "formateurBeep",
        "func": "// Extraire les donnes des capteurs du payload TTN\nlet ttnPayload = msg.payload.uplink_message.decoded_payload;\n\n// Formater les donnes pour BEEP\nlet beepData = {\n    key: \"zxwyt3aqxmc2ae6r\",  // Remplace par ta cl BEEP\n    t_0: ttnPayload.dht22_1_humidity.value,\n    t_i: ttnPayload.dht22_1_temperature.value,\n    h: ttnPayload.dht22_2_humidity.value,\n    t: ttnPayload.dht22_2_temperature.value,\n    t_1: ttnPayload.ds18b20_1_temperature.value,\n    t_2: ttnPayload.ds18b20_2_temperature.value,\n    l: ttnPayload.light_intensity.value,\n    weight_kg: ttnPayload.weight.value\n};\n\n// Assigner les donnes formates au payload\nmsg.payload = beepData;\n\n// Retourner le message\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 340,
        "y": 400,
        "wires": [
            [
                "940e40a6602163f0",
                "a105f1375f47a467"
            ]
        ]
    },
    {
        "id": "a105f1375f47a467",
        "type": "http request",
        "z": "c526047e539d0589",
        "name": "BEEP",
        "method": "POST",
        "ret": "txt",
        "paytoqs": "ignore",
        "url": "https://api.beep.nl/api/sensors",
        "tls": "",
        "persist": false,
        "proxy": "",
        "insecureHTTPParser": false,
        "authType": "",
        "senderr": false,
        "headers": [
            {
                "keyType": "other",
                "keyValue": "Content-Type",
                "valueType": "other",
                "valueValue": "application/json"
            }
        ],
        "x": 530,
        "y": 400,
        "wires": [
            [
                "8eea2a2d9309e704"
            ]
        ]
    },
    {
        "id": "940e40a6602163f0",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 5",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 540,
        "y": 440,
        "wires": []
    },
    {
        "id": "8eea2a2d9309e704",
        "type": "function",
        "z": "c526047e539d0589",
        "name": "reponseBEEP",
        "func": "// Afficher le code de statut HTTP et la rponse\nmsg.payload = {\n    statusCode: msg.statusCode,\n    response: msg.payload\n};\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 720,
        "y": 400,
        "wires": [
            [
                "ccead9de17024575"
            ]
        ]
    },
    {
        "id": "ccead9de17024575",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 6",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 900,
        "y": 440,
        "wires": []
    },
    {
        "id": "82b2f88561bb1db8",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 7",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1080,
        "y": 220,
        "wires": []
    },
    {
        "id": "bb711d297e5b74cb",
        "type": "debug",
        "z": "c526047e539d0589",
        "name": "debug 8",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "false",
        "statusVal": "",
        "statusType": "auto",
        "x": 1080,
        "y": 300,
        "wires": []
    },
    {
        "id": "24ef70963ac20ffd",
        "type": "function",
        "z": "c526047e539d0589",
        "name": "OK",
        "func": "msg.payload = {\n  \"downlinks\": [{\n    \"f_port\": 2, // Port LoRaWAN\n    \"frm_payload\": \"AA==\", // 0 en base64\n    \"priority\": \"NORMAL\"\n  }]\n};\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 890,
        "y": 260,
        "wires": [
            [
                "577c64374091de44",
                "82b2f88561bb1db8"
            ]
        ]
    },
    {
        "id": "127aa1ec29e97b9b",
        "type": "function",
        "z": "c526047e539d0589",
        "name": "WARNING",
        "func": "msg.payload = {\n  \"downlinks\": [{\n    \"f_port\": 2, // Port LoRaWAN\n    \"frm_payload\": \"AQ==\", // 1 en base64\n    \"priority\": \"NORMAL\"\n  }]\n};\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 910,
        "y": 300,
        "wires": [
            [
                "577c64374091de44",
                "bb711d297e5b74cb"
            ]
        ]
    },
    {
        "id": "577c64374091de44",
        "type": "mqtt out",
        "z": "c526047e539d0589",
        "name": "downlink_commande",
        "topic": "v3/projet-lorawan-open-ruche@ttn/devices/stm32-device/down/push",
        "qos": "0",
        "retain": "",
        "respTopic": "",
        "contentType": "",
        "userProps": "",
        "correl": "",
        "expiry": "",
        "broker": "34fc5ca9084ec789",
        "x": 1120,
        "y": 260,
        "wires": []
    },
    {
        "id": "34fc5ca9084ec789",
        "type": "mqtt-broker",
        "name": "LoRa_STM32",
        "broker": "eu1.cloud.thethings.network",
        "port": "8883",
        "tls": "",
        "clientid": "projet-lorawan-open-ruche@ttn",
        "autoConnect": true,
        "usetls": true,
        "protocolVersion": "4",
        "keepalive": "60",
        "cleansession": true,
        "autoUnsubscribe": true,
        "birthTopic": "",
        "birthQos": "0",
        "birthRetain": "false",
        "birthPayload": "",
        "birthMsg": {},
        "closeTopic": "",
        "closeQos": "0",
        "closeRetain": "false",
        "closePayload": "",
        "closeMsg": {},
        "willTopic": "",
        "willQos": "0",
        "willRetain": "false",
        "willPayload": "",
        "willMsg": {},
        "userProps": "",
        "sessionExpiry": ""
    }
]

Python Server

Python
The server receives data from TTN via Node-RED, checks each measurement against predefined thresholds and returns a JSON response with "status" : "OK/WARNING" and an anomaly description in "details". It makes sure that each distinct problem triggers only one email to avoid spamming and gets the sunshine-duration during the day time by sending a GET request through Open-Meteo API and send the result directely to BEEP at the end of the day. Meanwhile, it creates a database by recording all the measurements in a csv file..
import csv
import os
import requests
import schedule
import threading
import time
from datetime import datetime
from flask import Flask, request, jsonify
import openmeteo_requests
import requests_cache
from retry_requests import retry

app = Flask(__name__)

# Dfinition des seuils
THRESHOLDS = {
    "dht22_1_temperature": {"min": 0.0, "max": 36.0},  # Temprature en C (extrieure)
    "dht22_1_humidity": {"min": 20.0, "max": 85.0},     # Humidit en %
    "dht22_2_temperature": {"min": 0.0, "max": 40.0},  # Temprature en C (intrieure)
    "dht22_2_humidity": {"min": 20.0, "max": 85.0},     # Humidit en %
    "ds18b20_1_temperature": {"min": 0.0, "max": 36.0},# Temprature en C
    "ds18b20_2_temperature": {"min": 0.0, "max": 36.0},# Temprature en C
    "light_intensity": {"min": 0.0, "max": 1000.0},    # Intensit lumineuse en lux
    "weight": {"min": 27.0, "max": 200.0}                # Poids en kg
}
LOG_FILE = "data_base/sensor_data.csv"

def ensure_csv_exists():
    """Cre le fichier CSV avec l'en-tte s'il n'existe pas"""
    if not os.path.exists(LOG_FILE):
        os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True)  # Cre le dossier si besoin
        with open(LOG_FILE, "w", newline="") as f:
            csv.writer(f).writerow(["Timestamp", "Capteur", "Valeur", "Unite", "Anomalie"])

def get_last_anomaly_state(sensor_name):
    """Rcupre le dernier tat d'anomalie du capteur depuis le CSV."""
    ensure_csv_exists()
    try:
        with open(LOG_FILE, "r") as f:
            reader = csv.reader(f)
            next(reader)  # Skip header
            for row in reversed(list(reader)):  # Lire depuis la fin
                if row[1] == sensor_name:
                    return row[4]  # Colonne "Anomalie"
    except FileNotFoundError:
        pass
    return "NORMAL"

@app.route('/data', methods=['POST'])
def check_sensors():
    ensure_csv_exists()
    data = request.json
    current_anomalies = []  # Toutes les anomalies actuelles
    new_anomalies = []      # Nouvelles anomalies (pas dans le dernier CSV)
    rows_to_write = []

    for key, values in data.items():
        if key in THRESHOLDS:
            value = values.get("value")
            unit = values.get("unit", "")
            min_val = THRESHOLDS[key]["min"]
            max_val = THRESHOLDS[key]["max"]

            is_anomaly = not (min_val <= value <= max_val)
            last_state = get_last_anomaly_state(key)

            # Enregistrer l'tat rel dans le CSV
            anomaly_status = "ANOMALIE" if is_anomaly else "NORMAL"
            rows_to_write.append([
                datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                key,
                value,
                unit,
                anomaly_status
            ])

            # Gestion des messages
            if is_anomaly:
                msg = f"{key}: {value} {unit} hors seuil [{min_val}, {max_val}]"
                current_anomalies.append(msg)
                if last_state != "ANOMALIE":
                    new_anomalies.append(msg)

    # crire dans le CSV
    with open(LOG_FILE, "a", newline="") as f:
        csv.writer(f).writerows(rows_to_write)

    # Gnrer la rponse
    if new_anomalies:
        status = "WARNING"
        message = f"Nouvelles alertes : {', '.join(new_anomalies)}"
        if current_anomalies:  # Ajouter les anomalies continues si ncessaire
            message += f" | Anomalies en cours: {', '.join(current_anomalies)}"
    elif current_anomalies:
        status = "OK"
        message = f"Anomalies en cours : {', '.join(current_anomalies)}"
    else:
        status = "OK"
        message = " Tout est normal"

    return jsonify({"status": status, "details": message})


# - Intgration BEEP/Open-Meteo

BEEP_API_URL = "https://api.beep.nl/api/sensors"
BEEP_API_KEY = "zxwyt3aqxmc2ae6r"  #  remplacer
COORDINATES = (48.8058614, 2.0769055)  # Coordonnes ruche


def get_sunshine_duration():
    """Rcupre la dure d'ensoleillement quotidienne"""
    try:
        # Configuration client API
        cache_session = requests_cache.CachedSession('.cache', expire_after=3600)
        retry_session = retry(cache_session, retries=5, backoff_factor=0.2)
        openmeteo = openmeteo_requests.Client(session=retry_session)

        # Appel API
        params = {
            "latitude": COORDINATES[0],
            "longitude": COORDINATES[1],
            "daily": "sunshine_duration",
            "timezone": "Europe/Paris",
            "forecast_days": 1
        }

        response = openmeteo.weather_api("https://api.open-meteo.com/v1/forecast", params=params)[0]
        daily = response.Daily()
        return daily.Variables(0).ValuesAsNumpy()[0]  # Dure en secondes

    except Exception as e:
        app.logger.error(f"Erreur Open-Meteo: {e}")
        return None


def send_sunshine_to_beep():
    """Envoi quotidien des donnes d'ensoleillement  BEEP"""
    sunshine = get_sunshine_duration()

    if sunshine is not None:
        try:
            payload = {
                "key": BEEP_API_KEY,
                "t_9": round(float(sunshine) / 3600, 2)  # Conversion en heures
            }

            response = requests.post(BEEP_API_URL, json=payload)
            response.raise_for_status()
            app.logger.info(f"Donnes envoyes  BEEP: {payload}")

        except Exception as e:
            app.logger.error(f"Erreur envoi BEEP: {e}")


def schedule_job():
    """Planifie les tches rcurrentes"""
    schedule.every().day.at("23:55:00").do(send_sunshine_to_beep)

    while True:
        schedule.run_pending()
        time.sleep(60)  # Vrifie toutes les minutes


# ================================================
# Dmarrage du serveur avec le scheduler
# ================================================

if __name__ == '__main__':
    # Dmarrer le scheduler dans un thread spar
    scheduler_thread = threading.Thread(target=schedule_job, daemon=True)
    scheduler_thread.start()

    # Dmarrer le serveur Flask
    app.run(host='0.0.0.0', port=14000, debug=False)

Credits

Aymen B.
1 project • 3 followers
Matteo ANDRIATSILAVO
1 project • 2 followers
Ioana Câcău
1 project • 3 followers
Zimeng Jiang
0 projects • 2 followers

Comments