This post is contributed by Charlie Key, CEO and Co-Founder of Structure. Structure is an Internet of Things platform that makes it simple to build connected experiences and solutions. Charlie has been working with Node.js for a number of years and is now using it to power the world of IoT.

The world of JavaScript continues to drive into new areas. Technologies like Node.js have allowed for rapid expansion on the server side, and now into the world of the Internet of Things. Node.js can now be ran on a variety of embedded devices, like the Intel Edison. Communicating with embedded devices has always been possible but now with Node.js and protocols like MQTT it's easier than ever.

In this post, we'll take a look at how to take advantage of these two technologies (Node.js and MQTT) to send simple messages and build a simple garage opener application. This is only one of the possibilities for this type of communication.


Need help with enterprise-grade Node.js Development?
Hire the experts of RisingStack!

MQTT itself is a very simple publish / subscribe protocol. It allows you to send messages on a topic (you can think of these as channels) passed through a centralized message broker. The whole protocol is very lightweight on purpose. This makes it easy to run on embedded devices. Nearly every microcontroller has a library available for it to send and receive MQTT messages. Below you can see the basics of MQTT communication.

MQTT Communication Diagram with Node.js Apps

Now, let's imagine we want to build a remote garage opener using MQTT now. The first thing we need to do is plan what messages we need to send between our garage door and the remote controller. To keep this example simple, we're going to say that we simply need to be able to open and close the door. The architecture for this in real life might look like:

Garage Opener Diagram with MQTT and Node.js

The door itself can be in several states. These are 'open', 'closed', 'opening', and 'closing'. A real door might have another state for 'paused' but we're not worried about that today.

Our application today will be broken into two files, one for the garage and one for the controller. I'll include the file name at the top of any code snippets. To get things started, we need to include the mqtt npm library and set the broker we'll be using. There are many open brokers to use for testing; I'm going to use broker.hivemq.com. Again, this is only for testing - don't run a production workload through it. This should be at the top of both files.

// contoller.js and garage.js
const mqtt = require('mqtt')
const client = mqtt.connect('mqtt://broker.hivemq.com')

Next up, we'll add some code to connect to the broker. Once connected we'll create a topic (channel) that will be used to communicate whether the garage door is connected to the system. On the door side, this is publishing a message to a topic and on the controller side it's subscribing to the topic. Also, at this time, we'll add a local variable to keep track of the current state of the garage door. You'll notice that we're prefixing of our topics with 'garage/'. This is simply for organizational purposes; you could name these whatever you'd like.

// garage.js
const mqtt = require('mqtt')
const client = mqtt.connect('mqtt://broker.hivemq.com')

/**
* The state of the garage, defaults to closed
* Possible states : closed, opening, open, closing
*/

var state = 'closed'

client.on('connect', () => {
  // Inform controllers that garage is connected
  client.publish('garage/connected', 'true')
})

On the controller side, we not only need to subscribe to the topic, we need to add a message listener to take action when a message is published. Once the message is received, we check the value and keep track to whether the door is connected to the overall system using a variable.

// controller.js
const mqtt = require('mqtt')
const client = mqtt.connect('mqtt://broker.hivemq.com')

var garageState = ''
var connected = false

client.on('connect', () => {
  client.subscribe('garage/connected')
})

client.on('message', (topic, message) => {
  if(topic === 'garage/connected') {
    connected = (message.toString() === 'true');
  }
})

So far the door and controller only know if the door is connected to the system. We can't take any action just yet. To make sure that the controller in the system knows going on with the door let's add a function to send the current door state. This function looks like the following:

// added to end of garage.js
function sendStateUpdate () {
  console.log('sending state %s', state)
  client.publish('garage/state', state)
}

To make use of this function, we'll add to our initial garage connect call.

// updated garage.js connect
client.on('connect', () => {
  // Inform controllers that garage is connected
  client.publish('garage/connected', 'true')
  sendStateUpdate()
})

Now that the garage door can update tell everyone it's current state, the controller needs to update it's garageState variable. However, at this time let's update the message handler to call separate functions for the different incoming topics. This will improve our code structure a bit. The fully updated file follows.

// updated controller.js
const mqtt = require('mqtt')
const client = mqtt.connect('mqtt://broker.hivemq.com')

var garageState = ''
var connected = false

client.on('connect', () => {
  client.subscribe('garage/connected')
  client.subscribe('garage/state')
})

client.on('message', (topic, message) => {
  switch (topic) {
    case 'garage/connected':
      return handleGarageConnected(message)
    case 'garage/state':
      return handleGarageState(message)
  }
  console.log('No handler for topic %s', topic)
})

function handleGarageConnected (message) {
  console.log('garage connected status %s', message)
  connected = (message.toString() === 'true')
}

function handleGarageState (message) {
  garageState = message
  console.log('garage state update to %s', message)
}

At this point, our controller can keep up to date with the garage door state and connection status. It's probably a good time to start adding some functionality to control our door. The first thing we'll do is have the garage start listening for messages telling it to open and close. This again updates the connect call for the garage.

// updated garage.js connect call
client.on('connect', () => {
  client.subscribe('garage/open')
  client.subscribe('garage/close')

  // Inform controllers that garage is connected
  client.publish('garage/connected', 'true')
  sendStateUpdate()
})

We now need to add a message listener to our garage door.

// added to garage.js
client.on('message', (topic, message) => {
  console.log('received message %s %s', topic, message)
})

For the controller, we'll add the ability to send an open message or close message. These are two simple functions. In a real application these two functions would be called from outside input (a web application, mobile app, etc...). For this example, we'll call them with a timer just to test the system. The additional code for this follows.

// added to controller.js
function openGarageDoor () {
  // can only open door if we're connected to mqtt and door isn't already open
  if (connected && garageState !== 'open') {
    // Ask the door to open
    client.publish('garage/open', 'true')
  }
}

function closeGarageDoor () {
  // can only close door if we're connected to mqtt and door isn't already closed
  if (connected && garageState !== 'closed') {
    // Ask the door to close
    client.publish('garage/close', 'true')
  }
}

//--- For Demo Purposes Only ----//

// simulate opening garage door
setTimeout(() => {
  console.log('open door')
  openGarageDoor()
}, 5000)

// simulate closing garage door
setTimeout(() => {
  console.log('close door')
  closeGarageDoor()
}, 20000)

The code above includes the open and close functions. They make sure that the garage is actually connected to the system and not already in the requested state. That gives us the final code for our controller which can be viewed below.

// controller.js
const mqtt = require('mqtt')
const client = mqtt.connect('mqtt://broker.hivemq.com')

var garageState = ''
var connected = false

client.on('connect', () => {
  client.subscribe('garage/connected')
  client.subscribe('garage/state')
})

client.on('message', (topic, message) => {
  switch (topic) {
    case 'garage/connected':
      return handleGarageConnected(message)
    case 'garage/state':
      return handleGarageState(message)
  }
  console.log('No handler for topic %s', topic)
})

function handleGarageConnected (message) {
  console.log('garage connected status %s', message)
  connected = (message.toString() === 'true')
}

function handleGarageState (message) {
  garageState = message
  console.log('garage state update to %s', message)
}

function openGarageDoor () {
  // can only open door if we're connected to mqtt and door isn't already open
  if (connected && garageState !== 'open') {
    // Ask the door to open
    client.publish('garage/open', 'true')
  }
}

function closeGarageDoor () {
  // can only close door if we're connected to mqtt and door isn't already closed
  if (connected && garageState !== 'closed') {
    // Ask the door to close
    client.publish('garage/close', 'true')
  }
}

// --- For Demo Purposes Only ----//

// simulate opening garage door
setTimeout(() => {
  console.log('open door')
  openGarageDoor()
}, 5000)

// simulate closing garage door
setTimeout(() => {
  console.log('close door')
  closeGarageDoor()
}, 20000)

Now the garage door must do something with these messages. Again, we'll use a switch statement to route the different topics. Once the message has been received the door will attempt to handle it by checking to make sure it can go to that state. It will then go to the transitional state (opening, closing), send an update message, and finally go to resting state (open, closed). For testing purposes, this last part is done on a timer. In reality, the system would be waiting for the hardware to signal it's completed.

// updated garage.js message handler
client.on('message', (topic, message) => {
  console.log('received message %s %s', topic, message)
  switch (topic) {
    case 'garage/open':
      return handleOpenRequest(message)
    case 'garage/close':
      return handleCloseRequest(message)
  }
})

The open and close request handlers can be added to the end of the file.

// added to garage.js
function handleOpenRequest (message) {
  if (state !== 'open' && state !== 'opening') {
    console.log('opening garage door')
    state = 'opening'
    sendStateUpdate()

    // simulate door open after 5 seconds (would be listening to hardware)
    setTimeout(() => {
      state = 'open'
      sendStateUpdate()
    }, 5000)
  }
}

function handleCloseRequest (message) {
  if (state !== 'closed' && state !== 'closing') {
    state = 'closing'
    sendStateUpdate()

    // simulate door closed after 5 seconds (would be listening to hardware)
    setTimeout(() => {
      state = 'closed'
      sendStateUpdate()
    }, 5000)
  }
}

With these functions, we now have a fully functioning garage system. To test you should start the controller then immediately after the garage door. The controller will send an open command after 5 seconds of starting and a close command after 20 seconds.

The final thing I'd recommend is to make our garage door update it's connected status when the application closes for any reason. This exit cleanup code is based on a stackoverflow answer and modified to send a mqtt message. This can be dropped at the end of the garage file. All of this combined leaves us with the final garage file.

// garage.js
const mqtt = require('mqtt')
const client = mqtt.connect('mqtt://broker.hivemq.com')

/**
 * The state of the garage, defaults to closed
 * Possible states : closed, opening, open, closing
 */
var state = 'closed'

client.on('connect', () => {
  client.subscribe('garage/open')
  client.subscribe('garage/close')

  // Inform controllers that garage is connected
  client.publish('garage/connected', 'true')
  sendStateUpdate()
})

client.on('message', (topic, message) => {
  console.log('received message %s %s', topic, message)
  switch (topic) {
    case 'garage/open':
      return handleOpenRequest(message)
    case 'garage/close':
      return handleCloseRequest(message)
  }
})

function sendStateUpdate () {
  console.log('sending state %s', state)
  client.publish('garage/state', state)
}

function handleOpenRequest (message) {
  if (state !== 'open' && state !== 'opening') {
    console.log('opening garage door')
    state = 'opening'
    sendStateUpdate()

    // simulate door open after 5 seconds (would be listening to hardware)
    setTimeout(() => {
      state = 'open'
      sendStateUpdate()
    }, 5000)
  }
}

function handleCloseRequest (message) {
  if (state !== 'closed' && state !== 'closing') {
    state = 'closing'
    sendStateUpdate()

    // simulate door closed after 5 seconds (would be listening to hardware)
    setTimeout(() => {
      state = 'closed'
      sendStateUpdate()
    }, 5000)
  }
}

/**
 * Want to notify controller that garage is disconnected before shutting down
 */
function handleAppExit (options, err) {
  if (err) {
    console.log(err.stack)
  }

  if (options.cleanup) {
    client.publish('garage/connected', 'false')
  }

  if (options.exit) {
    process.exit()
  }
}

/**
 * Handle the different ways an application can shutdown
 */
process.on('exit', handleAppExit.bind(null, {
  cleanup: true
}))
process.on('SIGINT', handleAppExit.bind(null, {
  exit: true
}))
process.on('uncaughtException', handleAppExit.bind(null, {
  exit: true
}))

With that, we're finished with our garage door controller. I challenge you to take this to the next level. A few modifications and an Intel Edison would let you setup a full remote garage opener. The full source code for this example is available on Github as well.

That'll do it for this initial tutorial. There are additional options and capabilities in MQTT including SSL and username / password authentication to beef up security.

If you liked this post and are interested in where Node.js is going there's a fantastic conference coming up, Node Community Convention. There will be great talks on topics including the Internet of Things, scaling systems, and more.