Motivation
Haven't you always dreamed of having all the variables on your embedded devices dancing in a real-time plot in the browser? I'm talking the kind of visualisation that contains all the pedagogical nuance you miss when staring at lines of code in your IDE.
Isn't it annoying when you have to use manual storage on the device for logging data on your devices? You can run out of space so quickly, unless you have a big ol' SD card and the know-how, time and patience to write to it! Even then, every time you collect more data it's laborious.
Do you have multiple interconnected devices, but want a simple way to communicate between them, while monitoring what's being said in real-time?
You must be feeling the urge to leverage the power of cloud computing and the BIG programming languages (python, JS, etc..) in your embedded projects!
Do you want a simple one-liner of code to achieve all of the above at the same time?
If you answered yes... Well.... blob is for you :)
In the following post, I'll aim to motivate some of the very simple functionality of blob through examples - strap in for a good time.
Build a simple app in C++
The following is a test application built with blob for linux. It can be built from the CMakeLists.txt file at make/test_blob/CMakeLists.txt.
Of note, is that the CMakeLists.txt file adds a precompiler definition called BLOB_WEBSOCKETS, which ensures that the communication backend blob uses will be the websockets interface.
#include <assert.h>
#include <math.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "include/blob.h"
#define NELEM 100
int
main(int argc, char **argv)
{
int j = 0;
unsigned int abs_j = 0;
int jval = 0;
float jval_squared = 0.0f;
float jval_cubed = 0.0f;
if (argc != 2)
{
printf("Usage: %s <blob_server_ip>\n", argv[0]);
}
BLOB_INIT(argv[1], 8000);
while (1)
{
abs_j = abs(jval);
BLOB_START("main");
BLOB_INT_A("jval", &jval, 1);
jval_squared = 1.0 * (jval) * (jval) / (NELEM / 2);
jval_cubed = 1.0 * (jval) * (jval) * (jval) / (NELEM * NELEM / 4);
BLOB_FLOAT_A("jval_squared", &jval_squared, 1);
BLOB_FLOAT_A("jval_cubed", &jval_cubed, 1);
BLOB_UNSIGNED_INT_A("abs_j", &abs_j, 1);
jval = ((j + 1) % NELEM) - NELEM / 2;
j++;
usleep(20000); // 20ms
BLOB_FLUSH();
}
BLOB_TERMINATE();
return 0;
}
The below video depicts the app streaming data real-time to a server, which is then forwarding packets into the browser for live plotting with plotly.
We have 5 essential functions:
Initialisation
BLOB_INIT(ip_addr, websocket_port)
Initialises the code to point to the user's websocket server. Or,
BLOB_INIT(addr0, addr1, addr2, addr3, port, latency)
to point to a UDP server. UDP requires an additional field of latency, which configures the blob protocol's receiving network jitterbuffer to a particular latency - ensuring robustness to packet reordering and jitter on the user's network.
I run the server I wrote at: https://github.com/jzmcke/core-server/blob/master/script/server.py, which forwards all UDP and websocket connections to the browser. You can download the core-server application and run it too.
Create a namespace for a set of variables
This step is not strictly required (we could log all variables in the root namespace of blob no worries, but sometimes it is nice to add hierarchy to your variable logs to make it easier to track the state of variables with the same name.
Always follow BLOB_INIT with at least a single BLOB_START command.
BLOB_START(node_name)
Log a value or set of values.
We can log integer arrays for transmission with the following command:
BLOB_INT_A(var_name, p_var_val, n)
We can also send single integer numbers by setting n=1!
Alternatives are
BLOB_FLOAT_A(var_name, p_var_val, n)
BLOB_UNSIGNED_A(var_name, p_var_val, n)
For now we do not support writing different values, but support could be easily added for different data-types to the protocol!
Flush the namespace
Once all variables have been logged in the namespace we should end the namespace with this statement.
BLOB_FLUSH()
If the namespace being flushed is the root namespace, then we will send all the data in a websocket or UDP packet to the server waiting at the relevant IP address and port.
Build the same app in Python
import numpy as np
import blob.blob_write as bw
import blob.blob_udp as bu
import sys
import time
bwrite = bw.BlobWriter('test', ['jval', 'jval_squared', 'jval_cubed', 'abs_jval'])
# Get the port from the command line
budp = bu.BlobUDPTx(str(sys.argv[1]), port=int(sys.argv[2]), fragment_tx_size=1400)
i = 1
N_ELEM = 100
while (True):
jval = (i + 1) % N_ELEM - N_ELEM // 2
bwrite.jval = np.array([jval])
bwrite.jval_squared = np.array([jval**2]) / N_ELEM
bwrite.jval_cubed = np.array([jval**3]) / (N_ELEM**2)
bwrite.abs_jval = np.array([abs(jval)])
data = bwrite.flush()
budp.send(data)
time.sleep(0.02)
i = i + 1
The above implementation streams variables over a UDP connection rather than a websocket connection. UDP is better in some cases since it is a non-blocking transmission protocol (the real-time thread can persist without waiting from an ACK from the server).
For now, the python API does not support namespacing - but the underlying implementation still does! It should be a fairly simple change to namespace the python variables in the same way.
Finishing up
Here we've built some very simple apps, and streamed data over a new network protocol called blob, which has both websocket and UDP backends. Here is a support matrix for the blob backends for the different targets. The backend can be configured by the relevant compiler definition in blob.h
| ESP32 C/C++ | Linux/OSX C/C++ | Python |
UDP | Supported | Currently unsupported | Supported |
websockets | Supported | Supported | Custom support required |
Next up
We'll be receiving data to our app via blob in real-time. We'll talk about all the pain that entails - namely due to the use of UDP. We'll discuss a network jitter buffer design - including packet fragmentation and packet reordering.
Commenti