├── .gitattributes
├── README.md
├── door_lock.sql
└── lock.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Raspberry Pi Smart Door Lock
2 | This award-winning project combines a Raspberry Pi, a MySQL database, an RFID reader, an LCD touchscreen, a relay switch, an electronic door strike and a Twilio SMS account to create the "ultimate" Raspberry Pi smart door lock security system - with "three factor" authentication.
3 |
4 | It's not quite true three-factor authentication, as two of the pieces are "something you have"... but it's still 3 hoops to jump through to gain access.
5 |
6 | In this repo you will find the Python code required as well as the MySQL database table setup.
7 |
8 | Why on earth would you want to require an RFID fob, followed by a PIN entered on the touchscreen, followed by yet another one-time code sent to you via SMS? Well, mainly for the fun of building it! There are also many applications where this might be useful for either an external or an internal door, or even on a cupboard or safe. Just make sure you build in some fail-safe overrides if you use this in the real-world!
9 |
10 | The video below shows a demonstration of the finished system and an in-depth tutorial of how to build and configure it. I hope you enjoy it, and please give the video a thumbs-up and subscribe to the YouTube channel.
11 |
12 | [](https://www.youtube.com/watch?v=TX_WQMYc0SU)
13 |
14 | You can also read an in-depth text tutorial on the Switched On Network website.
15 |
--------------------------------------------------------------------------------
/door_lock.sql:
--------------------------------------------------------------------------------
1 | -- phpMyAdmin SQL Dump
2 | -- version 4.2.12deb2+deb8u2
3 | -- http://www.phpmyadmin.net
4 | --
5 | -- Host: localhost
6 | -- Generation Time: Sep 28, 2017 at 03:22 PM
7 | -- Server version: 5.5.55-0+deb8u1
8 | -- PHP Version: 5.6.30-0+deb8u1
9 |
10 | SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
11 | SET time_zone = "+00:00";
12 |
13 |
14 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
15 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
16 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
17 | /*!40101 SET NAMES utf8 */;
18 |
19 | --
20 | -- Database: `door_lock`
21 | --
22 |
23 | -- --------------------------------------------------------
24 |
25 | --
26 | -- Table structure for table `access_list`
27 | --
28 |
29 | CREATE TABLE IF NOT EXISTS `access_list` (
30 | `user_id` int(11) NOT NULL,
31 | `name` varchar(100) NOT NULL,
32 | `image` varchar(50) DEFAULT NULL,
33 | `rfid_code` varchar(20) NOT NULL,
34 | `pin` char(6) NOT NULL,
35 | `sms_number` varchar(15) NOT NULL
36 | ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;
37 |
38 | -- --------------------------------------------------------
39 |
40 | --
41 | -- Table structure for table `access_log`
42 | --
43 |
44 | CREATE TABLE IF NOT EXISTS `access_log` (
45 | `access_id` int(11) NOT NULL,
46 | `rfid_presented` varchar(20) DEFAULT NULL,
47 | `rfid_presented_datetime` datetime DEFAULT NULL,
48 | `rfid_granted` tinyint(1) DEFAULT NULL,
49 | `pin_entered` char(6) DEFAULT NULL,
50 | `pin_entered_datetime` datetime DEFAULT NULL,
51 | `pin_granted` tinyint(1) DEFAULT NULL,
52 | `mobile_number` varchar(15) DEFAULT NULL,
53 | `smscode_entered` char(6) DEFAULT NULL,
54 | `smscode_entered_datetime` datetime DEFAULT NULL,
55 | `smscode_granted` tinyint(1) DEFAULT NULL
56 | ) ENGINE=InnoDB AUTO_INCREMENT=116 DEFAULT CHARSET=latin1;
57 |
58 | -- --------------------------------------------------------
59 |
60 | --
61 | -- Table structure for table `twilio_api_credentials`
62 | --
63 |
64 | CREATE TABLE IF NOT EXISTS `twilio_api_credentials` (
65 | `id` int(11) NOT NULL,
66 | `account_sid` varchar(50) NOT NULL,
67 | `auth_token` varchar(50) NOT NULL,
68 | `twilio_sms_number` varchar(15) NOT NULL
69 | ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
70 |
71 | --
72 | -- Indexes for dumped tables
73 | --
74 |
75 | --
76 | -- Indexes for table `access_list`
77 | --
78 | ALTER TABLE `access_list`
79 | ADD PRIMARY KEY (`user_id`), ADD UNIQUE KEY `rfid_code` (`rfid_code`), ADD UNIQUE KEY `image` (`image`);
80 |
81 | --
82 | -- Indexes for table `access_log`
83 | --
84 | ALTER TABLE `access_log`
85 | ADD PRIMARY KEY (`access_id`);
86 |
87 | --
88 | -- Indexes for table `twilio_api_credentials`
89 | --
90 | ALTER TABLE `twilio_api_credentials`
91 | ADD PRIMARY KEY (`id`);
92 |
93 | --
94 | -- AUTO_INCREMENT for dumped tables
95 | --
96 |
97 | --
98 | -- AUTO_INCREMENT for table `access_list`
99 | --
100 | ALTER TABLE `access_list`
101 | MODIFY `user_id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=3;
102 | --
103 | -- AUTO_INCREMENT for table `access_log`
104 | --
105 | ALTER TABLE `access_log`
106 | MODIFY `access_id` int(11) NOT NULL AUTO_INCREMENT,AUTO_INCREMENT=116;
107 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
108 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
109 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
110 |
--------------------------------------------------------------------------------
/lock.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | import sys
3 | import MySQLdb
4 | from threading import Thread
5 | import threading
6 | import time
7 | import RPi.GPIO as GPIO
8 | import json
9 | from random import randint
10 | from evdev import InputDevice
11 | from select import select
12 | from twilio.rest import Client
13 |
14 | GPIO.setmode(GPIO.BCM)
15 | GPIO.setwarnings(False)
16 | GPIO.setup(13,GPIO.OUT)
17 |
18 | try:
19 | # python 2
20 | import Tkinter as tk
21 | import ttk
22 | except ImportError:
23 | # python 3
24 | import tkinter as tk
25 | from tkinter import ttk
26 |
27 | class Fullscreen_Window:
28 |
29 | global dbHost
30 | global dbName
31 | global dbUser
32 | global dbPass
33 |
34 | dbHost = 'localhost'
35 | dbName = 'DB_NAME'
36 | dbUser = 'USER'
37 | dbPass = 'PASSWORD'
38 |
39 | def __init__(self):
40 | self.tk = tk.Tk()
41 | self.tk.title("Three-Factor Authentication Security Door Lock")
42 | self.frame = tk.Frame(self.tk)
43 | self.frame.grid()
44 | self.tk.columnconfigure(0, weight=1)
45 |
46 | self.tk.attributes('-zoomed', True)
47 | self.tk.attributes('-fullscreen', True)
48 | self.state = True
49 | self.tk.bind("", self.toggle_fullscreen)
50 | self.tk.bind("", self.end_fullscreen)
51 | self.tk.config(cursor="none")
52 |
53 | self.show_idle()
54 |
55 | t = Thread(target=self.listen_rfid)
56 | t.daemon = True
57 | t.start()
58 |
59 | def show_idle(self):
60 | self.welcomeLabel = ttk.Label(self.tk, text="Please Present\nYour Token")
61 | self.welcomeLabel.config(font='size, 20', justify='center', anchor='center')
62 | self.welcomeLabel.grid(sticky=tk.W+tk.E, pady=210)
63 |
64 | def pin_entry_forget(self):
65 | self.validUser.grid_forget()
66 | self.photoLabel.grid_forget()
67 | self.enterPINlabel.grid_forget()
68 | count = 0
69 | while (count < 12):
70 | self.btn[count].grid_forget()
71 | count += 1
72 |
73 | def returnToIdle_fromPINentry(self):
74 | self.pin_entry_forget()
75 | self.show_idle()
76 |
77 | def returnToIdle_fromPINentered(self):
78 | self.PINresultLabel.grid_forget()
79 | self.show_idle()
80 |
81 | def returnToIdle_fromAccessGranted(self):
82 | GPIO.output(13,GPIO.LOW)
83 | self.SMSresultLabel.grid_forget()
84 | self.show_idle()
85 |
86 | def returnToIdle_fromSMSentry(self):
87 | self.PINresultLabel.grid_forget()
88 | self.smsDigitsLabel.grid_forget()
89 | count = 0
90 | while (count < 12):
91 | self.btn[count].grid_forget()
92 | count += 1
93 | self.show_idle()
94 |
95 | def returnToIdle_fromSMSentered(self):
96 | self.SMSresultLabel.grid_forget()
97 | self.show_idle()
98 |
99 | def toggle_fullscreen(self, event=None):
100 | self.state = not self.state # Just toggling the boolean
101 | self.tk.attributes("-fullscreen", self.state)
102 | return "break"
103 |
104 | def end_fullscreen(self, event=None):
105 | self.state = False
106 | self.tk.attributes("-fullscreen", False)
107 | return "break"
108 |
109 | def listen_rfid(self):
110 | global pin
111 | global accessLogId
112 |
113 | keys = "X^1234567890XXXXqwertzuiopXXXXasdfghjklXXXXXyxcvbnmXXXXXXXXXXXXXXXXXXXXXXX"
114 | dev = InputDevice('/dev/input/event0')
115 | rfid_presented = ""
116 |
117 | while True:
118 | r,w,x = select([dev], [], [])
119 | for event in dev.read():
120 | if event.type==1 and event.value==1:
121 | if event.code==28:
122 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName)
123 | cur = dbConnection.cursor(MySQLdb.cursors.DictCursor)
124 | cur.execute("SELECT * FROM access_list WHERE rfid_code = '%s'" % (rfid_presented))
125 |
126 | if cur.rowcount != 1:
127 | self.welcomeLabel.config(text="ACCESS DENIED")
128 |
129 | # Log access attempt
130 | cur.execute("INSERT INTO access_log SET rfid_presented = '%s', rfid_presented_datetime = NOW(), rfid_granted = 0" % (rfid_presented))
131 | dbConnection.commit()
132 |
133 | time.sleep(3)
134 | self.welcomeLabel.grid_forget()
135 | self.show_idle()
136 | else:
137 | user_info = cur.fetchone()
138 | userPin = user_info['pin']
139 | self.welcomeLabel.grid_forget()
140 | self.validUser = ttk.Label(self.tk, text="Welcome\n %s!" % (user_info['name']), font='size, 15', justify='center', anchor='center')
141 | self.validUser.grid(columnspan=3, sticky=tk.W+tk.E)
142 |
143 | self.image = tk.PhotoImage(file=user_info['image'] + ".gif")
144 | self.photoLabel = ttk.Label(self.tk, image=self.image)
145 | self.photoLabel.grid(columnspan=3)
146 |
147 | self.enterPINlabel = ttk.Label(self.tk, text="Please enter your PIN:", font='size, 18', justify='center', anchor='center')
148 | self.enterPINlabel.grid(columnspan=3, sticky=tk.W+tk.E)
149 | pin = ''
150 |
151 | keypad = [
152 | '1', '2', '3',
153 | '4', '5', '6',
154 | '7', '8', '9',
155 | '*', '0', '#',
156 | ]
157 |
158 | # create and position all buttons with a for-loop
159 | # r, c used for row, column grid values
160 | r = 4
161 | c = 0
162 | n = 0
163 | # list(range()) needed for Python3
164 | self.btn = list(range(len(keypad)))
165 | for label in keypad:
166 | # partial takes care of function and argument
167 | #cmd = partial(click, label)
168 | # create the button
169 | self.btn[n] = tk.Button(self.tk, text=label, font='size, 18', width=4, height=1, command=lambda digitPressed=label:self.codeInput(digitPressed, userPin, user_info['sms_number']))
170 | # position the button
171 | self.btn[n].grid(row=r, column=c, ipadx=10, ipady=10)
172 | # increment button index
173 | n += 1
174 | # update row/column position
175 | c += 1
176 | if c > 2:
177 | c = 0
178 | r += 1
179 |
180 |
181 | # Log access attempt
182 | cur.execute("INSERT INTO access_log SET rfid_presented = '%s', rfid_presented_datetime = NOW(), rfid_granted = 1" % (rfid_presented))
183 | dbConnection.commit()
184 | accessLogId = cur.lastrowid
185 |
186 | self.PINentrytimeout = threading.Timer(10, self.returnToIdle_fromPINentry)
187 | self.PINentrytimeout.start()
188 |
189 | self.PINenteredtimeout = threading.Timer(5, self.returnToIdle_fromPINentered)
190 |
191 | rfid_presented = ""
192 | dbConnection.close()
193 | else:
194 | rfid_presented += keys[ event.code ]
195 |
196 | def codeInput(self, value, userPin, mobileNumber):
197 | global accessLogId
198 | global pin
199 | global smsCodeEntered
200 | pin += value
201 | pinLength = len(pin)
202 |
203 | self.enterPINlabel.config(text="Digits Entered: %d" % pinLength)
204 |
205 | if pinLength == 6:
206 | self.PINentrytimeout.cancel()
207 | self.pin_entry_forget()
208 |
209 | if pin == userPin:
210 | pin_granted = 1
211 | else:
212 | pin_granted = 0
213 |
214 | # Log access attempt
215 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName)
216 | cur = dbConnection.cursor()
217 | cur.execute("UPDATE access_log SET pin_entered = '%s', pin_entered_datetime = NOW(), pin_granted = %s, mobile_number = '%s' WHERE access_id = %s" % (pin, pin_granted, mobileNumber, accessLogId))
218 | dbConnection.commit()
219 |
220 | if pin == userPin:
221 | self.PINresultLabel = ttk.Label(self.tk, text="Thank You, Now\nPlease Enter Code\nfrom SMS\n")
222 | self.PINresultLabel.config(font='size, 20', justify='center', anchor='center')
223 | self.PINresultLabel.grid(columnspan=3, sticky=tk.W+tk.E, pady=20)
224 |
225 | self.smsDigitsLabel = ttk.Label(self.tk, text="Digits Entered: 0", font='size, 18', justify='center', anchor='center')
226 | self.smsDigitsLabel.grid(columnspan=3, sticky=tk.W+tk.E)
227 |
228 | smsCode = self.sendSMScode(mobileNumber)
229 | smsCodeEntered = ''
230 |
231 | keypad = [
232 | '1', '2', '3',
233 | '4', '5', '6',
234 | '7', '8', '9',
235 | '', '0', '',
236 | ]
237 |
238 | # create and position all buttons with a for-loop
239 | # r, c used for row, column grid values
240 | r = 4
241 | c = 0
242 | n = 0
243 | # list(range()) needed for Python3
244 | self.btn = list(range(len(keypad)))
245 | for label in keypad:
246 | # partial takes care of function and argument
247 | #cmd = partial(click, label)
248 | # create the button
249 | self.btn[n] = tk.Button(self.tk, text=label, font='size, 18', width=4, height=1, command=lambda digitPressed=label:self.smsCodeEnteredInput(digitPressed, smsCode))
250 | # position the button
251 | self.btn[n].grid(row=r, column=c, ipadx=10, ipady=10)
252 | # increment button index
253 | n += 1
254 | # update row/column position
255 | c += 1
256 | if c > 2:
257 | c = 0
258 | r += 1
259 |
260 | self.SMSentrytimeout = threading.Timer(60, self.returnToIdle_fromSMSentry)
261 | self.SMSentrytimeout.start()
262 |
263 | else:
264 | self.PINresultLabel = ttk.Label(self.tk, text="Incorrect PIN\nEntered!")
265 | self.PINresultLabel.config(font='size, 20', justify='center', anchor='center')
266 | self.PINresultLabel.grid(sticky=tk.W+tk.E, pady=210)
267 | self.PINenteredtimeout.start()
268 |
269 | def smsCodeEnteredInput(self, value, smsCode):
270 | global smsCodeEntered
271 | global accessLogId
272 | smsCodeEntered += value
273 | smsCodeEnteredLength = len(smsCodeEntered)
274 |
275 | self.smsDigitsLabel.config(text="Digits Entered: %d" % smsCodeEnteredLength)
276 |
277 | if smsCodeEnteredLength == 6:
278 | self.SMSentrytimeout.cancel()
279 | self.pin_entry_forget()
280 |
281 | if smsCodeEntered == smsCode:
282 | smscode_granted = 1
283 | else:
284 | smscode_granted = 0
285 |
286 | # Log access attempt
287 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName)
288 | cur = dbConnection.cursor()
289 | cur.execute("UPDATE access_log SET smscode_entered = '%s', smscode_entered_datetime = NOW(), smscode_granted = %s WHERE access_id = %s" % (smsCodeEntered, smscode_granted, accessLogId))
290 | dbConnection.commit()
291 |
292 | if smsCodeEntered == smsCode:
293 | self.SMSresultLabel = ttk.Label(self.tk, text="Thank You,\nAccess Granted")
294 | self.SMSresultLabel.config(font='size, 20', justify='center', anchor='center')
295 | self.SMSresultLabel.grid(columnspan=3, sticky=tk.W+tk.E, pady=210)
296 |
297 | self.PINresultLabel.grid_forget()
298 | self.smsDigitsLabel.grid_forget()
299 | GPIO.output(13,GPIO.HIGH)
300 |
301 | self.doorOpenTimeout = threading.Timer(10, self.returnToIdle_fromAccessGranted)
302 | self.doorOpenTimeout.start()
303 | else:
304 | self.PINresultLabel.grid_forget()
305 | self.smsDigitsLabel.grid_forget()
306 |
307 | self.SMSresultLabel = ttk.Label(self.tk, text="Incorrect SMS\nCode Entered!")
308 | self.SMSresultLabel.config(font='size, 20', justify='center', anchor='center')
309 | self.SMSresultLabel.grid(sticky=tk.W+tk.E, pady=210)
310 |
311 | self.SMSenteredtimeout = threading.Timer(10, self.returnToIdle_fromSMSentered)
312 | self.SMSenteredtimeout.start()
313 |
314 | def sendSMScode(self, mobileNumber):
315 |
316 | # Retreive our Twilio access credentials and "from" number
317 | dbConnection = MySQLdb.connect(host=dbHost, user=dbUser, passwd=dbPass, db=dbName)
318 | cur = dbConnection.cursor(MySQLdb.cursors.DictCursor)
319 | cur.execute("SELECT account_sid, auth_token, twilio_sms_number FROM twilio_api_credentials WHERE id = 1")
320 | credentials = cur.fetchone()
321 | account_sid = credentials['account_sid']
322 | auth_token = credentials['auth_token']
323 | twilio_sms_number = credentials['twilio_sms_number']
324 | dbConnection.close()
325 |
326 | smsCode = str(randint(100000, 999999))
327 | messageText = "Your access code is %s. Please enter this on the touchscreen to continue." % smsCode
328 |
329 | client = Client(account_sid, auth_token)
330 | message = client.messages.create(
331 | to=mobileNumber,
332 | from_=twilio_sms_number,
333 | body=messageText)
334 |
335 | return smsCode
336 |
337 | if __name__ == '__main__':
338 | w = Fullscreen_Window()
339 | w.tk.mainloop()
--------------------------------------------------------------------------------