{"id":20455656,"url":"https://github.com/johnbigeon/cbe_co2","last_synced_at":"2026-05-03T21:32:25.185Z","repository":{"id":170580149,"uuid":"646746718","full_name":"JohnBigeon/CBE_CO2","owner":"JohnBigeon","description":null,"archived":false,"fork":false,"pushed_at":"2023-05-29T08:51:15.000Z","size":3387,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-08-02T13:08:32.824Z","etag":null,"topics":["co2-sensor","co2monitor","esp32","htm","micropython","plotly","websocket"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JohnBigeon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-29T08:47:36.000Z","updated_at":"2023-05-29T08:52:06.000Z","dependencies_parsed_at":null,"dependency_job_id":"fe69ffdb-2a34-45fd-a26f-68d20b801842","html_url":"https://github.com/JohnBigeon/CBE_CO2","commit_stats":null,"previous_names":["johnbigeon/cbe_co2"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/JohnBigeon/CBE_CO2","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnBigeon%2FCBE_CO2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnBigeon%2FCBE_CO2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnBigeon%2FCBE_CO2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnBigeon%2FCBE_CO2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JohnBigeon","download_url":"https://codeload.github.com/JohnBigeon/CBE_CO2/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JohnBigeon%2FCBE_CO2/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32586187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["co2-sensor","co2monitor","esp32","htm","micropython","plotly","websocket"],"created_at":"2024-11-15T11:19:30.173Z","updated_at":"2026-05-03T21:32:25.169Z","avatar_url":"https://github.com/JohnBigeon.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CBE_CO2\n\n\u003cp float=\"center\"\u003e\n  \u003cimg src=\"https://github.com/JohnBigeon/CBE_CO2/blob/main/Pictures/plot.png\" /\u003e\n\u003c/p\u003e\n\n\n## Introduction\nHere, we will build/assemble a device to measure environmental parameters such as:\n- temperature, \n- CO2 concentration,\n- Volatile Organic Compounds (VOC) cocentration\n- Temperature\n- Humidity\n- Pression\n\nThis device consists of a microcontroller (ESP32), a BME280 chip and a CCS811 chip.\n\n## Concept\n\nFrom a practical view, the environmental information is obtained by a the small chip and then saved on a micro SD-card. The remote access is achieved using the websocket protocol.\n\n## Hardware \u0026 Integration\n### Wiring\n\n```\nESP32        BME280    CCS811    SD card reader\n----------   -------   --------  --------------\n       3V3 - Vin     - Vcc\n       GND - GND     - GND      - GND\n        V5 -         -          - Vcc\n       G23 -         -          - MOSI\n       G22 -         - SCL\n       G21 -         - SDA\n       GND -         - WAK\n       G19 -         -          - MISO\n       G18 -         -          - SCK\n        G5 -         -          - CS\n       G33 - SDA\n       G32 - SCL \n```\n\n![KiCad](https://github.com/JohnBigeon/CBE_CO2/blob/main/KiCad_files/schematic.png)\n\n![PCB](https://github.com/JohnBigeon/CBE_CO2/blob/main/KiCad_files/pcb.png)\n\n### First integration\n![Integration](https://github.com/JohnBigeon/CBE_CO2/blob/main/Pictures/integration_v01.jpg)\n\n### Price\n```\n         Object           Price (€)\n-----------------------   -----\n       AZDelivery ESP32 - 18\n                 BME280 - 0 \n                 CCS811 - 10\n                 Cables - 1\n```\n## Software\n## ESP32 with micropython\n### Flashing the ESP32 to install micropython\nOn Anaconda, create an anaconda environment and install esptool:\n```\nconda create --name pyesp32 python=3.9.13\nconda activate pyesp32\npip install esptool\n```\nCheck if everything is working:\n```\nesptool.py version\n```\nIf you are on Windows:\n```\nesptool version\n```\nTo check the version of your board:\nPress the boot button and enter the following command:\n```\nesptool.py chip_id\nesptool.py v4.4\nFound 2 serial ports\nSerial port /dev/ttyUSB0\nConnecting..............\nDetecting chip type... Unsupported detection protocol, switching and trying again...\nConnecting....\nDetecting chip type... ESP32\nChip is ESP32-D0WD-V3 (revision v3.0)\nFeatures: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme None\nCrystal is 40MHz\nMAC: XXX\nUploading stub...\nRunning stub...\nStub running...\nWarning: ESP32 has no Chip ID. Reading MAC instead.\nMAC: XXX\nHard resetting via RTS pin...\n```\n#### If ESP32 is not recognized\nUSB drivers missing\nhttps://www.silabs.com/documents/public/software/CP210x_Windows_Drivers.zip\n\nDownload the firmware on the website here [https://micropython.org/download/esp32-ota/].\n\n```\nesptool.py --port /dev/ttyUSB0 erase_flash\n```\n```\nesptool.py --chip esp32 --port /dev/ttyUSB0 write_flash -z 0x1000 esp32-20180511-v1.9.4.bin\n```\n\n## First connection\n### SD card\n\nTo be able to connect to the wifi, you should update the wifi credentials on the SD card as \n```\nssid = \u003cyour wifi SSID\u003e\npassword = \u003cpassword\u003e\n```\n\n\n### Websocket connection\nThe IP's address of your device is saved on the SD card in the file params.txt:\n```\nnetwork config: ('XXX.XXX.XXX.XX', ...)\n```\n\nThen, test the connection with the device using your laptop or smartphone:\n![Check_connection](https://github.com/JohnBigeon/CBE_CO2/blob/main/Pictures/record.png)\n\n### Full measurement\nMeasurement are saved on the SD card as:\n```\nDate(ISO 8601), Time(s), CO2 (ppm), VOC (ppb), Temp (degC), hum (%), pressure (hPa)\n2023-04-30T12:06:33.000Z, 736171593, 470, 9, 21.28, 50.13, 1009.03\n2023-04-30T12:06:35.000Z, 736171595, 470, 9, 21.28, 50.19, 1009.14\n2023-04-30T12:06:37.000Z, 736171597, 464, 9, 21.29, 50.15, 1009.11\n2023-04-30T12:06:39.000Z, 736171599, 476, 10, 21.28, 50.15, 1009.14\n2023-04-30T12:06:41.000Z, 736171601, 470, 12, 21.27, 50.15, 1009.11\n2023-04-30T12:06:43.000Z, 736171603, 486, 12, 21.28, 50.15, 1009.14\n2023-04-30T12:06:45.000Z, 736171605, 486, 10, 21.27, 50.1, 1009.03\n2023-04-30T12:06:47.000Z, 736171607, 486, 13, 21.27, 50.07, 1009.11\n2023-04-30T12:06:49.000Z, 736171609, 491, 13, 21.26, 50.08, 1009.11\n2023-04-30T12:06:51.000Z, 736171611, 496, 13, 21.26, 50.09, 1009.14\n2023-04-30T12:06:53.000Z, 736171613, 496, 14, 21.26, 50.1, 1009.14\n```\nThe date is following the ISO8601 format. \n## Micropython code\n### Main\n```\n# -*- coding: utf-8 -*-\n\"\"\"\nCreated on Sun Jan 22 13:13:42 2023\n\n@author: JBI\n\"\"\"\n\n\"\"\"\n###############################################\n##Title             : main.py\n##Description       : Main script for CBE-CO2 project\n##Author            : John Bigeon   @ Github\n##Date              : 20230122\n##Version           : Test with \n##Usage             : MicroPython (esp32-20220618-v1.19.1)\n##Script_version    : 0.0.5 (not_release)\n##Output            : \n##Notes             :\n###############################################\n\"\"\"\n###############################################\n### Package\n###############################################\nfrom microWebSrv import MicroWebSrv\nimport time\nimport machine\nimport utime\nfrom utime import localtime\nimport ntptime\nimport network\nimport socket\nfrom machine import Pin, I2C, SoftI2C, SDCard\nimport CCS811\nimport json\nimport uos\nimport os\nimport re\nimport BME280\n\n\n###############################################\n### Function: JSON\n###############################################\ndef importFile_to_JSON(file):\n    max_len = 27  # maximum number of lines in content variable\n    timus, dat_CO2, dat_VOC, dat_temp, dat_hum, dat_pres = [], [], [], [], [], []\n\n    with open(file, 'r') as fp:\n        num_lines = sum(1 for line in fp)  # count the total number of lines in the file\n        step = max(1, num_lines // max_len)  # calculate the step value based on the file length\n        fp.seek(0)  # reset the file pointer to the beginning of the file\n\n        for i, line in enumerate(fp):\n            if i % step == 0 and i \u003e 0:  # check if the current line should be extracted\n                tempList = line.strip().split(',')\n                timus.append(str(tempList[0]))\n                #timus_sec.append(int(tempList[1]))\n                dat_CO2.append(float(tempList[2]))\n                dat_VOC.append(float(tempList[3]))\n                dat_temp.append(float(tempList[4]))\n                dat_hum.append(float(tempList[5]))\n                dat_pres.append(float(tempList[6]))\n                if len(timus) \u003e= max_len:  # check if the maximum length has been reached\n                    break  # stop processing the file\n\n    # Remove dummy \n    if timus and timus[-1] == '':\n        timus.pop()\n    #if timus_sec and timus_sec[-1] == '':\n    #    timus_sec.pop()\n    if dat_CO2 and dat_CO2[-1] == '':\n        dat_CO2.pop()\n    if dat_VOC and dat_VOC[-1] == '':\n        dat_VOC.pop()\n    if dat_temp and dat_temp[-1] == '':\n        dat_temp.pop()\n    if dat_hum and dat_hum[-1] == '':\n        dat_hum.pop()\n    if dat_pres and dat_pres[-1] == '':\n        dat_pres.pop()\n\n    graph_to_send = json.dumps({'timus':timus, 'dat_CO2':dat_CO2, 'dat_VOC':dat_VOC, 'dat_temp':dat_temp, 'dat_hum':dat_hum, 'dat_pres':dat_pres})\n    return graph_to_send\n    \n    \n###############################################\n### Class: CCS811\n###############################################\nclass gaz_sensor:\n    def __init__(self):\n        self.i2c = SoftI2C(scl=Pin(22), sda=Pin(21))\n        self.sens = CCS811.CCS811(i2c=self.i2c, addr=90) # Adafruit sensor breakout has i2c addr: 90; Sparkfun: 91\n\n    def read(self):\n        while True:\n            try:\n                if self.sens.data_ready():\n                    return {'val_CO2': self.sens.eCO2, 'val_VOC': self.sens.tVOC}\n            except:\n                pass\n\n###############################################\n### Class: BME280\n###############################################          \nclass env_sensor:\n    def __init__(self):\n        self.i2c = SoftI2C(scl=Pin(32), sda=Pin(33), freq=10000)\n        self.bme = BME280.BME280(i2c=self.i2c, address=118)\n\n    def read(self):\n        temp = float(self.bme.temperature)\n        hum = float(self.bme.humidity)\n        pres = float(self.bme.pressure)\n        return {'val_temp': temp, 'val_hum': hum, 'val_pres' : pres}\n    \n###############################################\n### Function: Websocket\n############################################### \n# Inspired by http://staff.ltam.lu/feljc/electronics/uPython/uPy_WiFi_03_websockets.pdf\ndef _acceptWebSocketCallback(webSocket, httpClient) :\n    print(\"WS ACCEPT\")\n    webSocket.RecvTextCallback = _recvTextCallback\n    ## webSocket.RecvBinaryCallback = _recvBinaryCallback\n    webSocket.ClosedCallback = _closedCallback\n    \ndef _recvTextCallback(webSocket, msg) :\n    if msg == \"LEDon\":\n        d12.on()\n        \n    elif msg  == \"plot\":\n        webSocket.SendText(importFile_to_JSON(fname))\n        \n    elif msg == \"Get\":\n        with open(fname, \"r\") as my_file:\n            # read first line\n            header = my_file.readline()\n\n            # read last 200 lines\n            footer = \"\"\n            my_file.seek(0, 2) # go to the end of the file\n            file_size = my_file.tell() # get the file size in bytes\n            pos = max(file_size-900, 0)\n            my_file.seek(pos, 0) # seek to the end minus 200 lines\n            while pos \u003c file_size:\n                line = my_file.readline()\n                if not line:\n                    break # end of file reached\n                footer += line\n                pos = my_file.tell()\n\n        print('****************************')\n        header = \"\u003cbr /\u003e\".join(header.split(\"\\n\")) # replace \\n for html communication\n        footer = \"\u003cbr /\u003e\".join(footer.split(\"\\n\")) # replace \\n for html communication\n        print('****************************')\n        webSocket.SendText(\"%s\" % header)\n        webSocket.SendText(\"%s\" % footer)\n        \n    else:\n        print('*')\n        print(\"WS RECV TEXT : %s\" % msg)\n        webSocket.SendText(\"Reply for %s\" % msg)\n        \ndef _closedCallback(webSocket) :\n    print(\"WS CLOSED\")\n\ndef printtime():\n    return('{:04d}-{:02d}-{:02d}T{:02d}:{:02d}:{:02d}.000Z'.format(localtime()[0], localtime()[1], localtime()[2], localtime()[3], localtime()[4], localtime()[5]))\n \ndef save_data(fname, msg):\n    with open(fname,'a+') as f:\n        f.write(msg + '\\n')\n    f.close()\n\n###############################################\n### Prerequisites\n###############################################\n# Mount the SD card\nif '/sd' not in os.listdir():\n    sdcard=machine.SDCard(slot=2, sck=18, miso=19, mosi=23, cs=5, freq=19000000)\n    sdcard.info()\n    os.mount(sdcard, \"/sd\")\n    print(\"SD card mounted successfully!\")\nelse:\n    print(\"SD card already mounted!\")\n\n# Extract wifi ssid and password from sdcard\nwifi_credentials_loc = open(\"/sd/wifi_credentials.dat\")\nwifi_credentials = wifi_credentials_loc.read().split(\"\\n\")\nwifi_credentials_ssid = wifi_credentials[0].split(\" = \",1)[1]\nwifi_credentials_password = wifi_credentials[1].split(\" = \",1)[1]\n#wifi_credentials_username = wifi_credentials[2].split(\" = \",1)[1]\n\n# Configure the ESP32 wifi\nsta = network.WLAN(network.STA_IF)\nif not sta.isconnected():\n    print('connecting to network...')\n    sta.active(True)\n    sta.connect(wifi_credentials_ssid, wifi_credentials_password)\n    count = 0 # initialize the counter variable\n    while not sta.isconnected() and count \u003c 25: # add the counter variable and conditional statement\n        print('Not connected yet')\n        count += 1 # increment the counter variable\n        time.sleep(1)\n        pass\n\n    if sta.isconnected():\n        print('Connected to network')\n        print(sta.ifconfig())\n    else:\n        print('Failed to connect to network')\n\n\n###############################################\n### Init\n###############################################\n### Update timer\nif sta.isconnected():\n    # Set the time using NTP\n    ntptime.settime()\n\n    # Get the current UTC time\n    utc_time = machine.RTC().datetime()\n\n    # Add 1 hour to the UTC time to adjust for the Belgium time zone\n    belgium_time = utc_time[0], utc_time[1], utc_time[2], utc_time[3], utc_time[4] + 1, utc_time[5], utc_time[6], utc_time[7]\n\n    # Set the adjusted time\n    machine.RTC().datetime(belgium_time)         \n    \n### Name of the file   \ndatus = '{:02d}{:02d}{:02d}{:02d}{:02d}{:02d}'.format(localtime()[0], localtime()[1], localtime()[2], localtime()[3], localtime()[4], localtime()[5])\nfname = '/sd/measure_'+ datus +'.txt'\ncsvdata = []\n\n###############################################\n### Debug\n###############################################\nd12 = Pin(12, Pin.OUT)\n\n###############################################\n### Microweb\ndef connect_ws():\n    print(\"Preparing server\")\n    srv = MicroWebSrv(webPath='www/')\n    srv.WebSocketThreaded = True\n    srv.AcceptWebSocketCallback = _acceptWebSocketCallback\n    print(\"Starting server\")\n    srv.Start(threaded = True)\n    \n###############################################\n### Main\n###############################################\nif __name__ == \"__main__\":\n    if sta.isconnected():\n        connect_ws()\n        params_used = 'network config: %s' %str(sta.ifconfig())\n        save_data('/sd/params.txt', params_used)\n    utime_start = utime.time()\n    \n    gaz_sens = gaz_sensor()\n    env_sens = env_sensor()\n    \n    save_data(fname, 'Date(ISO 8601), Time(s), CO2 (ppm), VOC (ppb), Temp (degC), hum (%), pressure (hPa)')\n\n    try:\n        while True:\n            val = str(\"{}, {}, {}, {}, {}, {}, {}\".format(printtime(), utime.time(), gaz_sens.read()['val_CO2'], gaz_sens.read()['val_VOC'], env_sens.read()['val_temp'], env_sens.read()['val_hum'], env_sens.read()['val_pres']))\n            print(val)\n            save_data(fname, val)\n            time.sleep(1)\n    except KeyboardInterrupt:\n        pass\n```\n\n### Well-known bugs\n#### \n    \n### Future improvements\n* Use battery as power supply ?\n* Use a Bluetooth module to replace the wired connection for transmitting serial data.\n* Not able to connect to Wifi requiring a login and a password.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnbigeon%2Fcbe_co2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjohnbigeon%2Fcbe_co2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjohnbigeon%2Fcbe_co2/lists"}