Author Topic: Genie Garage Door opener using Digispark Pro  (Read 376 times)

DaveFromRI

  • Newbie
  • *
  • Posts: 6
Genie Garage Door opener using Digispark Pro
« on: August 02, 2017, 09:33:58 am »
I could write some comments in here, but most of what I wanted to say are in the code comments, so without further wasting your time:

Code: [Select]
/*
 * 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.
 *
 * --------------------------------------------------------------
 * =    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 5 minutes (with a 20 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 5 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 5 minute mark (due to an obstruction), it will try again in 5 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 280000   // The timer value to warn that the door is about to close (4 minutes & 40 seconds here).
#define CLOSE_NOW 300000    // The timer value when to close the door (5 minutes here).
#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;
bool progMode = false;
bool doorDown = false;
bool openMode = false;
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;

    // 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;
        }
        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. Not coincidentally, the Genie garage door opener
  // (that my project controls) shuts off it's built-in light exactly at 4:40, which is intentional on my part.
  if (!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.
  if (!openMode and doorTimer > CLOSE_NOW and !doorDown) {
    triggerRelay();
    doorTimer = 0;
  }

  // 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();
    pinCode = "";
    progMode = false;
    openMode = false;
    doorTimer = 0;
    badCounter = 0;

    /*
    // If one needs to detect *which* transition the door has made, this code can detect it.
    if (doorDown) {
    // Door just closed
    playBeep(1,50);
    }
    else {
    // Door just opened
    playBeep(2,50);
    }
    */
  }
}

// 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);
  }
}

// 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;
}

NOTE: Edits made for comment grammatical corrections only.

<RANT>
Personal observation: Yea, I'm hijacking my own post, but I hope the moderators will be kind to me and not delete my contribution.
But I don't get it. Yea, I didn't know about this Digistump product until a friend showed it to me recently, but I find myself very surprised about the apparent lack of interest in this device. Why?
I have been current with similar devices such as the Raspberry Pi (Raspberry Pi 2B...or not 2B...that's the question  ;)), and I honestly found them very problematic, especially for a project like this. For example:
  • Serious overkill (HDMI video? Networking? Quad-core?)
  • The RPI is not as great as the hype. Novel? Yes. Cheap? Yes. But not the be-all and end-all.
  • Raspberry Pi's require an orderly shut-down or else files (or the file system) can be corrupted. Umm, power-outage???

Again, I don't get it! For $10 you've got a board that's easy to power (pretty much any current MicroUSB cellphone/tablet charger), lot's of IO pins, built-in EEProm, and decent enough flash memory and ram to do some really cool stuff.

Maybe I do get it...I'm "late to the party" as they say, which is sad, because I think this product should still be partying.
</RANT>
« Last Edit: August 03, 2017, 09:20:59 am by DaveFromRI »

DaveFromRI

  • Newbie
  • *
  • Posts: 6
Re: Genie Garage Door opener using Digispark Pro
« Reply #1 on: August 16, 2017, 05:28:07 am »
Version 2 of this project, even though I doubt anyone cares. But, perhaps in the future someone will find this thread and appreciate my admittedly limited effort.

Here is "Version 2", mostly program fixes where I didn't like the way I programmed it initially (as compared to true bug fixes). The code is well commented, so I'll only post the changes here:
- When auto-closing, any key-press on the outside keypad will reverse the door and open it (a safety feature).
- While beeping (warning) about auto-close, any key will stop the beeping (and subsequent timeout/closing). This
   will allow one to enter "OPEN" (or any valid code).
- The auto-close time is reduced from 5 minutes to 3. After using it a bit of time, 3 minutes is sufficient for me (with
   an increase from 20 seconds to 30 seconds warning beeping).
Code: [Select]
/*
 * 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 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.
 *
 * --------------------------------------------------------------
 * =    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 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
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 (!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.
  if (!openMode and doorTimer > CLOSE_NOW and !doorDown) {
    triggerRelay();
    doorTimer = 0;
    autoClose = true;
  }

  // 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();

    /*
    // If one needs to detect *which* transition the door has made, this code can detect it.
    if (doorDown) {
    // Door just closed
    playBeep(1,50);
    }
    else {
    // Door just opened
    playBeep(2,50);
    }
    */
  }
}

// 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;
}
« Last Edit: August 20, 2017, 06:04:28 am by DaveFromRI »

DaveFromRI

  • Newbie
  • *
  • Posts: 6
Re: Genie Garage Door opener using Digispark Pro
« Reply #2 on: August 19, 2017, 09:25:33 am »
In case anyone wants to consider a project like this, I'd like to publicly share an experience I had yesterday/today.

Yesterday I wanted to test the "Auto-reverse" safety feature of my door opener (nothing to do with this project), where the door opener itself should detect when too much force is required to proceed, as in a person or object blocking the door, and stop (or reverse). I placed my foot under the door (about 3 inches high, close to my ankle) and indeed the door reversed when hitting my foot, but...one of the upper-panel rollers came off the track. It was fairly easy to fix and it appeared to operate properly after that.

But this morning I found my circuit design/code giving it's 30-second warning that it was about to close the door, yet the door was closed. After doing what I needed to do in the garage, I closed the door and came in to examine the code (thinking I had made a coding error). While it's true that my code let's the "Door Open Timer" run continuously (even when the door is closed), other logical condition tests essentially ignore whatever the timer has run up to, or perhaps even wrapped around back to zero. I made a small change in the code that would constantly reset the timer if the door was closed, thinking that it would guarantee this lousy condition.

While uploading new code to the Digispark Pro, it was only then that I noticed that the magnet screwed into the garage door was pretty much gone, and only the plastic base plus 2 screws remaining. Apparently my "accident" yesterday snapped off the magnet and I didn't realize it.

It's not a real coding error, though I suppose one could argue something like "If the door is open for 3 minutes, try to shut it only once, and then quit." That may not be a perfect resolution because in my case if I shut the door with the hard-wired doorbell button, the Digispark won't know it, and it will, through the relay contacts, try to shut the door only once. Of course the door will truly open at that point, and then stay open. With my original code the door would keep cycling every 3 minutes, which IMO is better in that it will scare away potential thieves, and hopefully catch my (or a neighbor's) attention.

My thought process is that the code (v2) is still valid, but the hardware is at fault. Honestly, I was already apprehensive about using a "Normally Closed" magnetic switch (borrowed from spare parts of my home alarm system), for this exact reason. Consideration was given to the thought of the magnet breaking or the reed switch failing, but it's not really a common experience in the home-alarm system field.

Bottom line: The choice of hardware was a mistake, in that if it fails, the Digispark  thinks the door is stuck open and will keep trying to shut it. But if a different magnetic switch is used, one that is "Normally Open", it would mean that should the relay or magnet fail, the Digispark will think the door is closed, and will not perform any routines on it's own. One huge caveat: If the magnet should fall off and stick to the switch side, we're back at square one. However, there is not much metal in there (only glass, a reed switch, and some wires), so it's less likely.

I've ordered a replacement "Normally Open" reed switch to replace the one I have (https://www.amazon.com/gp/product/B012T6SO4Q). It's going to take a few weeks to get here, so in the mean time I'll just keep an eye on things.

In my code (v2), I think a global search & replace can be done to accommodate the new "Normally Open" switch:
Replace: doorSensor.isDown()
With: !doorSensor.isDown()

In hind-site, the code is poorly written to not consider this circumstance, but I'll fix it when I get my new switch and update the code here.

This site is (almost) fun! It's like having my own little blog where I an talk about my project, the work I'm doing, and experiences I'm having. Yea, it would be more fun if someone contributed something...
 

exeng

  • Sr. Member
  • ****
  • Posts: 441
Re: Genie Garage Door opener using Digispark Pro
« Reply #3 on: August 19, 2017, 10:17:40 am »
Dave,

I've enjoyed your postings. Keep it coming.

Awhile back I implemented a similar garage door opener using an Oak simply for the ability to send and receive status to and from the cloud (specifically Thinkspeak and Blynk). Both let my see door status from anywhere and Blynk allows me to control the door remotely (like from across the country).

I also incorporate and light sensor (does not distinguish daylight from garage light) so that I can vary the auto close... 4 hours at daylight and 15 minutes at night (unless lights are on). The light on state will need an enhancement. The four hour setting is because I'm often out and about in the yard working so I don't want the door to close as quickly as yours.

I also thought about the early warning but never implemented it, however within the home I have another Oak pulling time to close info from the cloud and sending it to a TFT display (along with other info such as pool temp and weather). Since I'm in and out of the garage frequently I considered a large wall display showing the time to close.

My doors are metal so I have a super magnet on the door that engages the NC (EDIT: Correction I believe it's actually a NO switch but will have to check the schematic to be sure) reed switch. Haven't lost it yet but your heads up on potential door confusion is helpful. I had considered a second sensor to sense the door fully open position but never implemented it.

Keep it coming. I know that there are others that have implemented similar door controls and hearing about all the different features / issues sparks (no pun intended) good ideas and safe implementations.
« Last Edit: August 19, 2017, 01:54:35 pm by exeng »

DaveFromRI

  • Newbie
  • *
  • Posts: 6
Re: Genie Garage Door opener using Digispark Pro
« Reply #4 on: August 20, 2017, 07:22:19 am »
OMG! Somebody is actually reading my stuff????  :o

Thank very much exeng for replying to my post, as honestly, it makes it less like I'm just wasting my time.

I would agree with you that for your needs, the auto-close feature changing time based on lighting is very appropriate. It wasn't something I considered (because we only use our garage for car parking), so unlike you, we don't have a need to keep it open. But your point is still valid, that using an available input (in my case the Digispark Pro), I could add a sensor that could extend the time as you've done. An argument could be made that your concept is better, that even in my case the daylight time is less likely to attract "undesirables". Whether one implements your version (variable) or mine (fixed) must include the safety and security of where you live, and how available the garage is to thieves. In my case, I'm in a city and the garage is VERY accessible to thieves.
Conversely, I thought my "OPEN" code concept idea was pretty clever to handle cases where we are cleaning out the garage or something.

I'm VERY jealous of your ability to send status to the cloud. Though I've got network access in the garage (my IP security camera router), it didn't seem worth the additional work, specifically because I was only replacing the original Genie board, and not trying to go with full Internet access. Your point about knowing the status is *really* relevant as if I'd implemented such I would have detected my hardware problem sooner. But, the broken magnet was truly my fault (not a part failure or coding failure), so I'll probably leave it as-is.

My doors are aluminum but there is lots of steel around (like the tracks the doors ride on). I'm sure you know companies make these types of switches for steel doors (only writing this so that others will be on the same page as you and I), so if one were to implement this concept, they would have to give your point (about having metal doors) consideration, and buy magnetic contact switches as appropriate.

Like you, I considered a second sensor for the door being fully open, and again like you, never implemented it. In fact, my recent "failure" only proves that the point that implementing such is pretty much unnecessary in that if the magnet comes off, neither switch will detect anything.

I've already coded for the new switch (version 3), but opted against coding for both a NC and NO switch. It's too much work and I really can't recommend a NC switch for the reasons referenced above about my mechanical failure.

However, I am adding new code to my auto-close logic to only attempt two retries, and then abort, versus my previous logic that would retry until infinity. Logic:

  • If the door is closed and the magnet has broken off, trigger the door (meaning open it) and see if the door has closed. If not, trigger one more time and then stop (closing the door). Because the door is normally left in this state by a human (either the inside door-bell button or the car remote), this can be reasonably assumed by the Digispark to be the default state, if unable to detect the state.
  • If the door is open, try two times and then abort. Without any sensory input, there's nothing the Digispark can do further.

In a perfect world I suppose I would have a magnet sensor for, let's say the door closed, plus an IR light-beam sensor to detect the door is open (two different mechanisms to determine the state), and that would certainly cover my situation where the magnet came off the door.

Or, I would tape black and white tape every 1/4th of an inch along the entire edge of the door, mount a reflective IR sensor to count the number of pulses, and therefore the code could detect the exact position of the door...unless the sensor got knocked off.

The above is just a goofy "overkill" comment to reflect what you and I have already figured out: There is reasonable hardware & coding, and then there's overkill.

Like me, you've opted to not go the overkill route, and I agree with your position.

Thank you SO MUCH for responding!!!!!!!!!!!!!!!!!

DaveFromRI

  • Newbie
  • *
  • Posts: 6
Re: Genie Garage Door opener using Digispark Pro
« Reply #5 on: September 18, 2017, 12:32:52 pm »
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!

Code: [Select]
/*
 * 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;
}