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

Share this article

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.

Marcello La RoccaMarcello La Rocca
View Author

I'm a full stack engineer with a passion for Algorithms and Machine Learning, and a soft spot for Python and JavaScript. I love coding as much as learning, and I enjoy trying new languages and patterns.

AurelioDbattery vizclijavascriptlinuxmac os xnodeNode-JS-Tutorialsnode.jsnodejswindows
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week