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 () 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!