Introduction: WebApp Controlled Gate Operator Add-on (IoT)
I have a client who had a gated area where many people needed to come and go. They didn't want to use a keypad on the outside and only had a limited number of keyfob transmitters. Finding an affordable source for additional keyfobs was difficult. I thought it would be a great opportunity to upgrade this Liftmaster gate operator to be IoT compatible with custom hardware, web API, and web app interface. This not only solved the mass access problem it opened up additional functionality as well!
In the last photo above is the test unit that I had running for almost a year in a ziplock bag. I thought it was time for an upgrade!
This is a fully functioning solution with all the code, hardware information, and designs listed here.
All of the projects files are also hosted on GitHub: github.com/ThingEngineer/IoT-Gate-Operator-Addon
An example of the CodeIgniter WebApp interface is hosted here: projects.ajillion.com/gate This instance is not connected to a live gate but is the exact interface and code that is running on the gates (minus some security features).
--
For even greater integration you could use the IFTTT library for Electric Imp. https://electricimp.com/docs/libraries/webservices/ifttt/
Step 1: Gather the Parts
- You'll need an Electric IMP with at least 4 GPIOs available, I'm using the IMP001 with an April breakout board.
- A regulator to drop the source voltage down to 5V. I'm using a DC-DC Buck Converter Step Down Module. eBoot's MP1584EN version from Amazon.
- A dual (or more) relay module or similar switching device that will work with the IMPs output. I'm using this one JBtek 4 Channel DC 5V Relay Module from Amazon.
- A 4 wire screw terminal. I am using this one 5Pcs 2 Rows 12P Wire Connector Screw Terminal Barrier Block 300V 20A from Amazon.
Step 2: Supplies
You'll also need:
- Access to a 3D printer or a small project box
- 4 small screws about 4mm x 6mm for the case lid
- Hookup wire
- Wire cutters
- Wire strippers
- Small screwdrivers
- Soldering iron
- Hot glue or silicone
- Zip ties
Step 3: Size Up the Case
Layout your parts to determine what size case you will need. With a layout as pictured I'll need a case that is about 140mm wide, 70mm deep, and 30mm tall.
Step 4: Wire DC-DC Converter
Cut 3 pairs of red and black hookup wire for power connections in and out of the DC-DC converter board.
- Input: 100mm
- Output to IMP: 90mm
- Output to Relay Module: 130mm
Solder them to your board as shown.
Step 5: Wire Power to Devices
- Connect the input of the DC-DC converter to two of the points on the screw terminal block.
- Solder the short 5V output wires to the IMP.
- Solder the longer 5V output wires to the relay module.
Step 6: Wire Relay Module Inputs
- Cut 4 x 90mm wires for the relay module input connections. I used 4 separate colors for easy reference later on while coding.
- Solder the wires to the relay module inputs 1-4 then to the first 4 IMP GPIO spots (Pin1, 2, 5, & 7) respectively.
Step 7: IMP Power Jumper
You may need to use USB power while you are initially programming and testing your IMP. When you finish, be sure to move the power jumper to the BAT side.
Step 8: Wire Gate Status Inputs
- Cut 2 x 80mm wires for the sate status inputs.
- Connect wires to the remaining 2 screw terminals.
- Solder wires to the next to IMP GPIO spots (Pin8 & 9) respectively.
Step 9: Print or Purchase a Case
You can download my .STL or .F3D for this case on GitHub or Thingiverse
If you don't have access to a 3D printer a small generic project case will work.
Step 10: Decorate Your Case
Because!
I put some indented text on mine and just colored it in with a black sharpie. If you're feeling adventurous you could use acrylic paint, fingernail polish or something else to make it even slicker.
Step 11: Drill Hole for Wires
Drill a small hole 10-15mm on the side near the middle of where all the wires will come together.
I used a Unibit for a clean, smooth hole in the plastic.
Step 12: Prep and Install Hookup Wires
Cut 9 x 5-600mm wires for hooking our device up to the gate operator board.
- 2 for the 24V power input
- 3 for the gate status (2 inputs and a common ground)
- 2 for the open gate signal
- 2 for the close gate signal
Twist together each of the groups listed above using a drill. This will make everything easier and look better.
Strip and connect each of the wires to the respective terminals as shown.
Step 13: Route Hookup Wires
Route the hookup wires through the hole as shown.
Step 14: Mount Components
Place and mount components with a small bead of hot glue or silicone. Don't use too much in case you need to remove a part, use just enough to secure them.
I originally wanted to print the case with clips/tabs to hold the boards in place but I needed to get this installed and didn't have time. Adding board clips to your case would be a nice touch.
Step 15: Seal Hookup Wires
Seal the hookup wires with hot glue or silicone.
Step 16: Close the Case
I used small ~4mm screws on the list of this 3D printed case. If you are concerned about dirt or moisture place a bead of silicone or hot glue around the lid joint before closing it up.
Step 17: Install in Gate Operator
On the main board:
- Hook the two wires connected to relay output 1 to the Open Gate terminal. (red/brown in photos)
- Hook the two wires connected to relay output 2 to the Close Gate terminal. (yellow/blue in photos)
- Hook the two wires connected to the DC-DC converter input to the 24V accessory power screw terminals (red/black in photos)
On the expansion board
- Jumper the relay common screw terminals together with a small piece of wire
- Connect the common ground to one of the relay common screw terminals (green in photos)
- Connect the 2 gate status inputs (IMP Pin8 & 9) to the relay normal open (NO) screw terminals (grey/yellow in photos)
Route the wires, zip tie them to look neat and find a place to mount or set your case.
There are additional, full resolution photos, hosted on the GitHub repository.
Step 18: Set Aux Relay Mode
Set the auxiliary relay switches as shown in the photo.
This will give the IMP the signals it needs to determine if the gate is closed, opening, open or closing.
Step 19: IMP Agent and Device Code
Electric Imp Agent Code:
- Create a new Model in the Electric Imp IDE: https://ide.electricimp.com/
- Replace URL to point at your server
// HTTP handler function function httpHandler(req, resp) { try { local d = http.jsondecode(req.body); //server.log(d.c); if (d.c == "btn") { //server.log(d.val); device.send("btn", d.val); resp.send(200, "OK"); } } catch(ex) { // If there was an error, send it back in the response server.log("error:" + ex); resp.send(500, "Internal Server Error: " + ex); } } // Register HTTP handler http.onrequest(httpHandler); // GateStateChange handler function function gateStateChangeHandler(data) { // URL to web service local url = "http://projects.ajillion.com/save_gate_state"; // Set Content-Type header to json local headers = { "Content-Type" : "application/json" }; // Encode received data and log local body = http.jsonencode(data); server.log(body); // Send the data to your web service http.post(url, headers, body).sendsync(); } // Register gateStateChange handler device.on("gateStateChange", gateStateChangeHandler);
Electric Imp Agent Code:
- Assign an Imp device to your model
- Verify hardware pins are Aliased as connected
// Debouce library #require "Button.class.nut:1.2.0" // Alias for gateOpen GPIO pin (active low) gateOpen <- hardware.pin2; // Alias for gateClose control GPIO pin (active low) gateClose <- hardware.pin7; // Configure 'gateOpen' to be a digital output with a starting value of digital 1 (high) gateOpen.configure(DIGITAL_OUT, 1); // Configure 'gateClose' to be a digital output with a starting value of digital 1 (high) gateClose.configure(DIGITAL_OUT, 1); // Alias for the GPIO pin that indicates the gate is moving (N.O.) gateMovingState <- Button(hardware.pin8, DIGITAL_IN_PULLUP); // Alias for the GPIO pin that indicates the gate is fully open (N.O.) gateOpenState <- Button(hardware.pin9, DIGITAL_IN_PULLUP); // Global variable to hold the gate state (Open = 1 / Closed = 0) local lastGateOpenState = 0; // Latch Timer object local latchTimer = null agent.on("btn", function(data) { switch (data.cmd) { case "open": gateOpen.write(0); if (latchTimer) imp.cancelwakeup(latchTimer); latchTimer = imp.wakeup(1, releaseOpen); server.log("Open command received"); break case "latch30m": gateOpen.write(0); if (latchTimer) imp.cancelwakeup(latchTimer); latchTimer = imp.wakeup(1800, releaseOpen); server.log("Latch30m command received"); break case "latch8h": gateOpen.write(0); if (latchTimer) imp.cancelwakeup(latchTimer); latchTimer = imp.wakeup(28800, releaseOpen); server.log("Latch8h command received"); break case "close": if (latchTimer) imp.cancelwakeup(latchTimer); gateOpen.write(1); gateClose.write(0); latchTimer = imp.wakeup(1, releaseClose); server.log("Close now command received"); break default: server.log("Button command not recognized"); } }); function releaseOpen() { if (latchTimer) imp.cancelwakeup(latchTimer); gateOpen.write(1); //server.log("Timer released gateOpen switch contact"); } function releaseClose() { if (latchTimer) imp.cancelwakeup(latchTimer); gateClose.write(1); //server.log("Timer released gateClose switch contact"); } gateMovingState.onPress(function() { // The relay is activated, gate is moving //server.log("Gate is opening"); local data = { "gatestate" : 1, "timer" : hardware.millis() }; agent.send("gateStateChange", data); }).onRelease(function() { // The relay is released, gate is at rest //server.log("Gate is closed"); local data = { "gatestate" : 0, "timer" : hardware.millis() }; agent.send("gateStateChange", data); }); gateOpenState.onPress(function() { // The relay is activated, gate is fully open //server.log("Gate is open"); local data = { "gatestate" : 2, "timer" : hardware.millis() }; agent.send("gateStateChange", data); }).onRelease(function() { // The relay is released, gate is not fully open //server.log("Gate is closing"); local data = { "gatestate" : 3, "timer" : hardware.millis() }; agent.send("gateStateChange", data); });
Step 20: Web Service PHP Code
I wrote this code for the CodeIgniter framework because I added it to an old existing project. The controller and view code can be easily adapted to the framework of your choice.
To keep things simple I saved JSON data to a flat file for data storage. If you need logging or more complex data related functions use a database.
The ajax library I wrote and used in this project can download from the GitHub repository: ThingEngineer/Codeigniter-jQuery-Ajax
PHP Controller Code:
- app/controllers/projects.php
- Ensure data path is accessible by your PHP script, both location and read/write privileges.
<?php class Projects extends CI_Controller { function index() { } function gate() { $this->load->helper(array('file', 'date')); $data = json_decode(read_file('../app/logs/gatestate.data'), TRUE); switch ($data['gatestate']) { case 0: $view_data['gatestate'] = 'Closed'; break; case 1: $view_data['gatestate'] = 'Opening...'; break; case 2: $view_data['gatestate'] = 'Open'; break; case 3: $view_data['gatestate'] = 'Closing...'; break; } $last_opened = json_decode(read_file('../app/logs/projects/gateopened.data'), TRUE); $view_data['last_opened'] = timespan($last_opened['last_opened'], time()) . ' ago'; //Load View $t['data'] = $view_data; $this->load->view('gate_view', $t); } function save_gate_state() { $this->load->helper('file'); $data = file_get_contents('php://input'); write_file('../app/logs/projects/gatestate.data', $data); $data = json_decode($data, TRUE); if ($data['gatestate'] == 1) { $last_opened = array('last_opened' => time()); write_file('../app/logs/projects/gateopened.data', json_encode($last_opened)); } } function get_gate_state() { $this->load->helper(array('file', 'date')); $this->load->library('ajax'); $data = json_decode(read_file('../app/logs/projects/gatestate.data'), TRUE); $last_opened = json_decode(read_file('../app/logs/projects/gateopened.data'), TRUE); $data['last_opened'] = timespan($last_opened['last_opened'], time()) . ' ago'; $this->ajax->output_ajax($data, 'json', FALSE); // send json data, don't enforce ajax request } } /* End of file projects.php */ /* Location: ./application/controllers/projects.php */
PHP View Code:
I used Bootstrap for the front-end because it's quick, easy and responsive. You can download it here: https://getbootstrap.com (jQuery is included)
- app/controllers/gate_view.php
- Replace YOUR-AGENT-CODE with your Electric Imp agent code
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="description" content=""> <meta name="author" content="Josh Campbell"> <link rel="shortcut icon" href="/bootstrap/assets/ico/favicon.png"> <title>IoT Gate Opperator Addon</title> <!-- Bootstrap core CSS --> <link href="/bootstrap/css/bootstrap.css" rel="stylesheet"> <link href="/bootstrap/slider/css/slider.css" rel="stylesheet"> <!-- Custom styles for this template --> <link href="/bootstrap/css/main.css" rel="stylesheet"> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="/bootstrap/assets/js/html5shiv.js"></script> <script src="/bootstrap/assets/js/respond.min.js"></script> <![endif]--> </head> <body> <div class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">IoT Gate Opperator Addon</a> </div> <div class="collapse navbar-collapse"> <!-- <ul class="nav navbar-nav"> <li class="active"><a href="http://projects.ajillion.com/gate">Home</a></li> <li class="active"><a href="http://projects.ajillion.com/gate">Admin</a></li> </ul>--> </div> </div> </div> <div class="container"> <div class="starter-template"> <button type="button" class="btn btn-lg btn-success btn-block bigger-button" id="open_gate">Open Gate</button><br> <button type="button" class="btn btn-lg btn-primary btn-block" id="latch30m_gate">Latch Open for 30 min</button><br> <button type="button" class="btn btn-lg btn-primary btn-block" id="latch8h_gate">Latch Open for 8 hours</button><br> <button type="button" class="btn btn-lg btn-warning btn-block" id="close_gate">Close Now</button> <br><br> <div class="jumbotron alert alert-info" role="alert"> <strong>Gate Status: </strong><span id="status"><?=$data['gatestate']?></span><br> Last opened <span id="last_opened"><?=$data['last_opened']?></span> </div> </div> </div><!-- /.container --> <script src="/bootstrap/slider/js/modernizr.js" type="text/javascript"></script> <script src="/bootstrap/assets/js/jquery.js" type="text/javascript"></script> <script src="/bootstrap/js/bootstrap.min.js" type="text/javascript"></script> <script> $(document).ready(function(){ resetStatus(); }) function sendJSON(JSONout){ var url = 'https://agent.electricimp.com/YOUR-AGENT-CODE'; $.post(url, JSONout); } $("#open_gate").click(function() { var JSONout = '{"c":"btn","val":{"cmd":"open"}}'; sendJSON(JSONout); $("#status").text("Opening..."); }); $("#latch30m_gate").click(function() { var JSONout = '{"c":"btn","val":{"cmd":"latch30m"}}'; sendJSON(JSONout); $("#status").text("Opening..."); }); $("#latch8h_gate").click(function() { var JSONout = '{"c":"btn","val":{"cmd":"latch8h"}}'; sendJSON(JSONout); $("#status").text("Opening..."); }); $("#close_gate").click(function() { var JSONout = '{"c":"btn","val":{"cmd":"close"}}'; sendJSON(JSONout); $("#status").text("Closing..."); }); function resetStatus() { // Target url var target = 'http://projects.ajillion.com/get_gate_state'; // Request var data = { agent : 'app' }; // Send ajax post request $.ajax({ url: target, dataType: 'json', type: 'POST', data: data, success: function(data, textStatus, XMLHttpRequest) { switch(data.gatestate) { case 0: $("#status").text('Closed'); break; case 1: $("#status").text('Opening...'); break; case 2: $("#status").text('Open'); break; case 3: $("#status").text('Closing...'); break; default: $("#status").text('Error'); } $("#last_opened").text(data.last_opened); }, error: function(XMLHttpRequest, textStatus, errorThrown) { // Error message $("#status").text('Server Error'); } }); setTimeout(resetStatus, 3000); } </script> </body> </html>