Garage Door Opener – An mDNS library
I’ve mentioned before that I plan on using mDNS to resolve the name of my server. While the ESP8266 Arduino library can broadcast a mDNS name, it doesn’t query mDNS when resolving names. I found mrdunk’s mdns on Github that implements enough of the mDNS protocol, that I should be able to hack mDNS name queries into the project.
I spun up a quick proof of concept sketch to see how it all works.
// This sketch will display mDNS (multicast DNS) data seen on the network.
#include <ESP8266WiFi.h>
#include "mdns.h"
// When an mDNS packet gets parsed this optional callback gets called once per Query.
// See mdns.h for definition of mdns::Answer.
void answerCallback(const mdns::Answer* answer){
if(strcmp(answer->name_buffer, "mqtt.local") == 0) {
Serial.print("Name: ");
Serial.println(answer->name_buffer);
Serial.print("Answer: ");
Serial.println(answer->rdata_buffer);
Serial.print("TTL: ");
Serial.println(answer->rrttl);
}
}
// Initialise MDns.
// If you don't want the optional callbacks, just provide a NULL pointer as the callback.
mdns::MDns my_mdns(NULL, NULL, answerCallback);
void setup() {
// Open serial communications and wait for port to open:
Serial.begin(115200);
// setting up Station AP
WiFi.begin("[ssid]", "[password]");
Serial.print("Connecting to WiFi");
// Wait for connect to AP
int tries = 0;
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
tries++;
if (tries > 30) {
break;
}
}
Serial.println();
}
void query(const char *name) {
mdns::Query q;
int len = strlen(name);
for(int i = 0; i < len; i++) {
q.qname_buffer[i] = name[i];
}
q.qname_buffer[len] = '\0';
q.qtype = 0x01;
q.qclass = 0x01;
q.unicast_response = 0;
q.valid = 0;
my_mdns.Clear();
my_mdns.AddQuery(q);
my_mdns.Send();
}
int send = 0;
void loop() {
if(send == 0) {
query("mqtt.local");
send = 1;
}
my_mdns.Check();
}
The sketch sends a query out for mqtt.local. We are building up a query struct with the requested host name, and a qtype of 0x01 (host address query), and qclass of 0x01 (internet).
When a response is received, the name is checked against the name we requested, and if it matches, the IP address and Time-to-Live (TTL) are printed to the Serial console. The name check is required, because that answerCallback will get called every time a mDNS packet is received, regardless of who sends it – It can get quite chatty.
I found that I needed to call Clear() before adding the query, otherwise the packet was filled with garbage – Clear seems to initialize all the required buffers.
For name resolution, all I really need is the IP address and TTL. My plan is to have an array of names that I need to resolve (for the moment, it’ll be a maximum of two – one for the MQTT server, and one for the log file server). If either of those names end with .local, I’ll resolve the name using mDNS.
On the first request, I’ll cache the result speeding up subsequent requests. I can use the TTL to expire the cached version, and re-query the network when required. A little clunky (it would be nice if the underlying network stack automatically did this), but it should work.
mDNS: A Teardown.
Trying to get mDNS queries working hasn’t quite been as straight forward as I was hoping. I mentioned in a previous log that I found a library, but it was a little overkill for what I need, so I did what any silly software developer does: started rolling my own.
How am I justifying this? Well, I’m fast running out of space. The GitHub version of the ESP8266 hardware definition I’m using is significantly larger than the distributed version, so I only have about 10k of program storage left – as a result I’m being more weary of how much source code I’m uploading. Since I don’t need to respond to mDNS questions (the ESP libs have one built in), I can skip question parsing, and since I’m only interested in name browsing, I can ignore a all bar two classes of responses. And sometimes learning a protocol can be fun. Sometimes.
My completed library can be found here.
The first thing I did was work out how mDNS works. It’s a pretty clever hack – It reuses standard DNS packets, but rather than ask a specific DNS server to answer a query, mDNS clients just broadcast a UDP packet to anyone who will listen. If another listener can answer the question, it broadcasts the answer. You can see more information about the packet format on the mDNS Wikipedia page.
Looking at the packet structure, and looking through the code from this mrdunk’s library, as well as some packet sniffing using wireshark, I was able to generate questions and parse answers. This was very much proof of concept code that was embedded in my project, and it worked, though it lacked formal testing, and this would definitely be something I would want to reuse in the future.
ime to break it out in to another library.
Now, my C++ isn’t particularly strong, so this sounded like a good opportunity to learn more about C++ classes. I had a niggling concern around code size, memory usage and speed when it came to using C++ for embedded systems, so I did a bit of research around best practices, and found this article. What I found particularly useful in the article was the explanation of how C++ achieves what is does in terms of the equivalent C code. This clicked in to place what C++ was doing and allowed me to make some better decisions around structuring my library. Though I did get tripped up. A lot
The basic rules are:
- Don’t use exceptions (you can’t on Arduino), so most functions return a status code indicating success or error states.
- Avoid virtual functions where possible, because you end up with a vtable which takes up memory and requires extra cycles to lookup where the address of the required function
- Avoid dynamic memory
- If you need dynamic memory, free it as soon as possible, or make it super long living to avoid fragmentation
- Don’t include unnecessary libraries. I thought I would do the right thing and use std::string which abstracts string handling. This made my library’s object file an order of magnitude larger. Reverting back to regular char* string keped everything nice and small, and as I didn’t need to do much actual string manipulation, the trade of was totally worth it.
Testing
I’m a big believer in automated testing. Especially for something as low level as a library where you rely on certain types of network packets. Automated testing on an Arduino is probably possible, but since I’m not doing anything too Arduino specific, I was able to build a test suite that ran on my laptop. This made the test cycle much quicker as I didn’t have to wait for my code to upload. The downside is I need to mock out a few objects, but with some clever code organisation I managed to avoid too many mocks.
I found Catch which is a header-only, lightweight testing framework for C++. It was pretty easy to setup, and works with TravisCI, so every time I commit a change the tests run automatically.
Travis was a bit of a pain to setup, as the C++ they run is ancient, so I needed to setup a custom toolchain in travis.yml.
Once I had everything written and tested, I was able to drop it in to the Garagedoor sketch, and now the sketch can find the mqtt server!