My PixelBot is Connected! (Part 1)
After many attempts and solving many problems, I've finally gotten my Wemos-powered PixelBot to connect via TCP to a C# server component I've written. Since I experienced so many problems with it, I decided to post about it here to help others who want to do the same as I.
The first (and arguably most difficult) hurdle I came across was the lack of correct information. For one, the TCP client class is actually called WifiClient
(which is confusing in and of itself). For another, there aren't any really good tutorials out there that show you what to do.
In addition, I wanted to build a (rather complicated as it turns out!) auto discovery system to allow my PixelBot to find the PixelHub (the server) automatically. As usual, there was even less information about this task! All I found was an outdated guide and rather simplistic example. I ended up inspecting the header file of the WiFiUDP
class in order to figure it out.
In this post, I'm going to explain how I got the autodiscovery mechanism working. Before we begin, you need to understand what multicast UDP is and how it works. For those of you who don't, I've posted all about it.
There are several ways that one can go about writing an autodiscovery mechanism. While Rob Miles chose mDNS, I ended up approaching the problem from a different angle and writing my own protocol. It's best explained with a diagram:
- The PixelHub server has a beacon that sends out pings to tell the PixelBots where it is
- The PixelBots subscribe to the beacon ping channel when they want to find the server
- The PixelBots decode the incoming ping packets to find the discover of the server
- The PixelBots connect to the PixelHub server using the credentials that it found in the beacon pings it decoded!
In order to receive these beacon pings, we need to set up a UDP listener and wire it up to receive multicast packets. In the following example I'm multicasting on 239.62.148.30
on port 5050
.
IPAddress beaconAddress = IPAddress(239, 62, 148, 30);
unsigned int beaconPort = 5050;
WiFiUDP UdpClient;
UdpClient.beginMulticast(WiFi.localIP(), beaconAddress, beaconPort);
After connecting to the multicast address, we then need to receive the pings:
// Create a buffer to hold the message
byte datagramBuffer[datagramBufferSize];
// Prefill the datagram buffer with zeros for protection later
memset(datagramBuffer, '\0', datagramBufferSize);
while(true) {
int datagramSize = UdpClient.parsePacket();
Serial.print("Received datagram #");
Serial.print(datagramSize);
Serial.print(" bytes in size from ");
Serial.print(UdpClient.remoteIP());
Serial.print(":");
Serial.print(UdpClient.remotePort());
// Don't overflow the message buffer!
if(datagramSize > datagramBufferSize) {
Serial.println(", but the message is larger than the datagram buffer size.");
continue;
}
// Read the message in now that we've verified that it won't blow our buffer up
UdpClient.read(datagramBuffer, datagramSize);
// Cheat and cast the datagram to a character array
char* datagramStr = (char*)datagramBuffer;
}
That's rather complicated for such a simple task! Anyway, now we've got our beacon ping into a character array, we can start to pick it apart. Before we do, here's what my beacon pings look like:
[email protected]:5050
^ ^ ^
Role IP Address Port
Although it looks simple, in C++ (the language of the arduino) it's rather a pain. Here's what I came up with:
// Define the role of the remote server that we're looking for
char desiredRemoteRole[7] = { 's', 'e', 'r', 'v', 'e', 'r', '\0' };
// Find the positions of the key characters
int atPos = findChar(datagramStr, '@');
int colonPos = findChar(datagramStr, ':');
// Create some variables to store things in
char role[7];
char serverIp[16];
char serverPortText[7];
int serverPort = -1;
// Fill everything with zeroes to protect ourselves
memset(role, '\0', 7);
memset(serverIp, '\0', 16);
memset(serverPortText, '\0', 7);
// Extract the parts of the
strncpy(role, datagramStr, atPos);
strncpy(serverIp, datagramStr + atPos + 1, colonPos - atPos - 1);
strncpy(serverPortText, datagramStr + colonPos + 1, datagramSize - colonPos - 1);
Serial.println("complete.");
// Print everything out to the serial console
Serial.print("atPos: "); Serial.println(atPos);
Serial.print("colonPos: "); Serial.println(colonPos);
Serial.print("Role: "); Serial.print(role); Serial.print(" ");
Serial.print("Remote IP: "); Serial.print(serverIp); Serial.print(" ");
Serial.print("Port number: "); Serial.print(serverPortText);
Serial.println();
// If the advertiser isn't playing the role of a server, then we're not interested
if(strcmp(role, desiredRemoteRole) != 0)
{
Serial.print("Incompatible role "); Serial.print(role); Serial.println(".");
continue;
}
Serial.println("Role ok!");
serverPort = atoi(serverPortText);
Phew! That's a lot of code! I've tried to annotate it the best I can. The important variables in the are serverIp
the serverPort
- they hold the IP address and the port number of the remote machine that we want to connect to.
I hope that's somewhat helpful. If there's anything you don't understand or need help with, please post a comment down below :-)
Next time, I'm going to show off the TCP connection bit of the system. Stay tuned :D