CC Blog Projects Research & Design Hub

Home Air Quality Monitoring (Part 1)

Using Nordic’s nRF5340 Dev Kit

Raul begins his multi-part article series on building a home air quality monitoring system comprised of an IoT device, an Android mobile app and a web server application. The IoT device is based on Nordic Semi’s nRF5340 dev kit.

  • How to building an IoT-based home air quality monitoring system

  • How to install the necessary software tools

  • How the monitoring system functions

  • How to add the sensor drivers

  • How to send data over BLE

  • How to implement the display and touchscreen

  • How to implemented running average, low-pass filters

  • Nordic Semi’s nRF5340 dev kit

  • CCS811 eCO2/eTVOC sensor from AMS

  • Sharp GP2Y1010AU0F dust/particle sensor (from Sparkfun)

  • STMicroelectronics LPS22HB barometric pressure sensor

  • DHT22 ambient temperature

  • Relative humidity sensor

  • Arduino Mega2560 prototyping shield 

  • SEGGER J-Link programmer/debugger

  • 2.8″ TFT Touch Shield for Arduino w/Capacitive Touch

In this article series, I discuss building a system for monitoring home air quality. The system is composed of three parts: an IoT device, an Android mobile application and a web server application. The IoT device is based on Nordic Semiconductor’s nRF5340 Development Kit (DK). It uses air quality sensors to collect data, and a wireless Bluetooth Low Energy (BLE) connection to send them to the Android device. The Android device appends its GPS coordinates and a timestamp to the received data, then it sends them to the web server to be stored in a database. When a user opens the web server’s home page, a web application reads from the database and visualizes the data graphically on the webpage.

This article has two main purposes. First, it introduces BLE embedded IoT development with the nRF5340 DK board, as well as Nordic’s “nRF Connect SDK” and related tools and libraries. More specifically, I will discuss how I transmitted sensor data from the nRF5340 DK to an Android mobile device via BLE. Second, it shows a workflow I use in personal projects for integrating sensor data from IoT devices with Android and web server applications.

To follow this article, you need to have a fair knowledge of embedded systems, Android application development with Java and web application development with PHP/MariaDB and HTML/CSS/JavaScript. However, if you don’t have previous knowledge of any of these technologies, I hope you will still be able to follow, at least to get a fair understanding about how these three technologies are being combined to produce the desired result. By no means do I consider myself an expert in these technologies. Moreover, this was my first time using Nordic Semiconductor’s system-on-chips (SoCs) and related tools.


Since this was my first time using Nordic Semiconductor Arm-based SoCs and related development tools, I had to learn them from scratch; but it wasn’t hard because of my previous experience with Arm microcontrollers (MCUs) and tools from other vendors. Nordic Semiconductor has an interesting portfolio of products, particularly SoC solutions that integrate cellular IoT, Bluetooth, ZigBee, Thread and ANT, along with power management [1].

The nRF5340 DK board is based on the nRF5340 dual-core Bluetooth 5.2 SoC that supports BLE, Bluetooth mesh, NFC, Thread, ANT and ZigBee. It includes an onboard SEGGER J-Link programmer/debugger that can also work with external targets. Analog and digital interfaces and GPIOs are available via headers and edge connectors. It also has Arduino UNO compatible headers to connect Arduino shields. The nRF5340 DK can be powered from a wide range of sources (1.7V to 5.0V), including USB, a CR2032 coin-cell and a Lithium Polymer (LiPo) battery [2].

nRF Connect SDK supports the nRF5340 DK for the development of BLE, Thread and Zigbee applications. The SDK integrates the Zephyr real-time operating system (RTOS), protocol stacks and hardware drivers for various devices. It also has many project samples showcasing different development aspects. So, before we can compile code and flash the nRF5340 DK, we must install Nordic’s software tools and libraries. Nordic Semiconductor’s “Infocenter” site has a getting started page to run a first board test and set up the toolchain [3]. It’s a great starting place for newcomers.

To install the toolchain, you must first install Nordic’s “nRF Connect for Desktop” cross-platform application (available for Windows, Linux and MacOS). It lets you install and manage various “nRF Connect Apps,” such as the “Toolchain Manager” that can be used to install/uninstall different versions of the nRF Connect SDK (NCS), the “Programmer” app that lets you flash “.hex” firmware files to your board, and the “Bluetooth Low Energy” app, a tool for development and testing with BLE. There are other apps and tools as well that can be useful in the development process.

At the time of developing the IoT device prototype, nRF Connect SDK v1.5.1 was the latest version available. The SDK uses the “SEGGER Embedded Studio for Arm” integrated development environment (IDE) for writing and debugging code, which integrates seamlessly with the SEGGER J-Link OB programmer/debugger in the nRF5340 DK. It is possible to have multiple versions of the SDK installed at the same time. After I installed the toolchain on Windows 10, the SDK folder path was : <drive letter>:\ncs\v1.x.x\.

Inside the ncs\v1.x.x\nrf\samples\bluetooth folder, there are many example projects that use BLE communication. For the IoT device’s firmware, I chose as a basis the one named peripheral_uart. This example reads data coming from the UART port in the nRF5340 DK and sends it wirelessly through the BLE connection to whatever device the nRF5340 is connected to. Received data from the remote device is passed-through to the UART’s output.


Figure 1 shows the Home Air Quality Monitoring (HAQM) system’s block diagram. The system is composed of three parts: the IoT device, the Android device with a custom application and the server with a web application.

The IoT device is based on the nRF5340 DK (Figure 2), and it has four sensors: an AMS CCS811 eCO2/eTVOC sensor, a Sharp GP2Y1010AU0F dust/particle sensor (from Sparkfun), a STMicroelectronics LPS22HB barometric pressure sensor and a DHT22 ambient temperature and relative humidity sensor. For powering the prototype, I used a USB power bank.

Figure 1 System block diagram
Figure 1
System block diagram
Figure 2 nRF5340 Development Kit board
Figure 2
nRF5340 Development Kit board

The IoT device takes readings from its sensors and sends the data to the Android device via BLE every 5 seconds. The CCS811 sensor detects and measures the volatile organic compounds (VOCs) in the air, and estimates the “equivalent total volatile organic compounds” (eTVOC) and the “equivalent CO2” (eCO2) concentrations.

The GP2Y1010AU0F dust/particle sensor detects very fine particles in the air, such as those coming from smoke, dust or pollen. It measures particle density in micrograms per cubic meter (µg/m3). The STMicroelectronics LPS22HB sensor measures barometric pressure and ambient temperature as well. Temperature readings from the LPS22HB sensor and the DHT22 sensor are averaged to get the final ambient temperature that’s sent to the mobile device.

The TFT display shows the Bluetooth connection status and the last sensor data sent to the mobile device. A Button gadget in the display GUI allows the activation/deactivation of the sensor data sending function. The display also shows data received from the mobile device—for instance, its latitude and longitude coordinates.

Sensor readings are taken every 1 or 2 seconds and passed through low-pass filters to reduce data noise. Every 5 seconds, filtered values are sent to the mobile device via BLE as a JSON string. The Android application takes the received measurements and adds its GPS coordinates and a timestamp to the received data. Then, it sends the JSON string along with the added data to the Web server via HTTP protocol. The web server receives and stores the data in a database. When a user enters the Web server’s IP address or domain, the server reads from the database and graphically displays current and historical sensor data.

Figure 3 shows the IoT device’s circuit diagram, and Figure 4 shows the prototype I built for testing. I used an Arduino Mega2560 prototyping shield (below the TFT shield) to solder connection headers for all sensors, including the RC circuit the GP2Y1010AU0F sensor needs to work (see this sensor’s pins 1 and 2 shown in Figure 3).

Figure 3 IoT device circuit diagram
Figure 3
IoT device circuit diagram
Figure 4 IoT device prototype
Figure 4
IoT device prototype

The original peripheral_uart example project used as a basis for the IoT device’s firmware doesn’t have any sensor interfacing. Fortunately, in the ncs\v1.x.x\zephyr\samples\sensor\ folder, there are examples of sensor interfacing for up to 50 sensors of different types! They use driver libraries already available for the Zephyr RTOS, which makes it really easy for us, if the sensors we want to use are already in that list. If not, it is always possible to take one of the examples and port the code to make a particular sensor work.

For the IoT device, I picked the CCS811, DHT22 and LPS22HB sensors from that list. The three of them use I2C interface, and they already have available drivers in the NCS. First, I tested them separately with their corresponding example projects, before connecting all of them to the same I2C port.

The GP2Y1010AU0F dust/particle sensor has an analog output. To interface it to the nRF5340, I used as a basis code from an ADC tutorial published on Nordic’s “DevZone” support forum [4]. This sensor uses an infrared emitting diode and a phototransistor to detect particles. So, a digital output must be used to turn on the emitting diode for a brief period of time (280µs recommended in its datasheet). Then, the sensor output voltage is read by using an analog input in the SoC. With the read voltage value, the particle concentration is computed using a transfer curve provided by its datasheet.

To integrate code from the aforementioned sensor examples into the peripheral_uart project, I basically took the main functions from the sensor examples, converted them into Zephyr RTOS thread functions and added them into the peripheral_uart project. I also added configuration parameters to the project’s “configuration” and “overlay” files. The NCS uses .conf files to configure different aspects of a NCS project, such as: enabling the use of hardware drivers (such as I2C, ADC, SPI), display and graphic libraries, Bluetooth parameters, heap and stack sizes and so forth. It also uses .overlay files to configure the interfacing of external devices via communication ports—for instance, which pins will be used for I2C, SPI, UART communication and so on.

I connected the CCS811, DHT22 and LPS22HB sensors to the same I2C bus, and then configured in the “overlay” file the corresponding nRF5340’s SDA and SCL pins to use. I also configured I2C register addresses for the three devices, labels (device names) and three additional auxiliary pins that the CCS811 sensor uses for IRQ, WAKE-UP and RESET. For the GP2Y1010AU0F sensor, I used the aforementioned ADC example as a basis to configure and use the nRF5340’s ADC. I also modified code from an available Arduino sketch for reading the sensor’s output voltage and computing the particle concentration value [5].

Next, I added the corresponding include files and initialization code copied from the examples, and some glue code of my own to make them work in the new modified project, which I named iot-ble-device. You can download this project’s files from Circuit Cellar’s article code and files webpage.

Listing 1 shows the Zephyr RTOS thread for the DHT22 sensor. The code is straightforward, and it mainly calls functions from the DHT22 driver. After reading the sensor values, line 48 puts the thread to sleep for 2 seconds (DHT22’s minimum recommended reading interval). Line 52 shows the thread initialization details. The other sensor threads are in general very similar to this one.

Listing 1
Zephyr RTOS thread for the DHT22 sensor

1 void dht22_thread(void)
2 {
3     const char *const label = DT_LABEL(DT_INST(0, aosong_dht));
4     const struct device *dht22 = device_get_binding(label);
5     LOG_INF(“--- Starting dht22_thread!”);
7     if (!dht22) {
8         printk(“Failed to find sensor %s\n”, label);
9         LOG_WRN(“Failed to find sensor %s\n”, log_strdup(label));
10         return;
11     }
13     while (true) {
14         int rc = sensor_sample_fetch(dht22);
15         LOG_INF(“--- Inside dht22_thread!”);
17         if (rc != 0) {
18             printf(“DHT22 sensor fetch failed: %d\n”, rc);
19         //break; // Still looking for a Zephyr way to break  
20                  // the current thread run
21         }
23         rc = sensor_channel_get(dht22, SENSOR_CHAN_AMBIENT_TEMP, &dht22_temp);
25         if (rc == 0) {
26             rc = sensor_channel_get(dht22, SENSOR_CHAN_HUMIDITY, &dht22_humidity);
27         }
29         if (rc != 0) {
30             printf(“get failed: %d\n”, rc);
31         //break; // Still looking for a Zephyr way to break  
32                  // the current thread run
33         }
35         uint8_t data[50];
36         uint16_t len;
37         len = sprintf(data, “%.1f Cel ; %.1f %%RH\n”,
38             sensor_value_to_double(&dht22_temp),
39             sensor_value_to_double(&dht22_humidity));
41         // Store readings in LP filter buffer
42         Store_Dht22_Reading(sensor_value_to_double(&dht22_temp), 
43             sensor_value_to_double(&dht22_humidity));
45         LOG_INF(“DHT data: %s”, log_strdup(data));
46         printf(“\n++++ DHT data: %s”, data);
48         k_sleep(K_SECONDS(2));
49     }
50 }
52 K_THREAD_DEFINE(dht22_thread_id, STACKSIZE, dht22_thread, NULL, NULL,
53                  NULL, PRIORITY, 0, 0);

The peripheral_uart example uses the Nordic UART Service (NUS) Bluetooth LE GATT custom service to receive and write data acting as a bridge to the UART port in the nRF5340 SoC. When there’s a connection, it forwards any data received in the RX pin of the UART 0 peripheral to the BLE unit (one of the Arm cores in the nRF5340 SoC), and any data received in the BLE unit is sent out of the UART 0 peripheral’s TX pin.

So, in my custom iot-ble-device project (derived from the peripheral_uart project) I modified the code to send sensor data instead of data coming from the UART port input. This data is sent to the mobile device as a lightweight JSON string, because the aforementioned NUS GATT profile services support sending only a few tens of bytes of payload in each transaction. Figure 5a shows an example of the aforementioned sensor data JSON string, and Listing 2 shows the ble_write_thread() function. This is the function that originally came with the peripheral_uart example.

Listing 2
BLE sending function

1 void ble_write_thread(void)
2 {
3     /* Don’t go any further until BLE is initialized */
4     k_sem_take(&ble_init_ok, K_FOREVER);
6     uint8_t ble_data[100];
7     uint8_t display_sensor_str[80];
8     uint16_t len;
9     uint8_t number = 0;
11     for (;;) {
12         // Get filtered sensor values 
13         struct t_ccs811_reading ccs811_filtered = Compute_Ccs811_Filtered();
14         struct t_dht22_reading dht22_filtered = Compute_Dht22_Filtered();
15         struct t_lps22hb_reading lps22hb_filtered = Compute_Lps22hb_Filtered();
16         float gp2y10_filtered = Compute_Gp2y10_Filtered();
18         // Average temperatures from the DHT22 and LP22HB to get the value 
19         // to be sent via BLE
20         float avg_temp = (dht22_filtered.temp + lps22hb_filtered.temp) / 2;
22         // Build JSON string to be sent to the mobile device
23         len = sprintf(ble_data, “{\”c\”:%u, \”tv\”:%u, \”t\”:%.1f, 
24                     \”h\”:%.1f, \”p\”:%.1f, \”pc\”:%.1f}”,
25                     ccs811_filtered.eco2, ccs811_filtered.etvoc,
26                     avg_temp, dht22_filtered.rh,
27                     lps22hb_filtered.pressure, gp2y10_filtered);
29         // Build an ASCII string for displaying sensor data on the GUI
30         // ‘%’ is the escape character for ‘%’ in ‘%HR’
31         sprintf(display_sensor_str, 
32             “[SEN]: CO2:%u | TVOC:%u | C:%.1f\n%%HR:%.1f | BP:%.1f | PC:%.1f”,
33             ccs811_filtered.eco2, ccs811_filtered.etvoc,
34             avg_temp, dht22_filtered.rh,
35             lps22hb_filtered.pressure, gp2y10_filtered);
37         LOG_INF(“--->>> Data to send:”);
38         LOG_INF(“%s”, log_strdup(ble_data));
39         LOG_INF(“len: %d\n”, len);
41         // Sen data over Bluetooth if data sending function is activated 
42         // from the display GUI
43         if (send_bt_data) {
44             if (bt_nus_send(NULL, ble_data, len)) {
45                 LOG_WRN(“Failed to send data over BLE connection”);
46             }
47         }
49         // Display sensor data on the GUI
50         lv_label_set_text(label_btn_state, display_sensor_str);
52         // Sleep this thread 
53         k_sleep(K_MSEC(5000));
54     }
55 }
57 // STACKSIZE is 1024, doubling it for this thread because of the many bytes used in buffers
58 K_THREAD_DEFINE(ble_write_thread_id, 2*STACKSIZE, ble_write_thread, NULL, NULL,
59                 NULL, PRIORITY, 0, 0);

I modified it to send the sensor JSON string, instead of data coming from the UART port. In lines 13-16, filtered sensor values are computed from the filter buffers (more on filters later), and in line 20, the average from the two available ambient temperatures is computed. Lines 23-27 build the JSON string, and lines 31-35 build another sensor data string that’s printed on the display GUI. Lines 43-47 send the data via BLE by invoking the bt_nus_send() function. send_bt_data is a Boolean variable that activates/deactivates the sending function; its value is modified by the GUI “Send” button, as shown in Figure 6a. This thread runs every 5 seconds (see line 53).

Some data is also received back from the mobile device as another JSON string. This JSON string contains the mobile device’s GPS coordinates, velocity and heading, and it is parsed to get those values instead of just deriving it to the nRF5340’s UART output. Figure 5b shows this JSON string. It is much simpler than the previous one, because it doesn’t contain “keys” for each value. So, their meaning is implicit in the occupied position in the string. However, in the first-discussed JSON string, each value has its corresponding key, because that’s the format in which final data will be sent to the web server, to make it easy to parse by using PHP libraries.

Figure 5 JSON strings. (a) from the IoT device, (b) from the mobile device
Figure 5
JSON strings. (a) from the IoT device, (b) from the mobile device
Figure 6 IoT device GUI. (a) with BLE connected, (b) with BLE disconnected
Figure 6
IoT device GUI. (a) with BLE connected, (b) with BLE disconnected

The ncs-display-ble-example project available for the Adafruit 2.8” TFT Touch Shield v2 shows how to use this shield with the NCS. The project is not available directly in the SDK installation folders, but in Nordic’s “NordicPlayground” GitHub repository [6]. This Arduino shield is a 240×320 pixel display (2.8” diagonal) with 18-bit color capability. It comes with a capacitive touchscreen and a microSD card slot. It uses SPI communication for the TFT display and the microSD card, and I2C for the touchscreen. The display driver chip is the ILITEK ILI9341, and the touchscreen driver is the FocalTech FT6206 controller chip.

This ncs-display-ble-example project showcases the use of the aforementioned display with the “Light and Versatile Graphics Library“ (LVGL), in conjunction with the peripheral_lbs (peripheral LED/Button Bluetooth service) example from the nRF Connect SDK. In this project, the display shows the state of the BLE connection (idle, advertising or connected). If connected, a button widget on the GUI turns on/off one of the LEDs in the nRF5340 DK board, and a light bulb widget visualizes the state of one of the pushbuttons in the board. LED and pushbutton states are also shared wirelessly via Bluetooth to whichever client device the nRF5340 DK is connected.

LVGL is an open-source graphics library for creating embedded GUIs with low-memory footprints [7]. It supports several MCU boards from different manufacturers, and it is also available as a Zephyr library. Although I’ve never used this library before, it was relatively easy to include it in my newly created iot-ble-device project, using as reference the way it was implemented in the ncs-display-ble-example project. Plain and simple, I just copied the relevant files to relevant project folders. Then, I copied all initialization code from the main.c file, along with code lines that display gadgets and text on the display. I also copied and modified the gui.c file, which creates and configures all objects on the GUI. For instance, in this file, I changed the logo and system name, and widget positions.

Figure 6a shows the IoT device’s GUI design. In the upper section, there’s a custom logo next to the system name. Immediately below, there’s the “Send On/Off” button, which activates and deactivates the data-sending function from the IoT device to the Android mobile device. To its right, there’s a compass widget that shows the Android mobile device’s heading. Below that, the Android device’s GPS coordinates and velocity are shown. I used the aforementioned heading and velocity for another prototype device for outdoor air quality mapping. They are not relevant for this project, but I didn’t want to erase them from this project, in case someone finds them useful.

The shown GPS coordinates from the Android device are assumed to also be the IoT device’s—as long as both are reasonably close. Below that, the last sensor values transmitted to the mobile device are shown, and finally, in the lower sector is the BLE connection status. As shown in Figure 6b, when the IoT device is advertising (not connected to any client), nothing is shown in the mid area of the GUI.

To generate the custom logo at the top left, I used the “Image Converter” online tool from the LVGL project’s website [8]. This tool takes a BMP, JPG or PNG file and converts it to a C array in a C file. The “Send On/Off” button has a callback function that’s executed when the button is pressed or released. Listing 3 shows this callback function. The function detects the event LV_EVENT_PRESSED when the button is pressed, and the event LV_EVENT_RELEASED when the button is released. When the button is pressed (lines 6-14), apart from GUI refreshing, nothing else is done concerning the data-sending function. However, when the button is released, the send_bt_data Boolean variable is verified and modified. If the variable is false (sending function deactivated), then it is flipped to true to activate the sending function, and the button label is also changed accordingly (lines 18-21). If it is true (sending function activated), then it is assigned false to deactivate the sending function, and the button label is also changed (lines 21-24).

Listing 3
GUI “Send” button callback function

1 bool send_bt_data = false; // Flags if the BLE sending func. is on/off
3 static void on_button1(lv_obj_t *btn, lv_event_t event)
4 {
5     if(btn == btn1){
6         if(event == LV_EVENT_PRESSED) {
7             printf(“LV_EVENT_PRESSED,send_bt_data: %d\n”, send_bt_data);
9             if(m_gui_callback) { 
10                m_gui_event.evt_type = GUI_EVT_BUTTON_PRESSED;
11                m_gui_event.button_checked = true;
12                m_gui_callback(&m_gui_event);
13            }
14        }
15        else if(event == LV_EVENT_RELEASED) {
16             printf(“LV_EVENT_RELEASED,send_bt_data: %d\n”, send_bt_data);
18             if (send_bt_data == false) {
19                 lv_label_set_text(btn1_label, “Send Off”);
20                 send_bt_data = true; // Toggle BT sending on
21             } else {
22                 lv_label_set_text(btn1_label, “Send On”);
23                 send_bt_data = false; // Toggle BT sending off
24             }
26             if(m_gui_callback) { 
27                 m_gui_event.evt_type = GUI_EVT_BUTTON_PRESSED;
28                 m_gui_event.button_checked = false;
29                 m_gui_callback(&m_gui_event);
30             }
31         }
32     }
33 }

I implemented running average, low-pass filters for each air quality parameter read from sensors. Because the sensor-reading period (1~2 seconds) is less than the data-sending period (5 seconds), noise from sensor data is constantly filtered between BLE transmissions and after each sensor read. By applying low-pass filters, we reduce sensor data noise that comes from sources, such as electromagnetic interference and inherent electric noise generated in the sensor circuitry.

The low-pass running average is one of the easiest filters to implement in embedded systems. The idea behind it is simple: With an N-point size filter, for each reading obtained from the sensor, take that reading and average it with the N-1 previous readings to obtain a filtered value (hence, the name moving average). An inevitable side effect of this is that you will have to wait N reading periods before obtaining a filtered reading, which adds delay to the system. For systems that don’t tolerate processing delays well, this could be an issue or not, depending on how filtered you need your signal to be (more filtering with a greater N filtering points, more delay added to the system). But in our case, this hardly affects the Home Air Quality Monitoring system, because air quality parameters don’t change rapidly, and delays of a couple of seconds won’t negatively affect the monitoring quality.

Listing 4 shows an excerpt from the filters.c file, where I implemented filters for each sensor. The excerpt shows the implementation for the eCO2 and eTVOC measurements from the CCS811 sensor. The code is straightforward. The Store_Ccs811_Reading() function (lines 8-24) stores sensor readings in the filter’s FIFO buffers(lines 3-4). The Compute_Ccs811_Filtered() function takes all values in the FIFO buffers, computes the averages and returns them as the current filtered values. Aside from the namings, filter code for the other sensors is virtually the same.


As I said at the beginning, this was my first time using a Nordic Semiconductor Arm-based SoC. It was also a first for me working with the Zephyr RTOS, the LGVL library, the nRF Connect SDK and the SEGGER Embedded Studio environment. Working in my spare time, it took me about two months to figure out all these tools, libraries and examples—and an additional month to combine all the pieces into the embedded IoT device prototype.

Before starting the project, I had only a basic understanding of the BLE specification, so I needed to do more research to understand it better. I was more familiar with classic Bluetooth modules that let you send data as a wireless UART connection. With BLE, the idea of application profiles with services and characteristics is central to the protocol specification. For anyone like me, who has only used classic plug-and-play serial-data Bluetooth modules in the past (such as the well-known HC-05), BLE can be a bit confusing at the beginning. This also made me scratch my head more than usual when working with the Android application. I used the HC-05 and HC-06 modules extensively in the past to transfer data from embedded devices to a mobile device, but never BLE ones.


In Part 2 of this article series, I will discuss the Android application that I modified for the HAQM system, and how it interfaces with the IoT device described here. Next, I will also discuss how I made the PHP/MariaDB backend and the HTML/JavaScript frontend for the web application, and how it interfaces with the Android application via the HTTP protocol. I will also discuss the database used to store sensor data from the IoT device, and the “Plotly” JavaScript library to make all graphical visualizations in the webpage.

Meanwhile, you can test the IoT device with the “nRF Toolbox” Android application. Follow the instructions in the “” file with this article’s code files. 


[1] Nordic Semiconductor Product Portfolio,
[2] nRF5340 Development Kit,
[3] nRF5 Getting Started – Running a first test,
[4] nRF Connect SDK Tutorial – Part 2 | v1.5.0,—part-2-ncs-v1-4-0
[5] Air Quality Monitoring,
[6] Display BLE Example,
[7] Light and Versatile Graphics Library,
[8] Online Image Converter,

Introduction to Bluetooth Low Energy



Advertise Here

CCS811 Carbon Monoxide Sensor

DHT22 Humidity/Temperature Sensor

GP2Y1010AU0F Optical Dust Sensor

LPS22HB Absolute Pressure Sensor (Barometer) Module

2.8″ TFT Touch Shield for Arduino w/Capacitive Touch

Arduino Mega 2560 Prototype Shield ProtoShield + mini breadboard

Adafruit |
Ams |
Arduino |
Nordic Semiconductor |
SEGGER Microcontroller |
SparkFun Electronics |
STMicroelectronics |


Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.

Note: We’ve made the Dec 2022 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
| + posts

Raul Alvarez Torrico has a B.E. in electronics engineering and is the founder of TecBolivia, a company offering services in physical computing and educational robotics in Bolivia. In his spare time, he likes to experiment with wireless sensor networks, robotics and artificial intelligence. He also publishes articles and video tutorials about embedded systems and programming in his native language (Spanish), at his company’s web site You may contact him at

Supporting Companies

Upcoming Events

Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2024 KCK Media Corp.

Home Air Quality Monitoring (Part 1)

by Raul Alvarez Torrico time to read: 19 min