If you had to connect a device you are working with, maybe a home appliance, thermostat or software application, and were looking for a solution with a low footprint, where would you start?

We faced this situation in 2004 when we were experimenting with different web servers to serve the Web UI for the network security appliance we were working on. After trying several servers, we decided to roll our own. We wanted to make it:

  • simple, able to run with no configuration, with a simple double-click
  • cross-platform, since we were using FreeBSD and OpenBSD system at work, and FreeBSD, Linux and Windows systems at home, and wanted to run and test my code in all environments
  • and keep source code in a single file, to make it easy to build and integrate.

We maintained the project for years (first on Sourceforge, then Google Code and finally GitHub), and as it gradually gained more popularity, especially in the embedded world.  Over the years, many projects and businesses have incorporated Mongoose Web server in their solutions (Apple maps, Panasonic, etc) due to its simplicity and portability.

With time, it was evident that Mongoose was being heavily used for the implementation of IoT functionality, and thus Cesanta added support for popular IoT protocols; making Mongoose more than just a web server, but a full-fledged networking library.

In the next sections, we are going to show a condensed overview of the common Mongoose use cases with the necessary code, explanations and screenshots.

You can download Mongoose and work through the examples as you read this post.

Connecting Devices

Straight to the business. I have an embedded device, this device runs an operating system, or operating environment, which provides a BSD sockets interface. Having a BSD sockets API is a requirement for Mongoose. Examples of such environments are embedded Linux, Windows CE, vxWorks, FreeRTOS, Ecos, QNX, and many others. Our objectives are:

1. Run a web server on a device to provide a dashboard that shows device settings, status, etc.
Rationale: end-user can access the device using any browser and see the device status.

2. Make the device implement a RESTful API.
Rationale: some external tools might want to query the device for information automatically. For example, an external monitoring tool might ask the device to report CPU/Memory usage, and other statistics.

3. Make the device implement a WebSocket full-duplex connection.
Rationale: In some cases, data transfer should be performed as fast as possible. Data might be in the binary format. Therefore, using WebSocket, which keeps a persistent TCP connection with the peer and provides low-overhead framing protocol, is a great option to encapsulate communication.

Example 1. Stream captured image data to the cloud.
Example 2. A device could have quite a slow CPU. Initiating SSL for each server connection might take up to dozens of seconds! That’s why initialising the SSL connection once and keeping it live (WebSocket does that) is the most efficient option.
Example 3. A device may implement a real-time user dashboard which wants to receive notifications from a device at any random moment. In web development, that use case is sometimes referred as server push.

So let’s address all three cases one-by-one, with code and explanations.

Making a basic device dashboard / settings page

To make a dashboard or a settings page, I need:

  • A collection of static files: HTML, CSS, JavaScript, image files.
  • Some dynamic content which displays device information.

Let’s start by serving static content. First, create a web_root directory with index.shtml, some css and a logo image (see here). That will be our web root with static content. Then, create a server.c file with the basic plumbing code that starts a web server, sets the web root directory to web_root, and serves files from there (here the source code).

The most important piece of the code that implements serving from web_root is the event handler function, which is called for each HTTP request, and which I will be expanding in the following examples:

static void ev_handler(struct mg_connection *nc, int ev, void *p) {
if (ev == MG_EV_HTTP_REQUEST) {
mg_serve_http(nc, p, s_http_server_opts); // Serves static content
}
}

Let’s see what we get in the end. Compile and start the server: make && ./server and type http://localhost:8000 in your browser:

Mongoose_Embedded_Web_Server_Tutorial_1

Moving on, let’s add some dynamic content to the Settings section. We assume that the device has some internal settings. I use two variables in the program to simulate those settings:

struct device_settings {
char setting1[100];
char setting2[100];
};
static struct device_settings s_settings = { "value1", "value2" };

Then I add a form for the Settings section to show the values of those variables, and allow users to change them.

The important piece here is that now I need to expose some internal device states to the GUI. Generally, there are two ways of doing that:

  1. Use a technology like PHP or SSI and embed code directly into the HTML.
  2. Implement a RESTful endpoint, make an AJAX request from a web page and update a page using JavaScript.

For the Settings section, I will use approach number 1. Mongoose supports a subset of SSI, the particular SSI call I am going to use is <!#call parameter_name -->  directive. I called the web_root’s index file index.shtml on purpose, to have SSI directives processed by Mongoose. So how does call SSI directive work? For each call directive, Mongoose triggers a MG_EV_SSI_CALL event, passing parameter_name string as an event parameter. A callback then can print some content, which will replace the content of the call directive (see source code).

I’m adding two pieces to the event handler, the first is to save the result of the form submission and the second is to handle SSI events:

static void ev_handler(struct mg_connection *nc, int ev, void *ev_data) {
struct http_message *hm = (struct http_message *) ev_data;
switch (ev) {
case MG_EV_HTTP_REQUEST:
if (mg_vcmp(&hm->uri, "/save") == 0) {
handle_save(nc, hm);
} else {
mg_serve_http(nc, hm, s_http_server_opts); // Serve static content
}
break;
case MG_EV_SSI_CALL:
handle_ssi_call(nc, ev_data);
break;
default:
break;
}
}

When you build this, start server and type http://localhost:8000 in your browser this is what it looks like:

Mongoose_Embedded_Web_Server_Tutorial_2

Make a device implement RESTful API

Let’s implement a RESTful endpoint. I have implemented a separate endpoint, /save, in the previous section - it was an endpoint to save a form submission. That endpoint saves form values into the device settings variables and redirects to a home page. A RESTful endpoint is done in almost the same way, the only difference is that it doesn’t redirect but returns a result back to the caller - usually in JSON format.

For this example, I add an endpoint /get_cpu_usage, which will be shown in the dashboard section. On the server side, that endpoint gets the cpu usage (I create fake value for the sake of simplicity) and returns it back to the caller as a JSON object:

static void handle_get_cpu_usage(struct mg_connection *nc) {
// Generate random value, as an example of changing CPU usage
// Getting real CPU usage depends on the OS.
int cpu_usage = (double) rand() / RAND_MAX * 100.0;
// Use chunked encoding in order to avoid calculating Content-Length
mg_printf(nc, "%s", "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
// Output JSON object which holds CPU usage data
mg_printf_http_chunk(nc, "{ "result": %d }", cpu_usage);
// Send empty chunk, the end of response
mg_send_http_chunk(nc, "", 0);
}

Now, on the web GUI side, I need to call that RESTful endpoint. I am adding Jquery to do it using Jquery’s ajax call, and adding a main.js file. In main.js, I am making a RESTful call to /get_cpu_usage every second, reflecting the result in the dashboard section:

$(document).ready(function() {
// Start 1-second timer to call RESTful endpoint
setInterval(function() {
$.ajax({
url: '/get_cpu_usage',
dataType: 'json',
success: function(json) {
$('#cpu_usage').text(json.result + '% ');
}
});
}, 1000);
});

Find the full source code is here. When you build, start server and type http://localhost:8000 in your browser this is what it looks like:

Mongoose_Embedded_Web_Server_Tutorial_3

Implementing real time data exchange over WebSocket

Moving on to implementing a real-time server data push. To do that, we’ll create a WebSocket connection from the client and Mongoose will push data to it periodically. As an example, we push fake memory usage to the dashboard, and visualise it on a real-time graph.

For graphing, I am going to use Flot client-side library. Add respective files, jquery.flot.min.js and jquery.flot.time.js to the web_root directory and link them from index.shtml.  In the main.js I add code for graphing as well as this snippet to grab WebSocket pushes from Mongoose:

var ws = new WebSocket('ws://' + location.host);
ws.onmessage = function(ev) {
updateGraph(ev.data);
};

On the Mongoose side, I set up a timer to push data to all connected WebSocket clients:

for (;;) {
static time_t last_time;
time_t now = time(NULL);
mg_mgr_poll(&mgr, 1000);
if (now - last_time > 0) {
push_data_to_all_WebSocket_connections(&mgr);
last_time = now;
}
}

And here is the implementation of an actual WebSocket push:

static void push_data_to_all_WebSocket_connections(struct mg_mgr *m) {
struct mg_connection *c;
int memory_usage = (double) rand() / RAND_MAX * 100.0;
for (c = mg_next(m, NULL); c != NULL; c = mg_next(m, c)) {
if (c->flags & MG_F_IS_WebSocket) {
mg_printf_WebSocket_frame(c, WebSocket_OP_TEXT, "%d", memory_usage);
}
}
}

Find the full source code here. And again when you  build, start server and type http://localhost:8000 in your browser this is what it looks like:

Mongoose_Embedded_Web_Server_Tutorial_4

Try it for yourself

Mongoose makes it easy to connect devices. You’ll find it secure, robust and with a core of under 40kB extremely lightweight.

We are committed to making Mongoose even better. So we hope you enjoyed the examples, test Mongoose for yourself and let us know how you got on!


To contact: send us a message or ask on the developer forum.