Using the UDP protocol with an ESP32 and Android Studio, Part 2

In the first episode of this three part saga, I gave you a basic overview of the workflow when working with ESP32s and UDP datagrams. This time, I am going to walk you through the receiving end, and the best way to do that is to explain to you how the code works. The entire system will be available on my GitHub account, and you can just download everything from there, use the app, and flash your ESP32 with the relevant code.

To get started with ESP32, if you are all new to the idea, you should install the support for ESP32 in your Arduino IDE. The best place to learn how to do that is RandomNerdTutorials, an excellent site for just about anything related to ESP32 and its predecessor, ESP8266.

The code I will go through is geared towards the Heltec ESP32 with an integrated OLED display. Therefore all code that starts with Heltec can be ignored by you if you don’t have such a device, but a regular ESP32 instead. On the other hand, if you want to see feedback from your device, you should add code that uses the Serial Monitor. I will add both versions of the code to the Github repo, so that you can pick the one that suits your ESP3a version

So, I’ll post parts of the code and explain each part as I go. The code is commented too, so both can be used to understand how it works. My system will eventually drive a small car, with the UDP commands setting out both speed and angle of the servo that is steering the car. If you use yours for something else, it’s easy enough to use these structures and do something different.

Includes and Variables

#include "AsyncUDP.h"
#include "WiFi.h"
#include "heltec.h"
#include "images.h"
const char* ssid = "YOUR_SSID_HERE";
const char* pass = "YOUR_PASSWORD_HERE";
String udpSpeed, udpDir;
int localPort = 1234;
int driveSpeed, driveDir;
// Motor A
int motor1Pin1 = 27;
int motor1Pin2 = 26;
int enable1Pin = 14;
// Setting PWM properties
const int freq = 30000;
const int pwmChannel = 0;
const int resolution = 8;
int dutyCycle = 200;
// Servo
const int turnPin = 12; //Steering Servo Pin
int myTurn;

AsyncUDP udp;

So, in the top we have four includes: Asynch.h and Wifi.h are vital for getting your ESP32 to connect to yourWifi and to use the UDP protocol. Heltec.h and Images.h are Heltec OLED specific files and can be omitted if you are not on Heltec.

Replace the ssid and pass with your own network info. The localPort variable states which port your UDP messages should be sent to. The next things from driveSpeed to myTurn are again specific to my use of the UDP, and you probably want to replace these with what you need to do.

The AsyncUDP creates an objcct called udp, which is the receptable of your messages. It will be vitally important later.

My messages are strings. You can also send hexadecimal messages, if that suits you better. I suggest you go get the very useful application, UDPSender, from App Store. With that installed, you can define the IP address you get from your ESP32, and the port you set up in the code, and see your messages appear on the ESP32.

The Setup() function

void setup() {
Heltec.begin(true /DisplayEnable Enable/, false /LoRa Enable/, true /Serial Enable/);
Heltec.display -> clear();
Heltec.display -> drawXbm(0, 0, hhlogo_width, hhlogo_height, hhlogo_bits);
Heltec.display -> display();
delay(2000);
Heltec.display -> clear();
Heltec.display -> drawXbm(0, 0, robolablogo_width, robolablogo_height, robolablogo_bits);
Heltec.display -> display();
delay(2000);
Heltec.display -> clear();
Heltec.display->setFont(ArialMT_Plain_10);
// Initialize Serial Monitor
Heltec.display->drawString(0, 0, "Connecting to WLAN");
Heltec.display -> display();
Serial.begin(115200);

In this part of the Setup function, a couple of logo files are displayed, as described in the blog post on Heltec, so if you are not on Heltec, you can again ignore everything up to Serial.begin(115200), which is needed to start the Serial Monitor communication.

Then we go on to starting the network connection and settling in to wait for the UDP datagrams.

WiFi.disconnect(true); //this is needed to make sure net is off
WiFi.mode(WIFI_STA); //setup wifi in station mode
WiFi.begin(ssid, pass); //start connection
while (WiFi.status() != WL_CONNECTED) { //loop and wait until connected
delay(500);
Serial.print(".");
}
Heltec.display->clear(); //clear display and show data
Heltec.display->drawString(0, 0, "WiFi connected.");
Heltec.display->drawString(0, 12, "IP address: ");
Heltec.display->drawString(0, 24, String(WiFi.localIP().toString()));
Heltec.display->drawString(0, 36, "UDP Listening on: " + String(localPort));
Heltec.display -> display(); //show the info
delay(2000); //for 2000 ms, ie. 2 seconds.

The last part of the Setup function is again specific to my adaptation of this protocol and deals with the setting up of a motor for driving the car and a servo for steering it.

// sets the pins as outputs:
pinMode(motor1Pin1, OUTPUT);
pinMode(motor1Pin2, OUTPUT);
pinMode(enable1Pin, OUTPUT);
// configure LED PWM functionalitites
ledcSetup(pwmChannel, freq, resolution);
// attach the channel to the GPIO to be controlled
ledcAttachPin(enable1Pin, pwmChannel);
ledcSetup(4, 50, 16); //channel, freq, resolution for servo
ledcAttachPin(turnPin, 4); // pin, channel
}

And now we are set up, the system is listening to the UDP datagrams on Port 1234, or whatever you set up in the code. The good thing about this little display is that if you were to have multiple devices around, you could easily see what IP and what port are available on any devices.

ESP32 all raring to go

The main loop

The main loop is just a cycle with a little delay in it to allow for the messages to be dealt with. The UDP is a buffer that is filled with the message, then the buffer is converted into text, and the text parsed into values. These values in my application are passed to a function that turns the motor at the set speed and turns the servo to the desired angle. Let’s look at the loop.

void loop()
{
Heltec.display->setFont(ArialMT_Plain_10); //set font
if (udp.listen(localPort)) { //this is triggered by a message
delay(100);
udp.onPacket([](AsyncUDPPacket packet) {
char buf[packet.length() + 1] = {};
memcpy(buf, packet.data(), packet.length());
String s = String(buf);
int udpSplit = s.indexOf(",");
udpSpeed = s.substring(0, udpSplit);
udpDir = s.substring(udpSplit + 1, packet.length());

//manage the display to show the data
Heltec.display->clear();
Heltec.display->drawString(0, 0, "Incoming packet: " + s);
Heltec.display->setFont(ArialMT_Plain_16);
Heltec.display->drawString(0, 15, "Speed: " + udpSpeed);
Heltec.display->drawString(0, 35, "Direction: " + udpDir);
Heltec.display -> display();

//make speed and direction integers
driveSpeed = udpSpeed.toInt();
driveDir = udpDir.toInt();
Serial.println(driveSpeed);

//go to function RunMotor to actually run the car
runMotor (driveDir, driveSpeed);
delay(100);
});
}
}

So, the way it works is this: when the UDP object finds out that there is a message in the UDP port, the packet is read into a character buffer, called buf. The function memcpy copies the message data to the buffer. This buffer is then made into a String, called s. This entire packet is displayed on the top of the OLED, in its whole form, with this piece of code:

Heltec.display->drawString(0, 0, "Incoming packet: " + s);
Listening and acting on packets

In my application, I send two integers for direction (values -5 to 5) and speed (values -10 to 10), values separated by a comma. If you have a different message you want to pass, you must handle the string according to your specifications, but the conversion to string structure is valid for any case. The following bit is just a way of splitting the message into two parts.

int udpSplit = s.indexOf(","); //find the index of the comma
udpSpeed = s.substring(0, udpSplit);//Speed is the first part
udpDir = s.substring(udpSplit + 1, packet.length()); //and direction is the latter part

Then there is some Heltec stuff again about displaying the UDP datagram first, and then split up into its constituent parts. This way I can monitor the incoming UDP data before I act on it. The last part merely takes the string, splits it at the comma, and then goes off to the function runMotor with the newly-minted values for speed and direction. The runMotor part isn’t probably all that interesting, but I’ll put it in here in any case.

The runMotor and its auxiliary function

void runMotor(int myDir, int mySpeed) {
Serial.println(mySpeed);
int finalSpeed;
finalSpeed = map(abs(mySpeed), 0, 10, 215, 255);
Serial.println(finalSpeed);
Serial.println("Moving Forward");
if (mySpeed > 0) {
Serial.println("Forward");
digitalWrite(motor1Pin1, HIGH);
digitalWrite(motor1Pin2, LOW);
} else if (mySpeed < 0) {
Serial.println("Back");
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, HIGH);
} else {
Serial.println("STOP");
digitalWrite(motor1Pin1, LOW);
digitalWrite(motor1Pin2, LOW);
}
ledcWrite(pwmChannel, finalSpeed);
myTurn = map (myDir, -10, 11, 36, 180);
steeringAnalogWrite(4, myTurn); // set steering
}

void steeringAnalogWrite(uint8_t channel, uint32_t value, uint32_t valueMax = 180) {
// calculate duty, 8191 from 2 ^ 13 - 1
uint32_t duty = (8191 / valueMax) * min(value, valueMax);
ledcWrite(channel, duty);
}

This batch is rather self-explanatory. The runMotor takes two parameters, speed and direction. The Serial.println is just for monitoring what is being passed to the car – if everything works, it’s zooming around the room and you can’t read the OLED in any case.

The finalSpeed variable is for mapping the inbound value, 10 to 10, to a scale of speed that the motor can use. The Map function is very handy in this and it maps the value from the parameter to a value between 210 and 255. 210 is rather slow and 255 is top speed. If the value is less than 0, then the motow is running counterclockwise and the car reverses, and id larger than 0, it moves forward.

The function steering AnalogWrite is needed because the Heltec ESP32 doesn’t have a servo library that is compatible with regular ESP32s. That’s why it has to be done the hard way as shown in this function. It is irrelevant to the functioning of the car, but it took some extra pondering to get to run.

To sum it up

UDP can be a very good tool if you have a simple message to pass to a device, especially if you don’t have any security issues in broadcasting data with no ability to confirm delivery. The third part of this blog post set will discuss the building of an Android app.

Loading

0 thoughts on “Using the UDP protocol with an ESP32 and Android Studio, Part 2”

  1. why have you buried the onpacket callback into the loop?
    It would be nicer and more understandable if you created the callback as a separate function and registered it.
    I’m struggling to work out how this works.

    1. Hi Robert,

      I am certain I struggled with this too, but as it now stands, I don’t think I understand the code too well. I am sure it could be improved upon, but am unable to invest time right now. If you have any ideas on how to make it better, I would love to hear an acknowledge you as the source.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.