🤯 50% Off! 700+ courses, assessments, and books

How to Build a WI-FI Dashboard Using Node.js and Ractive.js

Marcello La Rocca
Share

This article was peer reviewed by Marc Towler. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

In the past months, I’ve published a mini-series about dashboards. In the first article titled “Creating a Battery viz Using Node.js: Getting Started and Server”, I’ve shown how to create a Node.js server that checked the battery status on a laptop and returned a few useful information. In the second one, whose title was “Creating a Battery viz Using Node.js: Client”, I’ve explained how to build a Web application to visualize those information in a nicer and user-friendly way.

In this article, we’re going to build on that dashboard and add information about available WI-FI networks. A list of the available networks will be shown with a summary of the most relevant details (name, address, protected vs. open, and so on) and upon selection, further details of the network will appear in another panel.

Take a peek at our goal:

dashboard

If you want, you can consider it as a starting point. I strongly suggest you to customize it since this is the best way to improve your skills with the technologies we’re going to use.

Server

On the server side, we’re going to reuse and extend what we’ve created for the battery viz. In this tutorial we’ll focus on Ubuntu but the server code is structured in such a way that you’ll need to write just a couple of adapters to support Mac or Windows’ machines.

Bash Command

To start, we extend the original configuration method by adding commands and callbacks for the new endpoint. Some renaming has been necessary to prevent clashes with commands for the battery widget.

function switchConfigForCurrentOS () {
      switch(process.platform) {
        case 'linux':
          return {
            batteryCommand: 'upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"',
            batteryProcessFunction: processBatteryStdoutForLinux,
            wifiCommand: 'iwlist wlan0 scanning | egrep "Cell |Address|Channel|Frequency|Encryption|Quality|Signal level|Last beacon|Mode|Group Cipher|Pairwise Ciphers|Authentication Suites|ESSID"',
            wifiProcessFunction: processWifiStdoutForLinux
          };
        case 'darwin': //MAc OsX
        ...
      }
    }

Processing Command Output

The way we process the command output is really similar to what we’ve already done for battery. We go through the output line by line and process it to extract meaningful parameters from our readings. But in this case we’re getting readings about a list of items, not a single one! So we need to identify when a new item actually starts in the output, and create a new object for each item. Then we’ll filter valid lines, adding the properties we read to our current item.

function processWifiStdoutForLinux(stdout) {
      var networks = {};
      var net_cell = "";
      var cell = {};

      stdout.split('\n').map(trimParam).forEach(function (line) {
        if (line.length > 0) {
          //check if the line starts a new cell
          if (stringStartsWith(line, NET_CELL_PREFIX)) {
            if (net_cell.length > 0) {
              networks[net_cell] = mapWifiKeysForLinux(cell);
            }
            cell = {};
            line = line.split("-");
            net_cell = line[0].trim();
            line = line[1];
          }
          //Either way, now we are sure we have a non empty line with (at least one) key-value pair
          //       and that cell has been properly initialized
          processWifiLineForLinux(cell, line);
        }

      });
      if (net_cell.length > 0) {
        networks[net_cell] = mapWifiKeysForLinux(cell);
      }
      return networks;
    }

Before seeing in details what happens inside processWifiLineForLinux, let me highlight a few points:

  • Since we add a cell to our hash only when the description of the next one starts, we would otherwise miss the final if statement (to capture the last network in the output).
  • The code above assumes that two cells can’t share the same name. This is a reasonable assumption because networks are not indexed by their name (that info is captured by the ESSID field). They are listed and assigned a progressive identifier “Cell 0X”.
  • The last thing we do before storing properties is a call to mapWifiKeysForLinux and in this case they just return the keys unaltered.

Even the function that processes each line is very similar to what we’ve created for the battery: since each line holds a field name and its values, we first check for edge cases that need to be handled in a special way, and for the remaining lines we just split them and store the key-value pair appropriately.

function processWifiLineForLinux(cell, line) {
      var key;
      var val;

      line = line.trim();
      if (line.length > 0) {

        switch (true) {
        case stringStartsWith(line, NET_ADDRESS_PREFIX):
          line = line.split(':');
          line.splice(0, 1);
          //INVARIANT: Address in the format Address: DC:0B:1A:47:BA:07
          if (line.length > 0) {
            cell[NET_ADDRESS_PREFIX] = line.join(":");
          }
          break;
        case stringStartsWith(line, NET_QUALITY_PREFIX):
          //INVARIANT: this line must have a similar format: Quality=41/70  Signal level=-69 dBm
          line = line.split(NET_SIGNAL_PREFIX);
          cell[NET_QUALITY_PREFIX] = line[0].split("=")[1].trim();
          if (line.length > 1) {
            cell[NET_SIGNAL_PREFIX] = line[1].split("=")[1].trim();
          }
          break;
        case stringStartsWith(line, NET_EXTRA_PREFIX):
          //INVARIANT: this line must have a similar format: Extra: Last beacon: 1020ms ago
          line = line.split(":");
          //we can ignore the prefix of the string
          if (line.length > 2) {
            cell[line[1].trim()] = line[2].trim();
          }
          break;
        default:
          //INVARIANT: the field must be formatted as "key : value"
          line = line.split(":");
          if (line.length > 1) {
            //Just stores the key-value association, so that coupling with client is reduced to the min:
            //values will be examined only on the client side
            cell[line[0].trim()] = line[1].trim();
          }
        }
      }
      return cell;
    }

This discussion is the perfect chance to show you a neat trick I’ve recently “borrowed” from a fellow engineer. It will allow us to use a switch statement instead of a chain of ifelses.

Endpoint

Adding the new endpoint to our server is trivial thanks to Node’s HHTP module and the helper methods we’ve created in the previous tutorial. We just need to define the Regular Expression for the paths we want to respond to, and add an if statement to the server callback, triggered on incoming requests:

var server = http.createServer(function (request, response) {
      var requestUrl = request.url;
      var filePath = BASE_URL + requestUrl;

      if (requestUrl === '/' || requestUrl === '') {
        response.writeHead(301,
          {
            Location: BASE_URL + 'public/demo.html'
          });
        response.end();
      } else if (RE_BATTERY.test(requestUrl)) {
        getBatteryStatus(response, onBatteryInfo, onError);
      } else if (RE_NETWORKS.test(requestUrl)) {
        getWifiStatus(response, onWifiInfo, onError);
      }  

      ...

    }

At this point all we have to do is just creating a callback that will run the command, transform its output and finally send the JSON result to the client, wrapped in the HTTP response provided by http.createServer.

function getWifiStatus(response, onSuccess, onError) {

      child_process.exec(CONFIG.wifiCommand, function execWifiCommand(err, stdout, stderr) {
        var wifi;

        if (err) {
          console.log('child_process failed with error code: ' + err.code);
          onError(response, WIFI_ERROR_MESSAGE);
        } else {
          try {
            wifi = CONFIG.wifiProcessFunction(stdout);
            onSuccess(response, JSON.stringify(wifi));
          } catch (e) {
            console.log(e);
            onError(response, WIFI_ERROR_MESSAGE);
          }
        }
      });
    }

For the final step, notice that we reused the onSuccess function that we had conveniently defined for the battery endpoint (same thing for the onError handler).

Client

Now, let me introduce you the funniest part of this example. We’re going to massively use Ractive.js for the Web client. It is a lightweight, powerful framework that combines two-way binding (AngularJS-style) with HTML templates (like mustache or Handlebars).

The stress on templates (even more than AngularJS, way more than React), is indeed one of Ractive.js’ hallmarks, together with its blazingly fast performance, as a result of a clever engine that always computes the smallest possible DOM elements to be refreshed when data change.

We’re going to add two panels to our dashboard:

  • One for the list of networks in our surroundings (showing a brief summary for each item).
  • Another one that only appears once a network is selected and displays detailed information for that WI-FI connection.

Template

Let’s start by discussing the HTML templates to display our data, and then we’ll see how to bind server’s data to them.

Wi-Fi List

The most complex template we need is the one showing the list of the available networks. The first dozen of lines just define the container panel, and use Ractive.js’ binding to conditionally show an icon warning about server errors, and a button to pause/resume server polling:

<div class='col-md-6 outer-panel'>
      <div class='inner-panel networks-panel'>
        <span class='title'>Available WiFi Networks</span>

        <div class='update-error' style={{!networksUpdateError ? 'visibility:hidden;' : ''}} data-toggle="tooltip" data-placement="top" title='Unable to refresh WiFi data'>
        </div>

        <div class='play-button' on-click='networks-play' style={{!networksPaused ? 'display:none;' : ''}} data-toggle="tooltip" data-placement="top" title='Restarts WiFi updates'>
        </div>
        <div class='pause-button' on-click='networks-pause' style={{networksPaused ? 'display:none;' : ''}} data-toggle="tooltip" data-placement="top" title='Pause WiFi updates'>
        </div>

        <br>  
        <br>
        {{^wifiNetworks}}
            LOADING...
        {{/wifiNetworks}}
        <div class="list-group">
        {{#wifiNetworks: num}}
          <a href="javascript:" class="list-group-item" id={{'network_' + num}} on-click="expandWifi">
            <h5 class="list-group-item-heading">{{ESSID}}</h5>
            <p class="list-group-item-text">{{Address}}</p>
            {{#isNetworkEncrypted(this)}}
              <div class='protected-wifi'>
              </div>
            {{/Encription}}
          </a>
        {{/wifiNetworks}}
        </div>

      </div>
    </div>

The double-stache {{ }}, as in mustache and Handlebars, are markers for dynamically injected content. Ractive.js allows us to use expressions and run functions inside the brackets, as long as those functions and the data used are globally available (Math.round, for example) or have been added to the data field of the JavaScript object bound to the template.

The result of the expression inside the brackets will be escaped, so it’s going to be plain text. But sometimes you might need to add few HTML lines to your elements. There’s an alternative way to do it, but if you really think you need it, you can use triple-stache {{{ }}}.

Using triple-stache is safe because the scripts will be escaped and not executed, but it is slower than double-stache, so you should try to avoid it as much as possible.
The second part of the template is much more interesting. We iterate through the list of networks with {{#wifiNetworks: num}}, capturing the index of each item in the num variable.

For each item in the list, we add a callback handling clicks (see below) and show a summary of its values.

Notice how closing tags don’t have to match opening tags text:

{{#isNetworkEncrypted(this)}}
      ...
    {{/Encription}}

The opening one is an if tag whose condition is a function, which would make it weird running it again in the closing one. So we can use a meaningful message to pair the two tags, just for maintenance’s sake.

Selected Wi-Fi Details

{{#selectedNetwork !== null}}  
      <div class='inner-panel network-details-panel'>
      <span class='title'>Details about {{selectedNetwork.ESSID}}</span>
      <br>
      <br>
      {{#selectedNetwork:key}}
        <span class='key'>{{key}}:</span> <span class='value'>{{this}}</span>
        <br>
      {{/selectedNetwork}}  
      </div>
    {{/selectedNetwork}}

The panel for the network’s details is quite simple: we only show it if we have assigned a value to the selectedNetwork field in our ractive object. Then we show the network’s name (the ESSID field), and display all the key-value pairs we have received from the server.

This is aimed to obtain the lowest possible coupling, but of course you can modify it to highlight some information or display them in a more meaningful way.

JavaScript

We’ll setup a polling daemon that asynchronously queries the server at given intervals of time. Every Ajax call will provide the updated list of WI-FI networks. All we have to do when we receive a JSON response from the server, is to acknowledge we received a successful response and update the fields in which we store the list of networks inside the ractive object.

Setup

As we’ve shown in the previous article, to bind a template to some data, we just need to create a new Ractive object, hook it up with the template’s ID (#meterVizTemplate below), and the target DOM elements, i.e. the node that is going to be the template’s parent in the DOM tree (panels below).

Then we just need to add all the objects or values we want to use in the template as fields of ractive.data. This can be done on initialization (as below) or later, using ractive.set().

ractive = new Ractive({
      el: 'panels',
      template: '#meterVizTemplate',
      data: {
        wifiNetworks: []
        ...
      }
    });

Daemons

We’ll use the same mechanism for the daemon and to pause/restart querying the server as we did for the battery. For the sake of brevity we won’t repeat it here, but if you want to deepen this topic you can take a look at this article or to the GitHub repository.

Ajax Calls

The only thing that our new daemon does, is making an Ajax call and then updating our data in case of success or the field signaling network problems, in case of errors.

function updateWifiNetworksList () {
      $.ajax(WIFI_SERVICE_URL, {
          dataType: 'json',
          jsonp: false
        })
        .then(function (networks) {
          ractive.set('networksUpdateError', false);
          ractive.set('wifiNetworks', networks);
        }).fail(function () {
          ractive.set('networksUpdateError', true);
        });
    }

We should also check that the JSON file we got is well formatted. We don’t have to worry about script injection, since Ractive.js already escapes the field values before adding them to the DOM.

It’s worth noting that the jQuery.getJSON() method, which would be a shortcut for the $.ajax() method used above, is safe as long as:

1. You don’t include the 'callback=' string in your URL (which would allow the JSON code to be executed).
2. You can trust the server you are calling.

Since we’re not using user provided content for the URL, one would think that it should not be a concern.

However, if our server was to be compromised, then we would have no barrier to protect us from injected code. If an explicit 'dataType' header is not set, then jQuery will try to guess the content from the response, and a response from a malicious server might contain JavaScript code.

Although this possibility is not so common, we can’t rule it out completely. For this reason, it’s not a bad idea adding an extra layer of protection at the price of a little more typing.

Updating the Dashboard

The most relevant add-on for this step will be that we respond to clicks on the list and show details for the selected network:

expandWifi:   function (event) {
      var selectedNetworkItem = ractive.get('selectedNetworkItem'),
          currentSelection = $(event.node);
      if (selectedNetworkItem && $.isFunction(selectedNetworkItem.toggleClass)) {
        selectedNetworkItem.toggleClass("active");
      }
      currentSelection.toggleClass("active");
      ractive.set('selectedNetworkItem', currentSelection);
      ractive.set('selectedNetwork', event.context);
    },

To do this we define an ad-hoc event handler. As mentioned above, it will be called when we click on any list entry, and then the event associated with the click will carry information about the selected network itself.

Now, if we weren’t using Ractive.js, let’s say we were using jQuery only, we would have to:

  • Call a method that would take the selected network’s ID;
  • Use it to find the network object for that ID (likely stored in a dictionary);
  • Find the DOM element for the “selected network panel”;
  • Remove the old DOM tree inside the panel and iteratively create a new list displaying the key-value associations, mixing a lot of HTML strings inside our JavaScript code.

Ractive.js will take care of all this for us, and it will do it better than we would (on average), changing the smallest possible DOM subtree only.

First, the event object sent to the on-click handler will have a context field that contains the data bound to the DOM element. In other words, we get the network data object “for free”.

Once we have that, the only thing we have to do is using it to update our ractive object, already bound to the template. Ractive.js’ engine will do the rest, updating the DOM and displaying the changes.

Conclusions

Finished! We have our dashboard “pimped”. As I said in the introduction, this is just a starting point.
If you have followed along, you should now be able to easily display lists of complex items, handle item selection, and safely communicate to the server.

You can use these skills for a number of other tasks, not necessarily involving displaying stats for your laptop. From showing a list of restaurants around the user to enumerating home appliances, you can control all through a Web interface or your mobile. The choice is yours, and there is no limit.

If you want to deepen the topics covered in this article, I suggest you to take a look at these good resources: