IBM Z Day on Nov. 21: Discover the ideal environment for modern, mission-critical workloads. Learn more

Build a practical IoT app, an air quality monitor

In my previous articles, we discussed the IBM Watson IoT Platform and, in particular, its MQTT services for collecting and analyzing data from IoT devices. We also introduced you to the open source NodeMCU IoT development platform, which makes it easy to prototype and produce IoT device applications. In this article, we will tie all those components together into a practical IoT application that monitors air quality.

Air pollution is one of the most significant threats to human health in the modern world. It is estimated that smog kills 5.5 million people every year, making it a leading cause of death. In places like Beijing China, it is estimated that simply living there is equivalent to smoking two packs of cigarettes per day. And, it is not just a problem for countries like China and India. In fact, London reached its entire year’s quota for air pollution within the first 5 days of 2017, and Paris is now routinely choked in smog.

One of the most interesting ways to combat smog is to monitor air quality yourself. In this article, I will show you that for less than $35, you can build a NodeMCU-based air quality monitor device (see The assembled prototype device with power source.). Since the device has its own power source (the USB battery stick), it can be placed anywhere there is WIFI signal. I typically place it in a room to monitor indoor air, or next to an open window to monitor outdoor air. It is not only a science project, but also a potential game changer for healthcare (including public health research), as we can now track an individual’s accurate exposure to smog and study how it correlates with health problems.

The assembled prototype device with power source.


Step 1: Choosing and setting up the hardware for our IoT device

For air quality sensors, you can choose from a variety of sensors. The web site AQICN has some good reviews for these sensors. The cheaper ones all work the same way: They have an infrared light source (LED or laser) and a light detector diametrically positioned across an air chamber. The detector measures light scattered by the fine dust or smog particulates in the air chamber.

For this project, I chose to use the Grove Shinyei Model PPD42NS air quality sensor. It has demonstrated accurate measurement of PM2.5 levels at low pollution environments (that is, indoors).

The sensor module has 3 wires: two for power supply and one for data. The power wires (GND and VCC) connect to the GND (black) and Vin (red) PINs on the NodeMCU board, as they draw 5V power from the NodeMCU. The data wire (yellow) connects to one of the digital PINs on the NodeMCU. I choose to connect to D5. The easiest way to connect sensor wires to your NodeMCU board is by using a bread board. The wiring is shown in The assembled prototype device with power source. above (the wire colors refer to the wires coming out of the air quality sensor at the top of the photo).

The NodeMCU board can be powered by using a regular USB connection. So, I attached a rechargeable USB battery to the prototype as power source.

Step 2: Reading sensor data

The data output from the sensor is a waveform with random peaks and troughs. Every peak indicates that the sensor has detected Particulate Matter (PM) greater than 1um in size.

To read PM level from the sensor, the NodeMCU application needs to compute Lo Pulse Occupancy time (LPO time) in a given time unit. It needs to determine how much time (percentage) the data wire is in the low voltage state. The LPO value can then be converted to particulates per liter of air (or particulate mass per m³) using a response curve given in the product specification.

For our sample air pollution application, we need to modify the init.lua app on the NodeMCU. In the init.lua application, the NodeMCU first gets on the wifi network. It then takes one measurement every 10 minutes in a loop. The measurement itself takes 30 seconds. During that 30 seconds, every time the data wire on D5 jumps between high and low voltage states, an INT (interrupt) signal is sent to NodeMCU. NodeMCU measures the amount of time D5 spends in the high and low voltage state to compute the LPO.

The code for init.lua is listed below. The main timer loop activates PIN D5 into “interrupt mode” every 10 minutes (gpio.trig(dpin, “both”, dpin_cb)). Once activated, the dpin_cb function is called whenever the PIN changes state. The dpin_cb function maintains a timer to compute how much time the PIN is at HIGH vs LOW states, and it deactivates the “interrupt mode” after 30 seconds so that it could compute LPO (gpio.trig(dpin, “none”)).

-- 1. Setup the device ID and access credential. See later.

-- PIN assignment
-- D5 is the driver PIN for the dust detector
dpin = 5
-- D0 is the LED on the NodeMCU board
lpin = 0
gpio.mode(dpin, gpio.INT)
-- The current pulse params
rising_ts = 0
falling_ts = 0
-- aggregated timing vars
high_time = 0
low_time = 0
-- determine if the INT is legit
prev_level = -1

-- setup Wifi
wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, connected)

-- This is the main application loop
-- It starts after the network is up
function connected (e)
    -- 2. Connect to the MQTT service. See later.
-- Timer to take a measurement and send data
-- every 10 minutes.
    tmr.alarm(1, 600000, tmr.ALARM_AUTO, function()
        -- Trigger the "dpin_cb” function (defined below)
        -- when a pulse comes in.
        -- dpin_cb will turn off the trigger after 30s
        gpio.trig(dpin, "both", dpin_cb)

-- Define the INT callback function
function dpin_cb (level, when)
    -- current_level =
    if prev_level == level then
        -- there is no change. ignore
        prev_level = level
if level == 1 then
        rising_ts =
        print ("raising edge : " .. rising_ts)
        -- turn on the red LED
        gpio.write(lpin, gpio.LOW)
        falling_ts =
        print ("falling edge : " .. falling_ts)
        -- turn off the red LED
        gpio.write(lpin, gpio.HIGH)
    -- Start aggregated timer after a complete pulse is detected
    if falling_ts > 0 and rising_ts > 0 then
        if falling_ts > rising_ts then
            high_time = high_time + falling_ts - rising_ts
            low_time = low_time + rising_ts - falling_ts
    -- Sampling period is 30*1,000,000 macroseconds
    total_time = high_time + low_time
    if total_time > 30000000 then
        lpo = low_time / total_time
        -- remove the INT and reset timers
        gpio.trig(dpin, "none")
        rising_ts = 0
        falling_ts = 0
        high_time = 0
        low_time = 0
        -- turn off the red LED
        gpio.write(lpin, gpio.HIGH)
        -- Very rough estimate. More calibration needed
        pm25 = lpo * 100.0 * 1.5
        -- 3. Send data to the MQTT server. See later.

Notice that there are three “see later” comments in the code snippet (lines are highlighted). Those comments are placeholders for additional code to send data to a MQTT service, which I will discuss next.

Step 3: Connecting our IoT device to Watson IoT Platform

In my previous MQTT article, I described how to set up MQTT projects on Watson IoT Platform. Please follow the instructions in that article to setup an MQTT project in your IBM Cloud account and create authorization credentials for your device.

On the device side, we must first make sure that the NodeMCU firmware is built with the MQTT support module. Please follow the instructions in my previous NodeMCU article.

In the init.lua application, we will first assign the device ID and access token that was created from Watson IoT Platform to this device. This code segment must be customized for each device. So, an automated code generation and deployment tool (a devops solution) will be needed for a large-scale project that involves many devices.

-- 1. Setup the device ID and access credential.
-- Device ID. It is a random string for each device.
-- But, it should match the device ID set up on Watson IoTP.
device_id = "random_string"
-- The access token Watson IoTP assigned for the device.
access_token = "assigned_by_Watson"

Next, the device should connect to the MQTT service after the wifi network connection is established. That is in the #2 placeholder. Update the init.lua app using the code snippet below, where I also established a “last will” message so that the MQTT service can keep a record if the device drops off from the network.

-- 2. Connect to the MQTT service.
-- Init MQTT client with keepalive timer 120s
m = mqtt.Client(device_id, 120, "use-token-auth", access_token)

-- setup Last Will and Testament (optional)
-- Broker will publish a message "offline" to topic "dw/lwt"
-- if client doesn't send keepalive packet
m:lwt("dw/lwt", "offline", 0, 0)

Step 4: Sending the data to Watson IoT Platform and analyzing the data

After the device is connected, it can send messages to the MQTT service. In this case, update placeholder #3 in the init.lua application so it sends the measured value to a topic. In this setup, there is a single shared topic that aggregates all the sensor readings. Watson IoT Platform can then distinguish each sensor device’s data points through its device ID.

-- 3. Send data to the MQTT server.
m:connect("", 1883, 0,
    -- subscribe topic with QoS = 0
    client:subscribe("dw/air", 0, function(client)
  print("subscribe success")
    -- publish a message with QoS = 0, retain = 0
    client:publish("dw/air", pm25, 0, 0, function(client)
  function(client, reason)
    print("failed reason: " .. reason)

After the data is sent to the Watson IoT Platform server, you can visualize and manipulate the data as needed. Furthermore, a key feature of the MQTT protocol is that it allows the broker (server) to send messages back to the device to potentially control the device’s behavior. For example, we should set up a special topic on the server that all devices subscribe to. The server can then post a message to the topic to control an individual device’s sample frequency. The message needs to start with a device ID, followed by a space, and end with an integer number to specify the sampling interval for the device to sample air quality data (it is currently set to 10 minutes in our code example).

-- In the m:connect loop
client:subscribe("dw/control", 0)

-- ... ...

m:on("message", function(client, topic, data)
  if topic == "dw/control" then
t = {}
for k, v in string.gmatch(data, "[^%s]+") do
    t[k] = v

if t[0] == device_id then
    -- Change the main loop interval to t[1]

Through creative use of MQTT topics, we can even program sensor devices to communicate with each other. You can read about it in my MQTT article.


In this article, I discussed how to build an IoT air quality sensor using the NodeMCU hardware board and development kit, and then connect the device to IBM Watson IoT Platform’s MQTT service. The optional MQTT support module is very useful for IoT devices on the NodeMCU platform. The IBM Watson IoT Platform provides an easy-to-use hosted service to aggregate and manage data from MQTT-enabled IoT devices.

Michael Yuan