Une lumière pilotée par un détecteur d’obscurité + présence avec ESP-IDF
Posted on jeu. 30 janvier 2025 in Electronics
Le projet¶
Je cherche à allumer une lampe à l’extérieur de chez moi, sur détection de présence mais seulement si la luminosité est suffisament basse. Pourquoi le faire moi-même plutôt que d’acheter une solution toute faite dans le commerce ? Eh bien c’est ce que j’ai fait jusqu’à présent, mais ces produits n’étaient pas satisfaisants. Tous ceux que j’ai pu tester ont fini par lacher après une saison en extérieur…
Puisque je ne trouve pas mon bonheur sur un étalage, je vais assembler moi-même ce dont j’ai besoin !
Maintenant, pourquoi utiliser un ESP32 avec ESP-IDF ?
- J’ai des boards ESP32-WROOM-32 en nombre.
- Je suis dans ma phase de découverte du framework ESP-IDF.
- Concernant ce framework, ça me permettra d’utiliser des modules que je n’ai pas encore abordés :
- mise en sommeil ;
- interruption ;
- GPIOs.
L’algorithme¶
Une image vaut mieux qu’un long discours.
Premiers pas : sommeil et réveil¶
On utilise les fonctions du header esp_sleep.h
pour, dans l’ordre : activer les sources de réveil, puis déclencher la mise en sommeil profond.
main.c
#include <stdio.h>
#include <stdint.h>
#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"
#include "esp_sleep.h"
#define S_TO_US(x) (x*1000000)
#define LIGHTSENSOR GPIO_NUM_12
#define DETECTOR GPIO_NUM_33
RTC_DATA_ATTR uint8_t count = 0;
RTC_DATA_ATTR uint16_t light_threshold = 2048;
uint16_t light_measure = 3000;
void app_main(void)
{
printf("Cycle %d – ", count);
++count;
esp_sleep_enable_timer_wakeup(S_TO_US(3));
if(light_measure < light_threshold) {
printf("Waking from EXT0 enabled – ");
esp_sleep_enable_ext0_wakeup(DETECTOR, 1);
} else {
printf("Waking from EXT0 disabled — ");
}
printf("Going to sleep\n");
esp_deep_sleep_start();
}
Voyons ce code en détail. Je vous passe toutefois les include
.
#define S_TO_US(x) (x*1000000)
#define LIGHTSENSOR GPIO_NUM_12
#define DETECTOR GPIO_NUM_33
La fonction qui permet de régler le délai au bout duquel l’ESP32 doit se réveiller attend des microsecondes, or je trouve plus pratique de m’exprimer en secondes.
La macro S_TO_US
est justement là pour ça : elle prend des secondes en paramètre et renvoie des microsecondes.
Nous verrons son usage plus loin.
RTC_DATA_ATTR uint8_t count = 0;
RTC_DATA_ATTR uint16_t light_threshold = 2048;
uint16_t light_measure = 3000;
Je définis ici deux variables, count
et light_threshold
, préfixées par RTC_DATA_ATTR
.
Ce préfixe permet de stocker ces variables dans la mémoire SRAM de la RTC, ce qui les protège de l’effacement pendant le deep-sleep.
Sans cette instruction, celles-ci seraient remises à zéro à chaque réveil.
Le count
n’a pas d’intérêt technique pour mon projet, mais il me permet, au travers de son incrémentation, de savoir que l’ESP32 suit bien une série de sommeils et réveils.
Pour mon seuild de luminosité, j’ai mis la moitié de la valeur maximale de l’ADC de l’ESP32 à 12 bits (4095) de manière arbitraire. Ceci sera à ajuster en production.
Enfin, je définis ici une fausse mesure de luminosité pour me permettre de tester le fonctionnement du reste de mon code.
// in app_main()
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
printf("Cycle %d – ", count);
++count;
esp_sleep_enable_timer_wakeup(S_TO_US(3));
La première instruction de la fonction app_main
vise à désactiver toute source de réveil de l’ESP32.
Comme indiqué dans la documentation du framework, ces sources ne sont pas désactivées automatiquement au réveil. Si je n’y prends pas garde, je pourrais ainsi avoir un réveil lié au détecteur de présence alors qu’il est sensé être désactivé.
On appelle donc esp_sleep_disable_wakeup_source
, et par « commodité » on désactive toutes les sources de réveil avec ESP_SLEEP_WAKEUP_ALL
.
J’affiche le n° du cycle actuel, puis je le mets à jour (cette incrémentation pourrait en fait se trouver n’importe où dans le app_main
, tant qu’elle est avant la mise en sommeil effective).
J’active ensuite la possibilité de réveiller l’ESP32 par un timer interne au bout de 3 secondes. Notez ici qu’on donne cette possibilité au microcontrôleur, mais que celui-ci n’est pas encore en sommeil.
if(light_measure < light_threshold) {
printf("Waking from EXT0 enabled – ");
esp_sleep_enable_ext0_wakeup(DETECTOR, 1);
} else {
printf("Waking from EXT0 disabled — ");
}
Voici le premier embranchement logique de mon programme. Si la mesure de luminosité (qu’il me faut encore, à l’heure actuelle, coder) est inférieure au seuil de 2048, alors on active aussi la possibilité d’un réveil par un événement extérieur avec esp_sleep_enable_ext0_wakeup
Cet événement, c’est le pin DETECTOR
(soit GPIO_NUM_33
) qui va le porter. Au passage, avec le 1
en second paramètre, on demande à détecter l’état haut.
Attention, tous les pins GPIO ne peuvent pas être utilisés comme source de réveil EXT0. Vérifiez la doc pour savoir lesquels vous pouvez utiliser.
esp_deep_sleep_start();
C’est l’instruction qui permet de passer en mode deep sleep. Notez que tout ce qui suit cette instruction ne sera jamais exécuté.
Si on lance ce code et qu’on le laisse tourner, l’ESP va se mettre en sommeil pendant 3 secondes, se réveiller et recommencer.
En changeant la valeur de light_measure
à 2000
, le réveil par interruption sur le pin 33 sera activé, et le mettre à l’état haut pendant le temps de sommeil sortira automatiquement l’ESP de sa veille.
Récupérer les données de l’environnement¶
Maintenant que les cycles sommeil / réveil sont acquis, je peux passer à l’acquisition des données en me basant sur ce que j’avais déjà noté précédemment.
Note : dans la configuration du channel, j’ai voulu me passer du paramètre .atten
. Résultat : l’ADC me retournait 4095 en permanence. J’en conclus donc que ce paramètre est important…
#include "hal/adc_types.h"
#include "esp_adc/adc_oneshot.h"
adc_oneshot_unit_handle_t adc_handle;
adc_oneshot_unit_init_cfg_t adc_config = {
.unit_id = ADC_UNIT_1,
};
adc_oneshot_chan_cfg_t channel_config = {
.bitwidth = ADC_BITWIDTH_DEFAULT,
.atten = ADC_ATTEN_DB_12,
};
uint16_t measure_light_sensor(adc_channel_t channel) {
int adc_raw = 0;
adc_oneshot_read(adc_handle, channel, &adc_raw);
return(adc_raw);
}
Dans le app_main
, je peux ensuite faire appel à measure_light_sensor
quand j’en ai besoin. Avant de l’appeler, il me faut toutefois initialiser l’ADC.
//app_main
adc_oneshot_new_unit(&adc_config, &adc_handle);
adc_oneshot_config_channel(adc_handle, ADC_CHANNEL_0, &channel_config);
Piloter la lumière : déclenchement du relais¶
Pour allumer et éteindre la lampe, j’ai choisi d’utiliser un relais bistable Double-Pole Double-Throw (DPDT) :
- sur la partie puissance, j’ai deux circuits séparés (DP), chacun avec une position Normally Open (pas de contact) et Normally Closed (contact) (=DT) ;
- sur la partie commande, j’ai deux bobines différentes : une qui active le relais (bascule du contact, le courant passe désormais vers les broches NO), et l’autre qui le désactive.
J’ai donc besoin de 2 broches pour contrôler le relais. Je fais le choix d’utiliser les n°26 et 27, mais il serait tout à fait possible d’en utiliser d’autres.
Configuration des GPIO¶
D’un point de vue du code, ces deux broches sont identifiées comme GPIO_NUM_26
et GPIO_NUM_27
. Mais avant de pouvoir les utiliser, il faut les configurer. On doit en effet faire savoir à l’ESP qu’on veut utiliser ces GPIO comme sorties. Cette configuration peut se faire de deux façons différentes.
Configuration unitaire¶
Dans ce mode, on configure chaque GPIO un par un.
gpio_set_direction(GPIO_NUM_XX, GPIO_MODE_OUTPUT);
Pour plus de renseignement sur les modes de fonctionnement des GPIO, je vous invite à vous référer à cette section de la documentation officielle ESP-IDF.
Comme j’ai ici seulement besoin de les utiliser comme sorties, je les configure en GPIO_MODE_OUTPUT
. Je ne définis pas de pullups ni pulldowns.
Configuration « globale »¶
Cette méthode, dont un exemple d’utilisation est disponible sur Github, permet de configurer plusieurs pins d’un coup, avec les mêmes paramètres.
// On créée un masque qui permettra de sélectionner les pins que l’on souhaite
#define GPIO_OUTPUT_SEL ((1ULL<<RELAY_ON_PIN) | (1ULL<<RELAY_OFF_PIN))
// Dans app_main()
gpio_config_t gpio_conf = {
.pin_bit_mask = GPIO_OUTPUT_SEL, // Quels seront les pins affectés par cette configuration
.intr_type = GPIO_INTR_DISABLE, // Désactiver les interrupts
.mode = GPIO_MODE_OUTPUT, // Sortie uniquement
.pull_down_en = 0, // Nous n’activons pas le pull-down intégré…
.pull_up_en = 0, // … Ni les pull-ups
};
gpio_config(&gpio_conf);
Avec cette méthode, on configure tous les GPIO d’une même série (partageant la même configuration) en une passe.
La logique de déclenchement et d’arrêt¶
// app_main()
/* À chaque redémarrage, il faut récupérer la cause du réveil pour
* savoir quelles actions faire ensuire */
esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause();
// On configure les GPIO et, par sécurité, on les met à l’état bas
gpio_config(&gpio_conf);
gpio_set_level(RELAY_OFF_PIN, 0);
gpio_set_level(RELAY_ON_PIN, 0);
esp_sleep_disable_wakeup_source(ESP_SLEEP_WAKEUP_ALL);
// Configuration de l’ADC
adc_oneshot_new_unit(&adc_config, &adc_handle);
adc_oneshot_config_channel(adc_handle, ADC_CHANNEL_0, &channel_config);
printf("Cycle %d – ", count);
++count;
switch(wakeup_cause) {
case ESP_SLEEP_WAKEUP_EXT0:
// On est réveillés par le détecteur
printf("Woke-up from EXT0 — ");
printf("Lumiere = %d – ", lumiere); // Juste pour vérifier la valeur du booléen
if(!lumiere){
// Si la lumière est off, on l’allume. Sinon, pas la peine de relancer un pulse
ESP_ERROR_CHECK(pulse_port(RELAY_ON_PIN, 500));
printf("Light OFF, switching ON — ");
fflush(stdout);
lumiere = true;
}
vTaskDelay(2000/portTICK_PERIOD_MS); // Pas nécessaire, probablement à supprimer en prod
break;
case ESP_SLEEP_WAKEUP_TIMER:
printf("Woke-up from timer — ");
light_measure = measure_light_sensor(ADC_CHANNEL_0);
printf("Light level = %d – ", light_measure);
if(lumiere){
/* Si l’ESP est réveillé par le timer alors que la lumière est allumée,
* ça signifie que la présence est partie. On peut dès lors éteindre
* la lumière */
printf("Light ON, switching OFF – ");
ESP_ERROR_CHECK(pulse_port(RELAY_OFF_PIN, 500));
lumiere = false;
}
break;
default:
/* Ce cas se produit au reboot. Dans ce cas, on veut s’assurer que la
* lumière est bien off une première fois */
printf("Unknown wake-up cause — ");
pulse_port(RELAY_OFF_PIN, 1000);
break;
};
esp_sleep_enable_timer_wakeup(S_TO_US(SLEEP_DURATION_S));
if(light_measure < light_threshold) {
printf("Waking from EXT0 enabled – ");
esp_sleep_enable_ext0_wakeup(DETECTOR, 1);
} else {
printf("Waking from EXT0 disabled — ");
}
printf("Going to sleep\n");
esp_deep_sleep_start();
Note 1 : un pulse trop court (10 mS) semble poser parfois problème pour un bon déclenchement du relais. J’ai mis 500 mS, c’est peut-être un peu excessif mais ça fonctionne. Il faudra que je voie à l’usage.
Note 2 : dans tout cet exemple, le résultat des printf
s’affiche en une fois au moment de l’appel au deep sleep. Cela tient au fait que l’ESP32 flush son buffer stdout
à ce moment là. Pour forcer un flush à un autre moment, on peut soit ajouter un \n
en fin de chaîne, soit utiliser la fonction fflush(stdout);
(voir références).
Note 3 : ce code présente un gros défaut : tant qu’une présence est détectée, on ne refait pas de check sur le niveau de luminosité. Il faudrait le remettre à jour périodiquement pour éviter un allumage illimité alors que le jour est revenu.
Point sur le code¶
Je vais m’arrêter ici pour ce post. Le code à l’étape actuelle est disponible sur Github. Il est assez mal organisé, présente quelques lacunes et gagnerait peut-être à être refactoré, mais ce sera pour une prochaine fois.
Les tests « labo » que j’ai pu mener sont concluants, il me reste maintenant à :
- définir le seuil de luminosité approprié ;
- choisir un éclairage à raccorder à mon relais ;
- préparer un boîtier pour accueillir l’ESP, le régulateur solaire & la batterie ;
- et enfin installer tout ça sur place !
Si j’ai le temps, j’essaierai d’y ajouter un moyer de configurer certains paramètres du dispositif sans devoir modifier le code (et donc sans re-compiler ni flasher l’ESP32), soit via un serveur WiFi, soit par une liaison UART.
Note : pour faire mes tests labo, j’ai remplacé la photorésistance par un potentiomètre (le principe reste le même) et le détecteur de présence par un bouton-poussoir avec une résistance de pulldown. Prochaine étape, utiliser les composants définitifs.
Bibliographie¶
- Espressif. Sleep Modes. Version 5.4. Consulté le 2025-01-25.
- Last Minute Engineers. ESP32 Deep Sleep & Wakeup Sources. Consulté le 2025-01-25.
- Printf() requires a \n for output?. Forum ESP32. Consulté le 2025-01-29.