MySensors Library & Examples  2.3.2-36-g2e00bf6
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-2020 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 //#define MY_PJON
63 
64 #include <MySensors.h>
65 
66 
67 #define CHILD_ID 99 // Id of the sensor child
68 
69 const byte eepromValid = 121; // If the first byte in eeprom is this then the data is valid.
70 
71 /*Pin definitions*/
72 const int programButton = 0; // (Digital 0) Record A New Knock button.
73 const int ledPin = 1; // (Digital 1) The LED pin (if any)
74 const int knockSensor =
75  5; // (Digital 5) for using the microphone digital output (tune knob to register knock)
76 const int audioOut =
77  2; // (Digital 2) for using the peizo as an output device. (Thing that goes beep.)
78 const int lockPin = 4; // (Digital 4) The pin that activates the relay/solenoid lock.
79 
80 /*Tuning constants. Changing the values below changes the behavior of the device.*/
81 int threshold =
82  3; // Minimum signal from the piezo to register as a knock. Higher = less sensitive. Typical values 1 - 10
83 const int rejectValue =
84  25; // If an individual knock is off by this percentage of a knock we don't unlock. Typical values 10-30
85 const int averageRejectValue =
86  15; // If the average timing of all the knocks is off by this percent we don't unlock. Typical values 5-20
87 const int knockFadeTime =
88  150; // Milliseconds we allow a knock to fade before we listen for another one. (Debounce timer.)
89 const int lockOperateTime =
90  2500; // Milliseconds that we operate the lock solenoid latch before releasing it.
91 const int maximumKnocks = 20; // Maximum number of knocks to listen for.
92 const int knockComplete =
93  1200; // Longest time to wait for a knock before we assume that it's finished. (milliseconds)
94 
95 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."
96 int knockReadings[maximumKnocks]; // When someone knocks this array fills with the delays between knocks.
97 int knockSensorValue = 0; // Last reading of the knock sensor.
98 bool programModeActive = false; // True if we're trying to program a new knock.
99 
100 bool lockStatus;
101 
102 MyMessage lockMsg(CHILD_ID, V_LOCK_STATUS);
103 
104 void setup()
105 {
106 
107  pinMode(ledPin, OUTPUT);
108  pinMode(knockSensor, INPUT);
109  pinMode(lockPin, OUTPUT);
110  pinMode(programButton, INPUT);
111  digitalWrite(programButton, HIGH); // Enable internal pull up
112 
113  readSecretKnock(); // Load the secret knock (if any) from EEPROM.
114 
115  digitalWrite(lockPin,
116  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
117  delay(500); // Wait a short time
118 
119  lockStatus = loadState(0); // Read last lock status from eeprom
120  setLockState(lockStatus, true); // Now set the last known state and send it to controller
121 
122  delay(500); // This delay is here because the solenoid lock returning to place can otherwise trigger and inadvertent knock.
123 }
124 
126 {
127  sendSketchInfo("Secret Knock", "1.0");
128  present(CHILD_ID, S_LOCK);
129 }
130 
131 void loop()
132 {
133  // Listen for any knock at all.
134  knockSensorValue = digitalRead(knockSensor);
135  if (digitalRead(programButton) == LOW) { // is the program button pressed?
136  delay(100); // Cheap debounce.
137  if (digitalRead(programButton) == LOW) {
138  if (programModeActive == false) { // If we're not in programming mode, turn it on.
139  programModeActive = true; // Remember we're in programming mode.
140  digitalWrite(ledPin, HIGH); // Turn on the red light too so the user knows we're programming.
141  chirp(500, 1500); // And play a tone in case the user can't see the LED.
142  chirp(500, 1000);
143  } else { // If we are in programming mode, turn it off.
144  programModeActive = false;
145  digitalWrite(ledPin, LOW);
146  chirp(500, 1000); // Turn off the programming LED and play a sad note.
147  chirp(500, 1500);
148  delay(500);
149  }
150  while (digitalRead(programButton) == LOW) {
151  delay(10); // Hang around until the button is released.
152  }
153  }
154  delay(250); // Another cheap debounce. Longer because releasing the button can sometimes be sensed as a knock.
155  }
156 
157 
158  if (knockSensorValue == 0) {
159  if (programModeActive == true) { // Blink the LED when we sense a knock.
160  digitalWrite(ledPin, LOW);
161  } else {
162  digitalWrite(ledPin, HIGH);
163  }
164  knockDelay();
165  if (programModeActive == true) { // Un-blink the LED.
166  digitalWrite(ledPin, HIGH);
167  } else {
168  digitalWrite(ledPin, LOW);
169  }
170  listenToSecretKnock(); // We have our first knock. Go and see what other knocks are in store...
171  }
172 
173 }
174 
175 // Records the timing of knocks.
176 void listenToSecretKnock()
177 {
178  int i = 0;
179  // First reset the listening array.
180  for (i=0; i < maximumKnocks; i++) {
181  knockReadings[i] = 0;
182  }
183 
184  int currentKnockNumber = 0; // Position counter for the array.
185  uint32_t startTime = millis(); // Reference for when this knock started.
186  uint32_t now;
187 
188  do { // Listen for the next knock or wait for it to timeout.
189  knockSensorValue = digitalRead(knockSensor);
190 
191  if (knockSensorValue == 0) { // Here's another knock. Save the time between knocks.
192  Serial.println("knock");
193 
194  now=millis();
195  knockReadings[currentKnockNumber] = now - startTime;
196  currentKnockNumber ++;
197  startTime = now;
198 
199  if (programModeActive==true) { // Blink the LED when we sense a knock.
200  digitalWrite(ledPin, LOW);
201  } else {
202  digitalWrite(ledPin, HIGH);
203  }
204  knockDelay();
205  if (programModeActive == true) { // Un-blink the LED.
206  digitalWrite(ledPin, HIGH);
207  } else {
208  digitalWrite(ledPin, LOW);
209  }
210  }
211 
212  now = millis();
213 
214  // Stop listening if there are too many knocks or there is too much time between knocks.
215  } while ((now-startTime < knockComplete) && (currentKnockNumber < maximumKnocks));
216  Serial.println("end");
217 
218  //we've got our knock recorded, lets see if it's valid
219  if (programModeActive == false) { // Only do this if we're not recording a new knock.
220  if (validateKnock() == true) {
221  // Lock/unlock door
222  chirp(500, 1500); // And play a tone in case the user can't see the LED.
223  chirp(500, 1000);
224  setLockState(!lockStatus, true);
225  } else {
226  Serial.println("fail unlock");
227 
228  // knock is invalid. Blink the LED as a warning to others.
229  for (i=0; i < 4; i++) {
230  digitalWrite(ledPin, HIGH);
231  delay(50);
232  digitalWrite(ledPin, LOW);
233  delay(50);
234  }
235  }
236  } 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.
237  validateKnock();
238  }
239 }
240 
241 
242 // Unlocks the door.
243 void setLockState(bool state, bool doSend)
244 {
245  if (state) {
246  Serial.println("open lock");
247  } else {
248  Serial.println("close lock");
249  }
250  if (doSend) {
251  send(lockMsg.set(state));
252  }
253 
254  digitalWrite(ledPin, state);
255  digitalWrite(lockPin, state);
256  saveState(0,state);
257  lockStatus = state;
258  delay(500); // This delay is here because releasing the latch can cause a vibration that will be sensed as a knock.
259 }
260 
261 // Checks to see if our knock matches the secret.
262 // Returns true if it's a good knock, false if it's not.
263 bool validateKnock()
264 {
265  int i = 0;
266 
267  int currentKnockCount = 0;
268  int secretKnockCount = 0;
269  int maxKnockInterval = 0; // We use this later to normalize the times.
270 
271  for (i=0; i<maximumKnocks; i++) {
272  if (knockReadings[i] > 0) {
273  currentKnockCount++;
274  }
275  if (secretCode[i] > 0) {
276  secretKnockCount++;
277  }
278 
279  if (knockReadings[i] > maxKnockInterval) { // Collect normalization data while we're looping.
280  maxKnockInterval = knockReadings[i];
281  }
282  }
283 
284  // If we're recording a new knock, save the info and get out of here.
285  if (programModeActive == true) {
286  for (i=0; i < maximumKnocks; i++) { // Normalize the time between knocks. (the longest time = 100)
287  secretCode[i] = map(knockReadings[i], 0, maxKnockInterval, 0, 100);
288  }
289  saveSecretKnock(); // save the result to EEPROM
290  programModeActive = false;
291  playbackKnock(maxKnockInterval);
292  return false;
293  }
294 
295  if (currentKnockCount !=
296  secretKnockCount) { // Easiest check first. If the number of knocks is wrong, don't unlock.
297  return false;
298  }
299 
300  /* Now we compare the relative intervals of our knocks, not the absolute time between them.
301  (ie: if you do the same pattern slow or fast it should still open the door.)
302  This makes it less picky, which while making it less secure can also make it
303  less of a pain to use if you're tempo is a little slow or fast.
304  */
305  int totaltimeDifferences = 0;
306  for (int timeDiff = 0, i=0; i < maximumKnocks; i++) { // Normalize the times
307  knockReadings[i]= map(knockReadings[i], 0, maxKnockInterval, 0, 100);
308  timeDiff = abs(knockReadings[i] - secretCode[i]);
309  if (timeDiff >
310  rejectValue) { // Individual value too far out of whack. No access for this knock!
311  return false;
312  }
313  totaltimeDifferences += timeDiff;
314  }
315  // It can also fail if the whole thing is too inaccurate.
316  if (totaltimeDifferences / secretKnockCount > averageRejectValue) {
317  return false;
318  }
319 
320  return true;
321 }
322 
323 
324 // reads the secret knock from EEPROM. (if any.)
325 void readSecretKnock()
326 {
327  byte reading;
328  reading = loadState(1);
329  if (reading == eepromValid) { // only read EEPROM if the signature byte is correct.
330  for (int i=0; i < maximumKnocks ; i++) {
331  secretCode[i] = loadState(i+2);
332  }
333  }
334 }
335 
336 
337 //saves a new pattern too eeprom
338 void saveSecretKnock()
339 {
340  saveState(1,
341  0); // clear out the signature. That way we know if we didn't finish the write successfully.
342  for (int i=0; i < maximumKnocks; i++) {
343  saveState(i+2, secretCode[i]);
344  }
345  saveState(1, eepromValid); // all good. Write the signature so we'll know it's all good.
346 }
347 
348 // Plays back the pattern of the knock in blinks and beeps
349 void playbackKnock(int maxKnockInterval)
350 {
351  digitalWrite(ledPin, LOW);
352  delay(1000);
353  digitalWrite(ledPin, HIGH);
354  chirp(200, 1800);
355  for (int i = 0; i < maximumKnocks ; i++) {
356  digitalWrite(ledPin, LOW);
357  // only turn it on if there's a delay
358  if (secretCode[i] > 0) {
359  delay(map(secretCode[i], 0, 100, 0,
360  maxKnockInterval)); // Expand the time back out to what it was. Roughly.
361  digitalWrite(ledPin, HIGH);
362  chirp(200, 1800);
363  }
364  }
365  digitalWrite(ledPin, LOW);
366 }
367 
368 // Deals with the knock delay thingy.
369 void knockDelay()
370 {
371  int itterations = (knockFadeTime /
372  20); // Wait for the peak to dissipate before listening to next one.
373  for (int i=0; i < itterations; i++) {
374  delay(10);
375  analogRead(
376  knockSensor); // This is done in an attempt to defuse the analog sensor's capacitor that will give false readings on high impedance sensors.
377  delay(10);
378  }
379 }
380 
381 // Plays a non-musical tone on the piezo.
382 // playTime = milliseconds to play the tone
383 // delayTime = time in microseconds between ticks. (smaller=higher pitch tone.)
384 void chirp(int playTime, int delayTime)
385 {
386  long loopTime = (playTime * 1000L) / delayTime;
387  pinMode(audioOut, OUTPUT);
388  for(int i=0; i < loopTime; i++) {
389  digitalWrite(audioOut, HIGH);
390  delayMicroseconds(delayTime);
391  digitalWrite(audioOut, LOW);
392  }
393  pinMode(audioOut, INPUT);
394 }
395 
396 
397 
398 void receive(const MyMessage &message)
399 {
400  // We only expect one type of message from controller. But we better check anyway.
401  if (message.getType()==V_LOCK_STATUS) {
402  // Change relay state
403  setLockState(message.getBool(), false);
404 
405  // Write some debug info
406  Serial.print("Incoming lock status:");
407  Serial.println(message.getBool());
408  }
409 }
410 
HIGH
#define HIGH
Definition: bcm2835.h:572
sendSketchInfo
bool sendSketchInfo(const char *name, const char *version, const bool requestEcho=false)
receive
void receive(const MyMessage &message)
Callback for incoming messages.
Definition: SecretKnockSensor.ino:398
loop
void loop()
Main loop.
Definition: SecretKnockSensor.ino:131
LOW
#define LOW
Definition: bcm2835.h:574
loadState
uint8_t loadState(const uint8_t pos)
presentation
void presentation()
Node presentation.
Definition: SecretKnockSensor.ino:125
saveState
void saveState(const uint8_t pos, const uint8_t value)
MyMessage::set
MyMessage & set(const void *payload, const size_t length)
Set entire payload.
send
bool send(MyMessage &msg, const bool requestEcho=false)
MyMessage::getType
uint8_t getType(void) const
Get message type.
present
bool present(const uint8_t sensorId, const mysensors_sensor_t sensorType, const char *description="", const bool requestEcho=false)
setup
void setup()
Called after node initialises but before main loop.
Definition: SecretKnockSensor.ino:104
MySensors.h
API declaration for MySensors.
MyMessage::getBool
bool getBool(void) const
Get bool payload.
MyMessage
MyMessage is used to create, manipulate, send and read MySensors messages.
Definition: MyMessage.h:289