ICT 360: Introduction to IoT
Mr. Seng Theara
Develop an IoT monitoring system using ESP32 and MQTT
Transmit and manage sensor data using a publish–subscribe architecture
Process and store time-series data using Node-RED and InfluxDB
Visualize real-time environmental data using Grafana dashboards
Apply MicroPython for cloud-integrated embedded systems
BMP280 is a digital sensor that measures:
🌡 Temperature
🌬 Air Pressure
🏔 Altitude (calculated)
We can build a simple weather prediction system using only BMP280 but has limitation
We can:
Limitation
Monitor pressure trend
Detect pressure drop
Estimate altitude
Log temperature
No humidity data
No wind speed
No rainfall detection
Atmospheric or Air pressure is the force per unit area exerted by the weight of air molecules in the Earth's atmosphere.
Where:
PPP = Pressure
FFF = Force
AAA = Area
At sea level, average atmospheric pressure is:
1013.25 hPa or 101,325 Pa
*1 hPa = 100 Pa
Atmospheric pressure exists because:
Air has mass
Mass experiences gravitational acceleration
Gravity pulls air molecules toward Earth
Air molecules collide with surfaces
Pressure is not static. it changes continuously due to:
Temperature
Altitude
Weather systems
Air movement
From Ideal Gas Law:
If volume is constant:
Increasing temperature increases pressure.
Decreasing temperature decreases pressure.
In the atmosphere, this relationship is more complex due to air expansion and movement.
Typical BMP280 specifications:
1 hPa difference corresponds to approx. 8 meters altitude difference.
Pressure resolution: 0.16 Pa
Absolute accuracy: ±1 hPa
Temperature resolution: 0.01°C
Drone altitude measurement
Indoor floor detection
Environmental monitoring
This makes it useful for:
Altitude is the vertical distance of an object above a reference level.
Reason:
Air has mass.
Gravity pulls air downward.
Higher altitude → less air above.
Less air above → lower pressure.
Pressure Can Measure Altitude Atmospheric pressure decreases as altitude increases.
So pressure and altitude are inversely related.
1 hPa pressure decrease ≈ 8–9 meters altitude increase
Pressure Can Measure Altitude Atmospheric pressure decreases as altitude increases.
Example:
If pressure drops by 5 hPa:
Altitude increase ≈ 40 meters.
Barometric altitude is used in:
Drones (height stabilization)
Aircraft altimeters
Hiking watches
Smartphone floor detection
Rocket experiments
Robotics navigation
Barometric altitude is used in:
Drones (height stabilization)
Aircraft altimeters
Hiking watches
Smartphone floor detection
Rocket experiments
Robotics navigation
from machine import I2C
import time
class BMP280:
def __init__(self, i2c, addr=0x76):
self.i2c = i2c
self.addr = addr
self.sea_level_pressure = 1011.7
chip_id = self.i2c.readfrom_mem(self.addr, 0xD0, 1)
if chip_id[0] != 0x58:
raise Exception("BMP280 not found")
self.i2c.writeto_mem(self.addr, 0xE0, b'\xB6')
time.sleep(0.2)
calib = self.i2c.readfrom_mem(self.addr, 0x88, 24)
def to_signed(val):
if val > 32767:
val -= 65536
return val
self.dig_T1 = calib[1] << 8 | calib[0]
self.dig_T2 = to_signed(calib[3] << 8 | calib[2])
self.dig_T3 = to_signed(calib[5] << 8 | calib[4])
self.dig_P1 = calib[7] << 8 | calib[6]
self.dig_P2 = to_signed(calib[9] << 8 | calib[8])
self.dig_P3 = to_signed(calib[11] << 8 | calib[10])
self.dig_P4 = to_signed(calib[13] << 8 | calib[12])
self.dig_P5 = to_signed(calib[15] << 8 | calib[14])
self.dig_P6 = to_signed(calib[17] << 8 | calib[16])
self.dig_P7 = to_signed(calib[19] << 8 | calib[18])
self.dig_P8 = to_signed(calib[21] << 8 | calib[20])
self.dig_P9 = to_signed(calib[23] << 8 | calib[22])
self.i2c.writeto_mem(self.addr, 0xF4, b'\x27')
self.i2c.writeto_mem(self.addr, 0xF5, b'\xA0')
self.t_fine = 0
def _read_raw(self):
data = self.i2c.readfrom_mem(self.addr, 0xF7, 6)
adc_p = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4)
adc_t = (data[3] << 12) | (data[4] << 4) | (data[5] >> 4)
return adc_t, adc_p
def _compensate_temperature(self, adc_t):
var1 = (((adc_t >> 3) - (self.dig_T1 << 1)) * self.dig_T2) >> 11
var2 = (((((adc_t >> 4) - self.dig_T1) *
((adc_t >> 4) - self.dig_T1)) >> 12) *
self.dig_T3) >> 14
self.t_fine = var1 + var2
T = (self.t_fine * 5 + 128) >> 8
return T / 100
def _compensate_pressure(self, adc_p):
var1 = self.t_fine - 128000
var2 = var1 * var1 * self.dig_P6
var2 = var2 + ((var1 * self.dig_P5) << 17)
var2 = var2 + (self.dig_P4 << 35)
var1 = ((var1 * var1 * self.dig_P3) >> 8) + ((var1 * self.dig_P2) << 12)
var1 = (((1 << 47) + var1) * self.dig_P1) >> 33
if var1 == 0:
return 0
p = 1048576 - adc_p
p = (((p << 31) - var2) * 3125) // var1
var1 = (self.dig_P9 * (p >> 13) * (p >> 13)) >> 25
var2 = (self.dig_P8 * p) >> 19
p = ((p + var1 + var2) >> 8) + (self.dig_P7 << 4)
return p / 256
def _read_all(self):
adc_t, adc_p = self._read_raw()
temp = self._compensate_temperature(adc_t)
pressure = self._compensate_pressure(adc_p)
return temp, pressure
@property
def temperature(self):
temp, _ = self._read_all()
return temp
@property
def pressure(self):
_, pressure = self._read_all()
return pressure
@property
def altitude(self):
pressure_hpa = self.pressure / 100
return 44330 * (1 - (pressure_hpa / self.sea_level_pressure) ** 0.1903)
Save this as bmp280.py in micropython device
from machine import Pin, I2C
import bmp280
import time
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
sensor = bmp280.BMP280(i2c)
while True:
print("Temperature:", sensor.temperature, "°C")
print("Pressure:", sensor.pressure / 100, "hPa")
print("Altitude:", sensor.altitude, "m")
print("---------------------------")
time.sleep(2)
BMP280
Temperature
Pressure
Altitute
MQTT Protocol
InfluxDB
ESP32
Expansion Board or External Shield
import network, time, ujson
from umqtt.simple import MQTTClient
from machine import Pin, I2C
from bmp280 import BMP280
SSID = "TP-LINK_56C612"
PASSWORD = "06941314"
BROKER = "test.mosquitto.org"
PORT = 1883
CLIENT_ID = b"esp32_bmp280_1"
TOPIC = b"/aupp/esp32/bmp280"
KEEPALIVE = 30
def wifi_connect():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print("Connecting to WiFi...")
wlan.connect(SSID, PASSWORD)
t0 = time.ticks_ms()
while not wlan.isconnected():
if time.ticks_diff(time.ticks_ms(), t0) > 20000:
raise RuntimeError("Wi-Fi connect timeout")
time.sleep(0.3)
print("WiFi OK:", wlan.ifconfig())
return wlan
def make_client():
return MQTTClient(client_id=CLIENT_ID, server=BROKER, port=PORT, keepalive=KEEPALIVE)
def connect_mqtt(c):
time.sleep(0.5)
c.connect()
print("MQTT connected")
def main():
wifi_connect()
# Setup BMP280
i2c = I2C(0, scl=Pin(22), sda=Pin(21))
sensor = BMP280(i2c)
client = make_client()
while True:
try:
connect_mqtt(client)
while True:
data = {
"temperature": round(sensor.temperature, 2),
"pressure": round(sensor.pressure / 100, 2),
"altitude": round(sensor.altitude, 2)
}
msg = ujson.dumps(data)
client.publish(TOPIC, msg)
print("Sent:", msg)
time.sleep(5)
except OSError as e:
print("MQTT error:", e)
try:
client.close()
except:
pass
print("Retrying MQTT in 3s...")
time.sleep(3)
main()
Node-RED is a flow-based programming tool for connecting hardware devices, APIs, and online services.
Developed by IBM
Built on Node.js
Uses a browser-based visual editor
Designed especially for IoT applications
Node-RED works by connecting:
Nodes (blocks of functionality)
With wires
To create a flow
BMP280
Temperature
Pressure
Altitute
MQTT Protocol
ESP32
So, from here the esp32 are reading the sensor value and send it to the Node-RED
In the Node-RED Block, we receive the sensor topic /aupp/esp32/bmp280 and then send it to the influxDB
msg.measurement = "bmp280";
msg.payload = {
temperature: Number(msg.payload.temperature),
pressure: Number(msg.payload.pressure),
altitude: Number(msg.payload.altitude)
};
return msg;BMP280
Temperature
Pressure
Altitute
MQTT Protocol
InfluxDB
ESP32
The Node-RED will send all the data to the influxDB
In InfluxDB, We create a database name aupp_lab and the image below shows all of the sensor data
BMP280
Temperature
Pressure
Altitute
MQTT Protocol
InfluxDB
ESP32
Below is the Grafana dashboard where we get it from the InfluxDB
MQ-5 Gas Sensor used to detect:
LPG
Natural Gas
Methane
Hydrogen
MQ-5 Gas Sensor is commonly used in:
Gas leakage detection
Kitchen safety systems
Industrial safety systems
This sensor can detect these gases when their concentrations are between 200 and 10,000 parts per million (ppm).
PPM stands for parts per million, and it’s a common unit used to measure the concentration of a specific gas in the air. It simply represents the ratio of how many gas molecules of one type exist among a million total gas molecules in the air. For example, if the air has 500 ppm of carbon monoxide, that means out of every 1,000,000 gas molecules, 500 are carbon monoxide, and the other 999,500 molecules are different gases.
| CO Level (ppm) | Effect |
|---|---|
| 0–9 ppm | Safe (normal air) |
| 35 ppm | Headache after 6–8 hours |
| 100 ppm | Headache, dizziness in 1–2 hours |
| 400 ppm | Life-threatening in 3 hours |
| 800+ ppm | Very dangerous |
MQ-5 does NOT directly give exact ppm.
It gives:
Voltage change
Resistance change
To know exact ppm:
Calibrate in clean air
Use datasheet curve
Compute Rs/R0
Map to ppm graph
The analog output (from the AO pin) changes based on the concentration of gas. When there’s more gas in the air, the output voltage increases. When there’s less gas, the output voltage decreases
ESP32 ADC is 12 bits, so it means that it can read the value between 0 to 4095
If we want to know the voltage value from the gas sensor we can do:
Text
from machine import Pin, ADC
import time
# Configure ADC pin
mq5 = ADC(Pin(33))
mq5.atten(ADC.ATTN_11DB)
mq5.width(ADC.WIDTH_12BIT)
while True:
gas_value = mq5.read()
print("Raw ADC Value:", gas_value)
voltage = (gas_value / 4095) * 3.3
print("Voltage: {:.2f} V".format(voltage))
print("----------------------")
time.sleep(1)
We see that the voltage is about 1.35V
MLX90614 is a non-contact infrared temperature sensor.
It measures:
MLX90614 sees heat radiation.It is like a "heat camera" but single pixel.
The sensor:
Detects IR radiation
Converts it into voltage
Uses internal calibration
Outputs digital temperature via I2C
It does NOT measure air temperature directly. It measures thermal radiation from an object.
from machine import I2C
class MLX90614:
def __init__(self, i2c, address=0x5A):
self.i2c = i2c
self.address = address
def read16(self, reg):
data = self.i2c.readfrom_mem(self.address, reg, 3)
return data[0] | (data[1] << 8)
def read_temp(self, reg):
temp = self.read16(reg)
return (temp * 0.02) - 273.15
def read_ambient_temp(self):
return self.read_temp(0x06)
def read_object_temp(self):
return self.read_temp(0x07)
This is the library. Save it as mlx90614.py in micropython device
from machine import Pin, I2C
import time
import mlx90614
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
sensor = mlx90614.MLX90614(i2c)
while True:
print("Ambient:", sensor.read_ambient_temp())
print("Object :", sensor.read_object_temp())
print("-----------------")
time.sleep(1)
Code for Testing
It keeps track of Year, Month, Date, Day, Hour, Minute, and Second Even when power is OFF
| NTP (WiFi) | DS3231 |
|---|---|
| Needs Internet | No Internet needed |
| Not good offline | Works standalone |
| Time lost if WiFi fails | Keeps time with battery |
Why Use DS3231 Instead of NTP?
from machine import I2C
class DS3231:
def __init__(self, i2c, addr=0x68):
self.i2c = i2c
self.addr = addr
def bcd2dec(self, bcd):
return (bcd >> 4) * 10 + (bcd & 0x0F)
def dec2bcd(self, dec):
return (dec // 10 << 4) + (dec % 10)
def get_time(self):
data = self.i2c.readfrom_mem(self.addr, 0x00, 7)
second = self.bcd2dec(data[0])
minute = self.bcd2dec(data[1])
hour = self.bcd2dec(data[2])
day = self.bcd2dec(data[4])
month = self.bcd2dec(data[5])
year = self.bcd2dec(data[6]) + 2000
return (year, month, day, hour, minute, second)
def set_time(self, year, month, day, hour, minute, second):
year = year - 2000 # DS3231 stores year as 00–99
data = bytearray(7)
data[0] = self.dec2bcd(second)
data[1] = self.dec2bcd(minute)
data[2] = self.dec2bcd(hour)
data[3] = self.dec2bcd(1) # Day of week (1=Monday, simple use)
data[4] = self.dec2bcd(day)
data[5] = self.dec2bcd(month)
data[6] = self.dec2bcd(year)
self.i2c.writeto_mem(self.addr, 0x00, data)
Save this as ds3231.py in the micropython device
from machine import Pin, I2C
import ds3231
import time
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
rtc = ds3231.DS3231(i2c)
# Set time: (Year, Month, Day, Hour, Minute, Second)
rtc.set_time(2026, 3, 2, 11, 24, 0)
print("Time Set!")First, you need to set the time based on the current date and time.
from machine import Pin, I2C
import time
import ds3231
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000)
rtc = ds3231.DS3231(i2c)
while True:
now = rtc.get_time()
print("Date: {}-{}-{}".format(now[0], now[1], now[2]))
print("Time: {:02}:{:02}:{:02}".format(now[3], now[4], now[5]))
print("----------------------")
time.sleep(1)
Try unplugged the usb cable for 2mn from your laptop and plug it in again and run this code more if you still see the time is correct or not