Htmx - the ideal web application library for MCUs

htmx is a very interesting library for building dynamic web applications. In its simplest form you add tags to HTML elements that execute HTTP requests and replace parts of the page with the response from the server. This allows granular updates of a web page without using a full blown front-end framework like React or Elm.

Example:

<div hx-get="/news" hx-trigger="every 2s"></div>

This fetches the contents of the /news URL and places the content in this div every 2 seconds.

One area where this is different than most frontend frameworks is that the server sends the HTML instead of JSON. While the API is not quite a general as a JSON based API, the htmx approach is very simple and avoids the serialization step.

I recently started using htmx to create a web UI for a MCU based system running Zephyr. In this case, we don’t have a full-blown web application framework. While I would have used Elm in the past, this time I decided to try htmx. So far, I’m very pleased with the result. It is much simpler and again avoids the JSON serialization step which is expensive on small systems like MCUs.

how expensive is json on MCUs? htmx needs server side logic, sometimes I feel just serving json data to the browser is more light-weight, i.e. the MCU is a json-rpc endpoint and does not know anything about html. frontend can be vanilla, or alpinejs, or even the old school jquery.

Yes, on the surface, I agree a complete separation of the backend/frontend is clean, and a more flexible API. But for a tightly integrated system, htmx may be simpler.

The Rails folks like this approach:

However, with rails, you have a powerful backend (Ruby on a server), not a MCU running C. Templating is easy in Rails, and hard in C.

In the case of a MCU, you can argue that the browser hosting the frontend is a much more capable platform than the MCU hosting the backend.

We’ll see – this is my first experience with it – will know more in a month or two after using it, or if I’ll just fall back to Elm.

Currently, I’m just rendering the bits on the MCU like:

// ==================================================
// /devices resource

static uint8_t recv_buffer[1024];

char *on = "<span class=\"on\"></span>";
char *off = "<span class=\"off\"></span>";

#define RENDER_ON_OFF(state) state ? on : off

static int devices_handler(struct http_client_ctx *client, enum http_data_status status,
			   uint8_t *buffer, size_t len, void *user_data)
{
	char *end = recv_buffer;

	end += sprintf(end, "<ul>");
	for (int i = 0; i < ARRAY_LENGTH(ind_state); i++) {
		end += sprintf(end, "<li><b>#%i</b>: AON:%s ONA:%s BON:%s ONB:%s</li>", i,
			       RENDER_ON_OFF(ind_state[i].aon), RENDER_ON_OFF(ind_state[i].ona),
			       RENDER_ON_OFF(ind_state[i].bon), RENDER_ON_OFF(ind_state[i].onb));
	}
	end += sprintf(end, "</ul>");

Kind of crude, and could likely be done better, but it is simple, and it works for my current needs.

An interesting article:

In summary, a JSON API is not really decoupling, because every time you add something new to the web page you need to change the JSON structure. And then how do you handle versioning?

With Simple IoT, we have standardized the data format and made it flexible enough that it works for most needs – a tree of nodes that contain points. When new features are added, only the ends need to change, not the middle:

image

However, while this is very useful for an IoT system, it is probably overkill for a simpler web app, hence HTMX, Hotwire, etc.

Made a little more progress in the zephyr-siot project.

The code like the following is used in the Zephyr C code to serve various status endpoints:

// ********************************
// CPU usage handler

static int cpu_usage_handler(struct http_client_ctx *client, enum http_data_status status,
			     uint8_t *buffer, size_t len, void *user_data)
{
	char *end = recv_buffer;
	static bool processed = false;
	int rc = 0;

	k_thread_runtime_stats_t stats;
	rc = k_thread_runtime_stats_all_get(&stats);

	if (rc == 0) { /* item was found, show it */
		sprintf(recv_buffer, "%0.2lf%%",
			((double)stats.total_cycles) * 100 / stats.execution_cycles);
	} else {
		strcpy(recv_buffer, "error");
	}

	if (processed) {
		processed = false;
		return 0;
	}

	processed = true;
	return strlen(recv_buffer);
}

struct http_resource_detail_dynamic cpu_usage_resource_detail = {
	.common =
		{
			.type = HTTP_RESOURCE_TYPE_DYNAMIC,
			.bitmask_of_supported_http_methods = BIT(HTTP_GET) | BIT(HTTP_POST),
		},
	.cb = cpu_usage_handler,
	.data_buffer = recv_buffer,
	.data_buffer_len = sizeof(recv_buffer),
	.user_data = NULL,
};

HTTP_RESOURCE_DEFINE(cpu_usage_resource, siot_http_service, "/cpu-usage",
		     &cpu_usage_resource_detail);

And then the following HTML snippets load data from the endpoints and display it:

    <li>Board: <span hx-get="/board" hx-trigger="load" />
    <li>Boot count: <span hx-get="/bootcount" hx-trigger="load" />
    <li>CPU Usage: <span hx-get="/cpu-usage" hx-trigger="every 2s" />

The entire HTML file is very simple:

<!DOCTYPE html>
<html>

<head>
  <title>Simple IoT Zephyr</title>
  <script src="https://unpkg.com/htmx.org@2.0.2"
    integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
    crossorigin="anonymous"></script>
  <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css" />
  <style>
    .on {
      display: inline-block;
      width: 15px;
      height: 15px;
      background-color: #0eff00;
      border-radius: 50%;
    }

    .off {
      display: inline-block;
      width: 15px;
      height: 15px;
      background-color: lightgrey;
      border-radius: 50%;
    }
  </style>
</head>

<body>
  <h1>SIOT Zephyr</h1>
  <h2>Status</h2>
  <ul>
    <li>Board: <span hx-get="/board" hx-trigger="load" />
    <li>Boot count: <span hx-get="/bootcount" hx-trigger="load" />
    <li>CPU Usage: <span hx-get="/cpu-usage" hx-trigger="every 2s" />
  </ul>
  <hr />
  <h2>Settings</h2>
  <form action="/" method="POST">
    <div>
      <label for="fid">Device ID:</label>
      <input type="text" id="fid" name="fid"><br>
    </div>
    <div>
      <input type="submit" value="Save">
    </div>
  </form>
  <hr />
</body>

</html>

So, I’m liking it for this kind of stuff. It is extremely simple and effective.

The nice thing about htmx is I don’t have to render templates on the backend or do full page reloads – just fetch one page, and then any dynamic data at load time using htmx. It is not as efficient, but with 6ms load times for an htmx transaction, we don’t really care.

And for small bits of data, templates are not needed on the backend – sprintf works just fine.

A screenshot of what the current web UI looks like:

1 Like

A basic settings web UI is now working:

Was fairly painful to get there and I’m less convinced now that HTMX is the way to go for something like this, but will keep going for a bit yet …