I really over-thought this whole concept of the proper use of a normally open or normally closed switches. It doesn't matter. Any magnetic contact switch you install to detect when the door is closed is going to report the door is open if the magnet is gone, end of story, no more thinking about it. The proper solution would be to mount two switches to detect both the open and closed position, each having their own magnet (discussed previously in this thread). This would give the the Digispark the best-effort chance of recovering after a catastrophic failure. But because my mounting locations don't provide for easy placement of an door-open sensing switch, I'm not going to bother.
So, below is Version 3. Enjoy!
/*
* Garage_Door_Too, a Garage Door Opener specifically to utilize the 'Genie" keypad but add to it's functionality.
* by DaveFromRI, first release: August, 2017.
* VERSION 3 - Modified auto-close function to only auto-close 2 times, and then stop, as there is no sense to try
* until infinity. If the door was left closed (as visually seen by the owner), but the sensor has
* failed, opening the door and then closing it again won't cause any harm. On the other-hand, if the
* door has been left open and despite two attempts to close it the sensor is not responding, there's
* nothing else to do. No key-press will clear this, only the sensor detecting the door closed.
* VERSION 2 - Safety feature added: During auto-close any key pressed will reverse the door back up.
* Additionally, any keypress during the auto-close warning will reset the door-open timer.
* VERSION 1 - First release.
*
* --------------------------------------------------------------
* = Licensed under the CC-GNU GPL version 2.0 or later =
* = http://creativecommons.org/licenses/GPL/2.0/ =
* --------------------------------------------------------------
*
* The name "Garage Door Too" came from the fact that I found at least one other project similar to mine, and
* I wanted to differentiate between the two. Our projects are different, but complimentary, which is good.
*
*
* NOTES:
* 1) This project is intended to be used with the DigiStump "Digispark Pro" product:
* (http://digistump.com/category/19)
* Granted several years old, but a friend of mine had bought two way back, couldn't figure them out, and asked me
* if I could help (so I did).
*
* 2) Besides a standard Micro-USB charger for power, this design requires several accessory components:
* a) A relay board with an embedded on-board driver transistor (or equivalent).
* b) A piezo electric element (the 'element' only, no driver circuitry...the Digispark Pro can do the job).
* (https://www.amazon.com/Adafruit-Buzzer-5V-Breadboard-friendly/dp/B00SK8NHZ4)
* c) A wired door sensor switch to be attached to the garage door. Similar to used in security alarm systems,
* it should be a "Normally Closed" magnetic contact. My code can easily be changed for a 'Normally Open' if necessary.
* (https://www.amazon.com/uxcell-Window-Sensor-Magnetic-Recessed/dp/B00HR8CT8E)
* d) A 14-pin IC socket
* (https://www.amazon.com/SODIAL-2-54mm-Sockets-Solder-Adaptors/dp/B00ZE9V074)
*
* 3) I never got around to taking a picture of the PCB, but here's the Digispark Pro wiring (see attached picture for reference):
* a) Yellow digital pins 6-12 are for keypad in (a 3x4 matrix). Because the keypad comes with a ribbon cable with
* protruding wires, I simply took an old IC socket, cut off one side, and soldered it directly to the Digispark Pro.
* If you're not sure which way to plug in the notched keypad cable, please see the notes in the code.
* b) Shared with the on-board LED, the external relay board is tied to yellow digital pin 1. This was not an oversight;
* I wanted to be able to test if the on-board LED would turn on as programmed, without needing a relay.
* c) The Piezo element positive lead is connected to the yellow Digispark Pro digital pin 0, and negative to ground.
* In the interest of full disclosure, I didn't buy the one in the link, but ripped it out of a broken blood-pressure machine.
* d) Lastly, the door switch is connected to the yellow Digispark Pro digital pin 2. REMINDER: This switch contact &
* magnet must be mounted to the garage door and frame so that when the door is closed, they match together.
* Like the piezo, the other wire of this switch contact goes to the Digispark Pro ground.
*
* FEATURES:
* 1) Easy programming: There is no need to remove some hard-to-get-off cover and locate a dark & hard to find switch,
* just punch in "PROG" and then your new 4-digit code. Done! Forgot your code? Open the door with a key and program it!
* 2) My code is currently set to close the door automatically after 3 minutes (with a 30 second warning). But what if you're
* working in the garage and need the door to stay open? Easy! Just press "OPEN", and it won't close on you!
* Yes, the 3 minute auto-close is because more than once I forgot to close the door and drove away. Also, either
* entering the correct code *or* closing the door resets this feature.
* 3) If the garage door fails to close at the 3 minute mark (due to an obstruction), it will try again in 3 minutes.
* 4) Both of the two above reserved PIN codes (7837 & 8874) will only work when the door is open, and are disallowed
* when programming your own PIN code (you'll hear a nasty beep).
* 5) After 3 failed attempts to enter an open code, the unit will start beeping (to alert you that someone has been
* tampering). Either entering the correct code or opening the door (with a key?) will shut off the annoying beeping.
*
* When someone enters the wrong code too many times, it was a tough decision on how to handle it. Most people prefer
* to lock out the "hacker" for a minute or so, but honestly, in my MANY years of having a garage, it has never happened.
* But to *not* code for it would be foolish, so I opted for the "Beep Constantly" option, specifically so that if an
* authorized person accidentally hits the wrong code, the correct code they can quickly silence the warning and open the door.
*
* A lot more thought went into this project than one might expect. For example, some code I've seen allows multiple
* access codes, but in my lifetime (quite long), the only time I've had to 'share' access with my garage was someone
* whom I absolute trust. So why bother? For the same reason, I abandoned the thought of a "One-Time-Open" code.
*
* Another feature I opted against (like both the original Genie device & another project I found), a user cannot press
* any key (within so many seconds) on the keypad to act as a "Close now" button. IMO, I see two problems with this:
* 1) The keypad DOES wear (I've already had to replace it twice). Either install a doorbell button on the inside or
* force the user to type in the 4-digit pin. These keypads are too expensive to waste on frivolous presses.
* 2) Very intentionally, I use the "Door open" state to allow for reprogramming or telling the logic to stay open.
* I think this concept is more user friendly, as most people have doorbell buttons plus wireless remotes in their car.
*
* As much as reasonable, I added lots of comments to help people, so...well...I hope they do.
*
* Thanks for looking at my very first public release of Arduino code.
*
*/
// Kind of annoying how you get this non-fatal warning when compiling. Just ignore it:
// WARNING: Category '' in library EEPROM is not valid. Setting to 'Uncategorized'
#include <EEPROM.h>
#include <Keypad.h>
#include <DebouncedSwitch.h>
#include <elapsedMillis.h>
#define PIEZO_PIN 0 // Piezo electric element (no driver circuity needed).
#define RELAY_PIN 1 // The relay output pin, shared with the LED for testing purposes.
#define INPUT_PIN 2 // The normally-closed magnetic door switch (closed when door is closed).
#define PIEZO_FREQ 2000 // A value to adjust to match the frequency of the piezo device.
// NOTE: I had to tweak the frequency for maximum volume. Your piezo may tweaking.
#define RELAY_DELAY 500 // Number of milliseconds to hold relay energized.
#define KEY_TIMEOUT 10000 // Number of milliseconds before keys are erased.
#define ROWS 4 // Keypad has four rows.
#define COLS 3 // Keypad has three columns.
#define RELAY_ON HIGH // A tad unnecessary, but incase a given relay board operates on opposite logic.
#define RELAY_OFF LOW // " " " " " "
#define CLOSE_SOON 150000 // The timer value to warn that the door is about to close (2 minutes, 30 seconds).
#define CLOSE_NOW 180000 // The timer value when to close the door (3 minutes here).
#define CLOSE_COUNT 2 // Number of times to try to auto-close before aborting.
#define OPEN_CODE "7837" // Reserved code for locking the door open (spells out "OPEN").
#define PROG_CODE "8874" // Reserved code for programming a new code (spells out "PROG").
#define DEFAULT_PIN "1234" // Upon first running of the code, the EEPROM is programmed with this 4-digit string.
#define FAILED_ATTEMPTS 3 // How many times a person can enter the wrong code before it starts beeping like crazy.
#define E_TEST_CODE 245 // The hex code of A5 (binary 10100101) to see if the EEPROM is already programmed.
// Admittedly a pretty lazy 'check', but A5 is not an ASCII character and it's binary
// pattern is relatively unlikely to come up at exactly EEPROM address 5.
// Define the Keymap
char keys[ROWS][COLS] = {
{'1', '2', '3'},
{'4', '5', '6'},
{'7', '8', '9'},
{'*', '0', 'z'}
};
// This definition is for when the GENIE keypad is plugged in notch down (away from USB).
byte rowPins[ROWS] = { 7, 8, 11, 12};
byte colPins[COLS] = { 6, 9, 10};
/*
// This definition is for when the GENIE keypad is plugged in knotch up (towards USB).
byte rowPins[ROWS] = { 11, 10, 7, 6};
byte colPins[COLS] = { 12, 9, 8};
*/
// Create the keypad
Keypad kpd = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
// Set up to debounce the magnetic door sensor
DebouncedSwitch doorSensor(INPUT_PIN);
// Set up two timers for they keypad and door
elapsedMillis keyTimer;
elapsedMillis doorTimer;
// Define some global variables and set them to an intial state
byte badCounter = 0; // Counter for the number of times a bad pin has been entered
byte closeCounter = 0; // Counter for the number of times auto-close has been attempted
bool progMode = false; // Indicator that we've entered program mode
bool doorDown = false; // A mirror of the actual door state, used to detect new state if needed
bool openMode = false; // Indicatator that "Open Mode" is active
bool autoClose = false; // Indicatator that we've started the auto-close process
String pinCode = ""; // Variable to hold Pin Code (4 digits)
// As the routine is self-described, set up the keyboard, our timers, pin configuration, & door state.
void setup() {
kpd.setHoldTime(1000);
kpd.setDebounceTime(30);
keyTimer = 0;
doorTimer = 0;
pinMode(RELAY_PIN, OUTPUT);
digitalWrite(RELAY_PIN, RELAY_OFF);
pinMode(PIEZO_PIN, OUTPUT);
if (doorSensor.isDown()) doorDown = true;
}
// The main program loop to check the keypad, door status, and timer events
void loop() {
char key = kpd.getKey();
// Check for a valid key.
if (key) {
keyTimer = 0;
// New logic feature in version 2. If door is beeping for auto-close, any key stops it and
// will reset the door timer, but valid codes (like OPEN & PROG) still work.
if (doorTimer > CLOSE_SOON and !openMode and !doorDown and !autoClose) doorTimer = 0;
// New safety feature in version 2. If the door is auto-closing and any key is pressed
// on the keypad, stop the door and then reverse it back up.
if (autoClose) {
triggerRelay();
clearVars();
key = '*'; //Fill this with the clear key just to satisfy the remaining tests
}
// Use the star key as a "Clear" function, to mimize mistaken entries.
if (key == '*') {
// Clear pinCode and program mode
pinCode = "";
progMode = false;
}
// Some other key pressed besides "*", so process it
else {
// Accumulate entered keys until we have 4. The "if" is probably not necessary because
// one can't press keys faster than this loop, but in the event the code is modified in
// some way (like keybounces become a problem), it's a nice, short, safeguard.
if (pinCode.length() < 4) pinCode += key;
// Four digits entered, so let's figure out what the user entered.
if (pinCode.length() == 4) {
// If the "program" code was entered and the door is not down, enter program mode.
if (progMode and !doorDown) {
// First make sure the user isn't trying to use one of the two reserved codes.
if (pinCode == OPEN_CODE or pinCode == PROG_CODE) {
// Invalid pin code so make a nasty beep.
for (int i = 0; i < 80; i++) playBeep(1, 5);
}
else {
// We are in program mode and a valid set of 4 digits were entered, so let's save them.
// Beep four short beeps to let the user know they were successful.
readPin(pinCode);
playBeep(4, 100);
}
progMode = false;
}
// If the door is open and the user entered the program code, enter reprogram mode
else if (pinCode == PROG_CODE and !doorDown and !progMode) {
// Beep two short beeps to let the user know that they've entered program mode.
playBeep(2, 100);
progMode = true;
}
// Check to see if the user has set open mode (the door stays open with no timer)
else if (pinCode == OPEN_CODE and !doorDown and !progMode) {
// Beep two long beeps to let the user know they've entered door open mode.
openMode = true;
playBeep(2, 500);
}
// If the correct pin was entered, open or close the door.
else if (readPin("") == pinCode and !progMode) {
triggerRelay();
badCounter = 0;
clearVars();
}
else {
// The only way we could get here is if an invalid code was entered, so let's count
// how often this happens.
badCounter++;
}
pinCode = ""; // Clear pinCode for next set of 4 characters
}
}
}
else {
// If no key is pressed after too long, clear all stored keys. Also cancel program mode if timeout.
if ((pinCode != "" or progMode) and keyTimer > KEY_TIMEOUT) {
pinCode = "";
progMode = false;
}
}
// If the door is about to close, start beeping.
if (closeCounter < CLOSE_COUNT and !openMode and doorTimer > CLOSE_SOON and !doorDown) playBeep(1, 30);
// If the door is open for more than the set limit, shut it. Reset the timer so that in the event it
// doesn't close (like it hits an object), it can try yet after the same duration until successful.
// However, only try so many times (CLOSE_COUNT) because there is little sense to repeat indefinitely.
if (closeCounter < CLOSE_COUNT and !openMode and doorTimer > CLOSE_NOW and !doorDown) {
triggerRelay();
doorTimer = 0;
autoClose = true;
// Increment 'closeCounter' to make sure we only don't perform this function until infinity.
closeCounter++;
}
// If the door is closed and the wrong code is entered 3 times, start beeping until the door opens
// or the correct code is entered. We could lock out the keypad for some duration, but honestly,
// there is little evidence of people "playing" with garage door keypads trying to guess codes.
// Besides, this code alerts the owner (by beeping) that someone has made a failed attempt.
if (doorDown and badCounter >= FAILED_ATTEMPTS) playBeep(1, 50);
// Debounce the door sense switch and set the state of our boolean variable, doorDown to reflect
// the current door position.
doorSensor.update();
if (doorSensor.isDown() != doorDown) {
// The door changed state, so let's store the new state and clear out some important variables
doorDown = doorSensor.isDown();
clearVars();
// Unlike 'clearVars', the counter 'closeCounter' can ONLY be cleared when the sensor has
// detected that the door sensor is actually working (versus a hardware failure). So if the
// door is (finally) down, reset this counter because clearly the sensor is working.
if (doorDown) closeCounter = 0;
}
// End of main loop
}
// Play a beep sound, sending the number of beeps and the duration (in milliseconds)
void playBeep(int repeat, int duration) {
for (int i = 0; i < repeat; i++) {
tone(PIEZO_PIN, PIEZO_FREQ, duration);
delay(duration * 1.30);
}
}
void clearVars() {
pinCode = "";
progMode = false;
openMode = false;
autoClose = false;
doorTimer = 0;
badCounter = 0;
}
// Trigger the relay for the desired amount of time
void triggerRelay() {
digitalWrite(RELAY_PIN, RELAY_ON);
delay(RELAY_DELAY);
digitalWrite(RELAY_PIN, RELAY_OFF);
}
// Read the pin code, set a pin code, or if a new device, create a 'Default' pin code "1234". It's pretty
// redundant to check EVERY TIME if the memory is programmed properly, but two functions combined into one
// seems to make more sense. After all, it's just a simple (non-destructive) eeprom read, so why care?
String readPin(String newCode) {
// initialize string variables
char pinChar; //Used to read individual characters from EEPROM
String storedCode; // Used to hold the memorized code
// Use E_TEST_CODE to signal memory already programmed
if (EEPROM.read(4) == E_TEST_CODE) {
// This is a lazy way of detecting whether to program a new code or not. If something was
// passed to this routine and NOT 4 digits, then just return the stored 4-digit code.
if (newCode.length() != 4) {
for (int i = 0; i < 4; i++) {
pinChar = EEPROM.read(i);
storedCode += pinChar;
}
}
// The only way to get here is if 4 digits were passed, so let's program them into the eeprom.
else {
for (int i = 0; i < 4; i++) {
// Very intentionally we use 'put', as it's less destructive by checking the contents first.
EEPROM.put(i, newCode.charAt(i));
}
storedCode = newCode;
}
}
else {
// The only way to get here is if our EEPROM test code is not already in address 4 of the
// eeprom, so let's store it now plus the default..
EEPROM.put(4, E_TEST_CODE);
storedCode = DEFAULT_PIN;
for (int i = 0; i < 4; i++) {
EEPROM.put(i, storedCode.charAt(i));
}
}
// Whether we read the eeprom or set the default, return the value.
return storedCode;
}