A smart home isn’t just a collection of controlling gadgets; it also includes a network of sensors. For example, we need to monitor the temperature in various building zones to control a heating system effectively.
So today, we will create a mesh of thermometers connected to a Wi-Fi network and develop a mobile application for both iOS and Android platforms to monitor the temperature in different rooms. We will utilize multiple ESP32 devices equipped with D18B20 temperature sensors. For cross-platform app development, we will leverage Flutter due to its capability to create a straightforward yet powerful application for this purpose.
The video version of the article is here:
System designThere are dozens of ways to implement such a mesh. There could be solutions with servers, sockets, HTTP requests, etc. I decided to make the system without any servers, and that won’t overload the Wi-Fi network no matter how many sensors we have. We will use UDP broadcasts to exchange data between clients and sensors. If you don’t know what UDP or TCP is, I would recommend watching this video:
In a nutshell, we have the next scenario:
- ESP32 boards with DS18B20 thermosensors are connected to a Wi-Fi network
- A user opens a Flutter mobile application on iPhone or Android (it doesn’t matter).
- The mobile app sends a broadcast message to all devices in the local network: “If you are a thermosensor, give me your name and the temperature.”
- The ESP32 responds with a similar broadcast message to all network devices: “I am the sensor, my id is “xxx, ” and my current temperature is XX.XX degrees celcium.
- All devices with opened application update their user interfaces and show the user a temperature from all known sensors.
We will need:
- ESP32 — a board with Wi-Fi.
- DS18B20 — There are two options: with a module or with resistors. You decide which one to use. The version with a separate resistor requires soldering.
- Charging adapters.
We have two options with thermosensors:
The first option is the easiest. It doesn’t require to solder anything. We are using a temperature sensor with a module that has a resistor on a board. We need to plug jumper wires into pins, and that’s it. The connection will be from:
- ESP32, 3v3 → Module VCC (Male) → Module VCC (Female) → Red wire of DS18B20
- ESP32 GND → Module GND (Male) → Module VCC (Female) → Black wire
- ESP32 D17 → Module DAT (Male) → Module DAT (Female) → Yellow wire
The second is a bit different:
- ESP32 3v3 → DS18B20 Red wire
- ESP GND → DS18B20 Black wire
- ESP D17 → DS18B20 Yellow wire
- Yellow wire → Resistor → Red wire
That’s it. We are ready to flash ESP32 with our OS.
Software partLet’s clone a git repository and set up the Visual Studio Code:
git clone https://github.com/Nerdy-Things/03-esp32-ds18b20-mesh-with-mobile-flutter.git
Open the folder 03-esp32-ds18b20-mesh-with-mobile-flutter/esp32 with Visual Studio Code (ESP-IDF plugin should be installed).
Then set up the ESP-IDF, open the Command Pallete (View -> Command Pallete)
Then search for ESP-IDF: Configure ESP-IDF extension.
Press Express and install ESP-IDF & ESP-IDF Tools. We can install ESP-IDF & esp-tools to the same folder.
The app has five modules:
- main — application entry point. app_main.c -> void app_main will be triggered when you install the software on ESP32.
- nerdy_mac_address — provides a mac address of the device. We will use this mac address to differentiate several ESP boards.
- nerdy_udp_client — sends UDP messages.
- nerdy_udp_server — the module listens to incoming UDP messages.
- nerdy_wifi — connects ESP32 board to the Wi-Fi network.
- nerdy_temperature — it provides a temperature value from the sensor.
A quick walk through the ESP-IDF code. All modules are defined in CMaleLists.txt
# The following lines of boilerplate have to be in your project's CMakeLists
# in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16)
set(EXTRA_COMPONENT_DIRS
nerdy_wifi
nerdy_udp_client
nerdy_udp_server
nerdy_mac_address
nerdy_temperature
)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(app_main)
Wi-Fi configurationTo connect to a Wi-Fi access point, we need to have SSID and password for a wi-fi network. We need to open the nerdy_wifi module and copy the file:
nerdy_wifi_config.c.example → nerdy_wifi_config.c
Then replace the credential in the file nerdy_wifi_config.c with your wi-fi name & password.
#define WIFI_SSID "SSID_NAME" // SET YOUR WI-FI ACCESS POINT NAME HERE
#define WIFI_PWD "SSID_PASSWORD" // SET YOUR WI-FI PASSWORD HERE
File nerdy_wifi.c has a method that will try to connect to a network
void nerdy_wifi_connect()
{
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL));
// Initialize default station as network interface instance (esp-netif)
esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
assert(sta_netif);
// Initialize and start WiFi
wifi_config_t wifi_config = {
.sta = {
.ssid = WIFI_SSID,
.password = WIFI_PWD,
.scan_method = DEFAULT_SCAN_METHOD,
.sort_method = DEFAULT_SORT_METHOD,
.threshold = {
.rssi = DEFAULT_RSSI,
.authmode = DEFAULT_AUTHMODE,
},
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
}
When the ESP32 board connects to the network, it will notify the event_handler method.
static void event_handler(void* arg, esp_event_base_t event_base,
int32_t event_id, void* event_data)
{
ESP_LOGI(TAG, "Received event: %s %" PRId32 , event_base, event_id);
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
nerdy_wifi_ip_address_clear();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
esp_wifi_connect();
nerdy_wifi_ip_address_clear();
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
nerdy_wifi_ip_address_clear();
ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
nerdy_wifi_ip_address_save(event->ip_info.ip);
ESP_LOGI(TAG, "got ip: %s broadcast: %s", nerdy_wifi_ip_address, nerdy_wifi_ip_broadcast);
}
}
The final event is IP_EVENT && IP_EVENT_STA_GOT_IP. We will store the ESP32 ip address in a file variable called nerdy_wifi_ip_address. It will also store a broadcast address. For example, if our IP is 192.168.1.72, the broadcast IP will be 192.168.1.255. If we send a UDP message to an IP that ends with 255 — it will be a broadcast message, and all devices in our local subnetwork will receive it.
OS entry pointWhen we power in the ESP board, it runs main.c -> app_main()
/**
* App entry point
*/
void app_main(void)
{
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// Connect ESP32 to a Wi-Fi network.
// Change Access Point Name and Password in the config.
// Read the README.md or nerdy_wifi/README.md for details!!!
nerdy_wifi_connect();
// Initiliaze the DS18B20 sensor
nerdy_temperature_init();
// Start UDP server, it will listen to a port #UDP_PORT_SERVER
nerdy_udp_server_start(UDP_PORT_SERVER);
// Register to messages from UDP server
esp_event_handler_instance_t instance;
ESP_ERROR_CHECK(
esp_event_handler_instance_register(
NERDY_UDP_SERVER_EVENT,
NERDY_UDP_SERVER_EVENT_MESSAGE,
&udp_server_event_handler,
NULL,
&instance
)
);
}
The main method initializes each part of the system, triggers the Wi-Fi connection, prepares the temperature sensor, and starts listening to incoming UDP messages.
The ESP32 will listen to port 54679. If the board receives a message with a text update, it will send a broadcast message to all devices with a mac address and a temperature value. We send a message five times because UDP isn’t very reliable, and half of the messages will not be received by some phones (especially if you have a Pixel XL device, Big hello to Google)
Listening to messages:
/**
* Receives messages from UDP server
*/
static void udp_server_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data)
{
if (event_base != NERDY_UDP_SERVER_EVENT) return;
if (event_id == NERDY_UDP_SERVER_EVENT_MESSAGE) {
char *message = (char*)event_data;
ESP_LOGI(TAG, "Received in main: %s", message);
if (strncmp(message, UPDATE_MESSAGE, strlen(UPDATE_MESSAGE)) == 0) {
// Send message 5 times in a row, with 1 second delay, because not all message will be received instantly (Pixel bug)
for (int i = 0; i < 5; i++) {
send_udp_temperature();
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
}
}
And the method that sends a message:
static void send_udp_temperature()
{
char *message;
// Build a message
asprintf(&message, "{\"mac_address\": \"%s\" , \"temperature\": \"%f\"}\n", nerdy_get_mac_address(), nerdy_temperature_get());
// Send message
nerdy_udp_client_send_message(nerdy_wifi_ip_broadcast, UDP_PORT_CLIENT, message);
// Release message from memory
free(message);
// Log to the console
ESP_LOGI(TAG, "Message is sent to %s %d", nerdy_wifi_ip_broadcast, UDP_PORT_CLIENT);
}
The message format:
{"mac_address": "REAL_MAC_ADDRESS" , "temperature": "20.0000"}"
I won’t go through each module because it would take a lot of time and be boring, so let me know in the comments if there is something that should be clarified.
In a nutshell, we have the next flow:
- ESP32 connects to a Wi-Fi network.
- Listens to port 54679 for broadcast messages “update.”
- After each message with the text “update, ” it sends a message with the Mac address & temperature to the network on the 54678 port.
Let’s build the project. Connect the ESP32 with a USB cable and press the “fire button” in VisualStudioCode.
When we see the message “WIFI: got ip: X.X.X.X, ” it means that the ESP board is online and waiting for messages.
Now, let’s open Wireshark application and see if we receive something on the laptop. To filter target messages, we can add a filter:
udp.port == 54678 || udp.port == 54679
Now, we need to send a message with the text “update” to this ESP32 device. We will use the Socat library (Linux, Mac) or a Netcat tool for Windows. Run from a command line:
socat - UDP-DATAGRAM:255.255.255.255:54679,broadcast
The command line will wait for your input, so type “update” without any quotes. You should see this message in Wireshark:
And the next message should appear with a JSON from ESP32:
It means that we are ready to go to the next stage 😃.
Flutter projectFirst things first: Let's install Flutter SDK. After, open the folder:
03-esp32-ds18b20-mesh-with-mobile-flutter -> mobile_web_clients
In Android Studio. You should see something like this:
All the magic happens in the lib folder:
Let’s review from the top to the bottom:
- model → temperature_sensor.dart — a data model that represents JSON from ESP23
- repository → preference_repository.dart — it stores ESP32 names. Users can assign a custom name to each temperature sensor.
- repository → temperature_sensor_repository.dart — it stores all known ESP devices. Basically, it’s a data-provider class.
- ui → page_sensor_item.dart — UI for the single list item:
- ui → page_sensors_button.dart — Button UI
- ui → page_sensors_list.dart — List of page_sensor_items
- ui → page_sensors_toolbar.dart — UI, top toolbar.
- ui → temperature_sensor_name_dialog.dart — A dialog for sensor’s name
- main.dart — the entry point of the application. The main method will be triggered when the app starts.
void main() {
runApp(
const MyApp(),
);
}
- udp_server.dart — the UDP server that listens to port 54678. All broadcast messages from all ESP boards will come there. Also, this file sends a message “update” to the local network.
That’s it. In a nutshell, the app does:
- Starts MyApp.
- Starts the UDP server.
- Sends a broadcast message “update”.
- Listens to incoming messages.
- It shows the data on the screen as soon as it receives JSON from any ESP.
- If the user does pull-to-refresh, the app sends another “update” command and receives all the temperatures.
In such a case, you can have several phones and several sensors, and all of them will receive messages from each other. Moreover, we only send status messages by request, so the local network won’t be overloaded with UDP messages that nobody will read.
Let’s build and install the project on a mobile phone (Android, iOS).
Important! The iOS has a tricky flow so that you will need XCode. In addition, the debug build works only when the cable is connected to your iPhone. So to avoid this limitation, use the command line and command:
flutter run - release
Unfortunately, applications built with a free iOS development account last only seven days on the phone. You can buy an Apple license for $ 100 per year and remove this limitation, but that’s a separate topic.
Android works well anyway 😃
The results:
Feel free to ask any questions :)
The repository:
GitHub - Nerdy-Things/03-esp32-ds18b20-mesh-with-mobile-flutterContribute to Nerdy-Things/03-esp32-ds18b20-mesh-with-mobile-flutter development by creating an account on GitHub.Have a wonderful day!
Comments