Station météo de balcon avec ESP32-C3 et Google sheet

Bonjour à toutes et tous, amis francophones et francophiles.

Ma dernière création roule depuis quelques jours et je suis ravi de vous en présenter quelques aspects.
Notez bien que j'avais déjà conçu un modèle simple de station météo basée sur une arduino 33IOT et le cloud d'arduino (visible ici) mais des soucis d'autonomie et les possibilités de la version gratuite du cloud m'ont poussé à choisir une autre solution.

J'ai arrêté mon choix sur une ESP32-C3 de Xiao pour la partie "capteur et communication wifi" et google sheet pour la partie "affichage / tableau de bord". L'objectif étant clairement:

  • dégoter une solution gratuite pour faire "trembler" les puissants (home automation et consorts)
  • limiter les intermédiaires au strict minimum (MQTT, RedNode etc...)
  • apprendre la patience, parceque ça a pas toujours été facile de trouver des infos cohérentes et fiables!

Voici une petite vue de la chose finalisée (:smiling_face_with_three_hearts:) dans son environnement naturel

Je n'ai pas de schéma correct pour le moment à vous présenter (le projet a subi pas mal de modifs depuis les plans initiaux) mais voici au moins quelques indications:

  • La pin A0 est connectée par pont diviseur (2 résistances de 100kOhm) à l'alimentation depuis la batterie pour une mesure de voltage;
  • Les pins A1 et A2 alimentent et lisent la valeur de la LDR;
  • Les pins A4 et A5 assurent la communication I2C avec les 2 modules (baromètre et hygromètre) tandis que la pin A3 assure l'alimentation des modules.
  • Enfin, la Pin A6 n'est pas soudée à la planche perforée, ce qui m'a libéré un emplacement pour y connecter la masse: j'ai donc mes 4 connexions par I2C côte à côte.

Les connexions de la planche perforée à la batterie sont faites par un connecteur JST, ce qui me permettra de remplacer la batterie par un module panneau solaire qui n'attend que ça.

Remarque: l'arrivée batterie depuis le connecteur JST est soudé sur 2 arrivées dédiées placées sous la carte ESP. Ces petits points de connexion sont très fragiles (pour mes gros doigts) et j'ai fusillé 2 cartes de suite en voulant à tout prix souder le cable d'alim ET la résistance du pont diviseur.
Ma solution a été de ne souder QUE les fils d'alimentation et de poser les résistances au niveau du connecteur JST, bien moins sensible à mes mauvais traitements.

Les connexions à la LDR et aux modules I2C (soudés entre eux en série) est faite par des headers, ça devrait me permettre de remplacer facilement les modules si besoin, sans toucher aux soudures.

Une fois tout ça en place, on passe au code (que je réserve pour la fin du post).
Bien sur la fonction dee-sleep est centrale dans mon usage, puisque l'autonomie sera un point clé de la réussite du projet.
La carte se réveille environ tous les quart d'heure, réalise ses mesures sur tous les capteurs, puis se connecte au wi-fi, transmet les valeurs et retourne faire dodo.

Toutes les valeurs sont envoyées à un script google sheet créé pour l'occasion dont l'unique rôle est de compléter la première ligne vide d'une page google sheet identifiée:

function doGet(e){

var sheet = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets/d/ADRESSE_DE_MA_GOOGLE_SHEET/edit#gid=0');

var CurrentDate = new Date();
var Date_Heure = Utilities.formatDate(CurrentDate, "Europe/Paris","dd/MM/YYYY HH:mm:ss")
sheet.appendRow([Date_Heure, e.parameter.val1, e.parameter.val2, e.parameter.val3, e.parameter.val4, e.parameter.val5]);
}

astuce de feignasse: c'est le script qui ajoute l'heure et la date à mes données. La carte ESP elle, se contente de compter par paquet de 15min, j'économise un module RTC!

et je récupère quelque chose comme ça:

Magnifique! mes 5 données (dans l'odre: millivolts de la batterie, valeur analogique de luminosité, température en degrés *100, pression atmosphérique en Pa et enfin humidité en % *100) sont dispo, le reste n'est qu'une histoire d'arrangement de données pour faire joli.

Non testé par frousse: mes données sont toutes transmises sous forme d'entier, éventuellement converties par la carte avant envoi (humidité et température). Je m'évite un traitement de conversion . <-> , ou autre joyeuseté non prévue.

Pour le client final, j'ai créé cette vue (désolé mes petits dessins de thermomètre et de gouttes d'eau n'aiment pas trop les conversions de fichier, mais je vous assure qu'ils sont très réussis!)

Bien sur j'ai aussi des vues plus "pro" qui me servent de tableau de bord pour le fonctionnement lui même: détection de valeurs hors normes, des échecs de connexion etc...

voici le code utilisé (wahou, c'est moi qui ai fait tout ça???):

// Version implémentée au 08 mars 23
// ESP32-C3 avec capteurs:
// - mesure de tension de la batterie en A0//
// - mesure de la luminosité en A1/A2
// - mesure I2C pression et température en A4/A5
// - mesure I2C humidité en A4/A5
// I2C commandé par A3 (power) A4 (SDA) A5 (SCL) et GND

// se réveille, lit les capteurs, envoie les données par internet, fonction sleep 15min, 
// si reset (connecté à un PC): 60 sec de pause avant sleep (pour upload new sketch)

#include <WiFi.h>
#include <HTTPClient.h>
#include <Arduino.h>
#include <Wire.h>

#define uS_TO_S_FACTOR 1000000 /* Conversion factor for micro seconds to seconds */
#define TIME_TO_SLEEP 900      /* Time ESP32 will go to sleep (in seconds) */

#define TAILLE_REGISTRE_BMP388 (21)  // pour lecture parametre compensation mesure
uint8_t MAP_REGISTER_BMP388[TAILLE_REGISTRE_BMP388];

#define ADRESSE_I2C_SI702 0x40
#define CMD_Read_Hold_RH_SI702 0xE5  // clock stretching during measurement
#define CMD_Read_TEMP_SI702 0xE0
#define CMD_Read_REG_SI702 0xE6  // D7-D0 : bit de resolution de lecture
#define CMD_Write_REG_SI702 0xE7

#define ADRESSE_I2C_BMP388 0x76
#define REG_CHIP_ID_BMP388 0x0
#define ID_BMP388 0x50
#define REG_STATUS_BMP388 0x03    // [5:6]: dataready P et T
#define REG_PWR_CTRL_BMP388 0x1B  // = 0b110011 // normal mode P/T ON // 0b010011 = forced mode
#define REG_OSR_BMP388 0x1C       // 0 = no oversampling
#define REG_DATA0_BMP388 0x04     // départ data P et T

const char* ssid = "";                     // change SSID
const char* password = "";  // change password

String GOOGLE_SCRIPT_ID = "";  // change Gscript ID

const int Pin_batterie = A0;
const int Pin_mesure_ldr = A1;
const int Pin_power_ldr = A2;
//const int Pin_Led = A0;
const int Pin_power_I2C = A3;

unsigned long elapsed_time;          // calculé à la fin: temps éveillé par cycle
uint32_t firstmillis = 0;            // time stamp pour connexion I2C
const uint32_t I2C_time_out = 1000;  // time interval pour connexion I2C

int batterie = 0;
int luminosite = 0;
int temperature = 0;  // issue du baromètre
int humidite = 0;
int pression = 0;  // 176m d'altitude 1013,03 hPa_mer <-> 990,766 hPa_alt
//int reason_wake_up = 0;

struct BMP388_calib_data {
  float par_t1;
  float par_t2;
  float par_t3;
  float t_lin;

  float par_p1;
  float par_p2;
  float par_p3;
  float par_p4;
  float par_p5;
  float par_p6;
  float par_p7;
  float par_p8;
  float par_p9;
  float par_p10;
  float par_p11;
};
BMP388_calib_data calib_data;

void setup() {

  unsigned long start_time = millis();  // heure de réveil
  setCpuFrequencyMhz(80);               // réglage de la fréquence du CPU à 80 Mhz

  // lecture des capteurs : 5 valeurs
  LECTURE_BATTERIE();
  LECTURE_LUMINOSITE();
  LECTURE_PRESSION_HUMIDITE_TEMPERATURE();  // lecture combinée I2C: SI702 et BMP388

  // connexion wifi et envoi des 5 valeurs
  WIFI_CONNECT();
  ENVOI_DONNEE_GOOGLE_SHEET();

  long elapsed_time = (millis() - start_time) / 1000;  // temps éveillé en seconde
  DEEP_SLEEP();
}

void loop() {
  //This is not going to be called
}

/////////////////
// Méthodes CAPTEURS
////////////////

void LECTURE_BATTERIE() {
  // lecture analogique compensée sur A0: fournit la tension en millivolt

  pinMode(Pin_batterie, INPUT);  // lecture voltage batterie par pont diviseur
  analogRead(Pin_batterie);
  delay(2);
  batterie = analogReadMilliVolts(Pin_batterie) * 1.943;  // *1,943 = ration pont diviseur (=1/2)
}

void LECTURE_LUMINOSITE() {
  // lecture analogique sur Pin_LDR après alimentation par Pin_power_LDR

  pinMode(Pin_mesure_ldr, INPUT);  // lecture LDR
  pinMode(Pin_power_ldr, OUTPUT);  // alimentation LDR
  digitalWrite(Pin_power_ldr, HIGH);
  delay(2);
  analogRead(Pin_mesure_ldr);
  delay(2);
  luminosite = analogRead(Pin_mesure_ldr);
  digitalWrite(Pin_power_ldr, LOW);
}

void LECTURE_PRESSION_HUMIDITE_TEMPERATURE() {

  // allume avant setup connexion I2C
  pinMode(A3, OUTPUT);
  digitalWrite(A3, HIGH);
  Wire.begin();

  LECTURE_PRESSION_TEMPERATURE();  // Lecture sur BMP388
  LECTURE_HUMIDITE_TEMPERATURE();  // Lecture sur SI702

  digitalWrite(A3, LOW);  // eteint après connexion I2C
}

void LECTURE_HUMIDITE_TEMPERATURE() {  // Lecture sur SI702

  uint8_t tampon[2];

  Wire.beginTransmission(ADRESSE_I2C_SI702);
  Wire.write(CMD_Read_Hold_RH_SI702);
  Wire.endTransmission();
  // delay(25);
  Wire.requestFrom(ADRESSE_I2C_SI702, 2);

  for (int i = 0; i < 2; i++) {
    *(tampon + i) = Wire.read();
  }

  float humid = (125.00 * (tampon[0] << 8 | tampon[1])) / (0xFFFF + 1) - 6;
  humidite = humid * 100;
}

void LECTURE_PRESSION_TEMPERATURE() {  // Lecture sur BMP388

  while ((!IS_ME_BMP388()) && (I2C_time_out > millis() - firstmillis)) {
    delay(20);
  }
  // module I2C Pression/Temperature pret

  SETUP_PARAMS_BMP388();
  READ_TEMP_PRESSION();
}

/////////////////
// Méthodes CORE
////////////////

void DEEP_SLEEP() {

  int reason_wake_up = esp_sleep_get_wakeup_cause();

  if (reason_wake_up == 4) {

  } else {
    delay(60000);
  }

  esp_sleep_enable_timer_wakeup((TIME_TO_SLEEP - elapsed_time) * uS_TO_S_FACTOR);
  Serial.flush();
  WiFi.disconnect();

  esp_deep_sleep_start();
  //    Serial.println("This will never be printed");
}

int WIFI_CONNECT() {

  int try_count = 0;
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  delay(2500);
  unsigned long currentMillis = millis();
  const int wifi_time_out = 15000;  // timeout connexion wifi
  while ((WiFi.status() != WL_CONNECTED)
         && (wifi_time_out > millis() - currentMillis)) {

    delay(900);
    try_count++;
  }
  return try_count;
}

void ENVOI_DONNEE_GOOGLE_SHEET() {

  // compose l'URL à envoyer (à paramétrer en fonction du script google)
  //https://script.google.com/macros/s/""/exec?val1=test
  String urlFinal = "https://script.google.com/macros/s/" + GOOGLE_SCRIPT_ID + "/exec?"
                    + "val1=" + String(batterie)
                    + "&val2=" + String(luminosite)
                    + "&val3=" + String(temperature)
                    + "&val4=" + String(pression)
                    + "&val5=" + String(humidite);

  // envoie l'URL
  HTTPClient http;
  http.begin(urlFinal.c_str());
  http.GET();
  http.end();
}

void READ_TEMP_PRESSION() {
  // write OSR[2:0] Pression / OSR[5:3] Température: set up oversampling et resolution
  writebyte(ADRESSE_I2C_BMP388, REG_OSR_BMP388, 0);
  writebyte(ADRESSE_I2C_BMP388, REG_PWR_CTRL_BMP388, 0b010011);  // forced mode
  delay(25);

  uint32_t firstmillis = millis();
  const uint32_t I2C_time_out = 1000;
  while (!IS_DATA_READY_BMP388() && (I2C_time_out > millis() - firstmillis)) {
    delay(10);
  }
  if (IS_DATA_READY_BMP388()) {
    readBytes(ADRESSE_I2C_BMP388, REG_DATA0_BMP388, MAP_REGISTER_BMP388, 6);

    uint32_t byte_pression = (MAP_REGISTER_BMP388[2] << 16
                              | MAP_REGISTER_BMP388[1] << 8 | MAP_REGISTER_BMP388[0]);
    uint32_t byte_temperature = (MAP_REGISTER_BMP388[5] << 16
                                 | MAP_REGISTER_BMP388[4] << 8 | MAP_REGISTER_BMP388[3]);

    float _temperature = CONVERT_TEMP(byte_temperature);
    temperature = (float)_temperature * 100;
    float _pression = CONVERT_PRESSION(byte_pression);
    pression = _pression;
  } else {
    //Serial.println("Erreur d'identification module Pression");
  }
}

/////////////////
// Méthodes internes BMP388
////////////////

float CONVERT_PRESSION(uint32_t _pression) {

  float partial_data1 = calib_data.par_p6 * calib_data.t_lin;
  float partial_data2 = calib_data.par_p7 * (calib_data.t_lin * calib_data.t_lin);
  float partial_data3 = calib_data.par_p8 * (calib_data.t_lin * calib_data.t_lin * calib_data.t_lin);
  float partial_out1 = calib_data.par_p5 + partial_data1 + partial_data2 + partial_data3;

  partial_data1 = calib_data.par_p2 * calib_data.t_lin;
  partial_data2 = calib_data.par_p3 * (calib_data.t_lin * calib_data.t_lin);
  partial_data3 = calib_data.par_p4 * (calib_data.t_lin * calib_data.t_lin * calib_data.t_lin);
  float partial_out2 = (float)_pression * (calib_data.par_p1 + partial_data1 + partial_data2 + partial_data3);

  partial_data1 = (float)_pression * (float)_pression;
  partial_data2 = calib_data.par_p9 + calib_data.par_p10 * calib_data.t_lin;
  partial_data3 = partial_data1 * partial_data2;
  float partial_data4 = partial_data3 + (float)_pression * (float)_pression * (float)_pression * calib_data.par_p11;

  float comp_press = partial_out1 + partial_out2 + partial_data4;

  return comp_press;
}

float CONVERT_TEMP(uint32_t temp) {

  float partial_data1 = (float)temp - calib_data.par_t1;
  float partial_data2 = (float)partial_data1 * calib_data.par_t2;

  calib_data.t_lin = partial_data2 + (partial_data1 * partial_data1) * calib_data.par_t3;

  return calib_data.t_lin;
}

void SETUP_PARAMS_BMP388() {
  // lecture des paramètres de compensation

  readBytes(ADRESSE_I2C_BMP388, 0x31, MAP_REGISTER_BMP388, TAILLE_REGISTRE_BMP388);

  // association des paramètres pour compensation température
  uint16_t NVM_PAR_T1 = MAP_REGISTER_BMP388[1] << 8 | MAP_REGISTER_BMP388[0];
  uint16_t NVM_PAR_T2 = MAP_REGISTER_BMP388[3] << 8 | MAP_REGISTER_BMP388[2];
  int8_t NVM_PAR_T3 = MAP_REGISTER_BMP388[4];

  calib_data.par_t1 = (float)(NVM_PAR_T1 / pow(2.0f, -8.0f));
  calib_data.par_t2 = (float)(NVM_PAR_T2 / pow(2.0f, 30.0f));
  calib_data.par_t3 = (float)(NVM_PAR_T3 / pow(2.0f, 48.0f));

  // association des paramètres pour compensation pression
  int16_t NVM_PAR_P1 = MAP_REGISTER_BMP388[6] << 8 | MAP_REGISTER_BMP388[5];
  int16_t NVM_PAR_P2 = MAP_REGISTER_BMP388[8] << 8 | MAP_REGISTER_BMP388[7];
  int8_t NVM_PAR_P3 = MAP_REGISTER_BMP388[9];
  int8_t NVM_PAR_P4 = MAP_REGISTER_BMP388[10];
  uint16_t NVM_PAR_P5 = MAP_REGISTER_BMP388[12] << 8 | MAP_REGISTER_BMP388[11];
  uint16_t NVM_PAR_P6 = MAP_REGISTER_BMP388[14] << 8 | MAP_REGISTER_BMP388[13];
  int8_t NVM_PAR_P7 = MAP_REGISTER_BMP388[15];
  int8_t NVM_PAR_P8 = MAP_REGISTER_BMP388[16];
  int16_t NVM_PAR_P9 = MAP_REGISTER_BMP388[18] << 8 | MAP_REGISTER_BMP388[17];
  int8_t NVM_PAR_P10 = MAP_REGISTER_BMP388[19];
  int8_t NVM_PAR_P11 = MAP_REGISTER_BMP388[20];

  calib_data.par_p1 = ((float)NVM_PAR_P1 - powf(2.0f, 14.0f)) / powf(2.0f, 20.0f);
  calib_data.par_p2 = ((float)NVM_PAR_P2 - powf(2.0f, 14.0f)) / powf(2.0f, 29.0f);
  calib_data.par_p3 = (float)NVM_PAR_P3 / powf(2.0f, 32.0f);
  calib_data.par_p4 = (float)NVM_PAR_P4 / powf(2.0f, 37.0f);
  calib_data.par_p5 = (float)NVM_PAR_P5 / powf(2.0f, -3.0f);
  calib_data.par_p6 = (float)NVM_PAR_P6 / powf(2.0f, 6.0f);
  calib_data.par_p7 = (float)NVM_PAR_P7 / powf(2.0f, 8.0f);
  calib_data.par_p8 = (float)NVM_PAR_P8 / powf(2.0f, 15.0f);
  calib_data.par_p9 = (float)NVM_PAR_P9 / powf(2.0f, 48.0f);
  calib_data.par_p10 = (float)NVM_PAR_P10 / powf(2.0f, 48.0f);
  calib_data.par_p11 = (float)NVM_PAR_P11 / powf(2.0f, 65.0f);
}

bool IS_DATA_READY_BMP388() {
  // renvoi 1 si mesure P et T pretes
  if (READ_BYTE(ADRESSE_I2C_BMP388, REG_STATUS_BMP388) & (0b11 << 6)) {
    return 1;
  } else {
    //  Serial.println("Capteur BMP388 non pret");
    return 0;
  }
}

bool IS_ME_BMP388() {
  // renvoi 1 si l'identification du module est correcte
  if (READ_BYTE(ADRESSE_I2C_BMP388, REG_CHIP_ID_BMP388) == ID_BMP388) {
    return 1;
  } else {
    // Serial.println("Erreur d'identification module Pression");
    return 0;
  }
}

///////////////////////
// I2C METHODES
//////////////////////

void writebyte(uint8_t I2C_adresse, uint8_t register_adresse, uint8_t message) {

  Wire.beginTransmission(I2C_adresse);
  Wire.write((uint8_t)register_adresse);
  Wire.write((uint8_t)message);
  Wire.endTransmission();
}

uint8_t READ_BYTE(uint8_t I2C_adresse, uint8_t registre) {

  // Serial.print("registre " + String(registre) + " : ");

  Wire.beginTransmission(I2C_adresse);
  Wire.write((uint8_t)registre);
  Wire.endTransmission();

  Wire.requestFrom(I2C_adresse, 1);
  uint8_t* byte;
  *byte = Wire.read();
  // Serial.println(*byte);
  return *byte;
}

void readBytes(uint8_t I2C_adresse, uint8_t register_adresse, uint8_t* data, uint8_t size) {

  Wire.beginTransmission(I2C_adresse);
  Wire.write((uint8_t)register_adresse);
  Wire.endTransmission();

  Wire.requestFrom(I2C_adresse, size);

  for (int i = 0; i < size; i++) {
    *(data + i) = Wire.read();
  }
}

Petite astuce en lien avec le deep_sleep: dans son sommeil, l'esp ne sera pas en état de recevoir un nouveau sketch (la carte n'est simplement pas visible par le PC). Ce qui me laisse environ 10s (son temps d'éveil) toutes les 15min pour télécharger une mise à jour.
Je peux vous dire que ça a été très pénible et que j'ai souvent eu à uploader d'abord un sketch vide avec le mode BOOT pour récupérer l'accès à la carte par le port COM.
La ruse est d'utiliser la variable esp_sleep_get_wakeup_cause() qui renvoie "4" si la carte se réveille "naturellement" par son timer interne.
Toute autre valeur, dans mon utilisation, correspond à un reset provoqué par le branchement au PC, ce qui provoque une pause de 60s avant que la carte ne replonge dans le sommeil. De quoi avoir le temps de télécharger un sketch dans la carte.

Deux dernières choses:
Je chronomètre le temps d'éveil total de la carte, et je le soustrait au temps de sommeil. De cette façon, j'ai globalement un signal toutes les 15 min, meme si la carte peut prendre plus ou moins de temps pour se connecter au wifi (l'étape la plus longue de mon code).
La communication I2C est surveillée par un time_out. Sinon, j'ai un risque de vider la batterie dans une boucle sans fin. Au pire, je rate une ou deux valeurs mais au moins la carte reprend son rythme normal.

Voilà, c'est à peu près tout, je sais que ce genre de réalisation est énormément documenté partout ici ou sur le net, mais là c'est la mienne, et j'avais très envie de partager ça avec vous!

1 Like

Bonjour @GrandPete

Merci pour cette présentation !

C'est un thème sur lequel j'ai passé pas mal de temps et il y a toujours à apprendre des solutions tierces :wink:

Dans mes réalisations voisines en Wifi (vers ThingSpeak en règle générale, pour profiter d'un peu de temps de calcul Matlab sur les données) la connection au point d'accès est la part prépondérante des périodes d'éveil des ESP et j'ai d'ailleurs l'impression que la bibliothèque WiFi des ESP32 fait quelque chose en plus au niveau d ela connectionpar rapport à celle des ES8266. Mes records en terme de brièveté des sessions étaient avec des cartes D1 mini (ESP8266) toutes choses égales par ailleurs.

Google Sheet : Dommage que Google ne propose pas , à ma connaissance, son broker MQTT pour alimenter sans intermédiaire une feuille de calcul !

J'en viens à privilégier les destinations accessibles directement par MQTT, j'apprécie son efficacité et la facilité avec laquelle, avec un client graphique sur ordi (MQTT Explorer...etc) ou sur smartphone, on peut interagir avec le serveur (broker) pour débugger ou juste observer.

1 Like

Bravo @GrandPete !C'est une belle réalisation!
Pardon si tu as déjà dit ça dans ta présentation mais... Ou récupére tu les données? Sur un ordi? Un téléphone? Les deux?
Merci.
Ta réalisation est SUPERBE!

les données sont exportées vers un google sheet hébergé par le drive d'une de mes adresses Gmail.
à partir de là, je peux les partager à qui veut bien, ou les afficher depuis n'importe quel accès internet, comme si je consultais mes mails!

merci! :smiling_face_with_three_hearts:

1 Like

Super!Là, je suis sur mon aspirateur mais il se peut que je réalise cet engin si j'en ai la motiv'!

Ah mais cela me dit quelque chose !
Belle réalisation !

Roland

Salut, merci pour ton commentaire!
Par contre je suis intrigué, est-ce que tu as reconnu dans mon projet des éléments à toi, ou tu as déjà posté un projet similaire?
au plaisir de te lire.

Hello !

J'ai reconnu l'une ou l'autre ligne de code !

Je viens de poster hier un sujet sur mon projet à moi que tu peux trouver là :
//forum.arduino.cc/t/afficheur-meteo-couleur-tempo-conso-elec-production-solaire-etc/1155122

Roland

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