Contents


Secure your environment with smart locks, Part 2

Build a smart lock for a connected environment

Connect a NodeMCU board to an electric lock, and use a simple cloud-based IoT app to open or close the lock

Comments

Content series:

This content is part # of 2 in the series: Secure your environment with smart locks, Part 2

Stay tuned for additional content in this series.

This content is part of the series:Secure your environment with smart locks, Part 2

Stay tuned for additional content in this series.

In my previous article, you learned how to build a smart lock for use in a disconnected environment, one where the lock itself cannot communicate with the internet and users needed to authenticate by using one-time passwords to open the smart lock.

In this article, I show you how to use the same hardware for a lock in a connected environment, one where a central server can determine when people are authorized to open the lock. You'll need to follow the steps in my previous article to set up the NodeMCU board, configure the board, and build the circuit.

What you’ll need to build this smart lock

  • The hardware from my previous tutorial: an electrically controlled lock, a 9-volt battery, a connector for the battery, and a digital relay. You'll also need the NodeMCU development board, a breadboard to make the circuit, and a few wires to connect everything together.
  • Set up the NodeMCU board as described in my previous tutorial. When you build the firmware for the board, choose the following modules: crypto, file, GPIO, HTTP, net, node, SJSON, timer, and WiFi. Because this smart lock is connected to the internet, make sure to include TLS/SSL support. Once again, you can get the integer version. Finally, flash the NodeMCU with the firmware that you build.
  • An account on IBM Bluemix, the IBM Cloud Platform.

Get the codeRun the app

1

Create a simple IBM Cloud Functions action that makes the smart lock open or closed

IBM Cloud Functions (based on Apache OpenWhisk) is a Function-as-a-Service (FaaS) platform. It allows you to run services on the internet that are constantly available at a very low cost (because instead of the resource requirements for an always running application, functions are only active when they are needed and for a few minutes afterward).

In this application, IBM Cloud Functions provides locks with the decision whether they need to be open or closed. The first step is to write an extremely simple action that randomly makes the lock open or closed.

  1. Go to the Bluemix console.
  2. From the hamburger menu, select Functions.
  3. Click Start Creating.
  4. Click Develop in the left sidebar.
  5. Click Create an Action.
  6. Name the action lock_query, leave the default execution runtime (Node.js), and click Create Action.
  7. Replace the main function with this code:
    	function main(params) {
    		return { open : Math.random()
            > 0.5 };
    	}
  8. Click Run this Action.
    Note: If you do not see the Run this Action button, widen your browser window.
  9. Click Run with this Value. If you modified an existing action, you need to click Make It Live first. Note that the input does not matter, you can put any JSON structure there.
  10. When the invocation console opens, click Run Again several times to see that the result varies.
  11. Click Close to leave the invocation console.
2

Make the action externally available as an API

You can already reach the action from outside of the IBM Cloud (in the Develop view, click View REST Endpoint, and scroll down to the cURL example, click Show Full Example), but it is simpler and more flexible to have the action as part of an external-facing API.

  1. Click APIs on the left sidebar (assuming you are still in IBM Cloud Functions).
  2. Click Create Managed API.
  3. Name the API SmartLock, and specify the base path as /smartlock.
  4. Create Create operation, and create an operation with these parameters:
    ParameterValue
    Path /lock_query
    Verb GET
    Package containing action Default
    Action lock_query
    Response content type application/json
  5. Click Save. Then, scroll down and click Save & expose.
  6. Click the icon to copy the route:
  7. Paste the route in a browser window as the URL, and type /lock_query at the end of the URL. Reload a few times to see that you get both responses.
3

Write a Lua program to lock or unlock the smart lock

The value that is returned by the action (open or locked) is meaningless unless the lock's NodeMCU retrieves it and acts accordingly. I've written a Lua program that does just that.

  1. Run esplorer.bat, which you installed in the previous article.
  2. Copy this code, and paste it into the left text area on ESPlorer.
  3. Modify the parameters at the top of the Lua program: SSID, wifi password, and the API's URL.
  4. Click the Send to ESP button.

Let's look at this Lua code that you run for the lock. As you can see, the configuration parameters are at the top to make them easy to change.

-- Configuration parameters
-- change these values for your environment
ssid =
        "<<redacted>>"
wifi_pwd =
        "<<redacted>>"
openwhisk_url =
        "https://service.us.apiconnect.ibmcloud.com/gws/apigateway/api/ec74d9ee76d47d2a5f9c4dbae2510b0b8ae5912b542df3e2d6c8308843e70d59/smartlock/lock_query"
lock_pin = 2   -- The GPIO pin connected to the lock
wait_time = 1  -- How long to wait until 
			         -- we ask OpenWhisk again

I couldn't get the NodeMCU http.get function to work with HTTPS, so I wrote my own. This function has two parameters – the URL to retrieve, and a callback function to run after it is retrieved.

-- Get an HTTPS response. 
-- According to the docs, http.get should support https
-- URLs. However, I couldn't get that working.

function getHttpsResponse(url, cb)

Lua functions can have multiple return values. The string.match function returns one value for each parenthesis it has in the pattern. In this case, the first parenthesis is the host name, which is terminated by a slash. The second is the path, including any query string.

  host, path = string.match(url, "https://([^/]+)/(.+)")

This connection needs to be a TLS connection, not a normal TCP one.

  conn = tls.createConnection()

The communication between IBM Cloud Functions and the NodeMCU is protected by HTTPS. Normally, that is sufficient to be able to trust the server's identity. However, because the NodeMCU is so resource constrained, it does not have CA certificates, which means that if hackers could control the DNS server, they could redirect the server (service.us.apiconnect.ibmcloud.com) to their own server and open the lock.

NodeMCU has a solution to this problem. The tls.cert.verify() function allows you to store a CA certificate and require certificates to be signed by that CA. To get the certificate, you can go to https://service.us.apiconnect.ibmcloud.com in a browser (ignore the 404 error). The procedure to get the file from this site varies between browsers.

If you do not want to advertise the fact that a certain lock is open or closed, you can include a shared secret as an authentication string in the request the lock sends to the IBM Cloud Functions action.

After we get connected, send the request. Note the use of [[<string>]] for strings that contain newlines, it is clearer syntax than "\r\n". Unfortunately, this syntax does not recognize empty lines – and the HTTP protocol requires such lines.

  -- Don't send the request before we are connected
  conn:on("connection", function(sck, c)
    req = "GET /" .. path .. [[ HTTP/1.1
      Host: ]] .. host .. "\r\n" .. [[ 
      Connection: close
      Accept: */*]] .. "\r\n\r\n"      
    sck:send(req)
  end)  -- of conn:on("connection") callback

When an answer is received, use string.match to ignore the header fields (everything before the two newlines) and keep the actual response. This code assumes that the response will fit within a single packet, which is reasonable considering the length of the response from the action.

  conn:on("receive", function(sck, c) 
    resp = string.match(c, ".+\r\n\r\n(.+)")

Use the sjson package to decode the JSON structure received from the action.

    decoder = sjson.decoder({})
    decoder:write(resp)

Call the callback function with the result.

    cb(decoder:result())
  end)   -- coon:on("receive") callback

By this point we have added the two event handlers we need (for connecting and for receiving a response). Now, we can actually connect to the server.

  conn:connect(443,host);  
end   -- of getHttpsResponse

This function calls the action on IBM Cloud Functions, uses the response, and then sets a timer to run again.

-- Call the action, and open or close the lock 
-- based on the response

function openOrCloseLock()
  getHttpsResponse(openwhisk_url,

The callback function does most of the work.

      function(t)
        print(t.open)
        if (t.open)
        then
          gpio.write(lock_pin, 1)
        else
          gpio.write(lock_pin, 0)
        end   -- if then else

        -- Call again in wait_time seconds
        tmr.create():alarm(wait_time*1000, 
            tmr.ALARM_SINGLE,
        openOrCloseLock)
      end)   -- getHttpsResponce callback
end -- openOrCloseLock

Note that this approach, checking the status of the lock on the server every few seconds, is inefficient. The extra processing required on the NodeMCU is practically free (the NodeMCU is not doing anything anyway), but the network bandwidth and server-side processing are not free. It would be more efficient to have the locks ask about their status only when a user asks them to open. You can implement this feature by using either a button or a web interface.

To use a button, connect it between one of the unused data pins on the NodeMCU (D0, D1, D3, and so on) and "ground". Configure it to produce an interrupt that calls openOrCloseLock:

pin = 1
gpio.mode(pin, gpio.INT, gpio.PULLUP)

gpio.trig(pin, "down", 
    function(level, time)
        openOrCloseLock()
    end
)

To use a web interface, configure the access point that provides the smart lock with its IP address to give it the same one each time and put an HTTP server that users can use from their smartphones. They need to go to a URL (in the case of the code below, any URL on that server). The exact path and the response don't matter.

httpServer = net.createServer(net.TCP)
httpServer:listen(80, function(conn) 
   conn:on("receive", function(conn, payload)
      print(payload)
      conn:send("Querying the server about the lock")
      openOrCloseLock()
   end)  -- of the conn:on function
end)   -- of the httpServer:listen function

If the lock is open, the openOrCloseLock function needs to check periodically if it can be closed again. If it is closed, there is no need to check until a user requests it. Replace the end of the function with this code:

       -- If the lock is open, call again in 
       -- wait_time seconds
        if (t.open) then
            tmr.create():alarm(wait_time*1000, 
                tmr.ALARM_SINGLE, openOrCloseLock)
        end   -- if t.open
      end)   -- getHttpsResponce callback
end -- openOrCloseLock

The internet is not available immediately when the NodeMCU starts. It first needs to associate with an access point, and then get an IP address. The wifi.eventmon.register function lets us run a function when we get that IP address and can start using the internet.

-- There's no point doing anything until we get an 
-- IP address from the access point
wifi.eventmon.register(wifi.eventmon.STA_GOT_IP, function(t)
  openOrCloseLock() 
end)   -- wifi connected function

This code gets executed as soon as the device starts (once the program is written to init.lua). It sets the wifi mode, connects, and then sets the mode for the pin that controls the lock.

-- Actually connect
wifi.setmode(wifi.STATION)
wifi.sta.config({
  ssid = ssid,
  pwd = wifi_pwd
})
      

gpio.mode(lock_pin, gpio.OUTPUT)

If we have smart locks in multiple locations, it is important to distinguish between them to avoid opening the wrong door. IBM Cloud Functions has a simple solution. If we append a query string to the URL going into the API, the action gets the query string values in its input structure.

To add this feature in our program, we modify the URL parameter:

openwhisk_url = "https://service.us.apiconnect.ibmcloud." .. 
   "com/gws/apigateway/api/ec74d9ee76d47d2a5f9c4dbae2510" ..    
   "b0b8ae5912b542df3e2d6c8308843e70d59" ..     
   "/smartlock/lock_query_2?chip=" .. node.chipid()

The only new part of this definition is the node.chipid() call. This call provides a unique chip identifier.

You can see the complete program for the NodeMCU here. Save it as init.lua to have it run automatically after the device is started.

4

Use Cloudant to store the smart lock information

A lock that opens and closes randomly is not very useful. The next step is to have a Cloudant database that stores the locks, their locations, and their statuses.

4a

Create the Cloudant database to store the lock information

  1. In the Bluemix console, from the hamburger menu, click Data & Analytics.
  2. Click Create Data & Analytics service, and select Cloudant NoSQL DB.
  3. Name the service SmartLock-System and click Create.
  4. When the service is created, open it. Then, click Service credentials. Then, click the New credential button.
  5. Name the new credential SmartLockAction, and click Add.
  6. Click View credentials, and click the copy icon to copy the credential into a text file.
  7. In the left sidebar, click Manage. Then, click the LAUNCH button.
  8. In the left sidebar, click the Databases menu.

    Then, click Create Database in the upper right.
  9. Name the database smartlocks.
  10. Do not create any documents. Our action will create them automatically to support registration.
4b

Configure the action to use Cloudant

The Cloudant database is not going to do us any good unless IBM Cloud Functions actually uses it. Either create a new action, or replace the existing query_lock action with this code. Remember to replace the Cloudant credential with your value, and if you create a new action remember to add it to the API and modify the Lua code to access the new action.

At this point, you should be able to open and close the door by changing the Cloudant database (it gets a document for the smart lock the first time the lock connects):

  1. From the hamburger menu for the console, click Data & Analytics.
  2. Click SmartLock-System, and then click LAUNCH. The database opens in a separate tab.
  3. Click the smartlocks database.
  4. Select your lock from the list (it should be the only item).
  5. Change the open value from false to true, and click Save Changes.
  6. See that the lock opens. You can change the value again to close it.

Let's look at this action code that communicates with Cloudant. It starts with the database credentials. I copied the entire credential because it's easier and storage is cheap, but technically speaking you only need the URL field.

var cloudantCred = {
  "username": "<<redacted>>",
.
.
.
  "url":
        "https://<<user name redacted>>:<<password
        redacted>>@4d1cded5-56a3-4ad9-a59a-9c68c192995c-bluemix.cloudant.com"
};

This action is simple enough to be contained within one JavaScript function.

function main(params) {

These lines connect to the smartlocks Cloudant database at the credential URL (which includes the user name and password).

    var cloudant = require("cloudant")(cloudantCred.url);
    var mydb = cloudant.db.use("smartlocks");

Database lookup is an asynchronous process, so the action cannot return the results immediately. When that is the case, it returns a Promise object that specifies what to do. The object constructor has one parameter – the function to run to get the result. This function receives two parameters, one a function to call in the case of success, the other a function to call in the case of failure.

    return new Promise(function(success, failure) {

The first step is to look in the database, with the chip IP as key, to find if there is a document for the lock.

        mydb.get(params.chip, function(err, body) {

If there is no document, this is a first-time registration. In such a case, err.statusCode is 404. However, we cannot assume that err actually exists. If there is no error, it is null.

            // If there is no document, this is a new 
            // smartlock to register
            if (err != null
        && err.statusCode == 404) {

Create the document for the new smart lock, indexed by the chip ID. The default location is unknown, and the default state is for the lock to be closed.

                mydb.insert(
                    {
                      "_id": params.chip, 
                      location: "unknown", 
                      open: false
                    }, 
                    function()
        {

After the smart lock's entry is created, return with its current status (closed).

                        success({open: false});   
                    });   // mydb.insert call

The previous function call is asynchronous. It sends out a request and then adds an entry to a table to call when the response arrives. Then, this function continues, but there is no point to run the rest of it – return lets us finish the function at this point.

                return ;
            }    // end of a new smartlock to register

If the document exists, return the value in it.

            // Return the read value
            success({open: body.open});    
        });    // mydb.get call
    });   // new Promise call
}
5

Build a user interface for the lock administrator to use to lock or unlock the smart lock

The user interface for the locks system lets administrators specify the location for newly registered locks and view and modify the state of existing locks. You can read the source code for it in my Smart Connected Lock GitHub repo.

After you create the action, add it to the API with the path /ui. Make sure to set the response content type to text/html so the browser will process it as a web page.

Let's look at how it works. The main function checks if there is any action to do. If not, it returns a Promise object that calls returnHtml to create the HTML to return to the user.

// If we get here, there is no action to do, 
// just return the HTML

return new Promise(function(success, failure) {
    returnHtml(success);
    });   // new Promise object

If there is an action (open the lock, close the lock, or set the location for a lock), return a Promise object that specifies the new field values and calls modifyEntry. That function modifies the appropriate entry, and then calls returnHtml.

if (params.action == "closeLock") 
    return new Promise(function(success, failure) {
        modifyEntry(params.id, {open: false}, success);
    });

The modifyEntry function first retrieves a copy of the existing entry for the lock from Cloudant.

var modifyEntry = function(id, newVals, success) {
    mydb.get(id, function(err, res) {

Next, the function iterates over the new values and creates or replaces those values in the existing result.

        Object.keys(newVals).forEach(
		function(key) {res[key]=newVals[key];});

We need to keep the time stamp for reasons explained in the next step.

        res.lastChange = Date.now();

We'll insert the modified version back to the database, and then return the HTML, same as if there is no action to perform.

        mydb.insert(res, function(err, body) {
            returnHtml(success);            
        });    // mydb.insert callback
    });   // mydb.get callback
};   // end of modifyEntry

The returnHTML function retrieves the entire Cloudant database.

var returnHtml = function(callback) {
    mydb.list({include_docs:true}, function(err, res) {

The database includes internal information, so we use a map function to keep only the information we need. The map function runs the function that it receives as a parameter on every entry of the list (in this case, res.rows, all the rows in the database) and returns the results in a list. The arrow function (=>) is a shorter notation to define a function.

        var data = res.rows.map((entry) => {
            return {
                id: entry.id,
                location: entry.doc.location,
                open: entry.doc.open
            };    
        });

The next step is to split the lock entries into types: those with a known location (which can be opened or closed from the interface) and those with an unknown location (which has to have a location assigned before they can be manipulated). The program splits the entries by using the filter function. This function returns a list with only those items in the original list for which the parameter function returned true.

        var unknownLoc = data.filter(
          (entry) => {return entry.location == "unknown";});
        var knownLoc = data.filter(
          (entry) => {return entry.location != "unknown";});

Next, we use map again and then reduce to turn the list of entries into the contents of an HTML table. We use a template literal for the HTML, so ${<expression>} is evaluated and put in that location in the string. For example, ${entry.location} is replaced by the location field in the current entry.

        var knownLocRows = knownLoc.map((entry) => {
           return `<tr>
                        <td>
                            ${entry.id}
                        </td>
                        <td>
                            ${entry.location}
                        </td>
                        <td>
${entry.open ? "Open" : "Locked"}
<button class="btn ${entry.open ? 
        "btn-danger" : "btn-success"}" type="button"
                                
onClick="window.location.href='ui?id=${entry.id}&action=${
entry.open ? "close" : "open"}Lock'">

${entry.open ? "Lock" : "Unlock"}
                            </button>
                        </td>
                    </tr>`;
        });            
          
        var knownLocTable = "";
        if (knownLocRows.length > 0)  
            knownLocTable = 
               knownLocRows.reduce((a,b) => {return a+b;});

Finally, the table content is embedded in HTML tables (one for locks with an unknown location, one for all the others). To be able to use the content of the location field, the HTML uses the Angular library.

Note that for this sample application I did not add authentication, but a real application would require it, probably by using OAuth specified in the API definition.

6

Create an IBM Cloud Functions action that handles automatic relocking

If a door is locked when it needs to be open, somebody can't get the job done. Users will call the lock administrator to rectify the situation. But when a door is accidentally left open, it is a silent failure (security issue) that can remain unrectified for a very long time.

To solve this problem, create an action with this code. It looks for locks that are currently open and whose entries were last modified over 5 minutes ago and then closes them. I assume that 5 minutes is enough time to open a door; if not, you can increase this time.

To run this action every 5 minutes, follow these steps:

  1. Open the action in the Develop view and click Automate this Action.
  2. Click the PERIODIC tile and then NEW ALARM.
  3. Click :MM to run the action every few minutes, and type 5. Name the trigger every-five-min and click Create Periodic Trigger.
  4. Click Next and then This Looks Good and Save Rule.
  5. Try to leave a lock open for ten minutes, and see it locks automatically. If you reload the browser page, remember to remove the query (the part of the URL from the question mark to the end).

Most of the code in this action is similar to the code used in the user interface. However, it may be necessary to lock multiple locks in one invocation. We shouldn't run the success function, which reports that we are done, until we get to the callbacks of all the database modifications.

We will use a global variable, leftToLock, with the number of locks left to modify. If gets its value from within the mydb.list callback, which has the size of the list of modifications.

var leftToLock;
… 
function main(params) {
    return new Promise(function(success, failure) {
        mydb.list({include_docs:true}, function(err, res) {
…           
            // We only care about entries that haven't 
            // been changed in the last five minutes
            var now = Date.now();
            data = data.filter((entry) => 
                {return now-entry.lastChange > 5*60*1000;});
              
            // We only care about those entries that
            // have an open lock
            data = data.filter((entry) => 
                 {return entry.open});
              
            // Lock the entries in data.
            leftToLock = data.length;
            data.map((entry) => {lock(entry.id, success)});
        });  // mydb.list
    });    // new Promise
}    // main

Each invocation of the lock function has a callback that decrements leftToLock. When it reaches zero, the final lock function call callback calls the success function.

var lock = function(id, success) {
    mydb.get(id, function(err, res) {
        res.open = false;
        res.lastChange = Date.now();
          
        mydb.insert(res, function(err, body) {
            leftToLock --;
              
            if (leftToLock == 0) {   // We're done
                success({});
            } // leftToLock == 0
        });    // mydb.insert
    });   // mydb.get
};   // end of
        lock

Note that this algorithm fails in the unlikely case that 5 minutes are not enough to modify the Cloudant database for all locks that need to be relocked. But this case is extremely unlikely.

Conclusion

In this article, you learned how to use IBM Cloud Functions to implement a smart lock that is connected to the internet. In addition to this exact use case, you should now be able to:

  1. Use NodeMCU as an HTTPS client, including parsing the server response
  2. Create IBM Cloud Functions
  3. Create APIs to access those IBM Cloud Functions
  4. Use a Cloudant database to store information.
  5. Access and modify information in a Cloudant database from IBM Cloud Functions
  6. Create a user interface as an IBM Cloud Function
  7. Run an IBM Cloud Function periodically to perform various maintenance actions

Hopefully, those skills will serve you in the future as you develop your own IoT products and applications.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Internet of Things, Security
ArticleID=1049860
ArticleTitle=Secure your environment with smart locks, Part 2: Build a smart lock for a connected environment
publish-date=09182017