JavaScript
Article

Remote Control Your Mac With Node.js and Arduino

By Patrick Catanzariti

The combination of Arduinos and Node.js allows us to do a lot of unexpected things. In this article, I’ll show how you can create a remote control for your Mac via Arduinos, Node.js and AppleScript.

If you are new to combining Arduinos and Node.js, I’ve previously covered turning on LED lights and displaying web API data on LCD text displays.

Our Arduino remote control will increase and decrease our Mac’s volume, tell our Mac to play an iTunes playlist of our choosing and set it to stop whatever is playing on iTunes (which is likely to be that playlist!).

Keep in mind, this demo provides access to commands directly on your Mac – there is the potential for this to be misused or harmful if you provide too much access! Keep it for personal use rather than big corporate projects.

Setting Up Our Arduino

Ensure that you’ve got the StandardFirmata sketch installed on your Arduino board itself, as we’ll be using the johnny-five library to send instructions to our Arduino. That will only work if you’ve got StandardFirmata on there first:

Installing Standard Firmata on an Arduino

Our Arduino breadboard set up for this demo looks like so:

Sketch for our Arduino Mac Remote Control

Our Server Code

Our Node.js server code is relatively short and sweet for this demo:

var five = require('johnny-five'),
      board = new five.Board(),
      exec = require('child_process').exec,
      btn1, btn2, btn3, btn4, btn5,
      currentVolLevels = {};

  board.on('ready', function() {
    console.log('Arduino board is ready!');

    btn1 = new five.Button(7);
    btn2 = new five.Button(6);
    btn3 = new five.Button(5);
    btn4 = new five.Button(4);
    btn5 = new five.Button(3);

    btn1.on('down', function(value) {
      askiTunes('play playlist \"Top 25 Most Played\"');
    });

    btn2.on('down', function(value) {
      askiTunes('stop');
    });

    btn3.on('down', function(value) {
      setVolumeLevel(currentVolLevels['output volume'] + 5);
    });

    btn4.on('down', function(value) {
      setVolumeLevel(currentVolLevels['output volume'] - 5);
    });

    btn5.on('down', function(value) {
      toggleMute();
    });

    getVolumeLevels();
  });

  function getVolumeLevels() {
    exec("osascript -e 'get volume settings'", function(err, stdout, stderr) {
      if (!err) {
        var levels = stdout.split(', ');

        levels.forEach(function(val,ind) {
          var vals = val.split(':');

          if (vals[1].indexOf('true') > -1) currentVolLevels[vals[0]] = true;
          else if (vals[1].indexOf('false') > -1) currentVolLevels[vals[0]] = false;
          else currentVolLevels[vals[0]] = parseInt(vals[1]);
        });
        console.log(currentVolLevels);
      }
    });
  }

  function setVolumeLevel(level) {
    console.log('Setting volume level to ' + level);
    exec("osascript -e 'set volume output volume " + level + "'",
      function() {
        getVolumeLevels();
      });
  }

  function toggleMute() {
    var muteRequest = currentVolLevels['output muted'] ? 'without' : 'with';
    console.log('Toggling mute to ' + muteRequest + ' muted');
    
    exec("osascript -e 'set volume " + muteRequest + " output muted'", function() {
      getVolumeLevels();
    });
  }

  function askiTunes(event, callback) {
    exec("osascript -e 'tell application \"iTunes\" to "+event+"'", function(err, stdout, stderr) {
      console.log('iTunes was just asked to ' + event + '.');
    });
  }

That Code Explained

Now the all important part of the article – what all of that code means! Lets go over how everything fits together.

In order to interface with our Arduino board, we are using johnny-five. We start by setting up our johnny-five module and our Arduino board through that. Then we define variables to store our five buttons.

var five = require('johnny-five'),
      board = new five.Board(),
      btn1, btn2, btn3, btn4, btn5,

We also set up our exec() function which is what allows us to run AppleScript commands from Node.js.

exec = require('child_process').exec,

When johnny-five lets us know our board is ready to use, we run a quick console.log and define our five buttons and the Arduino pins they are connected to (7, 6, 5, 4 and 3).

board.on('ready', function() {
    console.log('Arduino board is ready!');

    btn1 = new five.Button(7);
    btn2 = new five.Button(6);
    btn3 = new five.Button(5);
    btn4 = new five.Button(4);
    btn5 = new five.Button(3);

On each button’s down event, we run a different function. On our first button, we run the askiTunes() function which sends iTunes a request. In our case, it is requesting our “Top 25 Most Played” playlist.

btn1.on('down', function(value) {
    askiTunes('play playlist \"Top 25 Most Played\"');
  });

The askiTunes() function executes our first bit of AppleScript using the exec() function. All of our AppleScript commands run within Node.js using the command osascript.

Our askiTunes() function runs the command osascript -e 'tell application \"iTunes\" to "+event+"'. This gives us a generic command telling iTunes to do something. We can adjust what that something is via the event variable.

When done, we run a console.log just so we know that the event has been recognised.

function askiTunes(event, callback) {
    exec("osascript -e 'tell application \"iTunes\" to "+event+"'", function(err, stdout, stderr) {
      console.log('iTunes was just asked to ' + event + '.');
    });
  }

Our second button runs the same askiTunes() function but we pass it the event of stop to stop anything that is currently playing.

btn2.on('down', function(value) {
    askiTunes('stop');
  });

If we had more buttons to play with, we could add buttons to pause and a generic play event that’ll resume what is currently in queue.

Our third and fourth buttons turn our Mac’s volume up and down via a function we’ll call setVolumeLevel().

btn3.on('down', function(value) {
    setVolumeLevel(currentVolLevels['output volume'] + 5);
  });

  btn4.on('down', function(value) {
    setVolumeLevel(currentVolLevels['output volume'] - 5);
  });

setVolumeLevel() uses an object we define at the start of our code called currentVolLevels. This object stores the four different values which AppleScript returns from our Mac. A sample of this data looks like so:

{
  'output volume': 5,
  'input volume': 83,
  'alert volume': 100,
  'output muted': false
}

As you can see, we’ve got a value in that JSON object called 'output volume'. We add five to the volume level on our third button (increasing it) and reduce it by five on our fourth button (decreasing it), then we pass that value into the function to make the change happen.

Our setVolumeLevel() function uses the AppleScript command of set volume output volume to change our Mac’s volume to the level we’ve passed it. We also run a console log just so we can keep track of the volume level requests.

function setVolumeLevel(level) {
    console.log('Setting volume level to ' + level);
    exec("osascript -e 'set volume output volume " + level + "'", function() {
      getVolumeLevels();
    });
  }

When our AppleScript code has been run, we call getVolumeLevels() which is our function that sets up all of our currentVolLevels values and keeps track of our Mac’s volume. I’ll explain it in detail after we’ve covered our final button.

That aforementioned final button runs the toggleMute() function that’ll mute and unmute our Mac.

btn5.on('down', function(value) {
    toggleMute();
  });

Our toggleMute() function looks at the currentVolLevels['output muted'] and uses either osascript -e 'set volume without output muted' to turn off muting or osascript -e 'set volume with output muted' to turn it on. If currentVolLevels['output muted'] is true, then we set the keyword to 'without' to take away muting. If it is false, we set the keyword to 'with' to turn on muting.

function toggleMute() {
    var muteRequest = currentVolLevels['output muted'] ? 'without' : 'with';
    console.log('Toggling mute to ' + muteRequest + ' muted');
    
    exec("osascript -e 'set volume " + muteRequest + " output muted'", function() {
      getVolumeLevels();
    });
  }

This AppleScript call also runs the getVolumeLevels() function once it is finished. In this function, we run osascript -e 'get volume settings' to retrieve the current volume of our Mac. It returns these values in the format:

"output volume:5, input volume:83, alert volume:100, output muted:false"

Within our getVolumeLevels() we take the value returned within the stdout variable and format it into a JSON object stored in currentVolLevels using code that looks like this:

function getVolumeLevels() {
    exec("osascript -e 'get volume settings'", function(err, stdout, stderr) {
      if (!err) {
        var levels = stdout.split(', ');

        levels.forEach(function(val,ind) {
          var vals = val.split(':');

          if (vals[1].indexOf('true') > -1) currentVolLevels[vals[0]] = true;
          else if (vals[1].indexOf('false') > -1) currentVolLevels[vals[0]] = false;
          else currentVolLevels[vals[0]] = parseInt(vals[1]);
        });
        console.log(currentVolLevels);
      }
    });
  }

The JSON conversion is tailored specifically to the string we receive above. First, we split each key/value pair into an array called levels by splitting it between each comma to create an array like so:

['output volume:5', 'input volume:83', 'alert volume:100', 'output muted:false']

We then iterate through each string in the array, rearranging it neatly into our currentVolLevels JSON object. To do this, we split each key/value pair into an array called vals using the : character as our splitter. vals[0] will be each key such as output volume, whilst vals[1] contains the actual volume level values. We use vals[0] as our JSON object key, e.g currentVolLevels[vals[0]] = something.

There is one factor we need to keep in mind and account for in the volume levels that get returned. One of these values is a true/false value (our muted/unmuted status) whilst the rest are numbers. All of these are represented as strings and need to be converted. We’ll do this via a simple if statement that looks at the value of vals[1]. We check for the string of "true" and the string of "false". If we find either of these, we set the relevant value inside currentVolLevels to be the matching boolean. If it is neither of these, we parse the string into an integer that will represent a numeric volume level and store it inside currentVolLevels.

The end result looks like so:

{
  'output volume': 5,
  'input volume': 83,
  'alert volume': 100,
  'output muted': false
}

Our Package.json File

Our package.json file is rather simple in this case and mainly needs to ensure we’ve got the johnny-five and serialport npm modules installed.

{
    "name": "nodemaccontroller",
    "version": "1.0.0",
    "description": "Code to control your Mac via Node",
    "main": "index.js",
    "dependencies": {
      "johnny-five": "^0.8.76",
      "serialport": "^1.7.1"
    },
    "author": "Patrick Catanzariti"
  }

Our Remote Control In Action

Install all of the above dependencies using npm install, ensure your Arduino is connected and running the StandardFirmata sketch, then run node index.js. Upon running it, press a few buttons and you should be able to control your Mac! Whilst it’s running, it’ll look like this in the console:

Our Arduino Remote Control In Action

Our Arduino Remote Control In Action

Other Possibilities

If you’re not a big music fan or you don’t use your Mac for your music, there are a bunch of other AppleScript shortcuts you could hook up your Node.js server to. Here are a few ideas.

Launch Applications

function openCalculator() {
    exec("osascript -e 'tell application \"Calculator\" to launch'");
  }

Open a New Finder Window

function openFinderWindow() {
    exec("osascript -e 'tell app \"Finder\" to make new Finder window'");
  }

Make Your Mac Speak!

function tellMacToSpeak() {
    exec("osascript -e 'say \"I am completely operational, and all my circuits are functioning perfectly.\"'");
  }

Conclusion

You’ve now got a neat way of making your own personal Mac peripheral! If you wanted to make it work as a portable device, you could set up either websockets, socket.io or a basic HTTP server, give your Arduino Wi-Fi or some other way of accessing your Node.js server remotely (or use something like a Particle Photon or Particle Electron microcontroller, and then make these exec() calls based upon remote commands (please be careful though, exec() can be misused!). There’s plenty of opportunity for some very fun projects here! As always, if you do make something neat, leave a note in the comments or get in touch with me on Twitter (@thatpatrickguy), I’d love to check it out!

  • Dr Evil

    All this and more can be done without Node.js, Firmata or Processing. Just a Mac, Applescript, Arduino Micro Pro and any sensor you wish to use for control. For a demo of what I do with a Mac and Arduino, see https://vimeo.com/125117983.

  • Patrick Catanzariti

    That is fantastic! Nice work. Thanks for sharing :) I used JavaScript as I’d adapted this idea from another demo I had done with tech that required the JS interface. Quite neat to see it done sans-JS too!

    • Dr Evil

      Thanks. Sorry, I can’t comment on your .js work as I have not had the need to learn it. Still, I’m sure it must be brilliant.

      I’m currently expanding my home system with a network of motion sensors and light intensity meters inside the house. This network allows our lights to turn on/off without the need to touch switches or remotes. Even my Mac snaps awake and the password automatically keyed in whenever I sit in front of it and goes back to sleep along with the desk lamp after a set time of inaction. Our home alarm system is armed/disarmed this way by the network in conjunction with the iPhone’s Bluetooth. Of course, I can also turn all of this automatic behavior on/off. As I told my wife, switches and remotes are so last century. ;)

  • stephsmith

    Is there a picture of what the Arduino breadboard should look like?

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