Code can fight systemic racism. This Black History Month, let's rewrite the wrong. Get involved

Build a smart lock for a disconnected environment

An interesting use case for IoT is bringing a bit of connectivity to a part of the disconnected world. For example, many companies have a main location and several remote locations, with remote locations not necessarily having cellular or internet connectivity. Often, there are rooms in those remote locations that are locked for a variety of security purposes (such as specialized equipment that only certain people know how to maintain).

You can implement a smart lock (an IoT device that is connected to an electric lock) that grants access to these locked rooms in the remote location by using one time passwords. To save money on human interface equipment, the one time password can be entered by a smartphone. The list of one time passwords, and therefore access to the locked room, can be controlled from a central location.

What you’ll need to build this smart lock

  • An electrically controlled lock. I used the uxcell 12v solenoid.
  • A 9-volt battery. Technically speaking the solenoid requires twelve volts, but a nine-volt battery works.
  • A connector for the battery, such as a T Type Clip connector.
  • A digital relay, such as the CQRobot one. The NodeMCU hardware use 3.3 volts, which is insufficient to drive the lock. The digital relay allows a 3.3-volt device to control much higher voltages.
  • A NodeMCU development board. For example, I used one that I bought on for less than $10 US. For that price, you get internal flash memory for the software and its configuration files and wifi circuitry that can be used as either a station (a device connecting to a wifi access point) or as a wifi access point by itself. To learn more about NodeMCU boards, read “Getting to know NodeMCU and its DEVKIT board.”
  • A breadboard to make our circuit.
  • A few wires to use to connect our things together.


Setting up the NodeMCU board

Connect the NodeMCU development board to the breadboard, leaving a row of empty holes accessible on each side. In a breadboard, each column of five holes is connected together. On the top and bottom there are two rows that are connected for power. The one marked with blue and a minus is for the low voltage, ground. The one marked with red and a plus is for the high voltage, in this case 3.3 volts.

You get this power from the NodeMCU board itself. It has a number of pins marked GND for ground. Use a short wire to connect one of them to the blue line (like the green wire in the following figure). The NodeMCU board also has a number of pins marked 3V3 for the 3.3 volts. Connect one of them to the red line (like the orange wire in ).

Figure 1. NodeMCU board connected to a breadboard

NodeMCU comes with an operating system and a development environment as part of its System-on-a-Chip called the ESP8266. Its firmware includes the Lua scripting language. Also, NodeMCU lets you customize the firmware to include just the modules you need for your IoT project.

Note: I chose to use the NodeMCU Lua firmware because it is easy to use and because it is secure. If you want to use one of the other operating systems that can run on NodeMCU hardware, check if the web interface includes a remote procedure call that lets the user fully control the hardware (for example, change the state of an output pin to open a lock). Also, check if that function can be disabled. If it cannot be disabled, or if disabling this function makes development more difficult, then you should probably not use that operating system.

Create and install the firmware for your NodeMCU board by following these steps:

  1. Download and install the latest Python 3 version on your desktop or notebook computer. Make sure to add it to the PATH.
  2. Go to and create a Firmware. Make sure to use the master branch, and to select these modules: crypto, file,GPIO,HTTP, MQTT, net,node,timer, and WiFi. Also, select TLS/SSL support.
  3. Start a new command line interface (so it will have the updated PATH with Python), and run this command to install the firmware flasher: pip install esptool
  4. Wait until you receive the email that your firmware is ready, and then download the integer version (you do not need floating point in this article).
  5. Run this command (all one line) to flash the new firmware into the NodeMCU board:
 ‑‑port=<serial port> write_flash ‑fm=dio ‑fs=4MB 0 <firmware
  6. Download ESPlorer, and open the .zip file in a directory. Then, run ESPlorer.bat. You might have to install or upgrade Java first. If so, be careful not to allow the Java installer to change your browser settings.
  7. Select the serial port, and then click Open. If there are multiple serial ports, try them in sequence until you find the one that works. alt
  8. Click FS Info. If the connection is working, you will see the result in the right text area.

Write your first NodeMCU program to verify your configuration

At this point, we can write a small program to see that everything works. This program tells us when the status of D1 changes.

  1. Copy this code, and paste it into the left text area on ESPlorer.
    pin = 1
    gpio.mode(pin, gpio.INT)
    gpio.trig(pin, "both", 
            function(level, time)
    print("Connect D" .. pin .. " to one and then zero")
  2. Click the Send to ESP button.
  3. Connect a wire between the D1 pin and the red, positive power line (“1”). Then, connect the same wire between the D1 pin and the blue, negative power line (“0”). Make sure that every time you connect to a different voltage level you get text in the right text area.

Let’s walk through this code to see how it works. First, we declare a variable and assign it a value.

pin = 1

Next, we use the GPIO (general purpose input and output) module in the NodeMCU firmware to set the D1 pin to interrupt mode.

gpio.mode(pin, gpio.INT) 

This line sets an interrupt handler for D1. In this case, we want an interrupt whenever the value of D1 changes.

gpio.trig(pin, "both",

This function is the interrupt handler. Function definitions in Lua terminate with an end keyword. The level of the pin and the time at which the interrupt happened are both provided as arguments.

function(level, time)print(level) end

Finally, print directions for the user to know what to do. The double dot (..) operator is how you put two strings together in Lua.

print("Connect D"  .. pin
        .. " to one and then zero")


Build the circuit

Now, we need to connect the electrically controlled lock, its battery, and its relay to the NodeMCU board.

Use this circuit diagram in the following figure, which I created on Circuit Lab.

Figure 2. Circuit diagram for the smart lock

The relay has a control connector, which connects to the power lines (GND and 3V3) and to a pin that controls the relay’s connection. On the power side, it has four screw-style connectors. One is the common (COM) connector, one is the Normally Closed (NC) connector, which is normally (when there is no “1” signal on the control pin) connected to the common, and one is the Normally Open (NO) connector, which is normally disconnected and connected to the common when the control pin has a “1” signal. The fourth connector is not used.

Between the NO connector and the COM connector, we place the battery and the solenoid (the electromagnet) that controls the lock. The order and the polarity do not matter. You just need a closed circuit connecting the battery and the solenoid only when the control pin is “1”.

Figure 3. Photograph of connected components of the smart lock

To test the circuit, use this program. The lock will be open for 1 second, closed for 1 second, and then the cycle repeats for four more times.

Let’s walk through the scripting in this program. Here, we write to a pin, so the mode is output.

gpio.mode(pin, gpio.OUTPUT)

To do something every second, we use the timer module. This module is object-oriented; so, first we create a timer object.

timer = tmr.create()

Next, we register a timer (the Lua syntax for an object method is <object>:<method>). The first parameter is the time in milliseconds. The second is the type. A timer can either run once and be unregistered automatically (tmr.ALARM_``SINGLE), run once and be available for future use (tmr.ALARM_``SEMI), or run until stopped (tmr.ALARM_AUTO).


The handler function (itself a parameter to timer:register) receives the timer as a parameter

function (t) 

The first thing that the function does is write to the pin its new value.

gpio.write(pin, pinVal)

Then, this line toggles pinVal between zero and one and increments the counter. Notice the semicolon (;). That is the method to put multiple Lua commands on one line.

pinVal = 1‑pinVal; counter = counter + 1

This is the syntax for an if statement. Notice that the equality check uses two equal signs, same as in C and derived languages (C++, Java, C#, JavaScript, and so on). When the counter gets to ten, the timer (the parameter to the function) is stopped and then unregistered.

if counter == 10 then t:stop(); t:unregister() end

Finally, we start the timer.



Control the smart lock by a smartphone

To control the lock by a smartphone, we need to configure two elements:

  1. Turn the device into a wifi access point to have connectivity
  2. Configure an HTTP server that the smartphone can access

Turn the NodeMCU device into a wifi access point

Copy this code to ESPlorer, and send it to the NodeMCU to configure an access point. This configuration uses the wifi module. Let’s walk through this code.

First, we need to set the wifi mode. NodeMCU can be a station connected through an access point, an access point itself, or both. Because we are dealing with disconnected locks, we are using the NodeMCU as an access point to communicate with the smartphone.


Next, we need to configure the access point. The wifi.ap.config function gets as its parameter a table (the hash table is the main data structure in Lua). Table literals in Lua are similar to JavaScript ones, except that the separator between the key and value is equal (=) instead of a colon (:). A table is: {key1 = val1, key2 = val2, … keyLast = valLast}. In this case, there is only one key, the ssid.

result = wifi.ap.config({
    ssid = "Smartlock"

The wifi setup functions return a Boolean value — true if successful, and false if not. If one of them fails, it does not make sense doing those after it, so we just send out a message and skip them.

if (result == false) then
        print("wifi.ap.config failed") end

The wifi.ap.setip function sets the IP address configuration.

if result then 
    result =
        ip =
        netmask =
    if (result == false)
        then print("wifi.ap.setip failed") end

Finally, we need to start the DHCP server. The DHCP server automatically serves IP addresses that are compatible with the access point’s IP.

if result then 
    result =
    if (result == false)
        print("wifi.ap.dhcp.start failed") 

After you run the code, use a wifi device to connect to the Smartlock network that you just configured, and you’ll see that you do not need a password. We could have secured the network, but for our use case (single use passwords) there is no need.


Configure an HTTP server that the smartphone can access

The whole point of the access point is to run an HTTP server. To run a simple HTTP server, use this Lua program. Browse from a device that is connected to the Smartlock wifi network to You can open the lock at and close it at

You already know the parts that open and close the lock and that configure the access point.

Let’s walk through the code that implements this HTTP (web) server.

This function receives a path, handles the request, and returns a response.

function httpResponse(path)
  if path == "/on"
    return "Turn

  if path == "/off"
    return "Turn


The NodeMCU network API is very similar to the one in Node.js. However, it does not have an equivalent to the Express library. We need to specify that we create a server, and then that it listens on port 80. The function that is a parameter to the httpServer:listen function is called whenever a client connects to this server.

httpServer =

The conn:on in NodeMCU registers event handlers for events identified by strings. Here, the event is when information is received from the client.

conn:on("receive", function(conn, payload)

The only part that we care about in the HTTP header is the path, which is the second word (the first is the verb, and the third is the HTTP version). To get it, we use the string.match function. This function takes a string and a pattern, and returns the text that matches the part of the string within parentheses.

In Lua, patterns %s means a white space. Adding a plus makes it one or more. Changing a %<letter> to uppercase is negation, so %S means every character except for white spaces. The first string of non-white-spaces between which spaces is the second word. The return value is only the part in the parenthesis, the path (including query parameters).

path = string.match(payload, "%s+(%S+)%s+")
resp = httpResponse(path)

Some browsers get confused when they get text back from the server without a content type. By wrapping the response with h1 tags, we make it clear that the response is HTML.

conn:send("<h1>" .. resp .. "</h1>")

Lua uses two hyphens (–) to indicate the rest of the line is a comment.

end)  ‑‑ of the conn:on function
end)   ‑‑ of the httpServer:listen function 


Configure one-time passwords to allow remote access

Next, we need to implement the one-time passwords. We could just generate a number of them and store them on the NodeMCU’s flash memory, but that means anybody who got read access to the NodeMCU would be able to enter.

A safer solution is to use the S/Key protocol. This protocol is based a cryptographic hash function. In practical terms, this is an irreversible function. You can calculate it, but you cannot go back and figure what value produced a specific result.


Generate the keys

To generate keys, we start from a secret value and then run it through the hash multiple times. The intermediate values are used to authenticate, and the final value is stored on the device to verify authentication.

You can see Lua code to implement the key generation here. Let’s walk through the interesting parts.

The syntax to do a simple for loop in Lua is for <var>=<from>,<until>``do``<commands> end. You can add a third value after the equality sign to change the variable by a different value than one.

for i=1,keyNum do

This line uses two functions from the crypto module. First, crypto.hash calculates the cryptographic hash function. I chose to use the SHA-1 algorithm, but there are several other algorithms you can choose to use. Then, crypto.toHex turns the binary value into hexadecimal that can be easily displayed or copied into a web form. There is another function that uses base 64, but it uses some characters that are escaped when submitted in a web form, and there is no need to complicate the program by unescaping them.

   key = crypto.toHex(crypto.hash("sha1", key))
   print ("Key #" .. i .. " is " .. key)

So far, all our data storage has been in RAM. However, the authentication information needs to survive a reboot. To write to the flash, we use the file module. We need to save two values: the authentication key, and the number of the next key we need. It is easiest to do this with two separate files. The file module is very similar to the way files work on general purpose operating systems: You open the file, use it (write or read) and then close it.

storeMe = crypto.toHex(crypto.hash("sha1",

fd ="authkey", "w+")

fd ="keynum", "w+")


Check for authentication

To check for authentication, read the value stored in authkey, and compare it to the hash of the key that is received from the user. If the values don’t match, authentication fails. If the values do match, then this is the correct key. Decrement the key number and replace authkey with the new value. When the key number gets to zero, we need new S/Key keys. For this use case, I am going to assume that the device is simply replaced when this happens.

You can read the code here. Here is an explanation of the new parts:

The local keyword is used to define variables as local to the function (or any other block for that matter), rather than global.

function auth(newKey) 
  local oldKey
  local keyNum

Previously, we only wrote into files. Here, we need to read from them.

  fd ="authkey", "r"); oldKey = fd:read(); fd:close()
  fd ="keynum", "r"); keyNum = fd:read(); fd:close()

In this use case, lists of one-time passwords for all the remote locations are held by a central authority. When an employee in a remote location needs access to a lock, that central authority e-mails the next one-time password for that location. The employee can then go to the lock, pair the phone with the NodeMCU’s access point, and paste the one-time password into a web form to unlock the door.


Tying it all together

You can see a program that includes everything (except for key generation, which does not need to be done “in the field”) here. After you paste this program into ESPlorer, click Save, and type the file name init.lua (that file runs as part of the boot process). Then, click Save to ESP.

You might get an out of memory error when saving init.lua to the ESP. You can safely ignore this message. Click the Reset button.

To test this system, pretend you are a remote employee that needs to open the lock. Connect to the Smartlock network, and browse to Enter a wrong authorization key, and see that it doesn’t do anything. Return to the web form and enter the correct key (the list is here). See that the lock opens for a few seconds and then closes. Reload to verify that the key is only usable once. Go back to the form to see that it decremented the number of the requested key.

There are a few new things in the completed program that were not explained earlier. We use the string.match function again, this time to isolate the authorization code from the rest of the path. The question mark has special meaning in Lua patterns (one or zero of something), so we escape it. The escape character in Lua patterns is percent (%).

 authCode = string.match(path, "/on%?key=(%S+)") 

If the result is nil, it means that there is no authorization code. In that case, the program displays the form. To do this, it needs to read the key number. The local keyword is not restricted to functions – variables can be local to any block, in this case an if … then … end one.

if authCode == nil then
    local keyNum

fd ="keynum", "r"); keyNum = fd:read(); fd:close() 

Finally, the HTML for the web form is too big for one line. Luckily, Lua lets us specify multi-line string literal using the [[ … ]]construct.

return [
        <form action="/on"
        Authorization key #]        .. keyNum ..
        [: <input
        name="key" type="text">
        <br />


In this article, we developed a smart lock that authenticates a user without an internet connection. However, this lock is limited in several ways.

This smartlock will eventually run out of keys. In this sample application, it was not worth dealing with this problem. You can solve it by having a different form that lets users (if they are properly authenticated) specify a new password “seed”.

A harder problem to solve is tracking. We know that we provided the password to an employee at the remote site. We don’t know if that employee actually went and used it, or if the employee just kept it for later use. We could get a much clearer picture if the smart lock was connected to the internet and reported independently. I will explore this connected scenario in the next article in this series.