4396

A cheap Traffic-Light-style gas detector (ESP32 + CCS811 + 3 LEDs) that gives an indication of when to open the windows in a room with a lot of exhaled and thus possibly polluted air.

The intention of this project was to have a simple device that gives an indication of when to open the windows in a (class)room because of exhaled and possibly polluted air.

I have used a cheap CCS811 sensor in combination with an ESP32  . This sensor does not directly measure the CO2 content, but in particular the amount of particles exhaled by the human lungs. From this, the sensor indirectly calculates an eTVOC and eCO2 value.

The CCS811 requires a one-time BURN-IN time of at least 48 hours and a regular RUN-IN time of 20 minutes after start before reliable values can be output.

The sensor assumes that the lowest eCO2 value measured in a longer interval (at the same temperature and humidity) corresponds to "clean" air in an in-between well-ventilated room with about 400ppm CO2.
This highly scattering baseline value is also dependent on various environmental factors. It is determined dynamically in each case, as the sensor exhibits principle-related short-term and long-term changes in sensitivity. However, the simple and cheap sensor itself does not store this baseline value.

I have tried to take this behaviour into account in my code.
The code is written in ANNEX32,  a BASIC-programming language  for ESP32   
Find  its documentation HERE.

Some more details about this project are HERE

Helpful links for me:
https://www.sciosense.com/products/environmental-sensors/ccs811-gas-sensor-solution/
https://cicciocb.com/annex32help/V1.508/
https://flasher.cicciocb.com/dist/index.html

The ANNEX32-Code:

'#######################################################################
' CO2-TRAFFIC-LIGHT
' ##################
'Shows AIR QUALITY with
' - CCS811 eCO2-TVOC-Sensor at I2C-pins of an ESP32
' - 3 LEDs (green, yellow, red to show the range of CO2 ppm
' - saves the 2 BASELINE-bytes  in /BASELINE1.txt and /BASELINE2.txt 

' - SAVE OF BASELINE  after 20 Minutes in an unpolluted environment 
'     as a kind of calibration   if variable BASELINE_SAVE = 1
' - AUTOMATED RESTORE OF SAVED BASELINE  to avoid taking a baseline 
'     in polluted evir  if variable RESTORE_BASELINE = 1

'to do:
' - Webinterface with CO2 ppm   and TVOV ppb and settings 
' - BEEP if poor quality for more than  X seconds
' - eMail if poor quality for more than  X seconds

'#######################################################################
' DB9JG@me.com
Version$           = "V1.4"

'------SETTINGS-------------
SAVE_BASELINE      = 0    '1 => saves the two baseline-bytes to data file  after 20 Minutes of run-in regularly
RESTORE_BASELINE   = 1    '1 => restores the two baseline-bytes from data file after 20 Minutes of run-in regularly

SILENT             = 1    '1 => suppress  all messages   0 => show some status-messages via  wlog

LL_GREEN            = 400  'lower limit of the range for the green LED 
LL_Yellow           = 500  'lower limit of the range for the yellow LED
LL_RED              = 600  'lower limit of the range for the red LED
'---------------------------

SDA_PIN            = 21   'SDA=21 at ESP32
SCL_PIN            = 22   'SCL=22 at ESP32
CCS811temp         = 0
STATUS_REG         = &h00
MEAS_MODE_REG      = &h01
ALG_RESULT_DATA    = &h02
ENV_DATA           = &h05
NTC_REG            = &h06
THRESHOLDS         = &h10
BASELINE           = &h11
HW_ID_REG          = &h20
ERROR_ID_REG       = &hE0
APP_START_REG      = &hF4
SW_RESET           = &hFF
CCS811_I2C_ADR     = &h5A
GPIO_WAKE          = &h5
DRIVE_MODE_IDLE    = &h0
DRIVE_MODE_1SEC    = &h10
DRIVE_MODE_10SEC   = &h20
DRIVE_MODE_60SEC   = &h30
INTERRUPT_DRIVEN   = &h8
THRESHOLDS_ENABLED = &h4
BASELINE1$         = "??"
BASELINE2$         = "??"
count              = 0
Dim BUFFER(10)  'I2C-Sende- / Empfangspuffer.

LED_RED            = 14 'GPIOs for LEDs
LED_YELLOW         = 13
LED_GREEN          = 12

pin.mode LED_RED,     OUTPUT
pin.mode LED_YELLOW,  OUTPUT
pin.mode LED_GREEN,   OUTPUT

PIN(LED_RED)    = 1   'show that the device is working
PIN(LED_YELLOW) = 1   'show that the device is working
PIN(LED_GREEN)  = 1   'show that the device is working



I2C.SETUP SDA_PIN, SCL_PIN      ' set I2C ports
if not silent GOSUB I2C_SCANNER ' show all I2C-bus-devices

GOSUB SHOW_CCS811_STATUS        ' show Status-byte

GOSUB CCS811_GO_APP_MODE         'leave BOOT-Mode, start APP-Mode
GOSUB SHOW_CCS811_STATUS

GOSUB CCS811_SET_DRIVE_MODE      'CCS811 generates eCO2 and TVOC once per second
GOSUB SHOW_CCS811_STATUS

'save  baseline to files regularly after min 20Min of  run-in !! IN CLEAN AIR CONDITION !!
if SAVE_BASELINE    then TIMER0 (20*60000), CCS811_SAVE_BASELINE_TO_FILE  

'restore baseline from files regularly after min 20Min of  run-in
if RESTORE_BASELINE then TIMER0 (5*60000), CCS811_RESTORE_BL_FROM_FILE

TIMER1 1050, CCS811_SHOW   'read and show  eCO2 and TVOC from CCS811 sensor 

WAIT

end '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
'!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!



'######################################################################
CCS811_SHOW:

GOSUB CCS811_READ_eCO2
gosub SET_CO2_TRAFFIC_LIGHT
'wlog "eCO2 = ";eCO2,"TVOC = ";TVOC

if count = 0 then
  '  gosub CCS811_SAVE_BASELINE_TO_FILE  'TEST ONLY---------------------
  GOSUB CSS811_READ_BASELINE  
  wlog time$, "eCO2 = ";eCO2,"TVOC = ";TVOC ,"BASELINE: ";BASELINE1$, BASELINE2$   
end if
count = (count +1) mod 30 
'count = (count +1) mod 60 ' 60 * 1 Sekunden
'count = (count +1) mod 300 ' x * 1 Sekunden !!!!!TEST!!!!!!!!!!!!!!!!!
return

'######################################################################
CCS811_READ_eCO2:

' Bit 3 = Data Ready 0:no new Data 1:new Data
' Bit 0 Error dedection 0:no Error 1: Error on i2c or Sensor
'''''''''''''''''''''''''''''i2c.writeRegByte CCS811_I2C_ADR, STATUS_REG , 0
i2c.writeRegByte CCS811_I2C_ADR, STATUS_REG , 0
i2c.ReqFrom CCS811_I2C_ADR,1
CCS811temp = i2c.read

If CCS811temp and 8 then                               'DATA_available
  i2c.writeRegByte CCS811_I2C_ADR, ALG_RESULT_DATA ,0  'Select the Result mailbox
  i2c.ReqFrom CCS811_I2C_ADR, 8
  for i = 1 to 8                                       'The Result mailbox contains  8 Bytes
    BUFFER(i)  = i2c.read
  next
  eCO2 = 256*BUFFER(1) + BUFFER(2)    'eCO2 ppm
  TVOC = 256*BUFFER(3) + BUFFER(4)    'eTVOC 
else
  eCO2 = 0
  TVOC = 0
end if
return

'######################################################################
CSS811_READ_BASELINE:

'read 2 bytes from  baseline-mailbox
if not silent wlog "Read the two baseline bytes:"
i2c.begin CCS811_I2C_ADR
I2c.write BASELINE
i2c.end
i2c.ReqFrom CCS811_I2C_ADR, 2        'Two bytes from baseline mailbox 
BASELINE1$  = hex$(i2c.read)
BASELINE2$  = hex$(i2c.read)
if not silent wlog " ",BASELINE1$,BASELINE2$
return


'######################################################################
CCS811_SAVE_BASELINE_TO_FILE:

GOSUB CSS811_READ_BASELINE
File.save "/BASELINE1.txt", BASELINE1$
File.save "/BASELINE2.txt", BASELINE2$
if not silent wlog "Saved baseline-byte1 ";BASELINE1$ ; " to file /BASELINE1.txt"
if not silent wlog "Saved baseline-byte2 ";BASELINE2$ ; " to file /BASELINE2.txt"
return

'######################################################################
CCS811_RESTORE_BL_FROM_FILE:

'restore  2 bytes from Datafile to  baseline-mailbox
if not silent wlog "Restore the two baseline bytes:"
if file.exists("/BASELINE1.txt") then  BASELINE1$ = File.read$("/BASELINE1.txt")
if file.exists("/BASELINE2.txt") then  BASELINE2$ = File.read$("/BASELINE2.txt")

'BASELINE1$  = "09"  'TEST
'BASELINE2$  = "81"  'TEST

i2c.begin CCS811_I2C_ADR
I2c.write BASELINE               ' select baseline-mailbox
I2c.write val("&h" + BASELINE1$) ' write byte #1 to baseline-mailbox
I2c.write val("&h" + BASELINE2$) ' write byte #2 to baseline-mailbox
i2c.end

if not silent wlog "Restored baseline-byte1 ";BASELINE1$ ; " from file /BASELINE1.txt"
if not silent wlog "Restored baseline-byte2 ";BASELINE2$ ; " from file /BASELINE2.txt"
GOSUB CSS811_READ_BASELINE
return

'######################################################################
SET_CO2_TRAFFIC_LIGHT:

SELECT CASE eCO2
    
  CASE LL_GREEN to (LL_YELLOW -1) :    'GREEN range
    PIN(LED_GREEN)  = 1 - PIN(LED_GREEN)
    PIN(LED_RED)    = 0
    PIN(LED_YELLOW) = 0
    
  CASE LL_YELLOW to (LL_RED -1)  :      'YELLOW range
    PIN(LED_YELLOW)  = 1 - PIN(LED_YELLOW)
    PIN(LED_RED)    = 0
    PIN(LED_GREEN) = 0

  CASE LL_RED to 9999 :                 'RED range
    PIN(LED_RED)  = 0
    pause 200
    PIN(LED_RED)  = 1 - PIN(LED_RED)
    PIN(LED_GREEN)  = 0
    PIN(LED_YELLOW) = 0

  CASE ELSE:
    PIN(LED_RED)    = 1
    PIN(LED_YELLOW) = 1
    PIN(LED_GREEN)  = 1
END SELECT
return

'######################################################################
SHOW_CCS811_STATUS:

if silent return
wlog ""
wlog "-------------CCS811-Sensor_Status:-------------"
'Read the status-byte
i2c.writeRegByte CCS811_I2C_ADR, STATUS_REG , 0
i2c.ReqFrom CCS811_I2C_ADR,1
CCS811temp = i2c.read

wlog "STATUSBYTE  :",CCS811temp,"= &b"; bin$(CCS811temp)

If CCS811temp and 128 then
  wlog "  CCS811 is in Application Mode"
else
  wlog "  CCS811 is in Boot Mode"
end if
If CCS811temp and 16 then
  wlog "  A valid firmware is loaded"
else
  wlog "  NO valid firmware loaded!"
end if
If CCS811temp and 8 then
  wlog "  DATA is available to read"
else
  wlog "  NO DATA available to read"
end if 
If CCS811temp and 1 then
  wlog "  Error on i2c or sensor!!"
  'Read the ERROR-ID only if error bit is set
  i2c.writeRegByte CCS811_I2C_ADR, ERROR_ID_REG , 0
  i2c.ReqFrom CCS811_I2C_ADR,1
  CCS811temp = i2c.read
  wlog "     ERROR_ID_REG : dec",CCS811temp,"= &b";bin$(CCS811temp)
else
  wlog "  NO Error detected"
end if
wlog "----------------------------------------"
wlog " "
RETURN

'######################################################################
CCS811_GO_APP_MODE:

if not silent wlog "Change from BOOT-Mode to  App Mode"
i2c.begin CCS811_I2C_ADR
i2c.write APP_START_REG
i2c.end
return

'######################################################################
CCS811_SET_DRIVE_MODE:

'Bit7=0reserved, 'Bit 6-4=001 read data 1 per sec
'3=0 Interrupt off, 2=0 Interrupt Mode Normal,  1-0 =00 Reserved
if not silent then 
 wlog "Set MEAS_MODE_REG to &b:"; bin$(DRIVE_MODE_1SEC); " = read new data once per 1 second"
end if
i2c.writeRegByte CCS811_I2C_ADR, MEAS_MODE_REG, DRIVE_MODE_1SEC
return

'######################################################################
I2C_SCANNER:

'I2C Address Scanner
'wlog  all addresses of the I2C-devices found

'I2C.SETUP SDA_PIN, SCL_PIN    ' set I2C ports
wlog  "---start-I2C-scan---"
for i = 0 to 120
  i2c.begin i
  if i2c.end = 0 then
    wlog  "Found a device at I2C-Adr dec "; i ,", hex "; hex$(i)
    pause 10
  end if
next i
wlog  "---end-I2C-scan---"
return