The Arduino doorbell project: Ethernet shield and Multitasking

After the basic functionality has been implemented (see my previous post about the doorbell project), the next step of the doorbell project is to implement the Ethernet shield TCP/IP functionality with some extra features, such as NTP time synchronization, logging of doorbell events in an SQL server and a telnet service that allows to control the doorbell. One could for instance think about an API that makes the doorbell ring for incoming Skype calls.

a97251_g173_8-butt

Soon I will show you the source code for the doorbell project on my Arduino with the integration of the Ethernet shield. You will see that I gave up using the DCF77 hardware (for now) and use NTP time synchronisation instead. At this moment the time is not really necessary, but at a later stage I want to add functionality for quiet hours where the doorbell will not ring. I also decided to make the doorbell project more than just a doorbell. The doorbell can now ring in different patterns, indicating different type of events, i.e. when an email arrives or when something else happens that deserves some priority. To realize this the Arduino needs to be approachable for instance in the form of a TCP service. In this blog I will explain about

  • The Ethernet shield and TCP/IP integration
  • The NTP service and what one needs to keep in mnd
  • Logging the doorbell event into an external database
  • A telnet TCP service for handling external events that make the doorbell ring (in a different pattern)
  • Simple multitasking

 

TCP/IP: To get the Ethernet shield to work is real simple, first a few includes are required:

 #include <SPI.h>  
 #include <Ethernet.h>  
 #include <EthernetUdp.h>  

Now we need to set up TCP/IP, this has to happen only once, so it is part of setup

 void setup()   
 {  
  // Enter a MAC address for your controller below.  
  // Newer Ethernet shields have a MAC address printed on a sticker on the shield  
  byte mac[] = {   
   0x90, 0xA2, 0xDA, 0x01, 0x01, 0x01  };  

  IPAddress ip(192,168,2,202);  
  unsigned int localPort = 8888;   // local port to listen for UDP packets  

  // IP & UDP settings   
  Ethernet.begin(mac, ip);  // Set MAC and IP address, netmask is default 255.255.255.0  
}

The Wiznet chip on the Ethernet shield is a pretty nice chip, it does a lot automatically for us. The only thing we have to do is set up the MAC address, you can make up something like in the example or you can find a MAC address on the label on the shield. Furthermore in my example I chose to have a static IP address. DHCP is real easy too: just call Ethernet.begin(mac), so without ip-argument.

NTP: This code is readily available as an example in the Arduino IDE, but need to be changed a little. We basically don’t want to bother about the time synchronisation and let it happen automatically. Arduino offers this functionality via SetSyncProvider and  SetSyncInterval functions. SetSyncProvider requires an argument that points to a function that returns the time in unix format (the seconds that have passed since 1 Jan 1970). SetSyncInterval requires an argument the number of seconds between each call to the sync provider function. From the NTP sample code I took parts of the code.

 #define NTP_PACKET_SIZE 48      // NTP time stamp is in the first 48 bytes of the message  
 byte packetBuffer[ NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets  

 /******************************************  
  * send an NTP request to the time server   
  * at the given address   
  ******************************************/  
 unsigned long sendNTPpacket(IPAddress& address)  
 {  
  Serial.print("Sending NTP packet to ");  
  for (byte thisByte = 0; thisByte < 4; thisByte++) {  
   // print the value of each byte of the IP address:  
   Serial.print(address[thisByte], DEC);  
   Serial.print(".");   
  }  
  Serial.println();  

  // set all bytes in the buffer to 0  
  memset(packetBuffer, 0, NTP_PACKET_SIZE);   
  // Initialize values needed to form NTP request  
  // (see URL above for details on the packets)  
  packetBuffer[0] = 0b11100011;  // LI, Version, Mode  
  packetBuffer[1] = 0;   // Stratum, or type of clock  
  packetBuffer[2] = 6;   // Polling Interval  
  packetBuffer[3] = 0xEC; // Peer Clock Precision  
  // 8 bytes of zero for Root Delay & Root Dispersion  
  packetBuffer[12] = 49;   
  packetBuffer[13] = 0x4E;  
  packetBuffer[14] = 49;  
  packetBuffer[15] = 52;  

  // all NTP fields have been given values, now  
  // you can send a packet requesting a timestamp:               
  Udp.beginPacket(address, 123); //NTP requests are to port 123  
  Udp.write(packetBuffer,NTP_PACKET_SIZE);  
  Udp.endPacket();   
 }  

 /******************************************  
  * get the NTP time  
  ******************************************/  
 unsigned long getNTPtime()   
 {  
  Serial.println("Time synchronization starting");  
  IPAddress timeServer(64,147,116,229); // time.nist.gov NTP server  
  sendNTPpacket(timeServer); // send an NTP packet to a time server  
  delay(1000);   
  if ( Udp.parsePacket() ) {   
   // We've received a packet, read the data from it  
   Udp.read(packetBuffer,NTP_PACKET_SIZE); // read the packet into the buffer  

   //the timestamp starts at byte 40 of the received packet and is four bytes,  
   // or two words, long. First, esxtract the two words:  

   unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);  
   unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);   
   // combine the four bytes (two words) into a long integer  
   // this is NTP time (seconds since Jan 1 1900):  
   unsigned long secsSince1900 = highWord << 16 | lowWord;   
   Serial.print("Seconds since Jan 1 1900 = " );  
   Serial.println(secsSince1900);          

   // now convert NTP time into everyday time:  
   Serial.print("Unix time = ");  
   // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:  
   const unsigned long seventyYears = 2208988800UL;     
   // subtract seventy years:  
   unsigned long epoch = secsSince1900 - seventyYears;   

   //add correction for GMT+1  
   epoch = epoch + 3600;  
   //todo daylight saving time  

   // print Unix time:  
   Serial.println(epoch);                  
   return epoch;  
  }  
  return 0;  
 }  

The code is split into two functions, the function that is used as sync provider is getNTPtime. I believe the code is not too robust, also something is missing here. which is the correction for daylight savings time. But for now this is fine. In the setup function it is now possible to add two calls, after the IP address has been set up:

  // setup time synchronization, this is a call to the function that contacts the teim server  
  // synchronize time every 7200 seconds (2 hours)  
  setSyncProvider(getNTPtime);  
  setSyncInterval(7200);  

Note that 7200 (2 hours) is adjustable to anything you desire.

Logging: Now that we have TCP/IP set up logging is fairly easy, especially from the Arduino side. At my home I have a NAS from Synology. Pretty much out of the box you can run a Webserver with PHP and MySQL. A PHP-file is a script that is runs on the Webserver when somebody makes a request to the PHP-file. The PHP script usually does a couple of things in the background and produced HTML-code. In our case the script will connect to the MySQL database and make an entry to a table that captures doorbell events. The design of the SQL table is simple, it has two fields: a primary key (unique identifier) and a timestamp which records the time of the event.

Screen Shot 2013-02-07 at 11.28.03

I won’t go in much more details now since this is a blog about Arduino, but if you need help, I am happy to assist. From the Arduino site I only need to request a php file via the hypertext transfer protocol (HTTP). In your browser this would be the line HTTP://192.168.2.100/doring.php where 192.168.2.100 is the IP address of my NAS. The typical Arduino code would be:

 /******************************************  
  * make a request to a webpage on the NAS  
  * so the NAS can log a database entry  
  ******************************************/  
 void doWeblog() {  
  // Initialize the Ethernet client library  
  // with the IP address and port of the server   
  // that you want to connect to (port 80 is default for HTTP):  
  EthernetClient client;  

  if (client.connect(webServer, 80)) {  
   Serial.println("connected");  
   // Make a HTTP request:  
   client.println("GET /doring.php HTTP/1.0");  
   client.println();  
   if (client.available()) {  
    char c = client.read();  
    Serial.print(c);  
   }  
   // if the server's disconnected, stop the client:  
   if (!client.connected()) {  
    Serial.println();  
    Serial.println("disconnecting.");  

   }  
   client.stop();  
  }  
 }  

Note: I have not tested this function for robustness, but it worked just fine.

External events handling / multitasking: This is probably the most exciting part, because there are some nice challenges here. Our doorbell has now quite some extra functionality, and we are going to add one more, a telnet TCP service that can receive requests to make the doorbell ring. Implementation of this idea requires some new ideas: most of the time the Arduino will have nothing to do and if there is something to do it is mostly just a single task. It can handle a single task pretty well, but imagine a situation where an external event is triggered and somebody is opening a telnet connection. The regular code that you would use would probably involve a while loop that is waiting for characters, causing that other events will be blocked. That’s said this is a single tasking service. Let’s have a look to the simplified code as found in the Arduino Webserver example, this code is part of loop():

1:   EthernetClient client = server.available();  
2:   if (client) {  
3:      
4:    while (client.connected()) {  
5:     if (client.available()) {  
6:      char c = client.read();  
7:     
8:      }  
9:     }  
10:   }   

With this code a newly connected client is served pretty well, and as you see the code will loop until data is received. Receiving data is usually just a matter of a split second, but theoretically can be much longer and nothing else can happen in between (except for Interrupts and probably the time synchronization). If I’d implement this code and open a telnet session to the Arduino it would appear if the board is hanging! The answer to the solution is simple, but requires a different design:

Avoid time consuming loops (while) and delays (delay) in loop(), use if and millis() so you can do other things in between.

But how will we handle loops then you might think. Well that’s fairly easy when you consider that the loop() function is a loop in it self. so imagine about a project that involves two LED’s (with resistors) connected to analog ports of the Arduino board. without using for or a while loop you can fade them seperately.

 int i, j;  

 void setup()  
 {  
  pinMode(3,OUTPUT);  
  pinMode(5,OUTPUT);  
  i=0;  
  j=128;  
 }  

 void loop()  
 {  
  analogWrite(3,i);  
  analogWrite(5,j);  

  i = (i + 1) % 256;  
  j = (j + 1) % 256;   
  delay(10);  
 }  

This code handles two tasks at the same time, in a single cycle it sends a new value to each LED and increments the value that is send to the LEDs for the next cycle. This is in essence a multitasking program!

Back to the doorbell project,  the idea is that a telnet service is available on the Arduino board. the Arduino would greet you with a welcome message and you could start typing a sequence of rings and pauses that you want to be played by the doorbell. The Arduino would check the sequence and reply with an acknowledgement or a negative acknowledgement. When acknowledged the doorbell would start right away playing the pattern.

Screen Shot 2013-02-10 at 14.00.16

Below you find the code to this multitasking service. In the code you will see that I defined different states that can be reached, to make this primitive multitasking as fair as possible I ordered the states in the reverse order. This means that in one cycle only one new state of the telnet TCP service is handled (except for the situation when the user disconnects). Note that this code makes use of only the piezo speaker for the telnet service, else it drives people mad in the house. I use SVN for version control, soon this blog will be updated and you can checkout the code from there.

 /******************************************  
  * Doorbell project  
  *  
  * $Revision: 12 $:  
  * $Author: hans $:  
  * $LastChangedDate: 2013-02-10 13:56:18 +0100 (Sun, 10 Feb 2013) $:  
  *  
  ******************************************/  
 #include <Time.h>  
 #include <SPI.h>  
 #include <Ethernet.h>  
 #include <EthernetUdp.h>  

 #define DOORBELL_PIN 3  
 #define DOORBELL_INT 1        // only UNO and MEGA2560  
 #define DOORBELL_TIME 5000      // Minimal 5 seconds between the rings  

 #define RELAY1_PIN  5  
 #define RELAY_DOORBELL 5  
 #define RELAY2_PIN  6  
 #define RELAY3_PIN  7  
 #define RELAY4_PIN  8  
 #define PIEZO_PIN  9  

 #define NTP_PACKET_SIZE 48      // NTP time stamp is in the first 48 bytes of the message  

 #define STATE_DISCONNECTED 0  
 #define STATE_AVAILABLE 1   
 #define STATE_CONNECTED 2  
 #define STATE_DATAAVAIL 3  

 #define APP_VERSION "0.3"  

 boolean ring_doorbell = false;  
 // Initialize the Ethernet server library  
 // with the IP address and port you want to use   
 // (port 23 is default for telnet):  
 EthernetServer server(23);  
 EthernetUDP Udp;           // A UDP instance to let us send and receive packets over UDP  
 byte packetBuffer[ NTP_PACKET_SIZE]; // buffer to hold incoming and outgoing packets  
 IPAddress webServer(192,168,2,100); // The IP address of my NAS  
 EthernetClient telnetClient;        // telnetClient that will be connected  

 String message = "";  
 String buzz_pattern = "";  

 void setup()   
 {  
  // Enter a MAC address for your controller below.  
  // Newer Ethernet shields have a MAC address printed on a sticker on the shield  
  byte mac[] = {   
   0x90, 0xA2, 0xDA, 0x0D, 0xC2, 0x93  };  
  IPAddress ip(192,168,2,202);  
  unsigned int localPort = 8888;   // local port to listen for UDP packets  

  // Enable serial communication for debugging   
  Serial.begin(57600);  
  Serial.print("Arduino doorbell service v");  
  Serial.println(APP_VERSION);  

  // IP & UDP settings   
  Ethernet.begin(mac, ip);  // Set MAC and IP address, netmask is default 255.255.255.0  
  Udp.begin(localPort);   // local port to listen for UDP packets  

  // setup time synchronization, this is a call to the function that contacts the teim server  
  // synchronize time every 7200 seconds (2 hours)  
  setSyncProvider(getNTPtime);  
  setSyncInterval(7200);  

  // Attach interrupt for PIN 3. On rising edge interrupt service routine intDoorbell is called  
  attachInterrupt(DOORBELL_INT, intDoorbell, RISING);  

  // Set Piezo PIN to output mode  
  pinMode(PIEZO_PIN, OUTPUT);  

  // Set Doorbell relay to output mode  
  pinMode(RELAY_DOORBELL, OUTPUT);  

  //digitalWrite(9,HIGH);  
  analogWrite(PIEZO_PIN, 20);   // Almost any value can be used except 0 and 255  
  delay(650);        // wait for a delayms ms  
  //digitalWrite(9,LOW);  
  analogWrite(PIEZO_PIN, 0);    // 0 turns it off  
 }  

 void loop()  
 {  
  static byte srv_state = STATE_DISCONNECTED;  
  static unsigned int count = 0;  
  static unsigned int piezo_delay, doorbell_delay;  
  static boolean finish_ring = 0;   
  unsigned int cur_time = millis();  

  // Check whether ring_doorbell is set. typically it is set by the intDoorbell function that is called  
  // when the doorbell button is pressed.  
  if (ring_doorbell)  
  {  
   Serial.println("Doorbell button pressed - ring ring!");  
   ring_doorbell = false;      // clear the ring_doorbell  
   ctlDoorbell(true);        // start to ring the doorbell  
   doorbell_delay=cur_time + 100;  // calculate the time till when the doorbell has to ring  
   finish_ring = true;        // indicate that the doorbell has to be stopped after delay has been reached  
   logDoorbell();          // log to the webserver  

   if (buzz_pattern=="");      // if the piezo is not performing a buzz  
    buzz_pattern="r50p100r50p100"; // insert a new buzz_pattern  

  }  

  // check whether the doorbell is still ringing and the time has reached to switch it of again  
  if ((doorbell_delay<=cur_time) && (finish_ring))  
  {  
   ctlDoorbell(false);        // stop the doorbell  
   finish_ring = false;       // finish, the doorbell doesn't need to be stopped now anymore  
  }  

  // check whether a new tone has to be processed    
  if ((piezo_delay<=cur_time) && (buzz_pattern!=""))  
  {  
   ctlPiezo(procTone());       // process the tone (r for ring, p for pause)  
   piezo_delay = cur_time + procDelay();  
                     // calculate the time for the delay given in the buzz pattern   
  }  

  // Telnet stage 4.  
  // data is available!  
  if (srv_state==STATE_DATAAVAIL)   // reaching this state means data is available  
  {  
   message = checkMsg();       // check if received message is valid  
   if ((message!="") && (buzz_pattern=="")) {  
                     // the message is not empty & no other buzz_pattern is in use  
    buzz_pattern = message;     // the new buzz_pattern is install  
    telnetClient.println("ACK");      // Acknowledge to the telnetClient  
    Serial.print("Telnet - Message acknowledged, ring pattern: ");  
    Serial.println(buzz_pattern);  
    ctlPiezo(procTone());      // start immediately processing the pattern  
    piezo_delay= cur_time + procDelay();  
                     // calculate the time for the delay given in the buzz pattern   

   }  
   else  
   {  
    telnetClient.println("NAK");      // negative acknowledgement, invalid message or another buzz pattern is ongoing  
    Serial.println("Telnet - Message discarded");  
    Serial.print(message);  
   }  
   message = "";           // the buzz_pattern is installed or discarded, anyway, clear the message   
   telnetClient.stop();           // and disconnect the telnetClient  
   srv_state = STATE_DISCONNECTED;  // set the appropriate state.  
  }  

  // Telnet stage 3.  
  // check if data is available  
  if (srv_state==STATE_CONNECTED)   
  {  
   if (!telnetClient.connected())   
   {  
    srv_state=STATE_DISCONNECTED;  // Was the connection dropped intermediate?  
    message="";           // clear the message (cleanup)  
    telnetClient.stop();          // stop the telnetClient (cleanup)  

    Serial.println("Telnet - Stage 3 telnetClient disconnected");  
   }  
   else   
   {  
    if (telnetClient.available())   
    {  
     //data available  
     char c = telnetClient.read();    // read the character that is available  
     message = message + c;     // add to the existing message  
    }   
    else  
    {   
     if (message!="")        // no more data available and the message is not empty  
      srv_state=STATE_DATAAVAIL;  // so a new state has been reached  
    }  
   }   
  }  

  // Telnet stage 2.  
  // check if a telnetClient connection is connected and say hello  
  if (srv_state==STATE_AVAILABLE)   
  {  
   if (telnetClient.connected())  
   {  
    srv_state=STATE_CONNECTED;    // update the server state  
    telnetClient.print("Welcome to Arduino doorbell service ");   
    telnetClient.println(APP_VERSION);   // send the welcome message  
    telnetClient.flush();         // flush any data that is received too early  
    Serial.println("Telnet - Stage 2 telnetClient connected");  
   }  
   else  
    srv_state=STATE_DISCONNECTED;  
  }  

  // Telnet stage 1.  
  // create a telnetClient object  
  if (srv_state==STATE_DISCONNECTED)   
  {  
   srv_state=STATE_AVAILABLE;  
   telnetClient = server.available();  
  }  

 }  

 /******************************************  
  * interrupt handler when somebody presses  
  * the doorbell switch  
  ******************************************/  
 void intDoorbell()  
 {  
  byte doorbellValue;  
  unsigned long current_time = millis();  
  static unsigned long last_time = 0;  

  // wait 50ms and read the pin. if it is still high then this is not a spike  
  delay(50);   
  doorbellValue=digitalRead(DOORBELL_PIN);  

  // test whether the last doorbell press was long enough ago  
  if ((doorbellValue) && (current_time - last_time >= DOORBELL_TIME))  
  {  
   ring_doorbell = true;  
   last_time = current_time;  
  }  
 }  

 /******************************************  
  * Ring the doorbell  
  * rings the bell and buzz the piezo speaker  
  ******************************************/  
 void ctlDoorbell(boolean state)  
 {  
  bitWrite(PORTD,RELAY_DOORBELL,state);  
 }   

 /******************************************  
  * Buzz the piezo  
  * rings the bell and buzz the piezo speaker  
  ******************************************/  
 void ctlPiezo(boolean state){  
  //digitalWrite(PIEZO_PIN, state);  
  if (state) {  
   analogWrite(PIEZO_PIN,20);  
   return;  
  }  
  analogWrite(PIEZO_PIN,0);  
  return;  
 }   

 /******************************************  
  * Read the tone (ring or pause)  
  * and remove it from the buzz pattern  
  ******************************************/  
 boolean procTone()  
 {  
  char tone = buzz_pattern[0];  
  buzz_pattern = buzz_pattern.substring(1, buzz_pattern.length());  

  if (tone=='r')  
   return true;  
  return false;  
 }  

 /******************************************  
  * Read the delay  
  * and remove it from the buzz pattern  
  ******************************************/  
 unsigned int procDelay()  
 {  
  unsigned int ipause;  
  char pause[6];  
  byte i;  

  for (i=1; i<5; i++) {  
   if ((buzz_pattern[i]=='r') || (buzz_pattern[i]=='p'))  
    break;  
  }  

  buzz_pattern.toCharArray(pause,i+1);  
  ipause = atoi((char *)&pause);   

  buzz_pattern = buzz_pattern.substring(i, buzz_pattern.length());  
  return ipause;  

 }  

 /******************************************  
  * make a request to a webpage on the NAS  
  * so the NAS can log a database entry  
  ******************************************/  
 void logDoorbell() {  
  // Initialize the Ethernet telnetClient library  
  // with the IP address and port of the server   
  // that you want to connect to (port 80 is default for HTTP):  
  EthernetClient webClient;  

  if (webClient.connect(webServer, 80)) {  
   Serial.println("connected to Logserver");  
   // Make a HTTP request:  
   webClient.println("GET /doring.php HTTP/1.0");  
   webClient.println();  
   if (webClient.available()) {  
    char c = webClient.read();  
    Serial.print(c);  
   }  
   // if the server's disconnected, stop the telnetClient:  
   if (!webClient.connected()) {  
    Serial.println();  
    Serial.println("disconnecting.");  

   }  
   webClient.stop();  
  }  
 }  

 /******************************************  
  * send an NTP request to the time server   
  * at the given address   
  ******************************************/  
 unsigned long sendNTPpacket(IPAddress& address)  
 {  
  Serial.print("Sending NTP packet to ");  
  for (byte thisByte = 0; thisByte < 4; thisByte++) {  
   // print the value of each byte of the IP address:  
   Serial.print(address[thisByte], DEC);  
   Serial.print(".");   
  }  
  Serial.println();  

  // set all bytes in the buffer to 0  
  memset(packetBuffer, 0, NTP_PACKET_SIZE);   
  // Initialize values needed to form NTP request  
  // (see URL above for details on the packets)  
  packetBuffer[0] = 0b11100011;  // LI, Version, Mode  
  packetBuffer[1] = 0;   // Stratum, or type of clock  
  packetBuffer[2] = 6;   // Polling Interval  
  packetBuffer[3] = 0xEC; // Peer Clock Precision  
  // 8 bytes of zero for Root Delay & Root Dispersion  
  packetBuffer[12] = 49;   
  packetBuffer[13] = 0x4E;  
  packetBuffer[14] = 49;  
  packetBuffer[15] = 52;  

  // all NTP fields have been given values, now  
  // you can send a packet requesting a timestamp:               
  Udp.beginPacket(address, 123); //NTP requests are to port 123  
  Udp.write(packetBuffer,NTP_PACKET_SIZE);  
  Udp.endPacket();   
 }  

 /******************************************  
  * get the NTP time  
  ******************************************/  
 unsigned long getNTPtime()   
 {  
  Serial.println("Time synchronization starting");  
  IPAddress timeServer(64,147,116,229); // time.nist.gov NTP server  
  sendNTPpacket(timeServer); // send an NTP packet to a time server  
  delay(1000);   
  if ( Udp.parsePacket() ) {   
   // We've received a packet, read the data from it  
   Udp.read(packetBuffer,NTP_PACKET_SIZE); // read the packet into the buffer  

   //the timestamp starts at byte 40 of the received packet and is four bytes,  
   // or two words, long. First, esxtract the two words:  

   unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);  
   unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);   
   // combine the four bytes (two words) into a long integer  
   // this is NTP time (seconds since Jan 1 1900):  
   unsigned long secsSince1900 = highWord << 16 | lowWord;   
   Serial.print("Seconds since Jan 1 1900 = " );  
   Serial.println(secsSince1900);          

   // now convert NTP time into everyday time:  
   Serial.print("Unix time = ");  
   // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:  
   const unsigned long seventyYears = 2208988800UL;     
   // subtract seventy years:  
   unsigned long epoch = secsSince1900 - seventyYears;   

   //add correction for GMT+1  
   epoch = epoch + 3600;  
   //todo daylight saving time  

   // print Unix time:  
   Serial.println(epoch);                  
   return epoch;  
  }  
  return 0;  
 }  

 String checkMsg() {  
  String retstring = "";  
  if (message.length()>2)  
  {  
   for (int i=0; i<message.length(); i++) {  
    if (message[i]=='!')  
    {  
     //retval = true;  
     retstring = message.substring(0,i);  
     break;  
    }  
    if ((message[i]!='p') && (message[i]!='r') && ((message[i]<'0') || (message[i]>'9')))  
     break;  
   }  
  }  
  return retstring;  
 }  

I use SVN for version control. you can checkout the code soon from there. 

 

The Arduino doorbell project

My doorbell rings. That is what doorbells do. When it rings, it is mostly annoying, unless it is the pizza guy delivering a pizza. Some people ring the doorbell for several seconds or press it a couple of times, thinking that it makes people answer the door quicker. I just find it annoying. A simple solution would be to turn off the doorbell by installing a switch but for me that is way too simple; moreover you could forget switching the doorbell on again. Doorbells on the market that can be switched off or play a nice melody are vastly overpriced. Very often these are also wireless doorbells, leaving the wired system unused, which means that you have to give up the old doorbell, which is unlike the wireless one, maintenance free.

In my most creative moment I thought about creating a controllable doorbell with logging facility. The doorbell would ring briefly even if somebody presses it for 2 seconds, it should be silent at given hours and any doorbell button press would be logged into a database. Also the doorbell could ring on other sorts of activity, for instance when somebody wants to Skype you, when an email arrives or something else that’s important.

The answer to it is Arduino. The Arduino UNO that I have is ideal for this task. My last (and first) Arduino project was to pick up the DCF77 signal and get the time from the atomic clock. Basically the DCF77 signal is not necessary for the project, but the shield that I created for it has some room left which can be used to connect the doorbell switch and another peripheral like a piezo speaker. The piezo speaker will be nice for testing purposes. To really make this work the schematics of the current doorbell needs to be studied, I just looked for schematics on the Internet.

belaansluiting

The doorbell is connected to a power supply providing 8 Volts AC to the doorbell. In order control the doorbell separately from the doorbell switch the circuit has to be changed. Where now the doorbell switch is, the Arduino should be placed. A second circuit with the Arduino and the doorbell switch has to be created as well. The Arduino cannot handle AC power, also the current is around 1A, this is nothing for the senstive PINs of Transistor-transistor logic! The DFRobot relay shield seems to be able to do this task. It has four relays, so actually a little bit overkill.

IMGP9263-600x600

 

So now we have a little shopping list:

  • Arduino UNO (or another version, but this is the minimum)
  • Arduino Ethernet shield
  • DFRobot relay shield
  • DFRobot prototype shield, with DCF77 logic connected (not really required, but part of my existing project)
  • Piezo speaker (not really required)

The DCF77 is connected to pin 2, this is nice because we can attach an interrupt to it, which simplifies the code. Another interrupt can be attached to pin 3, so it seems good to connect the doorbell switch to that pin 3. The other boards also require some PINs and this leads to some interesting conflicts!

  • PIN 0 – unused
  • PIN 1 – unused
  • PIN 2 – relay shield, relay #1 & DCF77
  • PIN 3 – relay shield, relay #2 & Doorbell switch
  • PIN 4 – relay shield, relay #3  & Ethernet shield, SS MicroSD
  • PIN 5 – relay shield, relay #4
  • PIN 6 – unused
  • PIN 7 – unused
  • PIN 8 – unused
  • PIN 9 – Piezo speaker
  • PIN 10 – Ethernet shield, SS Ethernet
  • PIN 11 – Ethernet shield, MOSI
  • PIN 12 – Ethernet shield, MISO
  • PIN 13 – Ethernet shield, SCK

PIN 2 to PIN 5 are being used by the relay shield but also the DCF77 antenna, the doorbell switch and the Ethernet shield. So it would make sense that the relay shield moves out of the way. I found the “go between” shield that allows remapping of pins:

11002-01

I wanted relay #1 to be on PIN 5, relay #2 on PIN6, relay #3 on PIN7 and relay #4 on PIN8. After configuring it the shield looked like this:

130202_4858The only thing that now matters is the order of how the shield are stacked. The relay shield is on top, straight after the “go between” shield and after that either the Ethernet shield or the DCF77/piezo/doorbell switch shield. Hardware wise we are almost there, I attached a pull down resistor of 10K between PIN 3 and GND, the doorbell switch is placed between +5V and PIN3. The purpose of the pull down resistor is that in case the doorbell switch is open the remaining current or noise is burned and no tri-state occurs. Tri-state means that the state of the PIN 3 would be floating and the Arduino would not be able to determine the right state (which is 0).

Screen Shot 2013-02-03 at 12.09.25

Now we are ready to rock and roll. This bit of code proofs the concept:

 /******************************************  
  * Doorbell project  
  *  
  * $Revision: 3 $:  
  * $Author: hans $:  
  * $LastChangedDate: 2013-01-29 10:10:18 +0100 (Tue, 29 Jan 2013) $:  
  *  
  ******************************************/  

 #define DOORBELL_PIN 3  
 #define DOORBELL_INT 1 //only UNO and MEGA2560  

 #define RELAY1_PIN  5  
 #define RELAY2_PIN  6  
 #define RELAY3_PIN  7  
 #define RELAY4_PIN  8  
 #define PIEZO_PIN  9  

 void setup()   
 {  
  Serial.begin(57600);  
  Serial.println("Doorbell application");  
  attachInterrupt(DOORBELL_INT, ring, RISING);  
 }  

 void loop()  
 {  

 }  

 void ring()  
 {  
  Serial.println("Ring ring!");  
 }  

Now when I press the doorbell I can monitor the serial monitor and see something happening!

Screen Shot 2013-02-03 at 10.07.41

During this first test I noticed that sometimes pressing the doorbell switch caused the interrupt routine to be called twice, this is because one may press the switch halfway or it is caused by dust / debris in the switch. The ‘noise’ can be filtered with a small capacitor in parallel with the switch, but I decided to resolve this in software. I came up with the following solution, which also prevents multiple rings when people are impatient. The DOORBELL_TIME define is 5000, which means 5000 milliseconds, so the doorbell is only allowed to ring once every 5 seconds. the interrupt service routine is now called intDoorbell (before it was declared as ring).

update: I also noticed phantom doorbell interrupts, the doorbell would ring when nobody pressed the doorbell switch. It happened when somebody was touching metal in the proximity of the doorbell-switch or when somebody was switching on the light in house. To prevent this I added a delay of 50ms in the interrupt service and read the digital pin again. if it is not high anymore nobody pressed the doorbell and this was just a spike. This is called debounce, here you find an article about it. A hardware solution is on the way.

  #define RELAY_DOORBELL 5    
  #define DOORBELL_TIME 5000   // Minimal 5 seconds between the rings   
  unsigned char handle_ring = 0;   

 /******************************************  
  * interrupt handler when somebody presses  
  * the doorbell switch  
  ******************************************/  
 void intDoorbell()  
 {  
  byte doorbellValue;  
  unsigned long current_time = millis();  
  static unsigned long last_time = 0;  

  delay(50);   

  doorbellValue=digitalRead(DOORBELL_PIN);  

  // test whether the last doorbell press was long enough ago  
  if ((doorbellValue) && (current_time - last_time >= DOORBELL_TIME))  
  {  
    handle_ring = 1;  
    last_time = current_time;  
  }  
 }  

I declared a global variable handle_ring, the variable indicates that somebody pressed the switch, but only when meeting the criteria that a given time have passed since the last time the doorbell was pressed. It is good practice to keep interrupt service routines as short as possible, so business as usual can continue. the handle_ring variable is now handled (and cleared) by the loop() function.

 void loop()  
 {  
  if (handle_ring)  
  {  
   Serial.println("Ring ring!");  
   handle_ring = 0;  

   for (int i=0; i<2; i++)  
   {  
    bitWrite(PORTD,5,1);  
    delay(50);  
    bitWrite(PORTD,5,0);  
    delay(100);  
   }  
  }  
 }  

Although my doorbell rings now for only a split second (300 milliseconds), the project is not finished. Next is to install the Ethernet shield and communicate to my database on my NAS that somebody rang the doorbell. Please continue to follow this blog updates will be made… keep on reading in part 2 (Ethernetshield, logging in SQL and multitasking telnet service implementation)

Oh before i forget, I bought my Arduino hardware at iprototype.nl and floris.cc. The DCF77 receiver I bought via Conrad.  Special thanks to Frans Goddijn for some editorial work!