DetecteurPanneSecteur

De Wiki LOGre
Aller à : navigation, rechercher

Projet réalisé par Philippe.

Présentation

Etant embêté par des pannes secteur intempestives et imprévisibles depuis plusieurs mois, et ne souhaitant pas revenir d'un séjour à l'extérieur avec le congélateur rempli d'asticots, j'essaie de créer un système qui me prévienne en cas de panne. M'étant intéressé à LoRaWAN dans un autre contexte récemment (capteurs de qualité de l'air), je vais me baser sur cette techno.

Principe

Je mets en place un "device" LoRa, alimenté sur secteur (via un chargeur USB par exemple) mais disposant aussi d'une batterie de secours, qui vérifie régulièrement sa tension d'alimentation et l'envoie sur TTN (The Things Network). Je récupère cette info sur un dashboard et en cas de tension passant sous un certain seuil le système envoie mail ou sms pour me prévenir. La batterie de secours est utile en cas de coupure secteur évidemment ;-) .

Autre détail, il faut bien entendu pour que ça marche avoir une antenne Gateway LoRa accessible dans le voisinage. J'habite dans un village, mais un de mes voisins en a une, recensée sur TTN. Donc parfait ! Sans celle du voisin, j'ai deux solutions :

  • installer une gateway LoRa chez moi, et m'arranger pour qu'elle soit sur onduleur avec ma box Internet;
  • ne pas utiliser LoRa mais le WiFi par exemple (dans ce cas la plupart des cartes ESP32 conviennent, munies nativement du WiFi), et là encore, je devrais mettre ma box sur onduleur, ou profiter du WiFi du voisin... :-S

L'autre option envisagée est de simplement détecter sur le dashboard qu'il n'a pas reçu de données depuis un certain temps, pour déclencher l'alarme. Dans ce cas, un device sans batterie qui simplement n'émet plus en cas de panne peut faire l'affaire. Avec le petit risque que l'absence de réception soit due à une cause différente.

Eléments techniques

La carte LoRaWAN que j'utilise pour mes capteurs est le modèle LilyGO TTGO T3 LoRa32 868MHz V2.1.6 ESP32. Il dispose de tout ce qui est nécessaire à mon détecteur de panne.

  • Il est alimenté par une prise micro-USB 5V;
  • Il dispose d'une prise pour batterie et gère la charge de la batterie (type Li-Ion 18650 3.7V par exemple);
  • Il dispose d'un module LoRaWAN et de son antenne;
  • accessoirement il dispose d'un petit écran permettant d'afficher quelques infos localement, ce qui ne gâche rien.

Pour la batterie, j'utilise une 18650 récupérée sur un vieux PC portable.

Le montage actuel, version proto, est fait sur une mini-breadboard, sur laquelle j'ajoute une prise micro-USB pour faciliter la récupération de la tension d'alimentation. Cette tension d'alimentation est renvoyée vers la prise uUSB du TTGO, et vers un diviseur de tension (1/2) pour la rendre compatible avec une entrée analogique de l'ESP32.

La tension de la batterie est disponible sur la pin GPIO35 du TTGO. Je connecte la tension d'alimentation sur la broche GPIO34 voisine.

  • En situation alimentée, la tension de la batterie est de l'ordre de 4.1V, et celle de l'alimentation de l'ordre de 5V.
  • En cas de coupure d'alimentation secteur, la tension de la batterie chute à peine (elle va baisser progressivement si la panne se prolonge) et celle mesurée en entrée (sur l'USB) passe à 3.7V environ.

Un seuil d'alerte de 4V à 4.5V sur l'alimentation semble donc bien adapté.

Code

J'ai programmé sous VisualStudio et PlatformIO. Le code ci-dessous est largement emprunté à ce que m'a proposé Oliv' (que je remercie chaleureusement au passage ;-) ). C'est basé sur les librairies "MCCI LoRaWAN LMIC library" et "CayenneLPP".

Fichier main.cpp

  /*******************************************************************************
   * Copyright (c) 2015 Thomas Telkamp and Matthijs Kooijman
   * Copyright (c) 2018 Terry Moore, MCCI
   *
   * Permission is hereby granted, free of charge, to anyone
   * obtaining a copy of this document and accompanying files,
   * to do whatever they want with them without any restriction,
   * including, but not limited to, copying, modification and redistribution.
   * NO WARRANTY OF ANY KIND IS PROVIDED.
   *
   * This example sends a valid LoRaWAN packet with payload "Hello,
   * world!", using frequency and encryption settings matching those of
   * the The Things Network.
   *
   * This uses OTAA (Over-the-air activation), where where a DevEUI and
   * application key is configured, which are used in an over-the-air
   * activation procedure where a DevAddr and session keys are
   * assigned/generated for use with all further communication.
   *
   * Note: LoRaWAN per sub-band duty-cycle limitation is enforced (1% in
   * g1, 0.1% in g2), but not the TTN fair usage policy (which is probably
   * violated by this sketch when left running for longer)!

  * To use this sketch, first register your application and device with
  * the things network, to set or generate an AppEUI, DevEUI and AppKey.
  * Multiple devices can use the same AppEUI, but each device has its own
  * DevEUI and AppKey.
  *
  * Do not forget to define the radio type correctly in
  * arduino-lmic/project_config/lmic_project_config.h or from your BOARDS.txt.
  *
  *******************************************************************************/
  #include <main.h>

  /* NOTE 
  TTGO display size : 
    X : 0 --> 127
    Y : 0 --> 63
  */

  // pour battery check
  const uint8_t vbatPin = 35;
  const uint8_t vAlimPin = 34;
  float VBAT;  // battery voltage from ESP32 ADC read
  float vAlim; // tension alimentation USB


  // Initialize the OLED display using Arduino Wire:
  SSD1306Wire display(OLED_addr, SDA, SCL);   // ADDRESS, SDA, SCL  -  SDA and SCL usually populate automatically based on your board's pins_arduino.h


  static osjob_t sendjob;
  void do_send(osjob_t* j);

  // Schedule TX every this many seconds (might become longer due to duty
  // cycle limitations).
  const unsigned TX_INTERVAL = 120;


  // Pin mapping
  const lmic_pinmap lmic_pins = {
      .nss = PIN_LMIC_NSS,
      .rxtx = LMIC_UNUSED_PIN,
      .rst = PIN_LMIC_RST,
      .dio = {PIN_LMIC_DIO0, PIN_LMIC_DIO1, PIN_LMIC_DIO2},
  };




  void measure() {

    display.clear();
      // Battery check
      // Battery Voltage
      VBAT = (float)(analogRead(vbatPin)) / 4095*2*3.3*1.1;
      Serial.print("Vbat = "); Serial.print(VBAT); Serial.println(" V");
      display.drawString(0, 0, "Batterie : "+ String(VBAT) + " V");
      // Alim voltage
      vAlim = (float)(analogRead(vAlimPin)) / 4095*2*3.3*1.08;
      Serial.print("Valim = "); Serial.print(vAlim); Serial.println(" V");
      display.drawString(0, 16, "Valim : "+ String(vAlim) + " V");
      if (vAlim > 4.0) display.drawString(0, 32, "Alim active");
      else display.drawString(0, 32, "Alim débranchée");
      display.display();
      // stack messages to LoRa via CayenneLPP format
      lpp.addAnalogOutput(lppChanBatVoltage, VBAT);
      lpp.addAnalogOutput(lppChanAlimVoltage, vAlim);

      delay(3000);


    Serial.println();

    display.display();

  } // end measure()



  void printHex2(unsigned v) {
      v &= 0xff;
      if (v < 16)
          Serial.print('0');
      Serial.print(v, HEX);
  } // end printHex2()

  void onEvent (ev_t ev) {
      Serial.print(os_getTime());
      Serial.print(": ");
      switch(ev) {
          case EV_SCAN_TIMEOUT:
              Serial.println(F("EV_SCAN_TIMEOUT"));
              break;
          case EV_BEACON_FOUND:
              Serial.println(F("EV_BEACON_FOUND"));
              break;
          case EV_BEACON_MISSED:
              Serial.println(F("EV_BEACON_MISSED"));
              break;
          case EV_BEACON_TRACKED:
              Serial.println(F("EV_BEACON_TRACKED"));
              break;
          case EV_JOINING:
              display.setTextAlignment(TEXT_ALIGN_RIGHT);
              display.drawString(127, 0, "Joining");
              display.display();
              display.setTextAlignment(TEXT_ALIGN_LEFT);
              Serial.println(F("EV_JOINING"));
              break;
          case EV_JOINED:
              display.setTextAlignment(TEXT_ALIGN_RIGHT);
              display.drawString(127, 10, "Joined");
              display.display();
              display.setTextAlignment(TEXT_ALIGN_LEFT);
              Serial.println(F("EV_JOINED"));
              {
                u4_t netid = 0;
                devaddr_t devaddr = 0;
                u1_t nwkKey[16];
                u1_t artKey[16];
                LMIC_getSessionKeys(&netid, &devaddr, nwkKey, artKey);
                Serial.print("netid: ");
                Serial.println(netid, DEC);
                Serial.print("devaddr: ");
                Serial.println(devaddr, HEX);
                Serial.print("AppSKey: ");
                for (size_t i=0; i<sizeof(artKey); ++i) {
                  if (i != 0)
                    Serial.print("-");
                  printHex2(artKey[i]);
                }
                Serial.println("");
                Serial.print("NwkSKey: ");
                for (size_t i=0; i<sizeof(nwkKey); ++i) {
                        if (i != 0)
                                Serial.print("-");
                        printHex2(nwkKey[i]);
                }
                Serial.println();
              }
              // Disable link check validation (automatically enabled
              // during join, but because slow data rates change max TX
        // size, we don't use it in this example.
              LMIC_setLinkCheckMode(0);
              break;
          /*
          || This event is defined but not used in the code. No
          || point in wasting codespace on it.
          ||
          || case EV_RFU1:
          ||     Serial.println(F("EV_RFU1"));
          ||     break;
          */
          case EV_JOIN_FAILED:
              Serial.println(F("EV_JOIN_FAILED"));
              break;
          case EV_REJOIN_FAILED:
              Serial.println(F("EV_REJOIN_FAILED"));
              break;
          case EV_TXCOMPLETE:
              Serial.println(F("EV_TXCOMPLETE (includes waiting for RX windows)"));
              if (LMIC.txrxFlags & TXRX_ACK)
                Serial.println(F("Received ack"));
              if (LMIC.dataLen) {
                Serial.print(F("Received "));
                Serial.print(LMIC.dataLen);
                Serial.println(F(" bytes of payload"));
              }
              // Schedule next transmission
              os_setTimedCallback(&sendjob, os_getTime()+sec2osticks(TX_INTERVAL), do_send);
              break;
          case EV_LOST_TSYNC:
              Serial.println(F("EV_LOST_TSYNC"));
              break;
          case EV_RESET:
              Serial.println(F("EV_RESET"));
              break;
          case EV_RXCOMPLETE:
              // data received in ping slot
              Serial.println(F("EV_RXCOMPLETE"));
              break;
          case EV_LINK_DEAD:
              Serial.println(F("EV_LINK_DEAD"));
              break;
          case EV_LINK_ALIVE:
              Serial.println(F("EV_LINK_ALIVE"));
              break;
          /*
          || This event is defined but not used in the code. No
          || point in wasting codespace on it.
          ||
          || case EV_SCAN_FOUND:
          ||    Serial.println(F("EV_SCAN_FOUND"));
          ||    break;
          */
          case EV_TXSTART:
              Serial.println(F("EV_TXSTART"));
              break;
          case EV_TXCANCELED:
              Serial.println(F("EV_TXCANCELED"));
              break;
          case EV_RXSTART:
              /* do not print anything -- it wrecks timing */
              break;
          case EV_JOIN_TXCOMPLETE:
              Serial.println(F("EV_JOIN_TXCOMPLETE: no JoinAccept"));
              break;

          default:
              Serial.print(F("Unknown event: "));
              Serial.println((unsigned) ev);
              break;
      }
  } // end onEvent()

  void do_send(osjob_t* j){

      // Check if there is not a current TX/RX job running
      if (LMIC.opmode & OP_TXRXPEND) {
          Serial.println(F("OP_TXRXPEND, not sending"));
      } else {
          lpp.reset();
          uint8_t ackUp = false;
          // Prepare upstream data transmission at the next possible time.

          measure();
          Serial.println("lppBufferSize: "+String(lpp.getSize()));
          // Serial.println("lppBuffer: "); Serial.println(lpp.getBuffer());
          LMIC_setTxData2(2, lpp.getBuffer(), lpp.getSize(), ackUp);

      }
      // Next TX is scheduled after TX_COMPLETE event.

    
      display.display();
  } // end do_send()

  void setup() {
      Serial.begin(115200);
      Serial.println(F("Starting"));

      // Battery check
      pinMode(vbatPin, INPUT);
      pinMode(vAlimPin, INPUT);
    
      // OLED
        // Initialising the UI will init the display too.
        display.init();
        display.flipScreenVertically();
        // display.setFont(ArialMT_Plain_10);
        display.setFont(ArialMT_Plain_10);
        display.clear();
        display.display();


      // LMIC init
      os_init();
      // Reset the MAC state. Session and pending data transfers will be discarded.
      LMIC_reset();

      // Start job (sending automatically starts OTAA too)
      do_send(&sendjob);
  } // end setup()


  void loop() {
      os_runloop_once();
  }

Fichier main.h

#include <Arduino.h>
#include <configuration.h>

#include "CayenneLPP.h"

// I2C
#include <Wire.h>

// OLED Display
#include "SSD1306Wire.h"        // legacy: #include "SSD1306.h"

CayenneLPP lpp(51);

// This EUI must be in little-endian format, so least-significant-byte
// first. When copying an EUI from ttnctl output, this means to reverse
// the bytes. For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70.
    // ttgo_multisensor_app : these 3 ID generated by TheThingsNetwork (2021-02-08) : 
static const u1_t PROGMEM APPEUI[8]={ 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.. };
void os_getArtEui (u1_t* buf) { memcpy_P(buf, APPEUI, 8);}

// This should also be in little endian format, see above.
static const u1_t PROGMEM DEVEUI[8]={ 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.. };
void os_getDevEui (u1_t* buf) { memcpy_P(buf, DEVEUI, 8);}

// This key should be in big endian format (or, since it is not really a
// number but a block of memory, endianness does not really apply). In
// practice, a key taken from ttnctl can be copied as-is.
static const u1_t PROGMEM APPKEY[16] = { 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.., 0x.. };
void os_getDevKey (u1_t* buf) {  memcpy_P(buf, APPKEY, 16);}

Fichier configuration.h

// Configuration file for TTGO LoRaWAN power supply checking
#ifndef configuration_h
#define configuration_h

// LoRaWAN
#include <lmic.h>
#include <hal/hal.h>

#define PIN_LMIC_NSS 18
#define PIN_LMIC_RST 14
#define PIN_LMIC_DIO0 26
#define PIN_LMIC_DIO1 33
#define PIN_LMIC_DIO2 32

    // the Oled display address.
    #define OLED_addr 0x3c 

// Constants
const uint8_t lppChanBatVoltage = 0; 
const uint8_t lppChanAlimVoltage = 1; 

#endif

Version 2, transmission par HTTP

Alimentation par onduleur

Ayant finalement acheté un onduleur qui me servira pour d'autres appareils, j'en profite pour faire une nouvelle version du détecteur de panne EDF ne dépendant pas de LoRa ni donc de l'accès à une gateway : cette version n'utilise "que" la communication WiFi et le protocole HTTP. La box Wifi de la maison est alimentée par un onduleur. J'ai ce modèle Eaton 3S 850 (en promo à 80€ quand je l'ai commandé) qui dispose de prises parafoudres, de prises ondulées, et de 2 prises USB qui sont aussi alimentées sur batterie en cas de panne secteur. Il a une autonomie affichée de 70min quand il alimente la box Internet.

Carte de développement

J'utilise un microcontrôleur avec une entrée analogique (on devrait même pouvoir travailler en numérique d'ailleurs), et une connexion réseau WiFi, et je l'alimente par USB. N'importe quelle carte de développement basée ESP32 ou ESP8266 peut alors faire l'affaire. J'ai fait mes tests avec une carte de développement de type NodeMCU et avec une Wemos D1 Mini.

Le circuit

Le circuit est très simple. La liste des composants : une prise micro-USB, deux résistances, et la carte ESP. Le câblage se fait ainsi :

La carte ESP sera alimentée par une sortie USB sécurisée de l'onduleur, et un adaptateur secteur USB sera branché sur une prise protégée mais non ondulée pour alimenter la prise micro-USB à surveiller. En mode normal, l'alimentation à surveiller fournit 5V, qui se ramènent à 3.3V derrière le pont diviseur sur l'entrée A0. Une défaillance secteur fera passer la prise micro-usb à 0V, donc 0V sur l'ADC0 qui est tiré au GND par la résistance de 20k.

Montage

J'aurais pu quasiment fixer la carte ESP et la prise USB dans un boîtier et réaliser le câblage en direct. J'ai finalement opté pour poser le tout sur une plaque à trous. Quelques supports pour maintenir la carte NodeMCU, deux vis pour la prise USB. Les résistances soudées, ainsi que quelques connexions complémentaires sous la plaque.

Un petit boîtier est imprimé (fin de bobine = changement de couleur ;-) ) muni de deux ouvertures pour les prises micro-USB et de rails latéraux internes pour l'insertion de la carte. Le boîtier est fermé par un couvercle et une vis "spécial plastique" diamètre 2.2mm.

Quelques photos de l'ensemble :

Le Code

Basé sur un exemple de transmission HTTP à partir d'ESP32, j'envoie les données vers un dashboard Tago.IO J'ai choisi de transmettre la mesure de tension sur la prise USB toutes les 60 secondes environ. C'est alors le dashboard qui est en charge de détecter un défaut et de générer une alerte.


/*
  Web client

 This sketch connects to a website using a WiFi shield.

 This example is written for a network using WPA encryption. For
 WEP or WPA, change the Wifi.begin() call accordingly.

 created 13 July 2010
 by dlf (Metodo2 srl)
 modified 31 May 2012
 by Tom Igoe
 */
/*
Adapted by Philippe marin
pour surveiller l'alimentation secteur de la maison, transmettre une info sur tago.io,
et envoyer une alerte (SMS ou mail) en cas de défaillance.

*/
#include <Arduino.h>
#include <ESP8266WiFi.h>


int keyIndex = 0;            // your network key Index number (needed only for WEP)

int status = WL_IDLE_STATUS;
// if you don't want to use DNS (and reduce your sketch size)
// use the numeric IP instead of the name for the server:
//IPAddress server(74,125,232,128);  // numeric IP for Google (no DNS)
char server[] = "api.tago.io";    // name address for Tago.io(using DNS)

// Initialize the Ethernet client library
// with the IP address and port of the server
// that you want to connect to (port 80 is default for HTTP):
WiFiClient client;
const char* ssid = "monSSID";    // name of your wifi network
const char* password = "monPWD";     // wifi pasword



unsigned long lastConnectionTime = 0;            // last time you connected to the server, in milliseconds
// Le cycle global avec les attentes prend environ 5s, j'ajoute 55s pour faire 1 série d'envoi par minute, à peu près.
const unsigned long postingInterval = 55L * 1000L; // delay between updates, in milliseconds


// check alim
#define alim_pin 0 
uint32_t valeurLue = 0;
float tensionLue = 0;

// === connexion réseau wifi local
void setup_Wifi_Direct(){

  Serial.print("connecting to ");
  Serial.println(ssid);
  WiFi.mode(WIFI_STA); 
  WiFi.begin(ssid, password);
  int compteur = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
    compteur++;
    if (compteur >= 50) {
      compteur = 0;
      Serial.println("WiFi : Reconnexion...");
      WiFi.begin(ssid, password);
    }
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}
// === fin connexion réseau wifi local

// ====  envoi requete http
String Device_Token = "12345678-1234-1234-1234-1234567890"; // mettre ici l'identifiant du device sous TagoIO


// this method makes a HTTP connection to the server:
void httpRequest(String varName, String varVal, String varUnit) {
//void httpRequest() {
    // close any connection before send a new request.
    // This will free the socket on the WiFi shield
    client.stop();

//    Serial.println("\nStarting connection to server...");
    // if you get a connection, report back via serial:
    String PostData = String("{\"variable\":\"") + varName
                    + String("\", \"value\":") + varVal 
                    + String(",\"unit\":\"") + varUnit + String("\"}");
//    String PostData = String("{\"variable\":\"temperature\", \"value\":") + String(temp_string)+ String(",\"unit\":\"C\"}");
    String Dev_token = String("Device-Token: ")+ String(Device_Token);
    if (client.connect(server,80)) {                      // we will use non-secured connnection (HTTP) for tests
//    Serial.println("connected to server");
    // Make a HTTP request:
    client.println("POST /data? HTTP/1.1");
    client.println("Host: api.tago.io");
    client.println("_ssl: false");                        // for non-secured connection, use this option "_ssl: false"
    client.println(Dev_token);
    client.println("Content-Type: application/json");
    client.print("Content-Length: ");
    client.println(PostData.length());
    client.println();
    client.println(PostData);
    // note the time that the connection was made:
    lastConnectionTime = millis();
  }
  else {
    // if you couldn't make a connection:
    Serial.println("HTTP connection failed");
    // On reinit le wifi au cas où
     WiFi.begin(ssid, password);
  }
}
// ==== fin envoi requete http


void setup() {
  
  //Initialize serial and wait for port to open:
  Serial.begin(115200);
  while (!Serial) {
    ; // wait for serial port to connect. Needed for native USB port only
  }
  // init Wifi
  setup_Wifi_Direct();
  
    // note the time that the connection was made:
    lastConnectionTime = millis() + postingInterval; // pour ne pas attendre la première fois

  pinMode(alim_pin, INPUT);

}


void printWifiStatus() {
  // print the SSID of the network you're attached to:
  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  // print your WiFi shield's IP address:
  IPAddress ip = WiFi.localIP();
  Serial.print("IP Address: ");
  Serial.println(ip);

  // print the received signal strength:
  long rssi = WiFi.RSSI();
  Serial.print("signal strength (RSSI):");
  Serial.print(rssi);
  Serial.println(" dBm");
}

void loop() {

  // if there are incoming bytes available
  // from the server, read them and print them:
  while (client.available()) {
    char c = client.read();
    Serial.write(c);
  }

  // if "postingInterval" seconds have passed since your last connection, (/1000)
  // then connect again and send data:

  if (millis() - lastConnectionTime > postingInterval) {

    valeurLue = analogRead(alim_pin);
    Serial.println("\n\nValeurLue : " + String(valeurLue));
    tensionLue = valeurLue*5.0/1024; // on ramène à 5V compte tenu du diviseur de tension présent.
    Serial.println("\nTension alim : " + String(tensionLue) + "V\n");
  
      // check connexion Wifi
      long rssi = WiFi.RSSI();
      Serial.print("signal strength (RSSI):");
      Serial.print(rssi);
      Serial.println(" dBm");

      // === envoi à Tago.io
      httpRequest("AlimState", String(tensionLue), "V");
      
      delay(500);


  }

}