Problem with settings contrats to SH1106 screen in ESPHome

Hello everyone. I’ve started a very simple little device project for my parents to measure the CO2 concentration in a room and display this concentration over the last 12 hours.

I’m using an ESP32 C3, an SCD41 sensor, a 1.3-inch SH1106 OLED screen, and a TTP223 touch button.
Everything works more or less, but I’d like to be able to change the screen’s brightness levels by pressing the touch button as follows: low brightness, then medium, then high, then screen off. If possible, I’d like to be able to assign another function to a longer press of the button (24-hour readings instead of 12 hours, or possibly turning the screen off if no other option is available).

I’m using AI to help me code, but I can’t find any code that works for this. Every time I press the button, nothing happens except for the third press, when the screen turns off. By making a mistake in the code, I’ve managed to switch between low and high values ​​(current code), but it’s not satisfactory.
Is there anyone skilled enough to understand this and tell me how to fix it?
Thank you in advance.
Fabien

Here is the code :

esphome:
  name: esp-c3-co2
  friendly_name: ESP-C3 CO2

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

logger:
  level: ERROR
i2c:
  sda: GPIO8
  scl: GPIO9
  frequency: 100kHz

sensor:
  - platform: scd4x
    co2:
      id: co2_sensor
    update_interval: 60s

globals:
  - id: co2_history
    type: float[120]
    restore_value: false
  - id: co2_index
    type: int
    initial_value: '0'
  - id: screen_on
    type: bool
    initial_value: 'true'
  - id: contrast_level
    type: int
    initial_value: '2'
  - id: shift_step
    type: int
    initial_value: '0'
  - id: shift_x
    type: int
    initial_value: '0'
  - id: shift_y
    type: int
    initial_value: '0'
  - id: light_mode
    type: int
    initial_value: '1'    

interval:
  - interval: 6min
    then:
      - lambda: |-
          float v = id(co2_sensor).state;
          if (isnan(v)) return;
          id(co2_history)[id(co2_index)] = v;
          id(co2_index) = (id(co2_index) + 1) % 120;

  - interval: 1min  # Plus rapide pour une meilleure répartition
    then:
      - lambda: |-
          // On passe sur 8 positions pour décrire un carré de 2x2
          id(shift_step) = (id(shift_step) + 1) % 8;
          switch (id(shift_step)) {
            case 0: id(shift_x) = 0; id(shift_y) = 0;  break;
            case 1: id(shift_x) = 2; id(shift_y) = 0;  break;
            case 2: id(shift_x) = 2; id(shift_y) = -2; break;
            case 3: id(shift_x) = 0; id(shift_y) = -2; break;
            case 4: id(shift_x) = 1; id(shift_y) = -1; break;
            case 5: id(shift_x) = -1; id(shift_y) = 1; break;
            case 6: id(shift_x) = -2; id(shift_y) = 2; break;
            case 7: id(shift_x) = 0; id(shift_y) = 1;  break;
          }

binary_sensor:
  - platform: gpio
    id: button_touch
    pin:
      number: GPIO4
      mode: INPUT_PULLDOWN
    on_press:
      then:
        - logger.log: "=== BOUTON PRESSE ==="
        - lambda: |-
            ESP_LOGD("button", "Contrast level avant: %d", id(contrast_level));
            
            id(contrast_level) = (id(contrast_level) + 1) % 2;
            
            ESP_LOGD("button", "Contrast level après: %d", id(contrast_level));
            
            if (id(contrast_level) == 2) {
              ESP_LOGD("button", "Action: ETEINDRE écran");
              id(oled).turn_off();
            } else {
              ESP_LOGD("button", "Action: ALLUMER écran");
              id(oled).turn_on();
              
              static uint8_t levels[5] = {255};
              uint8_t contrast = levels[id(contrast_level)];
              
              ESP_LOGD("button", "Applying contrast: %d", contrast);
              id(oled).set_contrast(contrast);
            }
        - component.update: oled
        - logger.log: "=== FIN ACTION BOUTON ==="

display:
  - platform: ssd1306_i2c
    id: oled
    model: "SH1106 128x64"
    address: 0x3C
    update_interval: 50s
    lambda: |-
      if (id(light_mode) == 0) return; 

      // DÉCLARATION UNIQUE (Vérifie bien qu'elles n'existent pas plus bas !)
      int ox = id(shift_x);
      int oy = id(shift_y);
      float co2 = id(co2_sensor).state;
      
      it.fill(COLOR_OFF);

           
      if (!isnan(co2)) {
        int co2_x = (co2 >= 1000) ? 10 : 20;
        it.printf(co2_x + ox, 0 + oy, id(font_big), TextAlign::TOP_LEFT, "%.0f", co2);
        it.printf(65 + ox, 10 + oy, id(font_small), TextAlign::TOP_LEFT, "ppm");
        
         
        
        // Smiley
        int sx = 115 + ox; 
        int sy = 12 + oy;
        it.circle(sx, sy, 9);
        it.draw_pixel_at(sx - 3, sy - 2);
        it.draw_pixel_at(sx + 3, sy - 2);

        if (co2 < 910) {
          // --- SOURIRE : Les coins (sy+2) sont plus HAUTS que le centre (sy+4) ---
          it.line(sx - 3, sy + 4, sx + 3, sy + 4); // Centre
          it.line(sx - 5, sy + 2, sx - 3, sy + 4); // Coin gauche monte
          it.line(sx + 3, sy + 4, sx + 5, sy + 2); // Coin droit monte

        } else if (co2 < 1200) {
          // --- NEUTRE : Une ligne bien droite ---
          it.line(sx - 4, sy + 4, sx + 4, sy + 4);

        } else {
          // --- MÉCONTENT : Les coins (sy+6) sont plus BAS que le centre (sy+4) ---
          it.line(sx - 3, sy + 4, sx + 3, sy + 4); // Centre
          it.line(sx - 5, sy + 6, sx - 3, sy + 4); // Coin gauche descend
          it.line(sx + 3, sy + 4, sx + 5, sy + 6); // Coin droit descend
        }

      
      // GRAPHE (Remonté de 2 pixels : 30 au lieu de 32, 61 au lieu de 63)
      const int g_top = 30 + oy; 
      const int g_bottom = 61 + oy;
      const int g_left = 0 + ox;
      const int g_width = 127; 

      auto map_co2 = [&](float v) {
        if (v < 400) v = 400;
        if (v > 2000) v = 2000;
        return g_bottom - (int)((v - 400) * (g_bottom - g_top) / (2000 - 400));
      };

      // 1. DESSIN DES LIGNES DE RÉFÉRENCE (Pointillés)
      int y_1000 = map_co2(1000);
      int y_2000 = map_co2(2000);

      for (int x = 0; x <= 127; x += 6) {
        it.draw_pixel_at(x + ox, y_1000);
        it.draw_pixel_at(x + ox, y_2000);
      }

      // 2. TRACÉ DE L'HISTORIQUE
      int idx0 = id(co2_index);
      float first_val = id(co2_history)[idx0];
      int prev_y = map_co2(isnan(first_val) ? 400 : first_val);

      for (int i = 1; i < 120; i++) {
        int idx = (idx0 + i) % 120;
        float val = id(co2_history)[idx];
        if (isnan(val)) continue;
        
        int x_prev = (i - 1) * 127 / 119;
        int x_curr = i * 127 / 119;
        int y = map_co2(val);
        
        it.line(x_prev + ox, prev_y, x_curr + ox, y);
        prev_y = y;
      }

      // 3. AXE X ET REPERES
      it.line(0 + ox, g_bottom, 127 + ox, g_bottom); // Axe horizontal
      it.line(0 + ox, g_top, 3 + ox, g_top);          // Cran 2000
      it.line(0 + ox, y_1000, 3 + ox, y_1000);       // Cran 1000

      // 4. MARQUAGE DES HEURES (Sous l'axe X)
      // On place un point tous les 10 échantillons (10 * 6 min = 1 heure)
      // i=0 est le point le plus vieux, i=119 le plus récent
      for (int i = 9; i < 120; i += 10) {
          int x_hour = i * 127 / 119;
          it.draw_pixel_at(x_hour + ox, g_bottom + 2); 
      }



font:
  - file: "fonts/Roboto-Bold.ttf"
    id: font_big
    size: 24
  - file: "fonts/Roboto-Regular.ttf"
    id: font_small
    size: 10

This is not how you describe a problem you are looking for help, your “satisfactory” is completely unknown argument for us.
Anyway, your code is really bad even from AI.

This line already prevents you to get what you were looking for, it would be always 0 or 1.
I’m inviting you to read the documentation instead of spending hours with AI crap. Forum helps when you get stuck.

Try like this (not tested)

binary_sensor:
  - platform: gpio
    id: button_touch
    pin:
      number: GPIO4
      mode: INPUT_PULLDOWN
    filters:
      - delayed_on: 30ms
      - delayed_off: 30ms
    on_press:
      then:
        - globals.set:
            id: contrast_level
            value: !lambda 'return (id(contrast_level) + 1) % 4;'

        - if:
            condition:
              lambda: 'return id(contrast_level) == 0;'
            then:
              - lambda: 'id(oled).set_contrast(50);'
              - component.update: oled

        - if:
            condition:
              lambda: 'return id(contrast_level) == 1;'
            then:
              - lambda: 'id(oled).set_contrast(150);'
              - component.update: oled

        - if:
            condition:
              lambda: 'return id(contrast_level) == 2;'
            then:
              - lambda: 'id(oled).set_contrast(255);'
              - component.update: oled

        - if:
            condition:
              lambda: 'return id(contrast_level) == 3;'
            then:
              - lambda: 'id(oled).set_contrast(0);'
              - component.update: oled

Hello Karosm,

Thank you for taking the time to reply.

As I wrote, I know the code was wrong, but it’s the only way I found to change my screen’s brightness.

I searched (probably not very well) in the ESPhome documentation (SSD1306 OLED Display - ESPHome - Smart Home Made Simple) and on forums, but couldn’t find anything that helped.

AI is full of flaws, but I use it as a translator between my ideas and the electronics. Perhaps you never use Google Translate to write in a foreign language?

Thank you for the code. Unfortunately, it doesn’t work : “Unable to find action with the name ‘display.turn_on’”.

I wonder if the reason isn’t that the screen (bought on AliExpress) is too low-end.

I finally modified the code to have On-Off instead of two brightness levels.

Sincerely,

Fabien

esphome:
  name: esp-c3-co2
  friendly_name: ESP-C3 CO2

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

logger:
  level: ERROR
i2c:
  sda: GPIO8
  scl: GPIO9
  frequency: 100kHz

sensor:
  - platform: scd4x
    co2:
      id: co2_sensor
    update_interval: 60s

globals:
  - id: co2_history
    type: float[120]
    restore_value: false
  - id: co2_index
    type: int
    initial_value: '0'
  - id: screen_on
    type: bool
    initial_value: 'true'
  - id: contrast_level
    type: int
    initial_value: '2'
  - id: shift_step
    type: int
    initial_value: '0'
  - id: shift_x
    type: int
    initial_value: '0'
  - id: shift_y
    type: int
    initial_value: '0'
  - id: light_mode
    type: int
    initial_value: '1'    

interval:
  - interval: 6min
    then:
      - lambda: |-
          float v = id(co2_sensor).state;
          if (isnan(v)) return;
          id(co2_history)[id(co2_index)] = v;
          id(co2_index) = (id(co2_index) + 1) % 120;

  - interval: 1min  # Plus rapide pour une meilleure répartition
    then:
      - lambda: |-
          // On passe sur 8 positions pour décrire un carré de 2x2
          id(shift_step) = (id(shift_step) + 1) % 8;
          switch (id(shift_step)) {
            case 0: id(shift_x) = 0; id(shift_y) = 0;  break;
            case 1: id(shift_x) = 2; id(shift_y) = 0;  break;
            case 2: id(shift_x) = 2; id(shift_y) = -2; break;
            case 3: id(shift_x) = 0; id(shift_y) = -2; break;
            case 4: id(shift_x) = 1; id(shift_y) = -1; break;
            case 5: id(shift_x) = -1; id(shift_y) = 1; break;
            case 6: id(shift_x) = -2; id(shift_y) = 2; break;
            case 7: id(shift_x) = 0; id(shift_y) = 1;  break;
          }

binary_sensor:
  - platform: gpio
    id: button_touch
    pin:
      number: GPIO4
      mode: INPUT_PULLDOWN
    filters:
      - delayed_on: 30ms
      
    on_press:
      then:
        - lambda: |-
            id(screen_on) = !id(screen_on);
            if (id(screen_on)) {
              id(oled).turn_on();
            } else {
              id(oled).turn_off();
            }
        - component.update: oled

display:
  - platform: ssd1306_i2c
    id: oled
    model: "SH1106 128x64"
    address: 0x3C
    update_interval: 50s
    lambda: |-
      if (!id(screen_on)) return;

      // DÉCLARATION UNIQUE 
      int ox = id(shift_x);
      int oy = id(shift_y);
      float co2 = id(co2_sensor).state;
      
      it.fill(COLOR_OFF);
      // Si la valeur CO2 n'est pas encore disponible
      if (isnan(co2)) {
      it.printf(64 + ox, 32 + oy, id(font_small), TextAlign::CENTER, "Capteur en chauffe...");
      return; // On arrête là, on ne dessine pas le reste (smiley/graphe)
      }

      //----DESSIN DU CO2----     
      if (!isnan(co2)) {
        int co2_x = (co2 >= 1000) ? 10 : 20;
        it.printf(co2_x + ox, 0 + oy, id(font_big), TextAlign::TOP_LEFT, "%.0f", co2);
        it.printf(65 + ox, 10 + oy, id(font_small), TextAlign::TOP_LEFT, "ppm");
        
         
        
        // Smiley
        int sx = 115 + ox; 
        int sy = 12 + oy;
        it.circle(sx, sy, 9);
        it.draw_pixel_at(sx - 3, sy - 2);
        it.draw_pixel_at(sx + 3, sy - 2);

        if (co2 < 910) {
          // --- SOURIRE : Les coins (sy+2) sont plus HAUTS que le centre (sy+4) ---
          it.line(sx - 3, sy + 4, sx + 3, sy + 4); // Centre
          it.line(sx - 5, sy + 2, sx - 3, sy + 4); // Coin gauche monte
          it.line(sx + 3, sy + 4, sx + 5, sy + 2); // Coin droit monte

        } else if (co2 < 1200) {
          // --- NEUTRE : Une ligne bien droite ---
          it.line(sx - 4, sy + 4, sx + 4, sy + 4);

        } else {
          // --- MÉCONTENT : Les coins (sy+6) sont plus BAS que le centre (sy+4) ---
          it.line(sx - 3, sy + 4, sx + 3, sy + 4); // Centre
          it.line(sx - 5, sy + 6, sx - 3, sy + 4); // Coin gauche descend
          it.line(sx + 3, sy + 4, sx + 5, sy + 6); // Coin droit descend
        }

      
      // GRAPHE (Remonté de 2 pixels : 30 au lieu de 32, 61 au lieu de 63)
      const int g_top = 30 + oy; 
      const int g_bottom = 61 + oy;
      const int g_left = 0 + ox;
      const int g_width = 127; 

      auto map_co2 = [&](float v) {
        if (v < 400) v = 400;
        if (v > 2000) v = 2000;
        return g_bottom - (int)((v - 400) * (g_bottom - g_top) / (2000 - 400));
      };

      // 1. DESSIN DES LIGNES DE RÉFÉRENCE (Pointillés)
      int y_1000 = map_co2(1000);
      int y_2000 = map_co2(2000);

      for (int x = 0; x <= 127; x += 6) {
        it.draw_pixel_at(x + ox, y_1000);
        it.draw_pixel_at(x + ox, y_2000);
      }

      // 2. TRACÉ DE L'HISTORIQUE
      int idx0 = id(co2_index);
      float first_val = id(co2_history)[idx0];
      int prev_y = map_co2(isnan(first_val) ? 400 : first_val);

      for (int i = 1; i < 120; i++) {
        int idx = (idx0 + i) % 120;
        float val = id(co2_history)[idx];
        if (isnan(val)) continue;
        
        int x_prev = (i - 1) * 127 / 119;
        int x_curr = i * 127 / 119;
        int y = map_co2(val);
        
        it.line(x_prev + ox, prev_y, x_curr + ox, y);
        prev_y = y;
      }

      // 3. AXE X ET REPERES
      it.line(0 + ox, g_bottom, 127 + ox, g_bottom); // Axe horizontal
      it.line(0 + ox, g_top, 3 + ox, g_top);          // Cran 2000
      it.line(0 + ox, y_1000, 3 + ox, y_1000);       // Cran 1000

      // 4. MARQUAGE DES HEURES (Sous l'axe X)
      // On place un point tous les 10 échantillons (10 * 6 min = 1 heure)
      // i=0 est le point le plus vieux, i=119 le plus récent
      for (int i = 9; i < 120; i += 10) {
          int x_hour = i * 127 / 119;
          it.draw_pixel_at(x_hour + ox, g_bottom + 2); 
      }
      }

font:
  - file: "fonts/Roboto-Bold.ttf"
    id: font_big
    size: 24
  - file: "fonts/Roboto-Regular.ttf"
    id: font_small
    size: 10

That display turn on was left from your code, I removed it from the code above. Feel free to try it now if you want…

Thank you, again. I’ve tested your new code.
I’m no longer getting an error message, but I’m running into the same problems I’ve already encountered: when the buttons are pressed in conditions 0, 1, and 2, nothing happens, and in condition 3, the brightness decreases without turning off the screen.
I tried replacing the values ​​in set_contrast with different values: 1, 4, 10, and -10, but I don’t see any difference.
Again, perhaps the drivers for this screen don’t handle brightness/contrast changes?
However, the value 0 seems to be the condition that decreases the brightness. If I could run the code with a high value (255), the value 0, and the screen off, it might be better, but I had problems with that too. I’ll try again with that approach again.

Fabien

I’m not familiar with that display but quick look at the driver datasheet says that it has 256 step contrast. Quick look at the esphome api indicates that that while contrast levels are 0-255, the set_contrast expects float. So try with values 0-1.0
Like
id(oled).set_contrast(0.5);

You’re right. Thank you very much for your help. My project is now finished, and it is thanks to you.
Here is the code that works like I wish :

esphome:
  name: esp-c3-co2
  friendly_name: ESP-C3 CO2

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: esp-idf

logger:
  level: ERROR
i2c:
  sda: GPIO8
  scl: GPIO9
  frequency: 100kHz

sensor:
  - platform: scd4x
    co2:
      id: co2_sensor
    update_interval: 60s

globals:
  - id: co2_history
    type: float[120]
    restore_value: false
  - id: co2_index
    type: int
    initial_value: '0'
  - id: screen_on
    type: bool
    initial_value: 'true'
  - id: contrast_level
    type: int
    initial_value: '2'
  - id: shift_step
    type: int
    initial_value: '0'
  - id: shift_x
    type: int
    initial_value: '0'
  - id: shift_y
    type: int
    initial_value: '0'
  - id: light_mode
    type: int
    initial_value: '1'    

interval:
  - interval: 6min
    then:
      - lambda: |-
          float v = id(co2_sensor).state;
          if (isnan(v)) return;
          id(co2_history)[id(co2_index)] = v;
          id(co2_index) = (id(co2_index) + 1) % 120;

  - interval: 1min  # Plus rapide pour une meilleure répartition
    then:
      - lambda: |-
          // On passe sur 8 positions pour décrire un carré de 2x2
          id(shift_step) = (id(shift_step) + 1) % 8;
          switch (id(shift_step)) {
            case 0: id(shift_x) = 0; id(shift_y) = 0;  break;
            case 1: id(shift_x) = 2; id(shift_y) = 0;  break;
            case 2: id(shift_x) = 2; id(shift_y) = -2; break;
            case 3: id(shift_x) = 0; id(shift_y) = -2; break;
            case 4: id(shift_x) = 1; id(shift_y) = -1; break;
            case 5: id(shift_x) = -1; id(shift_y) = 1; break;
            case 6: id(shift_x) = -2; id(shift_y) = 2; break;
            case 7: id(shift_x) = 0; id(shift_y) = 1;  break;
          }

binary_sensor:
  - platform: gpio
    id: button_touch
    pin:
      number: GPIO4
      mode: INPUT_PULLDOWN
    filters:
      - delayed_on: 30ms
      - delayed_off: 30ms
    on_press:
      then:
        - globals.set:
            id: contrast_level
            value: !lambda 'return (id(contrast_level) + 1) % 4;'

        # ÉTAT 0 : FORT (On doit forcer le Turn On ici)
        - if:
            condition:
              lambda: 'return id(contrast_level) == 0;'
            then:
              - lambda: |-
                  id(oled).turn_on();       // INDISPENSABLE pour sortir du mode OFF
                  id(oled).set_contrast(1.0); // 1.0 = 255 (Max)
              - component.update: oled
        
        # ÉTAT 1 : MOYEN
        - if:
            condition:
              lambda: 'return id(contrast_level) == 1;'
            then:
              - lambda: |-
                  id(oled).turn_on();       // Sécurité au cas où
                  id(oled).set_contrast(0.5); // 0.0 = Min
              - component.update: oled


        # ÉTAT 1 : FAIBLE
        - if:
            condition:
              lambda: 'return id(contrast_level) == 2;'
            then:
              - lambda: |-
                  id(oled).turn_on();       // Sécurité au cas où
                  id(oled).set_contrast(0.0); // 0.0 = Min
              - component.update: oled

        # ÉTAT 2 : ÉTEINT
        - if:
            condition:
              lambda: 'return id(contrast_level) == 3;'
            then:
              - lambda: 'id(oled).turn_off();'
              - component.update: oled



display:
  - platform: ssd1306_i2c
    id: oled
    model: "SH1106 128x64"
    address: 0x3C
    update_interval: 50s
    lambda: |-
      if (!id(screen_on)) return;

      // DÉCLARATION UNIQUE 
      int ox = id(shift_x);
      int oy = id(shift_y);
      float co2 = id(co2_sensor).state;
      
      it.fill(COLOR_OFF) ;
      // Si la valeur CO2 n'est pas encore disponible 
      if (isnan(co2)) {
      it.printf(64 + ox, 32 + oy, id(font_small), TextAlign::CENTER, "Capteur en chauffe...");
      return; // On arrête là, on ne dessine pas le reste (smiley/graphe)
      }

      //----DESSIN DU CO2----    et remonté (oy-4) 
      if (!isnan(co2)) {
        int co2_x = (co2 >= 1000) ? 10 : 20;
        it.printf(co2_x + ox, -4 + oy, id(font_big), TextAlign::TOP_LEFT, "%.0f", co2);
        it.printf(65 + ox, 6 + oy, id(font_small), TextAlign::TOP_LEFT, "ppm");
        
         
        
        // Smiley (sy passée de 12 à 8)
        int sx = 110 + ox; 
        int sy = 11 + oy;
        it.circle(sx, sy, 9);
        it.draw_pixel_at(sx - 3, sy - 2);
        it.draw_pixel_at(sx + 3, sy - 2);

        if (co2 < 910) {
          // --- SOURIRE : Les coins (sy+2) sont plus HAUTS que le centre (sy+4) ---
          it.line(sx - 3, sy + 4, sx + 3, sy + 4); // Centre
          it.line(sx - 5, sy + 2, sx - 3, sy + 4); // Coin gauche monte
          it.line(sx + 3, sy + 4, sx + 5, sy + 2); // Coin droit monte

        } else if (co2 < 1200) {
          // --- NEUTRE : Une ligne bien droite ---
          it.line(sx - 4, sy + 4, sx + 4, sy + 4);

        } else {
          // --- MÉCONTENT : Les coins (sy+6) sont plus BAS que le centre (sy+4) ---
          it.line(sx - 3, sy + 3, sx + 3, sy + 3); // Centre
          it.line(sx - 3, sy + 3, sx - 5, sy + 5); // Coin gauche descend
          it.line(sx + 3, sy + 3, sx + 5, sy + 5); // Coin droit descend
        }

      
      // GRAPHE (Remonté de 2 pixels : 30 au lieu de 32, 61 au lieu de 63)
      // g_top passe de 30 à 24 (on gagne 6 pixels de hauteur !)
      const int g_top = 24 + oy; 
      const int g_bottom = 61 + oy;
      const int g_left = 0 + ox;
      const int g_width = 127; 

      auto map_co2 = [&](float v) {
        if (v < 400) v = 400;
        if (v > 2000) v = 2000;
        return g_bottom - (int)((v - 400) * (g_bottom - g_top) / (2000 - 400));
      };

      // 1. DESSIN DES LIGNES DE RÉFÉRENCE (Pointillés)
      int y_1000 = map_co2(1000);
      int y_2000 = map_co2(2000);

      for (int x = 0; x <= 127; x += 6) {
        it.draw_pixel_at(x + ox, y_1000);
        it.draw_pixel_at(x + ox, y_2000);
      }

      // 2. TRACÉ DE L'HISTORIQUE
      int idx0 = id(co2_index);
      float first_val = id(co2_history)[idx0];
      int prev_y = map_co2(isnan(first_val) ? 400 : first_val);

      for (int i = 1; i < 120; i++) {
        int idx = (idx0 + i) % 120;
        float val = id(co2_history)[idx];
        if (isnan(val)) continue;
        
        int x_prev = (i - 1) * 127 / 119;
        int x_curr = i * 127 / 119;
        int y = map_co2(val);
        
        it.line(x_prev + ox, prev_y, x_curr + ox, y);
        prev_y = y;
      }

      // 3. AXE X ET REPERES
      it.line(0 + ox, g_bottom, 127 + ox, g_bottom); // Axe horizontal
      it.line(0 + ox, g_top, 3 + ox, g_top);          // Cran 2000
      it.line(0 + ox, y_1000, 3 + ox, y_1000);       // Cran 1000

      // 4. MARQUAGE DES HEURES (Sous l'axe X)
      // On place un point tous les 10 échantillons (10 * 6 min = 1 heure)
      // i=0 est le point le plus vieux, i=119 le plus récent
      for (int i = 9; i < 120; i += 10) {
          int x_hour = i * 127 / 119;
          it.draw_pixel_at(x_hour + ox, g_bottom + 2); 
      }
      }

font:
  - file: "fonts/Roboto-Bold.ttf"
    id: font_big
    size: 24
  - file: "fonts/Roboto-Regular.ttf"
    id: font_small
    size: 10

Best regards,

Fabien

Nice,
you’re welcome!