Station météo Arduino avec sonde externe
par , le mercredi 25 décembre 2024 à 00:46

Catégorie : Général
Mots clés : Arduino

Suite à une erreur de manipulation, ma station météo a pris l'eau et ne veut plus redémarrer. J'ai donc décidé d'en développer une à partir d'une base Arduino.


Souhaits :

La station et la sonde doivent avoir les fonctionnalités suivantes :

  • Mesures très régulières (toutes les minutes ?) de la température et humidité interne et externe.
  • Station sur secteur (pour ne pas avoir à changer pile).
  • Sonde externe sur batterie rechargeable.
  • Envoie des informations vers un serveur consultable par une simple page web.
  • Plateforme matérielle et logiciel évolutive (ajout de capteurs possible dans le futur).

Mon plan de batailles est :

Plan de bataille

Prototypage :

Le choix d'une solution matérielle et logiciel ouverte s'impose, et l'écosystème Arduino vient immédiatement en tête. Cela permet de développer rapidement un prototype et laisse ouverte toute évolution future.

Au vu du matériel dont je dispose, j'envisage les configurations suivantes :

Station de base :

Carte Arduino classique alimentée sur le secteur avec envoi des informations par WiFi vers un serveur HTTP.

Schema station ProtoType station

Carte Arduino Montage station

Note : j'utilise une carte Arduino antérieure à la Rev3, et du coup il est nécessaire que je relie, sur la carte WiFi Shield, la broche 3.3v à la broche IOREF (je l'ai fait après la prise de photo et la conception du schéma électronique).

Le croquis Arduino de la sonde est :

#include <string.h>
#include <SPI.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include "DHTStable.h"
#include <SensorReceiver.h>

//#define DEBUG_
#define DEBUG_INFO

#define LOCAL_SENSOR_LED A0
#define REMOTE_SENSOR_LED A1
#define WIFI_LED A2

#define DHT22_PIN 3

#define RECEIVER_INT 0

#define INTERVAL 60 // in seconds

DHTStable DHT;
unsigned long last_sensor_read;

volatile bool remoteSensorTrigger = false;
volatile float remoteSensorTemperature, remoteSensorHumidity;

char ssid[] = "SSID";
char pass[] = "CLE_WPA2";
int status = WL_IDLE_STATUS;
char server[] = "virtualhost.domain.tld";  // Hostname du serveur PHP qui héberge le script PHP sonde-meteo.php

WiFiClient client;

//---------------------------------------------------------------------
void notifyError(int led) {
  for (int flash = 0; flash < 5; flash++) {
    delay(500);
    digitalWrite(led, HIGH);
    delay(500);
    digitalWrite(led, LOW);
  }

  delay(1000);
}

//---------------------------------------------------------------------
bool readLocalSensor(float &temp, float &humid) {
  if (millis() < last_sensor_read)
    last_sensor_read = millis();

  if (millis() - last_sensor_read < INTERVAL * 1000L)
    return false;

  last_sensor_read = millis();

  digitalWrite(LOCAL_SENSOR_LED, HIGH);
  int sensor_result = DHT.read22(DHT22_PIN);
  delay(1000);
  digitalWrite(LOCAL_SENSOR_LED, LOW);

  switch (sensor_result) {
    case DHTLIB_OK:
      temp = DHT.getTemperature();
      humid = DHT.getHumidity();
      break;
    case DHTLIB_ERROR_CHECKSUM:
      Serial.println("Checksum error");
      break;
    case DHTLIB_ERROR_TIMEOUT:
      Serial.println("Time out error");
      break;
    default:
      Serial.println("Unknown error");
      break;
  }

  if (sensor_result != DHTLIB_OK) {
    notifyError(LOCAL_SENSOR_LED);
    return false;
  }

  return true;
}

//---------------------------------------------------------------------
bool readRemoteSensor(float &temp, float &humid) {
  bool available = false;

  noInterrupts();
  if (remoteSensorTrigger) {
    temp = remoteSensorTemperature;
    humid = remoteSensorHumidity;
    remoteSensorTrigger = false;
    available = true;
  }
  interrupts();

  return available;
}

//---------------------------------------------------------------------
void displaySensor(int id, float temp, float humid) {
      Serial.print(String("Température_") + id + ":");
      Serial.print(temp, 1);
      Serial.print("\t");
      Serial.print(String("Humiditée_") + id + ":");
      Serial.print(humid);
      Serial.println();
}

//---------------------------------------------------------------------
void receiveRemoteSensor(byte *data) {
  if ((data[3] & 0x1f) == 0x1e) {
    byte channel, id;
    int temp;
    byte humid;

    digitalWrite(REMOTE_SENSOR_LED, HIGH);
    SensorReceiver::decodeThermoHygro(data, channel, id, temp, humid);
    delay(1000);
    digitalWrite(REMOTE_SENSOR_LED, LOW);

    remoteSensorTemperature = temp / 10.0;
    remoteSensorHumidity = humid;
    remoteSensorTrigger = true;
  }
}

//---------------------------------------------------------------------
void checkWifiShield() {
if (WiFi.status() == WL_NO_SHIELD) {
#ifdef DEBUG
    Serial.println("WiFi shield not present");
#endif
    // don't continue:
    while (true);
  }
}

//---------------------------------------------------------------------
void connectWifi() {
    while (status != WL_CONNECTED) {
#ifdef DEBUG
      Serial.print("Attempting to connect to SSID: ");
      Serial.println(ssid);
#endif
      digitalWrite(WIFI_LED, HIGH);
      status = WiFi.begin(ssid, pass);
      delay(10000);
      digitalWrite(WIFI_LED, LOW);
  }

#ifdef DEBUG
  Serial.println("Connected to wifi");
#endif
}

//---------------------------------------------------------------------
void printWiFiStatus() {
  byte mac[6];

  Serial.print("SSID: ");
  Serial.println(WiFi.SSID());

  Serial.print("signal strength (RSSI):");
  Serial.print(WiFi.RSSI());
  Serial.println(" dBm");

  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  WiFi.macAddress(mac);
  Serial.print("MAC: ");
  for (int i = 5 ; i >= 0; --i) {
    Serial.print(mac[i],HEX);
    if (i > 0)
      Serial.print(":");
  }
  Serial.println();
}

//---------------------------------------------------------------------
void listWifiNetworks() {
  Serial.println("*** Scan Networks ***");

  byte numSsid = WiFi.scanNetworks();

  Serial.print("number of available networks:");
  Serial.println(numSsid);

  for (int network = 0; network < numSsid; network++) {
    Serial.print(network);
    Serial.print(") ");
    Serial.print(WiFi.SSID(network));
    Serial.print("\tSignal: ");
    Serial.print(WiFi.RSSI(network));
    Serial.print(" dBm");
    Serial.print("\tEncryption: ");
    Serial.println(WiFi.encryptionType(network));
  }
}

//---------------------------------------------------------------------
void sendSensorToServer(int id, float temp, float humid) {
  String url = String("/sonde-meteo.php?id=") + id + "&temp=" + temp + "&humid=" + humid + " HTTP/1.1";

#ifdef DEBUG
  Serial.println("\nStarting connection to server...");
#endif

  if (client.connect(server, 80)) {
#ifdef DEBUG
    Serial.println("connected to server");
    Serial.println("Calling : http://" + String(server) + url);
    Serial.println("...");
#endif
    digitalWrite(WIFI_LED, HIGH);
    client.println("GET " + url);
    client.println("Host: " + String(server));
    client.println("Connection: close");
    client.println();
    delay(500);
    digitalWrite(WIFI_LED, LOW);
  }
}

//---------------------------------------------------------------------
void flushServerResponse() {
  digitalWrite(WIFI_LED, HIGH);

  while (client.connected()) {
    while (client.available()) {
      char c = client.read();
#ifdef DEBUG
      Serial.write(c);
#endif
    }
  }

#ifdef DEBUG
  Serial.println();
  Serial.println("------------------------------------------");
  Serial.println("Disconnecting from server.");
#endif

  client.stop();

  digitalWrite(WIFI_LED, LOW);
}

//---------------------------------------------------------------------
void setup() {
  Serial.begin(9600);
  pinMode(LOCAL_SENSOR_LED, OUTPUT);
  pinMode(REMOTE_SENSOR_LED, OUTPUT);
  pinMode(WIFI_LED, OUTPUT);

  digitalWrite(LOCAL_SENSOR_LED, HIGH);
  delay(500);
  digitalWrite(LOCAL_SENSOR_LED, LOW);
  digitalWrite(REMOTE_SENSOR_LED, HIGH);
  delay(500);
  digitalWrite(REMOTE_SENSOR_LED, LOW);
  digitalWrite(WIFI_LED, HIGH);
  delay(500);
  digitalWrite(WIFI_LED, LOW);
  delay(500);

  last_sensor_read = millis();

checkWifiShield();

#ifdef DEBUG
    listWifiNetworks();
#endif    

  connectWifi();

#ifdef DEBUG
    printWiFiStatus();
#endif

  SensorReceiver::init(RECEIVER_INT, receiveRemoteSensor);
}

//---------------------------------------------------------------------
void loop() {
  float temperature, humidity;

  if (readLocalSensor(temperature, humidity)) {
#ifdef DEBUG_INFO    
    displaySensor(0, temperature, humidity);
#endif  
    sendSensorToServer(0, temperature, humidity);
    flushServerResponse();
  }

  delay(500);

  if (readRemoteSensor(temperature, humidity)) {
#ifdef DEBUG_INFO
    displaySensor(1, temperature, humidity);
#endif    
    sendSensorToServer(1, temperature, humidity);
    flushServerResponse();
  }

  delay(500);
}

Il faut bien sûr modifier les #define en début de croquis pour indiquer les paramètres de connexion WiFi

Le #define DEBUG_INFO permet de renvoyer les valeurs de température (station + sonde) sur le moniteur série de l'IDE Arduino, et du coup d'afficher les valeurs sous forme texte ou graphique :

Température_0:20.7    Humiditée_0:67.40
Température_1:22.0    Humiditée_1:60.00
Température_0:20.7    Humiditée_0:67.50
Température_1:21.9    Humiditée_1:61.00

Le #define DEBUG (à ne pas laisser en production) permet d'afficher un maximum d'information (connexion WiFi, URL appelée...) :


*** Scan Networks *** number of available networks:9 0) kozodo2 Signal: -41 dBm Encryption: 4 1) FreeWifi_secure Signal: -45 dBm Encryption: 4 2) SFR_A2D0 Signal: -62 dBm Encryption: 4 3) Livebox-EFB0_EXT Signal: -67 dBm Encryption: 4 4) SFR_30D1 Signal: -83 dBm Encryption: 4 5) SFR-f79c Signal: -83 dBm Encryption: 4 6) Bbox-7DBC0456 Signal: -86 dBm Encryption: 4 7) SFR_FCA0 Signal: -89 dBm Encryption: 4 8) SFR-b728 Signal: -93 dBm Encryption: 4 Attempting to connect to SSID: kozodo2 Connected to wifi SSID: kozodo2 signal strength (RSSI):-47 dBm IP Address: 192.168.99.174 MAC: 78:C4:E:1:A0:94 Température_1:22.0 Humiditée_1:62.00 Starting connection to server... connected to server Calling : http://consommation.kozodo.com/sonde-meteo.php?id=1&temp=22.00&humid=62.00 HTTP/1.1 ... HTTP/1.1 200 OK Date: Sun, 22 Dec 2024 22:23:51 GMT Server: Apache/2.4.62 (Debian) Vary: Accept-Encoding Content-Length: 99 Connection: close Content-Type: text/html; charset=UTF-8 influx -database teleinfo -execute "insert sonde externe_temperature=22.00,externe_humiditee=62.00" ------------------------------------------ Disconnecting from server.

Sonde externe :

Carte TinyDuino qui utilise le même processeur que la UNO mais en 8MHz au lieu de 16Mhz et en 3.3V au lieu de 5v. Ce type de carte mesure 2cm*2cm et les shield associés ont en général la même taille et s'empile avec un micro connecteur intégré. Je vous invite à visiter le [https://tinycircuits.com/pages/tinyduino-overview](site du fabricant du TinyDuino) qui fait un boulot remarquable dans ce domaine.

Note: pour le prototype de la sonde, à cause de la taille du shield Proto Terminal, j'ai dû ajouter un shield LED pour pouvoir connecter la prise USB, ce dernier shield ne sera plus nécessaire une fois les composants soudés sur le shield Proto final.

Schema sonde Prototype sonde

Carte TinyDuino Carte TinyDuino sur la tranche Montage sonde

Le croquis de la sonde externe est :

#include "LowPower.h"
#include "DHTStable.h"
#include "SensorTransmitter.h"

#define DEBUG_INFO

#define LOCAL_SENSOR_LED 12

#define DHT22_PIN 3

#define TRANSMETER_PIN 2
#define TRANSMETER_ID 0
#define TRANSMETER_CHANNEL 1

#define INTERVAL 60 // in seconds

DHTStable DHT;

ThermoHygroTransmitter transmitter(TRANSMETER_PIN, TRANSMETER_ID, TRANSMETER_CHANNEL);

//---------------------------------------------------------------------
void notifyError(int led) {
  for (int flash = 0; flash < 5; flash++) {
    delay(500);
    digitalWrite(led, HIGH);
    delay(500);
    digitalWrite(led, LOW);
  }

  delay(1000);
}

//---------------------------------------------------------------------
bool readLocalSensor(float &temp, float &humid) {
  digitalWrite(LOCAL_SENSOR_LED, HIGH);
  int sensor_result = DHT.read22(DHT22_PIN);
  delay(1000);
  digitalWrite(LOCAL_SENSOR_LED, LOW);

  switch (sensor_result) {
    case DHTLIB_OK:
      temp = DHT.getTemperature();
      humid = DHT.getHumidity();
      break;
    case DHTLIB_ERROR_CHECKSUM:
      Serial.println("Checksum error");
      break;
    case DHTLIB_ERROR_TIMEOUT:
      Serial.println("Time out error");
      break;
    default:
      Serial.println("Unknown error");
      break;
  }

  if (sensor_result != DHTLIB_OK) {
    notifyError(LOCAL_SENSOR_LED);
    return false;
  }

  return true;
}

//---------------------------------------------------------------------
void sendRemoteSensor(float temp, float humid) {
  digitalWrite(LOCAL_SENSOR_LED, HIGH);
  transmitter.sendTempHumi(temp * 10, humid);
  delay(500);
  digitalWrite(LOCAL_SENSOR_LED, LOW);
  }

//---------------------------------------------------------------------
void displaySensor(float temp, float humid) {
      Serial.print("Température:");
      Serial.print(temp, 1);
      Serial.print("\t");
      Serial.print("Humiditée:");
      Serial.print(humid);
      Serial.println();
}

//---------------------------------------------------------------------
void setup() {
  Serial.begin(9600);

  pinMode(LOCAL_SENSOR_LED, OUTPUT);
  digitalWrite(LOCAL_SENSOR_LED, HIGH);
  delay(1000);
  digitalWrite(LOCAL_SENSOR_LED, LOW);
}

//---------------------------------------------------------------------
void loop() {
  float temperature, humidity;

  if (readLocalSensor(temperature, humidity)) {
#ifdef DEBUG_INFO    
    displaySensor(temperature, humidity);
#endif
    delay(500);
    sendRemoteSensor(temperature, humidity);
  }

  LowPower.longPowerDown(INTERVAL * 1000L);
}

Serveur :

  • Serveur Apache + PHP + InfluxDB (déjà en place sur mon serveur).
  • Petit script PHP pour enregistrer les données dans une base InfluDB.
  • Service Grafana pour afficher les mesures.

Script PHP pour enregistrer les données dans la base InfluxDB :

<?php

if (filter_var($_REQUEST['id'], FILTER_VALIDATE_INT) === false ||
    filter_var($_REQUEST['temp'], FILTER_VALIDATE_FLOAT) === false ||
    filter_var($_REQUEST['humid'], FILTER_VALIDATE_FLOAT) === false)
    die();

$sonde = 'interne';

if ($_REQUEST['id'] == 1)
     $sonde = 'externe';

$cmd = "influx -database teleinfo -execute \"insert sonde ${sonde}_temperature=$_REQUEST[temp],${sonde}_humiditee=$_REQUEST[humid]\"";

echo "$cmd";
passthru($cmd);
?>

Page Grafana :

Affichage des mesures

Réalisation finale :

Station de base :

Soudage de la station météo sur un shield Proto CISECO X-1 :

Note: la connexion des broches de la LED RGB ne sont pas dans le même ordre que sur la breadboard, les couleurs sont donc différentes, mais la logique reste la même.

Sonde externe :

Soudage de la sonde externe sur shield Proto CISECO X-1 :

Note 1 : vu la petite taille du shield Proto X-1, j'ai dû changer la broche du cateur DHT22 et celle de la LED.

Note 2 : la carte TinyDuino USB n'est réellement nécessaire que pour re-programmer la sonde ou recharger la batterie.

Mise en place :

J'ai réalisé 2 boitiers sous Freecad à imprimer en 3D sur ma Dagoma DiscoEasy200 (+ quelques options) :

  • boitier carré avec insertion par l'arrière du montage station météo. Le cache arrière n'a pas de système d'accroche, il faut juste le tenir avec un point de colle ou un petit bout de scotch.
  • boitier tube avec vissage par le bas pour le fermer et passage d'une petite cordelette pour le suspendre. Ce boitier est prévu pour un usage extérieur.

Boitier station météo Boitier sonde météo

Les fichiers 3D sont :

Ce qui donne en impression 3D :

Améliorations possibles :

  • optimiser la consommation énergétique de la sonde (éteindre le DAC, supprimer les delay()...) et changer de mode d'alimentation (pile bouton si durée de vie > 5ans, micro-batterie TinyDuino si durée de vie ≥ 1 an).
  • contrôler le niveau de batterie de la sonde externe, envoyer cette information à la station de base et notifier par LED (et email ?) l'approche de fin de batterie.
  • envoyer les données au serveur web en SLL.
  • améliorer le boitier de la station de base pour avoir un cache clipsable.
  • revoir le boitier de la sonde externe (en fonction de l'option d'alimentation).
  • surveiller la réception des valeurs sur le serveur et alerter en cas de perte prolongée.
  • rendre configurable le WiFi de la station de base (mini serveur web dans la station ?).
  • gérer plusieurs sondes externes.

A bientôt

Antoine

Ecrire à l'auteur