/*
 * Détecteur de CO2 v3
 * 
 * Auteur: Christophe Boyanique
 * Licence: CC-BY-NC (CC Attribution-Share Alike 4.0 International)
 * https://creativecommons.org/licenses/by-sa/4.0/deed.fr
 * 
 * Code issu d'autres auteurs:
 * 
 * routines pour le capteur S8:
 *   Auteur: SFeli@github
 *   Source: https://github.com/SFeli/ESP32_S8
 * 
 * tableau de capacité de l'accumulateur:
 *   Auteur: Henri BACHETTI @bitbucket.org
 *   Source: https://bitbucket.org/henri_bachetti/mysensors-battery-thermometer-hygrometer/src/master/
 *           https://riton-duino.blogspot.com/2019/05/un-thermometre-hygrometre-mysensors-sur.html
 *
 * Bibliothèques requises:
 * 
 * Tone32           v1.0.0    GNU LGPL v2.1  http://github.com/lbernstone/Tone32
 * GxEPD2           v1.3.8    GNU GPL  v3.0  https://github.com/ZinggJM/GxEPD2
 * Adafruit SHT31   v2.0.0    BSD            https://github.com/adafruit/Adafruit_SHT31
 * Adafruit GFX     v1.10.4   BSD            https://github.com/adafruit/Adafruit-GFX-Library

ESP32:                          3V3    GND
                                RST     23  SPI_MOSI  TF_MOSI   EPD_MOSI
                                VP      22  I2C_SCL             SHT31
                                VN      TX  TXD0                --
        VBATT    INPUT  ADC1/6  34      RX  RXD0                --
      LED_R/1  TFT_LED  ADC1/4  32      21  I2C_SDA             SHT31
      EPD_RST  TFT_RST  ADC1/5  33      19  SPI_MISO  TF_MISO   KY040_DT
      LED_G/2           ADC2/8  25      18  SPI_SCK   TF_SCK    EPD_SCK
      LED_B/3           ADC2/9  26       5  SPI_SS    LED0      KY040_SW
       EPD_DC   TFT_DC  ADC2/7  27      --                      --
       EPD_CS   TFT_CS  ADC2/6  14      --                      --
                 TS_CS  ADC2/5  12       4  TF_CS               S8_RXD
                        ADC2/4  13       0  ADC2/0  HIGH@Boot   KY040_CLK
                                EN       2  ADC2/2  DOWN@Flash  BUZZER/0
                                VBUS    15  ADC2/3              S8_TXD
                                VBAT   GND

                             ________________________
                            |   |° ° ° ° ° °|    |   |
                  +5V <- G+ |[X]| ° ° ° ° °/     |[ ]| DVCC_out
                  GND <- G0 |[X]|° ° ° ° °/      |[X]| UART_RxD
                   Alarm_OC |[ ]|_°_°_°_°|       |[X]| UART_TxD
                   PWM 1Khz |[ ]|                |[ ]| UART_R/T
                            |   |  SenseAir® S8  |[ ]| bCAL_in/CAL
                            |___|________________|___|

U = RI > R = U/I  Rr = (3.3-2.1)/0.020 = 60Ω  Rgb = (3.3-1.1)/0.020 = 110Ω

*/

#define WITH_BUZZER  // ESP32: installer https://github.com/lbernstone/Tone32
#define WITH_KY040
#define WITH_GxEPD2
#define WITH_GxEPD2_FONT
//#define WITH_SDCARD
#define WITH_EEPROM
#define VBATT

#define VERSION  F("v3.2 20220102")
#ifdef WITH_EEPROM
#define PREF_TAG "CO2v31"
#endif

//#define DEBUG
//#define DEBUG_VALUE
//#define WITH_GxEPD2_DRAW_FRAME
//#define ESP32_NOSLEEP
//#define ESP32_NOOPT
//#define LED_ALWAYS_ON
//#define WITHOUT_LED_TEST
//#define WITH_FAKE_CALIBRATION_OK
//#define WITH_FAKE_CALIBRATION_ERR

#ifdef VBATT
  /*
   * https://randomnerdtutorials.com/power-esp32-esp8266-solar-panels-battery-level-monitoring/
   * https://www.digikey.fr/fr/resources/conversion-calculators/conversion-calculator-voltage-divider
   * 
   * R1=2kΩ R2=6.8kΩ Vmax: 4.2V > 3.25V Coeff = (6800+2000)/6800
   * 
   */
  #define VBATT_PIN           34
  #define VBATT_MUL           ((6800.0 + 2000.0) / 6800.0)
  #define VBATT_HISTORY_SIZE  7
  #define VBATT_HISTORY_DIFF  5
#endif

/* 
 *  Temps de sommeil par défaut entre deux réveils pour flash de LED
 *  Temps de sommeil pour le "Flash time"
 */
#define SLEEP_DURATION_LOOP       8000L
#define SLEEP_DURATION_FLASH_TIME 2000L

/* Nombre de bloucles de sommeil entre chaque lecture du capteur S8 */
#define SLEEP_COUNT_S8_TRIGGER       1
#define SLEEP_COUNT_HISTORY_TRIGGER  8
#ifdef VBATT
#define SLEEP_COUNT_VBATT_TRIGGER    8
#endif

/*
 * Attente pre/post-calibration
 * 150 * 8 secondes > 20 minutes
 */
#define CALIBRATION_DELAY          150
//#define CALIBRATION_DELAY          5

/* Seuils d'éclairage des leds
 * Verte (OK sans masque), bleue (OK avec masque), Orange (aérez) et Rouge (évacuez!)
 */
#define DEFPREF_CO2_BLUE     600
#define DEFPREF_CO2_ORANGE   800
#define DEFPREF_CO2_RED     1000

/* Taille de l'historique pour affichage graphique en bas de l'écran */
#define CO2_HISTORY_SIZE  208  // Multiple de 4 !!!
#define EPD_HISTORY_H      74
#define EPD_HISTORY_Y0    121

/* Intervalle de valeurs pour l'affichage graphique:
 * les valeurs sont ramenées à la valeur min ou max si hors-échelle */
#define CO2_HISTORY_MIN  400
#define CO2_HISTORY_MAX  1200

/* Broches pour le capteur S8 */
#define S8_RXD              4
#define S8_TXD             15

/* Adresse i2c du capteur SHT31 */
#define SHT31_ADR 0x44

/* Broches pour les LEDs */
#define LED_PIN_R      32
#define LED_PIN_G      25
#define LED_PIN_B      26
#define LED_CHN_R       1
#define LED_CHN_G       2
#define LED_CHN_B       3
#define LED_PWM_FREQ 5000
#define LED_PWM_RES     8

#define LED_RGB_WHITE   0xffffff
#define LED_RGB_GREEN   0x007f00
#define LED_RGB_BLUE    0x0000ff
#define LED_RGB_ORANGE  0xbf3f00
#define LED_RGB_RED     0xff0000

/*Durée du flash LED (temps de sommeil) */
#define LED_NMB_FLASH           6
#define DELAY_LED_FLASH_ON    250
#define DELAY_LED_FLASH_OFF   120

/* 
 * Pin Buzzer
 */
#ifdef WITH_BUZZER
  #define PIN_BUZZER       2
  #define PIN_BUZZER_PWM   0
  #define DEFPREF_BUZZER   true
#endif


/*
 * Pin KY-040
 */
#ifdef WITH_KY040
  #define PIN_KY040_CLK     0
  #define PIN_KY040_DT     19
  #define PIN_KY040_SW      5
  /*
   * Durée anti-rebond
   */
  #define KY040_NOBOUNCE  250
#endif


/* Paramétrage de l'écran EPD
 *  https://forum.arduino.cc/index.php?topic=487007.msg4185170#msg4185170
 */


#ifdef WITH_GxEPD2
  #define EPD_W   250
  #define EPD_H   122
  #define EPD_FULL_REFRESH_DELAY 900000L
  #define EPD_CS   14
  #define EPD_SCK  18
  #define EPD_MOSI 23
  #define EPD_DC   27
  #define EPD_RST  33
  #define EPD_BUSY -1
#endif


#ifdef VBATT
  struct batteryCapacity {
    float voltage;
    int capacity;
  };

  // From: https://riton-duino.blogspot.com/2019/05/un-thermometre-hygrometre-mysensors-sur.html
  const batteryCapacity remainingCapacity[] = {
    4.20,   100,
    4.10,   96,
    4.00,   92,
    3.96,   89,
    3.92,   85,
    3.89,   81,
    3.86,   77,
    3.83,   73,
    3.80,   69,
    3.77,   65,
    3.75,   62,
    3.72,   58,
    3.70,   55,
    3.66,   51,
    3.62,   47,
    3.58,   43,
    3.55,   40,
    3.51,   35,
    3.48,   32,
    3.44,   26,
    3.40,   24,
    3.37,   20,
    3.35,   17,
    3.27,   13,
    3.20,   9,
    3.1,    6,
    3.00,   3,
    0.00,   0,
  };
  const int ncell = sizeof(remainingCapacity) / sizeof(struct batteryCapacity);
  byte vbatt_history[VBATT_HISTORY_SIZE];
  byte vbatt_history_sorted[VBATT_HISTORY_SIZE];
  byte vbatt_last = 0;
#endif


#ifdef WITH_KY040
volatile unsigned long ky040_pin_millis = 0;
volatile byte ky040_pin_current = LOW;
volatile byte ky040_pin_last = LOW;
volatile int8_t ky040_state_rotation = 0;
volatile boolean ky040_state_sw = false;
volatile boolean ky040_state_in_menu = false;

void IRAM_ATTR ky040_interrupt();
void IRAM_ATTR ky040_sw_isr();
#endif


/*
 * Paramétrage du buzzer
 *  nombre de notes multiple de 3 !
 */

#ifdef WITH_BUZZER
uint16_t buzzer_orange[] = {800, 850, 800, 850, 800, 850};
uint16_t buzzer_red[]    = {2500, 3000, 2500, 3000, 2500, 3000, 2500, 3000, 3000};
volatile boolean pref_buzzer = DEFPREF_BUZZER;
#include <Tone32.h>
#endif


/* FIN DES PARAMÈTRES */

#include <WiFi.h>
#include <BluetoothSerial.h>
#include "driver/adc.h"
#include <esp_bt.h>

BluetoothSerial SerialBT;

void setModemSleep();
void wakeModemSleep();

void disableWiFi(){
  adc_power_off();
  WiFi.disconnect(true);  // Disconnect from the network
  WiFi.mode(WIFI_OFF);    // Switch WiFi off
#ifdef DEBUG
  Serial.println("");
  Serial.println("WiFi disconnected!");
#endif
}
void disableBluetooth(){
  // Quite unusefully, no relevable power consumption
  btStop();
#ifdef DEBUG
  Serial.println("");
  Serial.println("Bluetooth stop!");
#endif
}
void setModemSleep() {
  /* 
   *  À priori inutile avec les versions récentes du SDK
   *  Si utilisé à voir pour ne pas couper l'ADC nécessaire à la mesure de niveau de batterie!
   */
//  disableWiFi();
//  disableBluetooth();
}


#ifdef WITH_GxEPD2
  #define ENABLE_GxEPD2_GFX 0
  #include <GxEPD2_BW.h>
  #define GxEPD2_DISPLAY_CLASS GxEPD2_BW
  #define GxEPD2_DRIVER_CLASS GxEPD2_213_B72
  #ifdef WITH_GxEPD2_FONT
    #include <Fonts/FreeMono9pt7b.h>
    #include <Fonts/FreeMono12pt7b.h>
    #include <Fonts/FreeMono18pt7b.h>
    #include <Fonts/FreeMono24pt7b.h>
    #include <Fonts/FreeMonoBold9pt7b.h>
    #include <Fonts/FreeMonoBold12pt7b.h>
    #include <Fonts/FreeMonoBold18pt7b.h>
    #include <Fonts/FreeMonoBold24pt7b.h>
    #define FONT1      &FreeMono9pt7b
    #define FONT1_TS   1
    #define FONT1B     &FreeMonoBold9pt7b
    #define FONT1B_TS  1
    #define FONT2      &FreeMono9pt7b
    #define FONT2_TS   1
    #define FONT2B     &FreeMonoBold9pt7b
    #define FONT2B_TS  1
    #define FONT3      &FreeMono12pt7b
    #define FONT3_TS   1
    #define FONT3B     &FreeMonoBold12pt7b
    #define FONT3B_TS  1
    #define FONT4      &FreeMono18pt7b
    #define FONT4_TS   1
    #define FONT4B     &FreeMonoBold18pt7b
    #define FONT4B_TS  1
    #define FONT5      &FreeMono24pt7b
    #define FONT5_TS   1
    #define FONT5B     &FreeMonoBold24pt7b
    #define FONT5B_TS  1
    #define FONT6      &FreeMono18pt7b
    #define FONT6_TS   2
    #define FONT6B      &FreeMonoBold18pt7b
    #define FONT6B_TS  2
    #define FONT7      &FreeMono24pt7b
    #define FONT7_TS   2
    #define FONT7B     &FreeMonoBold24pt7b
    #define FONT7B_TS  2
  #else
    #define FONT1      NULL
    #define FONT1_TS   1
    #define FONT1B     NULL
    #define FONT1B_TS  1
    #define FONT2      NULL
    #define FONT2_TS   2
    #define FONT2B     NULL
    #define FONT2B_TS  2
    #define FONT2      NULL
    #define FONT3_TS   3
    #define FONT3B     NULL
    #define FONT3B_TS  3
    #define FONT4      NULL
    #define FONT4_TS   4
    #define FONT4B     NULL
    #define FONT4B_TS  4
    #define FONT5      NULL
    #define FONT5_TS   5
    #define FONT5B     NULL
    #define FONT5B_TS  5
    #define FONT6      NULL
    #define FONT6_TS   6
    #define FONT6B     NULL
    #define FONT6B_TS  6
  #endif
  /*
   * À adapter suivant la plate-forme
   * mémoire très limitée sur l'Arduino PRO MINI
   */
  #define MAX_DISPLAY_BUFFER_SIZE 2048
  #define MAX_HEIGHT(EPD) (EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) ? EPD::HEIGHT : MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))
  GxEPD2_DISPLAY_CLASS<GxEPD2_DRIVER_CLASS, MAX_HEIGHT(GxEPD2_DRIVER_CLASS)> EPD(GxEPD2_DRIVER_CLASS(EPD_CS, EPD_DC, EPD_RST, EPD_BUSY));
  const char Fbuf_size = 24;
  char Fbuf[Fbuf_size+1];
  unsigned long gxepd2_last_refresh = 0;
  byte gxepd2_refresh = 0;
  #define GxEPD2_REFRESH_FULL      1
  #define GxEPD2_REFRESH_CO2       2
  #define GxEPD2_REFRESH_TEMP      4
  #define GxEPD2_REFRESH_HUMI      8
  #define GxEPD2_REFRESH_ICONS    16
  #define GxEPD2_REFRESH_HISTORY  32
  #define GxEPD2_REFRESH_LEGEND   64

  void EPD_message(const __FlashStringHelper* message, bool fullscreen, bool outline);

#endif

#ifdef WITH_SDCARD
#endif

#ifdef WITH_BUZZER
uint16_t * buzzer = NULL;
#endif

#ifdef WITH_EEPROM
#include <Preferences.h>
Preferences eeprom_pref;
boolean pref_changed = false;
#endif

/* Variables */
unsigned long date;
uint16_t co2 = 0;
#ifdef LOWMEM
uint8_t  yco2_history[CO2_HISTORY_SIZE*3/4];
#else
uint8_t  yco2_history[CO2_HISTORY_SIZE];
#endif
uint8_t sleep_count = SLEEP_COUNT_HISTORY_TRIGGER-1;

boolean _led_pwm = false;
uint32_t led = LED_RGB_WHITE;
uint8_t flash = 2;

byte calibration = 0; // 0: aucun, 1: attente pré-calibration 2: attente post-calibration
byte calibration_timer = 0;
byte calibration_percent = 0;

/* Fonctions d'arrondi */
#include <math.h>
#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
#endif
#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#endif

/* 
 * Capteur SHT31
 */

#include <Adafruit_SHT31.h>
Adafruit_SHT31 sht31 = Adafruit_SHT31();

bool sht31_enabled = false;
float sht31_t = 0;
float sht31_h = 0;


/* 
 * Capteur SenseAir S8
 * https://github.com/SFeli/ESP32_S8
 */

byte s8_Response[20];
byte s8_CO2req[]      = {0xFE, 0X04, 0X00, 0X03, 0X00, 0X01, 0XD5, 0XC5};
byte s8_read_abc[]    = {0xFE, 0X03, 0X00, 0X1F, 0X00, 0X01, 0XA1, 0XC3};
byte s8_read_co2[]    = {0xFE, 0X04, 0X00, 0X03, 0X00, 0X01, 0XD5, 0XC5};
byte s8_disable_abc[] = {0xFE, 0X06, 0X00, 0X1F, 0X00, 0X00, 0XAC, 0X03};
byte s8_clear_hr1[]   = {0xFE, 0X06, 0X00, 0X00, 0X00, 0X00, 0X9D, 0XC5};
byte s8_read_hr1[]    = {0xFE, 0X03, 0X00, 0X00, 0X00, 0X01, 0X90, 0X05};
byte s8_calibrate[]   = {0xFE, 0X06, 0X00, 0X01, 0X7C, 0X06, 0X6C, 0XC7};
byte s8_read_fw[]     = {0xFE, 0x04, 0x00, 0x1C, 0x00, 0x01, 0xE4, 0x03};    // FW      ->       334       1 / 78
byte s8_read_id_hi[]  = {0xFE, 0x04, 0x00, 0x1D, 0x00, 0x01, 0xB5, 0xC3};    // Sensor ID hi    1821       7 / 29
byte s8_read_id_lo[]  = {0xFE, 0x04, 0x00, 0x1E, 0x00, 0x01, 0x45, 0xC3};    // Sensor ID lo   49124     191 / 228 e.g. in Hex 071dbfe4

volatile uint16_t pref_co2_blue   = DEFPREF_CO2_BLUE;
volatile uint16_t pref_co2_orange = DEFPREF_CO2_ORANGE;
volatile uint16_t pref_co2_red    = DEFPREF_CO2_RED;

void flash_led_and_buzz(uint32_t led, uint8_t nmb, uint16_t *buzzer);


uint16_t s8_crc_02;
int s8_ASCII_WERT;
int s8_int01, s8_int02, s8_int03;
unsigned long s8_ReadCRC;      // CRC Control Return Code
char this_s8_id[9];
char this_s8_fw[5];
char this_s8_abc[6];

#define s8serial Serial1

#define _serial_printf Serial.printf

unsigned short int s8_ModBus_CRC(unsigned char * buf, int len) {
  unsigned short int crc = 0xFFFF;
  for (int pos = 0; pos < len; pos++) {
    crc ^= (unsigned short int)buf[pos];   // XOR byte into least sig. byte of crc
    for (int i = 8; i != 0; i--) {         // Loop over each bit
      if ((crc & 0x0001) != 0) {           // If the LSB is set
        crc >>= 1;                         // Shift right and XOR 0xA001
        crc ^= 0xA001;
      } else {                            // else LSB is not set
        crc >>= 1;                    // Just shift right
      }
    }
  }  // Note, this number has low and high bytes swapped, so use it accordingly (or swap bytes)
  return crc;  
}

void s8_send_request (byte * Request, int Re_len) {
//#ifdef DEBUG
//Serial.println("> s8_send_request");
//#endif
  while (!s8serial.available()) {

//Serial.println("> -");
    s8serial.write(Request, Re_len);   // Send request to S8-Sensor
    delay(50);
  }
//Serial.println("< s8_send_request");
}

void s8_read_response (int RS_len) {
//Serial.println("> s8_read_response");
  s8_int01 = 0;
  while (s8serial.available() < 7 ) {
//Serial.println("> -");
    s8_int01++;
    if (s8_int01 > 10) {
      while (s8serial.available()) {
        s8serial.read();
      }
      break;
    }
    delay(50);
  }
  for (s8_int02 = 0; s8_int02 < RS_len; s8_int02++) {
    // Empfangsbytes
    s8_Response[s8_int02] = s8serial.read();
  }
//Serial.println("< s8_read_response");
}


unsigned long s8_get_Value(int RS_len)
/* Extract the Value from the response (byte 3 and 4)  
    and check the CRC if the response is compleat and correct */
{
  /* Loop only for test and developtment purpose */
//  for (int i=0; i<RS_len-2; i++)
//  {
//    int03 = s8_Response[i];
//    _serial_printf("Wert[%i] : %i / ", i, int03);
//  }

// Check the CRC //
  s8_ReadCRC = (uint16_t)s8_Response[RS_len-1] * 256 + (uint16_t)s8_Response[RS_len-2];
  if (s8_ModBus_CRC(s8_Response, RS_len-2) == s8_ReadCRC) {
    // Read the Value //
    unsigned long val = (uint16_t)s8_Response[3] * 256 + (uint16_t)s8_Response[4];
    return val * 1;       // S8 = 1. K-30 3% = 3, K-33 ICB = 10
  } else {
#ifdef DEBUG
    Serial.print("Error");
#endif
    return 99;
  }
}

int16_t s8_getValue(byte *req, uint8_t req_len, uint8_t res_len) {
  s8_send_request(req, req_len);
  s8_read_response(res_len);
  return s8_get_Value(res_len);
}

void s8_init() {

  uint32_t s8res;

  s8serial.begin(9600, SERIAL_8N1, S8_RXD, S8_TXD);

// 12:30:37.706 -> S8  id: 073e575b
// 12:51:51.300 -> S8  fw: 0192
// 12:51:51.334 -> S8 ABC: 0

  s8res = s8_getValue(s8_read_id_hi, 8, 7);
  sprintf(&this_s8_id[0], "%02x%02x", (s8res>>8) & 0xFF, s8res&0xFF);
  s8res = s8_getValue(s8_read_id_lo, 8, 7);
  sprintf(&this_s8_id[4], "%02x%02x", (s8res>>8) & 0xFF, s8res&0xFF);
  s8res = s8_getValue(s8_read_fw, 8, 7);
  sprintf(&this_s8_fw[0], "%02d%02d", (s8res>>8) & 0xFF, s8res&0xFF);
  int16_t abc = s8_getValue(s8_read_abc, 8, 7);
  sprintf(&this_s8_abc[0], "%05d", s8_getValue(s8_read_abc, 8, 7));

  if (abc > 0) {
    EPD_message(F("S8 set ABC=0"), false, false);
    delay(5000);
    abc = s8_getValue(s8_disable_abc, 8, 7);
    sprintf(&this_s8_abc[0], "%05d", s8_getValue(s8_read_abc, 8, 7));
  }

  if (abc > 0) {
    EPD_message(F("S8 ABC=0 KO"), false, false);
    delay(10000);
  }

#ifdef DEBUG
  Serial.print("S8  id: ");
  Serial.println(this_s8_id);
  Serial.print("S8  fw: ");
  Serial.println(this_s8_fw);
  Serial.print("S8 ABC: ");
  Serial.println(this_s8_abc);
#endif
}



/* 
 * Capteur SHT31
 */


bool sht31_enable() {
    if (sht31.begin(SHT31_ADR)) {
      //_serial_printf("SHT31 found (0x%x)\n", SHT31_ADR);
      sht31_enabled = true;
      return true;
    }
    return false;
}

#ifdef ESP32_NOSLEEP
  void dodo(const long int ms) {
    delay(ms);
  }
#else
  void dodo(const long int ms) {

    // GPIO du bouton switch KY040 (TODO: define ?)
    gpio_wakeup_enable(GPIO_NUM_5, GPIO_INTR_LOW_LEVEL);
    esp_sleep_enable_gpio_wakeup();
    esp_sleep_enable_timer_wakeup(ms * 1000L);
    esp_light_sleep_start();
#ifdef DEBUG
    esp_sleep_wakeup_cause_t wakeup_reason;
    wakeup_reason = esp_sleep_get_wakeup_cause();
    switch(wakeup_reason){
      case ESP_SLEEP_WAKEUP_EXT0 : Serial.println("Wakeup caused by external signal using RTC_IO"); break;
      case ESP_SLEEP_WAKEUP_EXT1 : Serial.println("Wakeup caused by external signal using RTC_CNTL"); break;
      case ESP_SLEEP_WAKEUP_TIMER : Serial.println("Wakeup caused by timer"); break;
      case ESP_SLEEP_WAKEUP_TOUCHPAD : Serial.println("Wakeup caused by touchpad"); break;
      case ESP_SLEEP_WAKEUP_ULP : Serial.println("Wakeup caused by ULP program"); break;
      default : Serial.printf("Wakeup was not caused by deep sleep: %d\n",wakeup_reason); break;
    }
#endif
  }
#endif


#ifdef LOWMEM

/*
 * 64 valeurs > 6 bits
   24 bits -> 3 chars (8 bits)

    char 8 bits:  AAAAAAAABBBBBBBBCCCCCCCC DDDDDDDDEEEEEEEEFFFFFFFF
    values:       000000111111222222333333 444444555555666666777777
    index_lo      0     1     2     3      0     1     2     3
 */

void history_set(uint8_t *h, const uint8_t index, const uint8_t value) {

  // Indexes:
  uint8_t  index_hi = 3 * (index / 4);
  uint8_t  index_lo = index % 4;

  // Mask:
  // Décalage 6 bits
  uint32_t mask = (uint32_t)0x3f << (index_lo*6);

  // Valeur actuelle des 3 chars:
  uint32_t current = ((uint32_t)h[index_hi])<<16 | ((uint32_t)h[index_hi+1])<<8 | ((uint32_t)h[index_hi+2]);

  // Efface la valeur actuelle:
  current = current & (~mask);

  // Calcule la nouvelle valeur:
  if (index_lo == 0) {
    current = current | ((uint32_t)value);
  } else {
    current = current | ((uint32_t)value <<(index_lo*6));
  }

  // Insère la nouvelle valeur:
  h[index_hi]   = (uint8_t)((current>>16) & 0xff);
  h[index_hi+1] = (uint8_t)((current>>8)  & 0xff);
  h[index_hi+2] = (uint8_t)( current      & 0xff);

  //current = ((uint32_t)h[index_hi])<<16 | ((uint32_t)h[index_hi+1])<<8 | ((uint32_t)h[index_hi+2]);
}
#endif

uint8_t history_get(uint8_t *h, const uint8_t index) {

  uint8_t value = 0;

#ifdef LOWMEM
  char str[256];

  // Indexes:
  uint8_t  index_hi = 3 * (index / 4);
  uint8_t  index_lo = index % 4;

  // Mask:
  // Décalage 6 bits
  uint32_t mask = (uint32_t)0x3f << (index_lo*6);

  // Valeur actuelle des 3 chars:
  uint32_t current = ((uint32_t)h[index_hi])<<16 | ((uint32_t)h[index_hi+1])<<8 | ((uint32_t)h[index_hi+2]);

  // Valeur actuelle des 3 chars isolée:
  current = current & mask;

  // Calcule la nouvelle valeur:
  if (index_lo == 0) {
    value = (uint8_t)(current);
  } else {
    value = (uint8_t)(current >>(index_lo*6));
  }
#else
  value = h[index];
#endif

  return value;
}

void history_push(uint8_t *h, uint8_t size, uint8_t value) {

  for (uint8_t i = 0; i<size-1; i++) {
#ifdef LOWMEM
    history_set(h, i, history_get(h, i+1));
#else
    h[i] = h[i+1];
#endif
  }
#ifdef LOWMEM
  history_set(h, size-1, value);
#else
    h[size-1] = value;
#endif
}


#ifdef WITH_GxEPD2

#define EPD_DISP_VER_X                   0
#define EPD_DISP_VER_Y                 106
#define EPD_DISP_VER_W                 250
#define EPD_DISP_VER_H                  16
#define EPD_DISP_VER_FONT            FONT2B
#define EPD_DISP_VER_TS              FONT2B_TS
#define EPD_DISP_MSG_X                   0
#define EPD_DISP_MSG_Y                  48
#define EPD_DISP_MSG_W                 250
#define EPD_DISP_MSG_H                  24
#define EPD_DISP_MSG_FONT            FONT3B
#define EPD_DISP_MSG_TS              FONT3B_TS

  #define EPD_DISP_CO2_X                48
  #define EPD_DISP_CO2_Y                 0
  #define EPD_DISP_CO2_W               160
  #define EPD_DISP_CO2_H                48
  #define EPD_DISP_CO2_FONT          FONT6B
  #define EPD_DISP_CO2_TS            FONT6B_TS
  #define EPD_DISP_CO2_S_TS          FONT5B_TS
  #define EPD_DISP_CO2_S_FONT        FONT5B
  #define EPD_DISP_CO2_CAL_X            48
  #define EPD_DISP_CO2_CAL_Y            32
  #define EPD_DISP_CO2_CAL_W           160
  #define EPD_DISP_CO2_CAL_H            16
  #define EPD_DISP_CO2_CAL_FONT      FONT2B
  #define EPD_DISP_CO2_CAL_TS        FONT2B_TS

#define EPD_DISP_TEMP_X                  0
#define EPD_DISP_TEMP_Y                  0
#define EPD_DISP_TEMP_W                 48
#define EPD_DISP_TEMP_H                 24
#define EPD_DISP_TEMP_BIG_FONT       FONT3B
#define EPD_DISP_TEMP_BIG_TS         FONT3B_TS
#define EPD_DISP_TEMP_SMALL_FONT     FONT2B
#define EPD_DISP_TEMP_SMALL_TS       FONT2B_TS
#define EPD_DISP_HUMI_X                  0
#define EPD_DISP_HUMI_Y                 24
#define EPD_DISP_HUMI_W                 48
#define EPD_DISP_HUMI_H                 24
#define EPD_DISP_HUMI_BIG_FONT       FONT3B
#define EPD_DISP_HUMI_BIG_TS         FONT3B_TS
#define EPD_DISP_HUMI_SMALL_FONT     FONT2B
#define EPD_DISP_HUMI_SMALL_TS       FONT2B_TS

#define EPD_DISP_ICONS_X               208
#define EPD_DISP_ICONS_Y                 0
#define EPD_DISP_ICONS_W                42
#define EPD_DISP_ICONS_H                64

#define EPD_DISP_ICON_BATT_X           208
#define EPD_DISP_ICON_BATT_Y             0
#define EPD_DISP_ICON_BATT_W            42
#define EPD_DISP_ICON_BATT_H            18
#define EPD_DISP_ICON_BATT_FONT      FONT1B
#define EPD_DISP_ICON_BATT_TS        FONT1B_TS
#define EPD_DISP_ICON_BATT_XX          244
#define EPD_DISP_ICON_BATT_YY            5
#define EPD_DISP_ICON_BATT_WW            6
#define EPD_DISP_ICON_BATT_HH            6
#define EPD_DISP_LEGEND_BUZ_X          208
#define EPD_DISP_LEGEND_BUZ_Y           20
#define EPD_DISP_LEGEND_BUZ_W           42
#define EPD_DISP_LEGEND_BUZ_H           28

#define EPD_DISP_HISTORY_X               0
#define EPD_DISP_HISTORY_Y              48
#define EPD_DISP_HISTORY_W             208
#define EPD_DISP_HISTORY_H              74
#define EPD_DISP_LEGEND_X              208
#define EPD_DISP_LEGEND_Y               48
#define EPD_DISP_LEGEND_W               42
#define EPD_DISP_LEGEND_H               74
#define EPD_DISP_LEGEND_FONT         FONT1B
#define EPD_DISP_LEGEND_TS           FONT1B_TS
#define EPD_DISP_LEGEND_FONT_H          12


  void EPD_calc_text(const char * buf, int16_t x, int16_t y, uint16_t w, uint16_t h, int16_t* xx, int16_t* yy, uint16_t* ww, uint16_t* hh, int8_t jx, int8_t jy) {

    int16_t  x1, y1;
    uint16_t w1, h1;

#ifdef DEBUG
    Serial.print("y="); Serial.println(y);
    Serial.print("jy="); Serial.println(jy);
    Serial.print("h="); Serial.println(h);
#endif

    EPD.getTextBounds(buf, 0, 0, &x1, &y1, &w1, &h1);
#ifdef DEBUG
    Serial.print("h1="); Serial.println(h1);
    Serial.print("y1="); Serial.println(y1);
#endif

    if (xx != NULL) {
      *xx = x - x1;
      if (jx == 0) {
        *xx += (w-w1) /2;
      } else if (jx > 0) {
        *xx += (w-w1);
      }
    }

    if (yy != NULL) {
      *yy = y - y1;
#ifdef DEBUG
      Serial.print("yy="); Serial.println(*yy);
#endif
      if (jy == 0) {
        *yy += (h-h1) /2;
#ifdef DEBUG
      Serial.print("yy="); Serial.println(*yy);
#endif
      } else if (jy > 0) {
        *yy += (h-h1);
#ifdef DEBUG
      Serial.print("yy="); Serial.println(*yy);
#endif
      }
    }

    if (ww != NULL) {
      *ww = w1;
    }

    if (hh != NULL) {
      *hh = h1;
    }
  }

  void EPD_init() {
#ifdef DEBUG
    Serial.println(F(">EPD_init"));
#endif

    gxepd2_last_refresh = millis();
    EPD.setFullWindow();
    EPD.firstPage();
    do {
      EPD.fillRect(0, 0, EPD_W, EPD_H, GxEPD_WHITE);
    } while (EPD.nextPage());

#ifdef DEBUG
    Serial.println(F("<EPD_init"));
#endif
  }

  void EPD_version(const __FlashStringHelper* message) {
    int16_t  x1, y1;
    uint16_t w1, h1;

    if (message != NULL) {
#ifdef DEBUG
    Serial.println(F(">EPD_version"));
#endif

#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_VER_FONT);
#endif
      EPD.setTextSize(EPD_DISP_VER_TS);
      EPD.setTextColor(GxEPD_BLACK);

      strncpy_P(Fbuf, (PGM_P)message, Fbuf_size);
      EPD_calc_text(Fbuf, EPD_DISP_VER_X, EPD_DISP_VER_Y, EPD_DISP_VER_W, EPD_DISP_VER_H, &x1, &y1, &w1, &h1, 0, 0);
    }

    EPD.setPartialWindow(EPD_DISP_VER_X, EPD_DISP_VER_Y, EPD_DISP_VER_W, EPD_DISP_VER_H);
    EPD.firstPage();
    do {
      EPD.fillRect(EPD_DISP_VER_X, EPD_DISP_VER_Y, EPD_DISP_VER_W, EPD_DISP_VER_H, GxEPD_WHITE);
      if (message != NULL) {
        EPD.setCursor(x1, y1);
        EPD.print(Fbuf);
      }
      //EPD.drawRect(EPD_DISP_VER_X, EPD_DISP_VER_Y, EPD_DISP_VER_W, EPD_DISP_VER_H, GxEPD_BLACK);
    } while (EPD.nextPage());

    EPD.hibernate();

#ifdef DEBUG
    Serial.println(F("<EPD_version"));
#endif
  }

  void EPD_message(const __FlashStringHelper* message, bool fullscreen=false, bool outline=false) {
    int16_t  x1, y1;
    uint16_t w1, h1;

    if (message != NULL) {
#ifdef DEBUG
    Serial.println(F(">EPD_message"));
#endif

#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_MSG_FONT);
#endif
      EPD.setTextSize(EPD_DISP_MSG_TS);
      EPD.setTextColor(GxEPD_BLACK);

      strncpy_P(Fbuf, (PGM_P)message, Fbuf_size);
      EPD_calc_text(Fbuf, EPD_DISP_MSG_X, EPD_DISP_MSG_Y, EPD_DISP_MSG_W, EPD_DISP_MSG_H, &x1, &y1, &w1, &h1, 0, 0);
    }

    if (fullscreen) {
      EPD.setFullWindow();
    } else {
      EPD.setPartialWindow(EPD_DISP_MSG_X, EPD_DISP_MSG_Y, EPD_DISP_MSG_W, EPD_DISP_MSG_H);
    }

    if (fullscreen) {
      EPD.clearScreen();
    }
    EPD.firstPage();
    do {
      if (!fullscreen) {
        EPD.fillRect(EPD_DISP_MSG_X, EPD_DISP_MSG_Y, EPD_DISP_MSG_W, EPD_DISP_MSG_H, GxEPD_WHITE);
      }
      if (message != NULL) {
        EPD.setCursor(x1, y1);
        EPD.print(Fbuf);
      }
      if (outline) {
        EPD.drawRect(EPD_DISP_MSG_X, EPD_DISP_MSG_Y, EPD_DISP_MSG_W, EPD_DISP_MSG_H, GxEPD_BLACK);
      }
    } while (EPD.nextPage());

    EPD.hibernate();

#ifdef DEBUG
    Serial.println(F("<EPD_message"));
#endif
}

void GxEPD2_refresh() {

  int16_t  rx=0, ry=0, rw=0, rh=0;
  int16_t  x1, y1, x2, y2, x3, y3, x4, y4;
  uint16_t w1, h1, w2, h2, w3, h3, w4, h4;
  uint8_t  ly0, ly1, ly2;
  uint8_t  lly0, lly1, lly2;
  uint8_t t_ent = 0;
  uint8_t t_dec = 0;

  if (gxepd2_refresh & GxEPD2_REFRESH_FULL) {
    EPD.setFullWindow();
  } else {
    /*
     * Calcul de la zone à afficher:
     * découpage en 2 parties:
     *  - haute: CO2, Température, Humidité et icônes
     *  - basse: Historique et légende
     */
    if (gxepd2_refresh & GxEPD2_REFRESH_CO2 || gxepd2_refresh & GxEPD2_REFRESH_TEMP || gxepd2_refresh & GxEPD2_REFRESH_HUMI || gxepd2_refresh & GxEPD2_REFRESH_ICONS) {
      rw = EPD_W;
      rh = EPD_DISP_CO2_H;
      gxepd2_refresh |= (GxEPD2_REFRESH_CO2|GxEPD2_REFRESH_TEMP|GxEPD2_REFRESH_HUMI|GxEPD2_REFRESH_ICONS);
      if (gxepd2_refresh & GxEPD2_REFRESH_HISTORY || gxepd2_refresh & GxEPD2_REFRESH_LEGEND) {
        rh = EPD_H;
        gxepd2_refresh |= (GxEPD2_REFRESH_HISTORY|GxEPD2_REFRESH_LEGEND);
      }
    } else if (gxepd2_refresh & GxEPD2_REFRESH_HISTORY) {
      //rx = 0;
      ry = EPD_DISP_HISTORY_Y;
      rw = EPD_W;
      rh = EPD_DISP_HISTORY_H;
      gxepd2_refresh |= (GxEPD2_REFRESH_HISTORY|GxEPD2_REFRESH_LEGEND);
    } else {
      rx = EPD_DISP_LEGEND_X;
      ry = EPD_DISP_HISTORY_Y;
      rw = EPD_DISP_LEGEND_W;
      rh = EPD_DISP_LEGEND_H;
    }

    EPD.setPartialWindow(rx, ry, rw, rh);
  }

  EPD.setTextColor(GxEPD_BLACK);

  EPD.firstPage();
  do {

    if (gxepd2_refresh & GxEPD2_REFRESH_CO2 || gxepd2_refresh & GxEPD2_REFRESH_FULL) {

      sprintf(Fbuf, "%d", co2);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(calibration ? EPD_DISP_CO2_S_FONT : EPD_DISP_CO2_FONT);
#endif
      EPD.setTextSize(calibration ? EPD_DISP_CO2_S_TS : EPD_DISP_CO2_TS);
      EPD_calc_text(Fbuf, EPD_DISP_CO2_X, EPD_DISP_CO2_Y, EPD_DISP_CO2_W, calibration ? EPD_DISP_CO2_H-EPD_DISP_CO2_CAL_H : EPD_DISP_CO2_H, &x1, &y1, &w1, &h1, 0, 0);
      EPD.fillRect(EPD_DISP_CO2_X, EPD_DISP_CO2_Y, EPD_DISP_CO2_W, calibration ? EPD_DISP_CO2_H-EPD_DISP_CO2_CAL_H : EPD_DISP_CO2_H, GxEPD_WHITE);
#ifdef WITH_GxEPD2_DRAW_FRAME
      EPD.drawRect(EPD_DISP_CO2_X, EPD_DISP_CO2_Y, EPD_DISP_CO2_W, calibration ? EPD_DISP_CO2_H-EPD_DISP_CO2_CAL_H : EPD_DISP_CO2_H, GxEPD_BLACK);
#endif
      EPD.setCursor(x1, y1);
      EPD.print(Fbuf);

      if (calibration) {
        EPD.drawRect(EPD_DISP_CO2_CAL_X, EPD_DISP_CO2_CAL_Y, EPD_DISP_CO2_CAL_W, EPD_DISP_CO2_CAL_H, GxEPD_BLACK);
        byte curseur = calibration_percent ? min(EPD_DISP_CO2_CAL_W-4, max(0, (EPD_DISP_CO2_CAL_W-4)*calibration_percent/100)) : 0;
        EPD.drawRect(EPD_DISP_CO2_CAL_X+1, EPD_DISP_CO2_CAL_Y+1, EPD_DISP_CO2_CAL_W-2, EPD_DISP_CO2_CAL_H-2, GxEPD_WHITE);
        if (curseur > 0) {
          EPD.fillRect(EPD_DISP_CO2_CAL_X+2, EPD_DISP_CO2_CAL_Y+2, curseur, EPD_DISP_CO2_CAL_H-4, GxEPD_BLACK);
        }
        if (curseur < EPD_DISP_CO2_CAL_W-4) {
          EPD.fillRect(EPD_DISP_CO2_CAL_X+2+curseur, EPD_DISP_CO2_CAL_Y+2, EPD_DISP_CO2_CAL_W-4-curseur, EPD_DISP_CO2_CAL_H-4, GxEPD_WHITE);
        }

        if (calibration == 3) {
          sprintf(Fbuf, "OK");
        } else if (calibration == 4) {
          sprintf(Fbuf, "CALIB: OK");
        } else if (calibration == 5) {
          sprintf(Fbuf, "ERR");
        } else if (calibration == 6) {
          sprintf(Fbuf, "CALIB: ERR");
        } else {
          sprintf(Fbuf, "?");
        }

#ifdef WITH_GxEPD2_FONT
        EPD.setFont(EPD_DISP_CO2_CAL_FONT);
#endif
        EPD.setTextSize(EPD_DISP_CO2_CAL_TS);
        if (calibration == 3 || calibration == 5) {
          EPD.setTextColor(GxEPD_WHITE);
          EPD_calc_text(Fbuf, EPD_DISP_CO2_CAL_X+1, EPD_DISP_CO2_CAL_Y+1, curseur/*(EPD_DISP_CO2_CAL_W-2)/2*/, EPD_DISP_CO2_CAL_H-2, &x1, &y1, NULL, NULL, 0, 0);
          EPD.setCursor(x1, y1);
          EPD.print(Fbuf);
          EPD.setTextColor(GxEPD_BLACK);
        } else if (calibration == 4 || calibration == 6) {
          EPD.setTextColor(GxEPD_WHITE);
          EPD_calc_text(Fbuf, EPD_DISP_CO2_CAL_X+1, EPD_DISP_CO2_CAL_Y+1, EPD_DISP_CO2_CAL_W-2, EPD_DISP_CO2_CAL_H-2, &x1, &y1, NULL, NULL, 0, 0);
          EPD.setCursor(x1, y1);
          EPD.print(Fbuf);
          EPD.setTextColor(GxEPD_BLACK);
        }
      }
    }

     if (gxepd2_refresh & GxEPD2_REFRESH_TEMP || gxepd2_refresh & GxEPD2_REFRESH_FULL) {
      t_ent = floor(abs(sht31_t));
      t_dec = (abs(sht31_t) - floor(abs(sht31_t))) * 10;

      if (sht31_t < 0) {
        sprintf(Fbuf, "-");
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_TEMP_SMALL_FONT);
#endif
        EPD.setTextSize(EPD_DISP_TEMP_SMALL_TS);
        EPD_calc_text(Fbuf, 0, 0, EPD_DISP_TEMP_W, EPD_DISP_TEMP_H, NULL, NULL, &w1, &h1, 0, 0);
      } else {
        w1 = 0;
        h1 = 0;
      }

      sprintf(Fbuf, "%d", t_ent);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_TEMP_BIG_FONT);
#endif
      EPD.setTextSize(EPD_DISP_TEMP_BIG_TS);
      EPD_calc_text(Fbuf, 0, 0, EPD_DISP_TEMP_W, EPD_DISP_TEMP_H, NULL, NULL, &w2, &h2, 0, 0);
      sprintf(Fbuf, "%d", t_dec);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_TEMP_SMALL_FONT);
#endif
      EPD.setTextSize(EPD_DISP_TEMP_SMALL_TS);
      EPD_calc_text(Fbuf, 0, 0, EPD_DISP_TEMP_W, EPD_DISP_TEMP_H, NULL, NULL, &w3, &h3, 0, 0);
      // calcul largeur totale:
      w4 = w1 + 2 + w2 + 2 + w3;
      EPD.fillRect(EPD_DISP_TEMP_X, EPD_DISP_TEMP_Y, EPD_DISP_TEMP_W, EPD_DISP_TEMP_H, GxEPD_WHITE);
#ifdef WITH_GxEPD2_DRAW_FRAME
      EPD.drawRect(EPD_DISP_TEMP_X, EPD_DISP_TEMP_Y, EPD_DISP_TEMP_W, EPD_DISP_TEMP_H, GxEPD_BLACK);
#endif

      if (sht31_t < 0) {
        sprintf(Fbuf, "-");
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_TEMP_SMALL_FONT);
#endif
        EPD.setTextSize(EPD_DISP_TEMP_SMALL_TS);
        EPD_calc_text(Fbuf, EPD_DISP_TEMP_X +(EPD_DISP_TEMP_W - w4) / 2 - 1, EPD_DISP_TEMP_Y, w4, EPD_DISP_TEMP_H, &x4, &y4, NULL, NULL, -1, 0);
        EPD.setCursor(x4, y4);
        EPD.print(Fbuf);
        w1 += 2;
      }

      sprintf(Fbuf, "%d", t_ent);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_TEMP_BIG_FONT);
#endif
      EPD.setTextSize(EPD_DISP_TEMP_BIG_TS);
      EPD_calc_text(Fbuf, EPD_DISP_TEMP_X +(EPD_DISP_TEMP_W - w4) / 2 - 1 + w1 + 2, EPD_DISP_TEMP_Y, w4, EPD_DISP_TEMP_H, &x4, &y4, NULL, NULL, -1, 0);

      EPD.setCursor(x4, y4+2);
      EPD.print(Fbuf);

      sprintf(Fbuf, "%d", t_dec);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_TEMP_SMALL_FONT);
#endif
      EPD.setTextSize(EPD_DISP_TEMP_SMALL_TS);
      EPD_calc_text(Fbuf, EPD_DISP_TEMP_X +(EPD_DISP_TEMP_W - w4) / 2 - 1 + w1 + 2 + w2 + 2, EPD_DISP_TEMP_Y, w4, EPD_DISP_TEMP_H, &x4, &y4, NULL, NULL, -1, 0);
      EPD.setCursor(x4, y4);
      EPD.print(Fbuf);
    }

    if (gxepd2_refresh & GxEPD2_REFRESH_HUMI || gxepd2_refresh & GxEPD2_REFRESH_FULL) {
      EPD.fillRect(EPD_DISP_HUMI_X, EPD_DISP_HUMI_Y, EPD_DISP_HUMI_W, EPD_DISP_HUMI_H, GxEPD_WHITE);
#ifdef WITH_GxEPD2_DRAW_FRAME
      EPD.drawRect(EPD_DISP_HUMI_X, EPD_DISP_HUMI_Y, EPD_DISP_HUMI_W, EPD_DISP_HUMI_H, GxEPD_BLACK);
#endif

      sprintf(Fbuf, "%d", int(round(sht31_h)));
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_HUMI_BIG_FONT);
#endif
      EPD.setTextSize(EPD_DISP_HUMI_BIG_TS);
      EPD_calc_text(Fbuf, 0, 0, EPD_DISP_HUMI_W, EPD_DISP_HUMI_H, NULL, NULL, &w1, &h1, 0, 0);
      sprintf(Fbuf, "%%");
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_HUMI_SMALL_FONT);
#endif
      EPD.setTextSize(EPD_DISP_HUMI_SMALL_TS);
      EPD_calc_text(Fbuf, 0, 0, EPD_DISP_HUMI_W, EPD_DISP_HUMI_H, NULL, NULL, &w2, &h2, 0, 0);
      // calcul largeur totale:
      w3 = w1 + 2 + w2;

      sprintf(Fbuf, "%d", int(round(sht31_h)));
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_HUMI_BIG_FONT);
#endif
      EPD.setTextSize(EPD_DISP_HUMI_BIG_TS);
      EPD_calc_text(Fbuf, EPD_DISP_HUMI_X +(EPD_DISP_HUMI_W - w3) / 2 - 1, EPD_DISP_HUMI_Y, w3, EPD_DISP_HUMI_H, &x3, &y3, NULL, NULL, -1, 0);
      EPD.setCursor(x3, y3);
      EPD.print(Fbuf);
      sprintf(Fbuf, "%%", sht31_h);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_HUMI_SMALL_FONT);
#endif
      EPD.setTextSize(EPD_DISP_HUMI_SMALL_TS);
      EPD_calc_text(Fbuf, EPD_DISP_HUMI_X +(EPD_DISP_HUMI_W - w3) / 2 - 1 + w1 + 2, EPD_DISP_HUMI_Y, w3, EPD_DISP_HUMI_H, &x3, &y3, NULL, NULL, -1, 0);
      EPD.setCursor(x3, y3);
      EPD.print(Fbuf);
    }

    if (gxepd2_refresh & GxEPD2_REFRESH_ICONS || gxepd2_refresh & GxEPD2_REFRESH_FULL) {

      // Icône batterie
      EPD.drawLine(EPD_DISP_ICON_BATT_X, EPD_DISP_ICON_BATT_Y, EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_Y, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_X, EPD_DISP_ICON_BATT_Y+1, EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_Y+1, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_Y, EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_YY, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_XX+1, EPD_DISP_ICON_BATT_Y, EPD_DISP_ICON_BATT_XX+1, EPD_DISP_ICON_BATT_YY, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_YY, EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-1, EPD_DISP_ICON_BATT_YY, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_YY+1, EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-1, EPD_DISP_ICON_BATT_YY+1, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-1, EPD_DISP_ICON_BATT_YY, EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-1, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-1, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-2, EPD_DISP_ICON_BATT_YY, EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-2, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-1, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-1, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-1, EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-1, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_XX+EPD_DISP_ICON_BATT_WW-1, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-2, EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-2, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-1, EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-1, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_XX+1, EPD_DISP_ICON_BATT_YY+EPD_DISP_ICON_BATT_HH-1, EPD_DISP_ICON_BATT_XX+1, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-1, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-2, EPD_DISP_ICON_BATT_X, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-2, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_XX, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-1, EPD_DISP_ICON_BATT_X, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-1, GxEPD_BLACK);

      EPD.drawLine(EPD_DISP_ICON_BATT_X, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-1, EPD_DISP_ICON_BATT_X, EPD_DISP_ICON_BATT_Y, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_ICON_BATT_X+1, EPD_DISP_ICON_BATT_Y+EPD_DISP_ICON_BATT_H-1, EPD_DISP_ICON_BATT_X+1, EPD_DISP_ICON_BATT_Y, GxEPD_BLACK);

      sprintf(Fbuf, "%d", vbatt_last);
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_ICON_BATT_FONT);
#endif
      EPD.setTextSize(EPD_DISP_ICON_BATT_TS);
      EPD_calc_text(Fbuf, EPD_DISP_ICON_BATT_X+1, EPD_DISP_ICON_BATT_Y+1, EPD_DISP_ICON_BATT_W-EPD_DISP_ICON_BATT_WW-2, EPD_DISP_ICON_BATT_H-2, &x3, &y3, NULL, NULL, 0, 0);
      EPD.setCursor(x3, y3);
      EPD.print(Fbuf);

#ifdef WITH_BUZZER
      // Icône buzzer:
      EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+4, EPD_DISP_LEGEND_BUZ_Y+11, EPD_DISP_LEGEND_BUZ_X+4, EPD_DISP_LEGEND_BUZ_Y+16, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+4, EPD_DISP_LEGEND_BUZ_Y+11, EPD_DISP_LEGEND_BUZ_X+17, EPD_DISP_LEGEND_BUZ_Y+11, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+4, EPD_DISP_LEGEND_BUZ_Y+16, EPD_DISP_LEGEND_BUZ_X+17, EPD_DISP_LEGEND_BUZ_Y+16, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+17, EPD_DISP_LEGEND_BUZ_Y+11, EPD_DISP_LEGEND_BUZ_X+35, EPD_DISP_LEGEND_BUZ_Y+2, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+17, EPD_DISP_LEGEND_BUZ_Y+16, EPD_DISP_LEGEND_BUZ_X+35, EPD_DISP_LEGEND_BUZ_Y+25, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+35, EPD_DISP_LEGEND_BUZ_Y+2, EPD_DISP_LEGEND_BUZ_X+35, EPD_DISP_LEGEND_BUZ_Y+25, GxEPD_BLACK);

      if (!pref_buzzer) {
        EPD.drawLine(EPD_DISP_LEGEND_BUZ_X, EPD_DISP_LEGEND_BUZ_Y, EPD_DISP_LEGEND_BUZ_X+EPD_DISP_LEGEND_BUZ_W-1, EPD_DISP_LEGEND_BUZ_Y+EPD_DISP_LEGEND_BUZ_H-2, GxEPD_BLACK);
        EPD.drawLine(EPD_DISP_LEGEND_BUZ_X, EPD_DISP_LEGEND_BUZ_Y+1, EPD_DISP_LEGEND_BUZ_X+EPD_DISP_LEGEND_BUZ_W-2, EPD_DISP_LEGEND_BUZ_Y+EPD_DISP_LEGEND_BUZ_H-1, GxEPD_BLACK);
        EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+1, EPD_DISP_LEGEND_BUZ_Y, EPD_DISP_LEGEND_BUZ_X+EPD_DISP_LEGEND_BUZ_W-1, EPD_DISP_LEGEND_BUZ_Y+EPD_DISP_LEGEND_BUZ_H-2, GxEPD_BLACK);

        EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+EPD_DISP_LEGEND_BUZ_W-1, EPD_DISP_LEGEND_BUZ_Y, EPD_DISP_LEGEND_BUZ_X, EPD_DISP_LEGEND_BUZ_Y+EPD_DISP_LEGEND_BUZ_H-1, GxEPD_BLACK);
        EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+EPD_DISP_LEGEND_BUZ_W-2, EPD_DISP_LEGEND_BUZ_Y, EPD_DISP_LEGEND_BUZ_X, EPD_DISP_LEGEND_BUZ_Y+EPD_DISP_LEGEND_BUZ_H-2, GxEPD_BLACK);
        EPD.drawLine(EPD_DISP_LEGEND_BUZ_X+EPD_DISP_LEGEND_BUZ_W-1, EPD_DISP_LEGEND_BUZ_Y+1, EPD_DISP_LEGEND_BUZ_X+1, EPD_DISP_LEGEND_BUZ_Y+EPD_DISP_LEGEND_BUZ_H-1, GxEPD_BLACK);
      }
#endif

    }

    if (gxepd2_refresh & GxEPD2_REFRESH_HISTORY || gxepd2_refresh & GxEPD2_REFRESH_LEGEND || gxepd2_refresh & GxEPD2_REFRESH_FULL) {
      ly0 = EPD_HISTORY_Y0 - (uint8_t)((uint16_t)(pref_co2_blue - CO2_HISTORY_MIN) * (uint16_t)EPD_HISTORY_H / (uint16_t)(CO2_HISTORY_MAX-CO2_HISTORY_MIN));
      ly1 = EPD_HISTORY_Y0 - (uint8_t)((uint16_t)(pref_co2_orange - CO2_HISTORY_MIN) * (uint16_t)EPD_HISTORY_H / (uint16_t)(CO2_HISTORY_MAX-CO2_HISTORY_MIN));
      ly2 = EPD_HISTORY_Y0 - (uint8_t)((uint16_t)(pref_co2_red - CO2_HISTORY_MIN) * (uint16_t)EPD_HISTORY_H / (uint16_t)(CO2_HISTORY_MAX-CO2_HISTORY_MIN));
    }

    if (gxepd2_refresh & GxEPD2_REFRESH_HISTORY || gxepd2_refresh & GxEPD2_REFRESH_FULL) {

      EPD.fillRect(EPD_DISP_HISTORY_X, EPD_DISP_HISTORY_Y, EPD_DISP_HISTORY_W, EPD_DISP_HISTORY_H, GxEPD_WHITE);
#ifdef WITH_GxEPD2_DRAW_FRAME
      EPD.drawRect(EPD_DISP_HISTORY_X, EPD_DISP_HISTORY_Y, EPD_DISP_HISTORY_W, EPD_DISP_HISTORY_H, GxEPD_BLACK);
#endif

      for (uint8_t x=0 ; x < CO2_HISTORY_SIZE; x++) {
        if (history_get(yco2_history, x) > 0) {
          EPD.drawLine(x, EPD_HISTORY_Y0 - history_get(yco2_history, x), x, EPD_HISTORY_Y0, GxEPD_BLACK);
        }
      }

      EPD.drawLine(EPD_DISP_HISTORY_X, ly0, EPD_DISP_HISTORY_X+EPD_DISP_HISTORY_W-1, ly0, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_HISTORY_X, ly1, EPD_DISP_HISTORY_X+EPD_DISP_HISTORY_W-1, ly1, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_HISTORY_X, ly2, EPD_DISP_HISTORY_X+EPD_DISP_HISTORY_W-1, ly2, GxEPD_BLACK);

    }

    if (gxepd2_refresh & GxEPD2_REFRESH_LEGEND || gxepd2_refresh & GxEPD2_REFRESH_FULL) {
#ifdef WITH_GxEPD2_FONT
      EPD.setFont(EPD_DISP_LEGEND_FONT);
#endif
      EPD.setTextSize(EPD_DISP_LEGEND_TS);

      lly0 = min(max(ly0-EPD_DISP_LEGEND_FONT_H/2, EPD_DISP_LEGEND_Y), EPD_DISP_LEGEND_Y+EPD_DISP_LEGEND_H-EPD_DISP_LEGEND_FONT_H-1);
      lly1 = min(max(ly1-EPD_DISP_LEGEND_FONT_H/2, EPD_DISP_LEGEND_Y), EPD_DISP_LEGEND_Y+EPD_DISP_LEGEND_H-EPD_DISP_LEGEND_FONT_H-1);
      lly2 = min(max(ly2-EPD_DISP_LEGEND_FONT_H/2, EPD_DISP_LEGEND_Y), EPD_DISP_LEGEND_Y+EPD_DISP_LEGEND_H-EPD_DISP_LEGEND_FONT_H-1);

      sprintf(Fbuf, "%4d", pref_co2_blue);
      EPD_calc_text(Fbuf, EPD_DISP_LEGEND_X+2, lly0, EPD_DISP_LEGEND_W-2, EPD_DISP_LEGEND_FONT_H, &x1, &y1, &w1, &h1, 1, 0);
      sprintf(Fbuf, "%4d", pref_co2_orange);
      EPD_calc_text(Fbuf, EPD_DISP_LEGEND_X+2, lly1, EPD_DISP_LEGEND_W-2, EPD_DISP_LEGEND_FONT_H, &x2, &y2, &w2, &h2, 1, 0);
      sprintf(Fbuf, "%4d", pref_co2_red);
      EPD_calc_text(Fbuf, EPD_DISP_LEGEND_X+2, lly2, EPD_DISP_LEGEND_W-2, EPD_DISP_LEGEND_FONT_H, &x3, &y3, &w3, &h3, 1, 0);

      EPD.fillRect(EPD_DISP_LEGEND_X, EPD_DISP_LEGEND_Y, EPD_DISP_LEGEND_W, EPD_DISP_LEGEND_H, GxEPD_WHITE);

      EPD.drawLine(EPD_DISP_LEGEND_X, ly0, EPD_DISP_LEGEND_X+2, ly0, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_X, ly1, EPD_DISP_LEGEND_X+2, ly1, GxEPD_BLACK);
      EPD.drawLine(EPD_DISP_LEGEND_X, ly2, EPD_DISP_LEGEND_X+2, ly2, GxEPD_BLACK);

      sprintf(Fbuf, "%4d", pref_co2_blue);
      EPD.setCursor(x1, y1);
      EPD.print(Fbuf);

      sprintf(Fbuf, "%4d", pref_co2_orange);
      EPD.setCursor(x2, y2);
      EPD.print(Fbuf);

      sprintf(Fbuf, "%4d", pref_co2_red);
      EPD.setCursor(x3, y3);
      EPD.print(Fbuf);

    }


  } while (EPD.nextPage());

  EPD.hibernate();

}

#endif

void setup() {

  Serial.begin(115200);
  Serial.println("");

  // Flash time !
  // Temps d'attente de sécurité pour flasher un firmware...
  led_color(LED_RGB_WHITE);
  delay(SLEEP_DURATION_FLASH_TIME);

#ifdef WITH_EEPROM
  eeprom_read_preferences();
#endif

#ifndef ESP32_NOOPT
#ifdef DEBUG
  Serial.println("Normal");
  delay(3000);
  Serial.println("Modem Sleep");
#endif
  setModemSleep();
#ifdef DEBUG
  delay(3000);
  Serial.println("Frequency 20 Mhz");
#endif
  setCpuFrequencyMhz(20);
#endif

  led_color(LED_RGB_GREEN);
#ifdef WITH_GxEPD2
  EPD.init();
  EPD.setRotation(3);
  EPD_init();
  EPD_version(VERSION);
#endif

  led_color(LED_RGB_BLUE);
  // Initialise le capteur SenseAir S8
#if defined(WITH_GxEPD2)
  EPD_message(F("S8"));
#else
  Serial.println(F("S8"));
#endif
  s8_init();

  led_color(LED_RGB_ORANGE);
  // Initialise le capteur SHT31
#if defined(WITH_GxEPD2)
  EPD_message(F("SHT31"));
#else
  Serial.println(F("SHT31"));
#endif
  for(uint8_t i=0; i<5; i++) {
    if (sht31_enable()) {
      i = 5;
    } else {
      delay(500);
    }
  }

  led_color(LED_RGB_RED);

#if defined(WITH_GxEPD2)
  EPD_message(F("..."));
#else
  Serial.println(F("..."));
#endif

#ifdef WITH_KY040
  pinMode(PIN_KY040_CLK, INPUT_PULLUP);
  pinMode(PIN_KY040_DT, INPUT_PULLUP);
  pinMode(PIN_KY040_SW, INPUT_PULLUP);
  ky040_pin_last = digitalRead(PIN_KY040_DT);
  attachInterrupt(PIN_KY040_SW, ky040_sw_isr, CHANGE);
  attachInterrupt(PIN_KY040_DT, ky040_interrupt, CHANGE);
#endif

  // Initialise l'historique
  for (uint8_t i = 0; i < CO2_HISTORY_SIZE; i++) {
#ifdef LOWMEM
    history_set(yco2_history, i, 0);
#else
    yco2_history[i] = 0;
#endif
  }

#ifdef DEBUG
  // Données aléatoires:
  uint8_t val = 0;
  for (uint8_t i = 0; i < CO2_HISTORY_SIZE; i++) {
    val += random(4) - 2;
    val = min(EPD_HISTORY_H-1, val);
    val = max(0, val);
    history_push(yco2_history, CO2_HISTORY_SIZE, val);
  }
#endif

#ifdef VBATT
  for (byte i = 0; i < VBATT_HISTORY_SIZE; i = i + 1) {
    vbatt_history[i] = getBatteryCapacity();
    delay(25);
  }
  vbatt_history_sort(vbatt_history, vbatt_history_sorted);
  // Nouvelle valeur:
  vbatt_last = vbatt_history_sorted[VBATT_HISTORY_SIZE/2];
#endif

  led_off();

#ifndef WITHOUT_LED_TEST
  // Initialise les LEDs
  // pour vérification de leur bon fonctionnement

#if defined(WITH_GxEPD2)
  EPD_message(F("VERT"));
#else
  Serial.println(F("VERT"));
#endif
  flash_led_and_buzz(LED_RGB_GREEN, LED_NMB_FLASH, NULL);

#if defined(WITH_GxEPD2)
  EPD_message(F("BLEU"));
#else
  Serial.println(F("BLEU"));
#endif
  flash_led_and_buzz(LED_RGB_BLUE, LED_NMB_FLASH, NULL);

#if defined(WITH_GxEPD2)
  EPD_message(F("ORANGE"));
#else
  Serial.println(F("ORANGE"));
#endif
#ifdef WITH_BUZZER
  flash_led_and_buzz(LED_RGB_ORANGE, sizeof(buzzer_orange)/sizeof(*buzzer_orange), buzzer_orange);
#else
  flash_led_and_buzz(LED_RGB_ORANGE, LED_NMB_FLASH, NULL);
#endif

#if defined(WITH_GxEPD2)
  EPD_message(F("ROUGE"));
#else
  Serial.println(F("ROUGE"));
#endif
#ifdef WITH_BUZZER
  flash_led_and_buzz(LED_RGB_RED, sizeof(buzzer_red)/sizeof(*buzzer_red), buzzer_red);
#else
  flash_led_and_buzz(LED_RGB_RED, LED_NMB_FLASH, NULL);
#endif
#endif

#if defined(WITH_GxEPD2)
  EPD_message(F("Rock'n Roll"));
  //EPD_message(NULL);
#else
  Serial.println(F("Rock'n Roll"));
#endif

}


// https://forum.arduino.cc/index.php?topic=331879.0
float round_to_dp(float in_value, uint8_t decimal_place) {
  float multiplier = powf(10.0f, decimal_place);
  in_value = roundf(in_value * multiplier) / multiplier;
  return in_value;
}


void loop() {

  sleep_count += 1;

  if (calibration) {
    if (calibration < 6) {
      calibration_timer += 1;
      if (calibration_timer > CALIBRATION_DELAY) {
        calibration += 1;
        calibration_timer = 0;
      }

      if (calibration == 1 || calibration == 3) {
        calibration_percent = ((calibration == 3) ? 50 : 0) + min(50, floor(calibration_timer * 50.0 /CALIBRATION_DELAY));
      }

      gxepd2_refresh = GxEPD2_REFRESH_ICONS;
    }
    /* calibration = 1 attente pré-calibration
     *               2 calibration
     *               3 attente post-calibration OK
     *               4 calibré OK
     *               6 Erreur calibration
     */
    if (calibration == 2) {
      if (do_calibration(true)) {
        calibration = 3;
      } else {
        calibration = 6;
      }
      gxepd2_refresh = GxEPD2_REFRESH_FULL;
    } else if (calibration == 5) {
      calibration = 0;
    }
  }

  if (sleep_count % SLEEP_COUNT_S8_TRIGGER == 0) {

#ifdef WITH_GxEPD2
    date = millis();
    if (date - gxepd2_last_refresh > EPD_FULL_REFRESH_DELAY) {
      gxepd2_refresh = GxEPD2_REFRESH_FULL;
      gxepd2_last_refresh = date;
    } else {
      gxepd2_refresh = 0;
    }
#endif

    /*
     * Déclenchement de la mesure des capteurs
     * et mise à jour de l'historique
     */
    if (sht31_enabled == false) {
      sht31_enable();
    }

    if (sht31_enabled == true) {
      float t = round_to_dp(sht31.readTemperature(), 1);
      float h = round_to_dp(sht31.readHumidity(), 0);

      if (!isnan(t) && (t!=sht31_t)) {
        sht31_t = t;
#ifdef DEBUG_VALUE
    sht31_t = -99.9;
#endif
#ifdef WITH_GxEPD2
        gxepd2_refresh |= GxEPD2_REFRESH_TEMP;

#endif
      }

      if (!isnan(h) && (h!=sht31_h)) {
        sht31_h = h;
#ifdef DEBUG_VALUE
    sht31_h = 99;
#endif
#ifdef WITH_GxEPD2
        gxepd2_refresh |= GxEPD2_REFRESH_HUMI;
#endif
      }
    }

    // Lecture CO2
    uint16_t c = s8_getValue(s8_read_co2, 8, 7);

#ifdef DEBUG_VALUE
    c = 9999;
#endif
    if (c < 0) {
      c = 0;
    } else if (c > 9999) {
      c = 9999;
    } else {
      /*
       * Historique:
       * Stockage direct de la coordonnée graphique du point pour éviter
       * de recalculer tout l'historique à chaque affichage
       * 
       * La valeur est ramenée dans l'intervalle des valeurs MIN et MAX
       * puis passée à l'échelle 16 pixels proportionnellement à cet intervalle
       */

      uint8_t val = ((min(CO2_HISTORY_MAX, max(CO2_HISTORY_MIN, co2)) - CO2_HISTORY_MIN) * EPD_HISTORY_H / (CO2_HISTORY_MAX-CO2_HISTORY_MIN));

      history_push(yco2_history, CO2_HISTORY_SIZE, val);
      if (c!=co2) {
        co2 = c;
#ifdef WITH_GxEPD2
    gxepd2_refresh |= GxEPD2_REFRESH_CO2;
    if (sleep_count % SLEEP_COUNT_HISTORY_TRIGGER == 0) {
      gxepd2_refresh |= GxEPD2_REFRESH_HISTORY;
    }
#endif
      }
    }

#ifdef WITH_KY040
  if (ky040_state_sw) {
    ky040_state_sw = false;
    gxepd2_refresh = GxEPD2_REFRESH_ICONS;
    GxEPD2_refresh();
  }
  if (ky040_state_rotation) {
    ky040_menu();
  }
#endif

    // Mise à jour de l'affichage:
#ifdef WITH_GxEPD2
    if (gxepd2_refresh >0) {
      GxEPD2_refresh();
    }
#endif

  }

#ifdef WITH_KY040
  if (ky040_state_sw) {
    ky040_state_sw = false;
    gxepd2_refresh = GxEPD2_REFRESH_ICONS;
    GxEPD2_refresh();
  }
  if (ky040_state_rotation) {
    ky040_menu();
  }
#endif

    // Détermine la couleur et le nombre de flash de led à effectuer
    flash = LED_NMB_FLASH;
    if (co2 < pref_co2_blue) {
      led = LED_RGB_GREEN;
      //flash = 2;
#ifdef WITH_BUZZER
      buzzer = NULL;
#endif
    } else if (co2 < pref_co2_orange) {
      led = LED_RGB_BLUE;
      //flash = 2;
#ifdef WITH_BUZZER
      buzzer = NULL;
#endif
    } else if (co2 < pref_co2_red) {
      led = LED_RGB_ORANGE;
#ifdef WITH_BUZZER
      if (pref_buzzer) {
        buzzer = buzzer_orange;
        flash = sizeof(buzzer_orange) / sizeof(*buzzer_orange);
      }
#endif
    } else {
      led = LED_RGB_RED;
#ifdef WITH_BUZZER
      if (pref_buzzer) {
        buzzer = buzzer_red;
        flash = sizeof(buzzer_red) / sizeof(*buzzer_red);
      }
#endif
    }

    flash_led_and_buzz(led, flash, buzzer);


#ifdef VBATT
  // On stocke la valeur:
  vbatt_history_push(vbatt_history, getBatteryCapacity());
  // On trie de nouveau l'historique pour récupèrer la valeur nouvelle médiane:
  vbatt_history_sort(vbatt_history, vbatt_history_sorted);
  // Nouvelle valeur:
  vbatt_last = vbatt_history_sorted[VBATT_HISTORY_SIZE/2];
#endif

#ifdef WITH_KY040
  if (ky040_state_sw) {
    ky040_state_sw = false;
    gxepd2_refresh = GxEPD2_REFRESH_ICONS;
    GxEPD2_refresh();
  }
  if (ky040_state_rotation) {
    ky040_menu();
  }
#endif

  dodo(SLEEP_DURATION_LOOP);

#ifdef WITH_KY040
  if (ky040_state_sw) {
    ky040_state_sw = false;
    gxepd2_refresh = GxEPD2_REFRESH_ICONS;
    GxEPD2_refresh();
  }
  if (ky040_state_rotation) {
    ky040_menu();
  }
#endif

  if (sleep_count >= SLEEP_COUNT_HISTORY_TRIGGER) {
    sleep_count = 0;
  }
}


void flash_led_and_buzz(uint32_t rgb, uint8_t nmb, uint16_t *buzzer) {
  bool buzzer_bt_active = false;
  bool buzzer_bt_long = false;

#ifdef LED_ALWAYS_ON
  led_color(rgb);
#endif

  for (uint8_t i=0; i<nmb; i++) {
#ifndef LED_ALWAYS_ON
    if (i%3 == 0) {
      led_color(rgb);
    } else if (i%3 == 2) {
      led_color(0x000000);
    }
#endif

#ifdef WITH_BUZZER
    if (buzzer != NULL && pref_buzzer) {
      tone(PIN_BUZZER, buzzer[i], 0/*DELAY_LED_FLASH_ON*/, PIN_BUZZER_PWM);
      delay(DELAY_LED_FLASH_OFF);
      noTone(PIN_BUZZER, PIN_BUZZER_PWM);
    } else {
      delay(DELAY_LED_FLASH_OFF);
    }
#else
    delay(DELAY_LED_FLASH_OFF);
#endif
  }

#ifndef LED_ALWAYS_ON
    led_off();
#endif
}

#ifdef WITH_KY040

void IRAM_ATTR ky040_sw_isr() {

  date = millis();

  // État du bouton:
  if (!digitalRead(PIN_KY040_SW)) {
    if (date - ky040_pin_millis > KY040_NOBOUNCE) {
      ky040_pin_millis = date;
      ky040_state_sw = true;
      if (!ky040_state_in_menu) {
        pref_buzzer = !pref_buzzer;
      }
    }
  }
}


void IRAM_ATTR ky040_interrupt() {

  date = millis();

  // État de l'encodeur rotatif:
  ky040_pin_current = digitalRead(PIN_KY040_DT);
  if (ky040_pin_current != ky040_pin_last) {
    if (date - ky040_pin_millis > KY040_NOBOUNCE) {
      ky040_pin_millis = date;
      if (digitalRead(PIN_KY040_CLK) != ky040_pin_current) {
        ky040_state_rotation -= 1;
      } else {
        ky040_state_rotation += 1;
      }
    }
    ky040_pin_last = ky040_pin_current;
  }
}

typedef struct {
  char            title[25];
  byte            entry_type;
  volatile void * value;
  volatile void * value_min;
  volatile void * value_max;
} ky040_menu_entry;

void ky040_handle_menu(uint8_t size, ky040_menu_entry *menu, boolean *fin);

void ky040_menu() {

#define MENU_EXIT              1
#define MENU_VALUE_BOOL        2
#define MENU_VALUE_INT16       4

  uint16_t co2_min = CO2_HISTORY_MIN;
  uint16_t co2_max = CO2_HISTORY_MAX;

  boolean            fin = false;
  boolean   do_cal_tempo = false;
  boolean do_cal_instant = false;
  boolean          do_fw = false;

  ky040_menu_entry menu[] = {
    { "Buzzer",                 MENU_VALUE_BOOL,   &pref_buzzer,      NULL,              NULL },
    { "Seuil Bleu",             MENU_VALUE_INT16,  &pref_co2_blue,    &co2_min,          &pref_co2_orange },
    { "Seuil Orange",           MENU_VALUE_INT16,  &pref_co2_orange,  &pref_co2_blue,    &pref_co2_red },
    { "Seuil Rouge",            MENU_VALUE_INT16,  &pref_co2_red,     &pref_co2_orange,  &co2_max },
    { "Sortie",                 MENU_EXIT,         &fin,              NULL,              NULL },
    { "Firmware",               MENU_VALUE_BOOL,   &do_fw,            NULL,              NULL },
    { "Calibration",            MENU_VALUE_BOOL,   &do_cal_tempo,     NULL,              NULL },
    { "Calibrat. imm.",         MENU_VALUE_BOOL,   &do_cal_instant,   NULL,              NULL }
  };

  ky040_state_sw = false;
  ky040_handle_menu(sizeof(menu)/sizeof(*menu), menu, &fin);

#ifdef WITH_EEPROM
  if (pref_changed) {
    eeprom_write_preferences();
    pref_changed = false;
  }
#endif

  if (do_cal_instant) {
    do_calibration(true);
  }

  if (do_cal_tempo) {
    calibration = 1;
    calibration_timer = 0;
  }

  if (do_fw) {
    display_fw();
  }

#ifdef WITH_GxEPD2
    gxepd2_refresh = GxEPD2_REFRESH_FULL;
    gxepd2_last_refresh = millis();
    GxEPD2_refresh();
#endif
}

void ky040_handle_menu(uint8_t size, ky040_menu_entry *menu, boolean *fin) {

  ky040_state_in_menu = true;

  int8_t   current_title = 0;
  boolean  current_edit = false;
  boolean  current_val_bool;
  uint16_t current_val_int16;

#ifdef WITH_GxEPD2

  #define EPD_DISP_MENU_FONT           FONT1B
  #define EPD_DISP_MENU_TS             FONT1B_TS
  #define EPD_DISP_MENU_TH                14
  #define EPD_DISP_MENU_W                246
  #define EPD_DISP_MENU_TITLE_X            1
  #define EPD_DISP_MENU_TITLE_W          162
  #define EPD_DISP_MENU_ARROW_X          164
  #define EPD_DISP_MENU_ARROW_W           24
  #define EPD_DISP_MENU_VALUE_X          189
  #define EPD_DISP_MENU_VALUE_W           56

  uint8_t  menu_w = EPD_DISP_MENU_W;
  uint8_t  menu_h = EPD_DISP_MENU_TH * size;
  uint8_t  menu_x = (EPD_W-menu_w)/2-1;
  uint8_t  menu_y = (EPD_H-menu_h)/2-1;
  int16_t  x1, y1;
  uint16_t w1, h1;


#ifdef WITH_GxEPD2_FONT
    EPD.setFont(EPD_DISP_MENU_FONT);
#endif
    EPD.setTextSize(EPD_DISP_MENU_TS);
    EPD.setTextColor(GxEPD_BLACK);

  bool   draw_all   = true;
  int8_t draw_menu  = -1;
  int8_t draw_value = -1;

  do {
    if (draw_all || draw_menu>-1 || draw_value >-1) {
      if (draw_all) {
        EPD.setPartialWindow(menu_x-1, menu_y-1, menu_w+2, menu_h+2);
      } else if (draw_menu>-1) {
        EPD.setPartialWindow(menu_x+EPD_DISP_MENU_ARROW_X, menu_y, EPD_DISP_MENU_ARROW_W, menu_h);
      } else if (draw_value > -1) {
        EPD.setPartialWindow(menu_x+EPD_DISP_MENU_VALUE_X, menu_y + draw_value*EPD_DISP_MENU_TH+1, EPD_DISP_MENU_VALUE_W, EPD_DISP_MENU_TH-2);
      }
      EPD.firstPage();
      do {
        // Dessine le cadre:
        if (draw_all || draw_menu==0 || draw_menu==size-1) {
          EPD.drawRect(menu_x-1, menu_y-1, menu_w+2, menu_h+2, GxEPD_BLACK);
        }
        // Efface tout le menu:
        if (draw_all) {
          EPD.fillRect(menu_x, menu_y, menu_w, menu_h, GxEPD_WHITE);
        } else {
          // Efface toutes les flèches:
          if (draw_menu > -1) {
            EPD.fillRect(menu_x+EPD_DISP_MENU_ARROW_X, menu_y, EPD_DISP_MENU_ARROW_W, menu_h, GxEPD_WHITE);
            EPD.drawLine(menu_x+EPD_DISP_MENU_ARROW_X, menu_y-1, menu_x+EPD_DISP_MENU_ARROW_X+EPD_DISP_MENU_ARROW_W-1, menu_y-1, GxEPD_BLACK);
            EPD.drawLine(menu_x+EPD_DISP_MENU_ARROW_X, menu_y+menu_h, menu_x+EPD_DISP_MENU_ARROW_X+EPD_DISP_MENU_ARROW_W-1, menu_y+menu_h, GxEPD_BLACK);
          }
        }

        for (uint8_t i=0; i<size; i++) {
          // Dessine la ligne entière:
          if (draw_all) {
            sprintf(Fbuf, "%14s", menu[i].title);
            EPD_calc_text(Fbuf, menu_x+EPD_DISP_MENU_TITLE_X, menu_y + i*EPD_DISP_MENU_TH, EPD_DISP_MENU_TITLE_W, EPD_DISP_MENU_TH, &x1, &y1, &w1, &h1, 0, 0);
            EPD.setCursor(x1, y1);
            EPD.print(Fbuf);
          }
          // Dessine la flèche:
          if (draw_menu == i || (draw_all && current_title==i)) {
            EPD_calc_text(">", menu_x+EPD_DISP_MENU_ARROW_X, menu_y + i*EPD_DISP_MENU_TH+1, EPD_DISP_MENU_ARROW_W, EPD_DISP_MENU_TH-2, &x1, &y1, &w1, &h1, 0, 0);
            EPD.setCursor(x1, y1);
            if (menu[i].entry_type & MENU_EXIT) {
              EPD.print("<<");
            } else {
              EPD.print(">>");
            }
          }
          // Affiche la valeur:
          if (draw_value == i || draw_all) {
            EPD.fillRect(menu_x+EPD_DISP_MENU_VALUE_X, menu_y + i*EPD_DISP_MENU_TH+1, EPD_DISP_MENU_VALUE_W, EPD_DISP_MENU_TH-2, current_edit?GxEPD_BLACK:GxEPD_WHITE);
            if (menu[i].entry_type & MENU_EXIT) {
              sprintf(Fbuf, "%s", "");
            } else if (menu[i].entry_type & MENU_VALUE_BOOL) {
              sprintf(Fbuf, "%4s", (current_edit ? current_val_bool : *(boolean *)menu[i].value) ? "oui" : "non");
            } else if (menu[i].entry_type & MENU_VALUE_INT16) {
              sprintf(Fbuf, "%4d", (current_edit ? current_val_int16 : *(uint16_t *)menu[i].value));
            }
            EPD_calc_text(Fbuf, menu_x+EPD_DISP_MENU_VALUE_X+1, menu_y + i*EPD_DISP_MENU_TH+1, EPD_DISP_MENU_VALUE_W-2, EPD_DISP_MENU_TH-2, &x1, &y1, &w1, &h1, 0, 0);
            if (current_edit) {
              EPD.setTextColor(GxEPD_WHITE);
            }
            EPD.setCursor(x1, y1);
            EPD.print(Fbuf);
            if (current_edit) {
              EPD.setTextColor(GxEPD_BLACK);
            }
          }
        }
      } while (EPD.nextPage());

      draw_all = false;
      draw_menu = -1;
      draw_value = -1;
    }

    // Lecture encodeur rotatif:
    if (ky040_state_rotation != 0) {
      if (current_edit == false) {
        // Déplacement dans le menu:
        current_title += ky040_state_rotation;
        if (current_title < 0) {
          current_title = size - 1;
        } else if (current_title > size -1) {
          current_title = 0;
        }
        draw_menu = current_title;
      } else {
        // Mode édition
        if (menu[current_title].entry_type & MENU_EXIT) {
          *fin = true;
        } else if (menu[current_title].entry_type & MENU_VALUE_BOOL) {
          current_val_bool = !current_val_bool;
        } else if (menu[current_title].entry_type & MENU_VALUE_INT16) {
          if (ky040_state_rotation > 0) {
            current_val_int16 = min(current_val_int16+ky040_state_rotation*10, *(uint16_t *)menu[current_title].value_max-10);
          } else if (ky040_state_rotation < 0) {
            current_val_int16 = max(current_val_int16+ky040_state_rotation*10, *(uint16_t *)menu[current_title].value_min+10);
          }
        }
        draw_value = current_title;
      }
      ky040_state_rotation = 0;
    }

    // Lecture bouton:
    if (ky040_state_sw == true) {

      if (menu[current_title].entry_type & MENU_EXIT) {
        *(boolean *)menu[current_title].value = true;
      } else {
        if (current_edit == false) {
          // Entrée en mode édition:
          if (menu[current_title].entry_type & MENU_VALUE_BOOL) {
            current_val_bool = *(boolean *)menu[current_title].value;
          } else if (menu[current_title].entry_type & MENU_VALUE_INT16) {
            current_val_int16 = *(uint16_t *)menu[current_title].value;
          }
        } else {
          // Sortie du mode édition:
          if (menu[current_title].entry_type & MENU_VALUE_BOOL) {
            if (*(boolean *)menu[current_title].value != current_val_bool) {
              *(boolean *)menu[current_title].value = current_val_bool;
#ifdef WITH_EEPROM
              pref_changed = true;
#endif
            }
          } else if (menu[current_title].entry_type & MENU_VALUE_INT16) {
            if (*(uint16_t *)menu[current_title].value != current_val_int16) {
              *(uint16_t *)menu[current_title].value = current_val_int16;
#ifdef WITH_EEPROM
              pref_changed = true;
#endif
            }
          }
        }
        current_edit = !current_edit;
      }
      ky040_state_sw = false;
      draw_value = current_title;
    }

  } while (!*fin);
#endif

  ky040_state_sw = false;
  ky040_state_rotation = 0;
  ky040_state_in_menu = false;
}

#endif

typedef struct {
  char            title[25];
  char            *value;
} fw_menu_entry;

void display_fw(void) {

  #define EPD_DISP_FW_FONT             FONT1B
  #define EPD_DISP_FW_TS               FONT1B_TS
  #define EPD_DISP_FW_TH                  14
  #define EPD_DISP_FW_W                  246
  #define EPD_DISP_FW_TITLE_X              1
  #define EPD_DISP_FW_TITLE_W            112
  #define EPD_DISP_FW_VALUE_X            114
  #define EPD_DISP_FW_VALUE_W            132

/*
  char this_s8_id[9];
  char this_s8_fw[5];
  char this_s8_abc[6];
*/
  fw_menu_entry menu[] = {
    { "S8 Id",        this_s8_id  },
    { "S8 Firmware",  this_s8_fw  },
    { "S8 ABC",       this_s8_abc },
  };

  uint8_t  menu_n = sizeof(menu)/sizeof(*menu);
  uint8_t  menu_w = EPD_DISP_FW_W;
  uint8_t  menu_h = EPD_DISP_FW_TH * menu_n;
  uint8_t  menu_x = (EPD_W-menu_w)/2-1;
  uint8_t  menu_y = (EPD_H-menu_h)/2-1;
  int16_t  x1, y1;
  uint16_t w1, h1;
  #ifdef WITH_GxEPD2_FONT
    EPD.setFont(EPD_DISP_FW_FONT);
  #endif
    EPD.setTextSize(EPD_DISP_FW_TS);
    EPD.setTextColor(GxEPD_BLACK);

        EPD.setPartialWindow(menu_x-1, menu_y-1, menu_w+2, menu_h+2);
      EPD.firstPage();
      do {
        // Dessine le cadre:
          EPD.drawRect(menu_x-1, menu_y-1, menu_w+2, menu_h+2, GxEPD_BLACK);
          EPD.fillRect(menu_x, menu_y, menu_w, menu_h, GxEPD_WHITE);

        for (uint8_t i=0; i<menu_n; i++) {
          // Dessine la ligne entière:
            sprintf(Fbuf, "%12s", menu[i].title);
            EPD_calc_text(Fbuf, menu_x+EPD_DISP_FW_TITLE_X, menu_y + i*EPD_DISP_FW_TH, EPD_DISP_FW_TITLE_W, EPD_DISP_FW_TH, &x1, &y1, &w1, &h1, 0, 0);
            EPD.setCursor(x1, y1);
            EPD.print(Fbuf);
            sprintf(Fbuf, "%9s", menu[i].value);
            EPD_calc_text(Fbuf, menu_x+EPD_DISP_FW_VALUE_X, menu_y + i*EPD_DISP_FW_TH, EPD_DISP_FW_VALUE_W, EPD_DISP_FW_TH, &x1, &y1, &w1, &h1, 0, 0);
            EPD.setCursor(x1, y1);
            EPD.print(Fbuf);

        }
      } while (EPD.nextPage());

  delay(10000);
}


#ifdef VBATT
unsigned int getBatteryCapacity(void) {

  byte percent = 0;
  unsigned int adc = analogRead(VBATT_PIN);
  float voltage = round_to_dp(adc * VBATT_MUL * 3.3 / 4096, 2);

  if (voltage > 4.2) {
    voltage = 4.2;
  }

  for (int i = 0 ; i < ncell ; i++){
#ifdef DEBUG
    Serial.print(i);
    Serial.print(" : ");
    Serial.print(remainingCapacity[i].voltage);
    Serial.print(" | ");
    Serial.println(remainingCapacity[i].capacity);
#endif
    if (voltage > remainingCapacity[i].voltage) {
      float m = 0;
      float p = 0;
      // https://fr.wikipedia.org/wiki/Pente_(math%C3%A9matiques)
      m = (float)(remainingCapacity[i+1].capacity - remainingCapacity[i].capacity) / (remainingCapacity[i+1].voltage - remainingCapacity[i].voltage);
      // y=m.x+p -> p = y1 - mx1
      p = (float)remainingCapacity[i].capacity - m * remainingCapacity[i].voltage;
      // calcul:
      //float calcul = m * voltage + p;
      percent = round_to_dp(m * voltage + p, 0);
      if (percent < 0) {
        percent = 0;
      } else if (percent > 100) {
        percent = 100;
      }
      i = ncell;
      //return remainingCapacity[i].capacity;
      //return calcul;
    }
  }

#ifdef DEBUG
  Serial.print(F("ADC= "));
  Serial.print(adc);
  Serial.print(F(" V= "));
  Serial.print(voltage);
  Serial.print(F(" "));
  Serial.print(percent);
  Serial.println(F("%"));
#endif

  return percent;
}

void vbatt_history_push(byte *h, byte value) {
  for (byte i = 0; i < VBATT_HISTORY_SIZE-1; i = i+1) {
    h[i] = h[i+1];
  }
  h[VBATT_HISTORY_SIZE-1] = value;
}

String vbatt_history_string(const byte *h) {
  String s = "";
  for (int i = 0; i < VBATT_HISTORY_SIZE; i = i+1) {
    s = s + " " + h[i];
  }
  return s;
}

int vbatt_sort_desc(const void *cmp1, const void *cmp2) {
  // Need to cast the void * to byte *
  byte a = *((byte *)cmp1);
  byte b = *((byte *)cmp2);
  // The comparison
  return a > b ? -1 : (a < b ? 1 : 0);
}

void vbatt_history_sort(const byte *h, byte *hs) {
  // Copie:
  for (byte i = 0; i < VBATT_HISTORY_SIZE; i = i+1) {
    hs[i] = h[i];
  }
  // Tri:
  qsort(hs, VBATT_HISTORY_SIZE, sizeof(byte), vbatt_sort_desc);
}

#endif



void led_color(uint32_t rgb) {
//  return;

  byte R = 0xFF - ((rgb>>16) & 0xFF);
  byte G = 0xFF - ((rgb>>8) & 0xFF);
  byte B = 0xFF - (rgb & 0xFF);
  
  if (!_led_pwm) {
    led_init();
  }

  ledcWrite(LED_CHN_R, R);
  ledcWrite(LED_CHN_G, G);
  ledcWrite(LED_CHN_B, B);
}

void led_init() {
  if (!_led_pwm) {
    _led_pwm = true;
    ledcAttachPin(LED_PIN_R, LED_CHN_R);
    ledcSetup(LED_CHN_R, LED_PWM_FREQ, LED_PWM_RES);
    ledcAttachPin(LED_PIN_G, LED_CHN_G);
    ledcSetup(LED_CHN_G, LED_PWM_FREQ, LED_PWM_RES);
    ledcAttachPin(LED_PIN_B, LED_CHN_B);
    ledcSetup(LED_CHN_B, LED_PWM_FREQ, LED_PWM_RES);
  }
}

void led_off() {
  if (_led_pwm) {
    ledcWrite(LED_CHN_R, 0xff);
    ledcDetachPin(LED_PIN_R);
    digitalWrite(LED_PIN_R, HIGH);

    ledcWrite(LED_CHN_G, 0xff);
    ledcDetachPin(LED_PIN_G);
    digitalWrite(LED_PIN_G, HIGH);

    ledcWrite(LED_CHN_B, 0xff);
    ledcDetachPin(LED_PIN_B),
    digitalWrite(LED_PIN_B, HIGH);

    _led_pwm = false;
  }
}

boolean do_calibration(boolean automatic) {

  int pression = 0;
  boolean result = false;

  if (automatic == true) {
    pression = 1;
  } else {
    EPD_message(F("CALIBRATION?"), true);

    // Attend le relâchement éventuel du bouton:
    while (!digitalRead(PIN_KY040_SW)) {
      delay(10);
    }

    EPD_version(F("1 pression"));
    for (int i=0; i<500; i++) {
      if (!digitalRead(PIN_KY040_SW)) {
        // Bouton enfoncé:
        pression++;
      } else {
        if (pression>0) {
          i = 500;
        }
      }
      delay(10);
    }
    EPD_version(F(""));
  }

  if (pression > 0) {
    EPD_message(F("CALIBRATION"), true);
#ifdef WITH_FAKE_CALIBRATION_OK
    result = true;
    delay(4500);
    EPD_message(F("OK"));
#elif defined(WITH_FAKE_CALIBRATION_ERR)
    delay(4500);
    EPD_message(F("ERREUR"));
#else
    int16_t res;
    res = s8_getValue(s8_clear_hr1, 8, 8);
    delay(100);
    res = s8_getValue(s8_calibrate, 8, 8);
    delay(4500);
    res = s8_getValue(s8_read_hr1, 8, 7);
    if (res == 32) {
      EPD_message(F("OK"));
      result = true;
    } else {
      EPD_message(F("ERREUR"));
    }
#endif
  } else {
      EPD_message(F("ANNULATION"));
      result = true;
  }
  delay(5000);
  gxepd2_refresh = GxEPD2_REFRESH_FULL;
  GxEPD2_refresh();

  return result;
}

#ifdef WITH_EEPROM
void eeprom_read_preferences() {

  eeprom_pref.begin(PREF_TAG, true);
  pref_buzzer     = eeprom_pref.getBool("buzzer", DEFPREF_BUZZER);
  pref_co2_blue   = eeprom_pref.getUShort("co2_blue", DEFPREF_CO2_BLUE);
  pref_co2_orange = eeprom_pref.getUShort("co2_orange", DEFPREF_CO2_ORANGE);
  pref_co2_red    = eeprom_pref.getUShort("co2_red", DEFPREF_CO2_RED);
  eeprom_pref.end();
#ifdef DEBUG
  Serial.println("eeprom_read_preferences");
  Serial.print("    pref_buzzer = "); Serial.println(pref_buzzer);
  Serial.print("  pref_co2_blue = "); Serial.println(pref_co2_blue);
  Serial.print("pref_co2_orange = "); Serial.println(pref_co2_orange);
  Serial.print("   pref_co2_red = "); Serial.println(pref_co2_red);
#endif
}

void eeprom_write_preferences() {

#ifdef DEBUG
  Serial.println("eeprom_write_preferences");
#endif

  eeprom_pref.begin(PREF_TAG, false);
  eeprom_pref.putBool("buzzer", pref_buzzer);
  eeprom_pref.putUShort("co2_blue", pref_co2_blue);
  eeprom_pref.putUShort("co2_orange", pref_co2_orange);
  eeprom_pref.putUShort("co2_red", pref_co2_red);
  eeprom_pref.end();

}
#endif
