0

$USD

$USD
TUTORIALS ESP32

ESP32 Tutorial Arduino: 29. Websocket server - Sending binary frame to client

DFRobot Feb 19 2019 2246

Introduction

In this tutorial we will check how to send binary frames to a client that connects to websocket endpoint from a HTTP server running on the ESP32. We will be using the Arduino core and the HTTP async web server libraries.

For an explanation on how to receive binary frames from the client, please check this previous tutorial. So, in this example, we will do the opposite, which corresponds to the ESP32 server sending the data to the client in binary format.

Our websocket client will be implemented in Python. The script will be very simple and the basics on how to work with binary frames were already covered in this Python tutorial. It also explains how to install the Python module that will be needed to setup the client.

The tests were performed using a DFRobot’s ESP32 module integrated in a ESP32 development board. The Python version used was 2.7.8.

The code

We will start our code by including the libraries needed for this example to work. As usual, we will need the WiFi.h and the ESPAsyncWebServer.h libraries, to connect the ESP32 to a WiFi network and to setup the HTTP server and the websocket endpoint, respectively.

We will also need to store the WiFi credentials (the network name and the password), which we will assign to two global variables, making them easy to change later.

To finalize the global variable declarations, we need an object of class AsyncWebServer, which we will use below to setup the HTTP server, and an object of class AsyncWebSocket, which we use to setup a websocket endpoint and to associate that endpoint to a handling function.

Recall from the previous tutorial that the constructor of the AsyncWebSocket class receives as input the websocket endpoint, as a string. Our endpoint will be “/test” and this value will need to be used later in the Python code.

#include "WiFi.h"
#include "ESPAsyncWebServer.h"

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

AsyncWebServer server(80);
AsyncWebSocket ws("/test");

Moving on the setup function, its implementation will be the same we have been covering in other tutorials where we have used websockets and the HTTP async web server framework.

So, we start by opening a serial connection and then we connect the ESP32 to the WiFi network, using the previously declared credentials. After the connection procedure finishes, we print the local IP assigned to the ESP32, which will be needed later in the Python code.

Serial.begin(115200);

WiFi.begin(ssid, password);

while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Connecting to WiFi..");
}

Serial.println(WiFi.localIP());

After this, we will bind our websocket endpoint to a event handling function, which will be triggered when a websocket related event occurs. We do this by calling the onEvent method on our AsyncWebSocket object, passing as input the handling function.

We will call our handling function onWsEvent and we will define it later.

ws.onEvent(onWsEvent);

Then, we register the AsyncWebSocket object in our HTTP web server. This is done by calling the addHandler function on the AsyncWebServer object, passing as input the address of the AsyncWebSocket object.

server.addHandler(&ws);

To finalize the setup function, we need to call the begin method on our AsyncWebServer object, so the server starts listening to incoming requests. The full setup function, which already includes this method call, can be seen below.

void setup(){
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  Serial.println(WiFi.localIP());

  ws.onEvent(onWsEvent);
  server.addHandler(&ws);

  server.begin();
}

The only thing missing is the definition of the websocket event handling function, which we will now analyze. This function needs to have a signature accordingly to what is defined by the AwsEventHandler type.

void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){
// Handling function body implementation
}

Since the same function is invoked to handle the different types of websocket events from that route, then the easiest way to implement it is with simple If Else conditions.

We can check the event type by looking into the third argument of the event handling function, which corresponds to an enumerated value.

So, we will look for the client connection event and, when a new client connects, we will send a binary frame to it. The client connection event corresponds to the enumerated value WS_EVT_CONNECT.

if(type == WS_EVT_CONNECT){
// Send binary frame to the client
}

Inside the previous conditional block body, we will declare an array of three bytes with some arbitrary values to send to the client. Naturally, you can test with other values and array lengths.

uint8_t content[3] = {10,22,43};

To send the actual data to the client we will need to use the second argument that is passed to our handling function. This argument is a pointer to an object of class AsyncWebSocketClient, which we have already used in this previous tutorial to send textual data to the client with the text method.

This time, since we want to send binary data, we will use the binary method of the AsyncWebSocketClient object. This method receives as first input an array of bytes to send to the client and as second argument the length of the array.

Nonetheless, keep in mind that this method is overloaded and can be called with other argument types, as can be seen here.

client->binary(content, 3);

The complete handling function can be seen below. It also contains the handling of the client disconnected event, to help in debugging, and some extra prints.

void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){

  if(type == WS_EVT_CONNECT){
    Serial.println("Websocket client connection received");

    uint8_t content[3] = {10,22,43};

    client->binary(content, 3);

  } else if(type == WS_EVT_DISCONNECT){
    Serial.println("Client disconnected");
  }
}

The final code can be seen below.

#include "WiFi.h"
#include "ESPAsyncWebServer.h"

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

AsyncWebServer server(80);
AsyncWebSocket ws("/test");

void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len){

  if(type == WS_EVT_CONNECT){
    Serial.println("Websocket client connection received");

    uint8_t content[3] = {10,22,43};

    client->binary(content, 3);

  } else if(type == WS_EVT_DISCONNECT){
    Serial.println("Client disconnected");
  }
}

void setup(){
  Serial.begin(115200);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  Serial.println(WiFi.localIP());

  ws.onEvent(onWsEvent);
  server.addHandler(&ws);

  server.begin();
}

void loop(){}

The Python code

We will start our code by importing the websocket module, so we have access to all the functionalities needed to create the client, connect to the server and receive the binary frame data.

import websocket

After this, we will create an object of class WebSocket, which becomes available by importing the websocket module.

ws = websocket.WebSocket()

Next, we will establish the connection to the server. We do this by calling the connect method on our WebSocket object. The connect method receives as input the websocket endpoint as a string, in the following format:

ws://#ESP_IP#/test

You should change #ESP_IP# by the IP that will be printed once you run the ESP32 Arduino code and it finishes connecting to the WiFi network. You can check the final format below, where I’ve used the local IP assigned to my ESP32.

ws.connect("ws://192.168.1.78/test")

Finally, we will call the recv_frame method on our WebSocket object, in order to receive a frame from the server. Note that this is a blocking call which will wait indefinitely until a frame is sent by the server.

This method call will return as output an object of class ABNF, which is a lower level class that not only allows us to get the data returned by the server but also to analyze the frame type (binary or textual), as covered in more detail in this Python tutorial.

binAnswer = ws.recv_frame()

After receiving the frame, we will confirm that it is indeed a binary frame by accessing the opcode attribute of the ABNF object. Nonetheless, since this opcode is a numeric value, we can convert it to a user friendly textual format by using a python dictionary structure defined as static variable of the ABNF class. This dictionary variable is called OPCODE_MAP.

print websocket.ABNF.OPCODE_MAP[binAnswer.opcode]

To get the actual data sent by the server, we need to access the data attribute of our ABNF object. Nonetheless, we need to take in consideration that, independently of the format of the frame (binary or textual), this attribute is always a string. So, we need to convert it to a byte array, iterate over it and print all the values, since we are working with binary data and not textual data.

for byte in bytearray(binAnswer.data):
    print byte,

Note: we usually don’t work with a lower level class such as the ABNF one, specially in a higher level language such as Python. Naturally, this module we are using offers higher level methods that allow to directly obtain the data returned by the server. We are just using this approach so we can confirm that the server is indeed sending the frame as binary, like we expect.

After obtaining the data, we will call the close method on our WebSocket object, in order to close the connection to the server and free the resources. The final code can be seen below and already includes this method call.

import websocket

ws = websocket.WebSocket()
ws.connect("ws://192.168.1.78/test")

binAnswer = ws.recv_frame()

print websocket.ABNF.OPCODE_MAP[binAnswer.opcode]

for byte in bytearray(binAnswer.data):
    print byte,

ws.close()

Testing the code

To test the whole system, first compile and upload the Arduino code to the ESP32. After the procedure finishes, open the Arduino IDE serial monitor and copy the IP address that will be printed once the connection to the WiFi network is established.

That is the local IP assigned to the ESP32 that you should use on the Python code, when specifying the websocket endpoint.

After putting the correct IP in the Python code, simply run the script in a tool of your choice. I’m using IDLE, a Python IDE. After the code finishes the execution, you should have an output similar to figure 1, which illustrates the binary content sent by the ESP32 server.

As can be seen, the bytes received match the ones we have defined on the Arduino code. Besides that, the frame is of type binary, as expected.

Python client receiving binary websocket frame from ESP32

Figure 1 – Output of the Python program.