Air Quality Sensor

Hi All,

I am not a coder, and I am trying to get a grasp of this.

I built an Air Quality sensor for my Garage to monitor 3D printing emissions.

I used this rather excellent video I made it easier to measure your 3D printer’s emissions!, and I can see it and all the sensors in HA. But the screen is just white.

I can see it within the ESP Builder.

Whilst doing some research, it may be a pin configuration issue, but I am unsure how to resolve this.

From HA, I can access the Web UI

I can also toggle the Backlight on/off from the WebUI.

backlight

The Script can be downloaded from 3D Printables. I am also the latest PCB from the project.

I’ve also added it below.

# Sensorbox v2 config for PCB SBR1
# config r2 (20241130)
# https://go.toms3d.org/sbr1
# Leave the "substitutions" block in place, then add this config.
# You can choose a new "friendly name" in the substitutions, this will be its display name in Home Assistant.

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  min_version: 2024.6.0
  name_add_mac_suffix: false
  platformio_options:
    board_build.flash_mode: dio
  project:
    name: esphome.web
    version: dev

esp32:
  board: esp32-s2-saola-1
  framework:
    type: arduino
# Enable logging
logger:

# Enable Home Assistant API and failsafe mechanisms
api:

# Allow Over-The-Air updates
ota:
- platform: esphome

# Allow provisioning Wi-Fi via serial
improv_serial:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails, using your wifi's password
  ap:
    ssid: "Sensorbox v2"
    password: !secret wifi_password

# In combination with the `ap` this allows the user
# to provision wifi credentials to the device via WiFi AP.
captive_portal:

dashboard_import:
  package_import_url: github://esphome/firmware/esphome-web/esp32s2.yaml@main
  import_full_config: true

# To have a "next url" for improv serial
web_server:



debug:
  update_interval: 5s

# Display dimming
globals:
   - id: dbright
     type: int
     restore_value: no
     initial_value: '0'
     
output:
  - platform: ledc
    pin: GPIO15
    id: backlight_pwm

light:
  - platform: monochromatic
    output: backlight_pwm
    name: "Display Backlight"
    id: back_light
    restore_mode: ALWAYS_ON

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO13
      mode:
        input: true
        pullup: true
      inverted: true
    id: top_btn
    filters:
      - delayed_on: 10ms
      - delayed_off: 500ms
    on_press: 
      then:
        - lambda: |-
            switch(id(dbright)){
            case 0: id(dbright) = 3; id(back_light).turn_on().set_brightness(1.00).perform(); return;
            case 1: id(dbright) = 0; id(back_light).turn_off().perform();                     return;
            case 2: id(dbright) = 1; id(back_light).turn_on().set_brightness(0.05).perform(); return;
            case 3: id(dbright) = 2; id(back_light).turn_on().set_brightness(0.33).perform(); return;
            }

# /Display dimming




font:
  - file: "gfonts://Rubik@300"
    id: font_label_14
    size: 14
    bpp: 4
    glyphs: [
      "0","1","2","3","4","5","6","7","8","9", "µ", "³",  "/", "°", "%", ",", ".", "(", ")", "-", "_", A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z," ",a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z
      ]
  - file: "gfonts://Rubik@400"
    id: font_value_30
    size: 30
    bpp: 4
    glyphs: [
      "0","1","2","3","4","5","6","7","8","9", "."
      ]
  - file: "gfonts://Orbitron"
    id: font_heading_36
    size: 36
    bpp: 4
    glyphs: [
      "2",C,O,P,V,a,c,e,i,l,r,s,t
      ]


color:
  - id: white
    hex: FFFFFF
  - id: grey
    hex: AAAAAA
  - id: light_grey
    hex: CCCCCC
  - id: yellow
    hex: FFFA72
  - id: orange
    hex: FF9C32
  - id: red
    hex: FF1616
  - id: purple
    hex: FF16e0

display:
  - platform: ili9xxx    
    model: ILI9341
    cs_pin: GPIO03
    dc_pin: GPIO04
    reset_pin: GPIO06
    #data_rate: 10MHz
    transform:
      #mirror_x: true
      mirror_y: true
    #show_test_card: true
    color_order: bgr # CHANGE THIS if your screen has colors mapped wrong
    invert_colors: false
    lambda: |-
      it.fill(Color::BLACK);
      // Please forgive me. I know this is horrible spaghetti code, but for now it gets the job done. Will eventually move all this to LVGL. 
      ;
      auto cy = 0; // current Y position
      auto ix = 0; // current X grid position
      auto fv30 = id(font_value_30);
      auto fh36 = id(font_heading_36);
      auto fl14 = id(font_label_14);
      ;
      auto w = it.get_width();
      auto h = it.get_height();
      ;
      auto col = id(white);
      ;
      auto co2 = id(co2_scd40).state;
      auto e_co2 = id(eco2_ens160).state;
      auto s_co2 = id(eco2_sgp30).state;
      auto ch2o = id(ch2o_ze08).state;
      auto e_voc = id(tvoc_ens160).state;
      auto s_voc = id(tvoc_sgp30).state;
      auto pm1 = id(pm1_pms).state;
      auto pm25 = id(pm25_pms).state;
      auto pm10 = id(pm10_pms).state;
      auto aqi_voc = id(aqi_voc_sgp41).state;
      auto aqi_nox = id(aqi_nox_sgp41).state;
      ;
      ;
      ;
      // SET COLOR FOR EACH READING
      ;
      col = id(white);
      if (co2 > 800){col = id(yellow);}
      if (co2 > 1200){col = id(orange);}
      if (co2 > 1500){col = id(red);}
      if (co2 > 1800){col = id(purple);}
      auto co2_c = col;
      ;
      col = id(white);
      if (e_co2 > 800){col = id(yellow);}
      if (e_co2 > 1200){col = id(orange);}
      if (e_co2 > 1500){col = id(red);}
      if (e_co2 > 1800){col = id(purple);}
      auto e_co2_c = col;
      ;
      col = id(white);
      if (s_co2 > 800){col = id(yellow);}
      if (s_co2 > 1200){col = id(orange);}
      if (s_co2 > 1500){col = id(red);}
      if (s_co2 > 1800){col = id(purple);}
      auto s_co2_c = col;
      ;
      col = id(white);
      if (ch2o > 60){col = id(yellow);}
      if (ch2o > 100){col = id(orange);}
      if (ch2o > 150){col = id(red);}
      if (ch2o > 200){col = id(purple);}
      auto ch2o_c = col;
      ;
      col = id(white);
      if (e_voc > 400){col = id(yellow);}
      if (e_voc > 800){col = id(orange);}
      if (e_voc > 1400){col = id(red);}
      if (e_voc > 2000){col = id(purple);}
      auto e_voc_c = col;
      ;
      col = id(white);
      if (s_voc > 300){col = id(yellow);}
      if (s_voc > 600){col = id(orange);}
      if (s_voc > 1000){col = id(red);}
      if (s_voc > 1500){col = id(purple);}
      auto s_voc_c = col;
      ;
      col = id(white);
      if (pm1 > 5){col = id(yellow);}
      if (pm1 > 10){col = id(orange);}
      if (pm1 > 15){col = id(red);}
      if (pm1 > 25){col = id(purple);}
      auto pm1_c = col;
      ;
      col = id(white);
      if (pm25 > 5){col = id(yellow);}
      if (pm25 > 10){col = id(orange);}
      if (pm25 > 15){col = id(red);}
      if (pm25 > 25){col = id(purple);}
      auto pm25_c = col;
      ;
      col = id(white);
      if (pm10 > 15){col = id(yellow);}
      if (pm10 > 45){col = id(orange);}
      if (pm10 > 60){col = id(red);}
      if (pm10 > 80){col = id(purple);}
      auto pm10_c = col;
      ;
      ;
      // SELECT BEST AVAILABLE TEMPERATURE SENSOR
      float temps[5] = {id(temp_sht40).state, id(temp_aht20).state, id(temp_scd40).state, id(temp_aht20_ens).state, id(temp_bmp280).state};
      float hums[4] = {id(hum_sht40).state, id(hum_aht20).state, id(hum_scd40).state, id(hum_aht20_ens).state};
      float temp = 0;
      float hum = 0;
      for(int i = 0; i < 5; i++){
      if(!isnan(temps[i])){temp = temps[i]; break;}
      }
      for(int i = 0; i < 4; i++){
      if(!isnan(hums[i])){hum = hums[i]; break;}
      }
      ;
      // DRAW HUMITEMP
      it.printf(90, 30, fl14, id(grey), TextAlign::BOTTOM_LEFT, "°C");
      it.printf(90, 34, fv30, white, display::TextAlign::BOTTOM_RIGHT, "%.1f", temp);
      it.printf(197, 18, fl14, id(grey), TextAlign::BOTTOM_LEFT, "RH");
      it.print(197, 30, fl14, id(grey), TextAlign::BOTTOM_LEFT, "\%");
      it.printf(197, 34, fv30, white, display::TextAlign::BOTTOM_RIGHT, "%.1f", hum);
      ;
      ;
      // PREPARE VARIABLES
      auto title = "";
      auto unit = "";
      char *names[3];
      float values[3];
      esphome::Color colors[3];
      int count = 0;
      ;
      ;
      // PREPARE PARTICLES
      cy = 30;
      ;
      if (!isnan(pm1)){
      count = 0;
      title = "Particles";
      unit = "µg/m³";
      names[0] = "PM1";
      names[1] = "PM2.5";
      names[2] = "PM10"; //god C++ sucks
      values[0] = pm1;
      values[1] = pm25;
      values[2] = pm10;
      colors[0] = pm1_c;
      colors[1] = pm25_c;
      colors[2] = pm10_c;
      ;
      ;
      // START REUSABLE
      it.line(0,cy,w,cy,id(white));
      it.print(2, cy + 22, fh36, id(light_grey), TextAlign::CENTER_LEFT, title);
      it.print(w-2, cy + 31, fl14, id(grey), TextAlign::CENTER_RIGHT, unit);
      ix = 0;
      for (int i = 0; i < 3; i++){
      if(isnan(values[i])){continue;}
      it.printf(w/6*(1+ix*2), cy + 58, fl14, id(grey), TextAlign::BOTTOM_CENTER, "%s", names[i]);
      it.printf(w/6*(1+ix*2), cy + 50, fv30, colors[i], display::TextAlign::TOP_CENTER, "%.0f", values[i]);
      ix++;
      }
      // END REUSABLE
      ;
      cy += 88;
      }
      ;
      // END PARTICLES
      ;
      // PREPARE CO2
      ;
      if (!isnan(co2) or !isnan(e_co2) or !isnan(s_co2)){
      count = 0;
      title = "CO2";
      unit = "ppm";
      names[0] = "SCD40";
      names[1] = "ENS160 est.";
      names[2] = "SGP30 est.";
      values[0] = co2;
      values[1] = e_co2;
      values[2] = s_co2;
      colors[0] = co2_c;
      colors[1] = e_co2_c;
      colors[2] = s_co2_c;
      ;
      ;
      // START REUSABLE
      it.line(0,cy,w,cy,id(white));
      it.print(2, cy + 22, fh36, id(light_grey), TextAlign::CENTER_LEFT, title);
      it.print(w-2, cy + 31, fl14, id(grey), TextAlign::CENTER_RIGHT, unit);
      ix = 0;
      for (int i = 0; i < 3; i++){
      if(isnan(values[i])){continue;}
      it.printf(w/6*(1+ix*2), cy + 58, fl14, id(grey), TextAlign::BOTTOM_CENTER, "%s", names[i]);
      it.printf(w/6*(1+ix*2), cy + 50, fv30, colors[i], display::TextAlign::TOP_CENTER, "%.0f", values[i]);
      ix++;
      }
      // END REUSABLE
      ;
      ;
      cy += 88;
      }
      ;
      // END CO2
      ;
      // PREPARE VOC
      ;
      bool draw_general_voc = (!isnan(ch2o) or !isnan(e_voc) or !isnan(s_voc));
      bool draw_sgp4x = (!isnan(aqi_voc) or !isnan(aqi_nox));
      if (draw_general_voc or draw_sgp4x){
      count = 0;
      title = "VOC";
      unit = "ppb";
      names[0] = "SGP30";
      names[1] = "ENS160";
      names[2] = "ZE08-CH2O";
      values[0] = s_voc;
      values[1] = e_voc;
      values[2] = ch2o;
      colors[0] = s_voc_c;
      colors[1] = e_voc_c;
      colors[2] = ch2o_c;
      ;
      ;
      // START REUSABLE | UNIT CONDITION ADDED
      it.line(0,cy,w,cy,id(white));
      it.print(2, cy + 22, fh36, id(light_grey), TextAlign::CENTER_LEFT, title);
      if (draw_general_voc){it.print(w-2, cy + 31, fl14, id(grey), TextAlign::CENTER_RIGHT, unit);}
      ix = 0;
      for (int i = 0; i < 3; i++){
      if(isnan(values[i])){continue;}
      it.printf(w/6*(1+ix*2), cy + 58, fl14, id(grey), TextAlign::BOTTOM_CENTER, "%s", names[i]);
      it.printf(w/6*(1+ix*2), cy + 50, fv30, colors[i], display::TextAlign::TOP_CENTER, "%.0f", values[i]);
      ix++;
      }
      // END REUSABLE
      ;
      ;
      if (draw_general_voc){cy += 88;}
      else {cy += 46;}
      }
      // START SGP4x
      cy+=2;
      auto sgp4x = "SGP41";
      if (isnan(aqi_nox)){sgp4x = "SGP40";}
      if(!isnan(aqi_voc)){
      col = id(white);
      it.printf(2, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "%s VOC Index", sgp4x);
      it.printf(180, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "(1 ... 500)");
      it.printf(180, cy, fl14, col, TextAlign::CENTER_RIGHT, "%.0f ", aqi_voc);
      cy+=15;
      }
      ;
      if(!isnan(aqi_nox)){
      col = id(white);
      it.printf(2, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "%s NOx Index", sgp4x);
      it.printf(180, cy, fl14, id(grey), TextAlign::CENTER_LEFT, "(1 ... 500)");
      it.printf(180, cy, fl14, col, TextAlign::CENTER_RIGHT, "%.0f ", aqi_nox);
      }
      ;
      // END VOC & SGP4x
      ;
      ;
      ;
    dimensions:
      height: 320
      width: 240



uart:
  - tx_pin: GPIO10
    rx_pin: GPIO09
    baud_rate: 9600
    id: uart_ze08
    parity: none
    data_bits: 8
    stop_bits: 1
    debug: 
      direction: BOTH
      dummy_receiver: true
      after:
        # bytes: 9 
        timeout: 10ms
      sequence:
        - lambda: |-
            //UARTDebug::log_int(direction, bytes, ',');                 // Log the message as int. 
            //UARTDebug::log_hex(direction, bytes, ',');                 // Log the message in hex.
            ESP_LOGD("custom", "Bytes size: %d", bytes.size());
            if (direction == UART_DIRECTION_RX)                        
              {
                  if (bytes.size() == 9)                               
                    {
                        if ( bytes[0] == 0xFF &&                       // Check sensor identification
                            bytes[1] == 0x17
                            )       
                          {
                            float value = float((bytes[4] * 256) + bytes[5]);  // Decode message
                            id(ch2o_ze08).publish_state(value);     // Publish results to a sensor.
                          }
                    }
              }

  - tx_pin: GPIO08
    rx_pin: GPIO07
    baud_rate: 9600
    id: uart_pms

spi:
  clk_pin: GPIO01
  mosi_pin: GPIO02
  miso_pin: GPIO16

i2c:
  - sda: GPIO11
    scl: GPIO12
    scan: true
    id: i2c_main

  - sda: GPIO17
    scl: GPIO18
    scan: true
    id: i2c_2

sensor:
  - platform: template
    name: "ZE08 CH2O"
    unit_of_measurement: ppb
    state_class: measurement
    device_class: volatile_organic_compounds
    id: ch2o_ze08
    filters: 
      - filter_out: 2000.0
      - exponential_moving_average: 
          send_every: 1
          alpha: 0.5

  - platform: uptime
    name: "Uptime"
    id: uptime_sensor
    update_interval: 5s
    disabled_by_default: True

  - platform: debug
    free:
      name: "Heap Free"
      id: heap_free
      disabled_by_default: True
    loop_time:
      name: "Loop Time"
      id: loop_time
      disabled_by_default: True

  - platform: aht10
    #address: 0x38
    i2c_id: i2c_main
    variant: AHT20
    temperature:
      name: "AHT20 Temperature"
      id: temp_aht20
    humidity:
      name: "AHT20 Humidity"
      id: hum_aht20
    update_interval: 10s

  - platform: bmp280_i2c
    address: 0x77
    i2c_id: i2c_main
    temperature:
      name: "BMP280 Temperature"
      id: temp_bmp280
      oversampling: 16x
    pressure:
      name: "BMP280 Pressure"
      id: pres_bmp280
    update_interval: 10s


  - platform: sht4x
    #addr 0x44
    i2c_id: i2c_main
    temperature:
      name: "SHT40 Temperature"
      id: temp_sht40
    humidity:
      name: "SHT40 Relative Humidity"
      id: hum_sht40
    update_interval: 10s

  - platform: scd4x
    #address: 0x62
    i2c_id: i2c_main
    co2:
      name: "SCD40 CO2"
      id: co2_scd40
    temperature:
      name: "SCD40 Temperature"
      id: temp_scd40
    humidity:
      name: "SCD40 Humidity"
      id: hum_scd40
    update_interval: 30s

  - platform: ens160_i2c
    address: 0x53
    i2c_id: i2c_2
    eco2:
      name: "ENS160 eCO2"
      id: eco2_ens160
    tvoc:
      name: "ENS160 TVOC"
      id: tvoc_ens160
      filters: 
        - exponential_moving_average: 
            send_every: 1
            alpha: 0.5
    aqi:
      name: "ENS160 Air Quality Index"
      id: aqi_ens160
    update_interval: 10s
    compensation:
      temperature: temp_aht20_ens
      humidity: hum_aht20_ens

  - platform: aht10
    #address: 0x38
    i2c_id: i2c_2
    variant: AHT20
    temperature:
      name: "AHT20/ENS160 Temperature"
      id: temp_aht20_ens
      disabled_by_default: True
    humidity:
      name: "AHT20/ENS160 Humidity"
      id: hum_aht20_ens
      disabled_by_default: True
    update_interval: 10s

  - platform: sgp30
    i2c_id: i2c_main
    address: 0x58
    eco2:
      name: "SGP30 eCO2"
      id: eco2_sgp30
      accuracy_decimals: 0
      filters: 
        - exponential_moving_average: 
            send_every: 1
            alpha: 0.2
    tvoc:
      name: "SGP30 TVOC"
      id: tvoc_sgp30
      accuracy_decimals: 1
      filters: 
        - exponential_moving_average: 
            send_every: 1
            alpha: 0.2
    store_baseline: yes
    update_interval: 5s
    compensation: 
      humidity_source: hum_aht20
      temperature_source: temp_aht20

  - platform: sgp4x
    i2c_id: i2c_main
    #address: 0x59
    voc:
      name: "SGP41 VOC Index"
      id: aqi_voc_sgp41
      icon: "mdi:air-filter"
      device_class: aqi
      state_class: measurement
      unit_of_measurement: "index points"
    nox:
      name: "SGP41 NOx Index"
      id: aqi_nox_sgp41
      icon: "mdi:air-filter"
      device_class: aqi
      state_class: measurement
      unit_of_measurement: "index points"
    compensation: 
      humidity_source: hum_aht20
      temperature_source: temp_aht20
    store_baseline: True
    update_interval: 10s

  - platform: pmsx003
    type: PMSX003 
    update_interval: 120s
    uart_id: uart_pms
    pm_1_0:
      name: "PMS Particulate Matter <1.0µm Concentration"
      id: pm1_pms
    pm_2_5:
      name: "PMS Particulate Matter <2.5µm Concentration"
      id: pm25_pms
    pm_10_0:
      name: "PMS Particulate Matter <10.0µm Concentration"
      id: pm10_pms

I’m unsure where to go with this?

Does your device have PSRAM? If it’s the WROVER variant it should have.

Two things to try:

  1. Add:
psram:
  

to your config and re-install.

  1. If that doesn’t work (i.e you don’t have PSRAM) then add this to the display component:
    color_palette: 8BIT

and remove psram:

HI,

I sure when I ordered it, it stated PSRAM.

The description was ’ ESP32 S2 Mini v1.0.0 - LOLIN WIFI IOT Board based ESP32-S2FN4R2 (ESP32-S2 4MB Flash 2MB PSRAM) MicroPython for Arduino.

I guess add that within this section?

esp32:
  board: esp32-s2-saola-1
  framework:
    type: arduino

Thanks

I added psram: near the top and the screen started working.

Thanks for your help.