Build a distributed temperature sensor with Raspberry Pi
What’s involved?
AM2302 is a cheap, but precise temperature and humidity sensor. You can get one from eBay for as little as £5. It has temperature range of -40 to 80 degrees C with 1 degrees C accuracy, and 0-100% humidity with 2-5% accuracy. According to the DHT11, DHT22 and AM2302 Sensors overview. The only difference between DHT22 and AM2302 is AM2302 has a 5.1K pullup resistor.
Connect AM2302 to RPi4
Raspberry Pi GPIO Pinout is useful to identify the pins.
I chose to connect:
-
- -> pin 17 (3v3 power)
- data -> pin 37 (GPIO 26)
-
- -> pin 39 (GND)
Install AM2302 driver
The Adafruit_Python_DHT is a simple driver written in C and Python. Although deprecated, it is still good for my need. The more up-to-date driver is the Adafruit_CircuitPython_DHT package, but this one requires Adafruit CircuitPython. Simple is the best. Since I am using Manjaro Arm, it has added benefit of Arch User Repository. Luckily, there is AUR: python-adafruit_dht.
The AUR PKGBUILD is written for armv6h and armv7h. However, as we know, the source code is written in C and Python, which should complie to aarch64 target just fine.
yay python-adafruit_dht
Test AM2302 data reading
Once AM2302 is connected and AM2302 driver is installed,
I can begin the testing.
As we can see from the source code,
the driver needs read & write access to /dev/gpiomem
.
As a temporary solution, I granted myself RW permission to
this device:
sudo setfacl -m u:$USER:rw /dev/gpiomem
Next, I cloned the AM2302 driver repo. It contains example code. The one I am interested in is examples/AdafruitDHT.py. Simply type in the terminal:
./examples/AdafruitDHT.py 2302 26
It should print current temperature and humidity.
Build a database
I plan to take a measurement every 5 seconds, so that this is more than the sampling period (2 seconds) requires for AM2302. This is also frequent enough for me to display a temperature curve throughout the day. The only problem is the huge data it is going to produce. I don’t want to write to the un-reliable micro-SD card in the RPi4. Fortunately, I have a NAS!
MariaDB is a fork of MySQL with some extensions that you only get if you pay for MySQL. PostgreSQL is more advanced, but it suffers some performance issues.
Data produced by RPi4:
- timestamp
- temperature
- humidity
I need to build a table the has 3 coloums, with timstamp as the primary key.
CREATE DATABASE bel_env;
USE bel_env;
CREATE TABLE rpi4_am2302_measurements (
measurement_time TIMESTAMP PRIMARY KEY,
temperature FLOAT not null,
humidity FLOAT not null
);
CREATE USER 'USER'@'ip' IDENTIFIED BY 'password';
GRANT ALL ON bel_env.* to 'USER'@'ip' IDENTIFIED BY 'password' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;
The RPi4 does not have a Real Time Clock, it requires time update
every time it boots up. This leaves a period where timestamp
cannot be accurately created.
Fortunately, the TIMESTAMP
data type in MariaDB
will default to CURRENT_TIMESTAMP
when value is not supplied.
Change bind address of MariaDB server from 127.0.0.1
to 0.0.0.0
.
I read the How to install Mariadb on Ubuntu 20.04, and How to enable Remote access to your MariaDB/MySQL database
Store temperature measurements in the database
measure.py
#!/usr/bin/python
import time
import getpass
import Adafruit_DHT
from mysql.connector import connect, Error
sensor = Adafruit_DHT.AM2302
pin = 26
try:
with connect(
host="ip",
user="USER",
password=getpass.getpass(),
database="bel_env"
) as connection:
with connection.cursor() as cursor:
while True:
# Try to grab a sensor reading. Use the read_retry method which will retry up
# to 15 times to get a sensor reading (waiting 2 seconds between each retry).
humidity, temperature = Adafruit_DHT.read_retry(sensor, pin)
cursor.execute(
"INSERT INTO rpi4_am2302_measurements(temperature, humidity) VALUES (%(temp)s, %(hum)s)",
{
'temp': "{:.1f}".format(temperature),
'hum': "{:.1f}".format(humidity)
}
)
connection.commit()
time.sleep(4)
except Error as e:
print(e)
temp-mon.service
[Unit]
Description = Measure temperature and upload to DB
Requires = network-online.target
After = network-online.target
[Service]
User = temp-mon
DynamicUser = yes
StandardInput = file:/opt/rpi4-temp/db-password
ExecStartPre = !/usr/bin/setfacl -m u:temp-mon:rw /dev/gpiomem
ExecStart = /opt/rpi4-temp/measure.py
ExecStopPost = !/usr/bin/setfacl -x u:temp-mon:rw /dev/gpiomem
[Install]
WantedBy = multi-user.target
Visualise data
See Tutorial: Simple line graph with v4
get-temp.php
<?php
$username = "user";
$password = "pw";
$host = "IP";
$database = "bel_env";
$urlQueries = array();
parse_str($_SERVER['QUERY_STRING'], $urlQueries);
$startTime = htmlspecialchars($urlQueries['start_time']);
if (array_key_exists('end_time', $urlQueries)) {
if (!empty($urlQueries['end_time'])) {
$endTime = htmlspecialchars($urlQueries['end_time']);
}
else {
$endTime = date("Y-m-d\TH:i:s");
}
}
else {
$endTime = date("Y-m-d\TH:i:s");
}
$dbLink = new mysqli($host, $username, $password, $database);
$queryString = sprintf("
SELECT * FROM rpi4_am2302_measurements
WHERE measurement_time >= '%s' AND measurement_time <= '%s';
",
$dbLink->real_escape_string($startTime),
$dbLink->real_escape_string($endTime)
);
$res = $dbLink->query($queryString);
$data = array();
for ($i=0; $i < $res->num_rows; $i++) {
$data[] = $res->fetch_assoc();
}
echo json_encode($data);
$dbLink->close();
?>
index.html
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<style> /* set the CSS */
.temp-line {
fill: none;
stroke: coral;
stroke-width: 2px;
}
.hum-line {
fill: none;
stroke: blue;
stroke-width: 2px;
}
</style>
</head>
<body>
<p>
start time: <input type="datetime-local" id="startTime">
</p>
<p>
end time: <input type="datetime-local" id="endTime">
</p>
<button onclick="submit_datetime()">Submit datetime</button>
<div id="temp-graph"></div>
<script>
var margin = {top: 20, right: 20, bottom: 30, left: 50},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var parseTime = d3.timeParse("%Y-%m-%d %H:%M:%S");
var x = d3.scaleTime().range([0, width]);
var temp = d3.scaleLinear().range([height, 0]);
var hum = d3.scaleLinear().range([height, 0]);
var tempLine = d3.line()
.x(function(d) {return x(d.measurement_time);})
.y(function(d) {return temp(d.temperature);});
var humLine = d3.line()
.x(function(d) {return x(d.measurement_time);})
.y(function(d) {return hum(d.humidity);});
const svg = d3.select("#temp-graph").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
function submit_datetime() {
const startTime = document.getElementById("startTime").value;
const endTime = document.getElementById("endTime").value;
const stQStr = "start_time="+startTime;
const etQStr = "end_time="+endTime;
d3.json("get-temp.php?"+stQStr+"&"+etQStr, function(error, data) {
if (error) throw error;
data.forEach(function(d) {
d.measurement_time = parseTime(d.measurement_time);
});
x.domain(d3.extent(data, function(d) {return d.measurement_time; }));
temp.domain([d3.min(data, function(d) {return d.temperature;}), d3.max(data, function(d) {return d.temperature;})]);
hum.domain([d3.min(data, function(d) {return d.humidity;}), d3.max(data, function(d) {return d.humidity;})]);
svg.append("path")
.data([data])
.attr("class", "temp-line")
.attr("d", tempLine);
svg.append("path")
.data([data])
.attr("class", "hum-line")
.attr("d", humLine);
svg.append("g")
.attr("transform", "translate(0,"+height+")")
.call(d3.axisBottom(x));
svg.append("g")
.call(d3.axisLeft(temp));
svg.append("g")
.call(d3.axisRight(hum));
});
}
</script>
</body>