tf
Published © GPL3+

Personal IAQ Monitor

A simple personal indoor air quality (IAQ) monitor device based on the Nordic Semiconductor nRF5340 Development Kit.

IntermediateFull instructions provided3.14159265359 hours801

Things used in this project

Hardware components

nRF5340 Development Kit
Nordic Semiconductor nRF5340 Development Kit
×1
Adafruit 2.8" TFT Touch Shield v2
×1
SparkFun Air Quality Breakout - CCS811
SparkFun Air Quality Breakout - CCS811
×1
SparkFun Atmospheric Sensor Breakout - BME280
SparkFun Atmospheric Sensor Breakout - BME280
×1
Breadboard (generic)
Breadboard (generic)
×1
Jumper wires (generic)
Jumper wires (generic)
×1
Nordic Semiconductor nRF52840 Dongle
Optional, only used for testing / demo.
×1
Raspberry Pi 3 Model B
Raspberry Pi 3 Model B
Optional, only used for testing / demo.
×1

Software apps and online services

nRF Connect SDK
Nordic Semiconductor nRF Connect SDK
Zephyr RTOS
Zephyr Project Zephyr RTOS
LVGL - Light and Versatile Graphics Library
Nordic Semiconductor nRF Connect for Mobile
Node-RED
Node-RED
Optional, only used for test & demo.

Story

Read more

Schematics

Wiring

Code

src/main.c

C/C++
#include <device.h>
#include <devicetree.h>
#include <drivers/sensor.h>
#include <drivers/sensor/ccs811.h>
#include <drivers/display.h>
#include <zephyr.h>
#include <stdio.h>
#include <string.h>
#include <lvgl.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>

#include "gui.h"
#include "iaq.h"

#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
#include <logging/log.h>
LOG_MODULE_REGISTER(app);

#define DEVICE_NAME CONFIG_BT_DEVICE_NAME
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)

#define BME280 DT_INST(0, bosch_bme280)
#if DT_NODE_HAS_STATUS(BME280, okay)
#define BME280_LABEL DT_LABEL(BME280)
#else
#error The devicetree has no enabled nodes with compatible "bosch,bme280"
#define BME280_LABEL "<none>"
#endif

#define CCS811 DT_INST(0, ams_ccs811)
#if DT_NODE_HAS_STATUS(CCS811, okay)
#define CCS811_LABEL DT_LABEL(CCS811)
#else
#error The devicetree has no enabled nodes with compatible "ams_ccs811"
#define CCS811_LABEL "<none>"
#endif

#define CALIBRATION_TIME_SECONDS 20 // should be 20 minutes! ;)

/* Auxiliary function: Format time string.
*/
static const char *time_str(uint32_t time, bool with_millis)
{
	static char buf[16]; /* ...HH:MM:SS.MMM */
	unsigned int ms = time % MSEC_PER_SEC;
	unsigned int s;
	unsigned int min;
	unsigned int h;

	time /= MSEC_PER_SEC;
	s = time % 60U;
	time /= 60U;
	min = time % 60U;
	time /= 60U;
	h = time;
	if (with_millis)
		snprintf(buf, sizeof(buf), "%u:%02u:%02u.%03u",
				 h, min, s, ms);
	else
		snprintf(buf, sizeof(buf), "%u:%02u:%02u",
				 h, min, s);

	return buf;
}

static const char *now_str()
{
	return time_str(k_uptime_get_32(), true);
}

/* Bluetooth beacon setup ...
 * "stolen" from the Zephyr bluetooth beacon example 
 * (zephyr/samples/bluetooth/beacon/main.c).
 * Setup a non-connectable Eddystone beacon.
 * Later we will "abuse" the name data in the scan
 * resonse to transport our IAQ rating.
*/

	/*
	 * Set Advertisement data. Based on the Eddystone specification:
 	 * https://github.com/google/eddystone/blob/master/protocol-specification.md
 	 * https://github.com/google/eddystone/tree/master/eddystone-url
 	*/
static const struct bt_data ad[] = {
	BT_DATA_BYTES(BT_DATA_FLAGS, BT_LE_AD_NO_BREDR),
	BT_DATA_BYTES(BT_DATA_UUID16_ALL, 0xaa, 0xfe),
	BT_DATA_BYTES(BT_DATA_SVC_DATA16,
				  0xaa, 0xfe, /* Eddystone UUID */
				  0x10,		  /* Eddystone-URL frame type */
				  0x00,		  /* Calibrated Tx power at 0m */
				  0x00,		  /* URL Scheme Prefix http://www. */
				  'e', 'x', 'a', 'm', 'p', 'l', 'e',
				  0x08) /* .org */
};

	/* Set Scan Response data */
static const struct bt_data sd[] = {
	BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
	BT_DATA(BT_DATA_NAME_SHORTENED, DEVICE_NAME, DEVICE_NAME_LEN),
};

static void bt_ready(int err)
{

	char addr_s[BT_ADDR_LE_STR_LEN];
	bt_addr_le_t addr = {0};
	size_t count = 1;

	if (err)
	{
		printk("\n[%s]: Bluetooth init failed (err %d)\n", now_str(), err);
		return;
	}
	printk("\n[%s]: Bluetooth initialized\n", now_str());

	/* Start advertising */
	err = bt_le_adv_start(BT_LE_ADV_NCONN_IDENTITY, ad, ARRAY_SIZE(ad),
						  sd, ARRAY_SIZE(sd));
	if (err)
	{
		printk("\n[%s]: Advertising failed to start (err %d)\n", now_str(), err);
		return;
	}

	/* For connectable advertising you would use
	 * bt_le_oob_get_local().  For non-connectable non-identity
	 * advertising an non-resolvable private address is used;
	 * there is no API to retrieve that.
	 */

	bt_id_get(&addr, &count);
	bt_addr_le_to_str(&addr, addr_s, sizeof(addr_s));

	printk("\n[%s]: Beacon started, advertising as %s\n", now_str(), addr_s);
}

/* Auxiliary function to handle timing issues when fetching a
 * sample from the CCS811 sensor: Repeat sensor_sample_fetch
 * until valid data has been received.
*/
int ccs811_sample_fetch(struct device *dev)
{
	static bool first = true;
	static bool ccs811_fw_app_v2 = false;
	int rc;

	if (first)
	{
		struct ccs811_configver_type cfgver;
		rc = ccs811_configver_fetch(dev, &cfgver);
		if (rc == 0)
		{
			printk("\n[%s]: CCS811: HW %02x; FW Boot %04x App %04x ; mode %02x\n",
				   now_str(),
				   cfgver.hw_version, cfgver.fw_boot_version,
				   cfgver.fw_app_version, cfgver.mode);
			ccs811_fw_app_v2 = (cfgver.fw_app_version >> 8) > 0x11;
		}
		first = false;
	}

	rc = sensor_sample_fetch(dev);
	while (rc != 0)
	{
		const struct ccs811_result_type *rp = ccs811_result(dev);

		if (ccs811_fw_app_v2 && !(rp->status & CCS811_STATUS_DATA_READY))
		{
			printk("\n[%s]: CCS811: Stale data!\n", now_str());
			continue;
		}

		if (rp->status & CCS811_STATUS_ERROR)
		{
			printk("\n[%s]: CCS811: ERROR: %02x\n", now_str(), rp->error);
			break;
		}

		k_sleep(K_MSEC(10));
		rc = sensor_sample_fetch(dev);
	}

	return rc;
}

/*
 * Main application logic ...
*/
void main(void)
{
	/* General setup and initialization
	*/

	/* Setup sensor: BME280
	*/
	const struct device *bme280 = device_get_binding(BME280_LABEL);
	if (bme280 == NULL)
	{
		printk("No device \"%s\" found; Initialization failed?\n",
			   BME280_LABEL);
		return;
	}
	else
	{
		printk("Found device \"%s\"\n", BME280_LABEL);
		printk("Device is %p, name is %s\n", bme280, bme280->name);
	}

	/* Setup sensor: CCS811
	*/
	const struct device *ccs811 = device_get_binding(CCS811_LABEL);
	if (ccs811 == NULL)
	{
		printk("No device \"%s\" found; Initialization failed?\n",
			   CCS811_LABEL);
		return;
	}
	else
	{
		printk("Found device \"%s\"\n", CCS811_LABEL);
		printk("Device is %p, name is %s\n", ccs811, ccs811->name);
	}

	/* Setup and start Bluetooth beacon
	*/
	int bt_err;
	printk("\n[%s]: BT: Starting beacon ...\n", now_str());
	bt_err = bt_enable(bt_ready);
	if (bt_err)
	{
		printk("\n[%s]: BT: Initiialization failed (err %d)\n", now_str(), bt_err);
	}

	/* Setup GUI
	*/
	gui_setup();

	/* Forever ...
	*/
	while (1)
	{
		int rc = -1;
		bool valid_env_data_bme280 = false;
		bool valid_env_data_ccs811 = false;
		uint32_t now = k_uptime_get_32();
		int32_t calibration_time_remaining = CALIBRATION_TIME_SECONDS * MSEC_PER_SEC - now;

		struct sensor_value temp, press, humidity, co2, tvoc;

		/* Read sensor: BME280
		*/
		rc = sensor_sample_fetch(bme280);
		if (rc == 0)
		{
			valid_env_data_bme280 = true;

			/* Get sensor values for temperature, pressure and humidity
			*/
			sensor_channel_get(bme280, SENSOR_CHAN_AMBIENT_TEMP, &temp);
			sensor_channel_get(bme280, SENSOR_CHAN_PRESS, &press);
			sensor_channel_get(bme280, SENSOR_CHAN_HUMIDITY, &humidity);

			printk("\n[%s]: BME280: temp: %d.%06d; press: %d.%06d; humidity: %d.%06d\n",
				   now_str(),
				   temp.val1, temp.val2,
				   press.val1, press.val2,
				   humidity.val1, humidity.val2);

			/* Update the appropriate GUI elements
			*/
			gui_update_sensor_value(SENSOR_CHAN_AMBIENT_TEMP, temp);
			gui_update_sensor_value(SENSOR_CHAN_PRESS, press);
			gui_update_sensor_value(SENSOR_CHAN_HUMIDITY, humidity);

			/* Accurate calculation of gas levels requires accurate environment data. 
			 * Measurements are only accurate to 0.5 Cel and 0.5 RH.
			 * The CCS811 features an ENV_DATA register, which can be 'fed' with
			 * with the actual environmental data to improve the accuracy of the
			 * provided values for gas levels.
			*/
			rc = ccs811_envdata_update(ccs811, &temp, &humidity);
			if (rc == 0)
			{
				printk("\n[%s]: CCS811: Env data updated!\n", now_str());
			}
			else
			{
				printk("\n[%s]: CCS811: Failed to update env data!\n", now_str());
			}
		}
		else
		{
			valid_env_data_bme280 = false;
			printk("\n[%s]: BME280: Failed to fetch sensor data!\n", now_str());
		}

		/* Read sensor: CCS811
		*/
		rc = ccs811_sample_fetch(ccs811);
		if (rc == 0)
		{
			valid_env_data_ccs811 = true;

			/* Get sensor values for CO2 and VOC concentration
			*/
			sensor_channel_get(ccs811, SENSOR_CHAN_CO2, &co2);
			sensor_channel_get(ccs811, SENSOR_CHAN_VOC, &tvoc);

			printk("\n[%s]: CCS811: %u ppm eCO2; %u ppb eTVOC\n",
				   now_str(),
				   co2.val1,
				   tvoc.val1);

			/* Update the appropriate GUI elements
			*/
			gui_update_sensor_value(SENSOR_CHAN_CO2, co2);
			gui_update_sensor_value(SENSOR_CHAN_VOC, tvoc);
		}
		else
		{
			valid_env_data_ccs811 = false;
			printk("\n[%s]: CCS811: Failed to fetch sensor data!\n", now_str());
		}

		/* Calculate and display the IAQI rating
		*/

		/* If calibration time elapased and valid sensor readings are available ...
		*/
		if (calibration_time_remaining <= 0 && valid_env_data_bme280 && valid_env_data_ccs811)
		{
			/* Calculate the IAQI and update the GUI's meter component with the 'relative qualitity'
			 * and the IAQI rating.
			*/
			uint8_t iaq_index = get_iaq_index(temp.val1, humidity.val1, co2.val1, tvoc.val1);
			uint16_t quality = iaq_index * 100 / get_max_iaq_index();
			printk("\n[%s]: APP: IAQ index: %d (%d %%)\n", now_str(), iaq_index, quality);
			gui_update_qmeter(quality, get_iaq_rating(iaq_index));

			/* Update the scan reponse data for the Bluetooth beacon: 'Misuse' the name data for
			*  transporting the IAQI rating.
			*/
			struct bt_data new_sd[] = {
				BT_DATA(BT_DATA_NAME_COMPLETE, get_iaq_rating(iaq_index), strlen(get_iaq_rating(iaq_index))),
				BT_DATA(BT_DATA_NAME_SHORTENED, DEVICE_NAME, DEVICE_NAME_LEN),
			};
			bt_err = bt_le_adv_update_data(ad, ARRAY_SIZE(ad),
										   new_sd, ARRAY_SIZE(new_sd));
			if (bt_err)
			{
				printk("\n[%s]: BT: Advertising update failed (err %d)\n", now_str(), bt_err);
			}
		}
		/* If we are still calibrating ...
		*/
		else if (calibration_time_remaining > 0)
		{
			printk("\n[%s]: APP: Calibration time remaining: %s\n", now_str(), time_str(calibration_time_remaining, true));
			/* Show remaining time for calibration
			*/
			gui_update_qmeter(0, time_str(calibration_time_remaining, false));
		}

		k_sleep(K_MSEC(1000));
	}
}

src/gui.c

C/C++
#include <zephyr.h>
#include <device.h>
#include <drivers/display.h>
#include <drivers/sensor.h>
#include <lvgl.h>
#include <stdio.h>
#include <string.h>

#include "gui.h"

#define LOG_LEVEL CONFIG_LOG_DEFAULT_LEVEL
#include <logging/log.h>
LOG_MODULE_REGISTER(gui);

const struct device *display_dev;

/* GUI objects ... 
*/
lv_obj_t *headline;
lv_obj_t *qualitiy_meter;
lv_obj_t *qualitiy_label;
lv_obj_t *temp_label;
lv_obj_t *temp_value_label;
lv_obj_t *pressure_label;
lv_obj_t *press_value_label;
lv_obj_t *humidity_label;
lv_obj_t *humid_value_label;
lv_obj_t *co2_label;
lv_obj_t *co2_value_label;
lv_obj_t *tvoc_label;
lv_obj_t *tvoc_value_label;

/* GUI setup ... 
*/
void gui_setup(void)
{
	display_dev = device_get_binding(CONFIG_LVGL_DISPLAY_DEV_NAME);

	if (display_dev == NULL)
	{
		LOG_ERR("Display device not found!");
		return;
	}

	display_blanking_off(display_dev);

	lv_theme_t *theme = lv_theme_material_init(LV_COLOR_GREEN, LV_COLOR_WHITE, LV_THEME_MATERIAL_FLAG_DARK, &lv_font_montserrat_14, &lv_font_montserrat_16, &lv_font_montserrat_18, &lv_font_montserrat_22);
	lv_theme_set_act(theme);

	headline = lv_label_create(lv_scr_act(), NULL);
	qualitiy_meter = lv_linemeter_create(lv_scr_act(), NULL);
	qualitiy_label = lv_label_create(lv_scr_act(), NULL);
	temp_label = lv_label_create(lv_scr_act(), NULL);
	temp_value_label = lv_label_create(lv_scr_act(), NULL);
	pressure_label = lv_label_create(lv_scr_act(), NULL);
	press_value_label = lv_label_create(lv_scr_act(), NULL);
	humidity_label = lv_label_create(lv_scr_act(), NULL);
	humid_value_label = lv_label_create(lv_scr_act(), NULL);
	co2_label = lv_label_create(lv_scr_act(), NULL);
	co2_value_label = lv_label_create(lv_scr_act(), NULL);
	tvoc_label = lv_label_create(lv_scr_act(), NULL);
	tvoc_value_label = lv_label_create(lv_scr_act(), NULL);

	static lv_style_t large_style;
	lv_style_init(&large_style);
	lv_style_set_text_font(&large_style, LV_STATE_DEFAULT, lv_theme_get_font_title());

	lv_obj_add_style(headline, LV_LABEL_PART_MAIN, &large_style);
	lv_obj_set_x(headline, 115);
	lv_obj_set_y(headline, 10);
	lv_obj_set_height(headline, 20);
	lv_obj_set_width(headline, 50);
	lv_label_set_text(headline, "Air Quality");

	lv_obj_set_x(qualitiy_meter, 10);
	lv_obj_set_y(qualitiy_meter, 75);
	lv_obj_set_width(qualitiy_meter, 90);
	lv_obj_set_height(qualitiy_meter, 90);
	lv_linemeter_set_range(qualitiy_meter, 0, 100);

	lv_obj_set_x(qualitiy_label, 30);
	lv_obj_set_y(qualitiy_label, 170);
	lv_obj_set_width(qualitiy_label, 60);
	lv_obj_set_height(qualitiy_label, 40);
	lv_label_set_text(qualitiy_label, "Quality");

	unsigned int line0_y = 50;
	unsigned int line_space = 40;
	unsigned int line = 0;

	lv_obj_set_x(temp_label, 115);
	lv_obj_set_y(temp_label, line0_y + line_space * line);
	lv_label_set_text(temp_label, "Temp (C)");
	lv_obj_add_style(temp_value_label, LV_LABEL_PART_MAIN, &large_style);
	lv_obj_set_x(temp_value_label, 245);
	lv_obj_set_y(temp_value_label, line0_y + line_space * line);
	lv_label_set_text(temp_value_label, "...");

	line++;
	lv_obj_set_x(pressure_label, 115);
	lv_obj_set_y(pressure_label, line0_y + line_space * line);
	lv_label_set_text(pressure_label, "Pressure (hPa)");
	lv_obj_add_style(press_value_label, LV_LABEL_PART_MAIN, &large_style);
	lv_obj_set_x(press_value_label, 245);
	lv_obj_set_y(press_value_label, line0_y + line_space * line);
	lv_label_set_text(press_value_label, "...");

	line++;
	lv_obj_set_x(humidity_label, 115);
	lv_obj_set_y(humidity_label, line0_y + line_space * line);
	lv_label_set_text(humidity_label, "Humidity (%)");
	lv_obj_add_style(humid_value_label, LV_LABEL_PART_MAIN, &large_style);
	lv_obj_set_x(humid_value_label, 245);
	lv_obj_set_y(humid_value_label, line0_y + line_space * line);
	lv_label_set_text(humid_value_label, "...");

	line++;
	lv_obj_set_x(co2_label, 115);
	lv_obj_set_y(co2_label, line0_y + line_space * line);
	lv_label_set_text(co2_label, "eCO2 (ppm)");
	lv_obj_add_style(co2_value_label, LV_LABEL_PART_MAIN, &large_style);
	lv_obj_set_x(co2_value_label, 245);
	lv_obj_set_y(co2_value_label, line0_y + line_space * line);
	lv_label_set_text(co2_value_label, "...");

	line++;
	lv_obj_set_x(tvoc_label, 115);
	lv_obj_set_y(tvoc_label, line0_y + line_space * line);
	lv_label_set_text(tvoc_label, "TVOC (ppb)");
	lv_obj_add_style(tvoc_value_label, LV_LABEL_PART_MAIN, &large_style);
	lv_obj_set_x(tvoc_value_label, 245);
	lv_obj_set_y(tvoc_value_label, line0_y + line_space * line);
	lv_label_set_text(tvoc_value_label, "...");
}

/* Updates the value label for the refered sensor channel / value ... 
*/
void gui_update_sensor_value(enum sensor_channel channel, struct sensor_value value)
{
	value.val2 = value.val2 / 10000;
	switch (channel)
	{
	case SENSOR_CHAN_AMBIENT_TEMP:
		lv_label_set_text_fmt(temp_value_label, "%d.%02d", value.val1, value.val2);
		break;
	case SENSOR_CHAN_PRESS:
		lv_label_set_text_fmt(press_value_label, "%d.%02d", value.val1, value.val2);
		break;
	case SENSOR_CHAN_HUMIDITY:
		lv_label_set_text_fmt(humid_value_label, "%d.%02d", value.val1, value.val2);
		break;
	case SENSOR_CHAN_CO2:
		lv_label_set_text_fmt(co2_value_label, "%u", value.val1);
		break;
	case SENSOR_CHAN_VOC:
		lv_label_set_text_fmt(tvoc_value_label, "%u", value.val1);
		break;
	}
}

/* Updates the line meter and it's label with the 'relative IQAI' and the rating ... 
*/
void gui_update_qmeter(int8_t quality, const char *rating)
{
	lv_linemeter_set_value(qualitiy_meter, quality);
	lv_label_set_text(qualitiy_label, rating);
}

/* Thread for activating the LVGL taskhandler periodicly ... 
*/
void gui_run(void)
{
	while (1)
	{
		lv_task_handler();
		k_sleep(K_MSEC(20));
	}
}

// Define our GUI thread, using a stack size of 4096 and a priority of 7
K_THREAD_DEFINE(gui_thread, 4096, gui_run, NULL, NULL, NULL, 7, 0, 0);

src/gui.h

C Header File
#ifndef __GUI_H
#define __GUI_H

#include <zephyr.h>
#include <drivers/sensor.h>

void gui_setup(void);

void gui_update_sensor_value(enum sensor_channel channel, struct sensor_value value);

void gui_update_qmeter(int8_t quality, const char *rating);

void gui_update_headline(const char *str);

#endif

src/iaq.c

C/C++
#include <zephyr.h>
#include "iaq.h"

#define IAQ_REGARDED_MEASUREMENTS 4

/* 
This code provides calculations of the Indoor Air Qualitiy index according to
http://www.iaquk.org.uk/ESW/Files/IAQ_Rating_Index.pdf for
temperature, humidity, CO2 and TVOC concentration.
*/

/*
Temperature (C)

Excellent: 18 - 21C
Good: Plus or minus 1C 
Fair: Plus or minus 2C 
Poor: Plus or minus 3C 
Inadequate: Plus or minus 4C or more
*/
uint8_t points_temperature(uint32_t temperature)
{
    uint8_t points = 5;
    const uint32_t excellent_low = 18;
    const uint32_t excellent_high = 21;

    if (temperature < excellent_low)
    {
        points -= excellent_low - temperature > 4 ? 4 : excellent_low - temperature;
    }
    else if (temperature > excellent_high)
    {
        points -= temperature - excellent_high > 4 ? 4 : temperature - excellent_high;
    }

    return points;
}

/*
Relative Humidity (% RH)

Excellent: 40 - 60 % RH
Good: < 40 / > 60 % RH 
Fair: < 30 / > 70 % RH 
Poor: < 20 / > 80 % RH 
Inadequate: < 10 / > 90 % RH 
*/
uint8_t points_humidity(uint32_t humidity)
{
    uint8_t points = 0;

    if (humidity < 10 || humidity > 90)
    {
        points = 1;
    }
    else if (humidity < 20 || humidity > 80)
    {
        points = 2;
    }
    else if (humidity < 30 || humidity > 70)
    {
        points = 3;
    }
    else if (humidity < 40 || humidity > 60)
    {
        points = 4;
    }
    else if (humidity >= 40 && humidity <= 60)
    {
        points = 5;
    }

    return points;
}

/*
Carbon Dixoide (PPM)

Excellent: Below 600 PPM 
Good: 601 - 1000 PPM 
Fair: 1000 - 1500 PPM 
Poor: 1500 - 1800 PPM 
Inadequate: 1800 PPM + 
*/
uint8_t points_co2(uint32_t co2)
{
    uint8_t points = 0;

    if (co2 <= 600)
    {
        points = 5;
    }
    else if (co2 <= 800)
    {
        points = 4;
    }
    else if (co2 <= 1500)
    {
        points = 3;
    }
    else if (co2 <= 1800)
    {
        points = 2;
    }
    else if (co2 > 1800)
    {
        points = 1;
    }

    return points;
}

/*
TVOC (ppb)

Since the rating in http://www.iaquk.org.uk/ESW/Files/IAQ_Rating_Index.pdf 
is given only for units of mg/m3 and most of the available sensors produce
this value only in units of ppb, it would have been necessary to convert
measurements in ppb to values in mg/m3- 

In order to map the TVOC measurement in ppb to
mg/m3 a gas mixture would have to be assumed which represents a typical TVOC mixture.
Based on this mixture, an average molar mass could be calculated which could be further
used to directly convert ppb into mg/m3. 

As an alternative to this approach, the evaluation of the TVOC measurement in ppb,
is done by referencing a table published by the German Federal Environmental Agency.
Following the human perception, the German Federal Environmental Agency 
(Bundesgesundheitsblatt  Gesundheitsforschung Gesundheitsschutz 2007, 50:9901005, 
Springer Medizin Verlag 2007. (DOI 10.1007/s00103-007-0290-y) translates TVOC concentration 
(parts per billion) on a logarithmic scale into five indoor air quality levels (IAQ):

Excellent: 0 - 0.065 ppm  (<= 65 ppb)
Good: 0.065 - 0.22 ppm    (< =220 ppb)
Moderate: 0.22 - 0.66 ppm (<= 660 ppb)
Poor: 0.66 - 2.2 ppm      (<= 2200 ppb)
Unhealthy: 2.2 - 5.5 ppm   (> 2200 ppb) 
*/
uint8_t points_tvoc(uint32_t tvoc)
{
    uint8_t points = 0;

    if (tvoc <= 65)
    {
        points = 5;
    }
    else if (tvoc <= 220)
    {
        points = 4;
    }
    else if (tvoc <= 660)
    {
        points = 3;
    }
    else if (tvoc <= 2200)
    {
        points = 2;
    }
    else if (tvoc > 2200)
    {
        points = 1;
    }

    return points;
}

/* IAQI: The sum of all calculated points for each given indicator / sensor value.
*/
uint8_t get_iaq_index(uint32_t temperature, uint32_t humidity, uint32_t eco2, uint32_t tvoc)
{
    uint8_t points = 0;

    points += points_temperature(temperature);
    points += points_humidity(humidity);
    points += points_co2(eco2);
    points += points_tvoc(tvoc);

    return points;
}

/* IAQI rating: 5 levels based on the given IAQI.
*/
const char *get_iaq_rating(uint8_t iaq_index)
{

    if (iaq_index < 2 * IAQ_REGARDED_MEASUREMENTS)
    {
        return "Inadequate";
    }
    else if (iaq_index < 3 * IAQ_REGARDED_MEASUREMENTS)
    {
        return "Poor";
    }
    else if (iaq_index < 4 * IAQ_REGARDED_MEASUREMENTS)
    {
        return "Fair";
    }
    else if (iaq_index < 5 * IAQ_REGARDED_MEASUREMENTS)
    {
        return "Good";
    }
    else
    {
        return "Excellent";
    }
}

uint8_t get_min_iaq_index()
{
    return IAQ_REGARDED_MEASUREMENTS;
}

uint8_t get_max_iaq_index()
{
    return IAQ_REGARDED_MEASUREMENTS * 5;
}

src/iaq.h

C Header File
#ifndef __IAQ_H
#define __IAQ_H

#include <zephyr.h>
#include "iaq.h"

uint8_t get_iaq_index(uint32_t temperature, uint32_t humidity, uint32_t eco2, uint32_t tvoc);

const char *get_iaq_rating(uint8_t iaq_index);

uint8_t get_min_iaq_index();

uint8_t get_max_iaq_index();


#endif

boards/nrf5340dk_nrf5340_cpuapp.overlay

Plain text
&i2c1 {
        status = "okay";
       sda-pin = <34>; // P1.02 (34)
       scl-pin = <35>; // P1.03 (35)

	/* Sparkfun Environment Combo uses second I2C address */
        ccs811: ccs811@5b {
                compatible = "ams,ccs811";
                reg = <0x5a>;
                label = "CCS811";
                irq-gpios = <&gpio0 36 GPIO_ACTIVE_LOW>; // P1.04 (36)
                wake-gpios = <&gpio0 5 GPIO_ACTIVE_LOW>;
                reset-gpios = <&gpio0 6 GPIO_ACTIVE_LOW>;
        };

        bme280@76 {
		compatible = "bosch,bme280";
		reg = <0x76>;
		label = "BME280";
	};
};

prj.conf

Plain text
CONFIG_HEAP_MEM_POOL_SIZE=16384
CONFIG_MAIN_STACK_SIZE=4096

CONFIG_NEWLIB_LIBC=y

CONFIG_LOG=y

CONFIG_BT=y
CONFIG_BT_DEBUG_LOG=y
CONFIG_BT_DEVICE_NAME="IAQ"

CONFIG_SENSOR=y

CONFIG_BME280=y
CONFIG_CCS811=y


CONFIG_DISPLAY=y
CONFIG_DISPLAY_LOG_LEVEL_ERR=y

CONFIG_LVGL=y
CONFIG_LVGL_USE_THEME_MATERIAL=y
CONFIG_LVGL_USE_LABEL=y
CONFIG_LVGL_USE_LINEMETER=y
CONFIG_LVGL_FONT_MONTSERRAT_14=y
CONFIG_LVGL_FONT_MONTSERRAT_16=y
CONFIG_LVGL_FONT_MONTSERRAT_18=y
CONFIG_LVGL_FONT_MONTSERRAT_22=y

CMakeLists.txt

Plain text
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.13.1)

set(SHIELD adafruit_2_8_tft_touch_v2)

find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(lvgl)

FILE(GLOB app_sources src/*.c)
target_sources(app PRIVATE ${app_sources})

iaq-monitor-demo

Credits

tf

tf

13 projects • 3 followers

Comments