Name | Student ID
-|-
Viktor Hultsten | vh222mx
# A Cheap Temperature Monitor
Lets build a microcontroller that measures air temperature, frequently sends the data to a database, from which you can make any decisions you want. Visualize it, compare it, and take action when a threshold is exceeded!
### But why?
I don't know about you, but I have a room that simply can't be cold enough. It absorbs heat! If I open a window, nothing happens. I need a fan that creates an over pressure, and the fan will be placed in another room. The obvious solution is to have a sensor in the warm room that sends data to a central logic unit, and when a given temperature is exceeded this central unit will tell a fan to activate.
Another reason to do this is obviously to see how easy and fun it is to create an IoT unit by yourself, and to a reasonable cost!
### But I have a life, when am I supposed to do this?
The time to complete this project is dependent on how experienced you are with the given tools. Even if you're completely new to all of it, I think you'll manage to do it within a day!
## Let's go! What do I need?
You might have a micro USB cable and a USB adapter lying around. Perfect! I mean, who throws away old electronics and cables?
What | Cost (approx.)
-|-
ESP32 unit (ESP8266 would also work) | 94 SEK ([sizable.se](https://sizable.se/P.CE9S1/ESP32))
ES18B20 temperature sensor | 21 SEK ([sizable.se](https://sizable.se/P.36D4D/DS18B20-Temperatursensor))
Dupont cables to wire it all up | 24 SEK ([sizable.se](https://sizable.se/P.4LTWU/Dupont-kopplingskablar-Hona-Hona-40-pin))
Micro USB cable | -
USB adapter or powerbank | -
A UN*X Computer (I'm using Linux) | -
Interests no-one seem to understand | -
<b>Total</b> | <b>139 SEK</b>
:exclamation: Some micro USB cables won't work. Make sure you have a couple of different ones.
Additionally, we will need some software to make this work:
What | Why
-|-
Visual Studio Code | Code editing
PlatformIO (VSCode Extension) | IDE and flashing tools
Node Red | API layer and data visualization
MySQL | Data storage
Docker Compose | Containerizing
## Initial Setup
1. Start by installing Visual Studio Code
2. Then, open the Extensions tab and install PlatformIO
3. Install Docker and Docker Compose on the server (see next chapter). When completed, check the versions and make sure Docker Compose is greater than or equal to v2.6.0
## Platform
Ideally, you would have a server for the data storage, but your normal computer works as well. Be aware though, that the computer needs to be turned on and connected to the network whenever the ESP32 or a user wants to interact with the service. Why not find an old laptop or computer and turn it into an always turned on server? Perfect for things like these!
## Circuit Diagram
Wire up the temperature sensor with the ESP32 in accordance with Figure 1. V+ to 3V3, V- to GND and output signal to D4.

*Figure 1 - Circuit diagram*
## Code Time!
In Visual Studio Code, open the PlatformIO extension. Click `New Project` and give the project a name, select a board (the unit above is a _DOIT ESP32 DEVKIT V1_), keep the rest unchanged and press `Finish`.
We are going to need two additional libraries. Start by installing them on the PlatformIO extension page: click `Libraries` and search and install the following: _DallasTemperature_ and _OneWire_.
Let's open the project and main.cpp file. We start by including the named libraries.
```cpp
#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include "WiFi.h"
void setup() {
}
void loop() {
}
```
We are given two functions by the Arduino API. setup() will run once at startup and loop() will run over and over again. To make the program as lightweight as possible, we will declare a couple of macros. These will be converted before compiling. We will also initiate a couple of instances of the libraries we imported. These are parts of what is called Object Oriented Programming, which means each object/instance owns it's data. We won't dive too deep into that, but it's a great feature if you like OOP. Just above setup() we add the macros and variables:
```cpp
#define TEMPERATURE_PIN 4
#define SENSOR_ID 100
OneWire oneWire(TEMPERATURE_PIN);
DallasTemperature sensors(&oneWire);
```
Let's take a look at setup(). What do want to do here? We need to tell the ESP32 that we want to listen for signals on pin 4 (TEMPERATURE_PIN). We do so by calling a library method, that has abstracted away parts of the sensor communication. We also want to connect to the Wi-Fi network. Add two new macros and set the values to your Wi-Fi network.
```cpp
...
#define WIFI_SSID "my_network_name"
#define WIFI_PASSPHRASE "my_cats_name"
...
void setup() {
sensors.begin();
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
// WiFi.status() returns WL_CONNECTED when it's connected
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
}
}
```
By now, the ESP32 has started the temperature sensor and connected to the Wi-Fi network. We will now configure the loop() with the commands that will be run repeatedly. We run another library method that will tell the sensor to return the latest temperature measurement. We then put this into a variable. If the sensor is unplugged, the variable will be set to -255, which we obvsiously don't want to insert into the database. Let's add another macro at the top and an if statement in the loop().
```cpp
...
#define SENSOR_UNPLUGGED -255
...
void loop() {
sensors.requestTemperatures();
float temperature = sensors.getTempCByIndex(0);
if (temperature != SENSOR_UNPLUGGED) {
// Only do something if the sensor is plugged in
}
}
```
OK, so now we know that 1) the sensor is plugged in and 2) the air temperature. Last step for our ESP32 is to send it to an API that will take care of the data further. For this, we will do an HTTP request, which is also part of the WiFi.h library we used to connect to the Wi-Fi network. Let's create a new function for this feature, that takes the temperature as an argument.
We also need to know the IP Address and the port for the API server. We haven't created it yet, so we will give it some example data. Add two new macros below the other macros, then add the function just above setup().
```cpp
...
#define API_SERVER_IP IPAddress(192, 168, 0, 1)
#define API_SERVER_PORT 1234
...
void send_temperature(float temperature) {
WiFiClient client;
boolean connected_to_api = client.connect(API_SERVER_IP, API_SERVER_PORT);
}
...
```
If connected_to_api is true, that means the ESP32 has successfully connected to the server. If it is false, it cannot connect and we should stop the function and try again next time. If everything seems OK, we will simply print the data to the server. We do this by constructing a string that contains the data to be sent. We won't dive into why the message is constructed the way it is, it is basic HTTP syntax and there are plenty of Youtube videos about it if you want to know more (which you do!).
```cpp
void send_temperature(float temperature) {
WiFiClient client;
boolean connected_to_api = client.connect(API_SERVER_IP, API_SERVER_PORT);
if (!connected_to_api) return; // Note the exclamation mark
String message =
String("POST /api/sensor/") + SENSOR_ID +
String("/temp/") + temperature +
String(" HTTP/1.1");
// This string could look like this: "POST /api/sensor/100/temp/23.38 HTTP/1.1"
client.println(message);
client.println("Connection: close");
client.println();
}
```
We have two things left until the ESP32 code is basically done. We need to call send_temperature() and we will implement a built in function that put the ESP32 to a sleep state, in which it won't be as energy consuming:
```cpp
void loop() {
sensors.requestTemperatures();
float temperature = sensors.getTempCByIndex(0);
if (temperature != SENSOR_UNPLUGGED) {
send_temperature(temperature); // Add the function call
}
// Tells the ESP32 to wake up after one minute after it's put to sleep
esp_sleep_enable_timer_wakeup(1000000 * 60);
// Tells the ESP32 to go to sleep
esp_deep_sleep_start();
}
```
So? Are we done? With the ESP32, basically yes, but we don't have a server to send the data to. Let's get that up and running now.
## Data Storage
Create a new text file and name it docker-compose.yml by typing `touch docker-compose.yml` in the terminal. Open it with your favorite text editor. In here, we will describe the Docker containers we are interested in. Containers are great--they run isolated in their own environment and we simply tell it what environment it should be and what it is supposed to do. There are obviously plenty of videos about this to, so let's get started with the basic instructions here:
```yaml
services:
node_red:
image: nodered/node-red # the "program" we want to run
container_name: node_red # gives the container a name
ports:
- 1880:1880 # exposes the port on the host machine
networks:
- node_network # adds container to a virtual network
restart: unless-stopped # container restart policy
node_red_db:
image: mysql
container_name: node_red_db
networks:
- node_network
restart: unless-stopped
environment:
MYSQL_DATABASE: 'db'
MYSQL_USER: 'user'
MYSQL_PASSWORD: 'password'
MYSQL_ROOT_PASSWORD: 'password'
TZ: 'Europe/Stockholm' # your time zone
networks:
node_network: # creates a virtual docker network. Don't forget the : in the end!
```
Now, using the terminal in the directory where you stored this file, run the command to start the conatiners: `sudo docker-compose up -d`. Docker will now attempt to start the containers. It might take a while to download all necessary files.
When it has completed, you should have a node red server up and running! Get a hold of your IP address. If you don't know what it is, open a terminal, run `ip a | grep "inet 192"` and you should see something like for example 192.168.1.45. Remember this!
We could have added a start-up script in the Docker Compose file to let MySQL setup it the way we want right away, but I think it's easier to follow if we take it one step at a time. So, let's connect to our MySQL database!
Open a terminal and run `sudo docker exec -it node_red_db mysql -p`. You will be prompted to type the password (and possibly also sudo password), which we set to _password_ in the Docker Compose file. Hopefully our terminal prompt now looks like this:
```bash
mysql>
```
Type `show databases;`. You should see a list of databases of which one should be just _db_. We select it by typing `use db;`. Type `show tables;` to see tables. It should be empty, so we will create a table now! If you want to make a row break you can simply press `Enter`, the terminal won't execute the command until you type the `;` at the end. Don't get confused about the arrows below, they imply that I have made a row break. At the end, you should get a similar response. If you get an error, note the error code and the error message and I am sure you will solve it.
```sql
mysql> CREATE TABLE temperatures (
-> id INT AUTO_INCREMENT PRIMARY KEY,
-> sensor_id INT,
-> temperature DECIMAL(5,2),
-> time TIMESTAMP DEFAULT NOW()
-> );
Query OK, 0 rows affected (0.02 sec)
```
You now have a table! If you want, you can type `desc temperatures;` to get a description of the table:
```sql=
mysql> DESC temperatures;
+-------------+--------------+------+-----+-------------------+-------------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+-------------------+-------------------+
| id | int | NO | PRI | NULL | auto_increment |
| sensor_id | int | YES | | NULL | |
| temperature | decimal(5,2) | YES | | NULL | |
| timestamp | timestamp | YES | | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+-------------+--------------+------+-----+-------------------+-------------------+
4 rows in set (0.00 sec)
```
This tells us that we have three columns; a unique id that will automatically increment when we add entries, a sensor ID, a temperature that is a number with two decimals and a timestamp that will automatically be set to the current time when an entry is added.
You can do a lot of stuff with MySQL, but let's keep it simple. We need to at least be able to insert and fetch data from the database. Let's give it a try:
```sql=
mysql> INSERT INTO temperatures (sensor_id, temperature) VALUES (100, 13.37);
Query OK, 1 row affected (0.01 sec)
mysql> SELECT * FROM temperatures;
+----+-----------+-------------+---------------------+
| id | sensor_id | temperature | timestamp |
+----+-----------+-------------+---------------------+
| 1 | 100 | 13.37 | 2022-06-29 12:00:00 |
+----+-----------+-------------+---------------------+
1 row in set (0.00 sec)
```
Perfect! But we don't want to do this manually, of course. We want our Node Red server to handle this based on some sort of triggers, which is the next step.
## Configuring the Node Red Server
I hope you remember the IP address from before, because now we need it. Open a browser and type `http://[YOUR_IP]:1880/`. You simply drag and drop the nodes you want to the workflow. Before we start, we need to install two libraries. Click the hamburger menu at the top right and select _Manage palette_. Click the `Install` tab, then search for and install the following:
- node-red-dashboard
- node-red-node-mysql
Back to the workflow, we will start by adding an `http in` node. Set Method to `POST` and the URL to what we set in the ESP32 code. It is constructed like this:
```
/api/sensor/[SENSOR_ID]/temp/[TEMPERATURE]
```
So to extract the SENSOR_ID and TEMPERATURE we should type the following in the URL field:
```
/api/sensor/:sensor_id/temp/:temp
```
The `:` implies that we want to extract the values as variables. Click `Done` and add a `function` node. The variables from above is stored in `msg.req.params`. We will also construct a MySQL query to send to our database. `msg` is an object that Node Red uses everywhere. We simply add an entry in the existing msg object and they have specific names according to the documentation. The MySQL library expects a `topic` for the query and `payload` for the search terms (replaced with the question marks):
```javascript=
const { sensor_id, temp } = msg.req.params;
msg.topic = "INSERT INTO temperatures (sensor, temperature) VALUES (?,?);";
msg.payload = [sensor_id, temp];
return msg;
```
Now, we send this data onward. Add a `mysql` node. Add a new database by pressing the pen. The host will be the same name as we set in the Docker Compose file, so `node_red_db`. Port is unchanged. User is `user`, password is `password` and database is `db`. Click `Add`. When you're done, click `Deploy` in the top right.
It should look like this:

Let's take a break from Node Red for a while and make the last configurations on the ESP32 so we can try this out!
## Completing the ESP32
Let's go back to the ESP32 code and add the server IP and port. Your ESP32 code should now look like below.
Make sure to update row 8, 9, 11, 12:
- WIFI_SSID should be your network name (with quotation marks)
- WIFI_PASSPHRASE should be your network passphrase (with quotation marks)
- API_SERVER_IP should be the IP you retrieved earlier, for example IPAddress(192, 168, 1, 45)
- API_SERVER_PORT should be 1880 (node red specifically uses that port) if you havent changed anything
Optionally, you can change row 7 to another ID. For example, if you add another sensor you want separate IDs to be able to separate them.
```cpp=
#include <Arduino.h>
#include <OneWire.h>
#include <DallasTemperature.h>
#include "WiFi.h"
#define TEMPERATURE_PIN 4
#define SENSOR_ID 100
#define WIFI_SSID "my_network_name"
#define WIFI_PASSPHRASE "my_cats_name"
#define SENSOR_UNPLUGGED -255
#define API_SERVER_IP IPAddress(192, 168, 1, 45)
#define API_SERVER_PORT 1880
OneWire oneWire(TEMPERATURE_PIN);
DallasTemperature sensors(&oneWire);
void send_temperature(float temperature) {
WiFiClient client;
boolean connected_to_api = client.connect(API_SERVER_IP, API_SERVER_PORT);
if (!connected_to_api) return;
String message =
String("POST /api/sensor/") + SENSOR_ID +
String("/temp/") + temperature +
String(" HTTP/1.1");
client.println(message);
client.println("Connection: close");
client.println();
}
void setup() {
sensors.begin();
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
}
}
void loop() {
sensors.requestTemperatures();
float temperature = sensors.getTempCByIndex(0);
if (temperature != SENSOR_UNPLUGGED) {
send_temperature(temperature);
}
esp_sleep_enable_timer_wakeup(1000000 * 60);
esp_deep_sleep_start();
}
```
Now, press the build button in the bottom left of Visual Studio Code. To allow the ESP32 to accept flashing, you need to hold the BOOT button. Keep it pressed for 10-15 seconds, that should be sufficient. Then, put the ESP32 wherever you want to measure the air temperature.
## ...Is It Working?
In the MySQL terminal we configured the database in earlier, let's type another query and see if data is being inserted. If you have let it run for a couple of minutes, it should hopefully look something like this:
```sql=
mysql> SELECT * FROM temperatures;
+----+-----------+-------------+---------------------+
| id | sensor_id | temperature | timestamp |
+----+-----------+-------------+---------------------+
| 1 | 100 | 13.37 | 2022-06-29 12:00:00 |
| 2 | 100 | 26.56 | 2022-06-29 14:24:47 |
| 3 | 100 | 26.56 | 2022-06-29 14:25:48 |
| 4 | 100 | 26.56 | 2022-06-29 14:26:50 |
| 5 | 100 | 26.56 | 2022-06-29 14:27:52 |
+----+-----------+-------------+---------------------+
5 rows in set (0.00 sec)
```
Great! Now, let's add a second trigger in Node Red. Add an `inject` node. In the `msg.topic` entry, you can add a MySQL query. Let's say we want the latest measurement for sensor ID 100. The query would then look like this:
```sql
SELECT * FROM temperatures WHERE sensor_id = 100 ORDER BY id DESC;
```
This filters out the entry where sensor_id equals 100 and the entries are put in descending order (the latest first). Also, set an interval that suits you. For example repeat every minute. This will trigger a MySQL query to be sent once every minute. The `mysql` node will then output a `msg` object containing the result named `msg.payload`. Let's add a `gauge` node (it's part of the dashboard library). In the gauge settings, create a new _Group_ and _Tab_. Then we need to change the Value format. The MySQL node will output an array of entries in the `msg.payload`, and since we only get one entry it will be the first one (`msg.payload[0]`). This will be an object with every column, and since we are interested in the temperature, the variable of interest will be `msg.payload[0].temperature`. Since the `msg` object is used for all nodes, we can simply remove that part as it is implicit. To tell Node Red that it is a variable we use `{{ }}`, so in the Value format we will type `{{payload[0].temperature}}`.
And we are done! Your Node Red workflow should look like this:

Now visit http://[YOUR_IP]:1880/ui to see the dashboard. I've added some more gauges, which gives me a great overview of how bad the distribution of temperature is in my house.
