ESP32: Advertise service with mDNS

In this tutorial we are going to learn how to advertise a network service available on the ESP32 using mDNS, and get information about that service on a Python program. The tests from this tutorial were done using a DFRobot’s ESP32 module integrated in a ESP32 development board.

Introduction

In this tutorial we are going to learn how to advertise a network service available on the ESP32 using mDNS, and get information about that service on a Python program.

For illustration purposes, the service we will setup in the ESP32 is a simple HTTP web server, using the async HTTP web server library. For an introductory tutorial on how to install it and use it, please check here.

Note that the mDNS library for the ESP32 is already included in the Arduino core, so we don’t need to install any additional resource on the device.

For the Python program, we are going to use the zeroconf library. It can be installed with pip by sending the following command:

pip install zeroconf

Additionally, we will make use of Python’s Requests library to be able to send a HTTP request to the server hosted by the ESP32, based on the information obtained from the service.

Requests can be installed with the following pip command:

pip install requests

The tests from this tutorial were done using a DFRobot’s ESP32 module integrated in a ESP32 development board.

If you prefer a video version of this tutorial, please check below.

The Arduino code

We will start the code by the library includes:

  • ESPmDNS.h: Allows to setup the mDNS resolver and to register services;
  • WiFi.h: Allows to connect the ESP32 to a WiFi network;
  • ESPAsyncWebServer.h: Allows to setup a HTTP web server running on the ESP32.
#include <ESPmDNS.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>

After that we will define two variables that will hold the WiFi network credentials: network name and password.

const char* ssid = "yourNetworkName";
const char* password =  "yourNetworkPassword";

Then we will instantiate an object of class AsyncWebServer, which will allow us to setup the server on the ESP32. We will pass as input of the constructor the value 80, which corresponds to the port where the ESP32 will be listening to incoming requests.

AsyncWebServer server(80);

Moving to the Arduino setup function, we will start by opening a serial connection, so we can output some messages from our program. Then we will take case of connecting the ESP32 to the WiFi network, using the previously defined credentials.

Serial.begin(115200);
  
WiFi.begin(ssid, password);
  
while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
}

Then we will setup the mDNS responder. This is done with a call to the begin method on the MDNS extern variable.

As input of this method we need to pass a string with the ESP32 hostname. We will set it to the value “esp32“.

As output, this method returns a Boolean indicating if the setup procedure was successful or not. We will use it for error checking and print a message to the serial port in case something failed.

if(!MDNS.begin("esp32")) {
     Serial.println("Error starting mDNS");
     return;
}

Then, to register a service, we simply need to call the addService method on the MDNS extern variable.

As first input, this method receives the service name. As suggested by IDF’s documentation on the lower level mDNS libraries, you can check here a list of common service names. Since our service is a HTTP server, we will pass a string with the value “http“.

As second input, the addService method receives the protocol that the service runs on. In our case, our service operates over TCP, so we must pass the value “tcp“. Alternatively we could want to register a service running over UDP, for which we would have to pass “udp“.

As third and final input, we need to pass the number of the network port that the service runs on. In our case, as already mentioned, we are exposing our server on port 80.

Note that both the service name and protocol should be prepended with an underscore [1]. Nonetheless, if we don’t add that character to our strings, the addService method will add them for us, as can be seen here.

This is important since, when looking for the service on the Python code, we actually need to include the underscore characters.

MDNS.addService("http", "tcp", 80);

Optionally, we can also register properties for our service in the form of key value pairs. We will add two testing properties, for illustration purposes.

This is done with a call to the addServiceTxt method on our MDNS extern variable. As first and second inputs of this method we pass the service name and protocol, respectively. As third input we pass a string with the key and as fourth and last input a string with the value.

We can call this method more than once if we want to add multiple properties to our service.

MDNS.addServiceTxt("http", "tcp", "prop1", "test");
MDNS.addServiceTxt("http", "tcp", "prop2", "test2");

To finalize, we are going to setup a route on the “/hello” endpoint that will listen to HTTP GET requests and answer with a simple “Hello World” message. Then we will start the server with a call to the begin method on the server object.

server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello World");
});
  
server.begin();

The complete Arduino setup can be seen below. Note that we added the printing the local IP assigned to the ESP32 on the network, so we can confirm that the IP address we will obtain in the Python code matches.

void setup(){
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
 
  if(!MDNS.begin("esp32")) {
     Serial.println("Error starting mDNS");
     return;
  }

  MDNS.addService("http", "tcp", 80);
  MDNS.addServiceTxt("http", "tcp", "prop1", "test");
  MDNS.addServiceTxt("http", "tcp", "prop2", "test2");

  Serial.println(WiFi.localIP());
  
  server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello World");
  });
  
  server.begin();
}

The Arduino main loop can be left empty since we are working with an asynchronous HTTP web server and we don’t have any additional computation to perform there.

void loop(){}

The final ESP32 code can be seen below.

#include <ESPmDNS.h>
#include <WiFi.h>
#include <ESPAsyncWebServer.h>
  
const char* ssid = "yourNetworkName";
const char* password =  "yourNetworkPassword";
  
AsyncWebServer server(80);
  
void setup(){
  Serial.begin(115200);
  
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
 
  if(!MDNS.begin("esp32")) {
     Serial.println("Error starting mDNS");
     return;
  }

  MDNS.addService("http", "tcp", 80);
  MDNS.addServiceTxt("http", "tcp", "prop1", "test");
  MDNS.addServiceTxt("http", "tcp", "prop2", "test2");

  Serial.println(WiFi.localIP());
  
  server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(200, "text/plain", "Hello World");
  });
  
  server.begin();
}
  
void loop(){}

The Python code

We will start by importing the Zeroconf class from the installed module.

from zeroconf import Zeroconf

We will also import the request function from the requests module. This will allow us to perform a HTTP request to the ESP32 server, after we discover its IP address.

from requests import request

Then we will create an object of class Zeroconf. We will call the constructor without passing any parameters since all the defaults are enough for our test.

zconf = Zeroconf()

Then we will create an object of class Listener, which is a class implemented by our code that will take care of handling the detection of new services of a given type.

We will check its implementation below but, for now, we will assume that its constructor takes no arguments.

serviceListener = Listener()

Then we will call the add_service_listener method on our Zeroconf class object. As first input, this method receives a string with the service type and as second it receives the instance of our Listener class.

Note that the service type is a string composed by the service name, the protocol [2] and the “local” domain, separated by a “.” character. Take also in consideration that the protocol and the service name should include the underscore characters mentioned in the Arduino code.

zconf.add_service_listener("_http._tcp.local.", serviceListener)

From this point onward, our program should now be listening for new services of the given type. If the service was already up when we run our Python code, the Listener class handler will still run once to output its information.

So, to prevent our program from ending, we will now wait for a Enter key press in the console, by calling the input function. Note that this is a blocking function but our listener will still be running on the background.

If the user clicks the Enter key, the input function will return and we assume the program should end. Thus, we need to call the close method in our Zeroconf object, This will end all the background threads and will prevent the Zeroconf instance to do more mDNS queries [3].

input("Press enter to close... \n")
zconf.close()

To finalize, we will check the implementation of the Listener class. It should have a method called add_service, with the signature defined here.

This method receives as first input the instance of the class (self), the Zeroconf instance that registered the listener, the type of the service and the name of the service.

class Listener:

    def add_service(self, zeroconf, type, name):
    # class implementation

With this information, we can call the get_service_info method on the Zeroconf object and obtain an object of class ServiceInfo, which contains the details of the service.

info = zeroconf.get_service_info(type, name)

To get the list of IP addresses associated with the service, we can call the parsed_addresses method on the ServiceInfo object. This method takes no arguments and returns an array of strings with all the addresses. In our case, we expect a single IP address for our ESP32, so the list will only have one element.

print("Address: " + str(info.parsed_addresses()))

To get the network port, we can access the port attribute of our object.

print("Port: " + str(info.port))

To get the full name of the service, we can access the name attribute. The full name will be on the following format:

hostname._serviceName._protocol.local.

Since it is a string, we can directly print it to the console.

print("Service Name: " + info.name)

Then we will print the server attribute, which corresponds has the following format:

hostname.local.

Again, we can also print it directly to the console.

print("Server: " + info.server)

We will also print the properties associated to the service, which we defined in the Arduino code. We can obtain them by accessing the properties attribute, which corresponds to a Python dictionary.

print("Properties: " + str(info.properties))

To finalize, we are going to use the IP address obtained from the service information to do a GET request to the server.

address = "http://" + info.parsed_addresses()[0]+"/hello"

print("\n\nSending request...")
response = request("GET", address)
print(response.text)

The complete class implementation can be seen below.

class Listener:

    def add_service(self, zeroconf, serviceType, name):

        info = zeroconf.get_service_info(serviceType, name)

        print("Address: " + str(info.parsed_addresses()))
        print("Port: " + str(info.port))
        print("Service Name: " + info.name)
        print("Server: " + info.server)
        print("Properties: " + str(info.properties))

        address = "http://" + info.parsed_addresses()[0]+"/hello"

        print("\n\nSending request...")
        response = request("GET", address)
        print(response.text)

The final Python code can be seen below.

from zeroconf import Zeroconf
from requests import request

class Listener:

    def add_service(self, zeroconf, serviceType, name):

        info = zeroconf.get_service_info(serviceType, name)

        print("Address: " + str(info.parsed_addresses()))
        print("Port: " + str(info.port))
        print("Service Name: " + info.name)
        print("Server: " + info.server)
        print("Properties: " + str(info.properties))

        address = "http://" + info.parsed_addresses()[0]+"/hello"

        print("\n\nSending request...")
        response = request("GET", address)
        print(response.text)

zconf = Zeroconf()

serviceListener = Listener()

zconf.add_service_listener("_http._tcp.local.", serviceListener)

input("Press enter to close... \n")
zconf.close()

Testing the code

To test the code, you can start either by running the Arduino code or the Python code first. I’m assuming we will start by compiling and uploading the Arduino code to the ESP32.

Once the procedure finishes, open the IDE serial monitor. You should get an output similar to figure 1. As can be seen, we should obtain the IP address assigned to the ESP32 on the local network.

Output of the programon the Arduino IDE serial monitor.
Figure 1 – Output of the ESP32 program on the Arduino IDE serial monitor.

Then, run the Python code on a tool of your choice. I’m using PyCharm, a Python IDE from JetBrains.

You should get an output similar to figure 2. As can be seen, the address and the port of the service match the one from the ESP32. Also, both the service name and server contain the hostname we have defined for the ESP32: the value “esp32”.

We can also see that the dictionary with the service properties match the ones we have defined on the ESP32 program.

To finalize, we can confirm that we can reach the server with the information obtained from the service, since we can successfully send perform the HTTP GET request and receive a response.

Output of the Python program, displaying the service information.
Figure 2 – Output of the Python program, displaying the service information.

Related Posts

References

[1] https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/protocols/mdns.html

[2] https://agnat.github.io/node_mdns/user_guide.html#service_types

[3] https://github.com/jstasiak/python-zeroconf/blob/master/zeroconf/__init__.py#L2660

Leave a Reply

%d bloggers like this: