Edit: this project has since been updated to use my Arduino remote firmware solution. I no longer use Google Talk and instead use a simple REST API with embedded Jetty on a Raspberry Pi to send XBee messages to the Arduino. I use Cloud Messaging to send door events to my Android phone.
I did some research and found several garage door projects, using a variety of technologies: WiFi, Arduino, XBee and providing varying capabilities: monitor only, control only and both. I wanted to be able to control and monitor the door from my Android phone. I considered using WiFi. This would have involved a web server on the Arduino, but Arduino cannot support https (TLS) due to memory/processing limitations, so that rules out essentially all APIs for sending notifications back to a mobile phone. Another problem is accessing the web server from the internet would require configuring port forwarding from my router to the Arduino (not SSL), so bad idea. Additionally, the Arduino Wifi hardware is quite expensive at this time. I settled on XBee and Google Talk. Of course Google Talk is supported on both Android and IPhone, so both me and my wife (IPhone) could control and monitor the door. The downside to this solution is it does require running a server to bridge communication with both Google Talk and the Arduino. For this I'm using my Sheevaplug (plug computer), since it's very low power (~3W). Edit: I swapped this with a Raspberry Pi a few years later.
Controlling a garage door requires a relay. Fortunately there's a good reference circuit for relays on the Arduino site. I wired the relay to the Arduino and uploaded a simple script to verify I could make it open and close it. At first I wasted a lot of time troubleshooting what I thought was a circuit issue but turned out to be a bad breadboard. Note: the relay has a NO (normally open) and NC (normally closed). The garage door should be wired to the NO pin.
Note: The circuit above needs a 1K resistor from the transistor base to ground or it will not function!
The close-door contact was easily mounted on the wall. I used twist connectors to connect the sensor wire to the extension wire.
I used series 2 XBees in this solution, however series 1 would work just as well. The radios need to be configured with API firmware. Refer to XBeeConfiguration. I recommend configuring your XBees to use encryption for security.
The loop function looks for incoming XBee requests (close door, open door, door status), and detects changes in the closed and open door magnetic contacts. When the sketch receives an XBee packet (initiated by a Google Talk message), it performs the action (e.g. open door, close door, or status) and returns a acknowledgment packet. Similarly, when it detects a change in the door sensors (e.g. door opening or closing), whether initiated from Google Talk or the garage door button, it sends the corresponding event to Google Talk.
#define STATUS_RESPONSE_TIMEOUT 500 #define DEBUG 0 uint8_t openContact; uint8_t closeContact; // state variables bool doorFailure = false; bool closing; bool opening; bool relayActivated; // last time a contact fired long lastDoorActionTime; // last time the relay was activated long lastRelayActionTime; const uint8_t closePin = 8; const uint8_t openPin = 9; const uint8_t relayPin = 10; // TX Commands const uint8_t DOOR_OPEN = 4; const uint8_t DOOR_CLOSED = 5; const uint8_t DOOR_OPENING = 6; const uint8_t DOOR_CLOSING = 7; const uint8_t DOOR_FAILURE = 8; const uint8_t DOOR_ALREADY_OPEN = 9; const uint8_t DOOR_ALREADY_CLOSED = 10; const uint8_t CMD_ACK = 11; const uint8_t STARTUP = 12; // RX Commands const uint8_t DOOR_STATE_REQUEST = 1; const uint8_t OPEN_DOOR_REQUEST = 2; const uint8_t CLOSE_DOOR_REQUEST = 3; // TODO const uint8_t GET_DOOR_STATS = 4; const int MAX_DOOR_OPENCLOSE_TIME = 20000; const uint8_t debounceDelay = 50; uint16_t sendErrors = 0; // Define NewSoftSerial TX/RX pins // Connect TX of usb-serial device to NSS RX uint8_t ssRX = 6; // Connect RX of usb-serial device to NSS TX uint8_t ssTX = 7; // Remember to connect all devices to a common Ground: XBee, Arduino and USB-Serial device NewSoftSerial nss(ssRX, ssTX); XBee xbee = XBee(); XBeeResponse response = XBeeResponse(); // one byte payload uint8_t payload[] = { 0 }; // TODO replace with address of your coordinator (Connected to the Java app) uint32_t COORD_MSB_ADDRESS = 0x0013a41c; uint32_t COORD_LSB_ADDRESS = 0x403ef3b1; // Coordinator/XMPP Gateway XBeeAddress64 addr64 = XBeeAddress64(COORD_MSB_ADDRESS, COORD_LSB_ADDRESS); ZBTxRequest tx = ZBTxRequest(addr64, payload, sizeof(payload)); ZBTxStatusResponse txStatus = ZBTxStatusResponse(); // create reusable response objects for responses we expect to handle ZBRxResponse rx = ZBRxResponse(); void setup() { // start serial xbee.begin(9600); if (DEBUG) { // start soft serial nss.begin(9600); nss.println("Startup"); } // turn on internal pull-ups for magnetic switches pinMode(openPin, INPUT); digitalWrite(openPin, HIGH); pinMode(closePin, INPUT); digitalWrite(closePin, HIGH); pinMode(relayPin, OUTPUT); digitalWrite(relayPin, LOW); openContact = digitalRead(openPin); closeContact = digitalRead(closePin); opening = false; closing = false; doorFailure = false; lastDoorActionTime = 0; } void activateDoor() { digitalWrite(relayPin, HIGH); delay(200); digitalWrite(relayPin, LOW); relayActivated = true; } bool isDoorOpen() { // door is open only if open contact is closed and closed contact is open // 0 == closed contact, 1 == open contact return (openContact == 0) && (closeContact == 1); } bool isDoorClosed() { return (openContact == 1) && (closeContact == 0); } void handleXBeeResponse() { if (xbee.getResponse().getApiId() == ZB_RX_RESPONSE) { // now fill our zb rx class xbee.getResponse().getZBRxResponse(rx); // Make sure this is coming from our XBee (note: this is weak security.. using XBee encryption is highly recommended) if (!(rx.getRemoteAddress64().getMsb() == COORD_MSB_ADDRESS && rx.getRemoteAddress64().getLsb() == COORD_LSB_ADDRESS)) { if (DEBUG) nss.println("WARN: unknown source address"); return; } if (rx.getData(0) == OPEN_DOOR_REQUEST) { // open door if (isDoorClosed()) { if (DEBUG) nss.println("Opening door"); activateDoor(); // tell the sender the request was successful sendDoorEvent(CMD_ACK); } else { // closed contact = 1 (open) // tell sender door is already open if (DEBUG) nss.println("Door already open!"); sendDoorEvent(DOOR_ALREADY_OPEN); } } else if (rx.getData(0) == CLOSE_DOOR_REQUEST) { // close door if (isDoorOpen()) { if (DEBUG) nss.println("Closing door"); activateDoor(); // tell the sender the request was successful sendDoorEvent(CMD_ACK); } else { if (DEBUG) nss.println("Door already closed!"); // tell sender the door is already closed sendDoorEvent(DOOR_ALREADY_CLOSED); } } else if (rx.getData(0) == DOOR_STATE_REQUEST) { sendDoorEvent((openContact & 1) + ((closeContact << 1) & 2)); } else { // unknown command // TODO log if (DEBUG) nss.print("Unknown RX:"); if (DEBUG) nss.println(rx.getData(0)); } } else { // unsupported api -- TODO handle if (DEBUG) nss.print("Unsupported RX packet:"); if (DEBUG) nss.println(xbee.getResponse().getApiId(), HEX); } } void sendDoorEvent(uint8_t message) { payload[0] = message; switch (message) { case DOOR_OPEN: case DOOR_CLOSED: lastDoorActionTime = millis(); break; } // TODO set frame id with millis & 256 xbee.send(tx); // after sending a tx request, we expect a status response // wait up to half second for the status response if (xbee.readPacket(STATUS_RESPONSE_TIMEOUT)) { // got a response! // check if series 1 or series 2 tx status if (xbee.getResponse().getApiId() == ZB_TX_STATUS_RESPONSE) { xbee.getResponse().getZBTxStatusResponse(txStatus); // get the delivery status, the fifth byte if (txStatus.isSuccess()) { // good } else { if (DEBUG) nss.print("sendDoorEvent no ACK:"); // TODO resend with same frame id sendErrors++; } } } else if (xbee.getResponse().isError()) { if (DEBUG) nss.print("sendDoor TX error:"); if (DEBUG) nss.println(xbee.getResponse().getErrorCode()); } else { if (DEBUG) nss.print("sendDoor TX timeout"); // local XBee did not provide a timely TX Status Response -- should not happen if radio is configured and wired correctly // did you switch the TX/RX jumpers back to XBee? // is your baud rate correct? // in API mode? } } // detect pin state change with debounce bool pinChange(int pin, int current) { if (digitalRead(pin) != current) { // debounce delay(debounceDelay); // if state still the same, send event if (digitalRead(pin) != current) { return true; } else { // ignore spurious event // TODO log return false; } } return false; } void loop() { // reads a packet from Serial, if data is available; otherwise continues on xbee.readPacket(); if (xbee.getResponse().isAvailable()) { // got something handleXBeeResponse(); } else if (xbee.getResponse().isError()) { if (DEBUG) nss.print("RX packet loop() error:"); if (DEBUG) nss.println(xbee.getResponse().getErrorCode(), DEC); } // detect if open-door contact just tripped if (pinChange(openPin, openContact)) { // open-door contact tripped -- toggle it openContact = !openContact; // Remember that then the circuit is closed when the contacts meet, which makes the wire go to 0V (logical false). An open contact is 5V (logical true) if (openContact) { // Open contact is now OPEN -- door is closing if (DEBUG) nss.println("Door closing"); closing = true; lastDoorActionTime = millis(); sendDoorEvent(DOOR_CLOSING); } else { // Open contact is now CLOSED -- door has completed opening if (DEBUG) nss.println("Door finished opening"); opening = false; doorFailure = false; lastDoorActionTime = 0; sendDoorEvent(DOOR_OPEN); } } // detect if closed-door contact just tripped if (pinChange(closePin, closeContact)) { // closed-door contact tripped -- toggle it closeContact = !closeContact; if (closeContact) { // Close contact is now OPEN -- door is opening if (DEBUG) nss.println("Door opening"); // door opening opening = true; lastDoorActionTime = millis(); sendDoorEvent(DOOR_OPENING); } else { // Close contact is now CLOSED -- door has finished closing if (DEBUG) nss.println("Door finished closing"); closing = false; doorFailure = false; lastDoorActionTime = 0; sendDoorEvent(DOOR_CLOSED); } } if ((opening || closing) && (millis() - lastDoorActionTime) > MAX_DOOR_OPENCLOSE_TIME) { // Problem: door started opening or closing but did not complete with the expected time doorFailure = false; if (DEBUG) nss.println("Door failure"); sendDoorEvent(DOOR_FAILURE);
opening = false;
closing = false;
} }
The Java Part
Unless you have multiple Google accounts, you'll need to create a Google account for the garage door (e.g. mygaragedoor@gmail.com). If you reuse an existing account, remember that anyone that is in your roster list (friends), will be able to control your door, unless your remove them. For this reason it's better to create a separate account.
In initGoogleTalk, add a roster friend for each Google account that should be allowed to control the Garage Door, for example:
xmppClient.addRosterFriend("yourpersonalgmailaccount@gmail.com");
The software will automatically subscribe to and accept messages from this Google account. Only Google accounts specified here will be able to control and receive messages from the garage door.
Find the following line:
xmppClient.connect(new GtalkConnector("mygaragedoor@gmail.com", "password"), new MessageListener() {
and replace with the email/password of the Google account that was created for the garage door.
Once all changes have been made, you can run the application from your IDE. All required libraries are included in the software download, so your IDE should find them automatically. If not, add all JAR file in the "lib" folder.
Google Talk authentication and communication occurs over TLS (transport layer security), so you can consider it to be quite secure, to the extent that you protect your credentials and choose a strong password. Additionally, only users that you specify can send messages to the garage door account, so you don't have to worry about a spammer playing with your door. You could add additional security by requiring a pin number to be entered for each door control request.
The Arduino Sketch is configured to only accept XBee packets from your radio. It does this by checking the source address (64-bit serial high/low). This however is not good security as it could be defeated, but is somewhat safe in that Digi firmware will not let you spoof addresses. I strongly recommend using XBee's built-in support for encryption.
Case
For the case I found a plastic pizza dough container from Whole Foods in my recycle bin. You can see I'm lazy by using a breadboard instead of assembling the components on protoboard. I recommend mounting the Arduino such that you can access the jumpers and USB cable for updating the sketch.
Parts
- Omron G5SB Relay http://www.sparkfun.com/products/10509 This 5V relay is Arduino safe, in that the coil impedance is high enough that the Arduino can safely power it.
- 2N2222A transistor http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=497-2598-ND Doesn't have to be this exact one
- Couple 1K Resistors. I'm guessing you have some of these
- 1N4004 Diode http://search.digikey.com/scripts/DkSearch/dksus.dll?Detail&name=1N4004FSCT-ND Can also be found at Radio Shacks, in the US
- 2 Magnetic door contacts. I got mine on ebay: http://cgi.ebay.com/5-Set-Door-Window-Contact-Magnetic-Reed-Switch-Alarm-/110716359054?pt=LH_DefaultDomain_0&hash=item19c735918e#ht_2557wt_1114 Keep in mind that if you order from China, be prepared to wait 3 weeks, unless of course you live in China.
- Low voltage wire to connect Arduino circuit to the garage door unit and magnetic sensors. Measure first to get an idea of how much you need. I bought a 65' spool of 20 gauge at Home Depot which was more than enough
- Wire connectors caps, similar to this http://www.homedepot.com/h_d1/N-5yc1v/R-100628936/h_d2/ProductDisplay?langId=-1&storeId=10051&catalogId=10053
- Obviously you'll need two Series 2 XBees, one Arduino or clone (e.g. RBBB). A USB Explorer or equivalent for the PC side, and a XBee socket or XBee Shield to interface with the Arduino.
- 9V Arduino power supply and possibly an extension cord.
- Standoffs and zip ties to mount the Arduino in the enclosure.
- Some screws and washers to mount the enclosure to wall