MySensors Library & Examples  2.3.2
SecretKnockSensor.ino
1 /*
2  * The MySensors Arduino library handles the wireless radio link and protocol
3  * between your home built sensors/actuators and HA controller of choice.
4  * The sensors forms a self healing radio network with optional repeaters. Each
5  * repeater and gateway builds a routing tables in EEPROM which keeps track of the
6  * network topology allowing messages to be routed to nodes.
7  *
8  * Created by Henrik Ekblad <[email protected]>
9  * Copyright (C) 2013-2019 Sensnology AB
10  * Full contributor list: https://github.com/mysensors/MySensors/graphs/contributors
11  *
12  * Documentation: http://www.mysensors.org
13  * Support Forum: http://forum.mysensors.org
14  *
15  * This program is free software; you can redistribute it and/or
16  * modify it under the terms of the GNU General Public License
17  * version 2 as published by the Free Software Foundation.
18  *
19  *******************************
20  *
21  * REVISION HISTORY
22  * Version 1.0 - Henrik Ekblad
23  *
24  * DESCRIPTION
25  *
26  * Secret Knock Sensor
27  * http://www.mysensors.org/build/knock
28  *
29  * See original instructions here (note: The MySensors adopted code might differ in wiring. The instructions below is correct):
30  * https://learn.adafruit.com/secret-knock-activated-drawer-lock/
31  * Version 13.10.31 Built with Arduino IDE 1.0.5
32  *
33  * By Steve Hoefer http://grathio.com
34  * Adapted to MySensors by Henrik Ekblad
35  *
36  * Licensed under Creative Commons Attribution-Noncommercial-Share Alike 3.0
37  * http://creativecommons.org/licenses/by-nc-sa/3.0/us/
38  * (In short: Do what you want, as long as you credit me, don't relicense it, and don't sell it or use it in anything you sell without contacting me.)
39  *
40  * ------Wiring------
41  * Pin 0: Program button used for recording a new Knock (connect Pin0 -> button -> GND)
42  * Pin 1: Optional: Connect LED here (remember resisor in series)
43  * Pin 2: Optional: Piezo element (for beeps).
44  * Pin 5: A sound sensor (digital output) for sensing knocks. See MySensors purchase guide. I used this: http://rover.ebay.com/rover/1/711-53200-19255-0/1?icep_ff3=2&pub=5575069610&toolid=10001&campid=5337433187&customid=&icep_item=200941260251&ipn=psmain&icep_vectorid=229466&kwid=902099&mtid=824&kw=lg
45  * Pin 4: Connects to either 1. Relay which open door or lock or
46  * 2. transistor that opens a solenoid lock when HIGH (see adafruit guide for this option).
47  *
48  *
49  * Connect radio according as usual(you can skip IRQ pin)
50  * http://www.mysensors.org/build/connect_radio
51  */
52 
53 
54 // Enable debug prints to serial monitor
55 #define MY_DEBUG
56 
57 // Enable and select radio type attached
58 #define MY_RADIO_RF24
59 //#define MY_RADIO_NRF5_ESB
60 //#define MY_RADIO_RFM69
61 //#define MY_RADIO_RFM95
62 
63 #include <MySensors.h>
64 
65 
66 #define CHILD_ID 99 // Id of the sensor child
67 
68 const byte eepromValid = 121; // If the first byte in eeprom is this then the data is valid.
69 
70 /*Pin definitions*/
71 const int programButton = 0; // (Digital 0) Record A New Knock button.
72 const int ledPin = 1; // (Digital 1) The LED pin (if any)
73 const int knockSensor =
74  5; // (Digital 5) for using the microphone digital output (tune knob to register knock)
75 const int audioOut =
76  2; // (Digital 2) for using the peizo as an output device. (Thing that goes beep.)
77 const int lockPin = 4; // (Digital 4) The pin that activates the relay/solenoid lock.
78 
79 /*Tuning constants. Changing the values below changes the behavior of the device.*/
80 int threshold =
81  3; // Minimum signal from the piezo to register as a knock. Higher = less sensitive. Typical values 1 - 10
82 const int rejectValue =
83  25; // If an individual knock is off by this percentage of a knock we don't unlock. Typical values 10-30
84 const int averageRejectValue =
85  15; // If the average timing of all the knocks is off by this percent we don't unlock. Typical values 5-20
86 const int knockFadeTime =
87  150; // Milliseconds we allow a knock to fade before we listen for another one. (Debounce timer.)
88 const int lockOperateTime =
89  2500; // Milliseconds that we operate the lock solenoid latch before releasing it.
90 const int maximumKnocks = 20; // Maximum number of knocks to listen for.
91 const int knockComplete =
92  1200; // Longest time to wait for a knock before we assume that it's finished. (milliseconds)
93 
94 byte secretCode[maximumKnocks] = {50, 25, 25, 50, 100, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Initial setup: "Shave and a Hair Cut, two bits."
95 int knockReadings[maximumKnocks]; // When someone knocks this array fills with the delays between knocks.
96 int knockSensorValue = 0; // Last reading of the knock sensor.
97 bool programModeActive = false; // True if we're trying to program a new knock.
98 
99 bool lockStatus;
100 
101 MyMessage lockMsg(CHILD_ID, V_LOCK_STATUS);
102 
103 void setup()
104 {
105 
106  pinMode(ledPin, OUTPUT);
107  pinMode(knockSensor, INPUT);
108  pinMode(lockPin, OUTPUT);
109  pinMode(programButton, INPUT);
110  digitalWrite(programButton, HIGH); // Enable internal pull up
111 
112  readSecretKnock(); // Load the secret knock (if any) from EEPROM.
113 
114  digitalWrite(lockPin,
115  HIGH); // Unlock the door for a bit when we power up. For system check and to allow a way in if the key is forgotten
116  delay(500); // Wait a short time
117 
118  lockStatus = loadState(0); // Read last lock status from eeprom
119  setLockState(lockStatus, true); // Now set the last known state and send it to controller
120 
121  delay(500); // This delay is here because the solenoid lock returning to place can otherwise trigger and inadvertent knock.
122 }
123 
125 {
126  sendSketchInfo("Secret Knock", "1.0");
127  present(CHILD_ID, S_LOCK);
128 }
129 
130 void loop()
131 {
132  // Listen for any knock at all.
133  knockSensorValue = digitalRead(knockSensor);
134  if (digitalRead(programButton) == LOW) { // is the program button pressed?
135  delay(100); // Cheap debounce.
136  if (digitalRead(programButton) == LOW) {
137  if (programModeActive == false) { // If we're not in programming mode, turn it on.
138  programModeActive = true; // Remember we're in programming mode.
139  digitalWrite(ledPin, HIGH); // Turn on the red light too so the user knows we're programming.
140  chirp(500, 1500); // And play a tone in case the user can't see the LED.
141  chirp(500, 1000);
142  } else { // If we are in programming mode, turn it off.
143  programModeActive = false;
144  digitalWrite(ledPin, LOW);
145  chirp(500, 1000); // Turn off the programming LED and play a sad note.
146  chirp(500, 1500);
147  delay(500);
148  }
149  while (digitalRead(programButton) == LOW) {
150  delay(10); // Hang around until the button is released.
151  }
152  }
153  delay(250); // Another cheap debounce. Longer because releasing the button can sometimes be sensed as a knock.
154  }
155 
156 
157  if (knockSensorValue == 0) {
158  if (programModeActive == true) { // Blink the LED when we sense a knock.
159  digitalWrite(ledPin, LOW);
160  } else {
161  digitalWrite(ledPin, HIGH);
162  }
163  knockDelay();
164  if (programModeActive == true) { // Un-blink the LED.
165  digitalWrite(ledPin, HIGH);
166  } else {
167  digitalWrite(ledPin, LOW);
168  }
169  listenToSecretKnock(); // We have our first knock. Go and see what other knocks are in store...
170  }
171 
172 }
173 
174 // Records the timing of knocks.
175 void listenToSecretKnock()
176 {
177  int i = 0;
178  // First reset the listening array.
179  for (i=0; i < maximumKnocks; i++) {
180  knockReadings[i] = 0;
181  }
182 
183  int currentKnockNumber = 0; // Position counter for the array.
184  uint32_t startTime = millis(); // Reference for when this knock started.
185  uint32_t now;
186 
187  do { // Listen for the next knock or wait for it to timeout.
188  knockSensorValue = digitalRead(knockSensor);
189 
190  if (knockSensorValue == 0) { // Here's another knock. Save the time between knocks.
191  Serial.println("knock");
192 
193  now=millis();
194  knockReadings[currentKnockNumber] = now - startTime;
195  currentKnockNumber ++;
196  startTime = now;
197 
198  if (programModeActive==true) { // Blink the LED when we sense a knock.
199  digitalWrite(ledPin, LOW);
200  } else {
201  digitalWrite(ledPin, HIGH);
202  }
203  knockDelay();
204  if (programModeActive == true) { // Un-blink the LED.
205  digitalWrite(ledPin, HIGH);
206  } else {
207  digitalWrite(ledPin, LOW);
208  }
209  }
210 
211  now = millis();
212 
213  // Stop listening if there are too many knocks or there is too much time between knocks.
214  } while ((now-startTime < knockComplete) && (currentKnockNumber < maximumKnocks));
215  Serial.println("end");
216 
217  //we've got our knock recorded, lets see if it's valid
218  if (programModeActive == false) { // Only do this if we're not recording a new knock.
219  if (validateKnock() == true) {
220  // Lock/unlock door
221  chirp(500, 1500); // And play a tone in case the user can't see the LED.
222  chirp(500, 1000);
223  setLockState(!lockStatus, true);
224  } else {
225  Serial.println("fail unlock");
226 
227  // knock is invalid. Blink the LED as a warning to others.
228  for (i=0; i < 4; i++) {
229  digitalWrite(ledPin, HIGH);
230  delay(50);
231  digitalWrite(ledPin, LOW);
232  delay(50);
233  }
234  }
235  } else { // If we're in programming mode we still validate the lock because it makes some numbers we need, we just don't do anything with the return.
236  validateKnock();
237  }
238 }
239 
240 
241 // Unlocks the door.
242 void setLockState(bool state, bool doSend)
243 {
244  if (state) {
245  Serial.println("open lock");
246  } else {
247  Serial.println("close lock");
248  }
249  if (doSend) {
250  send(lockMsg.set(state));
251  }
252 
253  digitalWrite(ledPin, state);
254  digitalWrite(lockPin, state);
255  saveState(0,state);
256  lockStatus = state;
257  delay(500); // This delay is here because releasing the latch can cause a vibration that will be sensed as a knock.
258 }
259 
260 // Checks to see if our knock matches the secret.
261 // Returns true if it's a good knock, false if it's not.
262 bool validateKnock()
263 {
264  int i = 0;
265 
266  int currentKnockCount = 0;
267  int secretKnockCount = 0;
268  int maxKnockInterval = 0; // We use this later to normalize the times.
269 
270  for (i=0; i<maximumKnocks; i++) {
271  if (knockReadings[i] > 0) {
272  currentKnockCount++;
273  }
274  if (secretCode[i] > 0) {
275  secretKnockCount++;
276  }
277 
278  if (knockReadings[i] > maxKnockInterval) { // Collect normalization data while we're looping.
279  maxKnockInterval = knockReadings[i];
280  }
281  }
282 
283  // If we're recording a new knock, save the info and get out of here.
284  if (programModeActive == true) {
285  for (i=0; i < maximumKnocks; i++) { // Normalize the time between knocks. (the longest time = 100)
286  secretCode[i] = map(knockReadings[i], 0, maxKnockInterval, 0, 100);
287  }
288  saveSecretKnock(); // save the result to EEPROM
289  programModeActive = false;
290  playbackKnock(maxKnockInterval);
291  return false;
292  }
293 
294  if (currentKnockCount !=
295  secretKnockCount) { // Easiest check first. If the number of knocks is wrong, don't unlock.
296  return false;
297  }
298 
299  /* Now we compare the relative intervals of our knocks, not the absolute time between them.
300  (ie: if you do the same pattern slow or fast it should still open the door.)
301  This makes it less picky, which while making it less secure can also make it
302  less of a pain to use if you're tempo is a little slow or fast.
303  */
304  int totaltimeDifferences = 0;
305  for (int timeDiff = 0, i=0; i < maximumKnocks; i++) { // Normalize the times
306  knockReadings[i]= map(knockReadings[i], 0, maxKnockInterval, 0, 100);
307  timeDiff = abs(knockReadings[i] - secretCode[i]);
308  if (timeDiff >
309  rejectValue) { // Individual value too far out of whack. No access for this knock!
310  return false;
311  }
312  totaltimeDifferences += timeDiff;
313  }
314  // It can also fail if the whole thing is too inaccurate.
315  if (totaltimeDifferences / secretKnockCount > averageRejectValue) {
316  return false;
317  }
318 
319  return true;
320 }
321 
322 
323 // reads the secret knock from EEPROM. (if any.)
324 void readSecretKnock()
325 {
326  byte reading;
327  reading = loadState(1);
328  if (reading == eepromValid) { // only read EEPROM if the signature byte is correct.
329  for (int i=0; i < maximumKnocks ; i++) {
330  secretCode[i] = loadState(i+2);
331  }
332  }
333 }
334 
335 
336 //saves a new pattern too eeprom
337 void saveSecretKnock()
338 {
339  saveState(1,
340  0); // clear out the signature. That way we know if we didn't finish the write successfully.
341  for (int i=0; i < maximumKnocks; i++) {
342  saveState(i+2, secretCode[i]);
343  }
344  saveState(1, eepromValid); // all good. Write the signature so we'll know it's all good.
345 }
346 
347 // Plays back the pattern of the knock in blinks and beeps
348 void playbackKnock(int maxKnockInterval)
349 {
350  digitalWrite(ledPin, LOW);
351  delay(1000);
352  digitalWrite(ledPin, HIGH);
353  chirp(200, 1800);
354  for (int i = 0; i < maximumKnocks ; i++) {
355  digitalWrite(ledPin, LOW);
356  // only turn it on if there's a delay
357  if (secretCode[i] > 0) {
358  delay(map(secretCode[i], 0, 100, 0,
359  maxKnockInterval)); // Expand the time back out to what it was. Roughly.
360  digitalWrite(ledPin, HIGH);
361  chirp(200, 1800);
362  }
363  }
364  digitalWrite(ledPin, LOW);
365 }
366 
367 // Deals with the knock delay thingy.
368 void knockDelay()
369 {
370  int itterations = (knockFadeTime /
371  20); // Wait for the peak to dissipate before listening to next one.
372  for (int i=0; i < itterations; i++) {
373  delay(10);
374  analogRead(
375  knockSensor); // This is done in an attempt to defuse the analog sensor's capacitor that will give false readings on high impedance sensors.
376  delay(10);
377  }
378 }
379 
380 // Plays a non-musical tone on the piezo.
381 // playTime = milliseconds to play the tone
382 // delayTime = time in microseconds between ticks. (smaller=higher pitch tone.)
383 void chirp(int playTime, int delayTime)
384 {
385  long loopTime = (playTime * 1000L) / delayTime;
386  pinMode(audioOut, OUTPUT);
387  for(int i=0; i < loopTime; i++) {
388  digitalWrite(audioOut, HIGH);
389  delayMicroseconds(delayTime);
390  digitalWrite(audioOut, LOW);
391  }
392  pinMode(audioOut, INPUT);
393 }
394 
395 
396 
397 void receive(const MyMessage &message)
398 {
399  // We only expect one type of message from controller. But we better check anyway.
400  if (message.getType()==V_LOCK_STATUS) {
401  // Change relay state
402  setLockState(message.getBool(), false);
403 
404  // Write some debug info
405  Serial.print("Incoming lock status:");
406  Serial.println(message.getBool());
407  }
408 }
409 
MyMessage is used to create, manipulate, send and read MySensors messages.
Definition: MyMessage.h:289
bool sendSketchInfo(const char *name, const char *version, const bool requestEcho=false)
API declaration for MySensors.
bool getBool(void) const
Get bool payload.
bool send(MyMessage &msg, const bool requestEcho=false)
bool present(const uint8_t sensorId, const mysensors_sensor_t sensorType, const char *description="", const bool requestEcho=false)
void setup()
Called after node initialises but before main loop.
void receive(const MyMessage &message)
Callback for incoming messages.
void presentation()
Node presentation.
#define LOW
Definition: bcm2835.h:574
uint8_t getType(void) const
Get message type.
#define HIGH
Definition: bcm2835.h:572
uint8_t loadState(const uint8_t pos)
MyMessage & set(const void *payload, const size_t length)
Set entire payload.
void loop()
Main loop.
void saveState(const uint8_t pos, const uint8_t value)