JavaScript
Article

Creating a Battery viz Using Node.js: Getting Started and Server

By Marcello La Rocca

If your initial reaction to the title of this article was something like WHAT?, I want to reassure you. You don’t have to take my word for it! What I’m going to do is to show you how to build a fine piece of software that can run on multiple operating systems, interact with them and present the results in a pleasant way. The whole task will be achieved by using JavaScript and a small amount of bash/powershell commands.

Said that, you might be wondering why I want to make this experiment. It might come as a surprise, but “winter nights are long and lonely and I needed something to kill some time” is not the answer to this question. Maybe something on the line of “I wanted to refine my skills and master JS” would be closer.

Although this project does not carry a high value on itself, my humble opinion is that it will:

  • provide you the skills (and some basic design) to build a RESTful service and any interface you’d like for your favourite OS
  • let you focus on cross-OS compatibility
  • introduce you to valuable design patterns for JavaScript and useful Node.js modules.

With this in mind, let’s start talking about the server. What we need is to create a (RESTful) service that provides us, in real-time, the last readings from our OS.

Why do we need a server? And why RESTful?

The answer to these two smart questions is simple. Firstly, we need a server because, for security reasons, there is no way a browser would allow you to execute a command on the OS (I bet you wouldn’t be too happy if any creepy website was able to erase all your files, would you?). Secondly, we’ll have a RESTful service because there are several advantages in using REST interfaces. This is out of our scope, but I’ll point interested readers to a few good resources to learn more about this topic at the end of this article.

Now, what we want is at least one endpoint that can be called from any service over the HTTP protocol, hence decoupled from the actual representation of the data it’ll provide, and in response sends data back to the caller.

To send this data back, we’ll certainly need to agree on a format. We could send back some raw text and leave parsing to the client or, as an alternative, we could send structured data (using XML for instance). I ended up choosing JSON. The reason is that we’ll have structured data but far less redundant than XML. Note that by agreeing on a format for the data we introduce a certain coupling for the client, that now has to adhere to our formatting. Nevertheless, this choice gets several advantages:

  • We can specify the format as part of our interface: clients naturally have to adhere to APIs of any service they use (for instance, the name of the methods or the endpoint exposed) and as long as we don’t change the format, there will be no difference. Obviously we should still think this format through before hitting version 1. In fact, we should (almost) never change a public interface in order to avoid that clients will be broken.
  • We would sensibly slow down clients by delegating parsing to them.
  • We gain decoupling from different OSs by providing a common format for all of them. To support a new OS, all we need is an adapter for the data we receive from it.

At this point, we need to start talking about how and where we’ll get the data we send to the client. This is maybe the trickiest part of the game, but luckily there are plenty of modules for Node.js that allow our server to talk to our OS, and even understand which OS is running on our machine.

Creating Endpoints

To create the core of our service, we need to use Node.js’ HTTP module to handle incoming GET requests:

var http = require('http');
var PORT = 8080;

Since we are building an application that will run on localhost only, we can use a static (constant) value for the port. Another option is to read it from command line and fall back to the constant value when this is not provided. We can read command line arguments from process.argv. Since the first argument will always be "node" and the second one the name of the JavaScript file we are running, we are interested in the third argument:

var PORT = Number(process.argv[2]) || 8080;

HTTP module makes easy to create a server and listen to a port. We just need to use two functions declared in the module, createServer() and listen(). The former takes as input a callback with two arguments, the request and its response, while the latter just takes the port number we need to listen to. We want to create REST endpoints, so we need to check what path has been requested. Moreover, we want to perform different actions depending on which of our endpoints it matches. Let’s say we want the path for battery info to be /battery. To allow for small variations (like /battery/) we are going to define a regular expression to match our endpoint:

var RE_BATTERY = /\/battery\/?/;

Going back to createServer() argument, it’ll be a function providing access to the request (and response) object, which in turn has a field with the URL requested. Putting it all together, we should have the following code:

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

  if (RE_BATTERY.test(requestUrl)) {
    getBatteryStatus(response, onBatteryInfo, onError);
  }
}).listen(PORT);

getBatteryStatus() is a function we’ll define shortly. We delegate to this function the responsibility of sending a response to the caller by using two response‘s methods: write() and end().

Serving Static Content

Besides defining our endpoint(s), we also need to serve some static content which will be provided by the same server. A different design with two servers, one for static content and one for dynamic content, would have been possible as well. However, it might have been unnecessary if not detrimental since there’s no need to occupy one more port if we consider that we’ll be the only client requesting static content.

The HTTP module comes to the rescue even in this case. Firstly, if clients request our root, we’ll redirect them to our main page:

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

Then we’ll add an `else` branch to the conditional above. If the request does not match any of ours endpoints, our server will check if a static file exists for that path, and serve it, or respond with a 404 (not found) HTTP code.

else {
   fs.exists(filePath, function (exists) {
      if (exists) {
         fs.readFile(filePath, function (error, content) {
            if (error) {
               response.writeHead(500);
               response.end();
            } else {
               response.writeHead(200);
               response.end(content, 'utf-8');
            }
         });
      } else {
         response.writeHead(404, {'Content-Type': 'text/plain'});
         response.write('404 - Resurce Not found');
         response.end();
      }
   });
}

Running OS Commands

To run our operative system’s commands from Node.js we need another module called child_process, that will also provide us with a few utility methods.

var child_process = require('child_process');

In particular, we’re going to use the exec() method that allows to run commands in a shell and buffer their output.

child_process.exec("command", function callback(err, stdout, stderr) {
   //....
});

However, before this one, we have a few more steps to follow: as first thing, since we want our dashboard to work with multiple operative systems and the commands to get the battery status to be different from one OS to another, we need a way to let our server behave differently, depending on our current OS. It also goes without saying that we need to identify and test the right command for all the OSs we want to support.

Identify Current OS

Node.js provides an easy way to inspect the underlying OS. We need to check process.platform, and switch on its value (being careful about some idiosyncrasy in naming):

function switchConfigForCurrentOS () {
  switch(process.platform) {
    case 'linux': 
      //...
      break;
    case 'darwin': //MAC
      //...
      break;
    case 'win32':
      //...
      break;
    default:
      //...
  }
}

Once we got that information, we can focus on retrieving the right commands on different platforms. Besides the different syntax, the fields returned will have different naming/format. Therefore, we’ll have to take this into account once we retrieve the commands’ results. The following sections describe the command for the different operating systems.

OsX
pmset -g batt | egrep "([0-9]+\%).*" -o
Linux
upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"
Windows
wmic Path Win32_Battery

Applying The Template Pattern – OS-dependent Design

We could check which OS we are running on for every call, but that seems like a waste. The underlying operative system is one thing it’s unlikely to change during our server lifetime. That might be possible in theory if our server process was somehow going through marshaling/unmarshaling, but this is certainly not practical, nor easy nor sensible.

For this reason, we can just check the current OS at server startup and select the most appropriate commands and parsing functions according to it.

Although some details change, the general workflow for handling requests will be the same across all OSs:

  1. We call child_process.exec to run a command;
  2. We check if the command was successfully completed, otherwise we deal with the error;
  3. Assuming it was successful, we process the output of the command, extracting the information we need;
  4. We create a response and send it back to the client.

This is the perfect case use for the Template method design pattern described in the Gang of four book.

Since JavaScript is not really class-oriented, we implement a variant of the pattern where the details, instead that to subclasses, are deferred to functions that will be “overridden” (through assignment), depending on current OS.

function getBatteryStatus(response, onSuccess, onError) {

    child_process.exec(CONFIG.command, function execBatteryCommand(err, stdout, stderr) {
        var battery;

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

Now, we can plugin what we have already found out about the commands into our switchConfigForCurrentOS() function. As mentioned above, we’ll need to override both the command run and the post processing function, accordingly to the current OS.

function switchConfigForCurrentOS() {
    switch (process.platform) {
        case 'linux':
            return {
                command: 'upower -i /org/freedesktop/UPower/devices/battery_BAT0 | grep -E "state|time to empty|to full|percentage"',
                processFunction: processBatteryStdoutForLinux
            };
        case 'darwin':
            //MAC
            return {
                command: 'pmset -g batt | egrep "([0-9]+\%).*" -o',
                processFunction: processBatteryStdoutForMac
            };
        case 'win32':
            return {
                command: 'WMIC Path Win32_Battery',
                processFunction: processBatteryStdoutForWindows
            };
        default:
            return {
                command: '',
                processFunction: function () {}
            };
    }
}
Processing Bash Output

Our strategy is providing a different version of the post processing method for each OS. We want to have a consistent output – our data API, as mentioned in the introduction – with the same information mapped to the same fields, no matter what the platform is. To achieve this task, we basically define for each OS a different mapping between the output fields and the name of the corresponding field retrieved from the data.

An alternative could have been sending an extra "OS" parameter to the client, but I think that the coupling introduced. Moreover, splitting the logic between server (where it belongs) and client would have been a bigger turn-off than any possible simplification or performance gain.

function processLineForLinux(battery, line) {
    var key;
    var val;

    line = line.trim();
    if (line.length > 0) {
        line = line.split(':');
        if (line.length === 2) {
            line = line.map(trimParam);
            key = line[0];
            val = line[1];
            battery[key] = val;
        }
    }
    return battery;
}

function mapKeysForLinux(battery) {
    var mappedBattery = {};
    mappedBattery.percentage = battery.percentage;
    mappedBattery.state = battery.state;
    mappedBattery.timeToEmpty = battery['time to empty'];
    return mappedBattery;
}

function mapKeysForMac(battery) {
    var mappedBattery = {};
    mappedBattery.percentage = battery[0];
    mappedBattery.state = battery[1];
    mappedBattery.timeToEmpty = battery[2];
    return mappedBattery;
}

function processBatteryStdoutForLinux(stdout) {
    var battery = {},
    processLine = processLineForLinux.bind(null, battery);
    stdout.split('\n').forEach(processLine);
    return mapKeysForLinux(battery);
}

function processBatteryStdoutForMac(stdout) {
    var battery = stdout.split(';').map(trimParam);
    return mapKeysForMac(battery);
}

Processing functions for Windows are a bit more complicated and, for the sake of simplicity, they are omitted in this context.

Putting It All Together

At this point we just need to do some wiring, encoding our data in JSON, and a few constants that we still need to declare. You can take a look at the final code of the server on GitHub.

Conclusions

In this first part of this mini-series, we discussed the details of the service we’re building and what you’ll learn. We then covered why we need a server and why I chose to create a RESTful service. While discussing how to develop the server, I took the chance to discuss how you can identify the current operating system and also how to use Node.js to run commands on it.

In the second and final part of this series, you’ll discover how to build the client part to present the information to the users in a nice way.

  • M S i N Lund

    “Creating a Battery viz Using Node.js

    Fixed the headline for you…

    Seriously, you shouldn’t have to read half the article, to find out that this is a Node.js-only project.

    I like JS just fine, that is why i started reading, but i have zero interest in installing Node.js on my server.

    • OphelieLechat

      Thanks for that Mats, totally valid. I’ve just updated the article title to reflect this.

      • http://mlarocca.github.io Marcello La Rocca

        If Aurelio agrees, could we perhaps change the tile to:

        Creating a Battery viz Using JavaScript: Getting Started and Server with Node.js
        I kind of liked to stress that the whole project only requires JS.
        (sorry for the inconvenience)

      • M S i N Lund

        Perfect, thanks.

        Would it be possible to automatically display both the name of the section and sub-section in the little blurb on the side of the headline?

        That would offload the workload of the headline-author a bit.

        …and for the reader too.
        For example, if i accidentally open an article in a tab and notice “Ruby..whatever” in the blurb, i just close it.

        Id like to swiftly do the same with stuff like node.js, without reading any copy.

    • http://mlarocca.github.io Marcello La Rocca

      Hi Mats,
      sorry for the misunderstanding.
      You are right, we could and maybe should have been more precise in the title, or at least in the introduction to the article. but, I have got to say, perhaps a server side on a JavaScript-only project kind of should have raised some suspicions that NodeJs was involved. :D

      Anyway, in the second part we will deal with the client, so we go back to vanilla-browser-JS (well, not just vanilla-JS, we will use some frameworks to make our life easier).
      If you don’t like Node but you’d still like to try the client side, and in order to try to make up for your frustration, I could share a Python version of the same server (well, at least for Linux/Ubuntu). Please feel free to let me know if you’d interested.

      Thanks!

      • M S i N Lund

        Thanks.

        • http://mlarocca.github.io Marcello La Rocca

          I’ll push a branch with the Python server tomorrow, I’ll link it in the other article, as it will be more relevant to that part, as an alternative way to test client code only.

          Cheers

  • Christian Heilmann

    Why would I ever want to know the battery status of the server? And what’s wrong with navigator.battery? http://caniuse.com/#search=battery

    • http://mlarocca.github.io Marcello La Rocca

      Hi Christian, thanks a lot for your feedback!
      Let me start with saying that there is nothing wrong with `navigator.battery` :D
      I can see at least three reason, though, for this client/server approach:

      1) Currently in-browser check for battery is very limited:
      On my Chrome/Ubuntu, navigator.getBattery() just returns Promise {[[PromiseStatus]]: “pending”, [[PromiseValue]]: undefined}
      and navigator.battery return undefined
      Also support is not 100% cross browser/device, while if you decouple them you should’t have problems.

      2) You can extend the dashboard presented in these articles with more features, that still aren’t supported by browsers’ API

      3) It’s fun and a good way to learn JS and NodeJs.

      As for your first question, I had more laptops on my mind when I thought of this: just creating a dashboard for your laptop. I can even give you more context about why I initially did think so, but maybe we could continue that conversation in private, if you’d like to.
      Anyway, I guess also point #3 above kind of trumps it all: even if someone doesn’t actually need it but you wants to learn JS, might be a good idea for them to follow the article (I hope so at least) and above all to experiment!

      • dark4p

        Hi Marcello,

        Running this line of code `navigator.getBattery().then(function (d) { console.log(d); })` in Ubuntu on Chrome returned an object but with wrong properties (dischargingTime: Infinity for example) but running `navigator.battery` in Firefox did the trick.

        It seems it’s a working draft: https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getBattery

        Awesome tutorial btw.

        • http://mlarocca.github.io Marcello La Rocca

          Thanks! :)
          It does indeed, it works very nicely on Firefox, and I’m rather surprised that in Chrome getBattery is giving some problems.

          Trying to check on that promise it returns:

          navigator.getBattery().then(function(battery) {
          console.log(battery);
          });

          but it looks like it never gets filled – I’ll try it on a different machine, tomorrow, just out of curiosity

        • Aurelio De Rosa

          “dischargingTime: Infinity” is a correct value if the battery is charging. It means: “Hey dude, your battery will never be fully discharged since you’re using the power outlet”.

          • dark4p

            dischargingTime: Infinity would be correct if the battery was charging at the time.. but it wasn’t. Tried on several laptops running Ubuntu and Windows with same results. Did you try running it on laptops/desktop PCs?

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in JavaScript, once a week, for free.