How to use UDP and mDNS on ESP32

xiangyu fu Lv3

Introduction

In this tutorial, I will walk through using UDP and mDNS with the ESP32 platform. I'll cover how to set up UDP communication between an ESP32 and a client device, along with using mDNS to allow the client to discover the ESP32 on the network without needing to know its IP address. The code provided will demonstrate how to send data between the ESP32 and a client, handle heartbeats, and ensure the client can reconnect if communication is lost.

Prerequisites

  • Basic knowledge of ESP32 and its Wi-Fi capabilities.
  • Familiarity with Arduino IDE or PlatformIO for ESP32 development.
  • Understanding of network communication concepts such as UDP and mDNS.

What is UDP and mDNS?

UDP (User Datagram Protocol) is a communication protocol that allows devices to exchange messages over a network. It's a connectionless protocol, meaning data is sent without establishing a dedicated connection, which makes it faster but less reliable than TCP.

mDNS (Multicast DNS) allows devices on a local network to discover each other without the need for a central DNS server. It’s very useful for local network services, as it enables devices to advertise their presence and be discovered by name (like smart_knob.local in our case).

Setting up the ESP32 as a UDP Server with mDNS

First, we set up the ESP32 to connect to Wi-Fi, initialize an mDNS service, and create a UDP server to handle communication.

Here’s the core setup code for the ESP32:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void WifiTask::run(){
setupWiFi();

// MDNS
if (!MDNS.begin("smart_knob")) {
ESP_LOGI(TAG, "mDNS failed to start");
return;
}
ESP_LOGI(TAG, "mDNS started successfully, device name is smart_knob.local");
MDNS.addService(udpServiceName, "udp", localUdpPort);

udp.begin(localUdpPort);
heartbeatUdp.begin(heartbeatPort);
tcpServer.begin();
ESP_LOGI(TAG, "UDP service started, port number: %d", localUdpPort);
ESP_LOGI(TAG, "Heartbeat service started, port number: %d", heartbeatPort);
ESP_LOGI(TAG, "TCP service started, waiting for IP configuration, port number: %d", 4321);
}

In this block of code:

  1. The setupWiFi() function connects the ESP32 to the Wi-Fi network.
  2. mDNS is initialized with the name smart_knob.local using MDNS.begin(). This allows the ESP32 to be discovered on the network without needing its IP address.
  3. UDP services for data transmission (localUdpPort) and heartbeat monitoring (heartbeatPort) are started.
  4. A TCP server is also initialized to receive the client’s IP configuration.

Handling UDP Heartbeats

The heartbeat mechanism ensures that communication between the client and ESP32 is kept alive. If the ESP32 doesn’t receive a heartbeat within the specified timeout period, it assumes the client has disconnected.

1
2
3
4
5
6
7
8
9
10
11
12
int packetSize = heartbeatUdp.parsePacket();
if (packetSize) {
char incomingPacket[255];
int len = heartbeatUdp.read(incomingPacket, 255);
if (len > 0) {
incomingPacket[len] = 0;
}
String message = String(incomingPacket);
if (message == "heartbeat") {
lastHeartbeat = millis();
}
}

This code block listens for incoming heartbeat messages and resets the timer whenever it receives the expected heartbeat message.

Creating a Client in Python

On the client side, I will use Python to send and receive UDP messages and handle mDNS discovery.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ESP32UDPClient:
def __init__(self):
self.esp32_host = 'smart_knob.local' # ESP32 mDNS name or IP address
self.udp_port = 1234
self.heartbeat_udp_port = 1235
self.client_ip = '192.168.178.69'

# Create UDP socket
self.client_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.client_hearbeat_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# Resolve mDNS name to IP address
try:
self.esp32_ip = socket.gethostbyname(self.esp32_host)
except socket.gaierror:
return

# Bind the UDP socket to the client’s IP
self.client_udp_socket.bind((self.client_ip, self.udp_port))

# Start sending heartbeats
self.heartbeat_thread = threading.Thread(target=self.send_heartbeat)
self.heartbeat_thread.daemon = True
self.heartbeat_thread.start()

def send_heartbeat(self):
while True:
message = "heartbeat"
try:
# Send heartbeat message to ESP32
self.client_hearbeat_socket.sendto(message.encode(), (self.esp32_ip, self.heartbeat_udp_port))
except Exception as e:
pass
time.sleep(2)

This Python class allows the client to:

  1. Use mDNS to resolve the ESP32’s IP address.
  2. Create a UDP connection to communicate with the ESP32.
  3. Periodically send heartbeat messages to ensure the connection remains active.

Conclusion

In this project, I demonstrated how to use UDP and mDNS on the ESP32 for seamless communication with a client device. The ESP32 was set up as a UDP server, handling both data transmission and heartbeat monitoring, while the client communicated with it using Python.

This setup is highly efficient for local networks where devices need to be discovered and communicate without requiring a central server or hardcoded IP addresses.

Full Code

In this section, I provide the full code for the ESP32 and Python client to implement the UDP and mDNS communication. For the ESP32 codes, FreeRTOS is used to handle the tasks and queues for communication.

wifi_task.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#pragma once

#include <Arduino.h>
#include "logger.h"
#include "task.h"

class WifiTask : public Task<WifiTask> {
friend class Task<WifiTask>; // Allow base Task to invoke protected run()

public:
WifiTask(const uint8_t task_core);
~WifiTask();

void addListener(QueueHandle_t queue);
void setLogger(Logger* logger);
void publish(const PB_SmartKnobConfig & config);
void sendData(String data);

protected:
void run();

private:
// QueueHandle_t uart_queue_;
Logger* logger_;
std::vector<QueueHandle_t> listeners_;
PB_SmartKnobConfig wifiConfig;
static WifiTask* instance;
bool isUDPConnected = false;
bool isUDPReconnected = false;


void loop();
void setupWiFi();
void CommandCallback(const knob_robot_control::KnobCommand& msg);

};

wifi_task.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#include "wifi_task.h"
#include "WiFi.h"
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include "esp_log.h"

static const char *TAG = "WIFI_TASK";

const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";

// Static member initialization
WifiTask* WifiTask::instance = nullptr;

WiFiUDP udp;
WiFiUDP heartbeatUdp;
const int localUdpPort = 1234;
char incomingPacket[255];
const char *udpServiceName = "esp32";

const int heartbeatPort = 1235;

// TCP settings
WiFiServer tcpServer(4321); // ESP32 TCP port
String clientIp = "";

// Heartbeat settings
unsigned long lastHeartbeat = 0;
const unsigned long heartbeatTimeout = 5000; // 5 seconds


// Force feedback
float tcp_force = 1.0;

WifiTask::WifiTask(const uint8_t task_core) : Task("WIFI Task", 8192, 0, task_core) {
// wifi_queue_ = xQueueCreate(1, sizeof(WifiConfig));
instance = this;
}

WifiTask::~WifiTask() {
// Destructor code here
instance = nullptr;
}


void WifiTask::addListener(QueueHandle_t queue) {
ESP_LOGI(TAG, "Add listener");
listeners_.push_back(queue);
}

void WifiTask::setLogger(Logger* logger) {
logger_ = logger;
}

// publish is used for the logger
void WifiTask::publish(const PB_SmartKnobConfig & config) {
for (auto listener : listeners_) {
xQueueOverwrite(listener, &config); // xQueueOverwrite will overwrite the queue if it is full
}
}

void WifiTask::run(){
setupWiFi();

// MDNS
if (!MDNS.begin("esp32")) {
ESP_LOGI(TAG, "mDNS failed to start");
return;
}
ESP_LOGI(TAG, "mDNS started successfully, device name is esp32.local");
MDNS.addService(udpServiceName, "udp", localUdpPort);

udp.begin(localUdpPort);
heartbeatUdp.begin(heartbeatPort);
tcpServer.begin();
ESP_LOGI(TAG, "UDP service started, port number: %d", localUdpPort);
ESP_LOGI(TAG, "Heartbeat service started, port number: %d", heartbeatPort);
ESP_LOGI(TAG, "TCP service started, waiting for IP configuration, port number: %d", 4321);


for (;;) {
// Check if we have received the client's IP over TCP
if (clientIp == "") {
WiFiClient tcpClient = tcpServer.available();
if(isUDPReconnected){
Serial.print(".");
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
if (tcpClient) {
ESP_LOGI(TAG, "Client connected via TCP.");
while (tcpClient.connected() && tcpClient.available() == 0) {
delay(100);
}
if (tcpClient.available()) {
clientIp = tcpClient.readString();
ESP_LOGI(TAG, "Received client IP: %s", clientIp.c_str());
}
isUDPConnected = true;
isUDPReconnected = true;
tcpClient.stop();
}
} else {
// Check for UDP heartbeat
int packetSize = heartbeatUdp.parsePacket();
if (packetSize) {
char incomingPacket[255];
int len = heartbeatUdp.read(incomingPacket, 255);
if (len > 0) {
incomingPacket[len] = 0;
}
String message = String(incomingPacket);
if (message == "heartbeat") {
lastHeartbeat = millis();
}
}

// Check heartbeat timeout
if (millis() - lastHeartbeat > heartbeatTimeout) {
ESP_LOGW(TAG, "No heartbeat received, stopping communication.");
ESP_LOGW(TAG, "Waiting for client to reconnect.");
clientIp = "";
isUDPConnected = false;
tcpServer.begin();
}
}
}
}

void WifiTask::sendData(String data) {
if (isUDPConnected)
{
udp.beginPacket(clientIp.c_str(), localUdpPort);
udp.print(data.c_str());
udp.endPacket();
}
}

void WifiTask::setupWiFi()
{
ESP_LOGI(TAG, "Connecting to WiFi");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) { delay(500);Serial.print("."); }
ESP_LOGI(TAG, "Connected to WiFi %s", ssid);
ESP_LOGI(TAG, "IP: %s", WiFi.localIP().toString().c_str());
}

Here is the Python client code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
#!/usr/bin/env python3

# import rospy
import socket
import threading
import time
# from std_msgs.msg import String

class ESP32UDPClient:
def __init__(self):
self.esp32_host = 'esp32.local' # ESP32 mDNS name or IP address
self.udp_port = 1234 # ESP32 UDP port
self.heartbeat_udp_port = 1235
self.esp32_tcp_port = 4321 # ESP32 TCP port
self.heartbeat_interval = 2 # Heartbeat message interval
self.client_ip = '192.168.178.69'

# Create UDP and TCP sockets
self.client_udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.client_hearbeat_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.client_tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)


# Resolve mDNS name to IP address
try:
self.esp32_ip = socket.gethostbyname(self.esp32_host)
except socket.gaierror:
return

# Set socket timeout
self.client_udp_socket.settimeout(5)
self.client_hearbeat_socket.settimeout(5)

# Bind UDP socket to client IP address
self.client_udp_socket.bind((self.client_ip, self.udp_port))

# Start threads
self.receive_thread = threading.Thread(target=self.receive_udp_messages)
self.receive_thread.daemon = True
self.receive_thread.start()

self.heartbeat_thread = threading.Thread(target=self.send_heartbeat)
self.heartbeat_thread.daemon = True
self.heartbeat_thread.start()

self.send_ip_configuration()


def send_ip_configuration(self):
"""Send the client IP address to ESP32 over TCP."""
try:
# Connect to ESP32 TCP server
self.client_tcp_socket.connect((self.esp32_ip, self.esp32_tcp_port))

# Send local IP address of this machine to ESP32
local_ip = self.client_ip
self.client_tcp_socket.sendall(local_ip.encode())

# Close TCP connection
self.client_tcp_socket.close()
except Exception as e:
# rospy.logerr(f"Error during TCP communication: {e}")
pass


def request_callback(self, msg):
# Callback function when a message is received on 'esp32_request' topic
self.send_udp_message(msg.data)

def send_udp_message(self, message):
try:
# Send UDP packet to ESP32
self.client_udp_socket.sendto(message.encode(), (self.esp32_ip, self.udp_port))

except Exception as e:
# rospy.logerr(f"Error sending UDP message: {e}")
pass

def send_heartbeat(self):
"""Send heartbeat messages periodically to the ESP32."""
# while not rospy.is_shutdown():
while True:
message = "heartbeat"
try:
# Send heartbeat message to ESP32
self.client_hearbeat_socket.sendto(message.encode(), (self.esp32_ip, self.heartbeat_udp_port))

except Exception as e:
pass
time.sleep(self.heartbeat_interval)

def receive_udp_messages(self):
# while not rospy.is_shutdown():
while True:
try:
# Receive data from ESP32
data, addr = self.client_udp_socket.recvfrom(1024)
response = data.decode()
print(f"Received response from {addr}: {response}")

# Publish the response to a ROS topic
self.pub.publish(response)

except socket.timeout:
print("Timeout occurred, no data received.")
continue
except Exception as e:
pass

def run(self):
while True:
pass

if __name__ == '__main__':
try:
client = ESP32UDPClient()
client.run()

except KeyboardInterrupt:
pass

I also provide a simple mDNS service discovery code in Python to discover the ESP32 device on the network:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# https://github.com/python-zeroconf/python-zeroconf

from zeroconf import ZeroconfServiceTypes, ServiceBrowser, ServiceListener, Zeroconf

class MyListener(ServiceListener):

def update_service(self, zc: Zeroconf, type_: str, name: str) -> None:
print(f"Service {name} updated")

def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
print(f"Service {name} removed")

def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
info = zc.get_service_info(type_, name)
print(f"Service {name} added, service info: {info}")


zeroconf = Zeroconf()
listener = MyListener()
browser = ServiceBrowser(zeroconf, "_netx._tcp.local.", listener)
try:
print("press enter to exit...\n")
print('\n'.join(ZeroconfServiceTypes.find()))
input("\n")


finally:
zeroconf.close()

Happy coding! 🚀

  • Title: How to use UDP and mDNS on ESP32
  • Author: xiangyu fu
  • Created at : 2024-09-10 20:53:47
  • Updated at : 2024-09-10 20:55:34
  • Link: https://redefine.ohevan.com/2024/09/10/Reviews/esp32_udp/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments