Bridging the Web-Embedded Divide, Part 1: Switch Counter

NetBurner Dynamic Web Content

There’s a world of difference between a web application that runs on a $3000 server in a datacenter and an embedded application that runs on a $300 module in a factory.

Between the two environments there are different computing and network resources available, different programming languages, architectures, delegation of responsibilities, overall perspective, and industry best practices. An embedded processor attached to a CNC machine probably shouldn’t run a JavaScript app open to the public Internet, and a web app in a datacenter probably shouldn’t make realtime decisions about CNC machine motor movements. One “stack” is optimized for business and network tasks, and the other is optimized for bare-metal I/O and industrial control tasks.

Perfect Timing. Try Our ARM® Embedded Dev Kit Today.

Netburner ARM Cortex M7 embedded Development Kit for IoT product development and industrial automation.

Or, learn more about NetBurner IoT.

At NetBurner we pride ourselves in bridging these gaps: we’ve done our best to make it easy to update embedded firmware and accomplish networked industrial I/O for over three decades, enabling “IoT” before we had a name for it, but also by bringing both worlds of web and embedded development into one system used by major manufacturers all over the world. Our native JSON library and SSL WebClient help these projects follow modern best practices in development, architecture, and security.

Mission Statement

This will be a two-part blog series in which we use a few minimalistic but realistic examples to help Web Developers and Embedded Developers design both web and embedded applications in a way that interfaces well with “the other side.” The first example we call Switch Counter, which demonstrates a basic client-server web “REST” API, and the second example is an imaginary Laundromat that demonstrates a “push” or “pub sub” architecture to enable real-time digital payments.

In our years of experience helping customers deliver results, we’ve found that miscommunication and misunderstanding between these two programming worlds can have each side wondering why the other side is being difficult, when in fact the difficulties of integration are small compared to the pain of a delayed or poorly-architected product. Starting out with a well-designed architecture that spans everything from C++ and UARTs to JavaScript and HTTPS makes the testing and release process that much smoother, but many developers don’t “speak both languages” well enough to see why a certain architecture is better than what’s easy and familiar. We hope that these examples provide a concrete basis for these conversations, designs, and implementations.

Three icons showing a NetBurner microcontroller, a web server in the cloud, and a laptop or mobile device. The NetBurner is captioned "The goal: Submit data to a web server on boot and whenever the NetBurner's DIP switches are changed." The web server is captioned "The method: Create a rudimentary HTTP REST API to facilitate the communication." The laptop is captioned "The result: Data about the physical world is made available to any device with a web browser."
Figure 1: Architecture of our Switch Counter project

Example 1: The Switch Counter

A common need for network-connected electronics is to submit data they’ve gathered about their operation to a central place for processing, reporting, analysis, and further action. Something as simple as the number of times a library patron walks through an optical sensor or as complex as government weather station observations both represent one-way submission of data from a client in the world to a server in a datacenter. Response data can also be used for local decision-making, like if firmware needs updating or if a device reboot is requested, even though the server has no way of initiating that server-to-client communication directly.

For this example we use the DIP switches on the NetBurner development board to demonstrate any physical input and electronic sensor you can think of. With some jumper wires and the right components, you could modify this example to submit data about pretty much anything, and even act upon the server’s response.

This guide assumes two things:

  1. That you are familiar with publishing code to a web server and configuring the server to run server-side code and necessary libraries of your choosing. Any web host like GoDaddy, Dreamhost, Bluehost, or HostGator is fine, as is any self-hosted web server like Apache, Nginx, XAMPP, or Microsoft IIS. Their support teams should be able to help you with the basics of getting started. We’ve chosen to use PHP and SQLite3 for this example to keep things as basic and universal as possible, but there is nothing wrong with rewriting the PHP file in NodeJS, Python, Ruby, Perl, or ASP.Net as you see fit. Some web hosts may not have the PHP Sqlite3 extension installed and enabled, but this should be an easy task or the code can be trivially modified to use MySQL instead.
  2. That you have a NetBurner module and development board and are familiar with running NBEclipse IDE and uploading code to your NetBurner device. A tutorial is available here if you still need to do that part. We will be using the DIP switches on the NetBurner dev board to demonstrate physical and electronic inputs without delving into breadboards or soldering.

Now that you have these fundamentals taken care of, let’s get started!

HTTP GET/POST Refresher

As any web developer knows, the two most basic HTTP requests are GET and POST, and that HTTP uses TCP port 80 or port 443 for HTTPS (HTTPS is encrypted with TLS/SSL, as indicated by a lock icon in your browser address bar). GET requests are what happens every time a web browser or other program asks to simply view a web address without making any significant changes, and POST requests are what happens whenever a web form or other data is submitted to the server, like for an e-commerce order or login attempt. By convention, we avoid making changes to the system on a GET request (so you don’t order fifty of something just because you hit the Back or Refresh buttons) and we don’t repeat POST requests unless we really want to make changes multiple times. Most web browsers ask you if you really want to re-send a POST request, to prevent such problems.)

This makes our most common website browsing experience very easy since any web browser on Earth can ask for webpages and send data back up as desired. One limitation of this basic structure, however, is that it says nothing about whether the web server has an urgent need to communicate with a client that isn’t actively sending requests. It’s only relatively recently that web browsers can ask a user if they’d like to receive “Push” notifications from a website, or that our smartphone apps are able to get information from the “cloud” as it arrives instead of by polling for updates every few minutes. We’ll start by demonstrating the basic request architecture in this article, and delve into the opposite “Push” architecture in the next article.

REST API Refresher

REST is an acronym for REpresentational State Transfer, and describes an approach to web-based APIs and programs that make use of the web’s existing structure rather than fighting it. Other older API architectures like SOAP or even RPC take different tradeoffs, but REST is generally accepted for its ease of implementation within the bounds of HTTP and web applications, especially because web servers are stateless and text-based by default: a basic webpage without any kind of data storage is the same no matter who views it or when each web request is handled by the server in its own separate context, and the contents may be viewed or parsed by an unlimited variety of clients or users. This can all be a major stumbling block for those new to web development since things like logging in, storing information, and maintaining state between requests are harder: we quickly need things like caching, cookies, and databases in order to create functional applications.

REST takes all this in mind, so when we make a “RESTful” web app we do things like:

  • make our URLs hierarchically represent the resources they Create, Read, Update, or Delete (commonly called CRUD)
  • make full idiomatic use of the HTTP methods GET, POST, and sometimes also PUT, PATCH, and DELETE
  • expose unique identifiers and useful URLs to allow clients to navigate and make full use of API data without excessive requests or logic
  • make use of appropriate HTTP headers and response codes to allow automated clients to behave appropriately
  • communicate and represent objects using highly portable, open datatypes like XML or JSON: plaintext may not be the most efficient, but everyone can read it

REST is a little bit looser of an approach than others, but it’s also often more quick and lightweight. We don’t necessarily need to implement a lot of things if our needs are simple, for example in this article we won’t be implementing any capability to modify or delete data, and our authentication and validation will be very rudimentary, though they can and should become much more powerful in real-world usage.

Stateful Embedded Systems Refresher

Contrary to PHP, HTML and Javascript, the C++ programming language was not designed to operate in a web browser or even over a network and certainly doesn’t just exist as a script that handles HTTP requests. When we write a program in C++, we usually write something that runs directly “on bare metal” and will continue running for as long (or as short) as we tell it to. We don’t necessarily need a separate database, session cache, or external storage, because any variables we create can hang around indefinitely if we want. Here we’re responsible for the entire lifecycle of the program and for all the memory and CPU usage we’re requesting: a mistake in a long-running low-level program like this doesn’t just make for a slow web server or laggy browser, it can lock up all of the hardware it’s running on requiring a restart or even OS-level recovery!

Since we’re writing a program that will start on boot and continue running forever, managing our program’s long-running state is a top priority: when we allocate memory, it will persist until we release it (or until the device freezes, crashes, or reboots.) Our program even contains its own while() loop that runs forever, so we need to pay attention to how, when, and where we’re declaring variables and allocating memory so we don’t end up with unintended infinite loops or runaway memory usage, unlike a script running inside of a separate web server that is somewhat sandboxed and only responsible for itself for the duration of one request.

NetBurner is also not a multi-core system that can run multiple threads simultaneously, so it’s important to use built-in NNDK functions for handling long-running or complex tasks when we can, rather than building basic functionality from scratch. For example, we wouldn’t want a network listener loop running constantly and having us miss DIP switch events or vice versa. What looks like an elementary program can get complex quickly when you’re responsible for everything that a CPU is doing!

C++ and NetBurner Refresher

Low-level compiled programming languages like C and C++ give you lots of power over how exactly electronic devices like CPU, memory, GPIO pins, and network devices are used, and are highly reliable once a compiled program is developed and tested. This is at the cost of increased complexity during development, however; we might hold some incoming data in a memory buffer, but if we assume that the data is 100 characters of text and it ends up being a kilobyte of binary data then some very weird things can happen if we don’t design our program in a careful and thoughtful manner. Instead of always managing text handed to us over HTTP by default, we’re managing bytes.

Specifically, if we allocate memory by using functions like malloc() or keywords like new, we need to free() that memory when we’re done. If we’re using fixed-size buffers like char buffer[100], we need to use functions that respect that 100-character size and put a null-terminating \0 byte at the end of strings so that other functions don’t continue reading random bits of memory beyond what was intended. This is normally included in most functions we use, but it also means that you can only fit 99 characters of text in a 100-character buffer to have room for that terminator. Fortunately, in this program we aren’t doing anything fancy, so the buffers and objects we’re creating are freed once we hit the end of our while() loop and don’t accumulate.

If you can review some C++ learning materials to understand pointers, stacks, heaps, memory allocation and release, safe buffer management, and scopes, you’ll be in a much better position to do your own C++ programming. The NetBurner library documentation is also a valuable reference to using NetBurner devices effectively since we include many functions for common tasks that would otherwise take a lot of effort to program yourself and are unique to the NetBurner system.

Web-Embedded Project Objective

We’ll be creating a simple client-server embedded and web architecture that uses HTTP POST to submit information from a NetBurner device to a web server and uses HTTP GET to view that information from any web browser or API client. We’ll also make sure that when clients submit or view data it’s done in a reasonably safe and reliable manner.

We’ll only really be publishing two “resources” within one “collection” in our simple app: the Creation of a new entry in our database of Device Switch records, and Reading the list of all those entries. This is what POST and GET are supposed to be used for anyway outside of a REST context, so it’ll be pretty plain. If we hypothetically wanted to extend this app to more functionality and use the full suite of REST methods, we could set up a hierarchy for accessing more resources either alongside or underneath Device Switch entries, like GET button.php?resource=presses&id=1234 to Read more details about a particular entry, or PATCH button.php?resource=presses&id=1234 to submit partial Updates to an entry’s data, but these are left as exercises for the reader.

We could even expose other queryable resources like Date information or Device Metadata without needing to change the database or restructure our URL hierarchy: we’ve left room for any such changes by architecting our database and URLs around the Resources being represented, instead of the tempting structure many new programmers find themselves creating where new programs are written and copy-pasted every time something new needs to be done, or accumulating an unwieldy bunch of mismatched parameters that get harder to manage with each change. That’s the power of using industry standard API design patterns like REST from the very beginnings of a project, and doing a little bit of planning.

Breaking out the Code

Let’s get a boilerplate HTTP GET and POST set up in a single-file PHP program that changes its behavior based on the HTTP method (GET/POST) and the passed variables in the query string or POST body. Other more advanced web frameworks use fancy web server configurations to create hierarchical URLs separated by slashes like /site/1/post/5/comment/22 but we don’t need complexity today so everything will be in one file, and we’ll use ?query=strings to keep our URLs straight.

Upload this to any PHP-capable webserver you choose. We use SQLite 3 for this example for simplicity, so the PHP Sqlite library will need to be installed and enabled. On our system running Ubuntu this was accomplished with sudo apt install php8.1-sqlite3 however your system may be different. You may prefer to use MySQL, Postgres, or any other database engine, that’s fine too, you’ll just need to change the appropriate database lines according to those libraries’ documentation.

				
					<?php
// Save as device.php

// Remove in production: show errors for debugging
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

$db = new SQLite3('/tmp/device.db');

function saveToDb($table, $device, $switches) {
    global $db;
    $date = date('c');
    $stmt = $db->prepare("INSERT INTO $table VALUES (:date, :device, :switches)");
    $stmt->bindParam(':date', $date, SQLITE3_TEXT);
    $stmt->bindParam(':device', $device, SQLITE3_TEXT);
    $stmt->bindParam(':switches', json_encode($switches), SQLITE3_TEXT);
    $stmt->execute();
}

function outputJsonResponse($code, $object) {
    header('Content-Type: application/json');
    http_response_code($code);
    echo json_encode($object);
}

if (@$_GET['resource'] == "switches") {
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
        if (@$_POST['device']) {
            // HTML create press (x-www-form-urlencoded POST)
            saveToDb("device_switches", $_POST["device"], $_POST["switches"]);

            echo "Saved. <a href='?resource=switches'>Return</a>";
            return;
        } else {
            // JSON create press (HTTP body POST)
            $json = file_get_contents('php://input');
            $obj = json_decode($json, true); // decode as array
            if (!$obj || !$obj['device']) {
                // return 400 BAD REQUEST
                outputJsonResponse(400, ["success"=>false, "status"=>"No JSON value for device provided."]);
                return;
            }

            saveToDb("device_switches", $obj["device"], $obj["switches"]);

            // return 201 CREATED
            outputJsonResponse(201, ["success"=>true, "status"=>"Saved.", "obj" => $obj]);
            return;
        }
    } else if ($_SERVER['REQUEST_METHOD'] == 'GET') {
        if ($_SERVER['HTTP_ACCEPT'] == 'application/json') {
            // JSON list switches
            $res = $db->query("SELECT * FROM device_switches ORDER BY date DESC");
            $out = [];
            while (($row = $res->fetchArray(SQLITE3_ASSOC))) {
                $out[] = $row;
            }
            header('Content-Type: application/json');
            echo json_encode($out);
            return;
        } else {
            // HTML list switches
            $result = $db->query("SELECT * FROM device_switches ORDER BY date DESC");

            $out = "";
            if ($result) {
                $out .= "<table border=1><thead><tr><th>Date</th><th>Device</th><th>Switches</th></tr></thead><tbody>";
                while (($row = $result->fetchArray(SQLITE3_ASSOC))) {
                    $out .= "<tr><td>".$row['date']."</td><td>".htmlspecialchars($row['device'])."</td><td>".htmlspecialchars($row['switches'])."</td></tr>";
                }
                $out .= "</tbody></table>";
            } else {
                $out = "No results.";
            }

            echo '<form action="" method="POST">
            <h3>Add New Entry:<h3> Device: <input name="device"><br/> Switches: <input name="switches"><br/>
            <input type="submit" value="Submit">
            </form>';

            echo $out;
        }
    } else {
        // return 400 BAD REQUEST
        outputJsonResponse(400, ["success"=>false, "status"=>"Incorrect HTTP method."]);
        return;
    }
} else if (@$_GET['resource'] == "setup") {
    // Remove in production: allow easy database setup
    if (@$_POST['action'] == "Do Setup") {
        // do the recreation of the DB table (POST update)
        $db->exec("DROP TABLE IF EXISTS device_switches;");
        $db->exec("CREATE TABLE IF NOT EXISTS device_switches (date TEXT, device TEXT, switches TEXT)");
        echo "Setup complete. <a href='?'>Return</a>";
        return;
    } else {
        // present user with the option (GET read)
        echo '<form action="?resource=setup" method="POST">
        Are you sure you want to clear the database and reset to defaults?
        <input type="submit" name="action" value="Do Setup">
        </form>';
        return;
    }
} else {
    // no resource selected, present options (GET read)
    echo 'Welcome to the Device Switches app. <br/>';
    echo '<a href="?resource=switches">View Device Switches</a> <br/>';
    echo '<a href="?resource=setup">Setup Database</a>';
    return;
}
				
			

Review the code to see the basic structure we’ve created: major resources are accessed by visiting device.php?resource=switches or device.php?resource=setup and the interactions or contents of those pages change based on the HTTP method GET or POST. Further inputs like the Accept header sent from the client (the confusingly named $_SERVER['HTTP_ACCEPT']) or the existence of the $_POST array (which is only populated for web form submissions, not when data is passed in the HTTP request body) let us keep our human-readable HTML and machine-readable JSON requests separate. Finally, we use named parameters to bring user-supplied inputs into our SQL statements to avoid SQL injection, instead of simply concatenating untrusted data into our database statements. We also run htmlspecialchars() on that untrusted data before outputting it to any webpages, just in case a prankster wants to say that their device is named <script>alert('Download Javascript Virus');</script> and try to make us vulnerable to an XSS attack when we go to display that device name inside a webpage.

The reason we sanitize on output instead of on input is that different contexts require different sanitization: storing an HTML-encoded string in the database only fixes the problem for HTML outputs, and could look &lt;script&gt;mangled&lt;/script&gt; when output in any other context. Developers must be vigilant at every step to use safe and appropriate methods to work with untrusted data coming in from other programs or users, even two programs that are part of the same overall infrastructure. Remember, this PHP program runs inside your web server, the HTML output runs inside people’s web browsers, and both are accessible to the entire Internet! We wouldn’t want to give random people on the Internet the power to affect our coworkers’ computers, or the computers of other random people on the Internet. With great power comes great responsibility.

Let’s test out our new web app by pointing your web browser to device.php on your web server to make sure it works, something like http://change.me.example.com/device.php  . The first page should show you two options, View Presses or Setup Database. The View Presses page should throw an error since the database has never been set up before: you can fix this by visiting the Setup Database page and clicking the Do Setup button — once things are working, you’ll probably want to comment that section of the code out, which is lines 93-95 above.

Also be aware that this code stores our SQLite database in the web server’s /tmp folder, which means it will be erased whenever the web server wants (usually on reboot.) If you use this code for anything serious, a more permanent and safe location for your database is a good idea.

You can use the built-in form and submit button to add entries yourself and test that things are working:

You can also optionally test JSON POST without any web form or embedded device at all by submitting the request directly using a program like Curl for Linux and Mac, or PowerShell’s Invoke-RestMethod for Windows, making sure to change the URL to point to your web server:

curl -X POST -H "Content-Type: application/json" -d '{"device": "MyDeviceName", "switches": "[0,1,0,1,0,1,0]"}' "http://change.me.example.com/device.php?resource=switches"

Invoke-RestMethod -Uri "http://change.me.example.com/device.php?resource=switches" -Method POST -Body '{"device": "MyDeviceName","switches": "[0,1,0,1,0,1,0]"}' -ContentType "application/json"

Notice how we avoid quote mark issues by using the apostrophe symbol around values that contain quotes. On some systems, this doesn’t work and we have to “escape” quotes within quotes with a slash like "{ \"value\": \"MyTestValue\" }" so if you encounter issues with the commands above, try replacing all apostrophes with double quotes and escaping those inner double quotes.

Embedded Time

Next, let’s set up our embedded code. Create a new NetBurner Application inside of NBEclipse and paste the following into main.cpp . Notice that we have a config_string gTargetUrl set up near the top. You could change this to the URL where you’ve published your PHP file above, but leave it as-is for now because this is a good opportunity to demonstrate NetBurner’s powerful config system:

				
					// Overwrite main.cpp in a NetBurner project

#include <init.h>
#include <json_lexer.h>
#include <webclient/http_funcs.h>
#include <config_obj.h>

const char *AppName = "SwitchCounter";

//Device specific configurable variables (customize via config system)
config_string gDeviceId{appdata, "My Device", "Device ID"};
config_string gTargetUrl{appdata, "http://change.me.example.com/device.php?resource=switches", "Target URL"};

void UserMain(void *pd)
{
	init(); // Initialize network stack

	WaitForActiveNetwork(TICKS_PER_SECOND * 5);// Wait for DHCP address

	iprintf("Application started\n");

	uint16_t last_dip_switch=0xFFFF; // Use a value that can't be correct as a default

	while(1) {
		uint8_t sw=getdipsw();
		if (sw!=last_dip_switch) {
			ParsedJsonDataSet jsonResponse;
			ParsedJsonDataSet jsonRequest;
			jsonRequest.StartBuilding();
			jsonRequest.Add("device", gDeviceId.c_str());
			jsonRequest.AddArrayStart("switches");
		    jsonRequest.AddArrayElement(((sw>>7) & 1));
		    jsonRequest.AddArrayElement(((sw>>6) & 1));
		    jsonRequest.AddArrayElement(((sw>>5) & 1));
		    jsonRequest.AddArrayElement(((sw>>4) & 1));
		    jsonRequest.AddArrayElement(((sw>>3) & 1));
		    jsonRequest.AddArrayElement(((sw>>2) & 1));
		    jsonRequest.AddArrayElement(((sw>>1) & 1));
		    jsonRequest.AddArrayElement(((sw>>0) & 1));
		    jsonRequest.EndArray();
		    jsonRequest.DoneBuilding();

		    char jsonOutBuffer[200];
		    jsonRequest.PrintObjectToBuffer(jsonOutBuffer, 200, false);

		    printf("Switches changed, POSTing %s to %s ...\r\n", jsonOutBuffer, gTargetUrl.c_str());

			bool result = DoJsonPost(gTargetUrl.c_str(),jsonOutBuffer,jsonResponse,0,TICKS_PER_SECOND*60);
			if (result) {
				printf("The POST was a success:\r\n");
				jsonResponse.PrintObject(true);
			} else {
				 printf("The POST failed\r\n");
			}

			last_dip_switch=sw;
		}
		OSTimeDly(TICKS_PER_SECOND);
	}

}

				
			

Reviewing what we’ve just written, you’ll notice a few decisions being made and some basic structure of NetBurner programs: first, execution starts in the UserMain function, but we could add or use any functions we desire above or via included files.

Second, we use printf() functions to output serial information since otherwise we’ll have no insight into how the program is running unless the data shows up on the web side.

Third, we can’t make very many assumptions about the environment our NetBurner wakes up in: the network cable may be unplugged and we may be unable to contact the web server, so handling those conditions sanely is important.

Fourth, we once again use library functions to build our JSON instead of assembling the text strings by hand: it may seem more complex at first, but it’s the best way to reliably represent data without introducing bugs, especially intermittent or hard-to-diagnose bugs if a user does something like name their device with a quote or apostrophe. The resulting JSON would need to look like {"value": "My Alleged \"Name\""} and that gets messy and error-prone very quickly.

Finally, C++ is low-level enough that the easiest way to deal with strings is with arrays of characters, but that means we make assumptions about how big those arrays need to be: a huge security and reliability issue with C and C++ are buffer overflows, when the program tries to write more data to a variable than has been allocated. You’ll notice that some functions we use here like PrintObjectToBuffer() take a number of characters as an argument. We set that number equal to the size of the buffer we’re using to avoid overflows.

Now that we have both sides coded, we can see the whole project in action. Run the project in NBEclipse to upload the code to your device.

Once it’s uploaded, access the NetBurner device’s config page on port 20034 to change the Device ID and Target URL as desired to point to your web server correctly. By setting these values in the NetBurner Config instead of code, you can distribute the same code to many devices and then customize them at runtime instead of needing to compile new code for every device. Press the Update Record button when you’re done to save your changes:

Every time the device boots or the DIP switches on the NetBurner Dev Board are changed, it will submit new entries to the URL you’ve provided:

If you encounter issues, you can see some debugging information by viewing your NetBurner device’s serial terminal via the attached USB cable:

Finally, to really enable the power of the web, our PHP script exposes a JSON GET method that can be consumed by any other website or web service. Here’s some HTML and Javascript that can be hosted anywhere and pointed to the device.php we created earlier, to display the submitted data as desired. Remember, if you host it anywhere else besides the same server as your PHP file, change the URL to point to the full path to your web host as usual. Here we’ve assumed that device.php and device.html are hosted side by side.

				
					<!DOCTYPE html>
<!-- Save as device.html -->
<html>
<head>
  <script type="text/javascript">
    window.addEventListener('load', async function(){
      const response = await fetch("device.php?resource=switches&format=json", {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        }
      });
      const result = await response.json();
      console.log(result);

      let h3 = document.createElement("h3");
      h3.innerText = "Device Switch Entries: "+result.length;

      let dl = document.createElement("dl");
      result.map(function(item){
        const d = new Date(item.date);
        let dt = document.createElement("dt");
        dt.title = d.toLocaleString();
        dt.textContent = item.device;
        dt.style = "font-weight: bold";
        dl.appendChild(dt);
        let dd = document.createElement("dd");
        try {
          let switchArr = JSON.parse(item.switches);
          for (i=0;i<switchArr.length;i++) {
            let icon = document.createElement("span");
            if (switchArr[i] == 1) {
              icon.style = "background: green; color: white; padding: 0 0.1em;";
              icon.textContent = "On";
            } else {
              icon.style = "background: red; color: white; padding: 0 0.1em;";
              icon.textContent = "Off";
            }
            dd.appendChild(icon);
          }
        } catch (e) {
          dd.textContent = item.switches;
        }
        dl.appendChild(dd);
      });

      const out = document.getElementById("deviceOut");
      out.appendChild(h3);
      out.appendChild(dl);
    });
  </script>
</head>
<body>
  <div id="deviceOut"></div>
</body>
</html>
				
			

Once again review what we’ve written: we send a GET request to our device.php app asking for the listing of all button presses, in JSON format. We then we build an HTML view with an H3 heading tag and an OL numbered list tag containing the date and device name for each item in our JSON response which gets parsed into a native Javascript array of objects. Also, notice that to prevent the XSS attacks mentioned previously, we don’t simply "concatenate "+untrusted.strings+" together" and output the resulting HTML straight to the page like we might be tempted to: that would mix untrusted data in with the HTML we are writing, possibly exposing anyone who visits our page to malicious Javascript. Instead, we use the provided element builders and the documented safe textContent property to tell the web browser exactly how to handle this data: as plain text to be displayed, not as any kind of code to be executed.

And here’s how our new device.html should look in your browser, though obviously there is unlimited flexibility in how the Javascript, HTML, and styling can be customized:

Note that one thing this tutorial does not cover is securing the connection with HTTPS (aka SSL/TLS). Setting up a certificate on the web server and ensuring that the NetBurner only communicates with a valid server are left as separate steps.

Please have fun changing these examples to do something new that interests you, and make a comment here or in our community forum if you encounter any issues or would like to show off!

Our next example, building a pub/sub or “push” style payments architecture for a hypothetical Laundromat, will be posted soon!

Share this post

Subscribe to our Newsletter

Get monthly updates from our Learn Blog with the latest in IoT and Embedded technology news, trends, tutorial and best practices. Or just opt in for product change notifications.

Leave a Reply
Click to access the login or register cheese