Introduction: Card Reading Shop Power Switch

About: I like to build random things, some of which are presented here on Instructables.

This box reads any card with a magnet strip and, if you're on the registered personnel list, turn on the shop power via a relay. A sole person, to be added to the program file upon creation, has the power to add others to this list for the utmost security.

Step 1: Hardware

This is the list of hardware I ended up using, but it can be trimmed, pruned, or changed to fit your needs.

1 @ 8x8x4 steel box

1 @ Arduino Uno

1 @ USB Host shield

1 @ Adafruit DataLogger Shield

1 @ 4G SD Card

1 @ Magnetic Card Reader

1 @ 20x4 LCD Screen

1 @ 12VDC Power Supply

1 @ 120VAC Relay

1 @ Large 120VAC 20mA LED

1 @ 240VAC 3A Button

1@ 10A Key Switch

1 @ TIP120 Transistor

3 @ 1K Resistors

The LCD, button, key switch, LED, and card reader are mounted on the face of the box. The Arduino, relay, and power supply are mounted on an acrylic sheet, which is mounted to the back of the box on stand-offs. The hardware wired to the Arduino is attached with unpluggable connectors for ease of serviceability. The two shields are stacked up on the Arduino.

Step 2: Software

This is a lot of code.

This first section contains the libraries and global variables that are used to run the program.

#include <hidboot.h> /*handles the card reader*/
#include <spi.h> /*Serial for the mag Reader and SD Shield */
#include <sd.h> /*library to handle the SD SHield*/
#include <wire.h> /*I2c Library for Real Time Clock (RTC)*/
#include "RTClib.h" /*RTC library*/
#include <liquidcrystal_i2c.h> /*LCD Display library*/

/*various variables*/
String readMag; /*string to store car info*/
String checkName = ""; /*buffer for reading names from List file*/
unsigned long currentMillis = 0; /*handles timeout for card reader to reset readMag after entire card is read*/
long previousMillis = 0; /*part of currentMillis situation for the card reading code*/
long pMillis = 0; /*used for loop code to indicate "ready" or "wait" for user to waait for card reader to reset*/

/*various pins for the different hardware  Special note: USB Shield chip selecter is 10
                SD Shield chip selecter is 4
  Turn them low to activate and high to deactivate
*/
const byte relayPin = 3;
const byte keyInput = 5;
const byte GreenLED = 7;
const byte button = 6;

/*various state recording variables*/
boolean powerState = false; /*tool power is off at start*/
boolean keyState = false; /*key is off*/
boolean buttonState = false; /*button is off*/
boolean swipeState = false; /*no one has swiped yet*/
boolean once = false; /*only lets the lcd print the swiped card and time once rather than indeffinitely*/
boolean adminSwipe = false; /*handles admin adding cards*/
boolean timer = false; /*records state for*/

/*instnaces for the magReader*/
USB     Usb;
HIDBoot<usb_hid_protocol_keyboard>    HidKeyboard(&Usb);

/* instance for the SD file handler*/
File openedFile;

/* instance to RTC*/
RTC_PCF8523 rtc;

/* LCD instance */
LiquidCrystal_I2C lcd(0x27, 20, 4);

/*Class for the magReader*/
class KbdRptParser : public KeyboardReportParser {
  protected:
    void OnKeyDown	(uint8_t mod, uint8_t key);
    void OnKeyPressed(uint8_t key);
};
/*instance of the magReader class*/
KbdRptParser Prs;

This next section is the setup that initiates all of the variables and starts all the different serial communications, and the Class header and cpp code for the magReader. The thing to note is in the MagReader class cpp is where most of the action happens when a card is swiped. This is also where the Admin's first 15 characters of their card are stored.

The logic for manipulating the swiped card's data down below is as follows: *****

If the button is pressed and the Admin swiped then activate Admin mode so the next swiped card is added to the list of registered people. Else if admin mode has been activated go to WriteToList to add this new person's card. If neither of those then go to CheckList to see if this new swipe is on the list.

void setup() {
  Serial.begin( 115200 );

  /*start usb mag Reader*/
  digitalWrite(10, LOW);
  if (Usb.Init() == -1)
    Serial.println(F("OSC did not start."));
  ( 200 );
  HidKeyboard.SetReportParser(0, &Prs);
  digitalWrite(10, HIGH);

  /*start RTC */
  rtc.begin();
  
  if (! rtc.initialized()) {
    Serial.println(F("RTC is NOT running!"));
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); /*saves the time and date of your pc to rtc*/
  }

  /*start sd shield*/
  digitalWrite(4, HIGH);
  if (!SD.begin(4)) {
    Serial.println(F("initialization failed!"));
    lcd.print("init Failed");
    return;
  }
  digitalWrite(4, HIGH);
  digitalWrite(10, LOW);

  /*starts lcd display*/
  lcd.begin();
  lcd.backlight();

  /*pinmodes for the various hardware*/
  pinMode(button, INPUT);
  pinMode(keyInput, INPUT);
  pinMode(GreenLED, OUTPUT);
  pinMode(relayPin, OUTPUT);
}

//--------------------Reading card-------------------------------------------//
void KbdRptParser::OnKeyDown(uint8_t mod, uint8_t key) {
  //        Serial.print("DN ");
  // PrintKey(mod, key);
  uint8_t c = OemToAscii(mod, key);
  if (c)
    OnKeyPressed(c);
}
void KbdRptParser::OnKeyPressed(uint8_t key) {
  timer = true; /*card has been swiped, saved for lcd indication timeout*/
  /*timeout for card reader to reset readMag after larger cards have been read*/
  currentMillis = millis();
  if (currentMillis - previousMillis > 17000) {  //if it has been more than 1 second clear variable
    previousMillis = currentMillis;
    readMag = "";
  }
  char theChar; //buffer to store ascii value from reader
  theChar  = ((char)key); //pass ascii value from usbhost to variable

  /*this prevents the card reader from reading any more than the required characters*/
  if (readMag.length() < 16) {
    readMag += theChar;  // assemble the ascii into a string
  }

  /*if readMag is 15 characters long do the stuff*/
  if (readMag.length() == 15) {
    /*Switch communication from USB Shield to SD Shield*/
    digitalWrite(10, HIGH);
    digitalWrite(4, LOW);
    //    Serial.println(readMag);
    
    /*if the button is pressed during a swipe then activate admin mode and indicate on lcd*/
    *****
    if (digitalRead(button) == HIGH && readMag == /*Admin's card info*/(F("first_15_Characters_Of_Your_Admin_ID"))) {  
      adminSwipe = true;
      lcd.setCursor(0, 1);
      lcd.print(F("ADMIN ACCESS    "));
      lcd.setCursor(0, 2);
      lcd.print(F("               "));
      lcd.setCursor(0, 3);
      lcd.print(F("                 "));
    } else if (adminSwipe) {
      WriteToList(readMag); /*go to WriteToList method with swiped card info if in admin mode*/
    } else {
      checkList(readMag); /*check to see if you are on the list*/
    }   

    /*switch communication from SD Shield to USB Shield*/
    digitalWrite(4, HIGH);
    digitalWrite(10, LOW);
    return;
  }
}

The loop method monitors the user inputs. Usb.Task() watches the card reader. The timer controls the "READY" or "WAIT" indications on the LCD for when the card reader is busy. The rest monitors if the button is pressed and if a card turns tool power on(cardState = true) or if the key turns tool power on(keyState = true). I used this cascading boolean state logic so that either the key or card reader can activate the powerState, but whenever the powerState is true then power is on. Whenever either the swipeState or the keyState is true, the Arduino is constantly checking the time, and whenever it reaches 5:15 pm it turns the power off. If the button is pressed then power is turned off.

void loop() {

  /*handle the card reader*/
  Usb.Task();

  /*if a card has been swiped start the indication timer.  Run for 20 seconds indicating "wait",
    when the timer runs out record the state and indicate "ready"
  */
  if (timer) {
    if (millis() - pMillis > 20000) {
      pMillis = millis();
      timer = false;
    } else {
      lcd.setCursor(15, 0);
      lcd.print(F("WAIT "));
    }
  } else {
    lcd.setCursor(15, 0);
    lcd.print(F("READY"));
  }

  /*Do all the stuff if the button is not pressed*/
  if (!buttonState) {
    /*if the key is turned on turn power on*/
    if (digitalRead(keyInput) == HIGH) {
      keyState = true;
    } else {
      keyState = false;
      buttonState = false;
      powerState = false;
    }

    /*if either the key is on or someone on the list swiped, the power is on.
       Record the time someone swiped and turned power on
    */
    if (keyState || swipeState) {
      DateTime now = rtc.now();
      powerState = true;
      //      if (keyState && once) {
      //        lcd.setCursor(0, 1);
      //        lcd.print(F("KEY POWERED AT "));
      //        lcd.print(String(now.hour()));
      //        lcd.print(F(":"));
      //        lcd.print(String(now.minute()));
      //        lcd.setCursor(0, 2);
      //        lcd.print(F("               "));
      //        once = false;
      if (swipeState && once) {
        lcd.setCursor(0, 1);
        lcd.print(readMag);
        lcd.print(F("     "));
        lcd.setCursor(0, 2);
        lcd.print(F("SWIPED AT "));
        lcd.print(String(now.hour()));
        lcd.print(F(":"));
        lcd.print(String(now.minute()));
        once = false;
      }
      if (now.hour() == 17 && now.minute() == 15) {
        swipeState = false;
        keyState = false;
        powerState = false;
        lcd.setCursor(0,3);
        lcd.print(F("AUTO POWER OFF   "));
      }
    }

    /*If the button is pressed turn power off, record and indicate time, and reset states*/
    if (digitalRead(button) == HIGH) {
      buttonState = true;
      keyState = false;
      swipeState = false;
      powerState = false;
      DateTime rn = rtc.now();
      lcd.setCursor(0, 3);
      lcd.print("POWERED OFF ");
      lcd.print(rn.hour());
      lcd.print(":");
      lcd.print(rn.minute());
    }

    /*If the power is on, turn LED and relay on and indicate on lcd.  
      Else do the opposite of all that
    */
    if (powerState) {
      digitalWrite(GreenLED, HIGH);
      digitalWrite(relayPin, HIGH);
      lcd.setCursor(0, 0);
      lcd.print(F("TOOL POWER ON "));
      lcd.setCursor(0, 3);
      lcd.print(F("                    "));
    } else {
      digitalWrite(GreenLED, LOW);
      digitalWrite(relayPin, LOW);
      lcd.setCursor(0, 0);
      lcd.print(F("TOOL POWER OFF"));
    }
  }

The WriteToLog method takes the recently read card's info and writes it, the date, and the time to the SwipeLog file, creating a running log of whoever turns tool power on with their card. The benefit of using an SD card to store the log and the list of registered people is that if the power goes out in the building the data is preserved on the card. That and we can have much larger logs and lists on the SD card than stored on the Arduino itself.

/*This method writes the card info, time, and date to the SwipeLog file on the SD card*/
void WriteToLog(String Name) {
  DateTime now = rtc.now();
  openedFile.close();
  openedFile = SD.open(F("SwipeLog.txt"), FILE_WRITE);
  if (openedFile) {
    Serial.print(F("here?"));
    openedFile.print(Name);
    openedFile.print(',');
    openedFile.print(now.month(), DEC);
    openedFile.print('/');
    openedFile.print(now.day(), DEC);
    openedFile.print('/');
    openedFile.print(now.year(), DEC);
    openedFile.print(',');
    openedFile.print(now.hour(), DEC);
    openedFile.print(':');
    openedFile.print(now.minute(), DEC);
    openedFile.print(':');
    openedFile.print(now.second(), DEC);
    openedFile.println();
  }
  openedFile.close();
}

The checkList method takes the recently read card's info and compares it to the list of names in the List file. The file.read() command only reads one byte at a time so checkName is a buffer that stores the characters until it reaches 17, trims the carriage return, and compares to the newly read card. If there is a match then it writes the card to the log file in WriteToLog and activates swipeState to turn tool power on. There are three different checkName = ""; to ensure each name in the list file is only one name and not a combination of two. If there is no match then it indicates so on the LCD.

/*This method iterated through the List file and compares the swiped card with each in the list
  If one matches then power state is on and WriteToLog, else indicate you are not on the list
*/
void checkList(String Name) {
  openedFile.close();
  openedFile = SD.open(F("List.txt"));
  if (openedFile) {
    while (openedFile.available()) {
      /*only reads one character at a time, store wach in a string and when that reaches the return character
        in the file trim it and compare to swiped card info*/
      char temp = char(openedFile.read());
      checkName += temp;
      if (checkName.length() == 17) {
        checkName.trim();
        if (Name.equals(checkName)) {
          swipeState = true;
          once = true;
          WriteToLog(readMag);
          checkName = "";
          return;
        }
        checkName = "";
      }
    }
    checkName = "";
  }
  lcd.setCursor(0, 1);
  lcd.print(F("NOT ON LIST      "));
  lcd.setCursor(0, 2);
  lcd.print(F("                 "));
}

WriteToList takes the recently read card's info and writes it to the list of registered people and indicates this on the LCD. It restores the admin mode to false so the next swiped card isn't added as well.

void WriteToList(String Name) {
  openedFile.close();
  openedFile = SD.open(F("List.txt"), FILE_WRITE);
  if (openedFile) {
    openedFile.println(Name);
    openedFile.close();
    lcd.setCursor(0, 1);
    lcd.print(Name);
    lcd.print(F("       "));
    lcd.setCursor(0, 2);
    lcd.print(F("ADDED TO LIST      "));
    lcd.setCursor(0, 3);
    lcd.print(F("                  "));
    adminSwipe = false;
  }
}

Step 3: Issues I Ran Into and What You Might As Well

Fun Fact: There are so many libraries and global variables that, at its current state, compiles with 71% of dynamic memory taken up, leaving about 590 bytes for run-time operations. This is just barely enough to open the files to manipulate the data. I kept having this very issue as I developed the code. I found here a way to free up more data in a very obscure manner. By using an F macro on all of the string buffers, such as [lcd.print(F("ADDED TO LIST "));] the sting is stored in flash memory instead of Ram, allowing for more run-time space. At least that's how I understand it. It's basically JFM and freed up just enough space for me to open up my files and run my code. If I had to add more components I might have to upgrade the Arduino form factor to a larger model with more memory space or switch to a more robust microcontroller.

Something to keep in check is the chip selectors for your SPI components. Both the USB Card Reader and the SD Shield used the Arduino's SPI communication protocol. Whenever I wanted to use the SD Shield I had to activate its chip selector, basically its address, which was pin 4, with digitalWrite(4, LOW). The same went for the USB Card Reader, except its selector was pin 10. In the program, you'll see me switching these on and off at different moments to switch from USB Shield to SD Shield.

For some reason, the Real Time Clock on the SD Shield likes to run about 7 minutes slow, even after resetting it by taking the battery out and re-uploading the code, which is supposed to set the clock to the pc's time and date. I'm going to leave this as a non-issue because it doesn't necessarily break the system.

Step 4: Finished Product

It turned out great and I can't wait to put it on the wall and plug it in!

This video is just a demonstration of its features, not a representation of its installation.

Step 5: Installed and Updated

After installing and using this system for a few days we, the foreman and I, noticed a few bugs. The biggest was that whenever the off button was pressed the LCD display would start displaying random symbols and we would have to reset the Arduino to fix it to normal. But once the button was pressed again it would bug out again. We guessed that the multi-pull contacter (the last picture), which was providing power to the tools and was controlled by the relay, would cause a voltage spike when the coils were de-energized, which in turn, caused all kinds of mayhem on the 5V rail on the Arduino, which the LCD was connected to. To fix this, we swapped the relay module with a solid state relay, which uses a semiconductor instead of a physical contact. This has stopped any glitching on the LCD.

For convenience, we added a switch on the top of the box that controls the 120V going to the power supply for the Arduino. This lets us shut off power inside the box if we need to service the system, retrieve the SD card, or reset the Arduino without opening the box.

Another large bug was that if the key was left on, the button pressed, turning tool power off, the Arduino could be reset with the new switch and turn tool power back on. To fix this, we decided on a combination of software and behavioral changes. The key switch now only turns power on, the off position does not turn the power off. The only thing that turns power off is the off button. When someone turns power on with the key, they will turn it, turn power on, turn the key back, and then remove their key, leaving it in the "off" position. We might replace this switch with a momentary key switch, which will have the same functionality, if we find that people have difficulty remembering to turn the key back off before pulling it out.

We connected the 12V output from the power supply to the indication LED, which was its original requirement. Previously, I only had 5V powering it for ease of building and testing. The Arduino pin for the LED goes through a TIP120, which in turn controls the LED, so the digital pin on the Arduino is safe from the 12V.

There was a glitch where if the key is left turned on, the button is pressed to turn power off, and somebody swipes, the power would only turn back on when the key was reset to off, instead of turning power on once the card was swiped. This was because, in the code, the button state is false when not pressed and true when pressed, and the only time it is reset to false is when the key is switched off. This glitch was remedied by adding a line resetting the button state when when a registered card is swiped and logged.

Lastly, I reorganized what is displayed on the LCD. Previously, the card info stayed up until either someone else swiped or the admin swiped to add somebody, but now it only stays up for about 1 minute, for obvious security reasons. The 4th line is reserved for notating the time power was turned on and when power was turned off, whichever happened last.

Microcontroller Contest

Participated in the
Microcontroller Contest